机器学习开放课程(六):特征工程和特征选取

2018 年 5 月 29 日 论智
作者:Arseny Kravchenko
编译:weakish

编者按:机器学习开放课程第六课,AR和计算机视觉初创公司WANNABY研发部门主管Arseny Kravchenko讲解特征工程、特征转换、特征选取这三项类似而不同的任务。

在本课程中,我们已经了解了一些关键的机器学习算法。然而,在我们进一步了解更炫的算法之前,我们将小小地绕行一下,讨论一下数据预处理。著名的“垃圾进——垃圾出”概念100%适用于机器学习的任何任务。在高质量数据上训练的简单模型,表现优于在未清理的数据上训练的复杂多模型集成,任何有经验的专业人士都多次遇到这类情况。

我想检查三项类似而不同的任务:

  • 特征提取特征工程 将原始数据转换为适用于模型的特征

  • 特征转换 转换数据以提高算法的精确度

  • 特征选取 移除不必要的特征

本文基本不涉及数学,不过会有不少代码。一些示例用到了Renthop公司公开的数据集,用于预测新出租房屋的受欢迎程度。数据集可通过Kaggle下载:Two Sigma Connect: Rental Listing Inquiries。

  
  
    
  1. import json

  2. import pandas as pd

  3. # 加载数据集

  4. with open('train.json', 'r') as raw_data:

  5.    data = json.load(raw_data)

  6.    df = pd.DataFrame(data)

概览

  1. 特征提取

    • 文本

    • 图像

    • 地理空间数据

    • 日期和时间

    • 时序,web,等等

  2. 特征转换

    • 正则化和改变分布

    • 相互作用

    • 填充缺失值

  3. 特征选取

    • 统计学方法

    • 建模选取

    • 网格搜索

1. 特征提取

在实践中,数据极少是可以直接使用的矩阵格式的。这就是为什么几乎每项任务都从特征提取开始的原因。能够直接读取csv文件并转换为numpy.array,也是比较罕见的例外。让我们看看一些常见的数据类型中特征提取是如何进行的。

1.1 文本

文本类型的数据可能有不同的格式;一篇文章讲不完如此多的文本处理方法。不管怎么说,我们将查看下最流行的那些。

在处理文本之前,必须tokenzie文本,也就是将文本切分为单元(token)。在最简单的情形下,token不过是单词。但直接按单词切分可能会损失一些信息——“Santa Barbara”是一个token,而不是两个,“rock'n'roll”不应该被切分为两个token。现成的tokenizer会考虑语言的特性,但也会出错,特别是当你处理特定来源的文本时(报纸、俚语、误拼、笔误)。

接下来需要正则化数据。对文本而言,这涉及词干提取(stemming)和词形还原(lemmatization);这是用来处理同一单词的不同形式的类似过程。两者的区别可以参考《Introduction to Information Retrieval》一书的Stemming and lemmatization一节。

当我们将文档转换为单词序列之后,我们可以用向量表示它。最简单的方法是词袋(Bag of Words):我们创建一个长度等于字典的向量,计算每个单词出现在文本中的次数,然后将次数放入向量中对应的位置。用代码来表述更为简单明了:

  
  
    
  1. from functools import reduce

  2. import numpy as np

  3. texts = [['i', 'have', 'a', 'cat'],

  4.        ['he', 'have', 'a', 'dog'],

  5.        ['he', 'and', 'i', 'have', 'a', 'cat', 'and', 'a', 'dog']]

  6. dictionary = list(enumerate(set(list(reduce(lambda x, y: x + y, texts)))))

  7. def vectorize(text):

  8.    vector = np.zeros(len(dictionary))

  9.    for i, word in dictionary:

  10.        num = 0

  11.        for w in text:

  12.            if w == word:

  13.                num += 1

  14.        if num:

  15.            vector[i] = num

  16.    return vector

  17. for t in texts:

  18.    print(vectorize(t))

结果:

  
  
    
  1. [0. 1. 0. 1. 1. 0. 1.]

  2. [0. 1. 1. 0. 1. 1. 0.]

  3. [2. 1. 1. 1. 2. 1. 1.]

下面是这一过程的示意图:

这是一个极端幼稚的实现。在实践中,我们将需要考虑停止词,字典的最大长度,更高效的数据结构(通常将文本数据转换为稀疏向量),等等。

