特征工程是一个过程,它首先在概念上,然后在程序上将一个原始样本转化为特征向量。它包括将一个特征概念化,然后编写程序代码,可能借助一些间接数据,将整个原始样本转化为一个特征。
4.1 为什么要进行特征工程
具体来说,可以考虑在推文中识别电影标题的问题。假设你有一个庞大的电影标题集合,这是要间接使用的数据。你还有一个推文的集合,这些数据将直接用于创建样本。首先,建立一个电影标题索引,以便快速进行字符串匹配[1],然后在推文中找到所有匹配的电影标题。现在规定你的样本是一些匹配,你的机器学习问题是二分类的问题:一个匹配是电影,或不是电影。
考虑如图4.2所示的推文。
图4.2 Kyle的一条推文
我们的电影标题匹配索引可以帮助找到以下匹配信息:“avatar”“the terminator” “It” 和 “her”。这给了我们4个无标签样本。你可以给这4个样本贴上标签:{(avatar, False), (the terminator, True), (It, False), (her, False)}。然而,机器学习算法不能仅从电影标题中学习任何东西(人类也不能):它需要上下文。你可能会决定,匹配前的5个词和匹配后的5个词是一个信息量足够大的上下文。用机器学习的行话来说,我们将这样的上下文称为围绕匹配的“10词窗口”。你可以将窗口的宽度作为一个超参数来调整。
现在,你的样本在其上下文中被标记为匹配。然而,学习算法不能应用于这样的数据。机器学习算法只能应用于特征向量。这就是为什么你要求助于特征工程。
4.2 如何进行特征工程
特征工程是一个创造性的过程,在这个过程中,分析师应用他们的想象力、直觉和领域专业知识。在推文中电影标题识别的示例问题中,我们利用直觉将匹配周围的窗口宽度固定为10。现在,我们需要更有创意地将字符串序列转化为数字向量。
4.2.1 文本的特征工程
当涉及文本时,科学家和工程师经常使用简单的特征工程技巧。两种这样的技巧是“独热编码”和“词袋”。
一般来说,独热编码(one-hot encoding)将一个分类属性转化为多个二进制属性。假设你的数据集有一个属性“颜色”,可能的值有“红”“黄”和“绿”。我们将每个值转化为一个三维二进制向量,如下所示:
红 = [1, 0, 0]
黄 = [0, 1, 0]
绿 = [0, 0, 1]
在电子表格中,你将使用三个合成列来代替以属性“颜色”为标题的一列,其值为1或0。其优点是你现在可以使用大量的机器学习算法,因为只有少数学习算法支持分类属性。
词袋(bag-of-words)是将独热编码技术应用于文本数据的一种泛化。你不是将一个属性表示为二进制向量,而是用这种技术将整个文本文档表示为二进制向量。我们来看看它是如何工作的。
设想你有6份文本文档的集合,如图4.3所示。
图4.3 6份文本文档的集合
假设你的问题是按主题建立一个文本分类器。分类学习算法希望输入的内容是有标签的特征向量,所以你必须将文本文档集合转化为特征向量集合。词袋可以让你做到这一点。
首先,将文本词条化。词条化(tokenization)是将文本分割成小块的过程,这些小块称为“词条”。词条器(tokenizer)是一个软件,它将一个字符串作为输入,并返回从该字符串中提取的一系列词条。通常情况下,词条是单词,但这并不是严格的必要条件。它可以是一个标点符号、一个单词,或者在某些情况下,一个单词的组合,如一个公司(如麦当劳)或一个地方(如红场)。假设我们使用一个简单的词条器提取单词,忽略其他一切。我们得到如图4.4所示的集合。
图4.4 已词条化文档的集合
其次,建立一个词汇表。它包含如下16个词条。
a breath doing feathers
gentle impulsion is lighter
love makes me my
on shakes verb word
现在以某种方式对你的词汇表进行排序,并为每个词条分配一个独特的索引。我按字母顺序排列了这些词条,如图4.5所示。
图4.5 已排序并建立索引的词条
词汇表中的每个词条都有唯一索引——从1到16。我们将集合转化为二元特征向量的集合,如图4.6所示。
图4.6 特征向量
如果文本中存在相应的词条,则该位置的特征为1;否则,该位置的特征为0。
例如,文档1 “Love, love is a verb”由以下特征向量表示:
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0]
使用相应的标注特征向量作为训练数据,任何分类学习算法都可以使用这些数据。
有几种词袋的“风格”。上面的二进制值模型通常很好用。二进制值的替代方法包括:①词条的计数;②词条的频率;③TF-IDF(Term Frequency-Inverse Document Frequency,术语频率-反转文档频率)。如果采用单词计数,那么文档1“Love, love is a verb”中 “love”的特征值就是2,代表“love”这个词在文档中出现的次数。如果应用词条的频率,假设词条器提取了两个“love”的词条,从文档1中共提取了5个词条,那么“love”的值将是2/5=0.4。TF-IDF的值会随着文档中某个词的频率按比例增加,并被语料库中包含该词的文档数量所抵消。这样可以调整一些词,如介词和代词,它们一般情况下出现频率较高。关于TF-IDF,我就不多说了,但建议有兴趣的读者上网了解一下。
词袋技术的直接扩展是
元连续词袋(bag-of-
-gram)。一个
元连续词(n-gram)是一个从语料库中抽取的
个单词的序列。如果
=2,并且忽略标点符号,则文本“No, I am your father.”中可以找到的所有二元连续词(通常称为bigrams)包括[“No I” “I am” “am your” “your father”]。三元连续词是[“No I am” “I am your” “am your father”]。通过将一定
以内的所有
元连续词与一个词典中的词条混合,得到一个
元连续词袋,我们可以用处理词袋模型的方式来词条化。
因为词的序列通常比单个词的常见度低,所以使用
元连续词可以创建一个更稀疏(sparse)的特征向量。同时,
元连续词允许机器学习算法学习更细微的模型。例如,句子“this movie was not good and boring”和“this movie was good and not boring”的意思是相反的,但仅仅基于单词,就会得到相同的词袋向量。如果我们考虑二元连续词,那么这两个句子的二元连续词的词袋向量就会不同。
4.2.2 为什么词袋有用
特征向量只有在遵循某些规则的情况下才能发挥作用。其中一个规则是,特征向量中
位置的特征必须在数据集中的所有样本中代表相同的属性。如果该特征代表了数据集中某个人的身高(以厘米为单位),其中每个样本代表了一个不同的人,那么在所有其他样本中也必须成立。位置
的特征必须始终代表以厘米为单位的身高,而不是其他。
词袋技术的工作原理是一样的。每个特征都代表文档的同一属性:特定的词条在文档中是存在还是不存在。
另一个规则是,相似的特征向量必须代表数据集中的相似实体。在使用词袋技术时,也要尊重这一属性。两个相同的文档将具有相同的特征向量。同样,关于同一主题的两个文本将有更高的机会拥有相似的特征向量,因为它们会比两个不同主题的文本共享更多的单词。
4.2.3 将分类特征转换为数字
独热编码并不是将分类特征转换为数字的唯一方法,也并不总是最好的方法。
均值编码(mean encoding),也称为箱计数(bin counting)或特征校准(feature calibration),是另一种技术。首先,使用具有特征值
的所有样本来计算标签的样本均值(sample mean),然后用这个样本均值替换该分类特征的每个值
。这种技术的优点是数据维度不会增加,而且根据设计,数值包含了标签的一些信息。
如果你研究的是二分类问题,除了样本均值之外,还可以使用其他有用的量:给定
值的正类的原始计数、让步比(odd ratio)和对数让步比(log-odd ratio)。让步比(OR)通常定义在两个随机变量之间。从一般意义上讲,OR是量化两个事件
和
之间关联强度的统计量,如果两个事件的OR等于1,即一个事件在另一个事件存在或不存在的情况下,其概率都相同,则认为两个事件是独立的。
在应用于量化一个分类特征时,我们可以计算一个分类特征(事件
)的值
与正例标签(事件
)之间的让步比。我们用一个例子来说明。假定问题是预测一封邮件是否是垃圾邮件。假设我们有一个有标签的邮件信息数据集,我们设计了一个特征,它包含了每个邮件信息中最频繁的单词。假设我们找到能代替这个特征的分类值“infected”的数值。我们首先建立“infected”和“垃圾邮件”的列联表(contingency table),如图4.7所示。
图4.7 “infected”和“垃圾邮件”的列联表
“infected”和“垃圾邮件”的让步比为:
让步比(infected,拉圾邮件)
如你所见,根据列联表中的数值,让步比可以极低(接近零)或极高(任意高的正值)。为了避免数值上的溢出问题,分析师经常使用对数让步比:
对数让步比(infected, 垃圾邮件) = log(145/8) − log(346/2 909)
= log(145) − log(8) − log(346) + log(2 909) = 2.2
现在,你可以将上述分类特征中的“infected”值替换为值2.2。你可以对该分类特征的其他值进行同样的操作,并将它们全部转换为对数让步比值。
有时,分类特征是有序的,但不是周期性的。例子包括学校分数(从“A”到“E”)和资历级别(“初级”“中级”“高级”)。与其使用独热编码,不如用有意义的数字来表示它们,这很方便。在[0,1]范围内使用统一的数值,比如1/3代表“初级”,2/3代表“中级”,1代表“高级”。如果有些数值应该相距较远,可以用不同的比例来反映。如“高级”与“中级”应该比“中级”比“初级”更远,你可以用1/5、2/5、1分别代表“初级”“中级”和“高级”。这就是为什么领域知识很重要。
如果分类特征是周期性的,整数编码就不能很好地发挥作用。例如,尝试将周一到周日转换为整数1~7。周日和周六之间的差值是1,而周一和周日之间的差值是−6。但我们的推理表明,差值同样是1,因为周一只是周日过了一天。
作为替代,请使用正弦-余弦变换(sine-cosine transformation)。它将一个周期性特征转换为两个合成特征。令
表示我们周期性特征的整数值。将周期性特征的值
替换为以下两个值:
表4.1为一周七天的
和
值。
图4.8包含了使用上述表格建立的散点图。你可以看到两个新特征的周期性。
现在,在你的规整数据中,用两个值[0.78,0.62]替换“周一”,用[0.97,−0.22]替换“周二”,以此类推。数据集又增加了一个维度,但与整数编码相比,模型的预测质量明显提高。
图4.8 正弦-余弦变换后的特征表示一周的日子
4.2.4 特征哈希
特征哈希(feature hashing)或哈希技巧(hashing trick),将文本数据或具有许多值的分类属性转换为任意维度的特征向量。独热编码和词袋编码有一个缺点:许多独特的值将创建高维的特征向量。例如,如果一个文本文档集合中有100万个唯一的词条,词袋将产生每个维度为100万的特征向量。处理这样的高维数据,计算成本可能非常昂贵。
为了使数据易于管理,可以使用哈希技巧,其工作原理如下。首先,决定特征向量所需的维度。然后,使用哈希函数(hash function),先将分类属性(或文档集合中的所有词条)的所有值转换为一个数字,然后将这个数字转换为特征向量的索引。这个过程如图4.9所示。
图4.9 哈希技巧图示,一个属性值的原始基数为
,期望维度为5
接下来说明如何将一个文本“Love is a doing word”转换为特征向量。设我们有一个哈希函数
,它接受一个字符串作为输入,输出一个非负整数,并设所需的维度为5。将哈希函数应用于每个词,并应用5的模数来获得该词的索引,可以得到:
(love) mod 5 = 0
(is) mod 5 = 3
(a) mod 5 = 1
(doing) mod 5 = 3
(word) mod 5 = 4
然后建立特征向量为
[1, 1, 0, 2, 1]
事实上,
(love) mod 5 = 0意味着我们在特征向量的维度0处有一个词;
(is) mod 5 = 3和
(doing) mod 5 = 3意味着我们在特征向量的维度3处有两个词,以此类推。如你所见,“is”和“doing”这两个词之间存在碰撞(collision):它们都用维度3来表示。所需的维数越低,碰撞的概率就越大。这是学习速度和质量之间的权衡。
常用的哈希函数有MurmurHash3、Jenkins、CityHash和MD5。
4.2.5 主题建模
主题建模是使用无标签数据的一系列技术,这些数据通常以自然语言文本文档的形式存在。模型学习将文档表示为主题的向量。例如,在新闻文章的集合中,5个主要主题可以是“体育”“政治”“娱乐”“金融”和“技术”。然后,每个文档可以被表示为一个5维的特征向量,每个主题一个维度:
[0.04, 0.5, 0.1, 0.3, 0.06]
上面的特征向量代表了一个文档,它混合了两大主题:政治(权重为0.5)和金融(权重为0.3)。主题建模算法,如潜在语义分析(Latent Semantic Analysis,LSA)和潜在狄利克雷分布(Latent Dirichlet Allocation,LDA),通过分析无标签的文档进行学习。这两种算法基于不同的数学模型产生类似的输出。LSA使用“词到文档”矩阵的奇异值分解(Singular Value Decomposition,SVD),该矩阵使用二元连续词袋或TF-IDF构建。LDA使用分层贝叶斯模型(Bayesian model),其中每个文档是几个主题的混合(mixture),每个词的出现归因于其中一个主题。
我们用Python和R来说明它是如何工作的。下面是LSA的Python代码。
1 from sklearn.feature_extraction.text import TfidfVectorizer
2 from sklearn.decomposition import TruncatedSVD
3
4 class LSA():
5 def __init__ (self, docs):
6 # Convert documents to TF-IDF vectors
7 self.TF_IDF = TfidfVectorizer()
8 self.TF_IDF.fit(docs)
9 vectors = self.TF_IDF.transform(docs)
10
11 # Build the LSA topic model
12 self.LSA_model = TruncatedSVD(n_components=50)
13 self.LSA_model.fit(vectors)
14 return
15
16 def get_features(self, new_docs):
17 # Get topic-based features for new documents
18 new_vectors = self.TF_IDF.transform(new_docs)
19 return self.LSA_model.transform(new_vectors)
20
21 # Later, in production, instantiate LSA model
22 docs = ["This is a text.", "This another one."]
23 LSA_featurizer = LSA(docs)
24
25 # Get topic-based features for new_docs
26 new_docs = ["This is a third text.", "This is a fourth one."]
27 LSA_features = LSA_featurizer.get_features(new_docs)
R中对应的代码[3]如下所示。
1 library(tm)
2 library(lsa)
3
4 get_features <- function(LSA_model, new_docs){
5 # new_docs can be passed as a tm::Corpus object or as a vector
6 # holding character strings representing documents:
7 if(!inherits(new_docs, "Corpus")) new_docs <- VCorpus (VectorSource(new_docs))
8 tdm_test <- TermDocumentMatrix(
9 new_docs,
10 control = list(
11 dictionary = rownames(LSA_model$tk),
12 weighting = weightTfIdf
13 )
14 )
15 txt_mat <- as.textmatrix(as.matrix(tdm_test))
16 crossprod(t(crossprod(txt_mat, LSA_model$tk)), diag(1/LSA_
model$sk))
17 }
18
19 # Train LSA model using docs
20 docs <- c("This is a text.", "This another one.")
21 corpus <- VCorpus(VectorSource(docs))
22 tdm_train <- TermDocumentMatrix(
23 corpus, control = list(weighting = weightTfIdf))
24 txt_mat <- as.textmatrix(as.matrix(tdm_train))
25 LSA_fit <- lsa(txt_mat, dims = 2)
26
27 # Later, in production, get topic-based features for new_docs
28 new_docs <- c("This is a third text.", "This is a fourth one.")
29 LSA_features <- get_features(LSA_fit, new_docs)
下面是LDA的Python代码。
1 from sklearn.feature_extraction.text import CountVectorizer
2 from sklearn.decomposition import LatentDirichletAllocation
3
4 class LDA():
5 def __init__ (self, docs):
6 # Convert documents to TF-IDF vectors
7 self.TF = CountVectorizer()
8 self.TF.fit(docs)
9 vectors = self.TF.transform(docs)
10 # Build the LDA topic model
11 self.LDA_model = LatentDirichletAllocation(n_components=50)
12 self.LDA_model.fit(vectors)
13 return
14 def get_features(self, new_docs):
15 # Get topic-based features for new documents
16 new_vectors = self.TF.transform(new_docs)
17 return self.LDA_model.transform(new_vectors)
18
19 # Later, in production, instantiate LDA model
20 docs = ["This is a text.", "This another one."]
21 LDA_featurizer = LDA(docs)
22
23 # Get topic-based features for new_docs
24 new_docs = ["This is a third text.", "This is a fourth one."]
25 LDA_features = LDA_featurizer.get_features(new_docs)
下面是R中对应的代码。
1 library(tm)
2 library(topicmodels)
3
4 # Generate feature for new_docs by using LDA_model
5 get_features <- function(LDA_mode, new_docs){
6 # new_docs can be passed as tm::Corpus object or as a vector
7 # holding character strings representing documents:
8 if(!inherits(new_docs, "Corpus")) new_docs <- VCorpus(VectorSource
(new_docs))
9 new_dtm <- DocumentTermMatrix(new_docs, control = list(weighting =
weightTf))
10 posterior(LDA_mode, newdata = new_dtm)$topics
11 }
12
13 # train LDA model using docs
14 docs <- c("This is a text.", "This another one.")
15 corpus <- VCorpus(VectorSource(docs))
16 dtm <- DocumentTermMatrix(corpus, control = list(weighting =
weightTf))
17 LDA_fit <- LDA(dtm, k = 5)
18
19 # later, in production, get topic-based features for new_docs
20 new_docs <- c("This is a third text.", "This is a fourth one.")
21 LDA_features <- get_features(LDA_fit, new_docs)
在上面的代码中,docs是一个文本文档的集合。例如,它可以是一个字符串的列表,其中每个字符串是一个文档。
4.2.6 时间序列的特征
时间序列数据(time-series data)不同于传统的监督学习数据,那些数据的形式是独立观测值的无序集合。时间序列是一个有序的观测值序列,每个观测值都标有一个与时间相关的属性,如时间戳、日期、年月、年份等。图4.10展示了一个时间序列数据的例子。
图4.10 事件流形式的时间序列数据的例子
在图4.10中,每一行对应的是某只股票在某一时刻的成本,以及两个指数的数值——标准普尔500指数和道琼斯指数。观测的时间不固定:在2020-01-12,有3次观测;在2020-01-13,有2次观测。在经典时间序列数据(classical time-series data)中,观测值在时间上的间隔是均匀的,比如每秒、每分钟、每天等都有一次观测。如果观测值是不规则的,这样的时间序列数据称为点过程(point process)或事件流(event stream)。
通常可以通过聚合观测结果,将事件流转换为经典时间序列数据。聚合运算符的例子有COUNT和AVERAGE。通过对图4.10中的事件流数据应用AVERAGE运算符,我们得到了图4.11所示的经典时间序列数据。
图4.11 通过汇总图4.10的事件流得到的经典时间序列
虽然可以直接处理事件流,但将时间序列变成经典形式,可以更简单地应用进一步的聚合和生成特征进行学习。
分析师通常使用时间序列数据来解决两种预测问题。给定一个最近的观测序列:
- 预测下一次的观察结果(例如,给定过去7天的股票价格和股票指数的价值,预测明天的股票价格);
- 预测一些关于产生该序列的现象(例如,给定用户与软件系统的连接记录,预测他们是否有可能在本季度取消订阅)。
在神经网络达到它们现在的学习能力之前,分析师使用浅层机器学习(shallow machine learning)工具箱处理时间序列数据。为了将时间序列转化为特征向量形式的训练数据,必须做出两个决定:
- 需要多少个连续的观测值才能做出准确的预测(所谓的预测窗口);
- 如何将观测值序列转换为固定维度的特征向量。
这两个问题都没有简单的方法来回答。通常是根据主题专家的知识,或者通过使用超参数调整(hyperparameter tuning)技术来做出决定。然而,有些方法对许多时间序列数据都有效。下面是这样一个方法:
(1)把整个时间序列分成长度为
的片段;
(2)从每个片段
中创建一个训练样本
;
(3)对于每一个
,计算
中观测值的各种统计量。
我们将图4.11的数据分块成长度为
=2的片段,其中
为预测窗口的长度。图4.12显示,现在每个片段都是一个独立的样本。
图4.12 将时间序列分成长度为
=2的片段
在实践中,
通常大于2。假设预测窗口的长度为7。在时间序列数据处理方法的步骤(3)中计算的统计数据可以是:
- 平均数,例如,过去7天内股价的均值(mean)或中位数(median)。
- 价差,例如,标准普尔500指数在过去7天内的标准差(standard deviation)、绝对偏差中位数(median absolute deviation)或四分位数范围(interquartile range)。
- 离群值,例如,道琼斯指数的数值非典型偏低的观测值的部分,例如,与均值相差两个标准差以上。
- 增长,例如,标准普尔500指数的数值在
−6日和
日之间、
−3日和
日之间、
−1日和
日之间是否有增长。
- 视觉,例如,股票价格的曲线与已知的视觉形象(如帽子形态、头肩顶形态等)有多大差异。
现在明白为什么建议将时间序列转换为经典形式了吧:上述统计量只有在可比值上计算时才有意义。
需要注意的是,在现代神经网络时代,分析师最喜欢训练深度神经网络。长短期记忆(Long Short-Term Memory,LSTM)、卷积神经网络(Convolutional Neural Network,CNN)和Transformer是时间序列模型架构的流行选择。它们可以读取任意长度的时间序列作为输入,并根据整个序列生成预测。同样,神经网络也经常通过逐单词或逐字符读取文本来应用于文本。单词和字符通常被表示为嵌入向量(embedding vector),后者是从大型文本文档语料库中学习的。4.7.1节将讨论嵌入的问题。
4.2.7 发挥你的创造力
正如在本节开头提到的,特征工程是一个创造性的过程。作为一个分析师,你最适合决定哪些好特征适合你的预测模型。设想自己“就是”学习算法,你会从数据中看什么来决定分配哪个标签。
假设你正在将邮件分类为重要的或不重要的。你可能会注意到,在每个月的第一个星期一,有大量的重要邮件来自政府税收机构。创建一个特征“政府第一个星期一”。当邮件在每个月的第一个星期一来自政府税收机构时,让它等于1,否则为0。另外,你可能会注意到,一封邮件中包含一个以上的笑脸通常是不重要的。创建一个“包含笑脸”的特征。当一封邮件包含一个以上的笑脸时,让它等于1,否则等于0。
书籍推荐:《机器学习工程实战》
本书侧重于对机器学习应用和工程实践的关注,是对机器学习工程实践和设计模式的全面回顾。全书共 10 章,在概述之后,分别从项目开始前的准备,数据收集和准备,特征工程,监督模型训练,模型评估,模型部署,模型服务、监测和维护方面进行讲解,最后做了简短的总结。
本书适合想要从事机器学习项目的数据分析师、机器学习工程师以及机器学习相关专业的学生阅读,也可供需要处理一些模型的软件架构师参考。