Feature Engineering (Transform)

近期推荐模型也做了一些,小伙伴们也尝试了各种论文的模型,诸如deepFM,NCF,DCN等等,但效果都没有明显的提升,于是也想着是不是从数据集特征入手,重新梳理遍特征工程相关的东西,同时我也在思考,到底模型接受什么样的特征是好的,尤其是对于连续数值型特征,本文算是我对特征工程部分方面记录与总结。

Feature Engineering

特征工程是我们在处理机器学习问题首先要面对的问题,无论是传统的机器学习还是深度学习,我们都需要将特征处理成一个合适的形式输入到训练模型中,这其实也是我们将先验的知识带入的过程,取得更有效更直观的数据,省去模型对于一部分数据特征的学习。有一句话受到了广泛的认可,就是数据和特征决定了机器学习的上限,而算法只是尽可能逼近这个上限。

Feature engineering is the process of transforming raw data into features that better represent the underlying problem to the predictive models, resulting in improved model accuracy on unseen data.

                                                                    — Dr. Jason Brownlee

standard machine learning pipeline

所以数据到底该怎么处理,处理到什么样是一个好的特征呢,这个我们后续会一一讨论(虽然我也理解不透,哈哈😆)

Features一般可以分为两大类,Raw features 和 Derived features.Derived features一般就是通过特征工程产生的,比如说数据集里只有用户的出生日期,那我们可以根据生日得到年龄这一Derived feature,这也就是最基本的特征工程。

原始的数据集中会有各式各样的特征,我们先聊下最常见的特征,基本上就是连续数值(numeric)特征和泛类别(categorical)特征(像文字这种我也算入在内,当然文字还是比较特别的,早先是n-gram这种,现在都是类似word2vec的玩法,推荐里边也有item2vec这种弄法,不过这次不聊这块)

Numeric Data

数值特征可以说是最直观的特征了,许多数据集中都会看到数值特征,房价中的房屋面积,人均收入等等。数值特征的优势是一般其都可以直接作为输入进行数学运算。不过我们的目的显然不是只是为了让数据能变为数值参与计算就行,而是为了让模型能根据特征去更好的学习目标。所以,数值特征也需要对其进行进一步的特征工程。
要想做好特征工程,我们首先应该了解数据。一般要先看一下数据分布,均值,标准差和分位数等数据。

1.Binarization

有一些数据中次数可能不重要,比如说用户看电影,看了一遍和两遍在某些用户喜好场景下可以忽略,那么其实就是只要次数>= 1那么就转化为1,没看过转化为0

2.Rounding

有一些数据,比如百分比,在一些情况下精度可能不是那么重要,其实就可以约到整数位 如99.34% => 99%, 其可以就此作为数值输入,也可以转换为分类特征

3.Interactions

比如有特征宽高,相乘可获取到面积。但许多传统模型没有组合能力,这就需要我们手动的去做。由之前的y=wx变成y = w1x1 + w2x2 + w12x1x2 … 当然有些模型本身会自己做特征组合,

4.Binning

分桶,最常见的就是对年龄的处理,tf中有封装的函数 tf.feature_column.bucketized_column,其将数据按界限分隔为几个类别然后做one-hot处理。这种属于固定式的分桶,更进一步,我们可以根据数据自适应的去划分,比如n-Quantiles ,将数据划分到n个等宽的桶中,这样就由连续数值特征转变为了类别特征,再进行训练。

二值化和分桶甚至于进行约数其实都相当于是将数值特征转为类别特征,然后变成one-hot形式.

5.Transform

当特征为有意义的连续数对这种特征,一般需要进行变化

min-max
z-score
Log
Square root
1/x
Power transform (Box–Cox transformation、Yeo–Johnson transformation)
quantile