当使用类似词袋的算法时,我们丢失了文本中的单词顺序信息,这意味着向量化之后,“i have no cows”(我没有牛)和“no, i have cows”(没,我有牛)会变得一样,尽管事实上它们的意思截然相反。为了避免这个问题,我们可以转用N元语法。

  
  
    
  1. from sklearn.feature_extraction.text import CountVectorizer

  2. vect = CountVectorizer(ngram_range=(1,1))

  3. vect.fit_transform(['no i have cows', 'i have no cows']).toarray()

输出:

  
  
    
  1. array([[1, 1, 1],

  2.      [1, 1, 1]], dtype=int64)

  
  
    
  1. vect.vocabulary_

输出:

  
  
    
  1. {'cows': 0, 'have': 1, 'no': 2}

  
  
    
  1. vect = CountVectorizer(ngram_range=(1,2))

  2. vect.fit_transform(['no i have cows', 'i have no cows']).toarray()

输出:

  
  
    
  1. array([[1, 1, 1, 0, 1, 0, 1],

  2.      [1, 1, 0, 1, 1, 1, 0]], dtype=int64)

  
  
    
  1. vect.vocabulary_

输出:

  
  
    
  1. {'cows': 0,

  2.      'have': 1,

  3.      'have cows': 2,

  4.      'have no': 3,

  5.      'no': 4,

  6.      'no cows': 5,

  7.      'no have': 6}

另外,我们并不是非得用单词。在某些情形下,有可能需要生成字符的N元语法,以考虑相关单词的相似性或笔误。

  
  
    
  1. from scipy.spatial.distance import euclidean

  2. from sklearn.feature_extraction.text import CountVectorizer

  3. vect = CountVectorizer(ngram_range=(3,3), analyzer='char_wb')

  4. n1, n2, n3, n4 = vect.fit_transform(['andersen', 'petersen', 'petrov', 'smith']).toarray()

  5. euclidean(n1, n2), euclidean(n2, n3), euclidean(n3, n4)

  6. # (2.8284271247461903, 3.1622776601683795, 3.3166247903554)

在词袋的想法上加上一点:语料(数据集的全部文档)中罕见但在当前文档中出现的单词可能更重要。因此,增加领域特定单词的权重,以将它们和常用词区分开了,是很合理的。这一方法称为TF-IDF(词频-逆向文档频率)。默认选项为:

在文本问题之外,也可以看到和词袋类似的方法,比如,Catch Me If You Can竞赛中的“网站袋”,“应用袋”、“事件袋”,等等。

使用这些算法,在简单问题上可能得到不错的效果,可以作为基线。然而,对不喜欢古典的人而言,可以使用新方法——最流行的是Word2Vec,除此之外还有GloVe、Fasttext等。

Word2Vec是词嵌入算法的一个特殊情形。使用Word2Vec和类似的模型,我们不仅可以向量化高维空间(通常是成百上千维的空间)中的单词,还能比较它们的语义相似度。下图演示了一个经典例子:king(王)- man(男)+ woman(女) = queen(后)

值得注意的是,这一模型并不理解单词的意思,只是尝试将用于相同的上下文的单词向量放在相近的位置。如果不考虑这点,将得到许多可笑的结果。

这样的模型需要在非常大的数据集上训练,使向量坐标能够捕捉语义。你也可以在GitHub的3Top/word2vec-api仓库下载预训练的模型。

类似的方法也用于其他领域(比如生物信息学)。一个出人意料的应用是food2vec(食物向量)。你多半可以想到一些新的应用;这个概念足够通用。

1.2 图像

处理图像既更简单又更复杂。更简单是因为可能直接使用某个流行的预训练网络,而不用思考太多;更复杂是因为,如果你需要深入细节,你最终可能需要非常深入。让我们从头开始。

在GPU不够强,“神经网络复兴”尚未发生的时期,图像的特征生成本身是一个非常复杂的领域。我们需要在较低层级工作,检测角点,区域边界,色彩分布统计,等等。富于经验的计算机视觉专家可以在神经网络和古老方法间发现很多相通之处;特别是,今天常用的卷积网络层和哈尔级联很像。如果你对这些经典方法感兴趣,可以看下skimage和SimpleCV这两个库。

