面向机器学习的特征工程-全-
面向机器学习的特征工程(全)
一、引言
译者:@ZhipengYe
机器学习将数据拟合到数学模型中来获得结论或者做出预测。这些模型吸纳特征作为输入。特征就是原始数据某方面的数学表现。在机器学习流水线中特征位于数据和模型之间。特征工程是一项从数据中提取特征,然后转换成适合机器学习模型的格式的艺术。这是机器学习流水线关键的一步,因为正确的特征可以减轻建模的难度,并因此使流水线能输出更高质量的结果。从业者们认为构建机器学习流水线的绝大多数时间都花在特征工程和数据清洗上。然后,尽管它很重要,这个话题却很少单独讨论。也许是因为正确的特征只能在模型和数据的背景中定义。由于数据和模型如此多样化,所以很难概括项目中特征工程的实践。
尽管如此,特征工程不仅仅是一种临时实践。工作中有更深层的原则,最好就地进行说明。本书的每一章都针对一个数据问题:如何表示文本数据或图像数据,如何降低自动生成的特征的维度,何时以及如何规范化等等。把它看作是一个相互联系的短篇小说集,而不是一本长篇小说。每章都提供了大量现有特征工程技术的插图。它们一起阐明了总体原则。
掌握主题不仅仅是了解定义并能够推导出公式。仅仅知道这个机制是如何工作的以及它可以做什么是不够的。它还必须包括理解为什么要这样设计,它如何与其他技术相关联,以及每种方法的优缺点是什么。掌握就是要准确地知道如何完成某件事,对底层原则有一个感觉,并将其整合到我们已知的知识网络中。一个人通过读一本相关的书并不会成为某个东西的主人,尽管一本好书可以打开新的门。它必须涉及实践——将想法用于实践,这是一个反复的过程。随着每一次迭代,我们都会更好地了解这些想法,并在应用这些想法时变得越来越娴熟和富有创造性。本书的目标是促进其思想的应用。
本书首先尝试传授感觉,其次是数学。我们不是只讨论如何完成某些事情,而是试图引导发现原因。我们的目标是提供观点背后的感觉,以便读者了解如何以及何时应用它们。对于以不同方式学习的人们来说,有大量的描述和图片。提出数学公式是为了使感觉更加精确,并且还可以将本书与其他现有的知识结合起来。
本书中的代码示例在 Python 中给出,使用各种免费和开源软件包。NumPy 库提供数字向量和矩阵操作。Pandas 是一个强大的数据框架,是 Python 中数据科学的基石。Scikit-learn 是一个通用机器学习软件包,涵盖了广泛的模型和特征变换器。Matplotlib 和 Seaborn 的样式库提供了绘图和可视化。你可以在我们的 github 仓库中找到这些例子作为 Jupyter notebooks。
前几章开始较缓慢,为刚刚开始使用数据科学和机器学习的人们提供了一个桥梁。第 1 章从数字数据的基本特征工程开始:过滤,合并,缩放,日志转换和能量转换以及交互功能。第 2 章和第 3 章深入探讨了自然文本的特征工程:bag-of-words,n-gram 和短语检测。第 4 章将 tf-idf 作为特征缩放的例子,并讨论它的工作原理。围绕第 5 章讨论分类变量的高效编码技术,包括特征哈希和 bin-counting,步伐开始加速。当我们在第 6 章中进行主成分分析时,我们深入机器学习的领域。第 7 章将 k-means 看作一种特征化技术,它说明了模型堆叠的有效理论。第 8 章都是关于图像的,在特征提取方面比文本数据更具挑战性。在得出深度学习是最新图像特征提取技术的解释之前,我们着眼于两种手动特征提取技术 SIFT 和 HOG。我们在第 9 章中完成了一个端到端示例中的几种不同技术,为学术论文数据集创建了一个推荐器。
概括提示
作者建议本书中的插图最好以彩色显示。真的,你应该在第 7 章中打印出彩色版本的“瑞士卷”,然后粘贴到你的书中。你的审美意识会感谢我们。
特征工程是一个广泛的话题,每天都在发明更多的方法,特别是在自动特征学习方面。为了将本书的范围限制在一个可管理的大小,我们不得不做一些削减。本书没有讨论音频数据的傅里叶分析,但它是一个与线性代数中的特征分析密切相关的美丽主题(我们在第 4 章和第 6 章中会介绍)。我们也忽略了与傅立叶分析密切相关的随机特征的讨论。我们通过对图像数据的深度学习来介绍特征学习,但不会深入到积极开发中的众多深度学习模型中。超出范围的还有先进的研究思路,如随机投影,复杂文本特征模型(如 word2vec 和 Brown 聚类)以及潜在空间模型(如潜在狄利克雷分析和矩阵分解)。如果这些话对你来说毫无意义,那么你很幸运。如果特征学习的前沿是你的兴趣所在,那么这可能不是为你准备的书。
本书假设掌握了基本机器学习概念的知识,例如“什么是模型”和“什么是矢量”。它并不要求掌握了数学或统计学。线性代数,概率分布和优化的经验是有帮助的,但不是必需的。
机器学习流水线
在深入研究特征工程之前,让我们花点时间看看整个机器学习流水线。这将帮助我们更好地了解应用的大方向。为此,让我们从数据和模型等基本概念入手。
数据
我们所说的数据是对现实世界现象的观察。例如,股票市场数据可能涉及对每日股票价格的观察,个别公司的收益公告,甚至专家的意见文章。个人生物识别数据可以包括我们的心率,血糖,血压等的测量数据。客户情报数据包括诸如:“Alice 在周日买两本书”,“Bob 浏览网站上的这些页面”和“查理点击了上周的特别优惠链接”。我们可以在不同领域得到无数的数据例子。
任务
我们为什么收集数据?因为数据可以帮助我们回答很多问题。这些问题可能是:“我应该投资哪些股票?”,“我怎么样才能活得更健康?”,或者“我如何理解顾客变化的口味,以便我的企业能够更好的服务他们?”。
从数据中获取答案的路途充满了谜题。最开始充满希望的方法可能行不通。最初只是一个预感却可能是最佳解决方案。数据的工作流程通常是一个多阶段的迭代过程。例如,股票价格是在交易所观察到的,由汤普森路透社等中间人汇总,存储在数据库中,由公司购买,在 Hadoop 集群中转换为 Hive 存储,通过脚本从商店中抽出,进行二次抽样,由另一个脚本处理和清理,转储为文件,转换为可以在 R,Python 或 Scala 中最喜欢的建模库中尝试的格式,将预测转储回 csv 文件,由评估程序分析,迭代多次,最后由生产团队用 C++ 或 Java 重写,运行所有数据,并将最终预测输出到另一个数据库。
然而,如果我们暂时忽略工具和系统的混乱,我们可能可以看到上述过程涉及到的两个数学实体,它们是机器学习的黄油和面包:模型和特征。
模型
尝试着从数据中了解世界就像用不完整的拼图以及一堆额外的碎片拼凑出现实。这就是数学模型,特别是统计模型的由来。统计语言中包含许多频繁数据特征的概念:误差,冗余或者缺失。误差数据是测量中的错误导致的。冗余数据意味着数据的多个方面传达完全相同的信息。例如,星期几可以作为“星期一”,“星期二”,...“星期天”的分类变量存在,并且再次被包括为 0 到 6 之间的整数里。如果某些数据点中不存在这种星期几的信息,那么这个数据就产生了缺失。数据的数学模型描述数据不同方面的关系。举个例子。一个预测股票价格的模型可能是一个将公司收益历史、过去股票价格和行业映射到预测股票价格的公式。推荐音乐的模型可能考量了用户的相似处,然后给欣赏很多同样歌曲的用户推荐一样的艺术家。
数学公式将数量相互关联。但是原始数据通常不是数字。(“爱丽丝周三买了‘指环王’三部曲”的行为不是数字,她后来对这本书的评论也不是。)必须有一样东西把两者连接在一起。这就是特征的由来。
特征
特征是原始数据的数学表示。有很多方法可以将原始数据转换为数学测量值,这也是为什么特征最终看起来与许多事情相似。自然的,特征必须来自可用数据的类型。可能它们与模型相关联的事实也没那么明显;一些模型更适合某些类型的特征,反之亦然。正确的特征应该与手头的任务相关并且容易被模型摄取。特征工程是指给定数据、模型和任务是制定最佳特征的过程。
特征的数量也是很重要的。如果没有足够的信息特征,模型就无法完成最终的任务。如果存在太多的特征,或者如果它们大多数是无关紧要的,那么训练这个模型会更加的棘手并且代价更多。在训练过程中可能会出现一些错误影响模型的表现。
模型评估
特征和模型位于原始数据和期望的观察结果之间。在机器学习工作流程中,我们不仅挑选模型,还挑选特征。这是一个双节杆,一个选择会影响另一个。良好的特征使后续的建模步骤变得简单,并且所得到的模型能更容易实现所需的任务。糟糕的特征可能需要更复杂的模型才能达到相同的性能水*。在本书的其余部分中,我们将介绍不同类型的特征,并讨论它们对不同类型数据和模型的优缺点。不要犹豫,让我们开始吧!
二、简单数字的奇特技巧
译者:@coboe
校对者:@ZiseonJiao
在深入研究诸如文本和图像这样的复杂数据类型之前,让我们先从最简单的数字数据开始。它们可能来自各种来源:地理位置或人、购买的价格、传感器的测量、交通计数等。数字数据已经是数学模型容易消化的格式。这并不意味着不再需要特征工程。好的特征不仅代表数据的显著方面,而且符合模型的假设。因此,转换常常是必要的。数字特征工程技术是基础。当原始数据被转换为数字特征时,它们可以被应用。
数值数据的第一个健全检查是大小是否重要。我们只需要知道它是正面的还是负面的?或者我们只需要知道一个非常粗粒度的大小?这一明智的检查对于自动累积数尤其重要,比如统计,每天访问网站的次数,餐馆所获得的评论数量等等。
接下来,考虑特征的规模。最大值和最小值是什么?它们跨越几个数量级吗?输入特性*滑的模型对输入的尺度敏感。例如,3x+ 1 是输入 X 的简单线性函数,其输出的规模直接取决于输入的比例。其他例子包括 k-均值聚类,最*邻居方法,RBF 内核,以及使用欧几里得距离的任何东西。对于这些模型和建模组件,通常规范化特征以使输出保持在预期的规模上通常是一个好主意。
另一方面,逻辑函数对输入特征量表不敏感。无论输入是什么,它们的输出都是二进制的。例如,逻辑,并采取任何两个变量和输出 1,当且仅当两个输入均为真。逻辑函数的另一个例子是阶跃函数“输入 x 大于 5”。决策树模型由输入特征的阶跃函数组成。因此,基于空间划分树(决策树、梯度提升机、随机森林)的模型对尺度不敏感。唯一的例外是如果输入的规模随着时间的增长而增长,那么如果该特征是某种类型的累积计数。最终它将生长在树被训练的范围之外。如果可能是这样的话,那么就有必要周期性地重新调整输入。另一个解决方案是第 5 章讨论的 bin 计数方法。
考虑数值特征的分布也是很重要的。分布总结了承担特定价值的可能性。输入特征的分布对某些模型比其他模型更重要。例如,线性回归模型的训练过程假定预测误差分布得像高斯。这通常是好的,除非预测目标在几个数量级上扩散。在这种情况下,高斯误差假设可能不再成立。解决这一问题的一种方法是转变产出目标,以驯服规模的增长。(严格地说,这将是目标工程,而不是特征工程。)对数变换,这是一种功率变换,将变量的分布接*高斯。另一个解决方案是第 5 章讨论的 bin 计数方法。
除了裁剪模型或培训过程的假设, 多个功能可以组合成更复杂的功能。希望复杂的功能能够更简洁地捕捉原始数据中的重要信息。通过使输入功能更加 "雄辩", 模型本身可以更简单, 更容易进行培训和评估, 并做出更好的预测。作为一个极端的, 复杂的特点本身可能是统计模型的输出。这是一个称为模型堆叠的概念, 我们将在 7 章和 8 章中更详细地讨论。在本章中, 我们给出了复杂特征的最简单示例: 交互功能。
交互特征易于制定,但特征的组合导致更多的输入到模型中。为了减少计算开销,通常需要使用自动特征选择来修剪输入特征。
我们将从标量、向量和空间的基本概念开始,然后讨论尺度、分布、交互特征和特征选择。
标量、向量和空间
在我们开始之前, 我们需要定义一些基本概念, 这本书的其余部分。单个数字特征也称为标量。标量的有序列表称为向量。向量位于向量空间中。在绝大多数机器学习应用中, 对模型的输入通常表示为数字向量。本书的其余部分将讨论将原始数据转换为数字向量的最佳实践策略.
向量可以被可视化为空间中的一个点。(有时人们从原点到那一点画一条线和一个箭头。在这本书中,我们将主要使用这一点。例如,假设我们有一个二维向量 v=[1,-1]。也就是说,向量包含两个数,在第一方向 d1 中,向量具有 1 的值,并且在第二方向 d2 中,它具有-1 的值。我们可以在二维图中绘制 v。

Figure 2-1. A single vector.
在数据世界中, 抽象向量及其特征维度具有实际意义。例如, 它可以代表一个人对歌曲的偏爱。每首歌都是一个特征, 其中 1 的值相当于大拇指向上,-1 个拇指向下。假设向量 v 表示一个听众 Bob 的喜好。Bob 喜欢 Bob Dylan 的 “Blowin’ in the Wind” 和 Lady Gaga 的 "Poker Face"。其他人可能有不同的喜好。总的来说, 数据集合可以在特征空间中可视化为点云.
相反,一首歌可以由一组人的个人喜好来表示。假设只有两个听众,Alice 和 Bob。Alice 喜欢 Leonard Cohen 的 “Poker Face”, “Blowin’ in the Wind” 和 “Hallelujah”,但讨厌 Katy Perry 的 “Roar” 和 Radiohead 的 “Creep”。Bob 喜欢 “Roar", “Hallelujah”和“Blowin’ in the Wind”,但讨厌 “Poker Face” 和 “Creep” 。在听众的空间里,每一首歌都是一个点。就像我们可以在特征空间中可视化数据一样,我们可以在数据空间中可视化特征。图 2-2 显示了这个例子。

Figure 2-2. Illustration of feature space vs. data space.
处理计数
在大数据时代,计数可以快速积累而不受约束。用户可以将歌曲或电影放在无限播放中,或者使用脚本反复检查流行节目的门票可用性,这会导致播放次数或网站访问计数迅速上升。当数据可以以高的体积和速度产生时,它们很可能包含一些极值。这是一个好主意,检查他们的规模,并确定是否保持它们作为原始数字,将它们转换成二进制变量,以指示存在,或将它们放入粗粒度。
二值化
Million Song 数据集中的用户品味画像包含了一百万个用户在 Echo Nest 的完整音乐聆听历史。下面是有关数据集的一些相关统计数据。
Echo Nest 品味画像数据集的统计
- 有超过 4800 万个用户 ID、音乐 ID 和监听计数三元组。
- 完整的数据集包含 1019318 个独特用户和 384546 首独特歌曲。
- 引文:Echo Nest 品味画像的数据子集,官方的 Million Song 数据集的用户数据集,可从这里获得:http://labrosa.ee.columbia.edu/millionsong/tasteprofile
假设任务是建立一个推荐器向用户推荐歌曲。推荐器的一个组件可以预测用户将对一首特别的歌曲会有多少喜欢。由于数据包含实际的听歌次数,这应该是预测的目标吗?如果一个大的听计数意味着用户真的喜欢这首歌,反之亦然,那是正确的。然而,数据表明,虽然 99%的听计数是 24 或更低,也有一些听计数数以千计,最大为 9667。(如图 2-3 所示,直方图最接*于 0 的 bin 中的峰值。但是超过 10000 个三元组的计数更大,几千个则有几个。这些值异常大;如果我们试图预测实际的听计数,那么模型将被这些大的值拉离。

Figure 2-3. Histogram of listen counts in the user taste profile of the Million Song Dataset. Note that the y-axis is on a log scale.
在 Million Song 数据集中,原始监听计数不是用户口味的可靠度量。(在统计术语中,健壮性意味着该方法在各种各样的条件下工作。)用户有不同的听力习惯。有些人可能把他们最喜欢的歌曲放在无限的循环中,而其他人可能只在特殊的场合品尝它们。很难说听一首歌 20 次的人一定喜欢听 10 次的人的两倍。
用户偏好的更健壮表示是使计数二元化和修剪所有大于 1 的计数为 1。换句话说,如果用户至少听过一首歌,那么我们将其视为用户喜欢歌曲。这样,模型不需要花费周期来预测原始计数之间的微小差异。二进制目标是用户偏好的简单而稳健的度量。
例子 2-1。使 Million Song 数据集中听歌计数二进制化。
import pandas as pd
listen_count = pd.read_csv('millionsong/train_triplets.txt.zip', header=None, delimiter='\t')
# The table contains user-song-count triplets. Only non-zero counts are
# included. Hence to binarize the count, we just need to set the entire
# count column to 1.
listen_count[2] = 1
这是我们设计模型目标变量的一个例子。严格地说, 目标不是一个特征, 因为它不是输入。但有时我们确实需要修改目标以解决正确的问题。
量化或装箱
对于本练习, 我们从第 6 轮 Yelp 数据集挑战中采集数据, 并创建一个更小的分类数据集。Yelp 数据集包含用户对来自北美和欧洲十个城市的企业的评论。每个商户都标记为零个或多个类别。以下是有关数据集的相关统计信息。
关于第 6 轮 Yelp 数据集的统计
- 有 782 种商户类别。
- 完整的数据集包含 1569264 个(约 1.6M)评论和 61184 个(61K)商户。
- “餐厅”(990627 个评论)和“夜生活”(210028 个评论)是最流行的类别,评论计数是明智的。
- 没有一个商户同时属于餐厅和夜生活分类。因此,这两组评论之间没有重叠。
每个商户都有一个评论计数。假设我们的任务是使用协同过滤来预测用户可能给企业的评级。评论计数可能是一个有用的输入特征,因为通常在流行和良好的评级之间有很强的相关性。现在的问题是,我们应该使用原始评论计数或进一步处理它吗?图 2-4 显示了所有商户评论计数的直方图。我们看到和音乐听歌计数一样的模式。大部分的统计数字都很小,但一些企业有成千上万的评论。
例子 2-2。在 YELP 数据集中可视化商户评论计数。
>>> import pandas as pd
>>> import json
### Load the data about businesses
>>> biz_file = open('yelp_academic_dataset_business.json')
>>> biz_df = pd.DataFrame([json.loads(x) for x in biz_file.readlines()])
>>> biz_file.close()
>>> import matplotlib.pyplot as plt
>>> import seaborn as sns
### Plot the histogram of the review counts
>>> sns.set_style('whitegrid')
>>> fig, ax = plt.subplots()
>>> biz_df['review_count'].hist(ax=ax, bins=100)
>>> ax.set_yscale('log')
>>> ax.tick_params(labelsize=14)
>>> ax.set_xlabel('Review Count', fontsize=14)
>>> ax.set_ylabel('Occurrence', fontsize=14)

Figure 2-4. Histogram of business review counts in the Yelp reviews dataset. The y-axis is on a log-scale.
对于许多模型来说,跨越数个数量级的原始计数是有问题的。在线性模型中,相同的线性系数必须对计数的所有可能值工作。大量的计数也可能破坏无监督学习方法,如 k-均值聚类,它使用相似性函数来测量数据点之间的相似性。k-均值使用数据点之间的欧几里得距离。数据向量的一个元素中的大计数将超过所有其他元素中的相似性,这可能会丢弃整个相似性度量。
一种解决方案是通过量化计数来包含标量。换句话说,我们将计数分组到容器中,并且去掉实际的计数值。量化将连续数映射成离散数。我们可以把离散化的数字看作是代表强度度量的容器的有序的序列。
为了量化数据,我们必须决定每一个箱子应该有多宽。解决方案分为固定宽度或自适应两种类型。我们将给出每个类型的例子。
固定宽度装箱
对于固定宽度装箱, 每个 bin 都包含一个特定的数值范围。范围可以是定制设计或自动分割, 它们可以线性缩放或指数缩放。例如, 我们可以将一个人的年龄分组为十年: 0-9 岁归纳到 bin 1, 10-19 年归纳到 bin 2 等。要从计数映射到 bin, 只需除以 bin 的宽度并取整部分。
也经常看到定制设计的年龄范围更适合于生活的阶段:
- 0-12 岁
- 12-17 岁
- 18-24 岁
- 25-34 岁
- 35-44 岁
- 45-54 岁
- 55-64 岁
- 65-74 岁
- 75 岁以上
当数字跨越多个数量级时,最好用 10 个幂(或任何常数的幂)来分组:0-9、10-99、100-999、100-9999 等。容器宽度呈指数增长,从 O(10)、O(100)到 O(1000)和以上。要从计数映射到 bin,取计数的 log 值。指数宽度的划分与对数变换非常相关,我们在“对数变换”中讨论。
例子 2-3。用固定宽度的箱进行量化计数
>>> import numpy as np
### Generate 20 random integers uniformly between 0 and 99
>>> small_counts = np.random.randint(0, 100, 20)
>>> small_counts
array([30, 64, 49, 26, 69, 23, 56, 7, 69, 67, 87, 14, 67, 33, 88, 77, 75, 47, 44, 93])
### Map to evenly spaced bins 0-9 by division
>>> np.floor_divide(small_counts, 10)
array([3, 6, 4, 2, 6, 2, 5, 0, 6, 6, 8, 1, 6, 3, 8, 7, 7, 4, 4, 9], dtype=int32)
### An array of counts that span several magnitudes
>>> large_counts = [296, 8286, 64011, 80, 3, 725, 867, 2215, 7689, 11495, 91897, 44, 28, 7971, 926, 122, 22222]
### Map to exponential-width bins via the log function
>>> np.floor(np.log10(large_counts))
array([ 2., 3., 4., 1., 0., 2., 2., 3., 3., 4., 4., 1., 1., 3., 2., 2., 4.])
分位数装箱
固定宽度装箱很容易计算。但是如果计数有很大的差距, 那么将会有许多空的垃圾箱没有数据。该问题可以通过基于数据分布的垃圾箱自适应定位来解决。这可以使用分发的分位数来完成。
分位数是将数据划分为相等部分的值。例如, 中位数将数据分成一半;一半的数据是较小的, 一半大于中位数。分位数把数据分成几个部分, 十分位数把数据划分成十份。示例 2-4 演示如何计算 Yelp 商户评论数的十等分, 图 2-5 覆盖直方图上的十等分。这就更清楚地说明了对更小的计数的歪斜。
例子 2-4。计算 Yelp 商户评论数的十分位数
>>> deciles = biz_df['review_count'].quantile([.1, .2, .3, .4, .5, .6, .7, .8, .9])
>>> deciles
0.1 3.0
0.2 4.0
0.3 5.0
0.4 6.0
0.5 8.0
0.6 12.0
0.7 17.0
0.8 28.0
0.9 58.0
Name: review_count, dtype: float64
### Visualize the deciles on the histogram
>>> sns.set_style('whitegrid')
>>> fig, ax = plt.subplots()
>>> biz_df['review_count'].hist(ax=ax, bins=100)
>>> for pos in deciles:
... handle = plt.axvline(pos, color='r')
>>> ax.legend([handle], ['deciles'], fontsize=14)
>>> ax.set_yscale('log')
>>> ax.set_xscale('log')
>>> ax.tick_params(labelsize=14)
>>> ax.set_xlabel('Review Count', fontsize=14)
>>> ax.set_ylabel('Occurrence', fontsize=14)

Figure 2-5. Deciles of the review counts in the Yelp reviews dataset. Note that both x- and y-axes are in log scale.
为了计算分位数和映射数据到分位数箱,我们可以使用 Pandas 库。 pandas.DataFrame.quantile 和 pandas.Series.quantile 用于计算分位数。pandas.qcut 将数据映射到所需数量的分位数。
例子 2-5。按分位数分箱计数。
### Continue example Example 2-3 with large_counts
>>> import pandas as pd
### Map the counts to quartiles
>>> pd.qcut(large_counts, 4, labels=False)
array([1, 2, 3, 0, 0, 1, 1, 2, 2, 3, 3, 0, 0, 2, 1, 0, 3], dtype=int64)
### Compute the quantiles themselves
>>> large_counts_series = pd.Series(large_counts)
>>> large_counts_series.quantile([0.25, 0.5, 0.75])
0.25 122.0
0.50 926.0
0.75 8286.0
dtype: float64
对数转换
在“量化或装箱”中,我们简要地介绍了把计数的对数映射到指数宽度箱的概念。让我们现在再看一看。
对数函数是指数函数的逆。它定义为

其中 a 为正常数, x 可以是任何正数。由于
我们有
这意味着对数函数将小范围的数字 (0、1) 映射到负数的整个范围 (-∞, 0)。函数
将 [1、10] 映射到 [0、1]、将[10、100] 映射到 [1、2] 等等。换言之, 对数函数压缩大数的范围, 并扩展小数的范围。越大的 x , log(x)的增量越慢。通过查看 log 函数的图像, 可以更容易地消化这一点。(见图 2-6)。

Figure 2-6. The log function compresses the high numeric range and expands the low range. Note how the horizontal x values from 100 to 1000 got compressed into just 2.0 to 3.0 in the vertical y range, while the tiny horizontal portion of x values less than 100 are mapped to the rest of the vertical range.
对数变换是处理具有重尾分布的正数的有力工具。(重尾分布在尾部范围内的概率比高斯分布的概率大)。它将分布在高端的长尾压缩成较短的尾部,并将低端扩展成较长的头部。图 2-7 比较 d 对数转换之前和之后的 YELP 商户评论计数的直方图。Y 轴现在都在正常(线性)尺度上。在(0.5,1)范围内的底部图中增加的仓间隔是由于在 1 和 10 之间只有 10 个可能的整数计数。请注意,原始审查计数非常集中在低计数区域,离群值在 4000 以上。对数变换后,直方图不集中在低端,更分散在 X 轴上。
例子 2-6。可视化对数变换前后评论数分布
fig, (ax1, ax2) = plt.subplots(2,1)
biz_df['review_count'].hist(ax=ax1, bins=100)
ax1.tick_params(labelsize=14)
ax1.set_xlabel('review_count', fontsize=14)
ax1.set_ylabel('Occurrence', fontsize=14)
biz_df['log_review_count'].hist(ax=ax2, bins=100)
ax2.tick_params(labelsize=14)
ax2.set_xlabel('log10(review_count))', fontsize=14)
ax2.set_ylabel('Occurrence', fontsize=14)

Figure 2-7. Comparison of Yelp business review counts before (top) and after (bottom) log transformation.
另一个例子是来自 UC Irvine 机器学习库的在线新闻流行数据集。以下是有关数据集的相关统计信息。
在线新闻流行数据集的统计
- 该数据集包括 MasHabor 在两年的时间内出版的 39797 个新闻文章的 60 个特征。
- 引证: K. Fernandes, P. Vinagre 和 P. Cortez . 一种用于预测在线新闻的流行程度的主动智能决策支持系统。2015 第十七届 EPIA 活动, 葡萄牙人工智能会议论文集, 9 月, 葡萄牙科英布拉。
目的是利用这些特征来预测文章在社交媒体上的用分享数量表示的流行度。在本例中, 我们将只关注一个特征——文章中的单词数。图 2-8 显示了对数转换前后特征的直方图。请注意, 在对数转换后, 分布看起来更高斯, 除了长度为零的文章 (无内容) 的断裂。
例子 2-7。可视化在有对数变换和没有对数变换时新闻文章流行度的分布。
fig, (ax1, ax2) = plt.subplots(2,1)
df['n_tokens_content'].hist(ax=ax1, bins=100)
ax1.tick_params(labelsize=14)
ax1.set_xlabel('Number of Words in Article', fontsize=14)
ax1.set_ylabel('Number of Articles', fontsize=14)
df['log_n_tokens_content'].hist(ax=ax2, bins=100)
ax2.tick_params(labelsize=14)
ax2.set_xlabel('Log of Number of Words', fontsize=14)
ax2.set_ylabel('Number of Articles', fontsize=14)

Figure 2-8. Comparison of word counts in Mashable news articles before (top) and after (bottom) log transformation.
对数转换实战
让我们看看在监督学习中对数转换如何执行。我们将使用上面的两个数据集。对于 Yelp 评论数据集, 我们将使用评论的数量来预测商户的*均评级。对于 Mashable 的新闻文章, 我们将使用文章中的字数来预测其流行程度。由于输出是连续的数字, 我们将使用简单的线性回归作为模型。我们在没有对数变换和有对数变换的特色上,使用 Scikit Learn 执行 10 折交叉验证的线性回归。模型由 R 方评分来评估, 它测量训练后的回归模型预测新数据的良好程度。好的模型有较高的 R 方分数。一个完美的模型得到最高分 1。分数可以是负的, 一个坏的模型可以得到一个任意低的负评分。通过交叉验证, 我们不仅得到了分数的估计, 还获得了方差, 这有助于我们判断两种模型之间的差异是否有意义。
例子 2-8。使用对数转换 YELP 评论数预测*均商户评级
>>> import pandas as pd
>>> import numpy as np
>>> import json
>>> from sklearn import linear_model
>>> from sklearn.model_selection import cross_val_score
## Using the previously loaded Yelp reviews dataframe,
## compute the log transform of the Yelp review count.
## Note that we add 1 to the raw count to prevent the logarithm from
## exploding into negative infinity in case the count is zero.
>>> biz_df['log_review_count'] = np.log10(biz_df['review_count'] + 1)
## Train linear regression models to predict the average stars rating of a business,
## using the review_count feature with and without log transformation
## Compare the 10-fold cross validation score of the two models
>>> m_orig = linear_model.LinearRegression()
>>> scores_orig = cross_val_score(m_orig, biz_df[['review_count']], biz_df['stars'], cv=10)
>>> m_log = linear_model.LinearRegression()
>>> scores_log = cross_val_score(m_log, biz_df[['log_review_count']], biz_df['stars'], cv=10)
>>> print("R-squared score without log transform: %0.5f (+/- %0.5f)" % (scores_orig.mean(), scores_orig.std() * 2))
>>> print("R-squared score with log transform: %0.5f (+/- %0.5f)" % (scores_log.mean(), scores_log.std() * 2))
R-squared score without log transform: -0.03683 (+/- 0.07280)
R-squared score with log transform: -0.03694 (+/- 0.07650)
从实验的结果来看, 两个简单的模型 (有对数变换和没有对数变换) 在预测目标时同样不好, 而有对数变换的特征表现略差。真令人失望!这并不奇怪, 他们都不是很好, 因为他们都只使用一个功能。但是, 人们本来希望日志转换的功能执行得更好。
让我们看看对数转换在线新闻流行数据集上如何表现。
例子 2-9。利用经过对数转换在线新闻数据中的词数量预测文章流行度
## Download the Online News Popularirty dataset from UCI, then use
## Pandas to load the file into a dataframe
>>> df = pd.read_csv('OnlineNewsPopularity.csv', delimiter=', ')
## Take the log transform of the 'n_tokens_content' feature, which
## represents the number of words (tokens) in a news article.
>>> df['log_n_tokens_content'] = np.log10(df['n_tokens_content'] + 1)
## Train two linear regression models to predict the number of shares
## of an article, one using the original feature and the other the
## log transformed version.
>>> m_orig = linear_model.LinearRegression()
>>> scores_orig = cross_val_score(m_orig, df[['n_tokens_content']], df['shares'], cv=10)
>>> m_log = linear_model.LinearRegression()
>>> scores_log = cross_val_score(m_log, df[['log_n_tokens_content']], df['shares'], cv=10)
>>> print("R-squared score without log transform: %0.5f (+/- %0.5f)" % (scores_orig.mean(), scores_orig.std() * 2))
>>> print("R-squared score with log transform: %0.5f (+/- %0.5f)" % (scores_log.mean(), scores_log.std() * 2))
R-squared score without log transform: -0.00242 (+/- 0.00509)
R-squared score with log transform: -0.00114 (+/- 0.00418)
置信区间仍然重叠,但具有对数变换特征的模型比没有对数变换的表现更好。为什么对数转换在这个数据集上更成功?我们可以通过观察输入特征和目标值的散点图来得到线索。如图 2-9 的底部面板所示,对数变换重塑了 X 轴,将目标值(大于 200000 个份额)中的大离群值进一步拉向轴的右手侧。这给线性模型在输入特征空间的低端更多的“呼吸空间”。没有对数转换(上部面板),在输入值变化下非常小的情况下,模型有更大的压力下适应非常不同的目标值。
示例 2-10。可视化新闻流程度预测问题中输入输出相关性。
fig2, (ax1, ax2) = plt.subplots(2,1)
ax1.scatter(df['n_tokens_content'], df['shares'])
ax1.tick_params(labelsize=14)
ax1.set_xlabel('Number of Words in Article', fontsize=14)
ax1.set_ylabel('Number of Shares', fontsize=14)
ax2.scatter(df['log_n_tokens_content'], df['shares'])
ax2.tick_params(labelsize=14)
ax2.set_xlabel('Log of the Number of Words in Article', fontsize=14)
ax2.set_ylabel('Number of Shares', fontsize=14)

Figure 2-9. Scatter plot of number of words (input) vs. number of shares (target) in the Online News dataset. The top plot visualizes the original feature, and the bottom plot shows the scatter plot after log transformation.
将此与应用于 YELP 评论数据集的相同散点图进行比较。图 2-10 看起来与图 2-9 非常不同。在 1 到 5,步长 0.5 的区间,*均星级是离散的。高评论计数(大致>2500 评论)与较高的*均星级评级相关。但这种关系远不是线性的。没有一种清晰的方法可以根据输入来预测*均星级。从本质上讲,该图表明,评论数及其对数都是*均星级的不良线性预测因子。
例 2-11。可视化 Yelp 商户评论预测中输入与输出的相关性。
fig, (ax1, ax2) = plt.subplots(2,1)
ax1.scatter(biz_df['review_count'], biz_df['stars'])
ax1.tick_params(labelsize=14)
ax1.set_xlabel('Review Count', fontsize=14)
ax1.set_ylabel('Average Star Rating', fontsize=14)
ax2.scatter(biz_df['log_review_count'], biz_df['stars'])
ax2.tick_params(labelsize=14)
ax2.set_xlabel('Log of Review Count', fontsize=14)
ax2.set_ylabel('Average Star Rating', fontsize=14)

Figure 2-10. Scatter plot of review counts (input) vs. average star ratings (target) in the Yelp Reviews dataset. The top panel plots the original review count, and the bottom panel plots the review count after log transformation.
数据可视化的重要性
对数变换在两个不同数据集上的影响的比较,说明了可视化数据的重要性。在这里,我们故意保持输入和目标变量简单,以便我们可以很容易地可视化它们之间的关系。如图 2-10 所示的曲线,立即显示所选择的模型(线性)不可能代表所选择的输入和目标之间的关系。另一方面,人们可以令人信服地在给定*均星级模拟评论数的分布。在建立模型时,最好直观地检查输入和输出之间的关系,以及不同输入特征之间的关系。
功率变换:对数变换的推广
对数变换是一个称为功率变换的变换族的特殊例子。在统计方面,这些是方差稳定的变换。为了理解为什么方差稳定是好的,考虑泊松分布。这是一个方差等于它的*均值的重尾分布。因此,它的质量中心越大,其方差越大,尾部越重。功率变换改变变量的分布,使得方差不再依赖于*均值。例如,假设一个随机变量 X 具有泊松分布。假如我们使用开*方根变换 X,
的方差大致是恒定的, 而不是等于*均值。

Figure 2-11. A rough illustration of the Poisson distribution. λ represents the mean of the distribution. As λ increases, not only does the mode of of the distribution shift to the right, but the mass spreads out and the variance becomes larger. The Poisson distribution is an example distribution where the variance increases along with the mean.
*方根变换和对数变换的简单推广称为 Box-Cox 变换:

图 2-12, 展示出了在 λ = 0 (log 变换),λ = 0.25, λ = 0.5(*方根的缩放和移位版本),λ = 0.75, 和 λ = 1.5 时的 Box-Cox 变换。设置 λ 小于 1 时压缩较高的值,并且设置 λ 大于 1 时具有相反的效果。

Figure 2-12. Box-Cox transforms for different values of λ.
只有当数据为正值时, Box-Cox 公式才能工作。对于非正数据, 可以通过加上固定常量来移动数值。当应用 Box-Cox 变换或更一般的功率变换时, 我们必须确定参数 λ 的值。这可能是通过最大似然(找到的λ,使产生的变换信号的高斯似然最大) 或贝叶斯方法。完全介绍 Box-Cox 和一般功率变换的使用超出了本书的范围。感兴趣的读者可以通过 Jack Johnston 和 John DiNardo (McGraw Hill) 编写的 Econometric Methods 找到更多关于幂转换的信息。幸运的是, Scipy 的数据包包含了一个 Box-Cox 转换的实现, 其中包括查找最佳变换参数。
例子 2-12。Yelp 商户评论数的 Box-Cox 变换。
>>> from scipy import stats
# Continuing from the previous example, assume biz_df contains
# the Yelp business reviews data
# Box-Cox transform assumes that input data is positive.
# Check the min to make sure.
>>> biz_df['review_count'].min()
3
# Setting input parameter lmbda to 0 gives us the log transform (without constant offset)
>>> rc_log = stats.boxcox(biz_df['review_count'], lmbda=0)
# By default, the scipy implementation of Box-Cox transform finds the lmbda parameter
# that will make the output the closest to a normal distribution
>>> rc_bc, bc_params = stats.boxcox(biz_df['review_count'])
>>> bc_params
-0.4106510862321085
图 2-13 提供了原始和转换评论数分布的可视化比较。
例 2-13。可视化评论数的原始、对数转换和 Box-Cox 转换的直方图。
fig, (ax1, ax2, ax3) = plt.subplots(3,1)
# original review count histogram
biz_df['review_count'].hist(ax=ax1, bins=100)
ax1.set_yscale('log')
ax1.tick_params(labelsize=14)
ax1.set_title('Review Counts Histogram', fontsize=14)
ax1.set_xlabel('')
ax1.set_ylabel('Occurrence', fontsize=14)
# review count after log transform
biz_df['rc_log'].hist(ax=ax2, bins=100)
ax2.set_yscale('log')
ax2.tick_params(labelsize=14)
ax2.set_title('Log Transformed Counts Histogram', fontsize=14)
ax2.set_xlabel('')
ax2.set_ylabel('Occurrence', fontsize=14)
# review count after optimal Box-Cox transform
biz_df['rc_bc'].hist(ax=ax3, bins=100)
ax3.set_yscale('log')
ax3.tick_params(labelsize=14)
ax3.set_title('Box-Cox Transformed Counts Histogram', fontsize=14)
ax3.set_xlabel('')
ax3.set_ylabel('Occurrence', fontsize=14)

Figure 2-13. Box-Cox transformation of Yelp business review counts.
概率图是一种直观地比较数据分布与理论分布的简单方法。这本质上是观察到散点图的与理论分位数。图 2-14 显示 YELP 评论数的原始数据和转换后数据相对正态分布的概率图。由于观测数据是严格正的,高斯可以是负的,所以分位数在负端上永远不会匹配。所以我们关注的是正数这的一边。在这方面,原始评论数明显比正常分布更重尾。(有序值上升到 4000,而理论位数仅延伸到 4)。简单的对数变换和最优的 Box-Cox 变换都使正尾部接*正态分布。最优的 Box-Cox 变换比对数变换更缩小尾部,由于尾部在红色对角线等值线下*展可以明显看出。
例子 2-14。原始和变换后的数据相对正态分布的概率图。
fig2, (ax1, ax2, ax3) = plt.subplots(3,1)
prob1 = stats.probplot(biz_df['review_count'], dist=stats.norm, plot=ax1)
ax1.set_xlabel('')
ax1.set_title('Probplot against normal distribution')
prob2 = stats.probplot(biz_df['rc_log'], dist=stats.norm, plot=ax2)
ax2.set_xlabel('')
ax2.set_title('Probplot after log transform')
prob3 = stats.probplot(biz_df['rc_bc'], dist=stats.norm, plot=ax3)
ax3.set_xlabel('Theoretical quantiles')
ax3.set_title('Probplot after Box-Cox transform')

Figure 2-14. Comparing the distribution of raw and transformed review counts against the Normal distribution.
特征缩放或归一化
某些特征的值有界的,如纬度或经度。其他数值特征 (如数量) 可能会在无界的情况下增加。那些关于输入是*滑函数的模型, 如线性回归、逻辑回归或任何涉及矩阵的东西, 都受输入的数值范围影响。另一方面, 基于树的模型不太在意这个。如果你的模型对输入特征的数值范围敏感, 则特征缩放可能会有所帮助。顾名思义, 特征缩放会更改特征值的数值范围。有时人们也称它为特征规范化。功能缩放通常分别针对单个特征进行。有几种常见的缩放操作, 每个类型都产生不同的特征值分布。
Min-max 缩放
设 X 是一个单独的特征值(即,在某些数据点中的一个特征值),以及 min(x) 和 max(x) ,分别是整个数据集上该特征的最小值和最大值。Min-max 缩放压缩(或拉伸)所有特征值到[0, 1 ]的范围内。图 2-15 演示了这个概念。最小最大尺度的公式是


Figure 2-15. Min-max scaling
标准化(方差缩放)
特征标准化的定义为:

减去特征 (所有数据点) 的*均值并除以方差。因此, 它也可以称为方差缩放。缩放后的特征的*均值为 0, 方差为 1。如果原始特征具有高斯分布, 则缩放特征为标准高斯。图 2-16 包含了标准化的说明。

Figure 2-16. Illustration of feature standardization
不要中心化稀疏数据
最小最大缩放和标准化都从原始特征值中减去一个数量。对于最小最大缩放, 移动量是当前特征的所有值中最小的。对于标准化, 移动的量是*均值。如果移动量不是零, 则这两种转换可以将稀疏特征(大部分值为零)的向量转换为一个稠密的向量。这反过来会给分类器带来巨大的计算负担, 取决于它是如何实现的。词袋是一种稀疏表示, 大多数分类库都对稀疏输入进行优化。如果现在的表示形式包含了文档中没有出现的每个单词, 那就太可怕了。请谨慎对稀疏特征执行最小最大缩放和标准化操作。
L2 normalization
这项技术通过所谓的 L2 范数 (也称为欧几里德范数) 正常化 (划分) 原始特征值。

L2 范数度量向量在坐标空间中的长度。这个定义可以从众所周知的勾股定理中得到,给出三角形两边的长度,可以得到斜边长度。

L2 范数将求特征的各数据点的*方和, 然后取*方根。L2 规范化后, 该特征列具有范数 1。它也可以称为 L2 缩放。(不严谨的说, 缩放意味着和常量相乘, 而规范化可能涉及许多操作。)图 2-17 说明了 L2 规范化。

Figure 2-17. Illustration of L2 feature normalization
数据空间与特征空间
请注意,图 2-17 中的说明是在数据空间中,而不是特征空间。还可以对数据点进行 L2 归一化,而不是特征,这将导致具有单位范数(范数为 1)的数据向量。(参见词袋中关于数据向量和特征向量的互补性质的讨论)不管缩放方法如何,特征缩放总是将特征除以常数(也称为归一化常数)。因此,它不会改变单特征分布的形状。我们将用在线新闻文章标记计数来说明这一点。
例子 2-15。特征缩放示例。
>>> import pandas as pd
>>> import sklearn.preprocessing as preproc
# Load the online news popularity dataset
>>> df = pd.read_csv('OnlineNewsPopularity.csv', delimiter=', ')
# Look at the original data - the number of words in an article
>>> df['n_tokens_content'].as_matrix()
array([ 219., 255., 211., ..., 442., 682., 157.])
# Min-max scaling
>>> df['minmax'] = preproc.minmax_scale(df[['n_tokens_content']])
>>> df['minmax'].as_matrix()
array([ 0.02584376, 0.03009205, 0.02489969, ..., 0.05215955,
0.08048147, 0.01852726])
# Standardization - note that by definition, some outputs will be negative
>>> df['standardized'] = preproc.StandardScaler().fit_transform(df[['n_tokens_content']])
>>> df['standardized'].as_matrix()
array([-0.69521045, -0.61879381, -0.71219192, ..., -0.2218518 ,
0.28759248, -0.82681689])
# L2-normalization
>>> df['l2_normalized'] = preproc.normalize(df[['n_tokens_content']], axis=0)
>>> df['l2_normalized'].as_matrix()
array([ 0.00152439, 0.00177498, 0.00146871, ..., 0.00307663,
0.0047472 , 0.00109283])
我们也可以可视化用不同的特征缩放方法后的数据的分布。如图 2-18 所示,与对数变换不同,特征缩放不会改变分布的形状;只有数据的规模发生变化。
例 2-16。绘制原始数据和缩放数据的直方图。
>>> fig, (ax1, ax2, ax3, ax4) = plt.subplots(4,1)
>>> fig.tight_layout()
>>> df['n_tokens_content'].hist(ax=ax1, bins=100)
>>> ax1.tick_params(labelsize=14)
>>> ax1.set_xlabel('Article word count', fontsize=14)
>>> ax1.set_ylabel('Number of articles', fontsize=14)
>>> df['minmax'].hist(ax=ax2, bins=100)
>>> ax2.tick_params(labelsize=14)
>>> ax2.set_xlabel('Min-max scaled word count', fontsize=14)
>>> ax2.set_ylabel('Number of articles', fontsize=14)
>>> df['standardized'].hist(ax=ax3, bins=100)
>>> ax3.tick_params(labelsize=14)
>>> ax3.set_xlabel('Standardized word count', fontsize=14)
>>> ax3.set_ylabel('Number of articles', fontsize=14)
>>> df['l2_normalized'].hist(ax=ax4, bins=100)
>>> ax4.tick_params(labelsize=14)
>>> ax4.set_xlabel('L2-normalized word count', fontsize=14)
>>> ax4.set_ylabel('Number of articles', fontsize=14)

Figure 2-18. Original and scaled news article word counts. Note that only the scale of the x-axis changes; the shape of the distribution stays the same with feature scaling.
在一组输入特征在比例上差异很大的情况下,特征缩放非常有用。例如,一个流行的电子商务网站的每日访问者数量可能是十万,而实际销售额可能是几千。如果这两种功能都投入到模型中,那么该模型需要在确定要做什么的同时*衡它们的规模。输入特征的极大变化会导致模型训练算法的数值稳定性问题。在这些情况下,标准化功能是个好主意。第 4 章将详细介绍处理自然文本时的特征缩放,包括使用示例。
交互特征
简单的成对交互特征是两个特征的积。类似逻辑与。它以成对条件表示结果:“购买来自邮政编码 98121”和“用户的年龄在 18 到 35 之间”。这一点对基于决策树的模型没有影响,但发交互特征对广义线性模型通常很有帮助。
一个简单的线性模型使用单个输入特征线性组合 x1,x2,... xn 来预测结果 y

一个简单的扩展线性模型的方法是包含输入特征对的组合,如下所示:

这使我们能够捕获特征之间的相互影响,因此它们被称为交互特征。如果 x1 和 x2 是二元的,那么它们的积 x1x2 是逻辑函数 x1 AND x2 假设问题是根据他或她的个人资料信息预测客户的偏好。在这种情况下,交互特征不是仅基于用户的年龄或位置进行预测,而交互特征允许模型基于具有特定年龄和特定位置的用户进行预测。
在例 2-17 中,我们使用 UCI 在线新闻数据集中的成对交互特征来预测每篇新闻文章的分享数量。交互特征导致精度超过单身特征。两者都比例 2-9 表现得更好,它使用文章正文中单词数的单个预测器(有或没有经过对数变换)。
例子 2--17。用于预测的交互特征示例。
>>> from sklearn import linear_model
>>> from sklearn.model_selection import train_test_split
>>> import sklearn.preprocessing as preproc
### Assume df is a Pandas dataframe containing the UCI online news dataset
>>> df.columns
Index(['url', 'timedelta', 'n_tokens_title', 'n_tokens_content',
'n_unique_tokens', 'n_non_stop_words', 'n_non_stop_unique_tokens',
'num_hrefs', 'num_self_hrefs', 'num_imgs', 'num_videos',
'average_token_length', 'num_keywords', 'data_channel_is_lifestyle',
'data_channel_is_entertainment', 'data_channel_is_bus',
'data_channel_is_socmed', 'data_channel_is_tech',
'data_channel_is_world', 'kw_min_min', 'kw_max_min', 'kw_avg_min',
'kw_min_max', 'kw_max_max', 'kw_avg_max', 'kw_min_avg', 'kw_max_avg',
'kw_avg_avg', 'self_reference_min_shares', 'self_reference_max_shares',
'self_reference_avg_sharess', 'weekday_is_monday', 'weekday_is_tuesday',
'weekday_is_wednesday', 'weekday_is_thursday', 'weekday_is_friday',
'weekday_is_saturday', 'weekday_is_sunday', 'is_weekend', 'LDA_00',
'LDA_01', 'LDA_02', 'LDA_03', 'LDA_04', 'global_subjectivity',
'global_sentiment_polarity', 'global_rate_positive_words',
'global_rate_negative_words', 'rate_positive_words',
'rate_negative_words', 'avg_positive_polarity', 'min_positive_polarity',
'max_positive_polarity', 'avg_negative_polarity',
'min_negative_polarity', 'max_negative_polarity', 'title_subjectivity',
'title_sentiment_polarity', 'abs_title_subjectivity',
'abs_title_sentiment_polarity', 'shares'],
dtype='object')
### Select the content-based features as singleton features in the model,
### skipping over the derived features
features = ['n_tokens_title', 'n_tokens_content',
'n_unique_tokens', 'n_non_stop_words', 'n_non_stop_unique_tokens',
'num_hrefs', 'num_self_hrefs', 'num_imgs', 'num_videos',
'average_token_length', 'num_keywords', 'data_channel_is_lifestyle',
'data_channel_is_entertainment', 'data_channel_is_bus',
'data_channel_is_socmed', 'data_channel_is_tech',
'data_channel_is_world']
>>> X = df[features]
>>> y = df[['shares']]
### Create pairwise interaction features, skipping the constant bias term
>>> X2 = preproc.PolynomialFeatures(include_bias=False).fit_transform(X)
>>> X2.shape
(39644, 170)
### Create train/test sets for both feature sets
>>> X1_train, X1_test, X2_train, X2_test, y_train, y_test = train_test_split(X, X2, y, test_size=0.3, random_state=123)
>>> def evaluate_feature(X_train, X_test, y_train, y_test):
... '''Fit a linear regression model on the training set and score on the test set'''
... model = linear_model.LinearRegression().fit(X_train, y_train)
... r_score = model.score(X_test, y_test)
... return (model, r_score)
### Train models and compare score on the two feature sets
>>> (m1, r1) = evaluate_feature(X1_train, X1_test, y_train, y_test)
>>> (m2, r2) = evaluate_feature(X2_train, X2_test, y_train, y_test)
>>> print("R-squared score with singleton features: %0.5f" % r1)
>>> print("R-squared score with pairwise features: %0.10f" % r2)
R-squared score with singleton features: 0.00924
R-squared score with pairwise features: 0.0113276523
构造交互特征非常简单,但它们使用起来很昂贵。使用成对交互特征的线性模型的训练和得分时间将从 O(n)到 O(n2),其中 n 是单身特征的数量。
围绕高阶交互特征的计算成本有几种方法。可以在所有交互特征之上执行特征选择,选择前几个。或者,可以更仔细地制作更少数量的复杂特征。两种策略都有其优点和缺点。特征选择采用计算手段来选择问题的最佳特征。(这种技术不限于交互特征。)一些特征选择技术仍然需要训练多个具有大量特征的模型。
手工制作的复杂特征可以具有足够的表现力,所以只需要少量这些特征,这可以缩短模型的训练时间。但是特征本身的计算可能很昂贵,这增加了模型评分阶段的计算成本。手工制作(或机器学习)的复杂特征的好例子可以在第 8 章有关图像特征中找到。现在让我们看看一些特征选择技巧。
特征选择
特征选择技术会删除非有用的特征,以降低最终模型的复杂性。最终目标是快速计算的简约模型,预测准确性降低很小或不会降低。为了得到这样的模型,一些特征选择技术需要训练多个候选模型。换句话说,特征选择并不是减少训练时间,实际上有些技巧增加了整体训练时间,但是减少了模型评分时间。
粗略地说,特征选择技术分为三类。
Filtering(过滤): 预处理可以删除那些不太可能对模型有用的特征。例如,可以计算每个特征与响应变量之间的相关或相互信息,并筛除相关信息或相互信息低于阈值的特征。第 3 章讨论了文本特征的过滤技术的例子。过滤比下面的包装(wrapper)技术便宜得多,但是他们没有考虑到正在使用的模型。因此他们可能无法为模型选择正确的特征。最好先保守地进行预过滤,以免在进行模型训练步骤之前无意中消除有用的特征。
Wrapper methods(包装方法):这些技术是昂贵的,但它们允许您尝试特征子集,这意味着你不会意外删除自身无法提供信息但在组合使用时非常有用的特征。包装方法将模型视为提供特征子集质量分数的黑盒子。shi 一个独立的方法迭代地改进子集。
Embedded methods(嵌入式方法):嵌入式方法执行特征选择作为模型训练过程的一部分。 例如,决策树固有地执行特征选择,因为它在每个训练步骤选择一个要在其上进行树分裂的特征。另一个例子是
正则,它可以添加到任何线性模型的训练目标中。
鼓励模型使用一些特征而不是许多特征。因此它也被称为模型的稀疏约束。嵌入式方法将特征选择作为模型训练过程的一部分。它们不如包装方法那么强大,但也远不如包装方法那么昂贵。与过滤相比,嵌入式方法会选择特定于模型的特征。从这个意义上讲,嵌入式方法在计算费用和结果质量之间取得*衡。
特征选择的全面处理超出了本书的范围。有兴趣的读者可以参考 Isabelle Guyon 和 André Elisseeff 撰写的调查报告“变量和特征选择介绍”(“An Introduction to Variable and Feature Selection”)。
总结
本章讨论了许多常见的数字特征工程技术:量化,缩放(又称规范化),对数变换(一种功率变换),交互特征以及处理大量交互特征所需的特征选择技术的简要总结。在统计机器学习中,所有数据最终归结为数字特征。因此,所有道路最终都会指向某种数字特征工程技术。为了结束特征工程这个游戏,保证这些工具方便使用!
参考书目
Guyon, Isabell, and André Elisseeff. 2003. Journal of Machine Learning Research Special Issue on Variable and Feature Selection. 3(Mar):1157--1182.
Johnston, Jack, and John DiNardo. 1997. Econometric Methods (Fourth Edition). New York: McGraw Hill.
三、文本数据: 展开、过滤和分块
译者:@kkejili
校对者:@HeYun
如果让你来设计一个算法来分析以下段落,你会怎么做?
Emma knocked on the door. No answer. She knocked again and waited. There was a large maple tree next to the house. Emma looked up the tree and saw a giant raven perched at the treetop. Under the afternoon sun, the raven gleamed magnificently. Its beak was hard and pointed, its claws sharp and strong. It looked regal and imposing. It reigned the tree it stood on. The raven was looking straight at Emma with its beady black eyes. Emma felt slightly intimidated. She took a step back from the door and tentatively said, “hello?”
该段包含很多信息。我们知道它谈到了到一个名叫 Emma 的人和一只乌鸦。这里有一座房子和一棵树,艾玛正想进屋,却看到了乌鸦。这只华丽的乌鸦注意到艾玛,她有点害怕,但正在尝试交流。
那么,这些信息的哪些部分是我们应该提取的显着特征?首先,提取主要角色艾玛和乌鸦的名字似乎是个好主意。接下来,注意房子,门和树的布置可能也很好。关于乌鸦的描述呢?Emma 的行为呢,敲门,退后一步,打招呼呢?
本章介绍文本特征工程的基础知识。我们从词袋(bags of words)开始,这是基于字数统计的最简单的文本功能。一个非常相关的变换是 tf-idf,它本质上是一种特征缩放技术。它将被我在(下一篇)章节进行全面讨论。本章首先讨论文本特征提取,然后讨论如何过滤和清洗这些特征。
Bag of X:把自然文本变成*面向量
无论是构建机器学习模型还是特征工程,其结果应该是通俗易懂的。简单的事情很容易尝试,可解释的特征和模型相比于复杂的更易于调试。简单和可解释的功能并不总是会得到最精确的模型。但从简单开始就是一个好主意,仅在绝对必要时我们可以增加其复杂性。
对于文本数据,我们可以从称为 BOW 的字数统计开始。字数统计表中并没有特别费力来寻找"Emma"或乌鸦这样有趣的实体。但是这两个词在该段落中被重复提到,并且它们在这里的计数比诸如"hello"之类的随机词更高。对于此类简单的文档分类任务,字数统计通常比较适用。它也可用于信息检索,其目标是检索与输入文本相关的文档集。这两个任务都很好解释词级特征,因为某些特定词的存在可能是本文档主题内容的重要指标。
词袋
在词袋特征中,文本文档被转换成向量。(向量只是 n 个数字的集合。)向量包含词汇表中每个单词可能出现的数目。 如果单词"aardvark"在文档中出现三次,则该特征向量在与该单词对应的位置上的计数为 3。 如果词汇表中的单词没有出现在文档中,则计数为零。 例如,“这是一只小狗,它是非常可爱”的句子具有如图所示的 BOW 表示
图 3-1 转换词成向量描述图
BOW 将文本文档转换为*面向量。 它是“*面的”,因为它不包含任何原始的文本结构。 原文是一系列词语。但是词袋向量并没有序列;它只是记得每个单词在文本中出现多少次。 它不代表任何词层次结构的概念。 例如,“动物”的概念包括“狗”,“猫”,“乌鸦”等。但是在一个词袋表示中,这些词都是矢量的相同元素。
图 3-2 两个等效的词向量,向量中单词的排序不重要,只要它在数据集中的个数和文档中出现数量是一致的。
重要的是特征空间中数据的几何形状。 在一个词袋矢量中,每个单词成为矢量的一个维度。如果词汇表中有 n 个单词,则文档将成为 n 维空间中的一个点。 很难想象二维或三维以外的任何物体的几何形状,所以我们必须使用我们的想象力。 图 3-3 显示了我们的例句在对应于“小狗”和“可爱”两个维度的特征空间中的样子。
图 3-3 特征空间中文本文档的图示
图 3-4 三维特征空间
图 3-3 和图 3-4 描绘了特征空间中的数据向量。 坐标轴表示单个单词,它们是词袋表示下的特征,空间中的点表示数据点(文本文档)。 有时在数据空间中查看特征向量也是有益的。 特征向量包含每个数据点中特征的值。 轴表示单个数据点和点表示特征向量。 图 3-5 展示了一个例子。 通过对文本文档进行词袋特征化,一个特征是一个词,一个特征向量包含每个文档中这个词的计数。 这样,一个单词被表示为一个“一个词向量”。正如我们将在第 4 章中看到的那样,这些文档词向量来自词袋向量的转置矩阵。
Bag-of-N-gram
Bag-of-N-gram 或者 bag-of-ngram 是 BOW 的自然延伸。 n-gram 是 n 个有序的记号(token)。一个词基本上是一个 1-gram,也被称为一元模型。当它被标记后,计数机制可以将单个词进行计数,或将重叠序列计数为 n-gram。例如,"Emma knocked on the door"这句话会产生 n-gram,如"Emma knocked","knocked on","on the","the door"。
N-gram 保留了文本的更多原始序列结构,故 bag-of-ngram 可以提供更多信息。但是,这是有代价的。理论上,用 k 个独特的词,可能有 k 个独立的 2-gram(也称为 bigram)。在实践中,并不是那么多,因为不是每个单词后都可以跟一个单词。尽管如此,通常有更多不同的 n-gram(n > 1)比单词更多。这意味着词袋会更大并且有稀疏的特征空间。这也意味着 n-gram 计算,存储和建模的成本会变高。n 越大,信息越丰富,成本越高。
为了说明随着 n 增加 n-gram 的数量如何增加,我们来计算纽约时报文章数据集上的 n-gram。我们使用 Pandas 和 scikit-learn 中的CountVectorizer转换器来计算前 10,000 条评论的 n-gram。
>>> import pandas
>>> import json
>>> from sklearn.feature_extraction.text import CountVectorizer
# Load the first 10,000 reviews
>>> f = open('data/yelp/v6/yelp_dataset_challenge_academic_dataset/yelp_academic_dataset_review.json')
>>> js = []
>>> for i in range(10000):
... js.append(json.loads(f.readline()))
>>> f.close()
>>> review_df = pd.DataFrame(js)
# Create feature transformers for unigram, bigram, and trigram.
# The default ignores single-character words, which is useful in practice because it trims
# uninformative words. But we explicitly include them in this example for illustration purposes.
>>> bow_converter = CountVectorizer(token_pattern='(?u)\\b\\w+\\b')
>>> bigram_converter = CountVectorizer(ngram_range=(2,2), token_pattern='(?u)\\b\\w+\\b')
>>> trigram_converter = CountVectorizer(ngram_range=(3,3), token_pattern='(?u)\\b\\w+\\b')
# Fit the transformers and look at vocabulary size
>>> bow_converter.fit(review_df['text'])
>>> words = bow_converter.get_feature_names()
>>> bigram_converter.fit(review_df['text'])
>>> bigram = bigram_converter.get_feature_names()
>>> trigram_converter.fit(review_df['text'])
>>> trigram = trigram_converter.get_feature_names()
>>> print (len(words), len(bigram), len(trigram))
26047 346301 847545
# Sneak a peek at the ngram themselves
>>> words[:10]
['0', '00', '000', '0002', '00am', '00ish', '00pm', '01', '01am', '02']
>>> bigram[-10:]
['zucchinis at',
'zucchinis took',
'zucchinis we',
'zuma over',
'zuppa di',
'zuppa toscana',
'zuppe di',
'zurich and',
'zz top',
'à la']
>>> trigram[:10]
['0 10 definitely',
'0 2 also',
'0 25 per',
'0 3 miles',
'0 30 a',
'0 30 everything',
'0 30 lb',
'0 35 tip',
'0 5 curry',
'0 5 pork']
图 3-6 Number of unique n-gram in the first 10,000 reviews of the Yelp dataset
过滤清洗特征
我们如何清晰地将信号从噪声中分离出来? 通过过滤,使用原始标记化和计数来生成简单词表或 n-gram 列表的技术变得更加可用。 短语检测,我们将在下面讨论,可以看作是一个特别的 bigram 过滤器。 以下是执行过滤的几种方法。
停用词
分类和检索通常不需要对文本有深入的理解。 例如,在"Emma knocked on the door"一句中,"on"和"the"这两个词没有包含很多信息。 代词、冠词和介词大部分时间并没有显示出其价值。流行的 Python NLP 软件包 NLTK 包含许多语言的语言学家定义的停用词列表。 (您将需要安装 NLTK 并运行nltk.download()来获取所有的好东西。)各种停用词列表也可以在网上找到。 例如,这里有一些来自英语停用词的示例词
Sample words from the nltk stopword list
a, about, above, am, an, been, didn’t, couldn’t, i’d, i’ll, itself, let’s, myself, our, they, through, when’s, whom, ...
请注意,该列表包含撇号,并且这些单词没有大写。 为了按原样使用它,标记化过程不得去掉撇号,并且这些词需要转换为小写。
基于频率的过滤
停用词表是一种去除空洞特征常用词的方法。还有其他更统计的方法来理解“常用词”的概念。在搭配提取中,我们看到依赖于手动定义的方法,以及使用统计的方法。同样的想法也适用于文字过滤。我们也可以使用频率统计。
高频词
频率统计对滤除语料库专用常用词以及通用停用词很有用。例如,纽约时报文章数据集中经常出现“纽约时报”和其中单个单词。“议院”这个词经常出现在加拿大议会辩论的 Hansard 语料库中的“众议院”一词中,这是一种用于统计机器翻译的流行数据集,因为它包含所有文档的英文和法文版本。这些词在普通语言中有意义,但不在语料库中。手动定义的停用词列表将捕获一般停用词,但不是语料库特定的停用词。
表 3-1 列出了 Yelp 评论数据集中最常用的 40 个单词。在这里,频率被认为是它们出现在文件(评论)中的数量,而不是它们在文件中的数量。正如我们所看到的,该列表涵盖了许多停用词。它也包含一些惊喜。"s"和"t"在列表中,因为我们使用撇号作为标记化分隔符,并且诸如"Mary's"或"did not"之类的词被解析为"Mary s"和"didn t"。词"good","food"和"great"分别出现在三分之一的评论中。但我们可能希望保留它们,因为它们对于情感分析或业务分类非常有用。
最常用的单词最可以揭示问题,并突出显示通常有用的单词通常在该语料库中曾出现过多次。 例如,纽约时报语料库中最常见的词是“时代”。实际上,它有助于将基于频率的过滤与停用词列表结合起来。还有一个棘手的问题,即何处放置截止点。 不幸的是这里没有统一的答案。在大多数情况下截断还需手动确定,并且在数据集改变时可能需要重新检查。
稀有词
根据任务的不同,可能还需要筛选出稀有词。对于统计模型而言,仅出现在一个或两个文档中的单词更像噪声而非有用信息。例如,假设任务是根据他们的 Yelp 评论对企业进行分类,并且单个评论包含"gobbledygook"这个词。基于这一个词,我们将如何说明这家企业是餐厅,美容院还是一间酒吧?即使我们知道在这种情况下的这种生意发生在酒吧,它也会对于其他包含"gobbledygook"这个词的评论来说,这可能是一个错误。
不仅稀有词不可靠,而且还会产生计算开销。这套 160 万个 Yelp 评论包含 357,481 个独特单词(用空格和标点符号表示),其中 189,915 只出现在一次评论中,41,162 次出现在两次评论中。超过 60% 的词汇很少发生。这是一种所谓的重尾分布,在现实世界的数据中非常普遍。许多统计机器学习模型的训练时间随着特征数量线性地变化,并且一些模型是二次的或更差的。稀有词汇会产生大量的计算和存储成本,而不会带来额外的收益。
根据字数统计,可以很容易地识别和修剪稀有词。或者,他们的计数可以汇总到一个特殊的垃圾箱中,可以作为附加功能。图 3-7 展示了一个短文档中的表示形式,该短文档包含一些常用单词和两个稀有词"gobbledygook"和"zylophant"。通常单词保留自己的计数,可以通过停用词列表或其他频率进一步过滤方法。这些难得的单词会失去他们的身份并被分组到垃圾桶功能中.
由于在计算整个语料库之前不会知道哪些词很少,因此需要收集垃圾桶功能作为后处理步骤。
由于本书是关于特征工程的,因此我们将重点放在特征上。但稀有概念也适用于数据点。如果文本文档很短,那么它可能不包含有用的信息,并且在训练模型时不应使用该信息。
应用此规则时必须谨慎。维基百科转储包含许多不完整的存根,可能安全过滤。另一方面,推文本身就很短,并且需要其他特征和建模技巧。
词干解析(Stemming)
简单解析的一个问题是同一个单词的不同变体会被计算为单独的单词。例如,"flower"和"flowers"在技术上是不同的记号,"swimmer","swimming"和"swim"也是如此,尽管它们的含义非常接*。如果所有这些不同的变体都映射到同一个单词,那将会很好。
词干解析是一项 NLP 任务,试图将单词切分为基本的语言词干形式。有不同的方法。有些基于语言规则,其他基于观察统计。被称为词形化的算法的一个子类将词性标注和语言规则结合起来。
Porter stemmer 是英语中使用最广泛的免费词干工具。原来的程序是用 ANSI C 编写的,但是很多其他程序包已经封装它来提供对其他语言的访问。尽管其他语言的努力正在进行,但大多数词干工具专注于英语。
以下是通过 NLTK Python 包运行 Porter stemmer 的示例。正如我们所看到的,它处理了大量的情况,包括将"sixties"和"sixty"转变为同一根"sixti"。但这并不完美。单词"goes"映射到"goe",而"go"映射到它自己。
>>> import nltk
>>> stemmer = nltk.stem.porter.PorterStemmer()
>>> stemmer.stem('flowers')
u'lemon'
>>> stemmer.stem('zeroes')
u'zero'
>>> stemmer.stem('stemmer')
u'stem'
>>> stemmer.stem('sixties')
u'sixti'
>>> stemmer.stem('sixty')
u'sixty'
>>> stemmer.stem('goes')
u'goe'
>>> stemmer.stem('go')
u'go'
词干解析的确有一个计算成本。 最终收益是否大于成本取决于应用程序。
含义的原子:从单词到 N-gram 到短语
词袋的概念很简单。但是,一台电脑怎么知道一个词是什么?文本文档以数字形式表示为一个字符串,基本上是一系列字符。也可能会遇到 JSON blob 或 HTML 页面形式的半结构化文本。但即使添加了标签和结构,基本单位仍然是一个字符串。如何将字符串转换为一系列的单词?这涉及解析和标记化的任务,我们将在下面讨论。
解析和分词
当字符串包含的不仅仅是纯文本时,解析是必要的。例如,如果原始数据是网页,电子邮件或某种类型的日志,则它包含额外的结构。人们需要决定如何处理日志中的标记,页眉,页脚或无趣的部分。如果文档是网页,则解析器需要处理 URL。如果是电子邮件,则可能需要特殊字段,例如 From,To 和 Subject 需要被特别处理,否则,这些标题将作为最终计数中的普通单词统计,这可能没有用处。
解析后,文档的纯文本部分可以通过标记。这将字符串(一系列字符)转换为一系列记号。然后可以将每个记号计为一个单词。分词器需要知道哪些字符表示一个记号已经结束,另一个正在开始。空格字符通常是好的分隔符,正如标点符号一样。如果文本包含推文,则不应将井号(#)用作分隔符(也称为分隔符)。
有时,分析需要使用句子而不是整个文档。例如,n-gram 是一个句子的概括,不应超出句子范围。更复杂的文本特征化方法,如 word2vec 也适用于句子或段落。在这些情况下,需要首先将文档解析为句子,然后将每个句子进一步标记为单词。
字符串对象
字符串对象有各种编码,如 ASCII 或 Unicode。纯英文文本可以用 ASCII 编码。 一般语言需要 Unicode。 如果文档包含非 ASCII 字符,则确保分词器可以处理该特定编码。否则,结果将不正确。
短语检测的搭配提取
连续的记号能立即被转化成词表和 n-gram。但从语义上讲,我们更习惯于理解短语,而不是 n-gram。在计算自然语言处理中,有用短语的概念被称为搭配。用 Manning 和 Schütze(1999:141)的话来说:“搭配是一个由两个或两个以上单词组成的表达,它们对应于某种常规的说话方式。”
搭配比其部分的总和更有意义。例如,"strong tea"具有超越"great physical strength"和"tea"的不同含义,因此被认为是搭配。另一方面,“可爱的小狗”这个短语恰恰意味着它的部分总和:“可爱”和“小狗”。因此,它不被视为搭配。
搭配不一定是连续的序列。"Emma knocked on the door"一词被认为包含搭配"knock door",因此不是每一个搭配都是一个 n-gram。相反,并不是每个 n-gram 都被认为是一个有意义的搭配。
由于搭配不仅仅是其部分的总和,它们的含义也不能通过单个单词计数来充分表达。作为一种表现形式,词袋不足。袋子的 ngram 也是有问题的,因为它们捕获了太多无意义的序列(考虑"this is in the bag-of-ngram example"),而没有足够的有意义的序列。
搭配作为功能很有用。但是,如何从文本中发现并提取它们呢?一种方法是预先定义它们。如果我们努力尝试,我们可能会找到各种语言的全面成语列表,我们可以通过文本查看任何匹配。这将是非常昂贵的,但它会工作。如果语料库是非常特定领域的并且包含深奥的术语,那么这可能是首选的方法。但是这个列表需要大量的手动管理,并且需要不断更新语料库。例如,分析推文,博客和文章可能不太现实。
自从统计 NLP 过去二十年出现以来,人们越来越多地选择用于查找短语的统计方法。统计搭配提取方法不是建立固定的短语和惯用语言列表,而是依赖不断发展的数据来揭示当今流行的语言。
基于频率的方法
一个简单的黑魔法是频繁发生的 n-gram。这种方法的问题是最常发生的,这种可能不是最有用的。 表 3-2 显示了整个 Yelp 评论数据集中最流行的 bigram(n=2)。 正如我们所知的,按文件计数排列的最常见的十大常见术语是非常通用的术语,并不包含太多含义。
用于搭配提取的假设检验
原始流行度计数(Raw popularity count)是一个比较粗糙的方法。我们必须找到更聪慧的统计数据才能够轻松挑选出有意义的短语。关键的想法是看两个单词是否经常出现在一起。回答这个问题的统计机制被称为假设检验。
假设检验是将噪音数据归结为“是”或“否”的答案。它涉及将数据建模为从随机分布中抽取的样本。随机性意味着人们永远无法 100% 的确定答案;总会有异常的机会。所以答案附在概率上。例如,假设检验的结果可能是“这两个数据集来自同一分布,其概率为 95%”。对于假设检验的温和介绍,请参阅可汗学院关于假设检验和 p 值的教程。
在搭配提取的背景下,多年来已经提出了许多假设检验。最成功的方法之一是基于似然比检验(Dunning,1993)。对于给定的一对单词,该方法测试两个假设观察的数据集。假设 1(原假设)表示,词语 1 独立于词语 2 出现。另一种说法是说,看到词语 1 对我们是否看到词语 2 没有影响。假设 2(备选假设)说,看到词 1 改变了看到单词 2 的可能性。我们采用备选假设来暗示这两个单词形成一个共同的短语。因此,短语检测(也称为搭配提取)的似然比检验提出了以下问题:给定文本语料库中观察到的单词出现更可能是从两个单词彼此独立出现的模型中生成的,或者模型中两个词的概率纠缠?
这是有用的。让我们算一点。(数学非常精确和简洁地表达事物,但它确实需要与自然语言完全不同的分析器。)
似然函数L(Data; H)表示在单词对的独立模型或非独立模型下观察数据集中词频的概率。为了计算这个概率,我们必须对如何生成数据做出另一个假设。最简单的数据生成模型是二项模型,其中对于数据集中的每个单词,我们抛出一个硬币,并且如果硬币朝上出现,我们插入我们的特殊单词,否则插入其他单词。在此策略下,特殊词的出现次数遵循二项分布。二项分布完全由词的总数,词的出现次数和词首概率决定。
似然比检验分析常用短语的算法收益如下。
- 计算所有单体词的出现概率:
p(w)。 - 计算所有唯一双元的条件成对词发生概率:
p(W2 × W1) - 计算所有唯一的双对数似然比对数。
- 根据它们的似然比排序双字节。
- 以最小似然比值作为特征。
掌握似然比测试
关键在于测试比较的不是概率参数本身,而是在这些参数(以及假设的数据生成模型)下观察数据的概率。可能性是统计学习的关键原则之一。但是在你看到它的前几次,这绝对是一个令人困惑的问题。一旦你确定了逻辑,它就变得直观了。
还有另一种基于点互信息的统计方法。但它对真实世界文本语料库中常见的罕见词很敏感。因此它不常用,我们不会在这里展示它。
请注意,搭配抽取的所有统计方法,无论是使用原始频率,假设测试还是点对点互信息,都是通过过滤候选词组列表来进行操作的。生成这种清单的最简单和最便宜的方法是计算 n-gram。它可能产生不连续的序列,但是它们计算成本颇高。在实践中,即使是连续 n-gram,人们也很少超过 bi-gram 或 tri-gram,因为即使在过滤之后,它们的数量也很多。为了生成更长的短语,还有其他方法,如分块或与词性标注相结合。
分块(Chunking)和词性标注(part-of-Speech Tagging)
分块比 n-gram 要复杂一点,因为它基于词性,基于规则的模型形成了记号序列。
例如,我们可能最感兴趣的是在问题中找到所有名词短语,其中文本的实体,主题最为有趣。 为了找到这个,我们使用词性标记每个作品,然后检查该标记的邻域以查找词性分组或“块”。 定义单词到词类的模型通常是语言特定的。 几种开源 Python 库(如 NLTK,Spacy 和 TextBlob)具有多种语言模型。
为了说明 Python 中的几个库如何使用词性标注非常简单地进行分块,我们再次使用 Yelp 评论数据集。 我们将使用 spacy 和 TextBlob 来评估词类以找到名词短语。
>>> import pandas as pd
>>> import json
# Load the first 10 reviews
>>> f = open('data/yelp/v6/yelp_dataset_challenge_academic_dataset/yelp_academic_dataset_review.json')
>>> js = []
>>> for i in range(10):
js.append(json.loads(f.readline()))
>>> f.close()
>>> review_df = pd.DataFrame(js)
## First we'll walk through spaCy's functions
>>> import spacy
# preload the language model
>>> nlp = spacy.load('en')
# We can create a Pandas Series of spaCy nlp variables
>>> doc_df = review_df['text'].apply(nlp)
# spaCy gives you fine grained parts of speech using: (.pos_)
# and coarse grained parts of speech using: (.tag_)
>>> for doc in doc_df[4]:
print([doc.text, doc.pos_, doc.tag_])
Got VERB VBP
a DET DT
letter NOUN NN
in ADP IN
the DET DT
mail NOUN NN
last ADJ JJ
week NOUN NN
that ADJ WDT
said VERB VBD
Dr. PROPN NNP
Goldberg PROPN NNP
is VERB VBZ
moving VERB VBG
to ADP IN
Arizona PROPN NNP
to PART TO
take VERB VB
a DET DT
new ADJ JJ
position NOUN NN
there ADV RB
in ADP IN
June PROPN NNP
. PUNCT .
SPACE SP
He PRON PRP
will VERB MD
be VERB VB
missed VERB VBN
very ADV RB
much ADV RB
. PUNCT .
SPACE SP
I PRON PRP
think VERB VBP
finding VERB VBG
a DET DT
new ADJ JJ
doctor NOUN NN
in ADP IN
NYC PROPN NNP
that ADP IN
you PRON PRP
actually ADV RB
like INTJ UH
might VERB MD
almost ADV RB
be VERB VB
as ADV RB
awful ADJ JJ
as ADP IN
trying VERB VBG
to PART TO
find VERB VB
a DET DT
date NOUN NN
! PUNCT .
# spaCy also does some basic noun chunking for us
>>> print([chunk for chunk in doc_df[4].noun_chunks])
[a letter, the mail, Dr. Goldberg, Arizona, a new position, June, He, I, a new doctor, NYC, you, a date]
#####
## We can do the same feature transformations using Textblob
>>> from textblob import TextBlob
# The default tagger in TextBlob uses the PatternTagger, which is fine for our example.
# You can also specify the NLTK tagger, which works better for incomplete sentences.
>>> blob_df = review_df['text'].apply(TextBlob)
>>> blob_df[4].tags
[('Got', 'NNP'),
('a', 'DT'),
('letter', 'NN'),
('in', 'IN'),
('the', 'DT'),
('mail', 'NN'),
('last', 'JJ'),
('week', 'NN'),
('that', 'WDT'),
('said', 'VBD'),
('Dr.', 'NNP'),
('Goldberg', 'NNP'),
('is', 'VBZ'),
('moving', 'VBG'),
('to', 'TO'),
('Arizona', 'NNP'),
('to', 'TO'),
('take', 'VB'),
('a', 'DT'),
('new', 'JJ'),
('position', 'NN'),
('there', 'RB'),
('in', 'IN'),
('June', 'NNP'),
('He', 'PRP'),
('will', 'MD'),
('be', 'VB'),
('missed', 'VBN'),
('very', 'RB'),
('much', 'JJ'),
('I', 'PRP'),
('think', 'VBP'),
('finding', 'VBG'),
('a', 'DT'),
('new', 'JJ'),
('doctor', 'NN'),
('in', 'IN'),
('NYC', 'NNP'),
('that', 'IN'),
('you', 'PRP'),
('actually', 'RB'),
('like', 'IN'),
('might', 'MD'),
('almost', 'RB'),
('be', 'VB'),
('as', 'RB'),
('awful', 'JJ'),
('as', 'IN'),
('trying', 'VBG'),
('to', 'TO'),
('find', 'VB'),
('a', 'DT'),
('date', 'NN')]
>>> print([np for np in blob_df[4].noun_phrases])
['got', 'goldberg', 'arizona', 'new position', 'june', 'new doctor', 'nyc'
你可以看到每个库找到的名词短语有些不同。spacy 包含英语中的常见单词,如"a"和"the",而 TextBlob 则删除这些单词。这反映了规则引擎的差异,它驱使每个库都认为是“名词短语”。 你也可以写你的词性关系来定义你正在寻找的块。使用 Python 进行自然语言处理可以深入了解从头开始用 Python 进行分块。
总结
词袋模型易于理解和计算,对分类和搜索任务很有用。但有时单个单词太简单,不足以将文本中的某些信息封装起来。为了解决这个问题,人们寄希望于比较长的序列。Bag-of-ngram 是 BOW 的自然概括,这个概念仍然容于理解,而且它的计算开销这就像 BOW 一样容易。
Bag of-ngram 生成更多不同的 ngram。它增加了特征存储成本,以及模型训练和预测阶段的计算成本。虽然数据点的数量保持不变,但特征空间的维度现在更大。因此数据密度更为稀疏。n 越高,存储和计算成本越高,数据越稀疏。由于这些原因,较长的 n-gram 并不总是会使模型精度的得到提高(或任何其他性能指标)。人们通常在n = 2或 3 时停止。较少的 n-gram 很少被使用。
防止稀疏性和成本增加的一种方法是过滤 n-gram 并保留最有意义的短语。这是搭配抽取的目标。理论上,搭配(或短语)可以在文本中形成非连续的标记序列。然而,在实践中,寻找非连续词组的计算成本要高得多并且没有太多的收益。因此搭配抽取通常从一个候选人名单中开始,并利用统计方法对他们进行过滤。
所有这些方法都将一系列文本标记转换为一组断开的计数。与一个序列相比,一个集合的结构要少得多;他们导致*面特征向量。
在本章中,我们用简单的语言描述文本特征化技术。这些技术将一段充满丰富语义结构的自然语言文本转化为一个简单的*面向量。我们讨论一些常用的过滤技术来降低向量维度。我们还引入了 ngram 和搭配抽取作为方法,在*面向量中添加更多的结构。下一章将详细介绍另一种常见的文本特征化技巧,称为 tf-idf。随后的章节将讨论更多方法将结构添加回*面向量。
参考文献
Dunning, Ted. 1993. “Accurate methods for the statistics of surprise and
coincidence.” ACM Journal of Computational Linguistics, special issue on using large corpora , 19:1 (61—74).
“Hypothesis Testing and p-Values.” Khan Academy, accessed May 31,
Manning,Christopher D. and Hinrich Schütze. 1999. Foundations of StatisticalNatural Language Processing . Cambridge, Massachusettes: MIT Press.
Sometimes people call it the document “vector.” The vector extends from the original and ends at the specified point. For our purposes, “vector” and “point” are the same thing.
四、特征缩放的效果:从词袋到 TF-IDF
译者:@gin
校对者:@HeYun
字袋易于生成,但远非完美。假设我们*等的统计所有单词,有些不需要的词也会被强调。在第三章提过一个例子,Emma and the raven。我们希望在文档表示中能强调两个主要角色。示例中,“Eama”和“raven”都出现了 3 词,但是“the”的出现高达 8 次,“and”出现了次,另外“it”以及“was”也都出现了 4 词。仅仅通过简单的频率统计,两个主要角色并不突出。这是有问题的。
其他的像是“magnificently,” “gleamed,” “intimidated,” “tentatively,” 和“reigned,”这些辅助奠定段落基调的词也是很好的选择。它们表示情绪,这对数据科学家来说可能是非常有价值的信息。 所以,理想情况下,我们会倾向突出对有意义单词的表示。
Tf-Idf: 词袋的小转折
Tf-Idf 是词袋的一个小小的转折。它表示词频-逆文档频。tf-idf 不是查看每个文档中每个单词的原始计数,而是查看每个单词计数除以出现该单词的文档数量的标准化计数。

N 代表数据集中所有文档的数量。分数
就是所谓的逆文件频率。如果一个单词出现在许多文档中,则其逆文档频率接* 1。如果单词出现在较少文档中,则逆文档频率要高得多。
或者,我们可以对原始逆文档频率进行对数转换,可以将 1 变为 0,并使得较大的数字(比 1 大得多)变小。(稍后更多内容)
如果我们定义 tf-idf 为:

那么每个文档中出现的单词都将被有效清零,并且只出现在少数文档中的单词的计数将比以前更大。
让我们看一些图片来了解它的具体内容。图 4-1 展示了一个包含 4 个句子的简单样例:“it is a puppy,” “it is a cat,” “it is a kitten,” 以及 “that is a dog and this is a pen.” 我们将这些句子绘制在“puppy”,“cat”以及“is”三个词的特征空间上。

图 4-1: 关于猫和狗的四个句子
现在让我们看看对逆文档频进行对数变换之后,相同四个句子的 tf-idf 表示。 图 4-2 显示了相应特征空间中的文档。可以注意到,单词“is”被有效地消除,因为它出现在该数据集中的所有句子中。另外,单词“puppy”和“cat”都只出现在四个句子中的一个句子中,所以现在这两个词计数得比之前更高(log(4)=1.38...>1)。因此 tf-idf 使罕见词语更加突出,并有效地忽略了常见词汇。它与第 3 章中基于频率的滤波方法密切相关,但比放置严格截止阈值更具数学优雅性。

Figure 4-2: 图 4-1 中四个句子的 Tf-idf 表示
Tf-Idf 的含义
Tf-idf 使罕见的单词更加突出,并有效地忽略了常见单词。
测试
Tf-idf 通过乘以一个常量来转换字数统计特性。因此,它是特征缩放的一个例子,这是第 2 章介绍的一个概念。特征缩放在实践中效果有多好? 我们来比较简单文本分类任务中缩放和未缩放特征的表现。 coding 时间到!
本次实践, 我们依旧采用了 Yelp 评论数据集。Yelp 数据集挑战赛第 6 轮包含在美国六个城市将*一百六十万商业评论。
样例 4-1:使用 python 加载和清洗 Yelp 评论数据集
import json
import pandas as pd
## Load Yelp Business data
biz_f =
open('data/yelp/v6/yelp_dataset_challenge_academic_dataset/yelp_academic_datase
ess.json')
biz_df = pd.DataFrame([json.loads(x) for x in biz_f.readlines()])
biz_f.close()
## Load Yelp Reviews data
review_file = open('data/yelp/v6/yelp_dataset_challenge_academic_dataset/yelp_academic_dataset_review.json')
review_df = pd.DataFrame([json.loads(x) for x in review_file.readlines()])
review_file.close()
# Pull out only Nightlife and Restaurants businesses
two_biz = biz_df[biz_df.apply(lambda x: 'Nightlife' in x['categories']
or 'Restaurants' in x['categories'],axis=1)]
# Join with the reviews to get all reviews on the two types of business
twobiz_reviews = two_biz.merge(review_df, on='business_id', how='inner')
# Trim away the features we won't use
twobiz_reviews = twobiz_reviews[['business_id',
'name',
'stars_y',
'text',
'categories']]
# Create the target column--True for Nightlife businesses, and False otherwise
two_biz_reviews['target'] = twobiz_reviews.apply(lambda x: 'Nightlife' in
x['categories'],axis=1)
建立分类数据集
让我们看看是否可以使用评论来区分餐厅或夜生活场所。为了节省训练时间,仅使用一部分评论。这两个类别之间的评论数目有很大差异。这是所谓的类不*衡数据集。对于构建模型来说,不*衡的数据集存在着一个问题:这个模型会把大部分精力花费在比重更大的类上。由于我们在这两个类别都有大量的数据,解决这个问题的一个比较好方法是将数目较大的类(餐厅)进行下采样,使之与数目较小的类(夜生活)数目大致相同。下面是一个示例工作流程。
-
随机抽取 10%夜生活场所评论以及 2.1%的餐厅评论(选取合适的百分比使得每个种类的数目大致一样)
-
将数据集分成比例为 7:3 的训练集和测试集。在这个例子里,训练集包括 29,264 条评论,测试集有 12542 条。
-
训练数据包括 46,924 个不同的单词,这是词袋表示中特征的数量。
样例 4-2:创建一个分类数据集
# Create a class-balanced subsample to play with
nightlife = twobiz_reviews[twobiz_reviews.apply(lambda x: 'Nightlife' in
x['categories'], axis=1)]
restaurants = twobiz_reviews[twobiz_reviews.apply(lambda x: 'Restaurants' in
x['categories'], axis=1)]
nightlife_subset = nightlife.sample(frac=0.1, random_state=123)
restaurant_subset = restaurants.sample(frac=0.021, random_state=123)
combined = pd.concat([nightlife_subset, restaurant_subset])
# Split into training and test data sets
training_data, test_data = modsel.train_test_split(combined,
train_size=0.7,
random_state=123)
training_data.shape
# (29264, 5)
test_data.shape
# (12542, 5)
用 tf-idf 转换缩放词袋
这个实验的目标是比较词袋,tf-idf 以及 L2 归一化对于线性分类的作用。注意,做 tf-idf 接着做 L2 归一化和单独做 L2 归一化是一样的。所以我们需要只需要 3 个特征集合:词袋,tf-idf,以及逐词进行 L2 归一化后的词袋。
在这个例子中,我们将使用 Scikit-learn 的 CountVectorizer 将评论文本转化为词袋。所有的文本特征化方法都依赖于标记器(tokenizer),该标记器能够将文本字符串转换为标记(词)列表。在这个例子中,Scikit-learn 的默认标记模式是查找 2 个或更多字母数字字符的序列。标点符号被视为标记分隔符。
测试集上进行特征缩放
特征缩放的一个细微之处是它需要了解我们在实践中很可能不知道的特征统计,例如均值,方差,文档频率,L2 范数等。为了计算 tf-idf 表示,我们不得不根据训练数据计算逆文档频率,并使用这些统计量来调整训练和测试数据。在 Scikit-learn 中,将特征变换拟合到训练集上相当于收集相关统计数据。然后可以将拟合过的变换应用于测试数据。
样例 4-3:特征变换
# Represent the review text as a bag-of-words
bow_transform = text.CountVectorizer()
X_tr_bow = bow_transform.fit_transform(training_data['text'])
X_te_bow = bow_transform.transform(test_data['text'])
len(bow_transform.vocabulary_)
# 46924
y_tr = training_data['target']
y_te = test_data['target']
# Create the tf-idf representation using the bag-of-words matrix
tfidf_trfm = text.TfidfTransformer(norm=None)
X_tr_tfidf = tfidf_trfm.fit_transform(X_tr_bow)
X_te_tfidf = tfidf_trfm.transform(X_te_bow)
# Just for kicks, l2-normalize the bag-of-words representation
X_tr_l2 = preproc.normalize(X_tr_bow, axis=0)
X_te_l2 = preproc.normalize(X_te_bow, axis=0)
当我们使用训练统计来衡量测试数据时,结果看起来有点模糊。测试集上的最小-最大比例缩放不再整齐地映射到零和一。L2 范数,*均数和方差统计数据都将显得有些偏离。这比缺少数据的问题好一点。例如,测试集可能包含训练数据中不存在的单词,并且对于新的单词没有相应的文档频。通常的解决方案是简单地将测试集中新的单词丢弃。这似乎是不负责任的,但训练集上的模型在任何情况下都不会知道如何处理新词。一种稍微不太好的方法是明确地学习一个“垃圾”单词,并将所有罕见的频率单词映射到它,即使在训练集中也是如此,正如“罕见词汇”中所讨论的那样。
使用逻辑回归进行分类
逻辑回归是一个简单的线性分类器。通过对输入特征的加权组合,输入到一个 sigmoid 函数。sigmoid 函数将任何实数*滑的映射到介于 0 和 1 之间。如图 4-3 绘制 sigmoid 函数曲线。由于逻辑回归比较简单,因此它通常是最先接触的分类器。

Figure 4-3: sigmoid 函数
图 4-3 是 sigmoid 函数的插图。该函数将输入的实数 x 转换为一个 0 到 1 之间的数。它有一组参数 w,表示围绕中点 0.5 增加的斜率。截距项 b 表示函数输出穿过中点的输入值。如果 sigmoid 输出大于 0.5,则逻辑分类器将预测为正例,否则为反例。通过改变 w 和 b,可以控制决策的改变,以及决策响应该点周围输入值变化的速度。
样例 4-4:使用默认参数训练逻辑回归分类器
def simple_logistic_classify(X_tr, y_tr, X_test, y_test, description):
## Helper function to train a logistic classifier and score on test data
m = LogisticRegression().fit(X_tr, y_tr)
s = m.score(X_test, y_test)
print ('Test score with', description, 'features:', s)
return m
m1 = simple_logistic_classify(X_tr_bow, y_tr, X_te_bow, y_te, 'bow')
m2 = simple_logistic_classify(X_tr_l2, y_tr, X_te_l2, y_te, 'l2-normalized')
m3 = simple_logistic_classify(X_tr_tfidf, y_tr, X_te_tfidf, y_te, 'tf-idf')
# Test score with bow features: 0.775873066497
# Test score with l2-normalized features: 0.763514590974
# Test score with tf-idf features: 0.743182905438
矛盾的是,结果表明最准确的分类器是使用 BOW 特征的分类器。出乎意料我们之外。事实证明,造成这种情况的原因是没有很好地“调整”分类器,这是比较分类器时一个常见的错误。
使用正则化调整逻辑回归
逻辑回归有些华而不实。 当特征的数量大于数据点的数量时,找到最佳模型的问题被认为是欠定的。 解决这个问题的一种方法是在训练过程中增加额外的约束条件。 这就是所谓的正则化,技术细节将在下一节讨论。
逻辑回归的大多数实现允许正则化。为了使用这个功能,必须指定一个正则化参数。正则化参数是在模型训练过程中未自动学习的超参数。相反,他们必须手动进行调整,并将其提供给训练算法。这个过程称为超参数调整。(有关如何评估机器学习模型的详细信息,请参阅评估机器学习模型(Evaluating Machine Learning Models)).调整超参数的一种基本方法称为网格搜索:指定一个超参数值网格,并且调谐器以编程方式在网格中搜索最佳超参数设置 格。 找到最佳超参数设置后,使用该设置对整个训练集进行训练,并比较测试集上这些同类最佳模型的性能。
重点:比较模型时调整超参数
比较模型或特征时,调整超参数非常重要。 软件包的默认设置将始终返回一个模型。 但是除非软件在底层进行自动调整,否则很可能会返回一个基于次优超参数设置的次优模型。 分类器性能对超参数设置的敏感性取决于模型和训练数据的分布。 逻辑回归对超参数设置相对稳健(或不敏感)。 即便如此,仍然有必要找到并使用正确的超参数范围。 否则,一个模型相对于另一个模型的优点可能仅仅是由于参数的调整,并不能反映模型或特征的实际表现。
即使是最好的自动调整软件包仍然需要指定搜索的上限和下限,并且找到这些限制可能需要几次手动尝试。
在本例中,我们手动将逻辑正则化参数的搜索网格设置为{1e-5,0.001,0.1,1,10,100}。 上限和下限花费了几次尝试来缩小范围。 表 4-1 给出了每个特征集合的最优超参数设置。
Table4-1.对夜场和餐厅的 Yelp 评论进行逻辑回归的最佳参数设置
| Method | L2 Regularization |
|---|---|
| BOW | 0.1 |
| L2-normalized | 10 |
| TF-IDF | 0.01 |
我们也想测试 tf-idf 和 BOW 之间的精度差异是否是由于噪声造成的。 为此,我们使用 k 折交叉验证来模拟具有多个统计独立的数据集。它将数据集分为 k 个折叠。交叉验证过程通过分割后的数据进行迭代,使用除除去某一折之外的所有内容进行训练,并用那一折验证结果。Scikit-Learn 中的 GridSearchCV 功能通过交叉验证进行网格搜索。 图 4-4 显示了在每个特征集上训练的模型的精度测量分布箱线图。 盒子中线表示中位精度,盒子本身表示四分之一和四分之三分位之间的区域,而线则延伸到剩余的分布。
通过重采样估计方差
现代统计方法假设底层数据是随机分布的。 数据导出模型的性能测量也受到随机噪声的影响。 在这种情况下,基于相似数据的数据集,不止一次进行测量总是比较好的。 这给了我们一个测量的置信区间。 K 折交叉验证就是这样一种策略。 重采样是另一种从相同底层数据集生成多个小样本的技术。 有关重采样的更多详细信息,请参见评估机器学习模型。
样例 4-5:使用网格搜索调整逻辑回归超参数
>>> import sklearn.model_selection as modsel# Specify a search grid, then do 5-fold grid search for each of the feature sets
>>> param_grid_ = {'C': [1e-5, 1e-3, 1e-1, 1e0, 1e1, 1e2]# Tune classifier for bag-of-words representation
>>> bow_search = modsel.GridSearchCV(LogisticRegression(), cv=5,
param_grid=param_grid_)
>>> bow_search.fit(X_tr_bow, y_tr)# Tune classifier for L2-normalized word vector
>>> l2_search = modsel.GridSearchCV(LogisticRegression(), cv=5,
param_grid=param_grid_)
>>> l2_search.fit(X_tr_l2, y_tr)# Tune classifier for tf-idf
>>> tfidf_search = modsel.GridSearchCV(LogisticRegression(), cv=5,
param_grid=param_grid_)
>>> tfidf_search.fit(X_tr_tfidf, y_tr)
# Let's check out one of the grid search outputs to see how it went
>>> bow_search.cv_results_{'mean_fit_time':
array([ 0.43648252, 0.94630651, 5.64090128, 15.31248307,
31.47010217, 42.44257565]),
'mean_score_time':
array([ 0.00080056, 0.00392466, 0.00864897, 0.00784755, 0.01192751,
0.0072515 ]),
'mean_test_score':
array([ 0.57897075, 0.7518111 , 0.78283898, 0.77381766, 0.75515992,
0.73937261]),
'mean_train_score':
array([ 0.5792185 , 0.76731652, 0.87697341, 0.94629064, 0.98357195,
0.99441294]),
'param_C': masked_array(data = [1e-05 0.001 0.1 1.0 10.0 100.0],
mask = [False False False False False False],
fill_value = ?),
'params': ({'C': 1e-05},
{'C': 0.001},
{'C': 0.1},
{'C': 1.0},
{'C': 10.0},
{'C': 100.0}),
'rank_test_score': array([6, 4, 1, 2, 3, 5]),
'split0_test_score':
array([ 0.58028698, 0.75025624, 0.7799795 , 0.7726341 , 0.75247694,
0.74086095]),
'split0_train_score':
array([ 0.57923964, 0.76860316, 0.87560871, 0.94434003, 0.9819308 ,
0.99470312]),
'split1_test_score':
array([ 0.5786776 , 0.74628396, 0.77669571, 0.76627371, 0.74867589,
0.73176149]),
'split1_train_score':
array([ 0.57917218, 0.7684849 , 0.87945837, 0.94822946, 0.98504976,
0.99538678]),
'split2_test_score':
array([ 0.57816504, 0.75533914, 0.78472578, 0.76832394, 0.74799248,
0.7356911 ]),
'split2_train_score':
array([ 0.57977019, 0.76613558, 0.87689548, 0.94566657, 0.98368288,
0.99397719]),
'split3_test_score':
array([ 0.57894737, 0.75051265, 0.78332194, 0.77682843, 0.75768968,
0.73855092]),
'split3_train_score':
array([ 0.57914745, 0.76678626, 0.87634546, 0.94558346, 0.98385443,
0.99474628]),
'split4_test_score':
array([ 0.57877649, 0.75666439, 0.78947368, 0.78503076, 0.76896787,
0.75 ]),
'split4_train_score':
array([ 0.57876303, 0.7665727 , 0.87655903, 0.94763369, 0.98334188,
0.99325132]),
'std_fit_time':
array([ 0.03874582, 0.02297261, 1.18862097, 1.83901079, 4.21516797,
2.93444269]),
'std_score_time':
array([ 0.00160112, 0.00605009, 0.00623053, 0.00698687, 0.00713112,
0.00570195]),
'std_test_score':
array([ 0.00070799, 0.00375907, 0.00432957, 0.00668246, 0.00771557,
0.00612049]),
'std_train_score':
array([ 0.00032232, 0.00102466, 0.00131222, 0.00143229, 0.00100223,
0.00073252])}
########
# Plot the cross validation results in a box-and-whiskers plot to
# visualize and compare classifier performance
########
>>> search_results = pd.DataFrame.from_dict({'bow':
bow_search.cv_results_['mean_test_score'],
'tfidf':
tfidf_search.cv_results_['mean_test_score'],
'l2':
l2_search.cv_results_['mean_test_score']})
# Our usual matplotlib incantations. Seaborn is used here to make
# the plot pretty
>>> import matplotlib.pyplot as plt
>>> import seaborn as sns
>>> sns.set_style("whitegrid")
>>> ax = sns.boxplot(data=search_results, width=0.4)
>>> ax.set_ylabel('Accuracy', size=14)
>>> ax.tick_params(labelsize=14)

Figure 4-4: 分类器精度在每个特征集和正则化设置下的分布。 准确度是以 5 折交叉验证的*均准确度来衡量的
Table4-2.每个超参数设置的*均交叉验证分类器准确度。 星号表示最高精度
| Regularization Parameter | BOW | normalized | Tf-idf |
|---|---|---|---|
| 0.00001 | 0.578971 | 0.575724 | 0.721638 |
| 0.001 | 0.751811 | 0.575724 | 0.788648* |
| 0.1 | 0.782839* | 0.589120 | 0.763566 |
| 1 | 0.773818 | 0.734247 | 0.741150 |
| 10 | 0.755160 | 0.776756* | 0.721467 |
| 100 | 0.739373 | 0.761106 | 0.712309 |
在图 4-4 中,L2 归一化后的特征结果看起来非常糟糕。 但不要被蒙蔽了 。准确率低是由于正则化参数设置不恰当造成的 - 实际证明次优超参数会得到相当错误的结论。 如果我们使用每个特征集的最佳超参数设置来训练模型,则不同特征集的测试精度非常接*。
示例 4-6:最终的训练和测试步骤来比较不同的特征集
# Train a final model on the entire training set, using the best hyperparamete
# settings found previously. Measure accuracy on the test set.
>>> m1 = simple_logistic_classify(X_tr_bow, y_tr, X_te_bow, y_te, 'bow', _C=bow_search.best_params_['C'])
>>> m2 = simple_logistic_classify(X_tr_l2, y_tr, X_te_l2, y_te, 'l2-normalized', _C=l2_search.best_params_['C'])
>>> m3 = simple_logistic_classify(X_tr_tfidf, y_tr, X_te_tfidf, y_te, 'tf-idf', _C=tfidf_search.best_params_['C'])
Test score with bow features: 0.78360708021
Test score with l2-normalized features: 0.780178599904
Test score with tf-idf features: 0.788470738319
Table4-3.BOW, Tf-Idf,以及 L2 正则化的最终分类精度
| Feature Set | Test Accuracy |
|---|---|
| Bag-of-Words | 0.78360708021 |
| L2 -normalized | 0.780178599904 |
| Tf-Idf | 0.788470738319 |
适当的调整提高了所有特征集的准确性,并且所有特征集在正则化后进行逻辑回归得到了相*的准确率。tf-idf 模型准确率略高,但这点差异可能没有统计学意义。 这些结果是完全神秘的。 如果特征缩放效果不如 vanilla 词袋的效果好,那为什么要这么做呢? 如果 tf-idf 没有做任何事情,为什么总是要这么折腾? 我们将在本章的其余部分中探索答案。
深入:发生了什么?
为了明白结果背后隐含着什么,我们必须考虑模型是如何使用特征的。对于类似逻辑回归这种线性模型来说,是通过所谓的数据矩阵的中间对象来实现的。
数据矩阵包含以固定长度*面向量表示的数据点。 根据词袋向量,数据矩阵也被称为文档词汇矩阵。 图 3-1 显示了一个向量形式的词袋向量,图 4-1 显示了特征空间中的四个词袋向量。 要形成文档词汇矩阵,只需将文档向量取出,*放,然后将它们堆叠在一起。 这些列表示词汇表中所有可能的单词。 由于大多数文档只包含所有可能单词的一小部分,因此该矩阵中的大多数都是零,是一个稀疏矩阵。

Figure 4-5: 包含 5 个文档 7 个单词的文档-词汇矩阵
特征缩放方法本质上是对数据矩阵的列操作。特别的,tf-idf 和 L2 归一化都将整列(例如 n-gram 特征)乘上一个常数。
Tf-idf=列缩放
Tf-idf 和 L2 归一化都是数据矩阵上的列操作。 正如附录 A 所讨论的那样,训练线性分类器归结为寻找最佳的线性组合特征,这是数据矩阵的列向量。 解空间的特征是列空间和数据矩阵的空间。训练过的线性分类器的质量直接取决于数据矩阵的零空间和列空间。 大的列空间意味着特征之间几乎没有线性相关性,这通常是好的。 零空间包含“新”数据点,不能将其表示为现有数据的线性组合; 大的零空间可能会有问题。(强烈建议希望对诸如线性决策表面,特征分解和矩阵的基本子空间等概念进行的回顾的读者阅读附录 A。)
列缩放操作如何影响数据矩阵的列空间和空间? 答案是“不是很多”。但是在 tf-idf 和 L2 归一化之间有一个小小的差别。
由于几个原因,数据矩阵的零空间可能很大。 首先,许多数据集包含彼此非常相似的数据点。 这使得有效的行空间与数据集中数据的数量相比较小。 其次,特征的数量可以远大于数据的数量。 词袋特别擅长创造巨大的特征空间。 在我们的 Yelp 例子中,训练集中有 29K 条评论,但有 47K 条特征。 而且,不同单词的数量通常随着数据集中文档的数量而增长。 因此,添加更多的文档不一定会降低特征与数据比率或减少零空间。
在词袋模型中,与特征数量相比,列空间相对较小。 在相同的文档中可能会出现数目大致相同的词,相应的列向量几乎是线性相关的,这导致列空间不像它可能的那样满秩。 这就是所谓的秩亏。 (就像动物缺乏维生素和矿物质一样,矩阵秩亏,输出空间也不会像应该那样蓬松)。
秩亏行空间和列空间导致模型空间预留过度的问题。 线性模型为数据集中的每个特征配置权重参数。 如果行和列空间满秩\(^1\),那么该模型将允许我们在输出空间中生成任何目标向量。 当模型不满秩时,模型的自由度比需要的更大。 这使得找出解决方案变得更加棘手。
可以通过特征缩放来解决数据矩阵的不满秩问题吗? 让我们来看看。
列空间被定义为所有列向量的线性组合:
。比方说,特征缩放用一个常数倍来替换一个列向量,
。但是我们仍然可以通过用
来替换
,生成原始的线性组合。看起来,特征缩放不会改变列空间的秩。类似地,特征缩放不会影响空间的秩,因为可以通过反比例缩放权重向量中的对应条目来抵消缩放的特征列。
但是,仍然存在一个陷阱。 如果标量为 0,则无法恢复原始线性组合;
消失了。 如果该向量与所有其他列线性无关,那么我们已经有效地缩小了列空间并放大了零空间。
如果该向量与目标输出不相关,那么这将有效地修剪掉噪声信号,这是一件好事。 这是 tf-idf 和 L2 归一化之间的关键区别。 L2 归一化永远不会计算零的范数,除非该向量包含全零。 如果向量接*零,那么它的范数也接*于零。 按照小规范划分将突出向量并使其变大。
另一方面,如图 4-2 所示,Tf-idf 可以生成接*零的缩放因子。 当这个词出现在训练集中的大量文档中时,会发生这种情况。 这样的话有可能与目标向量没有很强的相关性。 修剪它可以使模型专注于列空间中的其他方向并找到更好的解决方案。 准确度的提高可能不会很大,因为很少有噪声方向可以通过这种方式修剪。
在特征缩放的情况下,L2 和 tf-idf 对于模型的收敛速度确实有促进。 这是该数据矩阵有一个更小的条件数的标志。 事实上,L2 归一化使得条件数几乎一致。 但情况并非条件数越多,解决方案越好。 在这个实验中,L2 归一化收敛比 BOW 或 tf-idf 快得多。 但它对过拟合也更敏感:它需要更多的正则化,并且对优化期间的迭代次数更敏感。
总结
在本章中,我们使用 tf-idf 作为入口点,详细分析特征变换如何影响(或不)模型。Tf-idf 是特征缩放的一个例子,所以我们将它的性能与另一个特征缩放方法-L2 标准化进行了对比。
结果并不如预期。Tf-idf 和 L2 归一化不会提高最终分类器的准确度,而不会超出纯词袋。 在获得了一些统计建模和线性代数处理知识之后,我们意识到了为什么:他们都没有改变数据矩阵的列空间。
两者之间的一个小区别是,tf-idf 可以“拉伸”字数以及“压缩”它。 换句话说,它使一些数字更大,其他数字更接*
归零。 因此,tf-idf 可以完全消除无意义的单词。
我们还发现了另一个特征缩放效果:它改善了数据矩阵的条件数,使线性模型的训练速度更快。 L2 标准化和 tf-idf 都有这种效果。
总而言之,正确的特征缩放可以有助于分类。 正确的缩放突出了信息性词语,并降低了常见单词的权重。 它还可以改善数据矩阵的条件数。 正确的缩放并不一定是统一的列缩放。
这个故事很好地说明了在一般情况下分析特征工程的影响的难度。 更改特征会影响训练过程和随后的模型。 线性模型是容易理解的模型。 然而,它仍然需要非常谨慎的实验方法和大量的深刻的数学知识来区分理论和实际的影响。 对于更复杂的模型或特征转换来说,这是不可能的。
参考书目
Strang, Gilbert. 2006. Linear Algebra and Its Applications. Brooks Cole Cengage, fourth edition.
\(^1\) 严格地说,矩阵矩阵的行空间和列空间不能都是满秩的。 两个子空间的最大秩是 m(行数)和 n(列数)中的较小者。 这就是我们所说的满秩。
五、类别特征:机器鸡时代的鸡蛋计数
译者:@ZhenLeiXu
一个类别特征,见名思义,就是用来表达一种类别或标签。比如,一个类别特征能够表达世界上的主要城市,一年四季,或者说一个公司的产品(石油、路程、技术)。在真实世界的数据集中,类别值的数量总是无限的。同时这些值一般可以用数值来表示。但是,与其他数值变量不一样的是,类别特征的数值变量无法与其他数值变量进行比较大小。(作为行业类型,石油与旅行无法进行比较)它们被称之为非序的。
一个简单的问题可以作为测试是否应该是一个分类变量的试金石测试:“两个价值有多么不同,或者只是它们不同?”500 美元的股票价格比 100 美元的价格高 5 倍。 所以股票价格应该用一个连续的数字变量表示。 另一方面,公司的产业(石油,旅游,技术等)应该无法被比较的,也就是类别特征。
大的分类变量在交易记录中特别常见。 对于实例中,许多 Web 服务使用 id 作为分类变量来跟踪用户具有数百至数百万的值,取决于唯一的数量服务的用户。 互联网交易的 IP 地址是另一个例子一个很大的分类变量。 它们是分类变量,因为即使用户 ID 和 IP 地址是数字,它们的大小通常与任务无关在眼前。 例如,在进行欺诈检测时,IP 地址可能是相关的个人交易。 某些 IP 地址或子网可能会产生更多欺骗性交易比其他人。 但是 164.203.x.x 的子网本质上并不多欺诈性比 164.202.x.x; 子网的数值无关紧要。
文档语料库的词汇可以被解释为一个大的分类变量,类别是唯一的单词。 它可能在计算上很昂贵代表如此多的不同类别。 如果一个类别(例如,单词)出现多个数据点(文档)中的时间,然后我们可以将它表示为一个计数并表示所有的类别通过他们的统计数字。 这被称为 bin-counting。 我们用分类变量的共同表示开始讨论,并且最终蜿蜒曲折地讨论了大范围的 bin-counting 问题变量,这在现代数据集中非常普遍。
对类别特征进行编码
分类变量的类别通常不是数字。例如,眼睛的颜色可以是“黑色”,“蓝色”,“棕色”等。因此,需要使用编码方法将这些非数字类别变为数字。 简单地将一个整数(比如 1 到 k)分配给 k 个可能的类别中的每一个都是诱人的。 但是,由此产生的价值观可以互相授权,这在类别中不应该被允许。
One-hot 编码
将类别特征进行表示一个最好的办法就是使用一组比特位来表达。每一位代表一个可能的类别。 如果该变量不能一次成为多个类别,那么该组中只有一位可以是 1。 这被称为独热编码,它在 Scikit Learn 中实现sklearn.preprocessing.OneHotEncoder。 每个位都是一个特征。 因此是一个绝对的具有 k 个可能类别的变量被编码为长度为 k 的特征向量。
表 5-1 对 3 个城市的类别进行独热编码
| City | e1 | e2 | e3 |
|---|---|---|---|
| San Francisco | 1 | 0 | 0 |
| New York | 0 | 1 | 0 |
| Seattle | 0 | 0 | 1 |
独热编码非常易于理解。 但它使用的是比严格必要的更多的一点。 如果我们看到 k-1 位是零,那么最后一位必须是 1,因为变量必须具有 k 个值中的一个。 在数学上,可以写下这个约束条件为“所有位的和必须等于 1”。
等式 5-1. 独热编码e1,e2,e3限制条件。
e1+e2+e3+...+ek=1
因此,我们有一个线性的依赖性。 线性相关特征,就像我们一样在tfidf中发现,有点烦人,因为它意味着训练线性模型不会是唯一的。 特征的不同线性组合可以做出同样的预测,所以我们需要跳过额外条件的来理解特征对预测的影响。
dummy 编码
独热编码的问题是它允许 k 个自由度,其中变量本身只需要 k-1。 虚拟编码通过仅使用表示中的 k-1 个特征来消除额外的自由度。 公共汽车下面有一个特征,由全零矢量表示。 这被称为参考类别。 虚拟编码和独热编码都是在 Pandas 中以pandas.get_dummies的形式实现的。
表 5-2 对 3 个城市的类别进行 dummy 编码
| City | e1 | e2 |
|---|---|---|
| San Francisco | 1 | 0 |
| New York | 0 | 1 |
| Seattle | 0 | 0 |
使用虚拟编码进行建模的结果比单编码更易解释。这很容易在简单的线性回归问题中看到。 假设我们有一些数据关于三个城市的公寓租赁价格:旧金山,纽约和西雅图。(见表 5-3)
表 5-3 三个不同城市的公寓价格数据集
| id | city | Rent |
|---|---|---|
| 0 | SF | 3999 |
| 1 | SF | 4000 |
| 2 | SF | 4001 |
| 3 | NYC | 3499 |
| 4 | NYC | 3500 |
| 5 | NYC | 3501 |
| 6 | Seattle | 2499 |
| 7 | Seattle | 2500 |
| 8 | Seattle | 2501 |

图 5-1 公寓租金价格在 one-hot 编码中的向量空间表示。点的大小表达了数据集中租金不同价格的*均数。
我们这时能够仅仅依靠城市这一个变量来建立线性回归来预测租金的价格。
线性回归模型可以这样写
y=w1x1+w2x2+w3x3+...+wnxn
习惯上我们还添加一个常量来,这样的话当 x 全部为 0,y 不会为 0.
例 5-1.在独热编码上的线性回归
import pandas as pd
from sklearn import linear_model
df = pd.DataFrame({'City': ['SF', 'SF', 'SF', 'NYC', 'NYC', 'NYC','Seattle', 'Seattle', 'Seattle'],
'Rent': [3999, 4000, 4001, 3499, 3500, 3501, 2499,2500,2501]})
>>> df['Rent'].mean()
3333.3333333333335
one_hot_df = pd.get_dummies(df, prefix=['city'])
>>> one_hot_df
Rent city_NYC city_SF city_Seattle
0 3999 0 1 0
1 4000 0 1 0
2 4001 0 1 0
3 3499 1 0 0
4 3500 1 0 0
5 3501 1 0 0
6 2499 0 0 1
7 2500 0 0 1
8 2501 0 0 1
model = linear_model.LinearRegression()
model.fit(one_hot_df[['city_NYC', 'city_SF', 'city_Seattle']],
one_hot_df[['Rent']])
>>> model.coef_
array([[ 166.66666667, 666.66666667, -833.33333333]])
>>> model.intercept_
array([ 3333.33333333])
使用 dummy code 进行回归
dummy_df = pd.get_dummies(df, prefix=['city'], drop_first=True)
>>> dummy_df
Rent city_SF city_Seattle
0 3999 1 0
1 4000 1 0
2 4001 1 0
3 3499 0 0
4 3500 0 0
5 3501 0 0
6 2499 0 1
7 2500 0 1
8 2501 0 1
>>> model.fit(dummy_df[['city_SF', 'city_Seattle']], dummy_df['Rent'])
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
>>> model.coef_
array([ 500., -1000.])
>>> model.intercept_
3500.0
通过独热编码,截距项表示目标变量的全局均值租金价格,并且每个线性系数表示该城市的*均租金与全局*均值的差异。
通过虚拟编码,偏差系数代表响应的*均值参考类别的变量 y,在这个例子中是纽约市。该第 i 个特征的系数等于*均响应之间的差异第 i 类别的值和参考类别的*均值。
表 5-4:线性回归学得的系数
| id | x1 | x2 | x3 | b |
|---|---|---|---|---|
| one-hot | 166.67 | 666.67 | -833.33 | 3333.33 |
| dummy coding | 0 | 500 | -1000 | 3500 |
Effect 编码
分类变量编码的另一种变体称为 Effect 编码。 Effect 编码与虚拟编码非常相似,区别在于参考类别现在由所有-1 的向量表示。
表 5-5: Effect 编码表示 3 个城市
| City | e1 | e2 |
|---|---|---|
| San Francisco | 1 | 0 |
| New York | 0 | 1 |
| Seattle | -1 | -1 |
Effect 编码与虚拟编码非常相似,但是在线性回归中更容易被拟合。例子 5-2 表达了运行机理。截距项表示目标的全球*均值变量,单个系数表示各个类别的*均值与全球*均值有多少差异。 (这被称为类别或级别的主要效果,因此名称为“效果编码”。)独热编码实际上具有相同的截距和系数,但在这种情况下,每个城市都有线性系数。 在效果编码中,没有单一特征代表参考类别。 因此,参考类别的影响需要分别计算为所有其他类别的系数的负和。(查看what is effect coding?)
例子 5-2 Effect 编码的线性回归
>>> effect_df = dummy_df.copy()
>>> effect_df.ix[3:5, ['city_SF', 'city_Seattle']] = -1.0
>>> effect_df
Rent city_SF city_Seattle
0 3999 1.0 0.0
1 4000 1.0 0.0
2 4001 1.0 0.0
3 3499 -1.0 -1.0
4 3500 -1.0 -1.0
5 3501 -1.0 -1.0
6 2499 0.0 1.0
7 2500 0.0 1.0
8 2501 0.0 1.0
>>> model.fit(effect_df[['city_SF', 'city_Seattle']], effect_df['Rent'])
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=1, normalize=False)
>>> model.coef_
array([ 666.66666667, -833.33333333])
>>> model.intercept_
3333.3333333333335
类别变量的优点和缺点
独热,虚拟和效果编码非常相似。 他们每个人都有优点和缺点。 独热编码是多余的,它允许多个有效模型一样的问题。 非唯一性有时候对解释有问题。该优点是每个特征都明显对应于一个类别。 此外,失踪数据可以编码为全零矢量,输出应该是整体目标变量的*均值。
虚拟编码和效果编码不是多余的。 他们产生独特和可解释的模型。 虚拟编码的缺点是它不能轻易处理缺少数据,因为全零矢量已经映射到参考类别。它还编码每个类别相对于参考类别的影响,其中看起来很奇怪。 效果编码通过使用不同的代码来避免此问题参考类别。 但是,所有-1 的矢量都是一个密集的矢量,对于存储和计算来说都很昂贵。 因此,Pandas 和 Scikit Learn 等流行的 ML 软件包选择了虚拟编码或独热编码,而不是效应编码。当类别数量变得非常多时,所有三种编码技术都会失效大。 需要不同的策略来处理非常大的分类变量。
处理大量的类别特征
互联网上的自动数据收集可以生成大量的分类变量。这在诸如定向广告和欺诈检测等应用中很常见。 在有针对性的广告中,任务是根据用户的搜索查询或当前页面将用户与一组广告进行匹配。 功能包括用户 ID,广告的网站域,搜索查询,当前页面以及这些功能的所有可能的成对连词。 (查询是一个文本字符串,可以切分成常用的文本特征,但查询通常很短,通常由短语组成,因此在这种情况下最好的行为通常是保持完整,或 通过哈希函数来简化存储和比较,我们将在下面更详细地讨论哈希。)其中每一个都是一个非常大的分类变量。 我们面临的挑战是如何找到一个能够提高内存效率的优秀特征表示,并生成训练速度快的准确模型。
对于这种类别特征处理的方案有:
-
对编码不做任何事情。 使用便宜的训练简单模型。 在许多机器上将独热编码引入线性模型(逻辑回归或线性支持向量机)。
-
压缩编码,有两种方式
a. 对特征进行哈希--在线性回归中特别常见
b. bin-counting--在线性回归中与树模型都常见
使用 one-hot 编码是可行的。在微软搜索广告研究中,Graepel 等人 [2010]报告在贝叶斯概率回归模型中使用这种二值特征,可以使用简单更新在线进行培训。 与此同时,其他组织则争论压缩方法。 来自雅虎的研究人员 通过特征散列发誓[Weinberger et al。2009 年]。 尽管 McMahan 等人[2013]在谷歌的广告引擎上尝试了功能哈希,并没有找到显着的改进。 然而,微软的其他人则被认为是计数[Bilenko,2015]。
我们将会看到,所有这些想法都有利有弊。 我们将首先描述解决方案本身,然后讨论他们的权衡。
特征哈希
散列函数是一个确定性函数,它映射一个潜在的无界整数到有限整数范围[1,m]。 由于输入域可能大于输出范围,多个数字可能会映射到相同的输出。 这被称为 a 碰撞。 统一的散列函数可确保大致相同数量的数字被映射到每个 m 箱。 在视觉上,我们可以将散列函数视为一台机器可以吸入编号的球并将它们传送到一个 m 箱。 球与相同的号码将始终被路由到同一个 bin。
散列函数可以为任何可以用数字表示的对象构造(对于可以存储在计算机上的任何数据都是如此):数字,字符串,复杂的结构等。

图 5-2 哈希编码
当有很多特征时,存储特征向量可能占用很多空间。 特征散列将原始特征向量压缩为 m 维通过对特征 ID 应用散列函数来创建矢量。 例如,如果原件特征是文档中的单词,那么散列版本将具有固定的词汇大小为 m,无论输入中有多少独特词汇。
例 5-3 对单词的特征哈希
def hash_features(word_list,m):
output = [0]*m
for word in word_list:
index = hash_fcn(word)%m
output[index] += 1
return output
功能散列的另一个变体添加了一个符号组件,因此计数也是从哈希箱中增加或减少。 这确保了内部产品之间散列特征与原始特征的期望值相同。
def hash_features(word_list,m):
output = [0]*m
for word in word_list:
index = hash_fcn(word)%m
sign_bit = sign_hash(word) % 2
if(sign_bit==0):
output[index] -= 1
else:
output[index] += 1
return output
哈希后内积的值在时间复杂度在O(1/(m**0.5)).所以哈希表 m 的大小可以根据可接受的错误来选择。在实践中,选择合适的 m 可能需要一些试验和错误。特征哈希可以用于涉及特征内积的模型矢量和系数,例如线性模型和核心方法。 它一直证明在垃圾邮件过滤任务中取得成功[Weinberger 等,2009]。在有针对性的广告案例中,McMahan et al。 [2013 年]报告不能将预测误差降低到可接受的水*,除非 m 的数量级为数十亿。散列特征的一个缺点是散列特征是聚合的原始特征,不再可解释。
在这个例子中,我们将使用 Yelp 评论数据集来演示存储和,解释性使用的为 sklearn 的库FeatureHasher。在有针对性的广告案例中,McMahan
import pandas as pd
import json
js = []
with open('yelp_academic_dataset_review.json') as f:
for i in range(10000):
js.append(json.loads(f.readline()))
review_df = pd.DataFrame(js)
m = len(review_df.business_id.unique())
>>>m
4174
In [4]: from sklearn.feature_extraction import FeatureHasher
...:
...: h = FeatureHasher(n_features=m, input_type='string')
...:
...: f = h.transform(review_df['business_id'])
...:
In [5]: review_df['business_id'].unique().tolist()[0:5]
Out[5]:
['9yKzy9PApeiPPOUJEtnvkg',
'ZRJwVLyzEJq1VAihDhYiow',
'6oRAC4uyJCsJl1X0WZpVSA',
'_1QQZuf4zZOyFCvXc0o6Vg',
'6ozycU1RpktNG2-1BroVtw']
In [6]: f.toarray()
Out[6]:
array([[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
...,
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.],
[ 0., 0., 0., ..., 0., 0., 0.]])
我们看看特征的存储
print('Our pandas Series, in bytes: ', getsizeof(review_df['business_id']))
print('Our hashed numpy array, in bytes: ', getsizeof(f))
>>>790104
>>>56
我们可以清楚地看到如何使用特征散列会以计算方式使我们受益,牺牲直接的用户解释能力。 这是一个容易的权衡来接受何时从数据探索和可视化发展到机器学习管道对于大型数据集。
bin-counting
Bin-counting 是机器学习中常见的重新发现之一。 从广告点击率预测到硬件分支预测,它已经被重新创建并用于各种应用[Yeh and Patt,1991; Lee 等人,1998; Pavlov 等,2009; 李等人,2010]。 然而,因为它是一种特征工程技术,而不是一种建模或优化方法,所以没有关于该主题的研究论文。 关于该技术最详细的描述可以在 Misha Bilenko 的博客文章“Big Learning Made with Easy”以及相关的幻灯片中找到。
bin-counting 的想法非常简单:而不是使用分类变量作为特征,而不是使用条件概率的目标在该价值下。 换句话说,而不是编码的身份分类值,计算该值和该值之间的关联统计量我们希望预测的目标。 对于那些熟悉 Na?veBayes 分类器的人来说,这个统计学应该敲响一下钟,因为它是该类的条件概率假设所有功能都是独立的。 最好用一个例。
表 5-6. bin-counting 的例子
| User | Number of clicks | Number of non-clicks | probability of click | QueryHash,AdDomain | Number of clicks | Number of non-clicks | probability of click |
|---|---|---|---|---|---|---|---|
| Alice | 5 | 120 | 0.0400 | 0x598fd4fe,foo.com | 5000 | 30000 | 0.167 |
| bob | 20 | 230 | 0.0800 | 0x50fa3cc0,bar.org | 100,900,0.100 | ||
| ... | |||||||
| joe | 2 | 3 | 0.400 | 0x437a45e1,qux.net | 6,18,0.250 |
Bin-counting 假定历史数据可用于计算统计。 表 5-6 包含分类变量每个可能值的汇总历史计数。 根据用户点击任何广告的次数以及未点击的次数,我们可以计算用户“Alice”点击任何广告的概率。 同样,我们可以计算任何查询 - 广告 - 域组合的点击概率。 在训练时,每当我们看到“爱丽丝”时,都使用她的点击概率作为模型的输入特征。 QueryHash-AdDomain 对也是如此,例如“0x437a45e1,qux.net”。
假设有 10,000 个用户。 独热编码会生成一个稀疏矢量长度为 10,000,在列中对应于值的单个 1 当前数据点。 Bin-counting 将所有 10,000 个二进制列编码为一个功能的真实值介于 0 和 1 之间。
除了历史点击概率外,我们还可以包含其他功能:原始计数本身(点击次数和非点击次数),对数比率或任何其他概率的衍生物。 我们的例子是预测广告点击率,通过率。 但该技术很容易应用于一般的二元分类。 它也可以使用通常的技术容易地扩展到多级分类将二元分类器扩展到多个类,即通过一对多优势比或其他多类标签编码。
Bin-counting 的优势比和对数比
比值比通常定义在两个二元变量之间。 它通过提出这样一个问题来看待他们的联想强度:“当 X 为真时,Y 有多大可能是真的”。例如,我们可能会问,“Alice 点击广告的可能性大于 一般人口?“在这里,X 是二进制变量”是 Alice 是当前用户“,而 Y 是变量”点击广告与否“。 该计算使用所谓的双向列联表(基本上,四个数字对应于 X 和 Y 的四种可能组合)。
表 5-7. 偶然发生的用户点击事件
| click | Non-Click | Total | |
|---|---|---|---|
| Alice | 5 | 120 | 125 |
| Not Alice | 995 | 18880 | 19875 |
| Total | 1000 | 19000 | 20000 |
给定输入变量 X 和目标变量 Y,优势比定义为优势比=(P(Y=1|X=1)/(P(Y=0|X=1))/(P(Y=1|X=0)/(P(Y=0|X=0))))
在我们的例子中,这意味着“爱丽丝点击广告而不是点击的可能性”和“其他人点击而非点击的可能性有多大”之间的比率。在这种情况下,数字是
![img/5-3.jpg)
更简单地说,我们可以看看分子,它检查多少可能性单个用户(Alice)是否点击广告而不是点击。 这适合大型具有许多值的分类变量,而不仅仅是两个。
![img/5-4.jpg)
概率比率可能很容易变得非常小或非常大。 (例如,将会有几乎不会点击广告的用户,也可能是点击广告的用户更频繁得多)日志转换再次来到我们的救援。 另一个对数的有用特性是它将一个划分变为一个减法。
![img/5-5.jpg)
简而言之,bin-counting 将分类变量转换为有关的统计信息值。 它变成了一个大的,稀疏的分类变量的二进制表示变成一个非常小,密集的实值数值表示。
![img/5-6.jpg)
图 5-3 分类变量的独热编码与二进制计数统计的说明。
在实施方面,垃圾箱计数需要在每个类别之间存储地图及其相关计数。 (其余的统计数据可以从中得到原始计数)。因此它需要 O(k)空间,其中 k 是唯一值的数量的分类变量。
我们采用 Kaggle 的比赛Avazu举个例子.
Avazu Click 数据集
- 有 24 个变量,包括'点击',一个二进制点击/不点击计数器和'device_id',用于跟踪显示广告的设备。
- 完整的数据集包含 4,0428,967 个观测值,其中有 2,686,408 个独特的设备。
Avazu 竞赛使用广告数据来预测点击率,但我们将使用它来演示如何 bin 计数可以大大减少大的特征空间流数据量。
例子 5-6 Bin-counting 例子
import pandas as pd
#读取前面的 10k 行
df = pd.read_csv('data/train_subset.csv')
#有多少独立的特征
len(df['device_id'].unique())
#对于每一个类别,我们想计算
# Theta = [counts, p(click), p(no click), p(click)/p(no click)]
def click_counting(x, bin_column):
clicks = pd.Series(x[x['click'] > 0][bin_column].value_counts(),
name='clicks')
no_clicks = pd.Series(x[x['click'] < 1][bin_column].value_counts(),
name='no_clicks')
counts = pd.DataFrame([clicks,no_clicks]).T.fillna('0')
counts['total_clicks'] = counts['clicks'].astype('int64') +
counts['no_clicks'].astype('int64')
return counts
def bin_counting(counts):
counts['N+'] =
counts['clicks'].astype('int64').divide(counts['total_clicks'].astype('int64'))
counts['N-'] =
counts['no_clicks'].astype('int64').divide(counts['total_clicks'].astype('int64'))
counts['log_N+'] = counts['N+'].divide(counts['N-'])
bin_column = 'device_id'
device_clicks = click_counting(df.filter(items= [bin_column, 'click']),
bin_column)
device_all, device_bin_counts = bin_counting(device_clicks)
device_all.sort_values(by = 'total_clicks', ascending=False).head(4)
clicks no_clicks total N+ N- log_N+
a99f214a 15729 71206 86935 0.180928 0.819072 0.220894
c357dbff 33
31da1bd0 0
936e92fb 5
134 167
62 62
54 59
0.197605 0.802395 0.246269
0.000000 1.000000 0.000000
0.084746 0.915254 0.092593
关于稀有类别
就像罕见的词,罕见的类别需要特殊的处理。 想想一个用户每年登录一次:几乎没有数据可以可靠估计她广告的点击率。 而且,稀有类别会在计数表中浪费空间。解决这个问题的一种方法是通过补偿,一种积累的简单技术一个特殊垃圾箱中所有稀有类别的数量。 如果计数大于 a 一定的门槛,那么这个类别就有自己的统计数字。 否则,使用来自回退箱的统计数据。 这基本上会恢复单个的统计信息罕见类别与所有罕见类别的统计数据进行比较。 当使用 back-off 方法,它有助于为统计信息添加二进制指标来自后退箱。
![img/5-7.jpg)
图 5-4
如果罕见类别获得收益,它可以使用自己的统计数据进行建模,从而超过回退库的阈值。
还有另一种方法来处理这个问题,称为 count-min sketch [Cormode 和 Muthukrishnan,2005]。 在这种方法中,所有类别,罕见或频繁类似通过多个散列函数进行映射,输出范围为 m,远小于类别的数量,k。 当检索一个统计量时,计算所有的哈希值该类别,并返回最小的统计量。 拥有多个散列函数减轻单个散列函数内碰撞的可能性。 该计划有效因为可以做出散列函数次数 m,散列表大小小于 k,类别的数量,仍然保持较低的整体碰撞可能性。
![img/5-9.jpg)
由于二进制计数依赖于历史数据来生成必要的统计数据需要通过数据收集期等待,导致了数据收集时间的轻微延迟学习管道。 这也意味着当数据分布发生变化时,计数需要更新。 数据变化越快,计数需要的次数越多重新计算。 这对于目标应用程序尤其重要广告,用户偏好和热门查询变化非常快,而且缺乏适应当前的分布可能意味着广告的巨大损失*台。
有人可能会问,为什么不使用相同的数据集来计算相关统计量并训练模型?这个想法看起来很无辜。这里最大的问题是统计涉及目标变量,这是模型试图预测的。使用输出来计算输入特征会导致一个称为泄漏的有害问题。简而言之,泄漏意味着信息被揭示给模型,从而使它有更好的预测的不切实际的优势。当测试数据泄露到训练集中,或者未来的数据泄漏到过去时,可能会发生这种情况。任何时候都会向模型提供在生产中实时进行预测时应该无法访问的信息,这会导致泄漏。 Kaggle 的维基提供了更多泄漏示例以及为什么它对机器学习应用程序不利。
如果二进制计数程序使用当前数据点的标签来计算输入统计量的一部分,则这构成直接泄漏。防止这种情况的一种方法是在计数收集(用于计算箱计数统计)和训练之间进行严格分离,即使用较早批次的数据点进行计数,将当前数据点用于训练(将分类变量映射到历史统计我们刚刚收集),并使用未来的数据点进行测试。这解决了泄漏问题,但引入了上述延迟(输入统计信息,因此模型将跟踪当前数据)。
事实证明,还有另一种基于差别隐私的解决方案。 如果统计数据的分布保持大致相同或不存在任何一个数据点,则该统计*似是防漏的。 在实践中,增加一个分布拉普拉斯(0,1)的小随机噪声足以掩盖单个数据点的任何潜在泄漏。 这个想法可以结合一次性计算来制定当前数据的统计数据。 Owen Zhang 在他的“赢得数据科学竞赛”的演讲中详细介绍了这个技巧。
Counts without bounds
如果在越来越多的历史数据下统计数据不断更新,原始数量将无限增长。这可能是模型的问题。训练有素的模型“知道”输入数据直至观察到的比例。一个训练有素的决策树可能会说“当 x 大于 3 时,预测为 1”。一个经过训练的线性模型可能会说“乘以 0.7 的多个 x 并查看结果是否大于全局*均值”。这些可能是 x 介于 0 和 5 之间。但是除此之外会发生什么?没有人知道。
当输入计数增加时,模型将需要重新训练以适应当前的比例。如果计数积累得相当缓慢,那么有效量表不会变得太快,并且模型不需要过于频繁地重新训练。但是当计数增加很快时,频繁的再培训将是一个麻烦。
出于这个原因,使用标准化计数通常会更好
以已知的时间间隔为界。例如,估计的点击率是
有界[0,1]之间。另一种方法是采取对数变换,即施加一个
严格的限制,但是当数量非常大时,增加速度会很慢。
这两种方法都不能防止转移投入分布,例如,去年的芭比娃娃现在已经过时,人们将不再点击这些广告。该模型需要重新训练以适应输入数据分布中的这些更根本性的变化,否则整个流程将需要迁移到模型不断适应输入的在线学习环境。
总结
Plain one-hot encoding
空间使用:O(n)
时间复杂度:O(nk)
优点:
- 容易实现
- 更高的精度
- 在线学习特别容易扩展
缺点
- 计算不足
- 如果类别增加则不能够使用
- 对线性模型以外的任何其他方法都不可行
- 对于大数据集需要分布式训练
Feature hashing
空间使用:O(n)
时间复杂度:O(nm)
优点:
- 容易实现
- 容易训练
- 容易扩展到新类别
- 容易处理稀有类别
- 在线学习容易扩展
缺点
- 只能够使用线性或核模型
- 哈希编码很难解释
- 精度有争议
Bin-counting
空间使用:O(n+k)
时间复杂度:O(n)
优点:
- 训练快
- 能够使用树模型
- 容易扩展到新列类别
- 容易处理稀有类别
- 可解释
缺点
- 需要利用历史信息
- 对于在线学习有困难
- 会有数据泄露
正如我们所看到的,没有任何方法是完美的。 选择使用哪一个取决于所需的型号。 线性模型比较便宜,因此可以进行训练处理非压缩表示,例如独热编码。 基于树的模型,另一方面,需要反复搜索右侧分割的所有特征,并且是因此限于小型表示,如箱计数。 功能哈希处于在这两个极端之间,但是由此产生的精确度有不同的报道。
六、降维:用 PCA 压缩数据集
译者:@cn-Wziv
校对者:@HeYun
通过自动数据收集和特征生成技术,可以快速获得大量特征,但并非所有这些都有用。在第 3 章和
在第 4 章中,我们讨论了基于频率的滤波和特征缩放修剪无信息的特征。现在我们来仔细讨论一下使用主成分分析(PCA)进行数据降维。
本章标志着进入基于模型的特征工程技术。在这之前,大多数技术可以在不参考数据的情况下定义。对于实例中,基于频率的过滤可能会说“删除所有小于n的计数“,这个程序可以在没有进一步输入的情况下进行数据本身。 另一方面,基于模型的技术则需要来自数据的信息。例如,PCA 是围绕数据的主轴定义的。 在之前的技术中,数据,功能和模型之间从来没有明确的界限。从这一点前进,差异变得越来越模糊。这正是目前关于特征学习研究的兴奋之处。
引言
降维是关于摆脱“无信息的信息”的同时保留关键点。有很多方法可以定义“无信息”。PCA 侧重于线性依赖的概念。在“矩阵的剖析”中,我们将数据矩阵的列空间描述为所有特征向量的跨度。如果列空间与特征的总数相比较小,则大多数特征是几个关键特征的线性组合。如果在下一步管道是一个线性模型,然后线性相关的特征会浪费空间和计算能力。为了避免这种情况,主成分分析尝试去通过将数据压缩成更低维的线性来减少这种“绒毛”子空间。
在特征空间中绘制一组数据点。每个数据点都是一个点,整个数据点集合形成一个 blob。在图 6-1(a) 中,数据点在两个特征维度上均匀分布,blob 填充空间。在这个示例中,列空间具有完整的等级。但是,如果其中一些特征是其他特征的线性组合,那么该 blob 看起来不会那么丰满; 它看起来更像图 6-1(b),这是一个*面斑点,其中特征 1 是特征 2 的重复(或标量倍数)。在这种情况下,我们说该 blob 的本征维数是 1,即使它位于二维空间之中。
在实践中,事情很少完全相同。这更可能是我们看到非常接**等但不完全相同的特征。在这种情况下,数据 blob 可能如图 6-1(c) 所示。这是一个憔悴的一团。要是我们想要减少传递给模型的特征的数量,那么我们可以用一个新特征替换特征 1 和特征 2,可能称之为位于两个特征之间的对线的 1.5 特征。原始数据集可以是用一个数字充分表示——沿着特征方 1.5 的方向——而不是两个维度f1和f2。

图 6-1 特征空间中的数据 blobs(a) 满秩数据 blob(b) 低维数据 blob(c) *似低维的数据 blob
这里的关键思想是用一些充分总结原始特征空间中包含的信息的新特征取代冗余特征。当只有两个特征的时候新特征很容易得到。这在当原始特征空间具有数百或数千个维度时将变得很难。我们需要一种数学描述我们正在寻找的新功能的方法。这样我们就可以使用优化技术来找到它们。
数学上定义“充分总结信息”的一种方法要求就是这样说新数据 blob 应该保留尽可能多的原来的列。我们是将数据块压扁成*坦的数据饼,但我们希望数据饼尽可能在正确的方向上。这意味着我们需要一种衡量特征列的方法。特征列与距离有关。但是在一些数据点中距离的概念有些模糊。可以测量任意两对之间的最大距离点。但事实证明,这是一个非常困难的数学优化功能。另一种方法是测量任意一对点之间的*均距离,或者等价地,每个点与它们的*均值之间的*均距离,即方差。事实证明,这优化起来要容易得多。(生活很难,统计学家已经学会了采取简便方法)在数学上,这体现为最大化新特征空间中数据点的方差。
导航线性代数公式的提示
为了保持面向线性代数的世界,保持跟踪哪些数量标量,它们是向量,向量的方向是垂直还是水*。知道你的矩阵的维度,因为他们经常告诉你感兴趣的向量是否在行或列中。绘制矩阵和向量作为页面上的矩形,并确保形状匹配。就像通过记录测量单位(距离以英里,速度以英里/小时计)一样,在代数中可以得到很大的代数,在线性代数中,所有人都需要的是尺寸。
求导
提示和符号
如前所述,让X表示n×d数据矩阵,其中n是数据点的数量d是特征的数量。令X是包含单个数据点的列向量(所以x是X中其中一行的转置)。设W表示我们试图找到的新的特征向量或主要分量之一。
矩阵的奇异值分解(SVD)
任何矩形矩阵都可以分解为三个特定形状和特征的矩阵:

这里,
和
是正交矩阵(即
并且
,
是对角线包含X的奇异值的矩阵,它可以是正的,零的或负的。
假设
有n行d列且n≥d,那么
的大小为n x d,
和
的大小为d x d。(请参阅“奇异值分解(SVD)”来获得矩阵的 SVD 和特征分解的完整评论。)
公式 6-1 有用的*方和标识

线性投影
让我们逐步分解 PCA 的推导。PCA 使用线性投影将数据转换为新的特征空间。 图 6-2(c)说明了线性投影是什么样子。当我们将
投影到
时,投影的长度与两者之间的内积成正比,用w(它与它本身的内积)进行规范归一化。稍后,我们将w单位化。所以只有相关部分是分子。
公式 6-2 投影坐标

请注意,z是一个标量,而
和
是列向量。由于有一堆的数据点,我们可以制定所有投影坐标的向量
在新特征
。这里,
是熟悉的数据矩阵,其中每行是数据点,得到的
是一个列向量。
公式 6-4 投影坐标向量


图 6-2 PCA 的插图
(a)特征空间中的原始数据,(b)以数据为中心 (c)将数据向量x投影到另一向量v上,(d)使投影坐标的方差最大化的方向是
的主要特征向量。
方差和经验方差
下一步是计算投影的方差。方差定义为到均值的距离的*方的期望。
公式 6-6 随机变量Z的方差
![Var(Z)=E[Z-E(Z)]^2](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/fe4ml-zh/img/tex-3fc38cf4f89bc510e39d9767cb0dac4c.gif)
有一个小问题:我们提出的问题没有提到*均值
;它是一个自由变量。一个解决方案是从公式中删除它,从每个数据点中减去*均值。结果数据集的*均值为零,这意味着方差仅仅是
几何的期望值,减去*均值会产生数据居中效应。 (见图 6-2(a-b))。密切相关的量是两个随机变量Z1和Z2之间的协方差。把它看作是方差思想的扩展(单个随机变量)到两个随机变量。
公式 6-8 两个随机变量Z1和Z2之间的协方差
![Cov(Z_1,Z_2)=E[(Z_1-E(Z_1))(Z_2-E(Z_2))]](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/fe4ml-zh/img/tex-e66f0e324a5b6e3b574c6f51b5bbda83.gif)
当随机变量的均值为零时,它们的协方差与它们的线性相一致相关性
。 稍后我们会听到更多关于这个概念的信息。
数据上定义了统计量,如方差和期望值分配。 在实践中,我们没有真正的分布,但只有一堆观察数据点z1, ..., z n。这被称为经验分布,它给我们一个方差的经验估计。
公式 6-10 基于观察z得到Z的经验方差
公式法:第一个要素
结合公式 6-2中
的定义,我们有以下公式用于最大化投影数据的方差。(我们从中删除分母n-1经验方差的定义,因为它是一个全局常数而不是影响价值最大化的地方。)
公式 6-12 主成分的目标函数

这里的约束迫使
与其自身的内积为 1,这是相当于说向量必须有单位长度。这是因为我们只关心
的方向而不是
的大小。
的大小是 1 不必要的自由度,所以我们把它设置为任意的值。
主要成分:矩阵-向量表达式
接下来是棘手的一步。公式 6-12中的*方和是相当的繁琐。它在矩阵向量格式中会更清晰。我们能做到吗?答案是肯定的。关键在于*方和的同一性:一组*方项的和等于向量的*方范数,其元素是那些项,这相当于向量的内积。有了这个标识,我们可以用矩阵向量表示法重写公式 6-12。
6-4 主成分的目标函数,矩阵-向量表达式

PCA 的这种表述更明确地提出了目标:我们寻找一个输入最大化输出的标准的方向。这听起来很熟悉吗? 答案在于X的奇异值分解(SVD)。最优
是
的主要左奇异向量,它也是
的主特征向量。投影数据被称为原始数据的主成分。
主成分的一般解决方案
这个过程可以重复。一旦找到第一个主成分,我们就可以重新运行公式 6-14,并添加新向量与之正交的约束条件先前发现的向量.
公式 6-16 目标函数的k + 1个主成分
, where , 
该解是X的第k+1个左奇异向量,按奇异值降序排序。因此,前k个主成分对应于前k个X的左奇异向量。
转换功能
一旦找到主成分,我们可以使用线性转换特征投影。令
是X和S的 SVD,第k列中包含的矩阵前k个左奇异向量。X的维数为nxd,其中d是个数原始特征,并且
具有尺寸d×k。 而不是单个投影如公式 6-4 中的向量,我们可以同时投影到a中的多个向量投影矩阵。
公式 6-18 PCA 投影矩阵

投影坐标矩阵易于计算,并且可以使用奇异向量彼此正交的事实来进一步简化。
公式 6-19 简单的 PCA 转换

投影值仅仅是第一个按比例缩放的前 k 个右向奇异向量k个奇异值。因此,整个 PCA 解决方案,成分和投影可以通过X的 SVD 方便地获得。
实现 PCA
PCA 的许多推导涉及首先集中数据,然后采取特征协方差矩阵的分解。但实现 PCA 的最简单方法是通对中心数据矩阵进行奇异值分解。
PCA 实现步骤
公式 6-20 数据矩阵中心化
,其中1是全部是 1 的列向量,并且μ是包含X的*均行数的列向量。
公式 6-21 计算 SVD

公式 6-22 主成分
前
个主分量是
的前k列,即右奇异向量对应于k个最大奇异值。
公式 6-23 转换数据
转换后的数据只是U的前k列。(如果需要 whitening,然后通过逆奇异值对向量进行缩放。这需要选择奇异值不为零。参见“whitening 和 ZCA”)
PCA 执行
让我们更好地了解 PCA 如何将其应用于某些图像数据。MNIST 数据集包含从 0 到 9 的手写数字的图像。原始图像是28 x 28像素。使用 scikit-learn 分发图像的较低分辨率子集,其中每个图像被下采样为8×8像素。原始数据在 scikit 学习有 64 个维度。我们应用 PCA 并使用第一个可视化数据集三个主要部分。
示例 6-1 scikit-learn 数字数据集(MNIST 数据集的一个子集)的主成分分析。
>>> from sklearn import datasets
>>> from sklearn.decomposition import PCA
# Load the data
>>> digits_data = datasets.load_digits()
>>> n = len(digits_data.images)
# Each image is represented as an 8-by-8 array.
# Flatten this array as input to PCA.
>>> image_data = digits_data.images.reshape((n, -1))
>>> image_data.shape(1797, 64)
# Groundtruth label of the number appearing in each image
>>> labels = digits_data.target
>>> labels
array([0, 1, 2, ..., 8, 9, 8])
# Fit a PCA transformer to the dataset.
# The number of components is automatically chosen to account for
# at least 80% of the total variance.
>>> pca_transformer = PCA(n_components=0.8)
>>> pca_images = pca_transformer.fit_transform(image_data)
>>> pca_transformer.explained_variance_ratio_
array([ 0.14890594, 0.13618771, 0.11794594, 0.08409979, 0.05782415,
0.0491691 , 0.04315987, 0.03661373, 0.03353248, 0.03078806
,
0.02372341, 0.02272697, 0.01821863])
>>> pca_transformer.explained_variance_ratio_[:3].sum()
0.40303958587675121
# Visualize the results
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
>>> %matplotlib notebook
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111, projection='3d')
>>> for i in range(100):
... ax.scatter(pca_images[i,0], pca_images[i,1], pca_images[i,2],
... marker=r''.format(labels[i]), s=64)
>>> ax.set_xlabel('Principal component 1')
>>> ax.set_ylabel('Principal component 2')
>>> ax.set_zlabel('Principal component 3')
前 6 个投影图像的图片显示在图 6-3 的 3D 图中。标记对应于标签。前三个主成分大约占数据集总差的 40%。这绝不是完美的,但它允许方便的低维可视化。我们看到 PCA 组类似数字彼此接*。数字 0 和 6 位于同一区域,如同 1 和 17,3 和 9。该空间大致分为 0,4 和 6 一侧,其余数字是其他类的。

图 6-3 PCA 预测 MNIST 数据的子集。标记对应于图像标签。
由于数字之间有相当多的重叠,因此很难清楚的将它们在投影空间中使用线性分类器分开。因此,如果任务是分类手写数字并且选择的模型是一个线性分类器,前三个主成分不足以作为功能。尽管如此有趣的是只有 3 个可以捕获多少个 64 维数据集尺寸。
白化和 ZCA
由于目标函数中的正交性约束,PCA 变换产生了很好的附带作用:转换后的特征不再相关。再换句话说,特征向量对之间的内积是零。这很容易使用奇异向量的正交性来证明这一点:结果是包含奇异值的*方的对角矩阵表示每个特征向量与其自身的相关性,也称为其 L2 规范。
有时候,将特征的比例标准化为 1.在信号中是有用的处理方式,这就是所谓的白化。 它产生了一组与自身具有单位相关性,并且彼此之间的相关性为零的结果。在数学上,白化可以通过将 PCA 变换乘以反奇异值。
公式 6-24 PCA 白化

白化与维度降低无关;可以独立执行不受其他的干扰。例如,零相分量分析(ZCA)(Bell 和 Sejnowski,1996)是一种与 PCA 密切相关的白化转化,但事实并非减少特征的数量。ZCA 白化使用全套主要特征
没有减少,并且包括一个额外的乘法回到
。
公式 6-25 ZCA 白化

简单的 PCA 投影(公式 6-19)在新特征中产生坐标空间,主成分作为基础。这些坐标表示只有投影向量的长度,而不是方向。乘以主成分给我们的长度和方向。另一个有效解释是,多余的乘法将坐标旋转回原点原始特征空间。(V是正交矩阵,并且正交矩阵旋转他们的输入不拉伸或压缩)。所以 ZCA 白化产生的数据尽可能接*原始数据(欧几里德距离)。
主成分分析的局限性
当使用 PCA 进行降维时,必须解决使用多少个主成分(k)的问题。像所有的超参数一样,这个数字可以根据最终模型的质量进行调整。但也有启发式算法不涉及高度的计算方法。
一种可能性是选择k来解释总方差的所需比例。(该选项在 scikit-learn 软件包的 PCA 中可用)投影到第 k 个分量上:
=
,这是正方形
的第k个最大奇异值。a的奇异值的有序列表矩阵被称为其频谱。因此,为了确定要使用多少个成分,人们可以对数据矩阵进行简单的频谱分析并选择阈值保留足够的差异。
基于占用方差的 k 选择
要保留足够的成分覆盖数据总方差的 80%,请这样选择k:
。
另一种选择 k 的方法涉及数据集的固有维度。这个是一个更朦胧的概念,但也可以从频谱中确定。基本上,如果谱包含一些很大的奇异值和一些小奇异值,那么可能只是收获最大的奇异值并丢弃其余的值。有时候其余的频谱不是很小,但头部和尾部值之间有很大差距。这也是一个合理的截止点。 该方法需要光谱进行视觉检查,因此不能作为自动化管线的一部分执行。
对 PCA 的一个关键批评是转变相当复杂,并且结果因此很难解释。主成分和投影向量是真实的价值,可能是积极的或消极的。主成分是(居中)行的基本线性组合,以及投影值为列的线性组合。例如,在股票申报中,每个因素都是股票收益时间片的线性组合。那是什么意思?学习因素很难附加人为的理解原因。因此,分析师很难相信结果。如果你不能解释你为什么正在把数十亿其他人的钱放在特定的股票上,你可能会这样做将不会选择使用该模型。
PCA 在计算上是繁杂的的。它依赖于 SVD,这是一个昂贵的过程。计算一个矩阵的全 SVD 需要
操作 [Golub 和 Van Loan,2012],假设n≥d,即数据点比特征更多。即使我们只需要k个主成分,计算截断 SVD(k个最大奇异值和向量)仍然需要
操作。这是令人望而生畏的当有大量数据点或特征时。
以流媒体方式,批量更新或者从 PCA 执行 PCA 是 很困难的完整数据的样本。SVD 的流式计算,更新 SVD 和从一个子样本计算 SVD 都是很难研究的问题。算法存在,但代价是精度降低。一个含义是人们应该期待将测试数据投影到主成分上时代表性较低在训练集上找到。随着数据分布的变化,人们不得不这样做重新计算当前数据集中的主成分。
最后,最好不要将 PCA 应用于原始计数(字数,音乐播放次数,电影观看次数等)。这是因为这种计数通常包含在内大的异常值。(这个概率非常高,有粉丝观看了 314,582 次“指环王”,这让其余的人望而生畏计数)。正如我们所知,PCA 在特征中查找线性相关性。相关性和方差统计对大的异常值非常敏感; 单一大量的数据可能会改变很多。 所以,首先修剪是个好主意大数值的数据(“基于频率的滤波”)或应用缩放变换如 tf-idf(第 4 章)或日志转换(“日志转换”)。
用例
PCA 通过查找线性相关模式来减少特征空间维度功能之间。由于涉及 SVD,PCA 计算数千个功能的代价很高。但是对于少量的实值特征而言,它非常重要值得尝试。
PCA 转换会丢弃数据中的信息。因此,下游模型可能会训练成本更低,但可能不太准确。在 MNIST 数据集上,有一些观察到使用来自 PCA 的降维数据导致不太准确分类模型。在这些情况下,使用 PCA 有好处和坏处。
PCA 最酷的应用之一是时间序列的异常检测。Lakhina,Crovella 和 Diot [2004] 使用 PCA 来检测和诊断异常互联网流量。他们专注于数量异常情况,即当出现波动或波动时减少从一个网络区域到另一个网络区域的通信量。这些突然更改可能表示配置错误的网络或协调的拒绝服务攻击。无论哪种方式,知道何时何地发生这种变化对互联网都是有价值的运营商。
由于互联网上的交通总量非常之多,孤立的激增规模很小地区很难被发现。一个相对较小的主干链路处理很多交通。 他们的重要见解是,数量异常会影响到多个链接同时(因为网络数据包需要跳过多个节点才能到达他们的网络目的地)。将每个链接视为一项功能,并将每个链接的流量数量对待时间步骤作为测量。数据点是流量测量的时间片跨越网络上的所有链接。这个矩阵的主成分表明了网络上的整体流量趋势。其余的成分代表了剩余信号,其中包含异常。
PCA 也经常用于金融建模。在这些用例中,它作为一种类型工作因子分析,一组旨在描述观察结果的统计方法使用少量未观察因素的数据变异性。在因素分析中应用程序,目标是找到解释性成分,而不是转换数据。
像股票收益这样的财务数量往往是相互关联的。股票可以同时上下移动(正相关),也可以相反移动方向(负相关)。为了*衡波动和降低风险,投资组合需要多种不相关的股票其他。(如果篮子要下沉,不要把所有的鸡蛋放在一个篮子里)寻找强大的相关模式有助于决定投资策略。
股票关联模式可能在行业范围内。 例如,科技股可能会上涨并一起下跌,而当油价高企时,航空股往往下跌。 但行业可能不是解释结果的最好方式。 分析师也在寻找观察到的统计数据中意外的相关性 特别是文体因素模型 [Connor,1995] 在个体股票时间序列矩阵上运行 PCA 返回寻找共同变化的股票。 在这个用例中,最终目标是主成分本身,而不是转换后的数据。
从图像中学习时,ZCA 可作为预处理步骤。在自然的图像中,相邻像素通常具有相似的颜色。ZCA 白化可以消除这种相关性,这允许后续的建模工作集中在更有趣的图像上结构。Alex Krizhevsky 的“学习多层特征”的论文图像“包含很好的示例,说明 ZCA 影响自然图片。
许多深度学习模型使用 PCA 或 ZCA 作为预处理步骤,但事实并非如此总是显示是必要的。在“Factored 3-Way Restricted Boltzmann Machines forModeling Natural Images”中,Ranzato et al,评论,“白化不是必要的,但加快了算法的收敛速度。在“An Analysis of Single-Layer Networks in Unsupervised Feature Learning”中,Coates 等人 发现 ZCA 白化是有帮助的对于某些号,但不是全部。(请注意,本文中的模型是无监督功能学习模型。 所以 ZCA 被用作其他功能的特征方法工程方法。方法的堆叠和链接在机器中很常见学习管道。)
总结
这结束了对 PCA 的讨论。关于 PCA 需要记住的两件事是其机制(线性投影)和目标(最大化方差预计数据)。该解决方案涉及协方差的特征分解矩阵,它与数据矩阵的 SVD 密切相关。人们还可以记住 PCA 的精神图像将数据挤压成像蓬松一样的煎饼可能。PCA 是模型驱动特征工程的一个示例。(应该立即怀疑当一个目标函数进入时,一个模型潜伏在背景中场景)。这里的建模假设是方差充分代表了包含在数据中的信息。等价地,该模型寻找线性特征之间的相关性。这在几个应用程序中用于减少相关性或在输入中找到共同因素。PCA 是一种众所周知的降维方法。但它有其局限性作为高计算成本和无法解释的结果。它作为一个预先定义好处理步骤,特别是在特征之间存在线性相关时。当被看作是一种消除线性相关的方法时,PCA 与其相关白化的概念。其表兄 ZCA 以可解释的方式使数据变白,但是不会降低维度。
参考书目
Bell, Anthony J. and Terrence J. Sejnowski. 1996. “Edges are the ‘Independent
Components’ of Natural Scenes.” Proceedings of the Conference on Neural Information Processing Systems (NIPS) .
Coates, Adam, Andrew Y. Ng, and Honglak Lee. 2011. “An Analysis of Single-Layer
Networks in Unsupervised Feature Learning." International conference on artificial intelligence and statistics .
Connor, Gregory. 1995. “The Three Types of Factor Models: A Comparison of Their Explanatory Power." Financial Analysts Journal 51, no. 3: 42-46. http://www.jstor.org/stable/4479845.
Golub, Gene H., and Charles F. Van Loan. 2012. Matrix Computations . Baltimore and London: Johns Hopkins University Press; fourth edition.
Krizhevsky, Alex. 2009. “Learning Multiple Layers of Features from Tiny Images.”
MSc thesis, University of Toronto.
Lakhina, Anukool, Mark Crovella, and Christophe Diot. 2004. “Diagnosing network-wide traffic anomalies.” Proceedings of the 2004 conference on Applications, technologies,architectures, and protocols for computer communications (SIGCOMM ’04). DOI=http://dx.doi.org/10.1145/1015467.1015492
Ranzato, Marc’Aurelio, Alex Krizhevsky, and Geoffrey E. Hinton. 2010. “Factored 3-Way Restricted Boltzmann Machines for Modeling Natural Images." Proceedings of the 13-th International Conference on Artificial Intelligence and Statistics (AISTATS 2010)
七、非线性特征提取和模型堆叠
当在数据一个线性子空间像扁*饼时 PCA 是非常有用的。但是如果数据形成更复杂的形状呢?一个*面(线性子空间)可以推广到一个 流形 (非线性子空间),它可以被认为是一个被各种拉伸和滚动的表面。
如果线性子空间是*的纸张,那么卷起的纸张就是非线性流形的例子。你也可以叫它瑞士卷。(见图 7-1),一旦滚动,二维*面就会变为三维的。然而,它本质上仍是一个二维物体。换句话说,它具有低的内在维度,这是我们在“直觉”中已经接触到的一个概念。如果我们能以某种方式展开瑞士卷,我们就可以恢复到二维*面。这是非线性降维的目标,它假定流形比它所占据的全维更简单,并试图展开它。

关键是,即使当大流形看起来复杂,每个点周围的局部邻域通常可以很好地*似于一片*坦的表面。换句话说,他们学习使用局部结构对全局结构进行编码。非线性降维也被称为非线性嵌入,或流形学习。非线性嵌入可有效地将高维数据压缩成低维数据。它们通常用于 2-D 或 3-D 的可视化。
然而,特征工程的目的并不是要使特征维数尽可能低,而是要达到任务的正确特征。在这一章中,正确的特征是代表数据空间特征的特征。
聚类算法通常不是局部结构化学习的技术。但事实上也可以用他们这么做。彼此接*的点(由数据科学家使用某些度量可以定义的“接*度”)属于同一个簇。给定聚类,数据点可以由其聚类成员向量来表示。如果簇的数量小于原始的特征数,则新的表示将比原始的具有更小的维度;原始数据被压缩成较低的维度。
与非线性嵌入技术相比,聚类可以产生更多的特征。但是如果最终目标是特征工程而不是可视化,那这不是问题。
我们将提出一个使用 k 均值聚类算法来进行结构化学习的思想。它简单易懂,易于实践。与非线性流体降维相反,k 均值执行非线性流形特征提取更容易解释。如果正确使用它,它可以是特征工程的一个强大的工具。
k 均值聚类
k 均值是一种聚类算法。聚类算法根据数据在空间中的排列方式来分组数据。它们是无监督的,因为它们不需要任何类型的标签,使用算法仅基于数据本身的几何形状来推断聚类标签。
聚类算法依赖于 度量 ,它是度量数据点之间的紧密度的测量。最流行的度量是欧几里德距离或欧几里得度量。它来自欧几里得几何学并测量两点之间的直线距离。我们对它很熟悉,因为这是我们在日常现实中看到的距离。
两个向量X和Y之间的欧几里得距离是X-Y的 L2 范数。(见 L2 范数的“L2 标准化”),在数学语言中,它通常被写成‖ x - y ‖。
k 均值建立一个硬聚类,意味着每个数据点被分配给一个且只分配一个集群。该算法学习定位聚类中心,使得每个数据点和它的聚类中心之间的欧几里德距离的总和最小化。对于那些喜欢阅读公式而非语言的人来说,目标函数是:

每个簇
包含数据点的子集。簇i的中心等于簇中所有数据点的*均值:
,其中
表示簇i中的数据点的数目。
图 7-2 显示了 k 均值在两个不同的随机生成数据集上的工作。(a)中的数据是由具有相同方差但不同均值的随机高斯分布生成的。(c)中的数据是随机产生的。这些问题很容易解决,k 均值做得很好。(结果可能对簇的数目敏感,数目必须给算法)。这个例子的代码如例 7-1 所示。

例 7-1
import numpy as np
from sklearn.cluster
import KMeans from sklearn.datasets
import make_blobs
import matplotlib.pyplot as plt %matplotlib notebook
n_data = 1000
seed = 1
n_clusters = 4
# 产生高斯随机数,运行 K-均值
blobs, blob_labels = make_blobs(n_samples=n_data, n_features=2,centers=n_centers, random_state=seed)
clusters_blob = KMeans(n_clusters=n_centers, random_state=seed).fit_predict(blobs)
# 产生随机数,运行 K-均值
uniform = np.random.rand(n_data, 2)
clusters_uniform = KMeans(n_clusters=n_clusters, random_state=seed).fit_predict(uniform)
# 使用 Matplotlib 进行结果可视化
figure = plt.figure()
plt.subplot(221)
plt.scatter(blobs[:, 0], blobs[:, 1], c=blob_labels, cmap='gist_rainbow')
plt.title("(a) Four randomly generated blobs", fontsize=14)
plt.axis('off')
plt.subplot(222)
plt.scatter(blobs[:, 0], blobs[:, 1], c=clusters_blob, cmap='gist_rainbow')
plt.title("(b) Clusters found via K-means", fontsize=14)
plt.axis('off')
plt.subplot(223)
plt.scatter(uniform[:, 0], uniform[:, 1])
plt.title("(c) 1000 randomly generated points", fontsize=14)
plt.axis('off')
plt.subplot(224)
plt.scatter(uniform[:, 0], uniform[:, 1], c=clusters_uniform, cmap='gist_rainbow')
plt.title("(d) Clusters found via K-means", fontsize=14) plt.axis('off')

曲面拼接聚类
应用聚类一般假定存在自然簇,即在其他空的空间中存在密集的数据区域。在这些情况下,有一个正确的聚类数的概念,人们已经发明了聚类指数用于测量数据分组的质量,以便选择 k。
然而,当数据像如图 7-2(c)那样均匀分布时,不再有正确的簇数。在这种情况下,聚类算法的作用是矢量量化,即将数据划分成有限数量的块。当使用量化矢量而不是原始矢量时,可以基于可接受的*似误差来选择簇的数目。
从视觉上看,k 均值的这种用法可以被认为是如图 7-3 那样用补丁覆盖数据表面。如果在瑞士卷数据集上运行 k 均值,这确实是我们所得到的。例 7-2 使用sklearn生成瑞士卷上的嘈杂数据集,将其用 k 均值聚类,并使用 Matplotlib 可视化聚类结果。数据点根据它们的簇 ID 着色。


例 7-2
from mpl_toolkits.mplot3d import Axes3D
from sklearn import manifold, datasets
# 在瑞士卷训练集上产生噪声
X, color = datasets.samples_generator.make_swiss_roll(n_samples=1500)
# 用 100 K-均值聚类估计数据集
clusters_swiss_roll = KMeans(n_clusters=100, random_state=1).fit_predict(X)
# 展示用数据集,其中颜色是 K-均值聚类的 id
fig2 = plt.figure() ax = fig2.add_subplot(111, projection='3d')
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=clusters_swiss_roll, cmap='Spectral')
在这个例子中,我们在瑞士卷表面上随机生成 1500 个点,并要求 k 均值用 100 个簇来*似它。我们提出 100 这个数字,因为它看起来相当大,使每一簇覆盖了相当小的空间。结果看起来不错;簇群确实是很小的的,并且流体的不同部分被映射到不同的簇。不错!但我们完成了吗?
问题是,如果我们选择一个太小的K,那么从多元学习的角度来看,结果不会那么好。图 7-5 显示了 k 均值用 10 个簇在瑞士卷的输出。我们可以清楚地看流体的完全的部分都被映射到相同的簇(例如黄色、紫色、绿色和品红簇)的数据。

如果数据在空间中均匀分布,则选择正确的k就被归结为球填充问题。在D维中,可以拟合半径约为R的1/r的D次幂的球。每个 k 均值聚类是一个球面,半径是用质心表示球面中的点的最大误差。因此,如果我们愿意容忍每个数据点R的最大逼*误差,那么簇的数目是O((1/R)^D),其中D是数据的原始特征空间的维数。
对于 k 均值来说,均匀分布是最坏的情况。如果数据密度不均匀,那么我们将能够用更少的簇来表示更多的数据。一般来说,很难知道数据在高维空间中是如何分布的。我们可以保守的选择更大的 K。但是它不能太大,因为K将成为下一步建模步骤的特征数量。
用于分类的 k 均值特征化
当使用 k 均值作为特征化过程时,数据点可以由它的簇成员(分类变量群组成员的稀疏独热编码)来表示,我们现在来说明。
如果目标变量也是可用的,那么我们可以选择将该信息作为对聚类过程的提示。一种合并目标信息的方法是简单地将目标变量作为 k 均值算法的附加输入特征。由于目标是最小化在所有输入维度上的总欧氏距离,所以聚类过程将试图*衡目标值和原始特征空间中的相似性。可以在聚类算法中对目标值进行缩放以获得更多或更少的关注。目标的较大差异将产生更多关注分类边界的聚类。
k 均值特征化
聚类算法分析数据的空间分布。因此,k 均值特征化创建了一个压缩的空间索引,该数据可以在下一阶段被馈送到模型中。这是模型堆叠(stacking)的一个例子。
例 7-3 显示了一个简单的 k 均值特征。它被定义为可以训练数据和变换任何新数据的类对象。为了说明在聚类时使用和不使用目标信息之间的差异,我们将特征化器应用到使用sklearn的 make——moons 函数(例 7-4)生成的合成数据集。然后我们绘制簇边界的 Voronoi 图。图 7-6 展示出了结果的比较。底部面板显示没有目标信息训练的集群。注意,许多簇跨越两个类之间的空空间。顶部面板表明,当聚类算法被给定目标信息时,聚类边界可以沿着类边界更好地对齐。
例 7-3
import numpy as np
from sklearn.cluster import KMeans
class KMeansFeaturizer:
"""将数字型数据输入 k-均值聚类.
在输入数据上运行 k-均值并且把每个数据点设定为它的簇 id. 如果存在目标变量,则将其缩放并包含为 k-均值的输入,以导出服从分类边界以及组相似点的簇。
"""
def __init__(self, k=100, target_scale=5.0, random_state=None):
self.k = k
self.target_scale = target_scale
self.random_state = random_state
def fit(self, X, y=None):
"""在输入数据上运行 k-均值,并找到中心."""
if y is None:
# 没有目标变量运行 k-均值
km_model = KMeans(n_clusters=self.k,n_init=20,random_state=self.random_state)
km_model.fit(X)
self.km_model_ = km_model
self.cluster_centers_ = km_model.cluster_centers_
return self
# 有目标信息,使用合适的缩减并把输入数据输入 k-均值
data_with_target = np.hstack((X, y[:,np.newaxis]*self.target_scale))
# 在数据和目标上简历预训练 k-均值模型
km_model_pretrain = KMeans(n_clusters=self.k,n_init=20,random_state=self.random_state)
km_model_pretrain.fit(data_with_target)
#运行 k-均值第二次获得簇在原始空间没有目标信息。使用预先训练中发现的质心进行初始化。
#通过一个迭代的集群分配和质心重新计算。
km_model = KMeans(n_clusters=self.k,init=km_model_pretrain.cluster_centers_[:,:2],n_init=1,max_iter=1)
km_model.fit(X)
self.km_model = km_model
self.cluster_centers_ = km_model.cluster_centers_
return self
def transform(self, X, y=None):
"""为每个输入数据点输出最接*的簇 id。"""
clusters = self.km_model.predict(X)
return clusters[:,np.newaxis]
def fit_transform(self, X, y=None):
self.fit(X, y)
return self.transform(X, y)
例 7-4
from scipy.spatial import Voronoi, voronoi_plot_2d
from sklearn.datasets import make_moons
training_data, training_labels = make_moons(n_samples=2000, noise=0.2)
kmf_hint = KMeansFeaturizer(k=100, target_scale=10).fit(training_data, training_labels)
kmf_no_hint = KMeansFeaturizer(k=100, target_scale=0).fit(training_data, training_labels)
def kmeans_voronoi_plot(X, y, cluster_centers, ax):
"""绘制与数据叠加的 k-均值聚类的 Voronoi 图"""
ax.scatter(X[:, 0], X[:, 1], c=y, cmap='Set1', alpha=0.2)
vor = Voronoi(cluster_centers)
voronoi_plot_2d(vor, ax=ax, show_vertices=False, alpha=0.5)

让我们测试 k 均值特征分类的有效性。例 7-5 对 k 均值簇特征增强的输入数据应用 Logistic 回归。比较了与使用径向基核的支持向量机(RBF SVM)、K *邻(KNN)、随机森林(RF)和梯度提升树(GBT)的结果。随机森林和梯度提升树是最流行的非线性分类器,具有最先进的性能。RBF 支持向量机是欧氏空间的一种合理的非线性分类器。KNN 根据其 K *邻的*均值对数据进行分类。(请参阅“分类器概述”来概述每个分类器。)
分类器的默认输入数据是数据的 2D 坐标。Logistic 回归也给出了簇成员特征(在图 7-7 中标注为“k 均值的 LR”)。作为基线,我们也尝试在二维坐标(标记为“LR”)上进行逻辑回归。
例 7-4
from sklearn.linear_model
import LogisticRegression
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier,GradientBoostingClassifier
#生成与训练数据相同分布的测试数据
test_data, test_labels = make_moons(n_samples=2000, noise=0.3)
# 使用 k-均值特技器生成簇特征
training_cluster_features = kmf_hint.transform(training_data) test_cluster_features = kmf_hint.transform(test_data)
# 将新的输入特征和聚类特征整合
training_with_cluster = scipy.sparse.hstack((training_data, training_cluster_features)) test_with_cluster = scipy.sparse.hstack((test_data, test_cluster_features))
# 建立分类器
lr_cluster = LogisticRegression(random_state=seed).fit(training_with_cluster, training_labels)
classifier_names = ['LR','kNN','RBF SVM','Random Forest','Boosted Trees']
classifiers = [LogisticRegression(random_state=seed),KNeighborsClassifier(5),SVC(gamma=2, C=1),RandomForestClassifier(max_depth=5, n_estimators=10, max_features=1),GradientBoostingClassifier(n_estimators=10, learning_rate=1.0, max_depth=5)]
for model in classifiers:
model.fit(training_data, training_labels)
# 辅助函数使用 ROC 评估分类器性能
def test_roc(model, data, labels):
if hasattr(model, "decision_function"):
predictions = model.decision_function(data)
else:
predictions = model.predict_proba(data)[:,1]
fpr, tpr, _ = sklearn.metrics.roc_curve(labels, predictions)
return fpr, tpr
# 显示结果
import matplotlib.pyplot as plt plt.figure()
fpr_cluster, tpr_cluster = test_roc(lr_cluster, test_with_cluster, test_labels) plt.plot(fpr_cluster, tpr_cluster, 'r-', label='LR with k-means')
for i, model in enumerate(classifiers):
fpr, tpr = test_roc(model, test_data, test_labels) plt.plot(fpr, tpr, label=classifier_names[i])
plt.plot([0, 1], [0, 1], 'k--')
plt.legend()
可选择的密集化
与独热簇相反,数据点也可以由其逆距离的密集向量表示到每个聚类中心。这比简单的二值化簇保留了更多的信息,但是现在表达是密集的。这里有一个折衷方案。一个热集群成员导致一个非常轻量级的稀疏表示,但是一个可能需要较大的K来表示复杂形状的数据。反向距离表示是密集的,这对于建模步骤可能花费更昂贵,但是这可以需要较小的K。
稀疏和密集之间的折衷是只保留最接*的簇的p的逆距离。但是现在P是一个额外的超参数需要去调整。(现在你能理解为什么特征工程需要这么多的步骤吗?),天下没有免费的午餐。
总结
使用 k 均值将空间数据转换为模型堆叠的一个例子,其中一个模型的输入是另一个模型的输出。堆叠的另一个例子是使用决策树类型模型(随机森林或梯度提升树)的输出作为线性分类器的输入。堆叠已成为*年来越来越流行的技术。非线性分类器训练和维护是昂贵的。堆叠的关键一点是将非线性引入特征,并且使用非常简单的、通常是线性的模型作为最后一层。该特征可以离线训练,这意味着可以使用昂贵的模型,这需要更多的计算能力或内存,但产生有用的特征。顶层的简单模型可以很快地适应在线数据的变化分布。这是精度和速度之间的一个很好的折衷,这经常被应用于需要快速适应改变数据分布的应用,比如目标广告。
模型堆叠的关键点
复杂的基础层(通常是昂贵的模型)产生良好的(通常是非线性的)特征,随后结合简单并且快速的顶层模型。这常常在模型精度和速度之间达到正确的*衡。
与使用非线性分类器相比,采用 logistic 回归的 k 均值更容易进行训练和存储。表 7-1 是多个机器学习模型的计算和记忆的训练和预测复杂性的图表。n表示数据点的数量,D(原始)特征的数量。
对于 k 均值,训练时间是O(nkd),因为每次迭代涉及计算每个数据点和每个质心(k)之间的d维距离。我们乐观地假设迭代次数不是n的函数,尽管并不普遍适用。预测需要计算新的数据点与质心(k)之间的距离,即O(kd)。存储空间需求是O(kd),对于K质心的坐标。
logistic 训练和预测在数据点的数量和特征维度上都是线性的。RBF SVM 训练是昂贵的,因为它涉及计算每一对输入数据的核矩阵。RBF SVM 预测比训练成本低,在支持向量S和特征维数D的数目上是线性的。改进的树模型训练和预测在数据大小和模型的大小上线性的(t个树,每个最多 2 的m次幂子叶,其中m是树的最大深度)。KNN 的实现根本不需要训练时间,因为训练数据本身本质上是模型。花费全都在预测时间,输入必须对每个原始训练点进行评估,并部分排序以检索 K *邻。
总体而言,k 均值 +LR 是在训练和预测时间上唯一的线性组合(相对于训练数据O(nd)的大小和模型大小O(kd))。复杂度最类似于提升树,其成本在数据点的数量、特征维度和模型的大小(O(2^m*t))中是线性的。很难说 k 均值 +LR 或提升树是否会产生更小的模型,这取决于数据的空间特征。

数据泄露的潜力
那些记得我们对数据泄露的谨慎(参见“防止数据泄露(桶计数:未来的日子)”)可能会问 k 均值特化步骤中的目标变量是否也会导致这样的问题。答案是“是的”,但并不像桶计数(Bin-counting)计算的那么多。如果我们使用相同的数据集来学习聚类和建立分类模型,那么关于目标的信息将泄漏到输入变量中。因此,对训练数据的精度评估可能过于乐观,但是当在保持验证集或测试集上进行评估时,偏差会消失。此外,泄漏不会像桶计数那么糟糕(参见“桶计数”),因为聚类算法的有损压缩将抽象掉一些信息。要格外小心防止泄漏,人们可以始终保留一个单独的数据集来导出簇,就像在桶计数下一样。
k 均值特化对有实数、有界的数字特征是有用的,这些特征构成空间中密集区域的团块。团块可以是任何形状,因为我们可以增加簇的数量来*似它们。(与经典的类别聚类不同,我们不关心真正的簇数;我们只需要覆盖它们。)
k 均值不能处理欧几里得距离没有意义的特征空间,也就是说,奇怪的分布式数字变量或类别变量。如果特征集包含这些变量,那么有几种处理它们的方法:
-
仅在实值的有界数字特征上应用 k 均值特征。
-
定义自定义度量(参见第?章以处理多个数据类型并使用 k 中心点算法。(k 中心点类似于 k 均值,但允许任意距离度量。)
-
类别变量可以转换为装箱统计(见“桶计数”),然后使用 K 均值进行特征化。
结合处理分类变量和时间序列的技术,k 均值特化可以自适应的处理经常出现在客户营销和销售分析中的丰富数据。所得到的聚类可以被认为是用户段,这对于下一个建模步骤是非常有用的特征。
我们将在下一章中讨论的深度学习,是通过将神经网络层叠在一起,将模型堆叠提升到一个全新的水*。ImageNet 挑战的两个赢家使用了 13 层和 22 层神经网络。就像 K 均值一样,较低层次的深度学习模型是无监督的。它们利用大量可用的未标记的训练图像,并寻找产生良好图像特征的像素组合。
八、自动化特征提取器:图像特征提取和深度学习
视觉和声音是人类固有的感觉输入。我们的大脑是可以迅速进化我们的能力来处理视觉和听觉信号的,一些系统甚至在出生前就对刺激做出反应。另一方面,语言技能是学习得来的。他们需要几个月或几年的时间来掌握。许多人天生就具有视力和听力的天赋,但是我们所有人都必须有意训练我们的大脑去理解和使用语言。
有趣的是,机器学习的情况是相反的。我们已经在文本分析应用方面取得了比图像或音频更多的进展。以搜索问题为例。人们在信息检索和文本检索方面已经取得了相当多年的成功,而图像和音频搜索仍在不断完善。在过去五年中,深度学习模式的突破最终预示着期待已久的图像和语音分析的革命。
进展的困难与从相应类型的数据中提取有意义特征的困难直接相关。机器学习模型需要语义上有意义的特征进行语义意义的预测。在文本分析中,特别是对于英语这样的语言,其中一个基本的语义单位(一个词)很容易提取,可以很快地取得进展。另一方面,图像和音频被记录为数字像素或波形。图像中的单个“原子”是像素。在音频数据中,它是波形强度的单一测量。它们包含的语义信息远少于数据文本。因此,在图像和音频上的特征提取和工程任务比文本更具挑战性。
在过去的二十年中,计算机视觉研究已经集中在人工标定上,用于提取良好的图像特征。在一段时间内,图像特征提取器,如 SIFT 和 HOG 是标准步骤。深度学习研究的最新发展已经扩展了传统机器学习模型的范围,将自动特征提取作为基础层。他们本质上取代手动定义的特征图像提取器与手动定义的模型,自动学习和提取特征。人工标定仍然存在,只是进一步深入到建模中去。
在本章中,我们将从流行的图像特征提取 SIFT 和 HOG 入手,深入研究本书所涵盖的最复杂的建模机制:深度学习的特征工程。
最简单的图像特征(为什么他们不好使)
从图像中提取的哪些特征是正确的呢?答案当然取决于我们试图用这些特征来做什么。假设我们的任务是图像检索:我们得到一张图片并要求从图像数据库中得到相似的图片。我们需要决定如何表示每个图像,以及如何测量它们之间的差异。我们可以看看图像中不同颜色的百分比吗?图 8-1 展示了两幅具有大致相同颜色轮廓但有着非常不同含义的图片;一个看起来像蓝色天空中的白云,另一个是希腊国旗。因此,颜色信息可能不足以表征图像。

另一个比较简单的想法是测量图像之间的像素值差异。首先,调整图像的宽度和高度。每个图像由像素值矩阵表示。矩阵可以通过一行或一列被堆叠成一个长向量。每个像素的颜色(例如,颜色的 RGB 编码)现在是图像的特征。最后,测量长像素向量之间的欧几里得距离。这绝对可以区分希腊国旗和白云。但作为相似性度量,它过于严格。云可以呈现一千种不同的形状,仍然是一朵云。它可以移动到图像的一边,或者一半可能位于阴影中。所有这些转换都会增加欧几里得距离,但是他们不应该改变图片仍然是云的事实。
问题是单个像素不携带足够的图像语义信息。因此,使用它们用于分析结果是非常糟糕的。在 1999 年,计算机视觉研究者想出了一种更好的方法来使用图像的统计数据来表示图像。它叫做 Scale Invariant Feature Transform(SIFT)。
SIFT 最初是为对象识别的任务而开发的,它不仅涉及将图像正确地标记为包含对象,而且确定其在图像中的位置。该过程包括在可能的尺度金字塔上分析图像,检测可以指示对象存在的兴趣点,提取关于兴趣点的特征(通常称为计算机视觉中的图像描述符),并确定对象的姿态。
多年来,SIFT 的使用扩展到不仅提取兴趣点,而且遍及整个图像的特征。SIFT 特征提取过程非常类似于另一种称为 Histogram of Oriented Gradients (HOG)的技术。它们都计算梯度方向的直方图。现在我们详细地描述一下。
人工特征提取:SIFT 与 HOG
图像梯度
要比原始像素值做得更好,我们必须以某种方式将像素组织成更多信息单元。相邻像素之间的差异通常是非常有用的特征。通常情况下像素值在对象的边界处是不同,当存在阴影、图案内或纹理表面时。相邻像素之间的差值称为图像梯度。
计算图像梯度的最简单的方法是分别计算图像沿水*(X)和垂直(Y)轴的差异,然后将它们合成为二维矢量。这涉及两个 1D 差分操作,可以用矢量掩模或滤波器方便地表示。掩码(1, 0, -1)可以得到在左像素和右像素之间的差异或者上像素和下像素之间的差异,取决于我们应用掩码的方向。当然也有二维梯度滤波器。但在本例中,1D 滤波器就足够了。
为了对图像应用滤波器,我们执行卷积。它涉及翻转滤波器和内积与一小部分的图像,然后移动到下一个块。卷积在信号处理中很常见。我们将使用*来表示操作:

在像素点(i,j)的 x 梯度和 y 梯度为:

他们一起组成了梯度:

向量可以通过它的方向和大小来完全描述。梯度的大小等于梯度的欧几里得范数,这表明像素值在像素周围变化得多大。梯度的位置或方向取决于水*方向和垂直方向上的变化的相对大小;图 8-2 说明了这些数学概念。

图 8—3 展出了由垂直和水*梯度组成的图像梯度的示例。每个示例是一个 9 像素的图像。每个像素用灰度值标记。(较小的数字对应于较深的颜色)中心像素的梯度显示在每个图像下面。左侧的图像包含水*条纹,其中颜色仅垂直变化。因此,水*梯度为零,梯度垂直为非零。中心图像包含垂直条纹,因此水*梯度为零。右边的图像包含对角线条纹,斜率也是对角线。

它们能在真实的图像上发挥作用吗?在例 8-1 中,我们使用图 8-4 所示的猫的水*和垂直梯度上来实验。由于梯度是在原始图像的每个像素位置计算的,所以我们得到两个新的矩阵,每个矩阵可以被可视化为图像。

例 8-1
import matplotlib.pyplot as plt
import numpy as np
from skimage import data, color
# Load the example image and turn it into grayscale
image = color.rgb2gray(data.chelsea())
# Compute the horizontal gradient using the centered 1D filter
# This is equivalent to replacing each non-border pixel with the # difference between its right and left neighbors. The leftmost
# and rightmost edges have a gradient of 0.
gx = np.empty(image.shape, dtype=np.double)
gx[:, 0] = 0
gx[:, -1] = 0
gx[:, 1:-1] = image[:, :-2] - image[:, 2:]
# Same deal for the vertical gradient
gy = np.empty(image.shape, dtype=np.double)
gy[0, :] = 0
gy[-1, :] = 0
gy[1:-1, :] = image[:-2, :] - image[2:, :]
# Matplotlib incantations
fig, (ax1, ax2, ax3) = plt.subplots(3, 1,figsize=(5, 9),sharex=True,sharey=True)
ax1.axis('off')
ax1.imshow(image, cmap=plt.cm.gray)
ax1.set_title('Original image')
ax1.set_adjustable('box-forced')
ax2.axis('off')
ax2.imshow(gx, cmap=plt.cm.gray)
ax2.set_title('Horizontal gradients')
ax2.set_adjustable('box-forced')
ax3.axis('off')
ax3.imshow(gy, cmap=plt.cm.gray)
ax3.set_title('Vertical gradients')
ax3.set_adjustable('box-forced')
注意,水*梯度提取出强烈的垂直模式,如猫眼睛的内边缘,而垂直梯度则提取强的水*模式,如晶须和眼睛的上下眼睑。这乍看起来似乎有些矛盾,如果我们仔细考虑一下,这还是有道理的。水*(X)梯度识别水*方向上的变化。强的垂直图案在大致相同的X位置上跨越多个Y像素。因此,垂直图案导致像素值的水*差异。这也是我们的眼睛也能察觉到的。
梯度方向直方图
单个图像梯度可以识别图像邻域中的微小差异。但是我们的眼睛看到的图案比那更大。例如,我们看到一整只猫的胡须,而不仅仅是一个小部分。人类视觉系统识别区域中的连续模式。因此,我们咋就图像梯度邻域有仍然有很多的工作要做。
我们如何精确地归纳向量?统计学家会回答:“看分布!SIFT 和 HOG 都走这条路。它们计算(正则化)梯度矢量直方图作为图像特征。直方图将数据分成容器并计算每容器中有多少,这是一个(不规范的)经验分布。规范化确保数和为 1,用数学语言描述为它具有单位 L 范数。
图像梯度是矢量,矢量可以由两个分量来表示:方向和幅度。因此,我们仍然需要决定如何设计直方图来表示这两个分量。SIFT 和 HOG 提供了一个解决方案,其中图像梯度被它们的方向角所包括,由每个梯度的大小加权。以下是流程:
-
将 0° - 360° 分成相等大小的容器。
-
对于邻域中的每个像素,将权重 W 添加到对应于其方向角的容器中。
-
W是梯度的大小和其他相关信息的函数。例如,其他相关信息可以是像素到图像贴片中心的逆距离。其思想是,如果梯度较大,权重应该很大,而图像邻域中心附*的像素比远离像素的像素更大。 -
正则化直方图。
图 8-5 提供了由4x4像素的图像邻域构成的 8 个容器的梯度方向直方图的图示。

当然,在基本的梯度方向直方图算法中还有许多选项。像通常一样,正确的设置可能高度依赖于想要分析的特定图像。
有多少容器?
他们的跨度是从 0° - 360°(有符号梯度)还是 0° - 180°(无符号梯度)?
具有更多的容器导致梯度方向的细粒度量化,因此会保留更多关于原始梯度的信息。但是,有太多的容器是不必要的,并可能导致过度拟合训练数据。例如,在图像中识别猫可能不依赖于精确地取向在 3° 的猫的晶须。
还有一个问题,容器是否应该跨越 0 - 360°,这将沿Y轴保持梯度,或跨越 0°- 180°,这将不会保留垂直梯度的符号。Dalal 与 Triggs 是 HOG 论文的最初作者,实验确定从 0 - 180° 跨越的 9 个容器是最好的,而 SIFT 论文推荐了 8 个跨越 0° - 360° 的容器。
使用什么样的权重函数?
HOG 论文比较各种梯度幅度加权方案:其大小本身、*方、*方根、二值化等等。没有改变的*面大小在实验中表现最好。
SIFT 还使用梯度的原始大小。最重要的是,它希望避免图像描述符在图像窗口位置的微小变化中的突然变化。因此,它使用从窗口中心测量的高斯距离函数来衡量来自邻域边缘的梯度。换言之,梯度幅值乘以高斯函数
,其中P是产生梯度的像素的位置,P0图像邻域的中心位置,并且σ为高斯的宽度,σ被设置为邻域半径的一半。
SIFT 还希望避免从单个图像梯度方向的微小变化来改变方向直方图中的大的变化。因此,它使用一个插值技巧,将权重从一个梯度扩展到相邻的方向箱。特别地,根箱(梯度分配的箱)得到加权幅度的 1 倍的投票。每个相邻的容器得到 1-D 的投票,其中D是来自根容器的直方图箱单元的差异。
总的来说,SIFT 的单一图像梯度的投票是

其中
是在箱b的像素点p的梯度,Wb是权值b的插值,σ是p距离中心的高斯距离。
邻域怎么定义?他们应该怎样覆盖图片?
HOG 和 SIFT 都基于图像邻域的两层表示:首先,将相邻像素组织成单元,然后将相邻单元组织成块。计算每个单元的方向直方图,并将单元直方图矢量连接起来,形成整个块的最终特征描述符。
SIFT 使用16x16像素的单元,将其组织成 8 个方向的容器,然后通过4x4单元的块分组,使得图像邻域的4x4x8=128个特征。
HOG 论文实验用矩形和圆形形状的单元和块。矩形单元称为 R-HOG。最好的 R-HOG 设置为8x8像素的 9 个定向仓,每个分组为2x2个单元的块。圆形窗口称为 C-HOG,具有由中心单元的半径确定的变量、单元是否径向分裂、外单元的宽度等。
无论邻域如何组织,它们通常重叠形成整个图像的特征向量。换言之,单元和块在水*方向和垂直方向上横移图像,一次只有几个像素,以覆盖整个图像。
邻域结构的主要组成部分是多层次的组织和重叠的窗口,其在图像上移动。在深度学习网络的设计中使用了相同的成分。
什么样的归一化?
归一化处理出特征描述符,使得它们具有可比的大小。它是缩放的同义词,我们在第 4 章中讨论过。我们发现,文本特征的特征缩放(以 tf-idf 的形式)对分类精度没有很大影响。图像特征与文字区别很大,其对在自然图像中出现的照明和对比度的变化可能是非常敏感的。例如,在强烈的聚光灯下观察苹果的图像,而不是透过窗户发出柔和的散射光。即使物体是相同的,图像梯度也会有非常不同的幅度。为此,计算机视觉中的图像特征通常从全局颜色归一化开始,以消除照度和对比度方差。对于 SIFT 和 HOG 来说,结果表明,只要我们对特征进行归一化,这种预处理是不必要的。
SIFT 遵循归一化-阈值-归一化方案。首先,块特征向量归一化为单位长度(L2 标准化)。然后,将特征剪辑除以最大值以摆脱极端的照明效果,如从相机的色彩饱和度。最后,将剪切特征再次归一化到单位长度。
HOG 论文实验涉及不同的归一化方案例如 L1 和 L2,包括 SIFT 论文中标归一化-阈值-归一化方案。他们发现 L1 归一化比其他的方法稍显不靠谱。
SIFT 结构
SIFT 需要相当多的步骤。HOG 稍微简单,但是遵循许多相同的基本步骤,如梯度直方图和归一化。图 8-6 展示了 SIFT 体系结构。从原始图像中的感兴趣区域开始,首先将区域划分为网格。然后将每个网格单元进一步划分为子网格。每个子网格元素包含多个像素,并且每个像素产生梯度。每个子网格元素产生加权梯度估计,其中权重被选择以使得子网格元素之外的梯度可以贡献。然后将这些梯度估计聚合成子网格的方向直方图,其中梯度可以具有如上所述的加权投票。然后将每个子网格的方向直方图连接起来,形成整个网格的长梯度方向直方图。(如果网格被划分为2x2子网格,那么将有 4 个梯度方向直方图拼接成一个。)这是网格的特征向量。从这开始,它经过一个归一化-阈值-归一化过程。首先,将向量归一化为单位范数。然后,将单个值剪辑除以最大阈值。最后,再次对阈值向量进行归一化处理。这是图像块的最终 SIFT 特征描述。

基于深度神经网络的图像特征提取
SIFT 和 HOG 在定义良好的图像特征方面走了很久。然而,计算机视觉的最新成果来自一个非常不同的方向:深度神经网络模型。这一突破发生在 2012 的 ImageNet 挑战中,多伦多大学的一组研究人员几乎将前一年的获奖者的错误率减半。他们强调他们的方法是“深度学习”。与以前的神经网络模型不同,最新一代包含许多层叠在彼此之上的神经网络层和变换。ImageNet 2012 的获奖模型随后被称为 AlexNet ,其神经网络有 13 层。之后 IMANET 2014 的获胜者有 27 层。
从表面上看,叠层神经网络的机制与 SIFT 和 HOG 的图像梯度直方图有很大的不同。但是 AlxNETA 的可视化显示,前几层本质上是计算边缘梯度和其他简单的操作,很像 SIFT 和 HOG。随后的层将局部模式组合成更全局的模式。最终的结果是一个比以前更强大的特征提取器。
堆叠层的神经网络(或任何其他分类模型)的基础思想不是新的。但是,训练这种复杂的模型需要大量的数据和强大的计算能力,这是直到最*才有的。ImageNet 数据集包含来自 1000 个类的 120 万个图像的标记集。现代 GPU 加速了矩阵向量计算,这是许多机器学习模型的核心,包括神经网络。深度学习方法的成功取决于大量可用的数据和大量的 GPU 小时。
深度学习架构可以由若干类型的层组成。例如,AlxNETs 包含卷积、全连接层、归一化层和最大池化层。现在我们将依次查看每一层的内容。
全连接层
所有神经网络的核心是输入的线性函数。我们在第 4 章中遇到的逻辑回归是神经网络的一个示例。全连接的神经网络只是所有输入特征的一组线性函数。回想一个线性函数可以被写为输入特征向量与权重向量之间的内积,加上一个可能的常数项。线性函数的集合可以表示为矩阵向量乘积,其中权重向量成为权重矩阵。
全连接层的数学定义

W的每一行是将整个输入向量X映射成Z中的单个输出的权重向量。b是表示每个神经元恒定偏移(或偏置)的标量。
全连接层之所以如此命名,是因为在每一个输入都要在每个输出中使用。在数学上,这意味着对矩阵W中的值没有限制。(如我们将很快看到的,卷积层仅利用每个输出的一小部分输入。)在图中,一个完全连接的神经网络可以由一个完整的二部图表示,其中前一层的每个结点输出都连接到下一层的每个输入。

全连接层包含尽可能多的参数。因此,它们是昂贵的。这种密集连接允许网络检测可能涉及所有输入的全局模式。由于这个原因,AlexNet 的最后两层完全连接。在输入为条件下输出仍然是相互独立的。
卷积层
与全连接层相反,卷积层仅使用每个输出的输入子集。通过在输入上移动窗,每次使用几个特征产生输出。为了简单起见,可以对输入的不同集合使用相同的权重,而不是重新学习新权重。数学上,卷积算子以两个函数作为输入,并产生一个函数作为输出。它翻转一个输入函数,将其移动到另一个函数上,并在每个点上在乘法曲线下输出总面积。计算曲线下总面积的方法是取其积分。操作符在输入中是对称的,这意味着不管我们翻转第一个输入还是第二个输入,输出都是一样的。
卷积定义为

我们已经看到了一个简单的卷积的示例,当我们看着图像梯度(“图像梯度”)。但是卷积的数学定义似乎仍有点复杂。用信号处理的示例来解释卷积后的结果是最容易的。
想象一下,我们有一个小黑匣子。为了看到黑匣子的作用,我们通过一个单一的刺激单位。无论输出看起来如何,我们记录在一张纸上。我们等到对最初的刺激没有反应为止。随时间变化的函数称为响应函数;我们称之为响应函数g(t)。
想想一下现在我们有一个疯狂的函数f(t),随后将它输入黑盒中。在时间t=0时,f(0)与黑盒进行通讯,随后用f(0)乘以g(0)。在时间t=1,f(1)进入黑盒,随后与g(0)相乘。在相同的时间,黑盒持续回复信号f(0),它现在是月g(1)相乘了。所以在t=1时的输出为(f(0)*g(1))+(f(1)*g(0))。在t=2时,输出会变得更复杂,当f(2)进入图片后,这时f(0)与f(1)持续产生回复。所以在t=2时的总输出为(f(0)*g(2))+(f(1)*g(1))+(f(2)*g(0))。通过这种方式,响应函数在时间上有效地被翻转,其中τ=0总是与当前进入黑匣子的信息进行交互,并且响应函数的尾部与先前发生的函数进行交互。图 8—8 展示了这一过程。到目前为止,为了便于描述,我们已经把时间离散了。在现实中,时间是连续的,所以总和是一个积分。
这个黑箱被称为线性系统,因为它不比标量乘法和求和更疯狂。卷积算子清楚地捕捉线性系统的影响。
卷积的思想
卷积算子捕获线性系统的效果,该线性系统将输入信号与其响应函数相乘,求出所有过去输入响应的和。
在上面的示例中,g(t)用来表示响应函数,f(t)表示输入。但是由于卷积是对称的,响应和输出实际上并不重要。输出只是两者的结合。g(t)也称为滤波器。

图像是二维信号,所以我们需要一个二维滤波器。二维卷积滤波器通过取两个变量的积分来推广一维情形。由于数字图像具有离散像素,卷积积分变成离散和。此外,由于像素的数量是有限的,滤波函数只需要有限数量的元素。在图像处理中,二维卷积滤波器也被称为核或掩模。
2 维卷积的离散定义

当将卷积滤波器应用于图像时,我们不需要定义一个覆盖整个图像的巨型滤波器。相反,只覆盖几个像素的滤波器就够了,并且在图像上应用相同的滤波器,并在在水*和垂直像素方向上移动。
因为在图像中使用相同的滤波器,所以我们只需要定义一组小的参数。权衡是滤波器只能在一个小像素邻域内吸收信息。换言之,卷积神经网络识别局部信息而不是全局信息。
卷积滤波器示例
在这个示例中,我们将高斯滤波器应用于图像。高斯函数在零附*形成光滑对称的图形。滤波器在附*函数值产生加权*均值。当应用于图像时,它具有模糊附*像素值的效果。二维高斯滤波器是由
定义的,其中σ为高斯函数的标准差,它控制着图形的宽度。
在下面的代码示例中,我们将首先创建二维高斯滤波器,然后将它与猫图像进行卷积以产生模糊的猫。注意,这不是计算高斯滤波器的最精确的方法,但它是最容易理解的。采取在每个离散点的加权*均值,而不是简单的点估计是更好的实现方法。
>>> import numpy as np
# First create X, Y meshgrids of size 5x5 on which we compute the Gaussian
>>> ind = [-1., -0.5, 0., 0.5, 1.]
>>> X,Y = np.meshgrid(ind, ind)
>>> X array([[-1. , -0.5, 0. , 0.5, 1. ], [-1. , -0.5, 0. , 0.5, 1. ], [-1. , -0.5, 0. , 0.5, 1. ], [-1. , -0.5, 0. , 0.5, 1. ], [-1. , -0.5, 0. , 0.5, 1. ]])
# G is a simple, unnormalized Gaussian kernel where the value at (0,0) is 1.0
>>> G = np.exp(-(np.multiply(X,X) + np.multiply(Y,Y))/2) >>> G array([[ 0.36787944, 0.53526143, 0.60653066, 0.53526143, 0.36787944], [ 0.53526143, 0.77880078, 0.8824969 , 0.77880078, 0.53526143 ], [ 0.60653066, 0.8824969 , 1. , 0.8824969 , 0. 60653066], [ 0.53526143, 0.77880078, 0.8824969 , 0.77880078, 0.53526143 ], [ 0.36787944, 0.53526143, 0.60653066, 0.53526143, 0.36787944 ]])
>>> from skimage import data, color
>>> cat = color.rgb2gray(data.chelsea())
>>> from scipy import signal
>>> blurred_cat = signal.convolve2d(cat, G, mode='valid')
>>> import matplotlib.pyplot as plt
>>> fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,4), ... sharex=True, sharey=True)
>>> ax1.axis('off')
>>> ax1.imshow(cat, cmap=plt.cm.gray)
>>> ax1.set_title('Input image')
>>> ax1.set_adjustable('box-forced')
>>> ax2.axis('off')
>>> ax2.imshow(blurred_cat, cmap=plt.cm.gray)
>>> ax2.set_title('After convolving with a Gaussian filter')>>> ax2.set_adjustable('box-forced')

AlxNet 中的卷积层是三维的。换言之,它们对前一层的三维像素进行操作。第一卷积神经网络采用原始 RGB 图像,并在所有三个颜色通道中学习用于局部图像邻域的卷积滤波器。随后的层跨越空间和内核尺寸将其作为输入像素。
整流线性单元(Relu)变换
神经网络的输出通常通过另一个非线性变换,也称为激活函数。常见的选择是 tanh 函数(在 -1 和 1 之间有界的光滑非线性函数),sigmoid 函数(0 到 1 之间的*滑非线性函数),或者称为整流线性单元的函数(Relu)。Relu 是一个线性函数的简单变化,其中负部分被归零。换言之,它修剪了负值,但留下无穷的正边界。Relu 的范围从 0 到无穷大。
整流线性单元(Relu)是线性函数,负部分归零。

tanh 函数,在 -1 和 1 之间有界的光滑非线性函数。

sigmoid 函数,0 到 1 之间的*滑非线性函数。


Relu 变换对原始图像或高斯滤波器等非负函数没有影响。然而,经过训练的神经网络,无论是完全连接的还是卷积的,都有可能输出负值。AlxNet 使用 Relu 代替其他变换,在训练过程中可以更快的收敛,它适用于每一个卷积和全连接层。
响应归一化层
在第 4 章和本章之前的讨论之后,归一化对大家来说应该是一个熟悉的概念。归一化将个体输出通过集体总响应的函数来划分。因此,理解归一化的另一种方式是,它在邻居之间产生竞争,因为现在每个输出的强度都相对于其邻居进行测量。AlexNet 在不同内核的每个位置上归一化输出。
局部响应归一化引起相邻核之间的竞争

其中,Xk是第k个核的输出,yk 是相对于邻域中的其他核的归一化响应。对每个输出位置分别执行归一化。换言之,对于每个输出位置(i, j),在附*的卷积核输出上进行归一化。注意,这与在图像邻域或输出位置上的归一化不相同。内核邻域的尺寸c、α和β的大小超参数都是通过图像验证集调整的。

池化层
池化层将多个输入组合成单个输出。当卷积滤波器在图像上移动时,它为其尺寸下的每个邻域生成输出。池化层迫使局部图像邻域产生一个值而不是许多值。这减少了在深度学习网络的中间层中的输出数量,这有效地减少了过拟合训练数据的概率。
有多种方法汇集输入:*均,求和(或计算一个广义范数),或取最大值。池化层通过图像或中间输出层移动。Alxnet 使用最大池化层,以 2 像素(或输出)的步幅移动图像,并在 3 个邻居之间汇集。

AlexNet 的结构
AlexNet 包含 5 个卷积层、2 个响应归一化层、3 个最大池化层和 2 个全连接层。与最终分类输出层一起,模型中共有 13 层神经网络层,形成 8 层层组。详情请参阅图 8-12。
输入图像首先被缩放到256x256像素。输入实际上是224x224大小的随机图像,具有 3 个颜色通道。前两个卷积层各有一个响应归一化层和一个最大池化层,最后一个卷积层接着是最大池化层。原始文件用两个 GPU 来分割训练数据和计算。层之间的通信主要限于在相同的 GPU 内。在层组 2 和 3 之间,并且在层组 5 之后是例外的。在这些边界点,下一层从两个 GPU 上的前一层的内核的像素作为输入。每个中间层都使用 Relu 变换。


注意 AlexNet 是 SIFT/HOG 特征提取器的梯度直方图标准化规范化体系结构(参见图 8 -6),但具有更多的层。(这就是“深度学习”中的“深度”)不同于 SIFT/HOG,卷积核和全连接权值是从数据中学习的,而不是预定义的。此外,SIFT 中的归一化步骤在整个图像区域上遍及特征向量执行,而 AlexNet 中的响应归一化层在卷积核上归一化。
深入的来看,模型从局部图像邻域中提取特征开始。每个后续层建立在先前层的输出上,有效地覆盖原始图像的相继较大区域。因此,即使前五个卷积层都具有相当小的内核宽度,后面的层依然能够制定更多的全局特征。端部的全连接层是最具全局性的。
尽管特征的要点在概念上是清晰的,但是很难想象每个层挑选出的实际特征。图 8- 14 和图 8-15 显示了由特征型学习的前两层卷积核的可视化。第一层包括在不同的方向上的灰度边缘和纹理的检测器,以及颜色斑点和纹理。第二层似乎是各种光滑图案的检测器。


尽管该领域有巨大的进步,图像特征仍然是一门艺术而不是科学。十年前,人工制作的特征提取步骤结合了图像梯度、边缘检测、定位、空间提示、*滑和归一化等。如今,深度学习架构师构建了封装相同想法的模型,但是这些参数是从训练图像中自动学习的。
总结
接*尾声,我们在直觉上更好地理解为什么最直接和简单的图像特征在执行任务时将永远不是最有用的,如图像分类。与其将每个像素表示为原子单位相反,更重要的是考虑像素与它们附*的其他像素之间的关系。我们可以将这些技术如 SIFT 和 HOG 一样,通过分析邻域的梯度更好地提取整个图像的特征,发展技术以适应其他任务。
*年来的又一次飞跃将更深层次的神经网络应用于计算机视觉,以进一步推动图像的特征提取。这里要记住的重要一点是,深度学习堆叠了许多层的神经网络和相互转换。这些层中的一些,当单独检查时,开始梳理出类似的特征,这些特征可以被识别为人类视觉的构建块:定义线条、梯度、颜色图。
附录、线性模型和线性代数基
线性分类概述
当我们有一个已经标记的数据集时,特征空间散布着来自不同类别的数据点。分类器的工作是将不同类别的数据点分开。它可以通过生成一个数据点与另一个数据点非常不同的输出来实现。例如,当这里只有两个类别的时候,一个好的分类器应该为一个类别产生大量的输出,而另一个则为小的输出。作为一个类别而不是另一个类别的点就形成了一个决策*面。

图 A-1:简单的二元分类找到了一个分离两类数据点的曲面
许多函数能被当作分类器。这是一个寻找能完全分离不同类的简单函数的好方法。首先,相比于寻找最复杂的分类器,寻找最简单的分类器更容易。而且,简单函数常常能更好地适应新数据,要将它们与训练数据(特别是过度拟合)相比。一个简单的模型也许会出错,就像上图一些数据点被分到了错误的一边。但是我们牺牲了一些训练的准确性,以便有一个更简单的决策*面,可以达到更好的测试精度。最大限度减少复杂性和最大限度增加精度被叫做“奥卡姆剃刀”,广泛适用于科学与工程。
最简单的函数是一条直线。一个一元线性函数是我们最熟悉的。

图 A-2:一元线性函数
二元线性函数可以显示为 3D 中的*面或 2D 中的等高线图(如图 A-3)。与拓扑地理图一样,等高线图的每一行代表输入空间中具有相同输出的点。

图 A-3:二维线性函数的等高线图
可视化更高维度的线性函数很难,它们被称作超*面。但是写出它们的代数公式还是很容易的。一个多维的线性模型有一个输入集合x1,x2,…,xn和一个权重参数集合w0,w1,…,wn:fw(x1,x2,…,xn) = w0+w1*x1+w2*x2+…+wn*xn。这个公式能被写成更简洁的向量形式 \(fw(X)=X^T W\)。 我们遵循通常的数学符号惯例,它使用粗体来表示一个向量和非粗体来表示一个标量。向量x在开始处用一个额外的 1 填充,作为截距项w0的占位符。如果所有输入特征都为零,那么函数的输出是w0。 所以w0也被称为偏差或截距项。
训练线性分类器相当于在类之间挑出最佳分离超*面。这意味着找到在空间中精确定位的最佳矢量w。由于每个数据点都有一个目标标签y,我们可以找到一个w,它试图直接模拟目标的标签 \(x^T w=y\)。
由于通常有多个数据点,我们需要一个w同时使所有的预测都接*目标标签:
方程 A-1:线性模型公式Aw=y
这里,A被称为数据矩阵(在统计中也被称为设计矩阵)。它包含特定形式的数据:每行是数据点,每列都是一个特征。(有时人们也会看到它的转置,其中的特征以行的形式显示,数据以列的形式显示。)
矩阵的解析
为了解决方程 A-1,我们需要一些线性代数的基本知识。为了系统地介绍这个主题,我们强烈推荐 Gilbert Strang 的书《Linear Algebra and Its Applications》(线性代数及其应用)。
方程 A-1 指出,当某个矩阵乘以某个向量时,会有一定的结果。一个矩阵也被叫做一个线性算子,这个名称使得矩阵更像一台小机器。该机器将一个矢量作为输入,并使用多个关键操作的组合来推导出另一个矢量:一个矢量方向的旋转,添加或减去维度,以及拉伸或压缩其长度。这种组合在输入空间中操纵形状时非常有用(见图 A-4)。

图 A-4:3 x 2矩阵可以将 2D 中的正方形区域转换为 3D 中的菱形区域。 它通过将输入空间中的每个向量旋转并拉伸到输出空间中的新向量来实现。
从矢量到子空间
为了理解线性算子,我们必须看看它如何将输入变为输出。幸运的是,我们不必一次分析一个输入向量。向量可以组织成子空间,而线性算子则可以处理向量子空间。子空间是一组满足两个标准的向量:(1)如果它包含一个向量,那么它包含通过原点和该点的直线,以及(2)如果它包含两个点,则它包含所有线性组合 的这两个向量。 线性组合是两种操作类型的组合:将矢量与标量相乘,并将两个矢量相加在一起。
子空间的一个重要属性是它的秩或维度,它是这个空间中自由度的度量。一条线的秩为 1,2D *面的秩为 2,依此类推。如果你可以想象在我们的多维空间中的多维鸟,那么子空间的秩告诉我们鸟可以飞行多少个“独立”的方向。这里的“独立”意思是“线性独立性”:如果一个不是另一个的常数倍,那么两个向量是线性独立的,即它们并不指向完全相同或相反的方向。
子空间可以被定义为一组基向量的范围。(跨度是描述一组向量的所有线性组合的技术术语。)一组向量的跨度在线性组合下是不变的(因为它是以这种方式定义的)。因此,如果我们有一组基向量,那么我们可以将这些向量乘以任何非零常数或者添加向量来获得另一个基。
有一个更独特和可识别的基来描述一个子空间将是很好的。标准正交基包含具有单位长度且彼此正交的矢量。正交性是另一个技术术语。(所有数学和科学中至少有 50%是由技术术语组成的,如果你不相信我,请在本书中做一个袋装词)。如果两个向量的内积是 零。 对于所有密集的目的,我们可以将正交矢量看作相互成 90 度角。(这在欧几里得空间中是真实的,这与我们的物理三维现实非常相似。)将这些向量标准化为单位长度将它们变成一组统一的测量棒。
总而言之,一个子空间就像一个帐篷,正交基矢量是支撑帐篷所需的直角杆数。秩等于正交基向量的总数。

图 A-5:四个有用的线性代数概念的插图:内积,线性组合,基向量和正交基向量。
对于那些在数学中思考的人来说,这里有一些数学来描述我们的描述。
有用的线性代数定义
- 标量:一个数
c,不是矢量 - 向量:\(x=(x_1,x_2,…,x_n)\)
- 线性组合:\(ax+by=ax_1+by_1,ax_2+by_2,…,ax_n+by_n\)
- 向量组的范围:向量组 \(u=a_1 v_1+⋯+a_k v_k\),\(a_1,…,a_k\) 任意
- 线性独立性:
x和y相互独立,如果x≠cy,c是任何标量常量 - 内在产品:\(x,y=x_1 y_1+x_2 y_2+⋯+x_n y_n\)
- 正交矢量:如果
x,y=0,两个向量x,y正交 - 子空间:包含更大的矢量空间中的矢量子集,满足这三个标准:
- 包含 0 向量
- 如果包含一个向量
v,那么也包含所有向量cv,其中c是一个标量。 - 如果包含两个向量
u和v,那么也包含u+v
- 基:一组跨向子空间的向量
- 正交基:一个基 \({v_1,v_2,…,v_d}\),\(v_i,v_j=0\),
i,j任意 - 子空间的秩:跨越子空间的线性无关基向量的最小数目。
奇异值分解(SVD)
矩阵对输入向量执行线性变换。线性变换非常简单且受到限制。因此,矩阵不能无情地操纵子空间。线性代数最迷人的定理之一证明了每一个方阵,无论它包含什么数字,都必须将某一组矢量映射回自己,并进行一些缩放。在矩形矩阵的一般情况下,它将一组输入向量映射为相应的一组输出向量,其转置将这些输出映射回原始输入。技术术语是正方形矩阵具有特征值的特征向量,矩形矩阵具有奇异值的左右奇异向量。
特征向量和奇异向量
设A是一个n*n的矩阵。如果这里有一个向量v和一个标量λ,使Av=λv,v称作特征向量,λ称为A的一个特征值。
让A成为一个长方形矩阵。如果这里有一个向量u和一个向量v和一个标量σ,那么Av=σu,\(A^T u=σv\),u和v被叫做左右奇异向量,σ是A的奇异值。
代数上,矩阵的 SVD 看起来像这样:
\(A=U \Sigma V^T\)
其中矩阵U和V的列分别形成输入和输出空间的正交基。Σ是一个一个包含奇异值的对角矩阵。
在几何学上,矩阵执行以下的变换序列(见图 A-6):
- 将输入向量映射到右奇异向量基 V 上
- 通过相应的奇异值缩放每个坐标
- 将此分数与每个左奇异向量相乘
- 总结结果
当A是实矩阵(即所有元素都是实值)时,所有的奇异值和奇异向量都是实值的。奇异值可以是正数,负数或零。矩阵的有序奇异值集称为谱,它揭示了很多矩阵。奇异值之间的差距会影响解的稳定性,最大绝对奇异值与最小绝对奇异值之间的比值(条件数)会影响迭代求解器的求解速度。这两个属性都对可找到的解决方案的质量产生显着影响。

图 A-6:矩阵分解成三个小机器:旋转,缩放,旋转。
操作从右到左进行矩阵向量乘法。最右边的机器旋转并潜在地将输入投影到较低维空间中。在这个例子中,输入立方体变成了一个扁*的正方形,并且也被旋转了。下一台机器在一个方向挤压正方形,并将其拉伸到另一个方向;正方形变成矩形。最后一个最左边的机器再次旋转矩形,并将其投影回可能更高的空间。但它仍然是一个扁*的矩形,而不是一些更高维的对象。
数据矩阵的四个基本子空间
解析矩阵的另一种有用方法是通过四个基本子空间:列空间,行空间,右空间和左空间。这四个子空间完全刻画了涉及A或 \(A^T\) 的线性系统的解决方案。因此它们被称为四个基本子空间。对于数据矩阵,可以根据数据和特征理解四个基本子空间。让我们更详细地看看它们。
数据矩阵
A:行是数据点,列是特征。
列空间
数学定义:
当我们改变权值向量w时,输出向量s的集合,其中s=Aw
数学解释:
所有可能的列的组合。
数据解释:
根据观察到的特征,所有结果都是线性可预测的,向量w包含每个特征的权重。
基本原理:
对应非零奇异值的左奇异向量(U列的子集)。
行空间
数学定义:
当我们改变权值向量u时,输出向量的集合 \(r=u^T A\)
数学解释:
所有可能的行线性组合
数据解释:
行空间中的向量可以表示为现有数据点的线性组合。因此,这可以解释为已有数据的空间。向量u包含线性组合中每个数据点的权重。
基本原理:
对应非零奇异值的右奇异向量(V列的子集)
零空间
数学定义:
输入向量w的集合,其中Aw=0。
数学解释:
正交于A的所有行的向量。零空间被矩阵压缩为 0。这是“fluff”,它增加了Aw=y的解空间的体积。
数据解释:
新数据点,它不能被表示为现有数据点的任何线性组合
基本原理:
对应零奇异值的右奇异向量(V的其余列)
左零空间
数学定义:
输入向量u的集合,其中 \(u^T A=0\)
数学解释:
正交于A的所有列的向量。左零空间正交于列空间。
数据解释:
不能用现有特征的线性组合来表示的新特征向量。
基本原理:
对应零奇异值的左奇异向量(U的其余列)
列空间和行空间包含根据观察到的数据和特性所能表示的内容。列空间中的这些向量是非新特征。那些位于行空间中向量都是非新的数据点。
对于建模和预测的目的,非新奇性是好的。完整的列空间意味着特征集包含足够的信息来建模我们的任何目标向量的愿望。一个完整的行空间意味着不同的数据点包含足够的变化来覆盖特性空间的所有可能的角落。我们要担心的是新的数据点和特征,它们分别包含在零空间和左零空间中。
在建立数据线性模型的应用,零空间中也可以看作是“新”数据点的子空间。在这种情况下,新奇并不是一件好事。新数据点是训练集不能线性表示的虚数据,同样,左零空间包含的新特征不能用现有特征的线性组合表示。
零空间正交于行空间。原因很简单,零空间的定义是w的内积为 0,每一个行向量都是A。因此,w正交于由这些行向量张成的空间即行空间。类似地,左零空间正交于列空间。
求解一个线性系统
让我们把所有这些数学问题都归结到手头的问题上:训练一个线性分类器,它与解决线性系统的任务密切相关。我们仔细看一个矩阵是如何运作的,因为我们要逆向设计它。为了建立一个线性模型,我们必须找到输入权向量w映射到观察到的输出目标y在系统Aw=y,其中A是数据矩阵。让我们试着把线性算子的机器反过来转动。如果我们有A的 SVD 分解,那么我们可以把y映射到左奇异向量(U的列),反转比例因子(乘以非零奇异值的逆)最后把它们映射回正确的奇异向量(V的列),很简单,是吧?
这实际上是计算a的伪逆的过程,它利用了一个标准正交基的一个关键性质:转置就是逆。这就是 SVD 如此强大的原因。(在实践中,真正的线性系统求解器不使用 SVD,因为它们计算是相当昂贵。还有其他更便宜的方法来分解矩阵,比如 QR、LU 或乔列斯基分解)。
然而,我们在匆忙中漏掉了一个小细节。如果奇异值为零会怎样?我们不能取零为倒数,因为1/0=∞。这就是为什么它叫伪逆。(矩形矩阵没有真实逆的定义,只有方阵(只要所有特征值非零)。无论输入什么,它的奇异值都为零;没有办法重新跟踪它的步骤并得出原始的输入。
好吧,让我们倒回来看这个小细节。让我们带着我们学到的,再往前走看看能不能把机器拆开。假设我们得到了Aw=y的答案,我们称它为特定的w,因为它特别适合y。假设还有一堆输入向量A被压到 0。让我们选一个,称它为 Wsad-trumpet。因为 wah。那么当我们把 Wparticular 添加到 Wsad-trumpet,您认为会发生什么呢?
A(Wparticular + Wsad-trumpet) = y
神奇!这也是一个解。事实上,任何被压缩到零的输入都可以被添加到一个特定的解中,并解决方案是这样的:
Wgeneral = Wparticular + Whomogeneous
Wparticular 是方程Aw=y的精确解,可能有也可能没有这样的解。如果没有,那么系统只能*似地解决。如果有,那么y属于已知的a的列空间,列空间是A可以映射到的向量集合,通过它的列的线性组合。
Whomogeneous 是方程Aw = 0的解。(Wsadd-trumpet 的完整名称是“Whomogeneous”。)这一点现在应该很熟悉了。所有的广义向量的集合构成了a的零空间,这是具有奇异值为 0 的右奇异向量张成的空间。
“零空间”这个名字听起来像是一场生死攸关的危机的结局。如果零空间包含除零向量之外的任何向量,那么就有方程Aw = y有无穷多个解,有太多的解可供选择
来自本身并不是一件坏事。有时任何解决方案都可以。
但是如果有许多可能的答案,那么就有许多对分类任务有用的特性集。很难理解哪些是真正重要的。
解决大零空间问题的一种方法是通过添加来调节模型
额外的约束:
Aw = y,其中 ww = c
这种正则化形式使得权向量具有一定的范数。这种正则化的强度是由正则化参数控制的,正则化参数必须进行调整,就像我们在实验中所做的那样。
一般来说,特性选择方法处理选择最有用的特性来减少计算负担,减少模型的混乱程度,并使学习的模式更独特。这是“特性选择”的重点。
另一个问题是数据矩阵的“不均匀性”。当我们训练一个线性分类器时,我们不仅关心线性系统有一个通解,而且关心我们能轻易地找到它。通常,训练过程使用一个求解器,它通过计算损失函数的梯度和以小步骤下坡来工作。当某些奇异值非常大,而另一些非常接*于零时,求解者需要仔细地遍历较长的奇异向量(那些对应于较大奇异值的向量),并花费大量时间挖掘较短的奇异向量以找到真正的答案。在光谱中这种“不均匀性”是由矩阵的条件数来衡量的,它基本上是最大和最小绝对值之间的比值。
综上所述,为了有一个相对独特的好的线性模型,为了让它容易被发现,我们希望:
1、标签向量可以通过特征子集(列向量)的线性组合很好地逼*。更好的是,特性集应该是线性无关的。
2、为了使零空间很小,行空间必须很大。(这是因为这两个子空间是正交的。)数据点(行向量)的集合越线性无关,零空间就越小。
•为了便于求解,数据矩阵的条件数——最大奇异值与最小奇异值之比——应该很小。
分类器的概述
使用统计分类或分类器的方法来识别一个或者一组新的观察将属于哪个类别。书中引用的大多数方法都属于监督学习领域,在监督学习中,分类是通过使用预先确定的类别标记的训练数据进行算法确定的。我们将在这里给出这些算法的高级视图,仅仅足以理解它们在文本中的应用。(实际上,维基百科是你最好的朋友,因为它在每一种方法中都能找到很好的参考文献。)
支持向量机的径向基函数核
支持向量机径向基函数核(RBF SVM)
在 Scikit Learn 的 sklearn.gaussian_process.kernes.RBF 包
K-最*邻
K-最*邻(kNN)
Scikit Learn 的 sklearn-neighbors 包
随机森林
随机森林(RF)
随机森林分类器在 Scikit Learn 的 sklearn.ensembles 包中
梯度提升树
梯度提升树(GBT)
梯度提升树分类器在 Scikit Learn 的 sklearn.ensembles 包中
严格地说,这里给出的公式是线性回归,而不是线性分类。不同的是,回归允许实值目标变量,而分类目标通常是表示不同类的整数。一个回归子可以通过非线性的变换转化为一个分类器。例如,logistic 回归分类器通过 logistic 函数传递输入的线性变换。这些模型被称为广义线性模型,其核心是线性函数。虽然这个例子是关于分类的,但是我们使用线性回归公式作为教学工具,因为它更容易分析。直觉容易映射到广义线性分类器
实际上,它比那要复杂一些。\(y\)可能不在它的列空间中,所以这个方程可能没有解。统计机器学习不是放弃,而是寻找一个*似的解决方案。它定义了一个量化解决方案质量的损失函数。如果解是精确的,那么损失是 0。小错误,小损失;大错误,大损失等等。然后,训练过程寻找最佳参数来最小化这个损失函数。在普通线性回归中,损失函数称为*方剩余损失,它本质上是将\(y\)映射到列空间中\(A\)的最*点。逻辑回归最小化了日志丢失。在这两种情况下,以及一般的线性模型中,线性系统\(Aw = y\)通常位于核心。因此,我们的分析是非常相关的。


浙公网安备 33010602011771号