有很多数据有很强的前后大小关系,做成离散高维的会丢失很多信息。比如用户对视频、直播的观看时长,消费这种数据,有强烈的大小意义。对这种特征,最基础的是归一化,是min-max将其转为[0,1]区间,统一转化到0-1区间会提升学习速度,这个不具体转开了,不了解的同学可以google下。
但是现实中如果画一个分布图,会发现观看时长这类数据的曲线非常左倾(Left Skewed),直接min-max后会发现,大部分的特征值可能都在0.01以内。
我们这时候可以套入常规的wx+b中,会发现这个特征大部分值太小了,导致在整个模型中基本没起作用。
粗暴的处理方法是直接将少部分特别大的值砍掉,超过给定max的都归一化到1.
这种方案对于有些数据还是不够work,大部分数据还是会积压在一块,除非砍掉特别多的大数据,但这又会丢掉不少信息。
于是对于skewed数据,我们可以选用log函数进行变换处理,他可以把聚集的小数拉宽,把稀疏的大数缩短,在很多情况下一个合适的底数都能将这种倾斜数据转为类似正太分布。
这种方案能解决一部分问题,还有些比较少见但好像很强的方法 ,Power transform
包括Box–Cox transformation、Yeo–Johnson transformation
以Box-Cox为例
对原始数据进行极大似然估计,找出最大可能的lmbda值,对所有数据进行变换,将数据转换为类似正太分布的形式。

拿我们自己的真实数据举例子,下图展现的是原始分布,已经经过变换后的分布

我们可以看到原始数据十分倾斜,绝大部分数据归一化到[0,1]区间后,其值会十分接近0,没有区分度,这样的特征也就很难在模型中起到作用
在线上对部分特征做了box-cox的对比,发现log通常会好一些。

这里也就是我想要强调的地方,优先选择直观可解释的变换,比如X是单位面积的房价,取了倒数后就变成了,一元能买多少平,log,exp也是类似,可解释性要好一些。Box-Cox不是银弹,它的可解释性要差很多,它对于寻找一个对应的变换是有用的,但是其最优指并不应该盲目去使用它,可以看一下置信区间,如果log或平方根能解决,那么优先使用这两种转换方式。

不要随意使用这种强力变换,除非确定特征符合某种非线性特征,否则可能会引入更多的非线性,导致更难拟合

这里边再补个demo代码,这是我用来测神经网络拟合log函数的,你会发现不同的网络层数、神经元数目和激活函数对应学习效果上的巨大影响。

Fortunately, feedforward networks with hidden layers provide a universal approxi-mation framework. Specifically, theuniversal approximation theorem(Horniket al., 1989; Cybenko, 1989) states that a feedforward network with a linear outputlayer and at least one hidden layer with any “squashing” activation function (suchas the logistic sigmoid activation function) can approximate any Borel measurablefunction from one finite-dimensional space to another with any desired nonzeroamount of error, provided that the network is given enough hidden units.

理论上ANN可以不做特征变换和选择,模型能自己处理,不过我还是觉得如果已经有先验经验的情况下,还是自己做一些比如log变换,然后归一化的会比直接归一化让模型自己去学习log这种变换方式要更好些。
另外,ftp://ftp.sas.com/pub/neural/FAQ2.html#A_std 上说将特征归一化到[-1,1]之间会更好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

# 生成1000个数据集, y = sum(log(x)) + random
data_X = np.array([])
data_Y = np.array([])
for i in range(6000):
#data_x = np.random.uniform(1,1000,10)
data_x = np.random.random(1)*3 + 1
if i == 0:
print(data_x)
data_y = sum(data_x**3)
#data_y = sum( 2 * data_x + 4)
data_X = np.append(data_X, data_x)
data_Y = np.append(data_Y, data_y)

data_X = data_X.reshape(6000,-1)


X_train, X_valid = data_X[:5000], data_X[5000:]
y_train, y_valid = data_Y[:5000], data_Y[5000:]

n_inputs = 1
X = tf.placeholder(tf.float32, shape=(None, n_inputs), name="X")
y = tf.placeholder(tf.float32, shape=(None), name="y")

he_init = tf.variance_scaling_initializer()