和图像相关的问题,常常使用卷积神经网络。你不需要从头设计网络架构,以及从头训练网络。相反,可以下载一个预训练好的当前最先进的网络,其权重基于公开数据训练。数据科学家经常“分离”网络的最后一个全连接层,增加针对特定任务的新层,接着在新数据上训练网络。这一让预训练网络适应其需求的工作称为微调(fine-tuning)。如果任务仅仅是向量化图像(比如使用非神经网络的分类器),那么只需移除最后一层,使用前一层的输出:

  
  
    
  1. from keras.applications.resnet50 import ResNet50, preprocess_input

  2. from keras.preprocessing import image

  3. from scipy.misc import face

  4. import numpy as np

  5. resnet_settings = {'include_top': False, 'weights': 'imagenet'}

  6. resnet = ResNet50(**resnet_settings)

  7. img = image.array_to_img(face())

  8. img

多可爱的浣熊!

在实际项目中,可能需要花更多心思调整尺寸。

  
  
    
  1. img = img.resize((224, 224))

需要一个额外的维度,以适配模型设计的输入格式——张量形状(batch_size, width, height, n_channels)

  
  
    
  1. x = image.img_to_array(img)

  2. x = np.expand_dims(x, axis=0)

  3. x = preprocess_input(x)

  4. features = resnet.predict(x)

这是一个分离最后一层并加入新层的例子

不管怎么说,我们不应该过于关注神经网络技术。手工生成的特征仍然是非常有用的:例如,为了预测出租房屋的受欢迎程度,我们可以假定更敞亮的公寓吸引更多注意,因而创建一个类似“像素均值”的特征。你可以在Pillow库的文档中找到一些富有启发性的例子。

如果图像上有文本,无需使用复杂的神经网络就可以识别。比如,使用pytesseract。

  
  
    
  1. import pytesseract

  2. from PIL import Image

  3. import requests

  4. from io import BytesIO

  5. # 随机搜索到的一张图片

  6. img = 'http://ohscurrent.org/wp-content/uploads/2015/09/domus-01-google.jpg'

  7. img = requests.get(img)

  8. img = Image.open(BytesIO(img.content))

  9. img

  
  
    
  1. text = pytesseract.image_to_string(img)

  2. text

结果:

  
  
    
  1. 'Google'

当然,pytesseract不是包治百病的灵丹妙药。比如,下面一张源自Renthop的图片:

  
  
    
  1. img = requests.get('https://photos.renthop.com/2/8393298_6acaf11f030217d05f3a5604b9a2f70f.jpg')

  2. img = Image.open(BytesIO(img.content))

  3. img

  
  
    
  1. pytesseract.image_to_string(img)

结果:

  
  
    
  1. 'Cunveztible to 4}»'

另一个神经网络无济于事的情形是从元信息中提取特征。就图像而言,EXIF储存了许多有用的元信息:相机制造商、相机型号、分辨率、是否使用闪关灯、拍摄时的地理坐标、用来处理图像的软件,等等。

1.3 地理空间数据

地理空间数据相对而言不像文本和图像那么常见,但掌握处理地理空间数据的基本技术仍然是有用的,特别是这一领域中有很多相当成熟的解决方案。

地理空间数据常常表示为地址或坐标(经纬度)的形式。取决于手头的任务,你可能需要两种互逆的操作:地理编码(由地址重建坐标点)和逆地理编码(由坐标点重建地址)。在实际项目中,这两个操作都可以通过访问外部API(谷歌地图或OpenStreetMap)进行。不同的地理编码器各有其特性,不同地区的编码质量也不一样。很幸运,geopy之类的通用库封装了这些外部服务。

如果你有大量数据,你很快会达到外部API的限制。此外,从HTTP获取信息并不总是最快的方案。因此,有必要考虑使用本地版的OpenStreetMap。

处理地理编码时,别忘了地址可能包含错误,因此需要清洗数据。坐标的错误更少,但由于GPS噪声或特定地点(比如隧道、商业区)的低精确度,可能导致位置不正确。如果数据源是移动设备,地理位置可能并不是由GPS决定的,而是由该区域的WiFi网络决定的,这会导致空间中的空洞和远距离传送。当你经过曼哈顿时,可能会突然碰到芝加哥的WiFi地点。

WiFi地点追踪基于SSID和MAC地址的组合,这些可能对应不同的地点,例如,联合供应商标准化MAC地址路由,并将其投放于不同城市。甚至一家公司带着路由器搬迁到另一个办公地点都可能造成问题。

