首先,弄清楚三个相似但是不同的任务:
- feature extraction and feature engineering: 将原始数据转换为特征,以适合建模。
- feature transformation: 对数据的转换以提高算法的精度。
- feature selection: 删除不必要的特征。
1 Feature Extraction
1.1 Text
1.1.1 Bag of Words
最简单的方法是 Bag of Words,首先有一个词典包含了文本中出现的所有的词,每个句子文本的表示就是一个向量,向量的长度等于词典长度,向量每个位置的元素值等于对应单词在句子中出现的次数。注意:单个字母的单词例如 I,a 会直接被忽略
from sklearn.feature_extraction.text import CountVectorizer
text = ['sentence 1','sentence 2']
vect = CountVectorizer(ngram_range=(1,1))
vect.fit_transform(text)
vect.vocabulary_ # 单词词频表
1.1.2 N-grams (the sentence of N consecutive tokens)
使用 Bag of Words的最大的问题在于它忽略了单词在句子中出现的顺序。为了避免这个问题,我们可以使用 N-grams。N-grams在 Bag of Words上做了一点改动:不再以单个 word 来做 tokenization ,而是把相邻的N个 word 看作一个整体来做 tokenization。例如:
In [11]: v = CountVectorizer(ngram_range=(1,2))
In [12]: v.fit_transform(['this is the most thing']).toarray()
Out[12]: array([[1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int64)
In [13]: v.vocabulary_
Out[13]:
{'this': 7,
'is': 0,
'the': 4,
'most': 2,
'thing': 6,
'this is': 8,
'is the': 1,
'the most': 5,
'most thing': 3}
注意:不仅仅可以使用N-grams的单词模式,还可以使用N-grams的字符模式。这种方法可以用来描述相关单词的相似性或者印刷错误的相似性。
from scipy.spatial.distance import euclidean
from sklearn.feature_extraction.text import CountVectorizer
vect = CountVectorizer(ngram_range=(3,3), analyzer='char_wb')
n1, n2, n3, n4 = vect.fit_transform(['andersen', 'petersen', 'petrov', 'smith']).toarray()
euclidean(n1, n2), euclidean(n2, n3), euclidean(n3, n4)
1.1.3 TF-IDF
tf-idf的简明解释参考博客
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf2 = TfidfVectorizer()
re = tfidf2.fit_transform(corpus)
print (re)
1.1.4 Word2Vec
上面的三种方法可以作为简单问题的解决方法,也可以作为参考的 baseline。更新更流行的处理方法是 **Word2Vec,GolVe,Fasttext ** 。
1.2 Images
处理图像既是简单的有时困难的。说它简单是因为我们可以仅仅只用流行的预训练的模型网络而不考虑其他的什么问题。说它难是因为如果你需要深入挖掘细节,那些你可能要下很大的功夫。
在GPU并不强大并且神经网络的复兴还没有发生的时期,feature generation from images 是一个复杂的问题。人们必须做底层的工作:确定边角,区域的边界,颜色的分布统计等等。有经验计算机视觉专家要在旧的的方法里做很多类似神经网络要做的工作。例如,今天神经网络中的卷积层就和Haar cascades很类似。如果你有兴趣了解更多,这里有一些有趣的库的网址:skimage and SimpleCV。
对于图像的相关问题,常用的方法是卷积神经网络。你不需要从零开始构想并训练一个网络,你可以从公共来源下载预训练好的领域内最先进的网络模型。数据科学加常常进行所谓的 fine-tuning 的工作来将这些网络移植到他们的任务中(主要是通过移除网络最后几层全连接层,然后对于特定任务添加新的网络层,然后在新的数据上训练网络。
不过,我们不应该太过于关注神经网络的技巧。通过手工产生的特征仍然是非常有效的。
对于图片上的文字,也可以不使用复杂的神经网络,pytesseract 是一个不错的库。
另一个神经网络无法提供帮助的是从 meta-information中提取特征。例如,EXIF 存储了许多有用的元信息:制造商和相机型号,分辨率,使用闪光灯,拍摄地理坐标,用于处理图片的软件等等。
1.3 Geospatial data
Geographic data 有关的问题不是很常见,但是掌握处理它的基本技巧是有帮助的,并且这个领域有很多现成的解决方法。
Geospatial data 常以经纬度位置坐标的形式给出。根据任务的不同,你可能需要两种互相转换的操作:geocoding (将地址编码成一个坐标点)和 reverse geocoding (把一个坐标点解析成地址)。这两种操作可以通过外部的 API 来完成,例如 Google Maps 或者 OpenStreetMap。不同的服务商有不同的特性,幸运的是,有一个通过的库 geopy 包装了这些外部服务。
如果你有大量的数据,你将很快达到外部API的访问限制。另外通过HTTP来获取数据的方式也不算快捷。因此有必要考虑使用 OpenStreetMap 的本地版本。
如果你有少量的数据,并且有足够的时间,不考虑提取 fancy features,你可以使用 reverse_geocoder
代替 OpenStreetMap:
import reverse_geocoder as revgc
revgc.search((df.latitude, df.longitude))
Loading formatted geocoded file...
Out: [OrderedDict([('lat', '40.74482'),
('lon', '-73.94875'),
('name', 'Long Island City'),
('admin1', 'New York'),
('admin2', 'Queens County'),
('cc', 'US')])]
在处理 geocoding 时,我们必须记住地址中可能包含打字错误,这使得数据清洗变得很重要。坐标包含更少的印刷错误,但是GPS噪声或者是地区不准确的信号可能会得出不正确的位置。如果数据来源是手机,地理位置可能是由WiFi网络确定的而不是GPS确定的,这可能造成地区空洞。
1.4 Date and time
对于日期常见的特征有weekday,dayofweek,dayofmonth等等。
对于小时的处理并不像看起来的那么简单。如果你把小时作为实数对待,我们可能会违反数据的本质:0 < 23 但是在同一天 23:00 < 0:00,对于一些问题这可能是很重要的。另一方面,如果你把它们编码成类别变量,你就会产生许多的特征并且会损失一些信息:22和23之间的差异会被认为与22和7之间的差异是相同的。
这儿有一个内行才知道的处理办法,那就是将时间数据投影到一个使用两个坐标的圆上(用弧度表示时间)。
def make_harmonic_features(value, period=24):
value *= 2 * np.pi / period
return np.cos(value), np.sin(value)
这种转换保留了数据点之间的距离信息,这对于估计距离的算法(kNN,SVM,k-means等)是很重要的。
from scipy.spatial import distance
euclidean(make_harmonic_features(23), make_harmonic_features(1))
# output 0.5176380902050424
euclidean(make_harmonic_features(9), make_harmonic_features(11))
# output 0.5176380902050414
euclidean(make_harmonic_features(9), make_harmonic_features(21))
# output 2.0
1.5 Time series, web, etc.
关于时间序列方面,我不会讲太多细节(主要是因为我自己缺乏这方面的经验),但是我告诉你一个自动生成时间序列特征的库 tsfresh
如果你处理的是web data, 那么通常会有用户的 User Agent的信息。这是一个很有用的信息。第一,你可以从中提取操作系统;第二,创造一个特征 is_mobile
;第三,查看浏览器。
# Install pyyaml ua-parser user-agents
import user_agents
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/56.0.2924.76 Chrome/56.0.2924.76 Safari/537.36'
ua = user_agents.parse(ua)
print('Is a bot? ', ua.is_bot)
print('Is mobile? ', ua.is_mobile)
print('Is PC? ',ua.is_pc)
print('OS Family: ',ua.os.family)
print('OS Version: ',ua.os.version)
print('Browser Family: ',ua.browser.family)
print('Browser Version: ',ua.browser.version)
2 Feature transformations
2.1 Normalization and changing distribution
单调的特征转换对于一些算法是重要的。这是决策树算法及其衍生算法(随机森林,梯度提升)流行的一个原因。并不是每个人都可以或是想要做特征转换,决策树类型的算法对于不寻常的分布是鲁棒的。
sklearn.preprocessimg.StandardScaler 标准化
sklearn.preprocessing.MinMaxScaler 归一化
StandardScaling 和 MinMax Scaling有着相似的应用并且或多或少是可以互换的。然而,如果算法涉及点或者向量之间的距离,默认的选择是 StandardScaling。但是 MinMax Scaling 对于把特征变换到 (0, 255) 之间的可视化方法是有用的。
如果我们假设有些数据不是正态分布的但是可以可以用 log-normal distribution来描述,它就可以很容易地转换成正态分布。
from sklearn.preprocessing import StandardScaler
from scipy.stats import beta
from scipy.stats import shapiro
import numpy as np
from scipy.stats import lognorm
data = lognorm(s=1).rvs(1000)
shapiro(data) # return test-statistics, p-value
# output: (0.5831204056739807, 1.3032075718220799e-43)
shapiro(np.log(data))
# output: (0.9991741180419922, 0.9468745589256287)
对数正态分布适用于描述工资,证券价格,城市人口,互联网上的文章评论数量等。然而,为了应用这个处理方法,数据的分布不必要求必须是 lognormal;你可以把这种转换方法应用到任意右长尾分布的数据上。你还可以尝试其他的变换:Box-Cox transformation (logarithm 是 Box-Cox transformation的特殊情况),Yeo-Johnson transformation(扩展应用范围到负数)。另外,你也可以尝试在特征上加一个常数——np.log(x + const)
在上面的例子中,我们在合成的数据上使用Shapiro-Wilk test 检验分布的正态性。我们也可以使用一个非正式检验正态性的方法——Q-Q plot 。对于正态分布,它看起来会像是一条平滑的对角线。
# Let's draw plots!
import statsmodels.api as sm
# Let's take the price feature from Renthop dataset and filter by hands the most extreme values for clarity
price = df.price[(df.price <= 20000) & (df.price > 500)]
price_log = np.log(price)
# A lot of gestures so that sklearn didn't shower us with warnings
price_mm = MinMaxScaler().fit_transform(price.values.reshape(-1, 1).astype(np.float64)).flatten()
price_z = StandardScaler().fit_transform(price.values.reshape(-1, 1).astype(np.float64)).flatten()
sm.qqplot(price, loc=price.mean(), scale=price.std())
sm.qqplot(price_z, loc=price_z.mean(), scale=price_z.std())
Q-Q plot after StandardScaler. Shape doesn’t change.
Q-Q plot after MinMaxScaler. Shape doesn’t change.
Q-Q plot after taking the logarithm. Things are getting better!
2.2 Interactions
并不是所有的 interactions between features 是由物理意义的,例如,polynomial features (see sklearn.preprocessing.PolynomialFeatures)在线性模型中用得较多,但是几乎是不可能解释它的物理意思。
处理缺失值的几种方法:
- 对于类别变量编码成另外的一个类别,或者用最多的类别填充。
- 对于数值变量用统计量填充。
- 编码成极端值(这对于决策树模型是有帮助的,因为它允许模型在缺失值和非缺失值之间做一个划分。)
- 对于有序数据(如时间序列),取临近值,前面的或是后面的。
3 Feature selection
做特征选择的原因至少有两个:
- 降低数据维度,减少计算复杂性。
- non-informative features 对于有些模型来说相当于 noise,如果模型拟合了这些噪声,就可能产生过拟合。
3.1 Statistical approaches
最明显的需要删除的特征是那些取值保持不变的特征,这些特征几乎不包含任何信息。根据这个思想,可以认为高方差的特征比起低方差的特征是更好的。
from sklearn.feature_selection import VarianceThreshold
from sklearn.datasets import make_classification
x_data_generated, y_data_generated = make_classification()
x_data_generated.shape
VarianceThreshold(.7).fit_transform(x_data_generated).shape
VarianceThreshold(.8).fit_transform(x_data_generated).shape
VarianceThreshold(.9).fit_transform(x_data_generated).shape
There are other ways that are also based on classical statistics.
3.2 Selection by modeling
两一个方法就是使用 baseine 模型来评估特征。两种模型是经常使用的:随机森林和Lasso。这里的逻辑是很直观的,如果特征对于这些 baseline 模型明显是无用的,那么也就不需要把它们加到更加复杂的模型中了。
永远牢记的是this is not a silver bulet—— 特征选择后可能会使模型的性能下降。
3.3 Grid search
最后,我们介绍最可靠的方法,也是计算复杂度最高的方法:trivial grid search 。在所有的特征子集上训练模型,然后比较不同模型的性能从而确定最佳特征集合。这种方法也被叫做 Exhaustive Feature Selection 。
搜索所有的组合通常需要很长时间,所以你可以尝试减少搜索的空间。固定一个较小的数N,重复所有N种特征的组合,选择最好的组合,然后重复N+1种特征的组合(这时前面最优特征的组合是固定的并且只有一个新的特征加入进去)。不断迭代直到我们到达最大的特征数目或者直到模型的性能不再显著的增加。这种算法被称作 Sequential Feature Selection 。
这种算法也可以反转过来:从全部的特征空间开始然后一次删除一个特征直到会削弱模型的性能。
# Install mlxtend
from mlxtend.feature_selection import SequentialFeatureSelector
selector = SequentialFeatureSelector(LogisticRegression(), scoring='neg_log_loss',
verbose=2, k_features=3, forward=False, n_jobs=-1)
selector.fit(x_data, y_data)
Take a look how this approach was done in one simple yet elegant Kaggle kernel.