with tf.name_scope("dnn"):
he_inithe_init = tf.variance_scaling_initializer()
hidden1 = tf.layers.dense(X, 50, activation=tf.nn.elu, kernel_initializer=he_init, name="hidden1")
hidden2 = tf.layers.dense(hidden1, 20, activation=tf.nn.elu, kernel_initializer=he_init, name="hidden2")
# hidden3 = tf.layers.dense(hidden2, 50, activation=tf.nn.relu, kernel_initializer=he_init, name="hidden3")
logits = tf.layers.dense(hidden2, 1, name="outputs")

with tf.name_scope("loss"):
loss = tf.losses.mean_squared_error(y, logits)


learning_rate = 0.001
with tf.name_scope("train"):
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
training_op = optimizer.minimize(loss)

init = tf.global_variables_initializer()
saver = tf.train.Saver()

n_epochs = 10
batch_size = 1


def shuffle_batch(X, y, batch_size):
rnd_idx = np.random.permutation(len(X))
n_batches = len(X) // batch_size
for batch_idx in np.array_split(rnd_idx, n_batches):
X_batch, y_batch = X[batch_idx], y[batch_idx]
yield X_batch, y_batch

with tf.Session() as sess:
init.run()
for epoch in range(n_epochs):
for X_batch, y_batch in shuffle_batch(X_train, y_train, batch_size):
#print(X_batch, y_batch)
sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
if epoch % 5 == 0:
train_loss = sess.run(loss, feed_dict={X: X_batch, y: y_batch})
valid_loss = sess.run(loss, feed_dict={X: X_valid[:200], y: y_valid[:200]})
print(epoch, "Batch loss:", train_loss, "Validation loss:", valid_loss/200)

new_X = np.sort(X_valid[200:300].reshape(-1)).reshape(100,1)
pred = sess.run(logits, feed_dict={X:new_X})

plt.plot(np.sort(X_valid[200:300].reshape(-1)), pred)
plt.grid(color="k", linestyle=":")
plt.plot(np.sort(X_valid[200:300].reshape(-1)), np.sort(y_valid[200:300]))
plt.show()

在简单说一下类别特征

Categorical Data

Nominal categorical

直播的类别,游戏,户外,音乐,跳舞等等

Ordinal categorical

衣服的尺码,S M L XL XXL…

One-hot Encoding

100 010 001

Dummy Coding

00 01 10

Effect Coding

-1-1 01 10

Hash

Embeding

Dummy和One-hot其实比较接近,不过相较于One-hot是少一维的,知乎王赟的回答说的清晰些,如果你不使用regularization,那么one-hot encoding的模型会有多余的自由度。这个自由度体现在你可以把某一个分类型变量各个值对应的权重都增加某一数值,同时把另一个分类型变量各个值对应的权重都减小某一数值,而模型不变。在dummy encoding中,这些多余的自由度都被统摄到intercept里去了。这么看来,dummy encoding更好一些。
如果你使用regularization,那么regularization就能够处理这些多余的自由度。此时,我觉得用one-hot encoding更好,因为每个分类型变量的各个值的地位就是对等的了。
以线性模型举例, 分类超平面是 wx+b =0,dummy下的话 w 有唯一解,one-hot 下 w 有无穷解 (就是上边所说的一部分权重增加点,另一部分权重减少点),这样每个变量的权重就没有解释意义了,也使得模型没有真正的预测效果。加了正则化之后,相当于约束了 w 的解空间,使得 w 相对有意义

Reference

ftp://ftp.sas.com/pub/neural/FAQ.html
ftp://ftp.sas.com/pub/neural/FAQ2.html#A_std
http://www.ijcte.org/papers/288-L052.pdf
https://towardsdatascience.com/understanding-feature-engineering-part-1-continuous-numeric-data-da4e47099a7b
https://towardsdatascience.com/understanding-feature-engineering-part-2-categorical-data-f54324193e63
https://www.zhihu.com/question/48674426/answer/112633127