地点常常位于许多基础设施之间。因此,你可以充分发挥想象力,基于生活经验和领域知识发明特征:地点到地铁口的接近程度,建筑物中的商户数,到最近的商店的距离,周边的ATM数目,等等。就任何任务而言,你很容易想到几十个特征并从不同的外部资源中提取它们。就城市以外的问题而言,你可以考虑来自更专门的数据源的特征,比如海拔。

如果两个以上的地名相互连接,可能有必要基于地点之间的路由提取特征。比如,距离(直线距离和基于路由图计算得出的道路距离),转弯数(包括左转和右转的比例),红路灯数,交叉路口数,桥梁数。在我自己遇到的一个任务中,我生成了一个称为“道路复杂度”的特征,该特征计算基于图得出的距离除以最大公因数。

1.4 日期和时间

你可能认为日期和时间是标准化的,因为它们是如此普遍,不过,其中仍然有一些坑。

让我们从星期几开始。这可以通过one-hot 编码很容易地转为7个伪变量。此外,我们可以为周末创建一个单独的二元特征is_weekend。

  
  
    
  1. df['dow'] = df['created'].apply(lambda x: x.date().weekday())

  2. df['is_weekend'] = df['created'].apply(lambda x: 1 if x.date().weekday() in (5, 6) else 0)

有些任务可能需要额外的日历特征。比如,现金提取可能与账单日相关联;地铁月卡的购买可能和每月开始相关联。一般而言,处理时序数据时,最好有一份包含公众节假日、异常天气情况及其他重要事件的日历。

问:春节、纽约马拉松赛、川普就职日有何共同点?

答:它们都需要记在潜在反常值的日历上。

处理小时和分钟不像看起来那么简单。如果你将小时作为实数变量,那么0 < 230:00:00 02.01 > 01.01 23:00:00。在有些问题上,这可能很关键。同时,如果你将它们编码为类别变量,你将生成大量特征,同时损失接近程度的信息——22和23之间的差别与22和7的差别一样。

存在一些更晦涩的处理这些数据的方法,比如将时间投影到圆上,然后使用两个坐标。

  
  
    
  1. def make_harmonic_features(value, period=24):

  2.    value *= 2 * np.pi / period

  3.    return np.cos(value), np.sin(value)

这一转换保留了时间点间的距离,对于需要估计距离的算法(kNN、SVM、k-均值等),这很重要。

  
  
    
  1. from scipy.spatial import distance

  2. euclidean(make_harmonic_features(23), make_harmonic_features(1))

输出:

  
  
    
  1. 0.5176380902050424

1.5 时序,web,等等

我们不会在这里介绍太多关于时序数据的细节(主要是因为我个人在这方面经验不多),我建议你看下tsfresh这个库,它可以自动基于时序数据生成特征。

如果你处理web数据,你通常具备用户的User Agent信息。这是一个信息的宝藏。首先,你可以从中提取操作系统信息。其次,你可以据此创建is_mobile(是否是移动端)信息。最后,你可以查看下浏览器类别。

首先,安装所需库pyyamlua-parseruser-agents

  
  
    
  1. import user_agents

  2. 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'

  3. ua = user_agents.parse(ua)

  4. print('Is a bot? ', ua.is_bot)

  5. print('Is mobile? ', ua.is_mobile)

  6. print('Is PC? ',ua.is_pc)

  7. print('OS Family: ',ua.os.family)

  8. print('OS Version: ',ua.os.version)

  9. print('Browser Family: ',ua.browser.family)

  10. print('Browser Version: ',ua.browser.version)

输出:

  
  
    
  1. Is a bot?  False

  2. Is mobile?  False

  3. Is PC?  True

  4. OS Family:  Ubuntu

  5. OS Version:  ()

  6. Browser Family:  Chromium

  7. Browser Version:  (56, 0, 2924)

就像在其他领域中一样,你可以基于关于数据本质的直觉想出自己的特征。在写作本文的时候,Chromium 56很新,但过了一段时间后,只有长时间没有重启浏览器的用户会使用这一版本。在这一情形下,为什么不引入一个称为“落后于浏览器最新版本”的特征呢?

除了操作系统和浏览器外,你还可以查看referrer(并不总是可用)、Accept-Language和其他元信息。

下一个有用的特征是IP地址,基于该数据可以提取国家,乃至城市、网络运营商、连接类型(是否为移动网络)。你需要了解,代理和数据库过期导致该特征可能包含噪声。网络管理专家可能会尝试提取更酷炫的特征,比如是否使用VPN。另外,IP地址数据和Accept-Language是一对良好的组合:如果用户的IP地址显示在智利,而浏览器的本地化设为ru_RU(俄罗斯),该用户的所在地并不清晰,需要查看对应的特征栏is_traveler_or_proxy_user(是旅行者还是代理用户)。

任何给定的领域都有许多专门的细节,难以完全掌握。因此,我邀请每个人分享他们的经验,在留言中讨论特征提取和生成。

2. 特征转换

2.1 正则化和改变分布

对有些算法而言,单调特征转换是关键,而对另一些算法而言,该转换毫无效果。这是决策树及其变体(随机森林、梯度提升)日益流行的一个原因。不是所有人都能够或愿意捣鼓变换,而这些算法在异常分布上的鲁棒性很好。

另外也有单纯的工程原因:np.log是一种处理np.float64无法容纳的大数的方式。不过,这更多地是一个例外而不是一条规则;常常它是由让数据集适配算法要求而驱动的。参数化方法通常最少要求对称单极分布,而真实数据并不总是满足这一点。可能还有更严厉的需求;可以回顾下我们之前的线性模型课程

然而,限定数据需求的并不仅仅是参数化方法,如果特征未经正则化,K近邻将预测出完全无意义的结果。例如,一个分布位于原点附近,不超过(-1, 1)的范围,而另一个分布的区间数量级成百上千。

最简单的转换是标准标度(Standard Scaling),又称Z值正则化(Z-score normalization):

注意,严格意义上说,标准标度并不生成正态分布。

  
  
    
  1. from sklearn.preprocessing import StandardScaler

  2. from scipy.stats import beta

  3. from scipy.stats import shapiro

  4. import numpy as np

  5. data = beta(1, 10).rvs(1000).reshape(-1, 1)

使用夏皮罗-威尔克检验(Shapiro–Wilk test)查看数据是否符合正态分布:

  
  
    
  1. shapiro(data)

结果:

  
  
    
  1. (0.8733664751052856, 1.0237383656642377e-27)

  
  
    
  1. shapiro(StandardScaler().fit_transform(data))

结果:

  
  
    
  1. (0.87336665391922, 1.0237810250125059e-27)

夏皮罗-威尔克检验计算所得的p值说明原数据和经标准标度处理后的数据都不符合正态分布。

不过,某种程度上而言,它能为离散值提供一些保护:

  
  
    
  1. data = np.array([1, 1, 0, -1, 2, 1, 2, 3, -2, 4, 100]).reshape(-1, 1).astype(np.float64)

  2. StandardScaler().fit_transform(data)

输出:

  
  
    
  1. array([[-0.31922662],

  2.       [-0.31922662],

  3.       [-0.35434155],

  4.       [-0.38945648],

  5.       [-0.28411169],

  6.       [-0.31922662],

  7.       [-0.28411169],

  8.       [-0.24899676],

  9.       [-0.42457141],

  10.       [-0.21388184],

  11.       [ 3.15715128]])

另一个相当流行的选项是极小化极大标度(MinMax Scaling),将所有数据点纳入一个预先规定的区间(通常是(0, 1))。

  
  
    
  1. from sklearn.preprocessing import MinMaxScaler

  2. MinMaxScaler().fit_transform(data)

结果:

  
  
    
  1. array([[ 0.02941176],

  2.       [ 0.02941176],

  3.       [ 0.01960784],

  4.       [ 0.00980392],

  5.       [ 0.03921569],

  6.       [ 0.02941176],

  7.       [ 0.03921569],

  8.       [ 0.04901961],

  9.       [ 0.        ],

  10.       [ 0.05882353],

  11.       [ 1.        ]])

标准标度和极小化极大标度的应用类似,常常可以互相替换。然而,如果算法涉及计算数据点或向量之间的距离,默认的选项是标准标度。而在可视化时,极小化极大标度很有用(将特征纳入(0, 255)区间)。

如果你假定某些数据并非正态分布,但可以由对数正态分布刻画,那么这些数据很容易就能转换为正态分布:

生成对数正态分布数据:

  
  
    
  1. from scipy.stats import lognorm

  2. data = lognorm(s=1).rvs(1000)

可以看到,原数据不符合正态分布:

  
  
    
  1. shapiro(data)

结果:

  
  
    
  1. (0.5831204056739807, 1.3032075718220799e-43)

而转换后的数据符合正态分布:

  
  
    
  1. shapiro(np.log(data))

结果:

  
  
    
  1. (0.9991741180419922, 0.9468745589256287)

对数正态分布适用于描述薪水、安保费用、城区人口、网络文章评论,等等。然而,底层分布不一定非得是对数正态分布才能应用这一过程;你可以应用转换于任何具有厚重的右端的分布。此外,你还可以使用其他类似的转换,形式化自身关于如何逼近可得分布至正态分布的假说。这类转换的例子包括Box-Cox转换(对数转换是Box-Cox转换的一个特例)和Yeo-Johnson转换(将应用范围扩展至负数)。此外,你也可以尝试在特征上加上一个常量——np.log (x + const)。

在以上的例子中,我们处理的是合成数据,并使用夏皮罗-威尔克检验严格地测试正态分布。下面让我们查看一些真实数据,并使用不那么形式化的方法测试正态分布——分位图(Q-Q plot)。正态分布的分位图看起来像一条平滑的对角线,而可视的异常值应该能够被直观地理解。

正态分布分位图

从Renthop数据集中提取价格特征,并手工过滤最极端的值:

  
  
    
  1. price = df.price[(df.price <= 20000) & (df.price > 500)]

绘制初始特征分位图:

  
  
    
  1. import statsmodels.api as sm

  2. sm.qqplot(price, loc=price.mean(), scale=price.std())

初始特征的分位图

应用标准标度和极小化极大标度后,形状并未改变。

  
  
    
  1. price_z = StandardScaler().fit_transform(price.values.reshape(-1, 1).astype(np.float64)).flatten()

  2. sm.qqplot(price_z, loc=price_z.mean(), scale=price_z.std())

标准标度分位图

  
  
    
  1. price_mm = MinMaxScaler().fit_transform(price.values.reshape(-1, 1).astype(np.float64)).flatten()

  2. sm.qqplot(price_mm, loc=price_mm.mean(), scale=price_mm.std())

极小化极大标度分位图

取对数后情况不一样了,更接近正态分布!

  
  
    
  1. price_log = np.log(price)

  2. sm.qqplot(price_log, loc=price_log.mean(), scale=price_log.std())

对数分位图

2.2 相互作用

如果前面的转换看起来更像是由数学驱动的,这一部分更多地牵涉数据的本质;它既可以算特征转换,也可以算特征创建。

让我们回到之前的出租房屋问题。该问题的其中两个特征是房间数和价格。逻辑表明每间房的价格比总价格更具指示性,所以我们可以生成这一特征。

  
  
    
  1. rooms = df["bedrooms"].apply(lambda x: max(x, .5))

  2. df["price_per_bedroom"] = df["price"] / rooms

注意,上面我们避免了除以零——0.5的选择多多少少是随意的。

在此过程中,你应该给自己一点限制。如果将特征数限定在一定数目一下,有可能生成所有可能的相互作用,然后使用下一节提到的技术去除不必要的特征。此外,并非所有特征的相互作用都有实际意义;比如,线性模型中常用的多项式特征(见sklearn.preprocessing.PolynomialFeatures)几乎无法解释。 2.3 填充缺失值

很多算法无法处理缺失值,而现实世界常常提供有缝隙的数据。很幸运,这不是什么需要创造性的任务。pandas和sklearn都提供了易于使用的解决方案:pandas.DataFrame.fillna和sklearn.preprocessing.Imputer。

这些解决方案背后并没有使用什么魔法。处理缺失值的方法相当直截了当:

  • 编码缺失值为一个单独的空值,比如,类别变量使用n/a值;

  • 使用该特征最可能的值(数值变量使用均值或中位数,类别变量使用最常见的值);

  • 或者,反其道而行之,使用某个极端值(比较适合决策树,允许模型在缺失值和未缺失值间分割);

  • 有序数据(例如,时序数据),使用相邻值——下一个值或上一个值。

有时库提供的解决方案会建议使用固定值,比如df = df.fillna(0),然后别再为缝隙操心。然而,这并非最佳方案:数据预处理比创建模型花去的时间更多,因此草率的缺失值填充可能隐藏处理过程中的bug,损害模型。

3. 特征选取

为什么有必要选取特征?对某些人而言,这个想法可能看起来违背直觉。但是,至少有两个摆脱不重要特征的重要原因:

  1. 数据越多,计算复杂度越高。在玩具数据集上,数据的尺寸不是一个问题。但在真实的生产环境中,很容易碰上数百个额外特征。

  2. 部分算法将噪声(不含信息量的特征)视作信号,导致过拟合。

3.1 统计学方法

最明显的待移除候选特征是值保持不变的特征,即,不包含信息的特征。沿着这一思路,有理由说方差较低的特征比不上方差较高的特征。所以,我们可以考虑去除方差低于特定阈值的特征。

生成数据(20个特征):

  
  
    
  1. from sklearn.feature_selection import VarianceThreshold

  2. from sklearn.datasets import make_classification

  3. x_data_generated, y_data_generated = make_classification()

  4. x_data_generated.shape

输出:

  
  
    
  1. (100, 20)

移除方差低于阈值的特征:

  
  
    
  1. VarianceThreshold(.7).fit_transform(x_data_generated).shape

输出:

  
  
    
  1. (100, 17)

可以看到,移除了3个特征。

另有其他根据统计数据移除特征的方式:

  
  
    
  1. from sklearn.feature_selection import SelectKBest, f_classif

  2. from sklearn.linear_model import LogisticRegression

  3. from sklearn.model_selection import cross_val_score

  4. x_data_kbest = SelectKBest(f_classif, k=5).fit_transform(x_data_generated, y_data_generated)

  5. x_data_varth = VarianceThreshold(.9).fit_transform(x_data_generated)

比较结果:

  
  
    
  1. cross_val_score(LogisticRegression(), x_data_generated, y_data_generated, scoring='neg_log_loss').mean()

输出:

  
  
    
  1. -0.37781295009028232

  
  
    
  1. cross_val_score(LogisticRegression(), x_data_kbest, y_data_generated, scoring='neg_log_loss').mean()

输出:

  
  
    
  1. -0.35972468376121292

  
  
    
  1. cross_val_score(LogisticRegression(), x_data_varth, y_data_generated, scoring='neg_log_loss').mean()

输出:

  
  
    
  1. -0.3087523637780904

我们可以看到,选取特征提高了分类器的表现。当然,这一例子纯属人造;然而,在真实问题上值得使用特征选取。

3.2 建模选取

另一种方法是使用某个基线模型评估特征,因为模型会显示特征重要性。常用的两类模型是:随机森林和搭配Lasso正则的线性模型。这一逻辑相当直观:如果简单模型中特征明显无用,没有必要将它们拖入更复杂的模型。

  
  
    
  1. from sklearn.datasets import make_classification

  2. from sklearn.linear_model import LogisticRegression

  3. from sklearn.ensemble import RandomForestClassifier

  4. from sklearn.feature_selection import SelectFromModel

  5. from sklearn.model_selection import cross_val_score

  6. from sklearn.pipeline import make_pipeline

  7. x_data_generated, y_data_generated = make_classification()

  8. pipe = make_pipeline(SelectFromModel(estimator=RandomForestClassifier()), LogisticRegression())

  9. lr = LogisticRegression()

  10. rf = RandomForestClassifier()

  11. print(cross_val_score(lr, x_data_generated, y_data_generated, scoring='neg_log_loss').mean())

  12. print(cross_val_score(rf, x_data_generated, y_data_generated, scoring='neg_log_loss').mean())

  13. print(cross_val_score(pipe, x_data_generated, y_data_generated, scoring='neg_log_loss').mean())

结果:

  
  
    
  1. -0.455661299521484

  2. -0.6817083161800542

  3. -0.28572375767268404

当然,没有银弹,这也可能导致表现下降。比如,另一次运行上述代码得到的结果为:

  
  
    
  1. -0.317184435457

  2. -0.728176055999

  3. -0.334236791084

3.3 网格搜索

最后,我们将介绍最可靠的方法,同时也是算力上最复杂的方法:微不足道的网格搜索。在特征子集上训练模型,储存结果,在不同子集上重复这一过程,比较模型的质量以识别最佳特征集。这一方法称为穷尽特征选取(Exhaustive Feature Selection)

搜索所有组合通常会花去太长的时间,因此你可以尝试缩减搜索空间。固定一个较小的数字N,迭代所有N特征的组合,选定最佳组合,接着迭代N+1特征组合,其中,之前的最佳组合是固定的,仅仅考虑一个新特征。重复这些迭代过程,直到达到特征最大值,或者直到模型的表现停止明显提升。这一算法称为循序特征选取(Sequential Feature Selection)

安装mlxtend库后运行可以下代码:

  
  
    
  1. from mlxtend.feature_selection import SequentialFeatureSelector

  2. selector = SequentialFeatureSelector(LogisticRegression(), scoring='neg_log_loss',

  3.                                     verbose=2, k_features=3, forward=False, n_jobs=-1)

  4. selector.fit(x_data, y_data)

结果:

  
  
    
  1. SequentialFeatureSelector(clone_estimator=True, cv=5,

  2.             estimator=LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,

  3.          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,

  4.          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,

  5.          verbose=0, warm_start=False),

  6.             floating=False, forward=False, k_features=3, n_jobs=-1,

  7.             pre_dispatch='2*n_jobs', scoring='neg_log_loss', verbose=2)

以上算法也可以反向操作:从完整的特征空间开始,逐个移除特征,移除后不会损害模型表现的特征为不重要的特征。

相关阅读:

机器学习开放课程(五):Bagging与随机森林

机器学习开放课程(四)线性分类与线性回归

机器学习开放课程(三):分类、决策树和K近邻

机器学习开放课程(二):使用Python可视化数据

机器学习开放课程(一):使用Pandas探索数据分析

原文地址:https://medium.com/open-machine-learning-course/open-machine-learning-course-topic-6-feature-engineering-and-feature-selection-8b94f870706a

登录查看更多
5

相关内容

【实用书】学习用Python编写代码进行数据分析,103页pdf
专知会员服务
190+阅读 · 2020年6月29日
【实用书】Python机器学习Scikit-Learn应用指南,247页pdf
专知会员服务
255+阅读 · 2020年6月10日
【经典书】精通机器学习特征工程,中文版,178页pdf
专知会员服务
347+阅读 · 2020年2月15日
【论文推荐】文本分析应用的NLP特征推荐
专知会员服务
33+阅读 · 2019年12月8日
【机器学习课程】Google机器学习速成课程
专知会员服务
161+阅读 · 2019年12月2日
新书《面向机器学习和数据分析的特征工程》,419页pdf
专知会员服务
140+阅读 · 2019年10月10日
特征工程方法:一、类别变量编码
论智
5+阅读 · 2018年11月20日
推荐 :一文带你读懂特征工程
数据分析
16+阅读 · 2018年8月26日
特征工程的特征理解(一)
机器学习研究会
10+阅读 · 2017年10月23日
超级干货 :一文读懂特征工程
数据分析
9+阅读 · 2017年9月6日
已删除
将门创投
9+阅读 · 2017年7月28日
Area Attention
Arxiv
5+阅读 · 2019年5月23日
Risk-Aware Active Inverse Reinforcement Learning
Arxiv
7+阅读 · 2019年1月8日
Implicit Maximum Likelihood Estimation
Arxiv
7+阅读 · 2018年9月24日
Physical Primitive Decomposition
Arxiv
4+阅读 · 2018年9月13日
Arxiv
13+阅读 · 2018年4月18日
VIP会员
相关资讯
特征工程方法:一、类别变量编码
论智
5+阅读 · 2018年11月20日
推荐 :一文带你读懂特征工程
数据分析
16+阅读 · 2018年8月26日
特征工程的特征理解(一)
机器学习研究会
10+阅读 · 2017年10月23日
超级干货 :一文读懂特征工程
数据分析
9+阅读 · 2017年9月6日
已删除
将门创投
9+阅读 · 2017年7月28日
相关论文
Area Attention
Arxiv
5+阅读 · 2019年5月23日
Risk-Aware Active Inverse Reinforcement Learning
Arxiv
7+阅读 · 2019年1月8日
Implicit Maximum Likelihood Estimation
Arxiv
7+阅读 · 2018年9月24日
Physical Primitive Decomposition
Arxiv
4+阅读 · 2018年9月13日
Arxiv
13+阅读 · 2018年4月18日
Top
微信扫码咨询专知VIP会员