Python 高级机器学习(二)
六、文本特征工程
简介
在前面的章节中,我们花时间评估了能够分析复杂或挑战性数据的强大技术。然而,对于最困难的问题,正确的技巧只会让你走得更远。
深度学习和监督学习试图解决的持续挑战是,找到解决方案通常需要相关团队的多项重大投资。在旧的模式下,人们经常不得不执行特定的准备任务,这需要时间、专业技能和知识。通常,甚至使用的技术也是特定于领域和/或特定于数据类型的。通过这个过程,特征被导出,被称为特征工程。
到目前为止,我们研究的大部分深度学习算法都是为了帮助寻找方法,避免需要执行大量的特征工程。然而,与此同时,特征工程继续被视为顶级 ML 从业者的一项非常重要的技能。以下引述来自卡格尔的主要竞争对手,通过大卫·科福德·温德对卡格尔博客的贡献:
| | “你使用的功能比其他任何东西都更能影响结果。据我所知,没有一种算法能够补充正确的特征工程所带来的信息增益。” | | | | --(Luca massaron) |
| | “特征工程当然是 Kaggle 比赛中最重要的方面之一,也是一个人应该花最多时间的部分。数据中通常有一些隐藏的特征,可以大大提高你的表现,如果你想在排行榜上获得好的位置,你必须找到它们。如果你在这里搞砸了,你多半不会再赢了;总有一个人会发现所有的秘密。然而,还有其他重要的部分,比如你如何表述这个问题。你会使用回归模型或分类模型,甚至两者结合,还是需要某种排名。这一点以及功能工程对于在这些比赛中取得好成绩至关重要。也有一些比赛不再需要(手动)特征工程;比如图像处理比赛。当前最先进的深度学习算法可以为您做到这一点。” | | | | -- (约瑟夫·费格尔) |
这里有几个关键主题;特征工程功能强大,即使是极少量的特征工程也能对一个人的分类器产生很大的影响。如果你想得到最好的结果,经常需要使用特征工程技术。最大化机器学习算法的有效性需要一定数量的特定领域和特定数据类型的知识(秘密)。
再引用一句话:
| | “对于大多数卡格尔竞赛来说,最重要的部分是功能工程,这很容易学习。” | | | | - (蒂姆萨利曼) |
蒂姆没有错;在本章中,您将学到的大部分内容都是直观、有效的技巧和转换。本章将从自然语言处理和金融时间序列应用程序中,向您介绍一些应用于文本和时间序列数据的最有效和最常用的准备技术。我们将介绍这些技术是如何工作的,人们应该期望看到什么,以及人们如何诊断它们是否如期望的那样工作。
文本特征工程
在前面的章节中,我们已经讨论了一些方法,通过这些方法我们可以获取数据集并提取有价值特征的子集。这些方法具有广泛的适用性,但是在处理非数值/非分类数据,或者不容易转换为数值或分类数据的数据时,这些方法的帮助较小。特别是,在处理文本数据时,我们需要应用不同的技术。
我们将在本节中研究的技术分为两大类——清洁技术和功能准备技术。这些通常以大致的顺序实现,我们将相应地研究它们。
清理文本数据
当我们处理自然文本数据时,应用了一组不同的方法。这是因为在现实世界的上下文中,自然干净的文本数据集的想法是非常不安全的;文本数据充斥着拼写错误、表情符号之类的非字典构造,在某些情况下,还有 HTML 标记。因此,我们需要非常彻底地清洁。
在这一节中,我们将使用一个相当粗糙的真实数据集,使用一些有效的文本清理技术。具体来说,我们将使用 2012 年卡格尔竞赛的无常数据集,该竞赛的目标是创建一个模型,准确检测社会评论中的侮辱。
是的,我指的是网络巨魔检测。
我们开始吧!
用漂亮的字体清理文字
我们的第一步应该是手动检查输入数据。这非常关键;有了文本数据,需要尝试初步了解数据中存在哪些问题,从而识别出需要清理的地方。
通读一个充满可恶的互联网评论的数据集有点痛苦,所以这里有一个示例条目:
|身份证明
|
日期
|
评论
|
| --- | --- | --- |
| 132 | 20120531031917Z | """\xa0@Flip\xa0how are you not ded""" |
我们有一个似乎不需要太多工作的 ID 字段和日期字段。然而,文本字段非常具有挑战性。从这一个案例中,我们已经可以看到拼写错误和 HTML 包含。此外,数据集中的许多条目包含绕过发誓过滤的尝试,通常是在单词中间包含一个空格或标点元素。其他数据质量问题包括多个元音字母(扩展一个单词)、非 ascii 字符、超链接...名单还在继续。
清理此数据集的一个选项是使用正则表达式,该表达式在输入数据上运行以消除数据质量问题。然而,问题格式的数量和种类使得使用基于正则表达式的方法不切实际,至少从一开始是这样。我们很可能会错过很多案例,也会误判所需准备的数量,导致我们过于积极地清理,或者不够积极;具体来说,我们可能会切入真实的文本内容或留下部分标签。我们需要的是一个解决方案,它将首先解决大多数常见的数据质量问题,这样我们就可以用基于脚本的方法专注于剩余的问题。
进入BeautifulSoup。BeautifulSoup是一个非常强大的文本清理库,除了其他功能之外,它还可以删除 HTML 标记。让我们来看看这个关于巨魔数据的库:
from bs4 import BeautifulSoup
import csv
trolls = []
with open('trolls.csv', 'rt') as f:
reader = csv.DictReader(f)
for line in reader:
trolls.append(BeautifulSoup(str(line["Comment"]), "html.parser"))
print(trolls[0])
eg = BeautifulSoup(str(trolls), "html.parser")
print(eg.get_text())
|
身份证明
|
日期
|
评论
|
| --- | --- | --- |
| 132 | 20120531031917Z | @Flip how are you not ded |
正如我们所看到的,我们已经在提高文本数据的质量方面取得了进展。然而,从这些例子中也可以清楚地看出,还有很多工作要做!如上所述,让我们继续使用正则表达式来帮助进一步清理和标记我们的数据。
管理标点和标记
标记化是从文本流中创建一组标记的过程。许多标记是单词,而其他标记可能是字符集(例如笑脸或其他标点字符串,例如????????)。
现在,我们已经从初始数据集中移除了许多 HTML 丑陋之处,我们可以采取措施进一步提高文本数据的整洁度。为此,我们将利用re模块,它允许我们对正则表达式使用操作,例如子串替换。在这次传递中,我们将对输入文本执行一系列操作,主要集中在用标记替换变量或有问题的文本元素。让我们从一个简单的例子开始,用_EM令牌替换电子邮件地址:
text = re.sub(r'[\w\-][\w\-\.]+@[\w\-][\w\-\.]+[a-zA-Z]{1,4}', '_EM', text)
同样,我们可以移除 URL,用_U标记替换它们:
text = re.sub(r'\w+:\/\/\S+', r'_U', text)
我们可以自动删除多余或有问题的空白和换行符、连字符和下划线。此外,我们将开始处理多个字符的问题,这些字符通常用于非正式对话中的强调。扩展系列的标点符号在这里使用_BQ、BX等编码进行编码;这些较长的标签用于区别于更直接的_Q和_X标签(分别指问号和感叹号的使用)。
我们还可以使用正则表达式来管理额外的字母;通过将这样的字符串最多减少到两个字符,我们能够将组合的数量减少到可管理的数量,并使用_EL标记对减少的组进行标记:
# Format whitespaces
text = text.replace('"', ' ')
text = text.replace('\'', ' ')
text = text.replace('_', ' ')
text = text.replace('-', ' ')
text = text.replace('\n', ' ')
text = text.replace('\\n', ' ')
text = text.replace('\'', ' ')
text = re.sub(' +',' ', text)
text = text.replace('\'', ' ')
#manage punctuation
text = re.sub(r'([^!\?])(\?{2,})(\Z|[^!\?])', r'\1 _BQ\n\3', text)
text = re.sub(r'([^\.])(\.{2,})', r'\1 _SS\n', text)
text = re.sub(r'([^!\?])(\?|!){2,}(\Z|[^!\?])', r'\1 _BX\n\3', text)
text = re.sub(r'([^!\?])\?(\Z|[^!\?])', r'\1 _Q\n\2', text)
text = re.sub(r'([^!\?])!(\Z|[^!\?])', r'\1 _X\n\2', text)
text = re.sub(r'([a-zA-Z])\1\1+(\w*)', r'\1\1\2 _EL', text)
text = re.sub(r'([a-zA-Z])\1\1+(\w*)', r'\1\1\2 _EL', text)
text = re.sub(r'(\w+)\.(\w+)', r'\1\2', text)
text = re.sub(r'[^a-zA-Z]','', text)
接下来,我们希望开始创建其他感兴趣的标记。其中一个更有用的指标是骂人的_SW标记。我们还将使用正则表达式来帮助识别并标记四个桶中的一个;大而快乐的微笑(_BS)、小而快乐的微笑(_S)、大而悲伤的微笑(_BF)和小而悲伤的微笑(_F):
text = re.sub(r'([#%&\*\$]{2,})(\w*)', r'\1\2 _SW', text)
text = re.sub(r' [8x;:=]-?(?:\)|\}|\]|>){2,}', r' _BS', text)
text = re.sub(r' (?:[;:=]-?[\)\}\]d>])|(?:<3)', r' _S', text)
text = re.sub(r' [x:=]-?(?:\(|\[|\||\\|/|\{|<){2,}', r' _BF', text)
text = re.sub(r' [x:=]-?[\(\[\|\\/\{<]', r' _F', text)
注
由于它们的用法经常变化,所以表情符号很复杂;虽然这一系列的角色相当流行,但绝不完整;例如,有关一系列非 ascii 表示形式,请参见表情符号。出于几个原因,我们将从这个例子中删除非 ascii 文本(类似的方法是使用字典来强制遵从),但是这两种方法都有一个明显的缺点,即它们从数据集中删除了案例,这意味着任何解决方案都是不完美的。在某些情况下,这种方法可能会导致删除大量数据。因此,一般来说,明智的做法是意识到文本内容中基于字符的图像面临的普遍挑战。
接下来,我们要开始将文本拆分成短语。这是str.split的一个简单应用,它使输入能够被视为单词(单词)的向量,而不是长字符串(re):
phrases = re.split(r'[;:\.()\n]', text)
phrases = [re.findall(r'[\w%\*&#]+', ph) for ph in phrases]
phrases = [ph for ph in phrases if ph]
words = []
for ph in phrases:
words.extend(ph)
这给了我们以下信息:
|身份证明
|
日期
|
评论
|
| --- | --- | --- |
| 132 | 20120531031917Z | [['Flip', 'how', 'are', 'you', 'not', 'ded']] |
接下来,我们对单字母序列执行搜索。有时,为了强调,互联网交流包括使用间隔的单字母链。这可以尝试作为一种避免检测诅咒词的方法:
tmp = words
words = []
new_word = ''
for word in tmp:
if len(word) == 1:
new_word = new_word + word
else:
if new_word:
words.append(new_word)
new_word = ''
words.append(word)
到目前为止,我们在清理和提高输入数据的质量方面已经走了很长的路。然而,仍然存在悬而未决的问题。让我们重新考虑我们开始的例子,现在看起来如下:
|身份证明
|
日期
|
话
|
| --- | --- | --- |
| 132 | 20120531031917Z | ['_F', 'how', 'are', 'you', 'not', 'ded'] |
我们早期的清理已经忽略了这个例子,但是我们可以看到向量化句子内容以及现在清理的 HTML 标签的效果。我们还可以看到使用的表情已经通过_F标签捕捉到了。当我们看一个更复杂的测试用例时,我们会看到更实质性的变化结果:
生的
|
清洁并分离
|
| --- | --- |
| GALLUP DAILY\nMay 24-26, 2012 \u2013 Updates daily at 1 p.m. ET; reflects one-day change\nNo updates Monday, May 28; next update will be Tuesday, May 29.\nObama Approval48%-\nObama Disapproval45%-1\nPRESIDENTIAL ELECTION\nObama47%-\nRomney45%-\n7-day rolling average\n\n It seems the bump Romney got is over and the president is on his game。 | ['GALLUP', 'DAILY', 'May', 'u', 'Updates', 'daily', 'pm', 'ET', 'reflects', 'one', 'day', 'change', 'No', 'updates', 'Monday', 'May', 'next', 'update', 'Tuesday', 'May', 'Obama', 'Approval', 'Obama', 'Disapproval', 'PRESIDENTIAL', 'ELECTION', 'Obama', 'Romney', 'day', 'rolling', 'average', 'It', 'seems', 'bump', 'Romney', 'got', 'president', 'game'] |
但是有两个显著的问题在两个例子中仍然很明显。在第一种情况下,我们有一个拼错的单词;我们需要找到消除这种情况的方法。其次,两个例子中的很多单词(例如。pm)本身并没有太多的信息。我们发现的问题是,特别是对于较短的文本样本,清理后剩下的内容可能只包含一两个有意义的术语。如果这些术语在整个语料库中并不十分常见,那么训练一个分类器来识别这些术语的重要性可能会非常困难。
对单词进行标记和分类
我想我们都知道英语单词有几种类型——名词、动词、副词等等。这些通常被称为词类。如果我们知道某个单词是形容词,而不是动词或停止词(如 a、the 或 of),我们可以对其进行不同的处理,或者更重要的是,我们的算法可以!
如果我们能够通过将词类识别和编码为分类变量来执行词性标注,我们就能够通过仅保留有价值的内容来提高数据质量。文本标记选项和技术的范围太广,本章的某一部分无法有效涵盖,因此我们将查看一些适用的标记技术。具体来说,我们将关注 n-gram 标记和 backoff taggers,这是一对互补的技术,允许我们创建强大的递归标记算法。
我们将使用一个名为 自然语言工具包 ( NLTK )的 Python 库。NLTK 提供了广泛的功能,我们将在本章的几个地方依赖它。现在,我们将使用 NLTK 来执行某些单词类型的标记和移除。具体来说,我们将过滤掉停止词。
先来回答显而易见的问题(为什么要消除停止词?),停止词对大多数文本分析来说几乎没有什么作用,并且可能会造成一定程度的噪音和训练差异,这是事实。幸运的是,过滤停止词非常简单。我们将简单地导入 NLTK,下载并导入字典,然后对预先存在的单词向量中的所有单词执行扫描,删除任何找到的停止单词:
import nltk
nltk.download()
from nltk.corpus import stopwords
words = [w for w in words if not w in stopwords.words("english")]
我相信你会同意这很简单!让我们继续讨论更多的 NLTK 功能,特别是标记。
用 NLTK 标记
标记是识别词类的过程,正如我们前面所描述的,并对每个术语应用标记。
在最简单的形式中,标记可以像对输入数据应用字典一样简单,就像我们之前对 stopwords 所做的那样:
tagged = ntlk.word_tokenize(words)
然而,即使是简单的考虑也会清楚地表明,我们对语言的使用要比这允许的复杂得多。我们可以用一个词(如 ferry)作为几个词类之一,决定如何对待每个话语中的每个词可能并不简单。很多时候,只有在给定其他单词及其在短语中的位置的情况下,才能理解正确的标签。
谢天谢地,我们有许多有用的技术可以帮助我们解决语言挑战。
顺序标记
一种顺序标记算法是通过从左到右和逐个标记地运行输入数据集来工作的(因此是顺序的!),连续标记每个令牌。分配哪个令牌的决定是基于该令牌、其前面的令牌以及这些前面的令牌的预测标签做出的。
在本节中,我们将使用一个 n-gram tagger 。n-gram 标记器是一种顺序标记器,用于识别适当的标记。n-gram 标记器在生成标记时会考虑到*(n-1)-多个*先前的位置标记和当前标记。
注
为了清楚起见,n-gram 是用于给定元素集合中 n 个元素的连续序列的术语。这可能是字母、单词、数字代码(例如,状态变化)或其他元素的连续序列。n-gram 被广泛用作一种手段,通过使用 n-multi 元素来捕捉元素集合的联合含义——无论是那些短语还是编码的状态转换。
n-gram tagger 最简单的形式是 n = 1 ,被称为 unigram tagger 。通过为每个令牌维护一个有条件的频率分布,单程序标记器的操作非常简单。这种条件频率分布是从术语的训练语料库中建立的;我们可以使用属于 NLTK 中NgramTagger类的有帮助的训练方法来实现训练。标记器假设在给定序列中给定标记最频繁出现的标记很可能是该标记的正确标记。如果术语 carp 在训练语料库中作为名词出现了四次,作为动词出现了两次,那么单语法标记器会将名词标记分配给任何类型为 carp 的标记。
这对于第一遍标记尝试来说可能足够了,但是很明显,一个只为每组同音异义词提供一个标记的解决方案并不总是理想的。我们可以利用的解决方案是使用数值更大的 n 克 n 。例如,通过 n = 3 (一个 T5】三元标记器,我们可以看到标记器如何更容易区分输入他倾向于在大量上鲤鱼,而不是他钓到了一条华丽的鲤鱼!
然而,这里又一次在标记的准确性和标记的能力之间进行了权衡。随着我们增加 n ,我们正在创造越来越长的 n 克,这变得越来越罕见。在很短的时间内,我们最终处于 n-grams 没有出现在训练数据中的情况,导致我们的标记器无法为当前令牌找到任何合适的标记!
在实践中,我们发现我们需要的是一套标签。我们希望最可靠、最准确的标记器在尝试标记给定数据集时有第一次机会,对于任何失败的情况,我们都可以尝试使用更可靠但可能不太准确的标记器。
令人高兴的是,我们想要的已经以退避标签的形式存在了。让我们了解更多!
回退标记
有时,给定的标记器可能会执行不可靠。当标记器具有高精度要求和有限的训练数据时,这尤其常见。在这种时候,我们通常希望构建一个集合结构,让我们同时使用几个标记器。
为此,我们在两种类型的标记器之间进行了区分:子标记器 和回退标记器。子标签就像我们之前看到的标签一样,依次为和 Brill 标签。标记结构可以包含一种或多种标记符。
**如果子标记器不能确定给定令牌的标记,则可以参考回退标记器。回退标记器专门用于组合(一个或多个)子标记的结果,如下图所示:
在简单的实现中,退避标记器将简单地按顺序轮询子标记器,接受提供的第一个非空标记。如果给定令牌的所有子标记都返回 null,则退避标记器将为该令牌分配一个 none 标记。顺序可以确定。
回退一般是和多个不同类型的子进程一起使用;这使得数据科学家能够同时利用多种标记器的优势。根据需要,回退可能指其他回退,这可能会产生高度冗余或复杂的标记结构:
一般来说,回退标记器提供了冗余,使您能够在复合解决方案中使用多个标记器。为了解决我们眼前的问题,让我们实现一系列嵌套的 n-gram 标记器。我们将从三元模型标记器开始,它将使用二元模型标记器作为它的回退标记器。如果这两个标记器都没有解决方案,我们将有一个单一的标记器作为额外的补偿。这可以非常简单地完成,如下所示:
brown_a = nltk.corpus.brown.tagged_sents(categories= 'a')
tagger = None
for n in range(1,4):
tagger = NgramTagger(n, brown_a, backoff = tagger)
words = tagger.tag(words)
从文本数据创建特征
一旦我们参与到深思熟虑的文本清理实践中,我们需要采取额外的步骤来确保我们的文本成为有用的特性。为了做到这一点,我们将研究 NLP 中的另一组主要技术:
- 堵塞物
- 引理
- 用随机森林装袋
堵塞
当处理语言数据集时,另一个挑战是许多词干存在多种词形。例如,根舞是其他多个词的词干——舞蹈、舞者、舞蹈等等。通过找到一种将这种多种形式简化为词干的方法,我们发现自己能够改进我们的 n-gram 标记,并应用新技术,如词条统计。
使我们能够将单词缩到词干的技术称为词干分析器。词干分析器通过将单词解析为辅音/元音串并应用一系列规则来工作。最受欢迎的词干器是 搬运工词干器,它通过执行以下步骤工作;
- 通过将(例如, ies 变成 i 来简化后缀的范围)减少到一个更小的集合。
- 在几个过程中删除后缀,每个过程都删除一组后缀类型(例如,过去分词或复数后缀,如 y 或 alism)。
- 删除所有后缀后,通过在需要的地方添加“e”来清理词尾(例如,ceas 变为 stop)。
- 移除双 l。
搬运工工作效率很高。为了确切了解它的工作原理,让我们来看看它的实际应用吧!
from nltk.stem import PorterStemmer
stemmer = PorterStemmer()
stemmer.stem(words)
这个stemmer的输出,正如我们先前存在的例子所展示的,是单词的根形式。这可能是一个真实的词,也可能不是;例如,跳舞变成了舞蹈。这还可以,但不是很理想。我们可以做得更好!
为了一致地达到一个真实的单词形式,让我们应用一个稍微不同的技术,引理。引理是一个更复杂的确定词干的过程;与波特词干不同,它对不同的词类使用不同的归一化过程。与波特词干不同,它还寻求找到单词的实际词根。在词干不一定是真词的地方,引理必须是真词。引理化也承担了将同义词简化到词根的挑战。例如,词干分析器可能会将术语书转换为术语书,但它并没有配备处理术语书的工具。引理者可以同时处理书和我,把两个术语都简化为书。
作为必要的先决条件,我们需要每个输入令牌的 POS。谢天谢地,我们已经应用了 POS 标记器,并且可以直接从该过程的结果中工作!
from nltk.stem import PorterStemmer, WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
words = lemmatizer.lemmatize(words, pos = 'pos')
现在的输出是我们期望看到的:
|源程序正文
|
后记忆障碍
|
| --- | --- |
| The laughs you two heard were triggered by memories of his own high-flying exits off moving beasts | ['The', 'laugh', 'two', 'hear', 'trigger', 'memory', 'high', 'fly', 'exit', 'move', 'beast'] |
我们现在已经成功地提取了输入文本数据,极大地提高了查找算法(如许多基于字典的方法)处理这些数据的效率。我们已经删除了停止字,并用正则表达式方法标记了一系列其他噪声元素。我们还删除了任何 HTML 标记。我们的文本数据已经达到合理的处理状态。我们还需要学习另一项关键技术,它可以让我们从文本数据中生成特征。具体来说,我们可以使用打包来帮助量化术语的使用。
让我们了解更多!
套袋和随机林
打包是技术家族的一部分,这些技术统称为子空间方法。方法有几种形式,每种都有一个单独的名称。如果我们从样本案例中抽取随机子集,那么我们正在执行粘贴。如果我们是从有替换的病例中取样,这被称为装袋。如果我们不是从案例中提取,而是使用特征的子集,那么我们就执行属性打包。最后,如果我们选择从样本案例和特征中抽取,我们将采用一种被称为 随机面片的技术。
基于特征的技术、属性打包和随机补丁方法在某些环境中非常有价值,尤其是在高维环境中。医学和遗传学领域都倾向于看到大量的高维数据,因此基于特征的方法在这些领域非常有效。
在自然语言处理环境中,专门使用打包是很常见的。在语言数据的上下文中,我们要处理的东西被恰当地称为一袋单词。单词包是一种文本数据准备方法,它通过识别数据集中所有不同的单词(或标记),然后计算它们在每个样本中的出现次数来工作。让我们从一个演示开始,该演示在数据集的几个示例案例上执行:
|身份证明
|
日期
|
话
|
| --- | --- | --- |
| 132 | 20120531031917Z | ['_F', 'how', 'are', 'you', 'not', 'ded'] |
| 69 | 20120531173030Z | ['you', 'are', 'living', 'proof', 'that', 'bath', 'salts', 'effect', 'thinking'] |
这为我们提供了以下 12 部分的术语列表:
[
"_F"
"how"
"are"
"you"
"not"
"ded"
"living"
"proof"
"that"
"bath"
"salts"
"effect"
"thinking"
]
使用这个列表的索引,我们可以为前面的每个句子创建一个 12 部分的向量。这个向量的值是通过遍历前面的列表并计算数据集中每个句子的每个术语出现的次数来填充的。给定我们先前存在的例子句子和我们从它们创建的列表,我们最终创建了以下包:
|身份证明
|
日期
|
评论
|
一大堆单词
|
| --- | --- | --- | --- |
| 132 | 20120531031917Z | _F how are you not ded | [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0] |
| 69 | 20120531173030Z | you are living proof that bath salts effect thinking | [0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1] |
这是一袋字实现的核心。自然,一旦我们将文本的语言内容翻译成数字向量,我们就能够开始使用技术来增加我们在分类中使用这些文本的复杂性。
一种选择是使用加权条款。我们可以使用术语加权方案来修改每个向量中的值,以便强调指示性的或有助于分类的术语。加权方案可以是简单的掩码,例如指示存在与否的二进制掩码。
如果某些术语的使用频率比正常情况高得多,二进制掩码可能会很有用;在这种情况下,如果不使用二进制掩码,可能需要特定的缩放(例如,对数缩放)。然而,与此同时,术语使用的频率可以提供信息(例如,它可以指示强调),并且关于是否应用二进制掩码的决定并不总是简单地做出。
另一个加权选项是术语频率-逆文档频率,或 tf-idf。该方案将特定句子和数据集内的使用频率作为一个整体进行比较,如果某个术语在给定样本中的使用频率高于在整个语料库中的使用频率,则使用的值会增加。
tf-idf 的变体经常用于文本挖掘上下文,包括搜索引擎。Scikit-learn 提供了一个 tf-idf 实现,TfidfVectoriser,我们将很快使用它来为我们自己使用 tf-idf。
既然我们已经理解了单词包背后的理论,并且可以看到我们可以利用的技术选项的范围,一旦我们开发了单词使用的载体,我们应该讨论如何实现单词包。单词包可以很容易地用作熟悉模型的包装。虽然一般来说,子空间方法可以使用一系列基本模型中的任何一个(支持向量机和线性回归模型是常见的),但是在一包单词实现中使用随机森林是非常常见的,将准备和学习总结成一个简洁的脚本。在这种情况下,我们将暂时独立使用单词包,通过随机森林实现为下一节保存分类!
注
虽然我们将在第 8 章、集成方法中更详细地讨论随机森林(描述了我们可以创建的各种类型的集成),但现在注意到随机森林是一组决策树是有帮助的。它们是强大的集成模型,要么并行运行(产生投票或其他净结果),要么相互促进(通过迭代添加一棵新树来模拟解决方案中现有树集无法很好地模拟的部分)。
由于随机森林的强大和易用性,它们通常被用作基准算法。
同样,实现单词包的过程相当简单。我们初始化我们的打包工具(事实上称为矢量器)。注意,在这个例子中,我们对特征向量的大小进行了限制。这很大程度上是为了给自己节省一些时间;每个文档必须与特性列表中的每个项目进行比较,所以当我们开始运行我们的分类器时,这可能需要一点时间!
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(analyzer = "word", \
tokenizer = None, \
preprocessor = None, \
stop_words = None, \
max_features = 5000)
我们的下一步是通过fit_transform在我们的单词数据上安装矢量器;作为拟合过程的一部分,我们的数据被转换成特征向量:
train_data_features = vectorizer.fit_transform(words)
train_data_features = train_data_features.toarray()
这就完成了对文本数据的预处理。我们已经通过一整套文本挖掘技术获取了这个数据集,遍历了每种技术背后的理论和推理作为,并使用了一些强大的 Python 脚本来处理我们的测试数据集。我们现在处于一个很好的位置来尝试一下卡格尔的侮辱检测挑战!
测试我们准备的数据
那么,现在我们已经完成了数据集的一些初始准备,让我们来试一试真正的问题,看看我们是如何做的。为了帮助设置场景,让我们考虑一下无常的指导和数据描述:
这是一个单类分类问题。标签或者是 0 表示中性评论,或者是 1 表示侮辱性评论(中性可以被认为不属于侮辱类。您的预测必须是[0,1]范围内的实数,其中 1 表示 100%有信心预测评论是一种侮辱。
- 我们正在寻找旨在侮辱作为更大的博客/论坛对话一部分的人的评论。
- 我们不寻找针对非参与者(如名人、公众人物等)的侮辱。).
- 侮辱可能包含亵渎、种族诽谤或其他冒犯性语言。但通常情况下,他们不会。
- 含有亵渎或种族诋毁的言论,但不一定是对另一个人的侮辱,则被认为不具有侮辱性。
- 评论的侮辱性要明显,不能含蓄。
- 标签中可能有少量噪音,因为它们没有经过仔细清洁。然而,参赛者可以确信训练和测试数据中的误差是< 1%。
还应提醒参赛选手,这一问题往往会严重超标。所提供的数据通常代表完整的测试集,但无论如何都不是详尽的。无常将根据从广泛样本中提取的一组未公布的数据进行最终评估。
这是非常好的指导,因为它提出了两个特别的注意点。期望的分数是曲线 ( AUC )下的 区域,这是一个对假阳性和不正确阴性结果(特异性和敏感性)都非常敏感的指标。
指南明确指出,需要连续预测,而不是二进制 0/1 输出。这在使用 AUC 时变得至关重要;如果你只使用分类值,即使给出少量不正确的预测也会从根本上降低一个人的分数。这表明,与其使用RandomForestClassifier算法,我们更希望使用RandomForestRegressor,一种专注于回归的替代方法,然后在 0 和 1 之间重新调整结果。
真正的卡格尔竞赛是在一个更具挑战性和现实的环境中进行的——一个没有正确解决方案的环境。在第 8 章、集成方法中,我们将探索顶级数据科学家如何在这样的环境中做出反应并茁壮成长。目前,我们将利用这一能力来确认我们在测试数据集上是否做得很好。请注意,这一优势也存在风险;如果问题过于严重,我们将需要遵守纪律,以确保我们没有在测试数据上过度训练!
此外,我们还有一个好处,就是能够看到真正的选手表现得有多好。虽然我们将真正的讨论留到第 8 章、合奏方法中,但是可以合理地预计每个高排名选手都提交了相当多的失败尝试;有一个基准将帮助我们判断我们是否朝着正确的方向前进。
具体来说,私人(测试)排行榜的前 14 名参与者设法达到了超过 0.8 的 AUC 分数。最佳射手获得了令人印象深刻的 0.84 分和 0.84 分,而参赛的 50 支球队中有一半以上的得分超过了 0.77 分*。*
*正如我们之前讨论的,让我们从随机森林回归模型开始。
注
一个随机森林是决策树的集合。
虽然单个决策树可能会受到方差或偏差相关问题的影响,但随机森林能够使用多个平行试验的加权平均值来平衡建模结果。
随机森林的应用非常简单,是应对新数据挑战的良好的第一步技术;在早期对数据应用随机森林分类器使您能够很好地理解初始、基线分类精度是什么样的,并对分类边界是如何形成的给出有价值的见解;在使用数据集的初始阶段,这种洞察力是非常宝贵的。
Scikit-learn 提供了RandomForestClassifier来实现随机森林算法的简单应用。
对于第一遍,我们将使用 100 棵树;增加树的数量可以提高分类精度,但需要额外的时间。一般来说,在模型创建的早期阶段尝试快速迭代是明智的;你重复运行模型的速度越快,你就能越快地了解你的结果是什么样的,以及如何改进它们!
我们从开始初始化和训练我们的模型:
trollspotter = RandomForestRegressor(n_estimators = 100, max_depth = 10, max_features = 1000)
y = trolls["y"]
trollspotted = trollspotter.fit(train_data_features, y)
然后,我们获取测试数据,并应用我们的模型来预测每个测试用例的得分。我们使用一种简单的拉伸技术重新调整这些分数:
moretrolls = pd.read_csv('moretrolls.csv', header=True, names=['y', 'date', 'Comment', 'Usage'])
moretrolls["Words"] = moretrolls["Comment"].apply(cleaner)
y = moretrolls["y"]
test_data_features = vectorizer.fit_transform(moretrolls["Words"])
test_data_features = test_data_features.toarray()
pred = pred.predict(test_data_features)
pred = (pred - pred.min())/(pred.max() - pred.min())
最后,我们应用roc_auc函数计算模型的 AUC 分数:
fpr, tpr, _ = roc_curve(y, pred)
roc_auc = auc(fpr, tpr)
print("Random Forest benchmark AUC score, 100 estimators")
print(roc_auc)
正如我们所看到的,结果肯定没有达到我们希望的水平:
Random Forest benchmark AUC score, 100 estimators
0.537894912105
值得庆幸的是,我们可以尝试在这里配置许多选项:
- 我们处理输入的方法(预处理步骤和标准化)
- 我们的随机森林中估计量的数量
- 我们选择使用的分类器
- 我们的单词包实现的属性(尤其是最大术语数)
- 我们的 n-gram 标记器的结构
接下来,让我们调整单词实现包的大小,将术语上限从稍微任意的 5000 个术语增加到最多 8000 个术语;我们将在一个范围内运行,看看我们能学到什么,而不是只挑选一个值。我们还会将树的数量增加到更合理的数量(在本例中,我们增加到1000):
Random Forest benchmark AUC score, 1000 estimators
0.546439310772
这些结果略好于前一组,但并不显著。他们离我们想去的地方绝对有一段距离!让我们更进一步,设置一个不同的分类器。让我们尝试一个相当熟悉的选择——SVM。我们将建立我们自己的 SVM 对象来处理:
class SVM(object):
def __init__(self, texts, classes, nlpdict=None):
self.svm = svm.LinearSVC(C=1000, class_weight='auto')
if nlpdict:
self.dictionary = nlpdict
else:
self.dictionary = NLPDict(texts=texts)
self._train(texts, classes)
def _train(self, texts, classes):
vectors = self.dictionary.feature_vectors(texts)
self.svm.fit(vectors, classes)
def classify(self, texts):
vectors = self.dictionary.feature_vectors(texts)
predictions = self.svm.decision_function(vectors)
predictions = p.transpose(predictions)[0:len(predictions)]
predictions = predictions / 2 + 0.5
predictions[predictions > 1] = 1
predictions[predictions < 0] = 0
return predictions
虽然SVM的工作方式对人类评估来说几乎是不可理解的,但作为一种算法,它可以有效地运行,迭代地将数据集转换成多个额外的维度,以便在最佳类边界创建复杂的超平面。因此,看到我们的分类质量有所提高并不令人感到惊讶:
SVM AUC score
0.625245653817
也许我们没有充分了解我们的结果正在发生什么。让我们尝试用不同的方式来衡量绩效。具体来说,让我们看看模型的标签预测和实际目标之间的差异,看看在某些类型的输入下,模型是否更频繁地失败。
所以我们的预测已经走了很远。虽然我们仍有许多选择,但值得考虑使用更复杂的模型集合作为可靠的选择。在这种情况下,利用多个模型而不是一个模型可以使我们获得每个模型的相对优势。要针对此示例尝试合奏,请运行score_trolls_blendedensemble.py脚本。
注
这套服装是一套混合/叠加服装。我们将在第八章、合奏方法中花更多的时间讨论这个合奏是如何工作的!
绘制结果时,我们可以看到性能有所提高,但幅度远低于我们的预期:
我们显然在根据这些数据构建模型时遇到了一些问题,但是在这一点上,用一个更发达的模型来解决这个问题并没有太大的价值。我们需要回到我们的特性,并致力于扩展特性集。
在这一点上,值得从这场特殊的卡格尔竞赛中最成功的参赛者之一那里得到一些启示。一般来说,得分最高的条目往往是通过发现输入数据周围的所有技巧来开发的。这个数据集来自于一个名为 tuzzeg 的用户。这位选手在 github.com/tuzzeg/dete… 提供了一个可用的代码库。
Tuzzeg 的实现与我们的不同之处在于更彻底。除了我们使用词性标注构建的基本特征之外,他还使用了基于词性的二元模型和三元模型以及子序列(从 N 个术语的滑动窗口创建)。他处理了多达 7 克的 n 克,并创建了长度为 2、3 和 4 的字符 n 克。
此外,tuzzeg 花时间创建了两种类型的复合模型,这两种模型都被纳入了他的解决方案——句子级别和排名模型。通过将数据中的案例转化为排序的连续值,排名将我们围绕问题本质的合理化向前推进了一步。
同时,他开发的创新句级模型在训练数据中专门针对单句案例进行了训练。为了对测试数据进行预测,他将案例分成几个句子,分别对每个句子进行评估,只对案例中的句子进行最高分。这是为了适应这样一种预期,即在自然语言中,说话者经常将侮辱性评论限制在他们讲话的一个部分。
Tuzzeg 的模型创建了 100 多个特征组(其中基于词干的二元模型是一个示例特征组——二元模型过程创建了一个特征向量意义上的组),最重要的特征组(按影响排序)如下:
stem subsequence based 0.66
stem based (unigrams, bigrams) 0.18
char ngrams based (sentence) 0.07
char ngrams based 0.04
all syntax 0.006
all language models 0.004
all mixed 0.002
这很有趣,因为它表明我们目前没有使用的一组功能翻译对于生成可用的解决方案很重要。特别是,基于子序列的特征距离我们的初始特征集只有很短的一步,这使得添加额外的特征变得简单明了:
def subseq2(n, xs):
l = len(xs)
return ['%s %s' % (xs[i], xs[j]) for i in xrange(l-1) for j in xrange(i+1, i+n+1) if j < l]
def getSubseq2(seqF, n):
def f(row):
seq = seqF(row)
return set(seq + subseq2(n, seq))
return f
Subseq2test = getSubseq2(line, 2)
这种方法产生了极好的结果。虽然我鼓励您导出 Tuzzeg 自己的解决方案并应用它,但您也可以查看本项目存储库中提供的score_trolls_withsubseq.py脚本,了解如何整合强大的附加功能。
添加了这些附加功能后,我们看到 AUC 分数有了显著提高:
运行此代码可以提供非常健康的0.834 AUC 分数。这只是为了展示深思熟虑和创新的特征工程的力量;虽然本章中生成的特定特征在其他上下文中会很好地为您服务,但是特定的假设(例如多句评论中的敌对评论被隔离到特定的句子中)会导致非常有效的特征。
由于我们在本章中有幸对照测试数据检查了我们的推理,我们不能合理地说我们已经在类似生活的条件下工作了。我们没有通过自己查看测试数据来利用获得测试数据的机会,但是可以公平地说,知道私人排行榜在这次挑战中的得分会让我们更容易找到正确的解决方案。在第 8 章、集合方法中,我们将以更加严谨和现实的方式处理另一个棘手的卡格尔问题。我们还将深入讨论合奏!
进一步阅读
本章开头的引文来自可读性很强的卡格尔博客《没有免费预感》。参考blog.kaggle.com/2014/08/01/…。
理解 NLP 任务有很多好的资源。一篇相当全面的八部分文章可以在网上找到。
如果您热衷于入门,一个很好的选择是尝试 Kaggle 的 for Knowledge NLP 任务,它非常适合作为本章中描述的技术的测试平台:https://www . Kaggle . com/c/word 2 vec-NLP-tutorial/details/part-1-for-初学者-单词包。
本章引用的卡格尔竞赛可在https://www . Kaggle . com/c/检测-社交侮辱-评论获得。
对于有兴趣进一步描述 ROC 曲线和 AUC 测量的读者,请考虑 Tom Fawcett 的精彩介绍,可在https://ccrma . Stanford . edu/workshop/mir 2009/references/rocintro . pdf获得。
总结
在本章中,我们已经了解了许多有用且高度适用的技能。在这一章中,我们采用了一组凌乱、复杂的文本数据,并通过一系列严格的步骤,将其转化为一大组有效的特性。我们首先学习了一套数据清理技巧,剔除了大量的噪音和问题元素,然后我们使用词性标注和单词包将文本转化为特征。在这个过程中,你学会了应用一套广泛适用且非常有效的技术,使我们能够在许多自然语言处理环境中解决困难的问题。
通过对多个单个模型和集成的实验,我们发现,在一个更智能的算法可能不会产生强有力的结果的地方,彻底和创造性的特征工程可以在模型性能方面产生巨大的改进。***
七、特征工程第二部分
简介
我们已经认识到特征工程的重要性。在前一章中,我们讨论了一些技术,这些技术使我们能够从一系列特征中进行选择,并有效地将原始数据转换为特征,这些特征可以通过我们迄今为止讨论的高级 ML 算法进行有效处理。
格言垃圾进来,垃圾出去在这种情况下是相关的。在前面的章节中,我们已经看到了图像识别和自然语言处理任务是如何需要精心准备的数据的。在这一章中,我们将看到一种更普遍的数据类型:从现实应用程序中收集的定量或分类数据。
我们将在本章中使用的数据类型在许多上下文中都很常见。我们可以讨论从森林、游戏机或金融交易中的传感器获取的遥测数据。我们可以利用通过研究收集的地质调查信息或生物测定数据。无论如何,核心原则和技术保持不变。
在本章中,您将学习如何询问这些数据以剔除或减轻质量问题,如何将其转换为有利于机器学习的形式,以及如何创造性地增强这些数据。
总的来说,我们将在本章中讨论的概念如下:
- 特征集创建的不同方法及特征工程的局限性
- 如何使用大量技术来增强和改进初始数据集
- 如何结合和使用领域知识来理解有效的选项,以转换和提高现有数据的清晰度
- 我们如何测试单个功能和功能组合的价值,以便只保留我们需要的东西
虽然我们将从底层概念的详细讨论开始,但在本章结束时,我们将进行多次迭代试验,并使用专门的测试来了解我们正在创建的特性对我们的帮助有多大。
创建特征集
成功的机器学习最重要的因素是你输入数据的质量。一个好的模型,如果有误导性的、不恰当的规范化的或不具信息性的数据,那么在模型运行适当准备的数据时,将不会看到同样的成功水平。
在某些情况下,您可以指定数据收集,或者访问一组有用的、大量的、多样的源数据。有了正确的知识和技能,您可以使用这些数据来创建非常有用的特征集。
一般来说,对于如何构建好的特征集有很强的知识是非常有帮助的,因为它使您能够审计和评估任何新的数据集,以发现错过的机会。在本章中,我们将介绍一个设计过程和技术集,使创建有效的特征集变得更加容易。
因此,我们将从讨论一些我们可以用来扩展或重新解释现有特性的技术开始,潜在地创建大量有用的参数来包含在我们的模型中。
然而,正如我们将会看到的,有效使用特征工程技术是有限制的,我们需要注意工程数据集周围的风险。
最大似然应用的工程特性
我们已经讨论了您可以做些什么来修补数据中的数据质量问题,并且我们已经讨论了如何在您必须加入到外部数据中的维度中创造性地使用维度。
一旦你面前有了一组相当好理解和经过质量检查的数据,在你能够从这些数据中产生有效的模型之前,通常还需要大量的工作。
使用重新缩放技术来提高特征的可学习性
将未准备好的数据直接输入许多机器学习模型的主要挑战是算法对不同变量的相对大小敏感。如果数据集有多个范围不同的参数,一些算法会将方差较大的变量视为比具有较小值和较小方差的算法更显著的变化。
解决这个潜在问题的关键是重新缩放,这是一个调整参数值相对大小的过程,同时保留每个参数中值的初始顺序(单调转换)。
如果在训练之前对输入数据进行缩放,梯度下降算法(包括大多数深度学习算法—sebastianruder.com/optimizing-…)的效率会显著提高。为了理解为什么,我们将求助于画一些图片。给定的一系列培训步骤可能如下所示:
当应用于未缩放的数据时,这些训练步骤可能无法有效收敛(如下图中的左侧示例所示)。
由于每个参数具有不同的标度,模型试图训练的参数空间可能会高度失真和复杂。这个空间越复杂,在其中训练模型就越困难。总的来说,这是一个可以通过隐喻有效描述的复杂主题,但是对于寻求更全面解释的读者来说,在本章的进一步阅读部分有一个很好的参考。就目前而言,将训练中的梯度下降模型视为像大理石滚下斜坡一样的行为并不是没有道理的。这些弹珠容易卡在斜坡上的鞍点或其他复杂几何形状中(在这种情况下,这是由我们的模型的目标函数创建的表面——我们的模型通常训练其输出最小化的学习函数)。然而,通过缩放数据,表面变得更加规则,训练可以变得更加有效:
经典的例子是 0 和 1 之间的线性重新缩放;用这种方法,最大的参数值被重新调整到 1 ,最小的被调整到 0 ,中间值落在 0-1 区间,与它们相对于最大和最小值的原始大小成比例。在这样的变换下,矢量*【0,10,25,20,18】将变为【0,0.4,1,0.8,0.72】*。
这种转换的特殊价值在于,对于原始形式中幅度可能不同的多个数据点,重新缩放的特征将位于相同的范围内,从而使您的机器学习算法能够在有意义的信息内容上进行训练。
这是最直接的缩放选项,但是有一些非线性缩放选项,在正确的情况下会更有帮助;其中包括平方缩放、平方根缩放,最常见的可能是对数缩放。
参数值的对数标度在物理学和底层数据经常受幂律影响的环境中非常常见(例如, y 的指数增长与 x 的线性增长一致)。
与线性重新缩放不同,对数缩放调整数据案例之间的相对间距。这可能是一把双刃剑。一方面,对数标度能很好地处理外围情况。让我们来看一个描述虚构群体成员个人净财富的示例数据集,由以下汇总统计数据描述:
在重新调整之前,这一群体严重倾向于拥有荒谬净资产的个人。每十分之一的病例分布如下:
在对数标度之后,这种分布更加友好:
我们可以选择进一步缩放,并通过这样做来绘制这个分布的前半部分。在这种情况下,log-10 规范化显著降低了这些外围值的影响,使我们能够在数据集中保留异常值,而不会丢失低端的细节。
尽管如此,重要的是要注意,在某些情况下,聚类情况的相同增强会增强不同参数值中的噪声,并产生值之间更大间距的错误印象。这往往不会对对数标度处理异常值的方式产生负面影响;这种影响通常出现在原始值非常相似的小值案例组中。
通过对数标度引入非线性带来的挑战是巨大的,一般来说,非线性标度只推荐用于你理解的变量,并且它们之间有非线性关系或趋势。
创建有效的衍生变量
在许多机器学习应用中(例如,几乎所有的神经网络),重新缩放是预处理的标准部分。除了重新缩放之外,还有其他准备技术,可以通过战略性地减少输入模型的参数数量来提高模型性能。最常见的例子是派生度量,它采用多个现有数据点,并在单个度量中表示它们。
这些是极其普遍的;例子包括加速度(作为来自两个时间点的速度值的函数)、体重指数(作为身高、体重和年龄的函数)和股票评分的 市盈率 ( 市盈率)比率。本质上,你曾经遇到的任何派生分数、比率或复杂度量都是由多个组成部分形成的组合分数。
对于熟悉环境中的数据集,许多这些预先存在的度量将是众所周知的。然而,即使在相对知名的领域,使用领域知识和现有数据的混合来寻找新的支持措施或转换也是非常有效的。思考派生度量选项时,一些有用的概念如下:
- 两个变量组合:作为 m 参数的函数的 n 参数的乘法、除法或归一化。
- 随时间变化的度量:这里的一个经典例子是一个度量中的加速度或 7D 变化。在更复杂的情况下,基础时间序列函数的斜率可能是一个有用的参数,而不是直接使用当前和过去的值。
- 减去基线:使用基本预期(一个平坦的预期,如基线流失率)根据该基线重新预测一个参数,可以更直接地了解同一变量。对于流失示例,我们可以生成一个参数,该参数根据与预期的偏差来描述流失。同样,在股票交易的情况下,我们可以根据开盘价来看收盘价。
- 归一化:根据前面的情况,基于另一个参数或基线的值对参数值进行归一化,该参数或基线是在给定其他变量属性的情况下动态计算的。这里的一个例子是失败交易率;除了将此值视为原始(或重新缩放的)计数之外,根据尝试的事务对其进行规范化通常也是有意义的。
这些不同元素的创造性重组让我们建立非常有效的分数。例如,有时,告诉我们客户参与度(下降或增加)随时间变化的斜率的参数需要以该客户之前是高度参与还是几乎不参与为条件,因为参与度的轻微下降在每种情况下可能意味着非常不同的事情。数据科学家的工作是有效地和创造性地为给定的领域捕获这些微妙的特征集。
到目前为止,这种讨论主要集中在数字数据上。然而,有用的数据通常被锁在非数字参数中,如代码或分类数据。因此,我们接下来将讨论一组将非数字特征转化为可用参数的有效技术。
重新解释非数字特征
一个常见的挑战是如何处理非数字特征,这可能是有问题的,也可能是特定问题的。通常,有价值的信息被编码在非数字速记值中。例如,在股票交易中,股票本身的身份(例如,AAPL)以及买方和卖方的身份是有趣的信息,我们期望这些信息与我们的问题有意义地联系起来。进一步举这个例子,我们可能还会期望一些股票的交易与其他股票不同,即使是在行业内,公司内部的组织差异也提供了重要的背景,这些差异可能发生在某些或所有时间点。
在某些情况下,一个简单的选择是构建一个聚合或一系列聚合。最明显的例子是出现次数,可以创建扩展度量(两个时间窗口之间的计数变化),如前一节所述。
构建汇总统计数据并减少数据集中的行数会带来减少模型可用信息量的风险(增加模型脆弱性和过度拟合的风险)。因此,广泛地聚合和减少输入数据通常不是一个好主意。深度学习技术更是如此,比如第 2-4 章中讨论和使用的算法。
与其大量使用基于聚合的方法,不如让我们看看将字符串编码值转换为数字数据的另一种方法。另一类非常流行的技术是编码,最常见的编码策略是一次性编码。One-hot 编码是将一系列分类响应(例如,年龄组)转换为一组二进制变量的过程,每个响应选项(例如,18-30)都由其自己的二进制变量表示。这在视觉上更直观:
编码后,这个分类变量和连续变量的数据集成为二元变量的张量:
这呈现的优势是显著的;它使我们能够挖掘包含在大量数据集中的非常有价值的标签信息,而不会聚合或降低数据的信息内容。此外,one-hot 允许我们将编码变量的特定响应代码分成单独的特征,这意味着我们可以为特定变量识别或多或少有意义的代码,并且只保留重要的值。
另一种非常有效的技术,主要用于文本代码,被称为哈希技巧。简单来说,散列是将数据转换成数字表示的函数。散列对许多人来说是一个熟悉的概念,因为它们经常被用来编码敏感的参数和总结庞大的数据。然而,为了最大限度地利用哈希技巧,了解技巧是如何工作的以及可以用它做什么是很重要的。
我们可以使用散列法将一个文本短语转换成一个数值,用作该短语的标识符。虽然不同的散列算法有许多应用,但在这种情况下,即使是简单的散列也可以直接将字符串键和代码转换为我们可以有效建模的数字参数。
一个非常简单的散列可以把每个字母字符变成一个相应的数字。 a 会变成 1 , b 会变成 2 ,以此类推。通过对这些值求和,可以为单词和短语生成哈希值。短语卡特彼勒 gif在此方案下的翻译如下:
Cat: 3 + 1 + 20
Gifs: 7 + 9 + 6 + 19
Total: 65
这是一个可怕的散列,原因有二(完全无视输入包含垃圾词的事实!).首先,它可以呈现多少输出没有真正的限制。当人们记得散列技巧的全部要点是提供降维时,从散列中可能输出的数量必须是有限的,这是理所当然的!大多数散列限制了它们输出的数字的范围,因此选择散列的部分决定与您希望模型具有的特征的数量有关。
注
一种常见的行为是选择 2 的幂作为散列范围;这有助于在哈希过程中允许按位运算,从而加快速度。
这种杂凑很糟糕的另一个原因是对单词的改变影响很小,而不是很大。如果猫变成了蝙蝠,我们希望我们的哈希输出发生实质性的变化。而是变化一(变成 64 )。一般来说,一个好的散列函数是输入文本中的一个小变化会导致输出中的一个大变化。这部分是因为语言结构趋向于非常一致(因此得分相似),但是给定结构内稍微不同的名词和动词集合趋向于赋予彼此非常不同的含义(猫坐在垫子上对比汽车坐在猫身上)。
所以我们已经描述了散列。哈希技巧让事情更进一步。假设,把每个单词都变成一个散列的数字代码将导致大量的散列冲突——两个单词具有相同散列值的情况。自然,这些是相当糟糕的。
很容易地,不同术语使用频率的分布对我们有利。称为 齐夫分布,它要求遇到第 n 个最常见项的概率近似为 P(n) = 0.1/n 直到大约 1000(齐夫定律)。这意味着每一项都比前一项更不容易遇到。在 n = 1000 之后,术语往往足够模糊,以至于在一个数据集中不太可能遇到两个具有相同散列的术语。
同时,一个好的散列函数的范围有限,并且会受到输入的微小变化的显著影响。这些属性使得哈希冲突机会在很大程度上与术语使用频率无关。
这两个概念——齐夫定律和一个好的散列与散列冲突机会和术语使用频率的独立性——意味着散列冲突的机会非常小,并且当一个散列冲突发生时,它极有可能在两个不常用的单词之间。
这给了哈希技巧一个特殊的属性。也就是说,与在未处理的词包特征上的训练相比,在不降低在散列数据上训练的模型的性能的情况下,可以大规模地降低一组文本输入数据的维度(从数万个自然出现的词到几百个或更少)。
正确使用哈希技巧可以实现很多可能性,包括对我们讨论的技术的扩展(特别是单词包)。本章末尾的进一步阅读一节中包含了对不同哈希实现的参考。
使用特征选择技术
现在我们有了一个很好的特征创建选项选择,以及对创造性特征工程可能性的理解,我们可以开始将我们现有的特征构建成更有效的变体。鉴于这一新发现的功能工程技能集,我们面临创建大量难以管理的数据集的风险。
无限制地添加特征会增加模型脆弱性和对某些类型模型过度拟合的风险。这与你试图模拟的趋势的复杂性有关。在最简单的情况下,如果您试图识别两个大组之间的显著区别,那么您的模型可能支持大量功能。但是,随着您需要适应的模型变得更加复杂,以及您必须处理的组变得越来越小,添加越来越多的特征会损害模型一致有效地分类的能力。
这一挑战因以下事实而变得更加复杂:哪个参数或变体最适合该任务并不总是显而易见的。适用性可能因基础模型而异;例如,决策森林在单调变换(也就是说,保持数据案例初始排序的变换)中表现不佳;一个例子是对数缩放)而不是未缩放的基础数据;但是,对于其他算法,选择重新缩放和使用的重新缩放方法都是非常有影响的选择。
传统上,特征的数量和参数数量的限制与开发将关键输入与期望的结果分数相关联的数学函数的愿望相关联。在这种情况下,需要加入额外的参数作为移动或有害变量。
每个新参数都引入了另一个维度,这使得建模的关系更加复杂,结果模型更有可能过度拟合现有数据。一个简单的例子是,如果您引入一个参数,它只是每个案例的唯一标签;在这一点上,您的算法将只学习那些标签,使得当您的模型被引入新的数据集时,它很可能完全失败。
不那么琐碎的例子同样问题重重;当您的功能将案例分成非常小的组时,案例与功能的比例变得非常重要。简而言之,增加建模函数的复杂性会导致模型更容易过度拟合,而添加特征会加剧这种影响。根据这个原则,我们应该从非常小的数据集开始,并且只有在证明它们改进了模型之后才添加参数。
然而,在最近,一种相反的方法论——现在被普遍认为是做数据科学的一种常见方式的一部分——已经取得了进展。这种方法表明,增加非常大的特征集来整合每一个潜在的有价值的特征是一个好主意,并且将降低到一个更小的特征集来完成这项工作。
这种方法得到了一些技术的支持,这些技术能够在庞大的特征集(可能有数百或数千个特征)上做出决策,并且倾向于以蛮力的方式进行操作。这些技术将彻底测试特征组合,串联或并联运行模型,直到识别出最有效的参数子集。
这些技术起作用,这就是为什么这种方法变得流行。如果不使用这些技术,了解它们肯定是值得的,所以在本章的后面,您将学习如何应用它们。
使用暴力技术进行特征选择的主要缺点是,很容易相信算法的结果,而不管它选择的特征实际上意味着什么。明智的做法是在高效黑盒算法的使用和领域知识以及对正在进行的工作的理解之间取得平衡。因此,本章将使您能够使用两种范式(构建和构建)的技术,以便您能够适应不同的上下文。我们将从学习如何缩小您必须处理的特征集开始,从许多特征到最有价值的子集。
执行特征选择
构建了一个大型数据集后,人们面临的下一个挑战往往是如何缩小选项范围,只保留最有效的数据。在这一节中,我们将讨论支持特征选择的各种技术,它们可以自己工作,也可以作为熟悉算法的包装器。
这些技术包括相关分析、正则化技术和递归特征消除 ( RFE )。当我们完成后,您将能够自信地使用这些技术来支持您的特征集选择,每次使用新数据集时,都有可能为自己节省大量工作!
相关性
我们将从寻找回归模型主要问题的简单来源:多重共线性开始我们对特征选择的讨论。多重共线性是数据集中要素之间中度或高度相关性的奇特名称。一个显而易见的例子是披萨切片计数如何与披萨价格共线。
多重共线性有两种类型:结构性的和基于数据的。当创建新要素(例如来自要素 f 的要素 f1 )时,会出现结构多重共线性,这可能会导致多个要素之间高度相关。当两个变量受同一致病因素影响时,基于数据的多重共线性倾向于发生。
这两种多重共线性都会造成一些不良影响。特别是,我们的模型的性能往往会受到所使用的特征组合的影响;当使用共线特征时,我们模型的性能将会下降。
无论是哪种情况,我们的方法都很简单:我们可以测试多重共线性,并移除表现不佳的特征。自然,性能不佳的特性对模型性能的贡献很小。它们可能表现不佳,因为它们复制了其他功能中可用的信息,或者它们可能根本没有提供对当前问题有意义的数据。有多种方法可以测试弱特征,因为许多特征选择技术会筛选出多共线特征组合,如果表现不佳,建议将其移除。
此外,还有一个具体的多重共线性检验值得考虑;即检查数据相关矩阵的特征值。特征向量和特征值是矩阵理论中的基本概念,有许多突出的应用。更多细节将在本章末尾给出。就目前而言,可以说数据集生成的相关矩阵中的特征值为我们提供了多重共线性的量化度量。考虑一组特征值来表示我们的特征给数据集带来了多少“新信息内容”;低特征值表明数据可能与其他特征相关。例如,在工作中,考虑以下代码,该代码创建一个特征集,然后将共线性添加到特征 0 、 2 和 4 :
import numpy as np
x = np.random.randn(100, 5)
noise = np.random.randn(100)
x[:,4] = 2 * x[:,0] + 3 * x[:,2] + .5 * noise
当我们生成相关矩阵并计算特征值时,我们发现如下:
corr = np.corrcoef(x, rowvar=0)
w, v = np.linalg.eig(corr)
print('eigenvalues of features in the dataset x')
print(w)
eigenvalues of features in the dataset x
[ 0.00716428 1.94474029 1.30385565 0.74699492 0.99724486]
显然,我们的第 0 个特征是可疑的!然后我们可以通过调用v来检查这个特征的特征值:
print('eigenvalues of eigenvector 0')
print(v[:,0])
eigenvalues of eigenvector 0
[-0.35663659 -0.00853105 -0.62463305 0.00959048 0.69460718]
从位置一和位置三的特征的小值,我们可以看出特征 2 和 4 与特征 0 高度多共线。在继续之前,我们应该删除这三个特征中的两个!
套索
正则化方法是最有帮助的特征选择技术,因为它们提供了稀疏的解决方案:较弱的特征返回零,只留下具有真实系数值的特征子集。
两种最常用的正则化模型是 L1 正则化和 L2 正则化,在线性回归环境中分别称为 LASSO 和岭回归。
正则化方法通过在损失函数中增加一个惩罚来发挥作用。该惩罚导致 E(X,Y) + a||w|| ,而不是最小化损失函数 E(X,Y) 。超参数 a 与正则化的数量有关(使我们能够调整正则化的强度,从而调整所选原始特征集的比例)。
在 LASSO 正则化中,使用的具体罚函数是 α∑ni=1|wi| 。每个非零系数增加了惩罚项的大小,迫使较弱的特征返回 0 的系数。使用 scikit-learn 对超参数的参数优化支持,可以选择合适的惩罚项。在这种情况下,我们将使用estimator.get_params()来执行网格搜索,以获得合适的超参数值。有关网格搜索如何操作的更多信息,请参见本章末尾的进一步阅读部分。
在 scikit-learn 中,逻辑回归为分类提供了 L1 惩罚。同时,LASSO 模块是为线性回归提供的。现在,让我们从将 LASSO 应用于示例数据集开始。在本例中,我们将使用波士顿住房数据集:
fromsklearn.linear_model import Lasso
fromsklearn.preprocessing import StandardScaler
fromsklearn.datasets import load_boston
boston = load_boston()
scaler = StandardScaler()
X = scaler.fit_transform(boston["data"])
Y = boston["target"]
names = boston["feature_names"]
lasso = Lasso(alpha=.3)
lasso.fit(X, Y)
print "Lasso model: ", pretty_print_linear(lasso.coef_, names, sort = True)
Lasso model: -3.707 * LSTAT + 2.992 * RM + -1.757 * PTRATIO + -1.081 * DIS + -0.7 * NOX + 0.631 * B + 0.54 * CHAS + -0.236 * CRIM + 0.081 * ZN + -0.0 * INDUS + -0.0 * AGE + 0.0 * RAD + -0.0 * TAX
原始集合中的几个特征返回了0.0的相关性。增加相关性会使解决方案越来越稀疏。例如,当alpha = 0.4时,我们会看到以下结果:
Lasso model: -3.707 * LSTAT + 2.992 * RM + -1.757 * PTRATIO + -1.081 * DIS + -0.7 * NOX + 0.631 * B + 0.54 * CHAS + -0.236 * CRIM + 0.081 * ZN + -0.0 * INDUS + -0.0 * AGE + 0.0 * RAD + -0.0 * TAX
我们可以立即看到 L1 正则化作为特征选择技术的价值。然而,重要的是要注意,L1 正则化回归是不稳定的。当数据中的特征相关时,即使数据变化很小,系数也会有很大变化。
这个问题可以通过 L2 正则化或岭回归有效地解决,岭回归开发了具有不同应用的特征系数。L2 归一化在损失函数中增加了一个额外的惩罚,即 L2 范数惩罚。这种处罚的形式为( a∑ni=1w2i )。目光敏锐的读者会注意到,与 L1 罚函数不同( α∑ni=1|wi| ),L2 罚函数使用平方系数。这使得系数值更均匀地分布,并且具有附加效果,即相关特征倾向于接收相似的系数值。这显著提高了稳定性,因为系数不再因小的数据变化而波动。
然而,L2 归一化对特征选择没有 L1 那么直接有用。相反,由于有趣的特征(具有预测能力)往往具有非零系数,L2 作为一种探索性工具更有用,它允许推断分类中特征的质量。它具有比 L1 正则化更稳定和可靠的额外优点。
递归特征消除
RFE 是一个贪婪的迭代过程,充当另一个模型的包装器,比如 SVM (SVM-RFE),它反复运行输入数据的不同子集。
与 LASSO 和岭回归一样,我们的目标是找到性能最好的特征子集。顾名思义,在每次迭代中,都会留出一个要素,允许对其余要素集重复该过程,直到数据集中的所有要素都被消除。消除特征的顺序成为它们的等级。在用增量较小的子集进行多次迭代之后,每个特征都被精确地评分,并且可以选择相关的子集来使用。
为了更好地理解这是如何工作的,让我们看一个简单的例子。我们将使用(现在已经很熟悉的)数字数据集来理解这种方法在实践中是如何工作的:
print(__doc__)
from sklearn.svm import SVC
fromsklearn.datasets import load_digits
fromsklearn.feature_selection import RFE
importmatplotlib.pyplot as plt
digits = load_digits()
X = digits.images.reshape((len(digits.images), -1))
y = digits.target
我们将使用 SVM 作为我们的基础估计器,通过SVC算子进行支持向量分类 ( 支持向量机)。我们然后在这个模型上应用 RFE 包装。RFE 提出了几个论点,第一个是对选择估计量的引用。第二个论点是n_features_to_select,相当不言自明。如果特征集包含许多相互关联的特征,这些特征的子集具有高效分类特征的多元分布,则可以选择两个或多个特征的组合。
步进允许在每次迭代中移除多个特征。当给定一个介于 0.0 和 1.0 之间的值时,每一步都允许移除特征集的一个百分比,对应于步骤参数中给出的比例:
svc = SVC(kernel="linear", C=1)
rfe = RFE(estimator=svc, n_features_to_select=1, step=1)
rfe.fit(X, y)
ranking = rfe.ranking_.reshape(digits.images[0].shape)
plt.matshow(ranking)
plt.colorbar()
plt.title("Ranking of pixels with RFE")
plt.show()
假设我们熟悉数字数据集,我们知道每个实例都是一个 8×8 的手写数字图像,如下图所示。每个图像位于 8×8 网格的中心:
当我们对数字数据集应用 RFE 时,我们可以看到它在应用排名时广泛地捕获了这些信息:
要剪切的第一个像素在图像的垂直边缘(通常是空的)内部和周围。接下来,算法开始剔除图像垂直边缘或顶部附近的空白区域。保留时间最长的像素是那些能够最大程度区分不同字符的像素,这些像素对于某些数字是存在的,而对于其他数字是不存在的。
这个例子给了我们很好的视觉确认 RFE 的作品。它没有给我们的是该技术如何持续工作的证据。RFE 的稳定性取决于基本模型的稳定性,在某些情况下,岭回归将提供更稳定的解。(有关涉及哪些情况和条件的更多信息,请参考本章末尾的进一步阅读部分。)
遗传模型
在这一章的前面,我们讨论了能够在非常大的参数集下进行特征选择的算法的存在。这种类型的一些最突出的技术是遗传算法,它模拟自然选择来生成越来越有效的模型。
用于特征选择的遗传解决方案大致工作如下:
- 将一组初始变量(预测因子是此上下文中通常使用的术语)组合成多个子集(候选),并为每个候选计算性能度量
- 来自具有最佳性能的候选的预测器被随机重组到新的迭代(一代)模型中
- 在该重组步骤中,对于每个子集,都有突变的概率,由此可以从子集添加或移除预测因子
该算法通常迭代多代。适当的迭代量取决于数据集的复杂性和所需的模型。与梯度下降技术一样,遗传算法的性能和迭代次数之间存在典型的关系,其中性能的提高随着迭代次数的增加而非线性下降,最终在过拟合风险增加之前达到最小值。
为了找到有效的迭代次数,我们可以使用训练数据进行测试;通过大量迭代运行模型并绘制均方根误差 ( RMSE ),我们能够在给定输入数据和模型配置的情况下找到合适的迭代次数。
让我们更详细地谈谈每一代人身上发生的事情。具体来说,我们来谈谈候选人是如何产生的,绩效是如何评分的,重组是如何进行的。
候选项最初被配置为使用可用预测值的随机样本。关于在第一代中使用多少预测器,没有硬性规定;这取决于有多少功能可用,但通常会看到第一代候选人使用 50%到 80%的可用功能(在功能较多的情况下使用较小的百分比)。
适合性度量可能很难定义,但是通常的做法是使用两种形式的交叉验证。内部交叉验证(仅在其自身参数的上下文中测试每个模型,而不比较模型)通常用于跟踪给定迭代的性能;来自内部交叉验证的适应度度量用于选择模型,以便在下一代中重新组合。还需要外部交叉验证(针对未在任何迭代中用于验证的数据集进行测试),以确认搜索过程生成的模型没有过度适应内部训练数据。
重组由三个关键参数控制:突变、交叉概率和精英化。后者是一个可选参数,人们可以使用它来保留当前世代中 n-许多表现最好的模型;通过这样做,可以防止特别有效的候选基因在重组过程中完全丢失。这可以在突变变体中使用该候选基因和/或将其用作下一代候选基因的亲本的同时进行。
突变概率定义了下一代模型被随机重新调整的机会(通过一些预测器,通常是一个,被添加或删除)。变异有助于遗传算法保持候选变量的广泛覆盖,降低陷入参数局部解的风险。
交叉概率定义了一对候选者被选择重组到下一代模型中的可能性。有几种交叉算法:可以将每个父要素集的一部分拼接(例如,前半部分/后半部分)到子要素中,或者可以随机选择每个父要素。默认情况下,也可能使用父母双方共有的功能。从父母的唯一预测者集合中随机抽样是一种常见的默认方法。
这些是通用遗传算法的主要部分,可以用作现有模型(逻辑回归、SVM 等)的包装器。这里描述的技术可以以许多不同的方式变化,并且与在多个定量领域中稍微不同地使用的特征选择技术相关。让我们把到目前为止已经讨论过的理论应用到一个实际的例子中。
实践中的特色工程
根据您正在使用的建模技术,其中一些工作可能比其他部分更有价值。深度学习算法在工程设计较少的数据上比在较浅的模型上表现更好,可能需要较少的工作来改进结果。
理解需要什么的关键是快速迭代从数据集获取到建模的整个过程。在第一次有明确的模型精度目标时,找到可接受的最小处理量并执行。尽可能了解结果,并为下一次迭代制定计划。
为了展示这在实践中的样子,我们将使用一个不熟悉的高维数据集,使用迭代过程来生成越来越有效的建模。
我最近住在温哥华。虽然它有许多积极的品质,但生活在城市中最糟糕的事情之一是有些不可预测的通勤。无论我是坐汽车旅行,还是乘坐 Translink 的 Skytrain 系统(一条单轨列车和过山车的高速线路),我都发现自己受到难以预测的延误和拥堵问题的困扰。
本着将我们的新功能工程技能付诸实践的精神,让我们看看是否可以通过采取以下步骤来改善这种体验:
- 编写代码以从多个 API 获取数据,包括文本和气候流
- 使用我们的特征工程技术从这个初始数据中导出变量
- 通过生成通勤延迟风险评分来测试我们的功能集
不同寻常的是,在这个例子中,我们将不再关注构建和评分一个高性能的模型。相反,我们的重点是创建一个自给自足的解决方案,您可以根据自己的本地情况进行调整和应用。虽然采取这种方法符合本章的目标,但还有另外两个重要的动机。
首先,围绕分享和利用推特数据存在一些挑战。使用推特应用编程接口的部分条款是开发者有义务确保对时间线或数据集状态的任何调整(例如,包括删除推文)都在从推特上提取并公开共享的数据集中重现。这使得在本章的 GitHub 存储库中包含真实的 Twitter 数据变得不切实际。最终,由于用户需要构建自己的流并积累数据点,以及环境的变化(如季节变化)可能会影响模型性能,因此很难根据流数据提供任何下游模型的可再现结果。
这里的第二个要素很简单:不是每个人都住在温哥华!为了给最终用户带来一些有价值的东西,我们应该考虑一个可调整的通用解决方案,而不是一个特定地域的解决方案。
因此,下一节中介绍的代码旨在作为构建和开发的基础。它提供了作为成功的商业应用的基础的潜力,或者仅仅是一个有用的、数据驱动的生活帮。考虑到这一点,请查看本章的内容(并利用相关代码目录中的代码),以便找到并创建适合您自己的情况、本地可用数据和个人需求的新应用程序。
通过 RESTful APIs 获取数据
为了开始,我们需要收集一些数据!我们需要寻找以足够的频率(最好每个通勤周期至少一个记录)捕获的丰富的、有时间戳的数据,以便进行模型训练。
一个自然的开始是推特应用编程接口,它允许我们收集最近的推文数据。我们可以将这个应用编程接口分为两种用途。
首先,我们可以从官方交通机构(特别是公交和火车公司)获得推文。这些公司提供有关延误和服务中断的运输服务信息,对我们有帮助的是,这些信息采用了有利于标记工作的一致格式。
其次,我们可以通过收听感兴趣地理区域的推文来挖掘通勤情绪,使用定制的字典来收听与中断案例或其原因相关的术语。
除了挖掘数据的推特应用编程接口来支持我们的模型,我们还可以利用其他应用编程接口来提取丰富的信息。一个特别有价值的数据来源是 必应流量 API 。这个应用编程接口可以很容易地被调用来提供跨用户指定的地理区域的交通拥堵或中断事件。
此外,我们可以利用来自 雅虎天气 API 的天气数据。该应用编程接口提供给定位置的当前天气,采用邮政编码或位置输入。它提供了丰富的当地气候信息,包括但不限于温度、风速度、湿度、大气压力和能见度。此外,它还提供了当前条件的文本字符串描述以及预测信息。
虽然我们可以考虑将其他数据源结合到我们的分析中,但我们将从这些数据开始,看看我们是如何做的。
测试我们模型的性能
为了有意义地评估我们的通勤中断预测尝试,我们应该尝试定义测试标准和适当的绩效评分。
我们试图做的是识别每天当天通勤中断的风险。最好,我们想知道通勤风险,并提前通知我们可以采取行动来减轻风险(例如,提前离开家)。
为了做到这一点,我们需要三样东西:
- 了解我们的模型将输出什么
- 我们可以用来量化模型性能的度量
- 我们可以使用一些目标数据,根据我们的衡量标准对模型性能进行评分
我们可以就为什么这很重要进行有趣的讨论。可以有效地说,有些模型是有目的的信息。可以说,我们的通勤风险评分是有用的,因为它产生了我们以前没有的信息。
然而,现实情况是,不可避免地会有一个性能标准。在这种情况下,可能只是我对模型输出的结果感到满意,但重要的是要意识到,总有一些性能标准在起作用。因此,量化性能是有价值的,即使在模型看起来是信息性的(甚至更好,不受监督)的情况下。这使得抵制放弃性能测试的诱惑变得谨慎;至少这样,你就有了一个量化的性能度量来迭代地改进。
一个合理的起点是断言我们的模型旨在输出给定日期出站(从家到工作)通勤的数值分数在 0-1 范围内。我们有几个关于如何呈现这个分数的选择;也许最明显的选择是对数据应用日志重新缩放。有充分的理由进行对数标度,在这种情况下,这可能不是一个坏主意。(通勤延迟时间的分布服从幂律并非不可能。)目前,我们不会重塑这组分数。相反,我们将等待查看我们模型的输出。
就提供实际指导而言, 0-1 的分数不一定很有帮助。我们可能会发现自己想要使用桶边界在 0-1 范围内的桶边界的桶系统(如高风险、中风险或低风险)。简而言之,我们将过渡到将问题视为具有分类输出(类标签)的多类分类问题,而不是具有连续输出的回归问题。
这可能会提高模型性能。(更具体地说,因为它将把自由误差幅度提高到相关桶的最大宽度,这是一个非常慷慨的性能度量。)同样,在第一次迭代中引入这种变化可能不是一个好主意。直到我们回顾了真实通勤延迟的分布,我们才知道阶级之间的界限在哪里!
接下来,我们需要考虑如何衡量模型的性能。选择合适的评分标准通常取决于问题的特点。我们有很多关于分类器性能评分的选项。(有关机器学习算法性能度量的更多信息,请参见本章末尾的进一步阅读部分。)
决定哪种性能度量适合手头的任务的一种方法是考虑混淆矩阵。混淆矩阵是一个偶然事件表;在统计建模的背景下,他们通常描述标签预测与实际标签的对比。为一个训练好的模型输出一个混淆矩阵是很常见的(特别是对于有更多类的多类问题),因为它可以产生关于按故障类型和类分类故障的有价值的信息。
在这种情况下,参考混淆矩阵更能说明问题。我们可以考虑以下简化矩阵来评估是否有我们不关心的意外情况:
在这种情况下,我们关心所有四种应急类型。假阴性会让我们陷入意想不到的延误,而假阳性会让我们提前出发去上班。这意味着我们需要一个既重视高灵敏度(真阳性率)又重视高特异性(假阳性率)的性能指标。考虑到这一点,理想的衡量标准是曲线下的面积(T2)。
第二个挑战是如何衡量这个分数;我们需要一些可以预测的目标。谢天谢地,这很容易获得。毕竟我每天都有通勤要做!我只是用秒表、一致的开始时间和一致的路线开始自我记录我的通勤时间。
重要的是认识到这种方法的局限性。作为一个数据源,我受制于自己的内部趋势。例如,我在早上喝咖啡之前有些懒散。同样,我自己的一贯通勤路线可能拥有其他路线所没有的本地趋势。从许多人和许多路线收集通勤数据会好得多。
然而,在某些方面,我对这个目标数据的使用感到满意。不仅仅是因为我试图对自己通勤路线的中断进行分类,并且不希望我的通勤时间的自然差异通过培训被误解,比如说,与其他通勤者群体或路线设定的目标相比较。此外,考虑到预期的日常轻微自然变化,功能模型应不予考虑。
就模型性能而言,很难判断什么足够好。更准确地说,不容易知道这个模型什么时候超过了我自己的预期。不幸的是,关于我自己的通勤延迟预测的准确性,我不仅没有任何非常可靠的数据,而且一个人的预测似乎不太可能推广到其他地方的其他通勤。训练一个模型超过一个相当主观的目标似乎是不明智的。
相反,让我们尝试超越一个相当简单的阈值——一个天真地认为每一天都不会包含通勤延迟的模型。这个目标具有反映我们实际行为的相当令人愉快的特性(因为我们倾向于每天起床,表现得好像不会有交通中断)。
在 85 个目标数据案例中,观察到 14 个通勤延误。基于这个目标数据和我们创建的评分标准,我们的目标是 0.5 。
推特
鉴于我们正在将这个示例分析的重点放在温哥华市,我们有机会利用第二个推特数据源。具体来说,我们可以使用温哥华公共交通管理局 Translink 的服务公告。
Translink 推特
如上所述,该数据已经结构良好,有利于文本挖掘和后续分析;通过使用我们在前两章中回顾的技术处理这些数据,我们可以清理文本,然后将其编码为有用的特征。
我们将应用推特应用编程接口在很长一段时间内收集 Translink 的推文。推特应用编程接口是一个非常友好的工具包,很容易从 Python 中使用。(有关如何使用推特应用编程接口的扩展指导,请参见本章末尾的进一步阅读部分!)在这种情况下,我们希望从推文中提取日期和正文。正文几乎包含了我们需要知道的一切,包括以下内容:
- 推文的性质(延迟或不延迟)
- 车站受到影响
- 关于延迟性质的一些信息
增加一点复杂性的一个因素是,同一个 Translink 账户在推特上发布了天空列车线路和公交线路的服务中断信息。幸运的是,在描述每种服务类型和主题的服务问题时,该帐户通常非常统一。特别是,推特账户使用特定的标签 (#RiderAlert 用于公交路线信息, #SkyTrain 用于列车相关信息, #TransitAlert 用于两种服务的一般警报,如法定假日)来区分服务中断的主题。
类似地,我们可以期望延迟总是用延迟这个词来描述,迂回这个词来描述迂回,而分流这个词来描述。这意味着我们可以使用特定的关键词过滤掉不想要的推文。干得好,特兰林克!
注
本章中使用的数据在本章随附的 GitHub 解决方案中的translink_tweet_data.json文件中提供。章节代码中还提供了刮擦脚本;为了利用它,您需要在 Twitter 上设置一个开发人员帐户。这很容易实现;此处记录了流程,您可以在此处注册。
一旦我们获得了推文数据,我们就知道下一步该做什么了——我们需要清理并规范正文!根据第六章、文本特征工程,我们对输入数据运行BeautifulSoup和NLTK:
from bs4 import BeautifulSoup
tweets = BeautifulSoup(train["TranslinkTweets.text"])
tweettext = tweets.get_text()
brown_a = nltk.corpus.brown.tagged_sents(categories= 'a')
tagger = None
for n in range(1,4):
tagger = NgramTagger(n, brown_a, backoff = tagger)
taggedtweettext = tagger.tag(tweettext)
我们可能不需要像上一章中的巨魔数据集那样进行密集的清理。Translink 的推文高度公式化,不包含非 ascii 字符或表情符号,所以我们在第六章、文本特征工程中需要用到的具体“深度清洗”regex 脚本,这里就不需要了。
这为我们提供了一个包含小写、正则化和字典检查术语的数据集。我们已经准备好开始认真思考我们应该从这些数据中构建什么特性。
我们知道,检测数据中服务中断问题的基本方法是在推文中使用延迟术语。延迟以下列方式发生:
- 在给定的位置
- 在给定的时间
- 出于某种原因
- 在给定的持续时间内
在前三个因素中的每一个都在 Translink 推文中被持续跟踪,但是有一些数据质量问题值得认识。
位置根据第 22 街的受影响街道或车站给出。对于我们的目的来说,这不是一个完美的描述,因为我们不太可能在不做大量额外工作的情况下将街道名称和路线起点/终点变成一般的受影响区域*(因为不存在允许我们基于该信息绘制边界框的方便参考)。*
推文日期时间给出的时间并不完美。虽然我们不清楚推文是否在服务中断后的一致时间内发出,但 Translink 很可能有服务通知的目标。目前,在推文时间可能足够准确的假设下进行是明智的。
例外情况是可能是长期运行的问题或改变严重程度的问题(预计很小但变得重要的延迟)。在这些情况下,推文可能会被推迟,直到 Translink 团队认识到这个问题已经变得值得推文。数据质量问题的另一个可能原因是 Translink 内部通信不一致;工程或平台团队可能不会总是以相同的速度通知客户服务通知团队。
不过,我们必须有一定的信心,因为如果没有实时、准确的跨链路服务延迟数据集,我们无法测量这些延迟影响。(如果我们有,我们会用它来代替!)
Translink 始终如一地描述了天空列车服务延迟的原因,这些原因可分为以下几类:
- 铁路
- 火车
- 转换
- 控制
- 未知的
- 闯入
- 医学的
- 警察
- 力量
在推文正文中使用前面列表中给出的特定术语描述每个类别。显然,其中一些类别(警察、电力、医疗)不太可能相关,因为它们不会告诉我们任何关于道路状况的有用信息。列车、轨道和道岔故障率可能与绕行可能性相关;这表明,出于分类目的,我们可能希望保留这些案例。
与此同时,公交路线服务延误包含一组类似的代码,其中许多与我们的目的非常相关。这些代码如下:
- 机动车事故 ( MVA )
- 建筑
- 火
- 运水人
- 交通
编码这些事件类型很可能会被证明有用!特别是,某些服务延迟类型可能比其他类型更有影响,从而增加了服务延迟更长的风险。我们希望对服务延迟类型进行编码,并在后续建模中将它们用作参数。
为此,让我们应用一种热编码的变体,它执行以下操作:
- 它为每种服务风险类型创建一个条件变量,并将所有值设置为零
- 它检查每项服务风险类型条款的推文内容
- 它将包含特定风险术语的每条推文的相关条件变量设置为 1
这有效地执行了一次性编码,而没有采取麻烦的中间步骤,即创建我们通常要处理的阶乘变量:
from sklearn import preprocessing
enc = preprocessing.OneHotEncoder(categorical_features='all', dtype= 'float', handle_unknown='error', n_values='auto', sparse=True)
tweets.delayencode = enc.transform(tweets.delaytype).toarray()
除了我们可以在每个事件的基础上使用的功能之外,我们还可以查看服务中断风险和中断频率之间的关系。如果我们在一周内看到两次中断,第三次中断的可能性更大还是更小?
虽然这些问题很有趣,而且可能很有成果,但通常更谨慎的做法是在第一遍就建立一个有限的特征集和简单的模型,而不是过度设计一个庞大的特征集。因此,我们将运行初始发生率特性,并查看最终结果。
消费者评论
2010 年的一个主要文化发展是广泛使用公共在线域名进行自我表达。如果我们知道如何利用这一点,这其中最令人高兴的产品之一就是可以获得大量关于任意数量主题的自我报告信息。
通勤中断是激发个人反应的频繁发生的事件,这意味着它们往往会在社交媒体上被广泛报道。如果我们为关键词搜索编写一个合适的字典,我们就可以开始使用推特,尤其是作为一个关于城市交通和运输问题的有时间戳的信息来源。
为了收集这些数据,我们将使用基于字典的搜索方法。我们对所讨论时期的大多数推文不感兴趣(由于我们使用的是 RESTful API,因此需要考虑回报限制)。相反,我们感兴趣的是识别包含与拥塞或延迟相关的关键术语的推文数据。
不幸的是,从大量用户那里获得的推文往往不符合有助于分析的一致风格。我们将不得不应用我们在前一章中开发的一些技术,将这些数据分解成更容易分析的格式。
除了使用基于字典的搜索,我们还可以做一些工作来缩小搜索范围。实现这一点最权威的方法是使用坐标边界框作为推特应用编程接口的参数,这样任何相关的查询都只返回从这个区域收集的结果。
一如既往,在我们第一次通过时,我们会保持简单。在这种情况下,我们将统计当前时段的流量中断推文数量。在随后的迭代中,我们可以利用这些数据做一些额外的工作。正如 Translink 数据包含明确定义的延迟原因类别一样,我们可以尝试使用专门的字典来基于关键术语(例如,与构造相关的术语和同义词的字典)隔离延迟类型。
我们还可以考虑定义一个比简单的近期统计更细致入微的颠覆性推文率量化。例如,我们可以考虑创建一个加权计数功能,通过非线性加权来增加多个并发推文的影响(可能表示严重中断)。
必应流量 API
我们要进入的下一个应用编程接口是必应流量应用编程接口。这个 API 的优点是很容易访问;它是免费提供的(而一些竞争对手的 API 坐在付费墙后面),返回数据,并提供良好的细节水平。除其他外,该应用编程接口还返回事故位置代码、事故的一般描述以及拥堵信息、事故类型代码和开始/结束时间戳。
有益的是,此 API 提供的事件类型代码描述了一组广泛的事件类型,如下所示:
Accident。Congestion。DisabledVehicle。MassTransit。Miscellaneous。OtherNews。PlannedEvent。RoadHazard。Construction。Alert。Weather。
此外,还提供了严重性代码,其严重性值翻译如下:
LowImpact。Minor。Moderate。Serious。
然而,一个缺点是,这个应用编程接口不能接收区域之间一致的信息。例如,在法国查询会返回多个其他事件类型的代码,(我观察了法国北部一个城镇一个月的时间,得到 1、3、5、8。)但似乎没有显示所有代码。在其他地方,可用的数据甚至更少。可悲的是,温哥华倾向于只显示代码 9 或 5 的数据,但即使是杂项编码的事件似乎也与建筑有关:
Closed between Victoria Dr and Commercial Dr - Closed. Construction work. 5
这是一个有些麻烦的限制。不幸的是,这不是我们可以轻易解决的事情;必应的应用编程接口并没有提供我们想要的所有数据!除非我们为更完整的数据集付费(或者在您所在的地区有更全面的数据采集应用编程接口!),我们将到需要继续使用我们所拥有的。
查询该应用编程接口的示例如下:
importurllib.request, urllib.error, urllib.parse
import json
latN = str(49.310911)
latS = str(49.201444)
lonW = str(-123.225544)
lonE = str(-122.903931)
url = 'http://dev.virtualearth.net/REST/v1/Traffic/Incidents/'+latS+','+lonW+','+latN+','+lonE+'?key='GETYOUROWNKEYPLEASE'
response = urllib.request.urlopen(url).read()
data = json.loads(response.decode('utf8'))
resources = data['resourceSets'][0]['resources']
print('----------------------------------------------------')
print('PRETTIFIED RESULTS')
print('----------------------------------------------------')
for resourceItem in resources:
description = resourceItem['description']
typeof = resourceItem['type']
start = resourceItem['start']
end = resourceItem['end']
print('description:', description);
print('type:', typeof);
print('starttime:', start);
print('endtime:', end);
print('----------------------------------------------------')
This example yields the following data;
----------------------------------------------------
PRETTIFIED RESULTS
----------------------------------------------------
description: Closed between Boundary Rd and PierviewCres - Closed due to roadwork.
type: 9
severity 4
starttime: /Date(1458331200000)/
endtime: /Date(1466283600000)/
----------------------------------------------------
description: Closed between Commercial Dr and Victoria Dr - Closed due to roadwork.
type: 9
severity 4
starttime: /Date(1458327600000)/
endtime: /Date(1483218000000)/
----------------------------------------------------
description: Closed between Victoria Dr and Commercial Dr - Closed. Construction work.
type: 5
severity 4
starttime: /Date(1461780543000)/
endtime: /Date(1481875140000)/
----------------------------------------------------
description: At Thurlow St - Roadwork.
type: 9
severity 3
starttime: /Date(1461780537000)/
endtime: /Date(1504112400000)/
----------------------------------------------------
即使在认识到不同地理区域代码可用性不均衡的缺点后,来自这个 API 的数据应该会给我们提供一些价值。对交通中断事件有一个局部的了解仍然能给我们一个合理时期的数据。在我们自己定义的区域内定位交通事故并返回与当前日期相关的数据的能力可能有助于我们模型的性能。
使用特征工程技术推导和选择变量
在我们第一次通过输入数据时,我们反复选择保持初始特征集小。虽然我们在数据中看到了许多机会,但我们优先考虑的是查看初步结果,而不是跟进这些机会。
然而,很可能我们的第一个数据集不会帮助我们非常有效地解决问题或达到我们的目标。在这种情况下,我们需要迭代我们的特征集,通过创建新的特征和筛选我们的特征集来减少特征创建过程的有价值的输出。
一个有用的例子涉及到一个热点编码和 RFE。在本章中,我们将使用 one-hot 将天气数据和推文词典转换为 m*n 大小的张量。产生了 m 个新的数据列后,我们希望减少我们的模型被这些新特性误导的可能性(例如,在多个特性强化相同信号的情况下,或者误导性但常用的术语没有被我们在第 6 章、文本特性工程中描述的数据清理过程清除的情况下)。RFE 可以非常有效地做到这一点,这是我们在本章前面讨论的特征选择技术。
总的来说,使用扩展-收缩过程应用上两章中的技术的方法工作会很有帮助。首先,使用能够生成潜在有价值的新特性的技术,例如转换和编码,来扩展特性集。然后,使用能够识别这些特性中性能最好的子集的技术来移除性能不佳的特性。在整个过程中,测试不同的目标特征计数,以确定在不同特征数量下的最佳可用特征集。
一些数据科学家解释了这是如何不同于其他人。一些人将使用我们已经讨论过的特征创建技术的重复迭代来构建他们所有的特征,然后减少那个特征集——其动机是这个工作流最小化了丢失数据的风险。其他人将迭代执行整个过程。你选择怎么做完全取决于你自己!
在我们最初传递输入数据时,我们有一个如下所示的特征集:
{
'DisruptionInformation': {
'Date': '15-05-2015',
'TranslinkTwitter': [{
'Service': '0',
'DisruptionIncidentCount': '4'
}, {
'Service': '1',
'DisruptionIncidentCount': '0'
}]
},
'BingTrafficAPI': {
'NewIncidentCount': '1',
'SevereIncidentCount': '1',
'IncidentCount': '3'
},
'ConsumerTwitter': {
'DisruptionTweetCount': '4'
}
}
这个数据集不太可能表现良好。尽管如此,让我们通过一个基本的初始算法来运行它,并大致了解我们离目标有多近;这样,我们可以用最少的开销快速学习!
为了方便起见,让我们从使用非常简单的回归算法运行第一遍开始。技术越简单,我们运行它的速度就越快(通常,它对我们来说就越清楚出了什么问题以及原因)。出于这个原因(因为我们处理的是具有连续输出的回归问题,而不是分类问题),第一遍我们将使用一个简单的线性回归模型:
from sklearn import linear_model
tweets_X_train = tweets_X[:-20]
tweets_X_test = tweets_X[-20:]
tweets_y_train = tweets.target[:-20]
tweets_y_test = tweets.target[-20:]
regr = linear_model.LinearRegression()
regr.fit(tweets_X_train, tweets_y_train)
print('Coefficients: \n', regr.coef_)
print("Residual sum of squares: %.2f" % np.mean((regr.predict(tweets_X_test) - tweets_y_test) ** 2))
print('Variance score: %.2f' % regr.score(tweets_X_test, tweets_y_test))
plt.scatter(tweets_X_test, tweets_y_test, color='black')
plt.plot(tweets_X_test, regr.predict(tweets_X_test), color='blue',linewidth=3)
plt.xticks(())
plt.yticks(())
plt.show()
在这一点上,我们的 AUC 相当烂;我们看到的是 AUC 为 0.495 的车型。我们实际上比我们的目标做得更糟!让我们打印出一个混淆矩阵,看看这个模型做错了什么:
根据这个矩阵,它做什么都不太好。事实上,它声称几乎所有的记录都没有显示任何事件,以至于错过了 90%的真正中断!
考虑到我们的模型和特性处于早期阶段,以及一些输入数据的不确定效用,这实际上一点也不坏。与此同时,我们应该预计发生率为 6%(因为我们的培训数据表明,事件大约每 16 次通勤发生一次)。我们仍然会做得更好一点,因为我们猜测每天的通勤都会中断(如果我们忽略了每天早退对我们生活方式的影响)。
让我们考虑下一轮我们能做什么改变。
- 首先,我们可以进一步改进我们的输入数据。我们确定了许多新特性,可以使用一系列转换技术从现有资源中创建这些特性。
- 其次,我们可以考虑使用附加信息来扩展数据集。特别是,描述温度和湿度的天气数据集可以帮助我们改进模型。
- 最后,我们可以升级我们的算法,让它更咕噜咕噜,随机森林或 SVM 就是明显的例子。有充分的理由暂时不这样做。主要原因是我们可以从线性回归中继续学到很多东西;我们可以与早期的结果进行比较,以了解我们的更改增加了多少价值,同时保留快速迭代循环和简单的评分方法。一旦我们开始在功能准备上获得最低回报,我们就应该考虑升级我们的模型。
目前,我们将继续升级数据集。我们有很多选择。我们可以将位置编码到来自必应应用编程接口“描述”字段的交通事件数据和 Translink 的推文中。就 Translink 而言,对于公交线路而言,这可能比天车线路更有用(鉴于我们将分析范围限制为仅关注交通通勤)。
我们可以通过两种方式中的一种来实现这个目标;
- 使用街道名称/位置的语料库,我们可以解析输入数据并构建一个热门矩阵
- 我们可以简单地对整个推文和整个 API 数据集进行一次性编码
有趣的是,如果我们打算在执行一次热编码后使用降维技术,我们可以对两条文本信息的整个主体进行编码,而没有任何重大问题。如果与推文和文本中使用的其他单词相关的功能不相关,它们将在 RFE 会议期间被删除。
这是一种稍微放任的方法,但有一个微妙的优势。也就是说,如果任何一个数据源中有一些其他潜在有用的内容,而这些内容是我们迄今为止忽略的潜在特性,那么这个过程将产生基于这些信息创建特性的额外好处。
让我们以编码延迟类型的相同方式编码位置:
from sklearn import preprocessing
enc = preprocessing.OneHotEncoder(categorical_features='all', dtype= 'float', handle_unknown='error', n_values='auto', sparse=True)
tweets.delayencode = enc.transform(tweets.location).toarray()
此外,我们应该跟进我们的意图,从 Translink 和 Bing 地图事件日志中创建最近的计数变量。本章随附的 GitHub 存储库中提供了这种聚合的代码!
用这个更新的数据重新运行我们的模型产生的结果略有改善;预测方差得分上升到 0.56。虽然不引人注目,但这绝对是朝着正确方向迈出的一步。
接下来,让我们继续我们的第二个选项——添加一个提供天气数据的新数据源。
天气空气污染指数
我们之前已经获取了数据,这些数据将帮助我们判断通勤中断是否正在发生——识别现有延误的反应性数据源。我们现在要做一些改变,试图找到与延误和拥堵原因相关的数据。道路工程和施工信息肯定属于这一类(还有其他一些必应交通应用编程接口代码)。
一个因素是(坊间流传!)与通勤时间增加相关的是坏天气。有时候这很明显;严寒或大风对通勤时间有明显影响。然而,在许多其他情况下,不清楚对于给定的通勤,气候因素和中断可能性之间的关系的强度和性质是什么。
通过从具有足够粒度和地理覆盖范围的来源提取相关天气数据,我们有望使用强天气信号来帮助改进我们对中断的正确预测。
出于我们的目的,我们将使用雅虎天气应用编程接口,它提供一系列温度、大气、压力相关和其他气候数据,包括当前和预测的。我们可以查询雅虎天气应用编程接口,而不需要密钥或登录过程,如下所示:
import urllib2, urllib, json
baseurl = https://query.yahooapis.com/v1/public/yql?
yql_query = "select item.condition from weather.forecast where woeid=9807"
yql_url = baseurl + urllib.urlencode({'q':yql_query}) + "&format=json"
result = urllib2.urlopen(yql_url).read()
data = json.loads(result)
print data['query']['results']
为了理解应用编程接口能提供什么,用*替换item.condition(在本质上是一个嵌入式的 SQL 查询中)。该查询会输出大量信息,但深入挖掘会发现有价值的信息,包括当前条件:
{
'channel': {
'item': {
'condition': {
'date': 'Thu, 14 May 2015 03:00 AM PDT', 'text': 'Cloudy', 'code': '26', 'temp': '46'
}
}
}
}
包含以下信息的 7 天预测:
{
'item': {
'forecast': {
'code': '39', 'text': 'Scattered Showers', 'high': '60', 'low': '44', 'date': '16 May 2015', 'day': 'Sat'
}
}
}
和其他当前天气信息:
'astronomy': {
'sunset': '8:30 pm', 'sunrise': '5:36 am'
'wind': {
'direction': '270', 'speed': '4', 'chill': '46'
为了建立一个训练数据集,我们每天通过一个从 2015 年 5 月到 2016 年 1 月运行的自动化脚本提取数据。这些预测可能对我们没有太大的用处,因为我们的模型可能会每天重新运行当前的数据,而不是依赖于预测。但是,我们肯定会使用wind.direction、wind.speed、wind.chill变量,以及condition.temperature、condition.text变量。
关于如何进一步处理这些信息,有一个选项跃入脑海。天气标签的一次性编码将使我们能够使用天气条件信息作为分类变量,就像我们在前面一章中所做的那样。这似乎是一个必要的步骤。这极大地扩充了我们的功能集,为我们留下了以下数据:
{
'DisruptionInformation': {
'Date': '15-05-2015',
'TranslinkTwitter': [{
'Service': '0',
'DisruptionIncidentCount': '4'
}, {
'Service': '1',
'DisruptionIncidentCount': '0'
}]
},
'BingTrafficAPI': {
'NewIncidentCount': '1',
'SevereIncidentCount': '1',
'IncidentCount': '3'
},
'ConsumerTwitter': {
'DisruptionTweetCount': '4'
},
'YahooWeather':{
'temp: '45'
'tornado': '0',
'tropical storm': '0',
'hurricane': '0',
'severe thunderstorms': '0',
'thunderstorms': '0',
'mixed rain and snow': '0',
'mixed rain and sleet': '0',
'mixed snow and sleet': '0',
'freezing drizzle': '0',
'drizzle': '0',
'freezing rain': '0',
'showers': '0',
'snow flurries': '0',
'light snow showers': '0',
'blowing snow': '0',
'snow': '0',
'hail': '0',
'sleet': '0',
'dust': '0',
'foggy': '0',
'haze': '0',
'smoky': '0',
'blustery': '0',
'windy': '0',
'cold': '0',
'cloudy': '1',
'mostly cloudy (night)': '0',
'mostly cloudy (day)': '0',
'partly cloudy (night)': '0',
'partly cloudy (day)': '0',
'clear (night)': '0',
'sunny': '0',
'fair (night)': '0',
'fair (day)': '0',
'mixed rain and hail': '0',
'hot': '0',
'isolated thunderstorms': '0',
'scattered thunderstorms': '0',
'scattered showers': '0',
'heavy snow': '0',
'scattered snow showers': '0',
'partly cloudy': '0',
'thundershowers': '0',
'snow showers': '0',
'isolated thundershowers': '0',
'not available': '0',
}
很有可能很多时间会被有价值地投入到进一步丰富雅虎天气应用编程接口提供的天气数据中。对于第一遍,一如既往,我们将继续专注于构建一个采用我们之前描述的特性的模型。
注
我们将如何利用这些数据做进一步的工作,这绝对值得考虑。在这种情况下,区分跨列数据转换和跨行转换非常重要。
跨列转换是指来自同一输入案例中不同特征的变量基于彼此进行转换。例如,我们可以获取案例的开始日期和结束日期,并使用它来计算持续时间。有趣的是,我们在本书中学习的大多数技术不会从许多这样的转换中获得很多。大多数能够绘制非线性决策边界的机器学习技术倾向于在数据集建模中对变量之间的关系进行编码。深度学习技术通常会使这种能力更进一步。这是一些特征工程技术(尤其是基本转换)对深度学习应用程序增加较少价值的部分原因。
同时,跨行转换通常是一种聚合。例如,最后 n 多持续时间值的中心趋势是一个可以通过对多行的操作得到的特征。自然,一些特性可以通过列式和行式操作的组合来导出。跨行转换的有趣之处在于,模型通常不太可能训练识别它们,这意味着它们倾向于在非常特殊的环境中继续增加价值。
当然,这些信息相关的原因是最近的天气是一个背景,在这个背景下,来自跨行操作的特征可能会给我们的模型增加新的信息。例如,过去 n 小时内大气压力或温度的变化可能是比当前压力或温度更有用的变量。(特别是,当我们的模型旨在预测当天晚些时候的通勤时!)
下一步是重新运行我们的模型。这一次,我们的 AUC 高了一点;我们得分 0.534 。查看我们的困惑矩阵,我们也看到了改进:
如果问题与天气因素相关联,继续提取天气数据是个好主意;将该解决方案设置为在一段较长的时间内运行,将逐渐从每个来源收集纵向输入,逐渐为我们提供更可靠的预测。
在这一点上,我们离我们的 MVP 目标只有很短的距离。我们可以继续扩展我们的输入数据集,但明智的解决方案是找到另一种方法来解决问题。我们可以有意义地采取两种行动。
注
作为人类,数据科学家倾向于从简化假设的角度来思考。其中一个经常出现的例子是帕累托原则在成本/收益分析决策中的应用。从根本上说,帕累托原则指出,对于许多事件,大约 80%的价值或效果来自大约 20%的投入努力或原因,遵循所谓的帕累托分布。这个概念在软件工程环境中非常流行,因为它可以指导效率的提高。
为了将这一理论应用于当前的情况,我们知道我们可以花更多的时间来完善我们的特征工程。有些技术我们还没有应用,有些功能我们可以创建。然而,与此同时,我们知道有整个领域我们还没有触及:尤其是外部数据搜索和模型更改,我们可以快速尝试。在深入研究额外的数据集准备之前,在我们的下一轮中探索这些便宜但可能有影响的选项是有意义的。
在我们的探索性分析中,我们注意到我们的一些变量相当稀疏。目前还不清楚它们的帮助有多大(尤其是对于特定类型事故发生较少的车站)。
让我们使用本章前面使用的一些技术来测试我们的变量集。具体来说,让我们将Lasso应用于将我们的特征集简化为性能子集的问题:
fromsklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(DisruptionInformation["data"])
Y = DisruptionInformation["target"]
names = DisruptionInformation["feature_names"]
lasso = Lasso(alpha=.3)
lasso.fit(X, Y)
print "Lasso model: ", pretty_print_linear(lasso.coef_, names, sort = True)
这种输出立即有价值。很明显,许多天气特征(要么没有足够频繁地出现,要么在出现时没有告诉我们任何有用的信息)对我们的模型没有任何帮助,应该删除。此外,我们没有从我们的流量总量中获得很多价值。虽然这些可以暂时保留下来(希望收集更多的数据将提高它们的有用性),但是对于我们的下一步,我们将重新运行我们的模型,没有我们使用 LASSO 所揭示的得分很低的特征。
有一个相当便宜的额外变化,我们应该做:我们应该升级我们的模型,可以非线性拟合,从而可以拟合任何函数。这是值得做的,因为正如我们所观察到的,我们的一些特征显示了一系列表明非线性潜在趋势的偏斜分布。让我们对这个数据集应用一个随机森林:
fromsklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
rf = RandomForestRegressor(n_jobs = 3, verbose = 3, n_estimators=20)
rf.fit(DisruptionInformation_train.targets,DisruptionInformation_train.data)
r2 = r2_score(DisruptionInformation.data, rf.predict(DisruptionInformation.targets))
mse = np.mean((DisruptionInformation.data - rf.predict(DisruptionInformation.targets))**2)
pl.scatter(DisruptionInformation.data, rf.predict(DisruptionInformation.targets))
pl.plot(np.arange(8, 15), np.arange(8, 15), label="r^2=" + str(r2), c="r")
pl.legend(loc="lower right")
pl.title("RandomForest Regression with scikit-learn")
pl.show()
让我们再次回到我们的困惑矩阵:
在这一点上,我们做得相当好。我们模型的简单升级带来了显著的改进,我们的模型正确识别了几乎 40%的通勤延迟事件(足以开始对我们有用!),同时对少量案例进行错误分类。
令人沮丧的是,这种模式仍然会让我们不正确地比正确地早起更多次。当然,黄金标准是,如果它预测更多的通勤延误,而不是导致错误(提前)开工!如果我们持续收集特征数据,我们有理由希望实现这一目标;这种模式的主要弱点是,考虑到通勤中断事件的罕见性,可供样本的案例非常少。
然而,我们已经成功地从不同的来源收集和整理了一系列数据,以便从免费获得的数据中创建一个模型,产生一个可识别的、真实世界的好处(将上班迟到的人数减少 40%)。这绝对是值得高兴的成就!
进一步阅读
我建议对特征选择进行介绍,这是安藤萨巴斯对广泛的特征选择技术的四部分探索。它充满了 Python 代码片段和明智的评论。从开始。
有关第 6 章和第 7 章中涵盖各种材料的功能选择和工程的讨论,请参考 Alexandre Bourhard-科特迪瓦在上的幻灯片 http://people . eecs . Berkeley . edu/~ Jordan/courses/294-fall 09/讲座/功能/幻灯片. pdf 。也可以考虑一下杰夫·豪伯特在courses.washington.edu/css490/2012…。
缺乏对特征创建的全面讨论,大量可用资料讨论了降维技术或特定领域所需的非常具体的特征创建。要更全面地了解可能的转换范围,一种方法是阅读代码文档。在您现有知识的基础上,一个不错的地方是 Spark ML 的特征转换算法文档,位于https://Spark . Apache . org/docs/1 . 5 . 1/ML-features . html # feature-transformers,它描述了数字和文本特征的一系列可能的转换。但是请记住,特性创建通常是特定于问题、特定于领域的,并且是一个高度创造性的过程。一旦你学会了一系列技术选项,诀窍就在于弄清楚如何将这些技术应用到手头的问题上!
对于对超参数优化感兴趣的读者,我推荐大家阅读 Alice Zheng 在 Turi 博客上的帖子,作为一个很好的起点:http://blog . Turi . com/how-evaluation-machine-learning-models-part-4-超参数-tuning 。
我还发现 scikit-learn 文档是网格搜索的有用参考,特别是:scikit-learn.org/stable/modu…。
总结
在本章中,您学习并应用了一套技术,使我们能够从非常少的初始数据开始,有效地构建和精细化机器学习数据集。这些强大的技术使数据科学家能够将看似浅薄的数据集转化为机遇。我们使用一组客户服务推文来展示这种能力,以创建一个旅行中断预测器。
但是,为了将该解决方案投入生产,我们需要添加一些功能。在倒数第二步移除一些位置是一个有问题的决定;如果此解决方案旨在识别旅程中断风险,那么移除位置似乎是不可能的!鉴于我们没有全年的数据,因此无法确定季节性或纵向趋势的影响(如延长的维护工程或计划中的车站关闭),这一点尤其正确。我们在删除这些元素时有点仓促,更好的解决方案是将它们保留更长时间。
基于这些担忧,我们应该认识到有必要开始为我们的解决方案注入一些活力。当春天来临,我们的数据集开始包含新的气候条件时,我们的模型完全有可能无法有效地适应。在下一章中,我们将着眼于构建更复杂的模型集成,并讨论在模型解决方案中构建健壮性的方法。
八、集成方法
随着本书前面章节的深入,您学习了如何应用一些新技术。我们开发了几种先进的机器学习算法,并获得了广泛的配套技术,通过更有效的特征选择和准备来提高您对学习技术的使用。本章试图使用集成方法来增强您现有的技术集:将多个不同的模型绑定在一起以解决现实问题的技术。
集成技术已经成为数据科学家工具集的一个基本部分。在竞争的机器学习环境中,集成的使用已经成为一种常见的做法,集成现在被认为是许多环境中不可或缺的工具。我们将在本章中开发的技术为我们的模型提供了性能优势,同时增强了它们对底层数据变化的鲁棒性。
我们将研究一系列集合选项,讨论这些技术的代码和应用。我们将通过指导和参考现实世界的应用程序,包括由成功的卡格尔斯创建的模型来丰富这个解释。
我们在本标题中回顾的任何模型的开发都允许我们解决广泛的数据问题,但是将我们的模型应用于生产环境会带来一系列额外的问题。我们的解决方案仍然容易受到潜在观察结果变化的影响。无论是在不同的个体群体中、在时间变化中(例如,被捕捉的现象的季节性变化)还是通过潜在条件的其他变化来表达,最终结果往往是相同的——在他们被训练的条件下运行良好的模型通常不能推广并继续表现良好久而久之。
本章的最后一节描述了将本书中的技术转移到操作环境的方法,以及如果您的预期应用程序必须能够适应变化,您应该考虑的附加监控和支持的种类。
引入合奏
| | “这就是你赢得 ML 比赛的方式:你拿着别人的作品,一起合奏。” | | | | - 维塔利·库兹涅佐夫 nip 2014 |
在机器学习的上下文中,集成是一组用于解决共享问题的模型。集成由两个部分组成:一组模型和一组决定规则,这些决定规则决定了如何将这些模型的结果组合成单个输出。
集成为数据科学家提供了为给定问题构建多个解决方案的能力,然后将这些解决方案组合成单个最终结果,该结果从每个输入解决方案的最佳元素中提取。这提供了对噪声的鲁棒性,这反映在针对初始数据集的更有效的训练(导致更低水平的过拟合和训练误差的减少)以及针对前面部分讨论的那种数据变化。
毫不夸张地说,集成是机器学习中最重要的最新发展。
此外,集成使人们能够更灵活地解决给定的问题,因为它们使数据科学家能够测试解决方案的不同部分,并解决特定于输入数据子集或正在使用的模型部分的问题,而无需完全重新调整整个模型。正如我们将看到的,这可以让生活变得更容易!
根据所使用的决策规则的性质,系综通常被认为属于几个类别之一。主要的合奏类型如下:
- 平均方法:他们并行开发模型,然后使用平均或投票技术来开发组合估计器
- 堆叠(或混合)方法:它们使用多个分类器的加权输出作为下一层模型的输入
- 增强方法:它们涉及按顺序构建模型,其中每个添加的模型旨在提高组合估计器的得分
考虑到这两类集成方法的重要性和实用性,我们将依次讨论每一个:讨论理论、算法选项和真实世界的例子。
理解平均系综
平均系综在物理科学和统计建模领域有着悠久而丰富的历史,在包括分子动力学和音频信号处理在内的许多领域都有着广泛的应用。这种集合通常被视为给定系统的几乎完全相同的复制情况。该系统中病例间的平均值和方差是整个系统的关键值。
在机器学习环境中,平均集成是在同一数据集上训练的模型的集合,其结果以一系列方式聚集。根据实现目标,平均集成可以带来几个好处。
平均系综可用于降低模型性能的可变性。一种常见的方法是创建多个模型配置,这些配置采用不同的参数子集作为输入。采用这种方法的技术统称为打包算法。
使用打包算法
不同的打包实现将有不同的操作,但是共享随机获取特征空间的子集的共同属性。打包方法有四种主要类型。粘贴绘制样本的随机子集,而不进行替换。当替换完成后,这种方法简单地称为装袋。粘贴在计算上通常比打包便宜,并且可以在更简单的应用程序中产生类似的结果。
当以特征方式采集样本时,该方法被称为 随机子空间。随机子空间方法提供了稍微不同的能力;它们基本上减少了对广泛的、高度优化的特征选择的需求。在这种活动通常导致具有优化输入的单个模型的情况下,随机子空间允许并行使用多个配置,并使任何一个解决方案的方差变平。
注
虽然使用合奏来减少模型性能的可变性听起来像是一个性能打击(自然的反应可能是,但是为什么不在合奏中选择一个表现最好的模型呢?),这种方法有很大的优势。
首先,如前所述,平均提高了模型集适应不熟悉的噪声的能力(也就是说,它减少了过拟合)。其次,可以使用集成来针对输入数据集的不同元素进行有效建模。这是竞争机器学习环境中的一种常见方法,其中数据科学家将基于分类结果和特定类型的故障案例迭代调整集成。在某些情况下,这是一个详尽的过程,包括检查模型结果(通常作为正常的迭代模型开发过程的一部分),但是许多数据科学家更喜欢他们将首先实现的技术或解决方案。
随机子空间可以是一种非常强大的方法,尤其是如果有可能使用多个子空间大小并彻底检查特征组合的话。随机子空间方法的成本随着数据集的大小非线性地增加,超过某个点,测试多个子空间大小的每个参数配置将变得昂贵。
最后,可以用一种称为 随机面片的方法,从样本和特征抽取的子集创建一个集合的估计器。在相似的情况下,随机补丁的性能通常与随机子空间技术的性能大致相同,内存消耗显著降低。
由于我们已经讨论了打包套装背后的理论,让我们看看如何实现一个。以下代码描述了使用 sklearn 的BaggingClassifier类实现的随机补丁分类器:
from sklearn.cross_validation import cross_val_score
from sklearn.ensemble import BaggingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale
digits = load_digits()
data = scale(digits.data)
X = data
y = digits.target
bagging = BaggingClassifier(KNeighborsClassifier(), max_samples=0.5, max_features=0.5)
scores = cross_val_score(bagging, X, y)
mean = scores.mean()
print(scores)
print(mean)
与许多 sklearn 分类器一样,所需的核心代码非常简单;分类器被初始化并用于对数据集进行评分。交叉验证(通过cross_val_score)不会增加任何有意义的复杂性。
这个打包分类器使用了一个 K 近邻 ( KNN )分类器(KNeighboursClassifier)作为基础,特征和案例的采样率各设置为 50%。这相对于数字数据集输出了非常强的结果,在交叉验证后正确地对 93%的病例进行了平均分类:
[ 0.94019934 0.92320534 0.9295302 ]
0.930978293043
使用随机森林
另一组平均集合技术统称为随机森林。随机森林可能是竞争数据科学家使用的最成功的集成技术,它开发了并行的决策树分类器集。通过给分类器结构引入两个主要的随机性来源,森林最终包含了不同的树。用于构建每个树的数据通过替换从训练集中采样,而树创建过程不再使用来自所有特征的最佳分割,而是从特征的随机子集选择最佳分割。
使用sklearn中的RandomForestClassifier类可以很容易地调用随机森林。举个简单的例子,考虑以下内容:
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale
digits = load_digits()
data = scale(digits.data)
n_samples, n_features = data.shape
n_digits = len(np.unique(digits.target))
labels = digits.target
clf = RandomForestClassifier(n_estimators=10)
clf = clf.fit(data, labels)
scores = clf.score(data,labels)
print(scores)
这个合奏输出的分数 0.999,很难打。事实上,我们在前面几章中使用的任何单个模型都没有看到这种水平的性能。
随机森林的变体,称为 极随机树(extracrees),使用相同的随机特征子集方法来选择树中每个分支的最佳分割。然而,它也随机化了辨别阈值;决策树通常选择最有效的类间分割,而提取树以随机值分割。
由于决策树的训练相对有效,随机森林算法可以潜在地支持大量不同的树,分类器的有效性随着节点数量的增加而提高。引入的随机性为噪声或数据变化提供了一定程度的鲁棒性;然而,就像我们前面回顾的 bagging 算法一样,这种增益通常是以性能略微下降为代价的。在提取树的情况下,稳健性可能会进一步提高,而性能度量会提高(通常偏差值会降低)。
下面的代码描述了提取树在实践中是如何工作的。就像我们的随机子空间实现一样,代码非常简单。在这种情况下,我们将开发一组模型来比较树外树和随机森林方法的效果:
from sklearn.cross_validation import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_digits
from sklearn.preprocessing import scale
digits = load_digits()
data = scale(digits.data)
X = data
y = digits.target
clf = DecisionTreeClassifier(max_depth=None, min_samples_split=1,
random_state=0)
scores = cross_val_score(clf, X, y)
print(scores)
clf = RandomForestClassifier(n_estimators=10, max_depth=None,
min_samples_split=1, random_state=0)
scores = cross_val_score(clf, X, y)
print(scores)
clf = ExtraTreesClassifier(n_estimators=10, max_depth=None,
min_samples_split=1, random_state=0)
scores = cross_val_score(clf, X, y)
print(scores)
分数分别如下:
[ 0.74252492 0.82136895 0.75671141]
[ 0.88372093 0.9015025 0.8909396 ]
[ 0.91694352 0.93489149 0.91778523]
假设我们在这里使用的是完全基于树的方法,分数就是正确标注的案例的比例。我们可以在这里看到,这两种森林方法之间没有太大的区别,它们都表现强劲,平均得分为 0.9 。在这个例子中,随机森林实际上比提取树略胜一筹(大约增加了 0.002 ,而这两种技术都大大优于基本决策树,基本决策树的平均得分为 0.77 。
使用随机森林时的一个缺点是(特别是随着森林规模的增加)很难检查或调整给定实现的有效性。虽然单独的树非常容易处理,但是一个开发的集合中的树的数量以及随机分裂所产生的混淆会使改进随机森林实现变得非常困难。一种选择是开始查看单个模型所画的决策边界。通过对比一个集合中的模型,可以更容易地识别出一个模型在划分类时比其他模型表现更好的地方。
例如,在这个例子中,我们可以很容易地看到我们的模型在高水平上的表现,而不需要挖掘具体的细节:
虽然超越简单的层次(使用高层次的图和汇总分数)理解随机森林实现的表现可能是具有挑战性的,但困难是值得的。随机森林的性能非常强,只需要最小的额外计算成本。在早期阶段,当一个人还在确定攻击角度时,他们往往是解决问题的好方法,因为他们快速产生强有力结果的能力可以提供一个有用的基准。一旦您知道了随机森林实现的性能,您就可以开始优化和扩展您的集成。
为此,我们应该继续探索不同的集合技术,以便进一步构建我们的集合选项工具包。
应用助推方法
系综创建的另一种方法是构建增强模型。这些模型的特点是它们按顺序使用多个模型来迭代地“提升”或提高集合的性能。
增强模型经常使用一系列弱学习者,与随机猜测相比,这些模型只能提供边际收益。在每次迭代中,一个新的弱学习者在一个调整过的数据集上被训练。在多次迭代中,集成在每次迭代中用一个新的树(优化集成性能分数的树)扩展。
也许最著名的提升方法是 AdaBoost ,它通过执行以下操作在每次迭代时调整数据集:
- 选择一个决策树桩(一个浅的、通常是一级的决策树,实际上是所讨论数据集最重要的决策边界)
- 增加决策树桩标注不正确的案例的权重,同时减少标注正确的案例的权重
这种迭代权重调整使得集成中的每个新分类器优先训练错误标记的案例;该模型通过瞄准高度加权的数据点进行调整。最终,树桩被组合成最终的分类器。
AdaBoost 可以在分类和回归上下文中使用,并获得令人印象深刻的结果。以下示例显示了在heart数据集上运行的 AdaBoost 实现:
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.datasets.mldata import fetch_mldata
from sklearn.cross_validation import cross_val_score
n_estimators = 400
# A learning rate of 1\. may not be optimal for both SAMME and SAMME.R
learning_rate = 1.
heart = fetch_mldata("heart")
X = heart.data
y = np.copy(heart.target)
y[y==-1]=0
X_test, y_test = X[189:], y[189:]
X_train, y_train = X[:189], y[:189]
dt_stump = DecisionTreeClassifier(max_depth=1, min_samples_leaf=1)
dt_stump.fit(X_train, y_train)
dt_stump_err = 1.0 - dt_stump.score(X_test, y_test)
dt = DecisionTreeClassifier(max_depth=9, min_samples_leaf=1)
dt.fit(X_train, y_train)
dt_err = 1.0 - dt.score(X_test, y_test)
ada_discrete = AdaBoostClassifier(
base_estimator=dt_stump,
learning_rate=learning_rate,
n_estimators=n_estimators,
algorithm="SAMME")
ada_discrete.fit(X_train, y_train)
scores = cross_val_score(ada_discrete, X_test, y_test)
print(scores)
means = scores.mean()
print(means)
在这种情况下,n_estimators参数指示使用的弱学习者的数量;在平均方法的情况下,添加估计器总是会降低模型的偏差,但会增加模型过度训练其训练数据的概率。base_estimator参数可以用来定义不同的弱学习者;默认值是决策树(因为训练一棵弱树很简单,可以使用树桩,非常浅的树)。当应用于heart数据集时,如本例所示,AdaBoost 在略高于 79%的情况下实现了正确标注,这对于第一遍来说是相当可靠的性能:
[ 0.77777778 0.81481481 0.77777778]
0.79012345679
增压模型比平均模型具有显著优势;它们使得创建识别问题案例或问题案例类型并解决它们的集合变得容易得多。增强模型通常会首先针对最容易预测的案例,每个添加的模型都适合剩余的错误预测案例的子集。
由此产生的一个风险是增强模型开始过度拟合(在最极端的情况下,你可以想象集成组件已经适合特定的情况!)的训练数据。管理集合组件的正确数量是一个棘手的问题,但谢天谢地我们可以借助一种熟悉的技术来解决它。在第 1 章、无监督机器学习中,我们讨论了一种称为 肘关节法的视觉启发式方法。在的情况下,该图是 K (平均值的数量),而不是集群实现的性能度量。在这种情况下,我们可以使用类似的过程,使用估计量的数量( n )和总体的偏差或误差率(我们称之为 e)。对于一系列不同的增强估计器,我们可以将它们的输出绘制如下:
通过确定曲线开始变平的点,我们可以降低我们的模型过度拟合的风险,随着曲线开始变平,这种风险变得越来越有可能。这是真的,原因很简单,随着曲线水平,这必然意味着来自每个新的估计器的附加增益是越来越少的情况的正确分类!
这种视觉辅助工具的部分吸引力在于,它使我们能够感受到我们的解决方案可能会过度拟合。我们可以(也应该!)尽可能地应用验证技术,但在某些情况下(例如,当目标是实现模型实现的特定 MVP 目标时,无论是通过用例还是 Kaggle 公共排行榜上的分数分布来通知),我们可能会倾向于推进性能实现。当我们添加每一个新的估计量时,准确理解我们所获得的收益是如何衰减的,这对于理解过度拟合的风险至关重要。
使用 XGBoost
2015 年年中,一种解决结构化机器学习问题的新算法——XGboost,在竞争激烈的数据科学领域掀起了一阵风暴。极限梯度增强 ( XGBoost )是一个编写良好的性能库,提供了一个通用的增强算法(梯度增强)。
XGBoost 的工作方式很像 AdaBoost,但有一个关键区别——改进模型的方式不同。
在每次迭代中,XGBoost 都试图通过减少该集合的残差(目标和标签预测之间的差异)来提高现有模型集的性能。每次迭代,所添加的模型都是根据它是否最能减少现有集合的残差来选择的。这类似于梯度下降(通过逆着损失梯度移动来迭代地最小化函数);因此,这个名字叫做梯度增强。
事实证明,Gradient Boosting 在最近的 Kaggle 竞赛中非常成功,它在 2015 年下半年支持了 CrowdFlower 竞赛和微软恶意软件分类挑战赛以及许多其他结构化数据竞赛的获胜者。
要应用 XGBoost,让我们获取 XGBoost 库。最好的方法是通过pip,命令行上有pip install xgboost命令。对于 Windows 用户,pip安装目前(2015 年末)在 Windows 上被禁用。为了您的利益,在本书的 GitHub 资源库的Chapter 8文件夹中提供了一份 XGBoost 的冷拷贝。
应用 XGBoost 相当简单。在这种情况下,我们将使用 UCI 皮肤病学数据集将该库应用于多类分类任务。该数据集包含一个年龄变量和大量分类变量。示例数据行如下所示:
3,2,0,2,0,0,0,0,0,0,0,0,1,2,0,2,1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,10,2
少数年龄值(倒数第二特征)缺失,由?编码。使用该数据集的目的是根据以下类别分布正确分类六种不同皮肤状况中的一种:
Database: Dermatology
Class code: Class: Number of instances:
1 psoriasis 112
2 seboreic dermatitis 61
3 lichen planus 72
4 pityriasis rosea 49
5 cronic dermatitis 52
6 pityriasis rubra pilaris 20
我们将通过加载数据并通过 70/30 分割将其划分为测试和训练案例来开始对这个问题应用 XGBoost:
import numpy as np
import xgboost as xgb
data = np.loadtxt('./dermatology.data', delimiter=',',converters={33: lambda x:int(x == '?'), 34: lambda x:int(x)-1 } )
sz = data.shape
train = data[:int(sz[0] * 0.7), :]
test = data[int(sz[0] * 0.7):, :]
train_X = train[:,0:33]
train_Y = train[:, 34]
test_X = test[:,0:33]
test_Y = test[:, 34]
此时,我们初始化并参数化我们的模型。eta参数定义步长收缩。在梯度下降算法中,使用收缩参数来减小更新的大小是非常常见的。梯度下降算法有一种趋势(特别接近收敛)在最优值上来回曲折;使用收缩参数来缩小变化的大小可以使梯度下降的效果更加精确。常见的(也是默认的)缩放值是0.3。在这个例子中,eta已经被设置为0.1以获得更高的精度(以更多迭代的可能代价)。
max_depth参数直观;它定义了示例中任何树的最大深度。给定六个输出类,六是一个合理的开始值。num_round参数定义了算法将执行多少轮梯度增强。同样,对于有更多类的多类问题,通常需要更多轮次。同时,nthread参数定义了代码将运行多少个 CPU 线程。
这里使用的DMatrix结构纯粹是为了训练速度和记忆优化。使用 XGBoost 时使用这些通常是个好主意;它们可以从numpy.arrays开始建造。使用DMatrix启用watchlist功能,解锁一些高级功能。特别是,watchlist允许我们监控所提供列表中所有数据的评估结果:
xg_train = xgb.DMatrix( train_X, label=train_Y)
xg_test = xgb.DMatrix(test_X, label=test_Y)
param = {}
param['objective'] = 'multi:softmax'
param['eta'] = 0.1
param['max_depth'] = 6
param['nthread'] = 4
param['num_class'] = 6
watchlist = [ (xg_train,'train'), (xg_test, 'test') ]
num_round = 5
bst = xgb.train(param, xg_train, num_round, watchlist );
我们训练我们的模型bst,以生成初始预测。然后,我们重复训练过程,生成启用softmax的预测(通过multi:softprob):
pred = bst.predict( xg_test );
print ('predicting, classification error=%f' % (sum( int(pred[i]) != test_Y[i] for i in range(len(test_Y))) / float(len(test_Y)) ))
param['objective'] = 'multi:softprob'
bst = xgb.train(param, xg_train, num_round, watchlist );
yprob = bst.predict( xg_test ).reshape( test_Y.shape[0], 6 )
ylabel = np.argmax(yprob, axis=1)
print ('predicting, classification error=%f' % (sum( int(ylabel[i]) != test_Y[i] for i in range(len(test_Y))) / float(len(test_Y)) ))
使用堆叠集合
我们在本章前面看到的传统集成都有一个共同的设计理念:它们涉及多个经过训练的分类器来适应一组目标标签,并涉及模型本身被应用来通过包括模型投票和增强在内的策略生成一些元函数。
关于整体创作,有一种替代的设计理念,称为堆叠,或者称为混合。堆叠涉及配置中的多层模型,其中一层模型的输出被用作下一层模型的训练数据。有可能成功地融合数百种不同的模式。
堆叠系综还可以从多个子混合(有时称为混合)中组成图层输出的混合要素集。为了增加乐趣,还可以从堆叠集合的模型中提取特别有效的参数,并在不同级别的混合或子混合中将其用作元特征。
**所有这些结合在一起,使堆叠集成成为一种非常强大和可扩展的技术。卡格尔网飞奖(以及相关的 100 万美元奖金)的获奖者在数百个特写镜头上使用了叠加合奏,效果非常好。他们使用了一些额外的技巧来提高预测的有效性:
- 他们在保留一些数据的同时训练和优化了他们的整体。然后,他们使用保留的数据进行再培训,并在将模型应用于测试数据集之前再次优化。这并不是一个罕见的做法,但它产生了良好的结果,值得记住。
- 他们使用梯度下降和 RMSE 作为性能函数进行训练。至关重要的是,他们使用整体的 RMSE,而不是任何模型的,作为相关的性能指标(残差的度量)。无论何时与合奏团合作,这都应该被视为一种健康的做法。
- 他们使用已知的模型组合来改善其他模型的残差。例如,基于邻域的方法改进了 RBM 残差,我们在本书前面已经讨论过了。通过了解机器学习算法的相对优势和劣势,您可以找到理想的集成配置。
- 他们使用 k 倍交叉验证计算混合的残差,这是我们在本书前面探索和应用的另一种技术。这有助于克服这样一个事实,即他们已经使用与最终混合相同的数据集训练了混合的组成模型。
从曾经获得网飞奖的务实混沌模型的高度定制化本质中抽离出来的要点是,一流的模型通常是密集迭代和一些创造性的网络配置变化的产物。另一个关键要点是堆叠集合的基本架构模式如下:
既然你已经学习了堆叠集合如何工作的基本原理,让我们尝试应用它们来解决数据问题。为了让我们开始,我们将使用Chapter 8附带的 GitHub 存储库中提供的blend.py代码。这种混合代码的版本已经被多个比赛中得分较高的卡格勒使用。
首先,我们将研究如何应用堆叠系综来解决一个真正的数据科学问题:卡格尔竞赛预测生物反应旨在建立一个尽可能有效的模型,以预测给定化学性质的分子的生物反应。我们将关注本次竞赛中一个特别成功的参赛作品,以了解堆叠合奏如何在实践中发挥作用。
在这个数据集中,每行代表一个分子,而 1,776 个特征中的每一个都描述了所讨论的分子的特征。考虑到这些特性,我们的目标是预测相关分子的二元反应。
我们将应用的代码来自该锦标赛中的一个竞争对手,他使用堆叠集成来组合五个分类器:两个不同配置的随机森林分类器、两个额外的树分类器和一个梯度提升分类器,这有助于产生与其他四个组件略有不同的预测。
重复的分类器具有不同的划分标准。其中一个使用了基尼不纯度 T2(基尼),这是一种衡量随机记录被错误标记的频率的方法,如果它根据潜在的有问题的分支中的标记分布被随机标记。另一棵树使用信息增益(熵),一种衡量信息内容的方法。潜在分支的信息内容可以通过对其编码所需的比特数来测量。使用熵作为衡量标准来确定适当的分割会导致分支变得越来越不多样化,但重要的是要认识到熵和gini标准会产生完全不同的结果:
if __name__ == '__main__':
np.random.seed(0)
n_folds = 10
verbose = True
shuffle = False
X, y, X_submission = load_data.load()
if shuffle:
idx = np.random.permutation(y.size)
X = X[idx]
y = y[idx]
skf = list(StratifiedKFold(y, n_folds))
clfs = [RandomForestClassifier(n_estimators=100, n_jobs=-1,
criterion='gini'),
RandomForestClassifier(n_estimators=100, n_jobs=-1,
criterion='entropy'),
ExtraTreesClassifier(n_estimators=100, n_jobs=-1,
criterion='gini'),
ExtraTreesClassifier(n_estimators=100, n_jobs=-1,
criterion='entropy'),
GradientBoostingClassifier(learning_rate=0.05,
subsample=0.5, max_depth=6, n_estimators=50)]
print "Creating train and test sets for blending."
dataset_blend_train = np.zeros((X.shape[0], len(clfs)))
dataset_blend_test = np.zeros((X_submission.shape[0], len(clfs)))
for j, clf in enumerate(clfs):
print j, clf
dataset_blend_test_j = np.zeros((X_submission.shape[0],
len(skf)))
for i, (train, test) in enumerate(skf):
print "Fold", i
X_train = X[train]
y_train = y[train]
X_test = X[test]
y_test = y[test]
clf.fit(X_train, y_train)
y_submission = clf.predict_proba(X_test)[:,1]
dataset_blend_train[test, j] = y_submission
dataset_blend_test_j[:, i] =
clf.predict_proba(X_submission)[:,1]
dataset_blend_test[:,j] = dataset_blend_test_j.mean(1)
print
print "Blending."
clf = LogisticRegression()
clf.fit(dataset_blend_train, y)
y_submission = clf.predict_proba(dataset_blend_test)[:,1]
print "Linear stretch of predictions to [0,1]"
y_submission = (y_submission - y_submission.min()) /
(y_submission.max() - y_submission.min())
print "Saving Results."
np.savetxt(fname='test.csv', X=y_submission, fmt='%0.9f')
当我们尝试在私人排行榜上运行这个提交时,我们发现自己处于相当令人印象深刻的第 12 位位置(在 699 个竞争对手中)!自然,我们不能从完成后进入的竞赛中得出太多结论,但是,考虑到代码的简单性,这仍然是一个相当令人印象深刻的结果!
在实践中应用合奏
在应用集成方法时需要注意的一个特别重要的品质是,您的目标是调整集成的性能,而不是组成集成的模型。因此,你的方法应该主要集中在建立一个强有力的合奏表演得分上,而不是最强的单个模型表演。
你对整体中的模特的关注程度会有所不同。对于单一类型(例如,随机森林)的不同配置或初始化模型的排列,明智的做法是几乎完全专注于集合的性能和塑造它的元参数。
对于更具挑战性的问题,我们经常需要更密切地关注我们整体中的单个模型。当我们试图为更具挑战性的问题创建更小的集成时,这显然是正确的,但是要构建真正优秀的集成,通常需要考虑您构建的结构背后的参数和算法。
说了这么多,你就会一直在看合奏的表现以及布景中模特的表现。你将检查你的模型的结果,试图找出每个模型做得好的地方。您还将寻找影响集合性能的不太明显的因素,最显著的是模型预测的相关性。人们普遍认为,一个更有效的合奏往往包含有表演性但不相关的成分。
要理解这种说法,可以考虑相关度量和主成分分析等技术,我们可以使用这些技术来度量数据集变量中存在的信息量。同样,我们可以使用皮尔逊相关系数与我们每个模型输出的预测进行比较,以了解每个模型的性能和相关性之间的关系。
具体地说,让我们回到堆叠系综,我们的系综模型输出元特征,这些元特征然后被用作下一层模型的输入。就像我们检查更传统的神经网络所使用的特征一样,我们希望确保由我们的集成组件输出的特征作为数据集工作良好。在这方面,计算模型输出之间的皮尔逊相关系数并在模型选择中使用结果是一个很好的起点。
当我们处理单模型问题时,我们几乎总是要花一些时间来检查问题并确定一个合适的学习算法。如果我们面临一个两类分类问题,其中有适量的特征( 10 个)和标记的训练案例,我们可能会选择逻辑回归、SVM 或其他适合上下文的算法。不同的方法将适用于不同的问题,并通过反复试验,平行测试和经验(个人和网上发布!),您将确定给定特定输入数据的特定目标的适当方法。
类似的逻辑也适用于合奏创作。挑战不是识别单一的适当模型,而是识别有效描述输入数据集不同元素的模型组合,从而充分描述数据集整体。通过了解您的组件模型的优势和劣势,以及通过探索和可视化您的数据集,您将能够得出关于如何通过多次迭代有效地开发您的集成的结论。
最终,在这个层面上,数据科学是一个拥有大量技术的领域。最好的实践者能够应用他们自己的算法和选项的知识,在多次迭代中开发出非常有效的解决方案。
这些解决方案涉及算法知识和模型组合的交互、模型参数调整、数据集转换和集成操作。同样重要的是,它们需要一种无拘无束和创造性的心态。
这方面的一个很好的例子是著名的卡格尔竞争对手亚历山大·古斯钦的作品。关注一个具体的例子——奥托产品分类竞赛——可以让我们了解自信而有创造力的数据科学家可以选择的范围。
大多数模型开发过程都是从一个阶段开始的,在这个阶段中,您会针对问题抛出不同的解决方案,试图找到数据背后的技巧,并找出有效的方法。亚历山大决定采用堆叠模型,开始构建图元特征。虽然我们将 XGBoost 视为一个独立的集成,但在这种情况下,它被用作堆叠集成的一个组件,以便生成一些元特征供最终模型使用。除了梯度增强树之外,还使用了神经网络,因为这两种算法都倾向于产生好的结果。
为了给混合物添加一些对比,Alexander 添加了一个 KNN 实现,特别是因为 KNN 生成的结果(以及元参数)往往与已经包含的模型有很大不同。这种拾取输出往往不同的组件的方法对于创建有效的堆叠集合(以及大多数集合类型)至关重要。
为了进一步开发这个模型,亚历山大在他的模型的第二层增加了一些定制元素。在结合 XGBoost 和神经网络预测的同时,他还在这一层增加了装袋。在这一点上,我们在本章中讨论的大多数技术已经在这个模型的某些部分出现了。除了模型开发之外,一些特征工程(特别是在一半的训练和测试数据中使用 TF-IDF)和使用绘图技术来识别类别差异也被贯穿始终。
一个真正成熟的模型可以解决最重要的数据科学挑战,它结合了我们在本书中看到的技术,通过对底层算法以及这些技术如何相互作用的可能性的深入理解而创建。
到目前为止,这本书已经教授了许多从业者必须收集的基础知识。它使用了许多例子和越来越多的真实案例来展示广泛的知识基础如何变得越来越强大,让你开发出解决困难问题的有效方法。
作为一名数据科学家,你需要做的是首先应用这一系列广泛的技术来发展一种经验,了解他们如何表现以及他们能为你做些什么。接下来就看你如何培养那种创造力和实验思维,这种思维让一些最优秀的数据科学家与众不同。
在动态应用中使用模型
我们花了这一章讨论在条件下管理模型性能的技术的使用,这些条件可能被视为理想的;具体来说,所有数据提前可用的条件,以便可以在所有数据上训练模型。这些假设在研究环境中或在处理一次性问题时通常是有效的,但在许多情况下,它们是不安全的假设。不安全环境的范围超出了数据根本不可用的情况,例如数据科学竞赛,使用一个保留的数据集来建立最终的排行榜。
回到本章前面的主题,你会想起获得网飞奖的实用混沌算法?当网飞开始评估实现算法时,业务环境和需求都发生了巨大的变化,以至于该算法提供的最小精度增益无法证明实现成本是合理的。100 万美元的算法是多余的,从未在生产中实现过!从这个例子中可以看出,在商业环境中,我们的模型尽可能具有适应性是至关重要的。
机器学习算法真正具有挑战性的应用是跨时间(或其他维度)发生真实数据变化的应用,在这些应用中,我们现有的运行一次的方法变得不那么有价值。在这些情况下,人们知道将会发生实质性的数据变化,并且现有的模型不容易被训练来适应这种数据变化。在这一点上,需要新的技术和新的信息。
为了适应和收集这些信息,我们需要更好地预测数据变化可能发生的方式。有了这些信息,我们的模型构建和集合的内容可以开始改变,以涵盖我们看到的最有可能的数据变化场景。这种自适应让我们能够抢先进行数据更改,并减少所需的调整时间。正如我们将在本章后面看到的,在现实世界的应用程序中,任何基于数据变化的数据透视时间的减少都是有价值的。
在下一节中,我们将研究可以用来使我们的模型对不断变化的数据更加健壮的工具。我们将讨论如何维护一组广泛的模型选项,同时适应一个或多个数据更改场景,而不降低模型的性能。
理解模型的鲁棒性
重要的是要准确理解这里的问题是什么,以及它是如何和何时出现的。这包括定义两件事;首先是鲁棒性,因为它适用于机器学习算法。第二,当然是数据变化。本节第一部分的一些内容处于入门水平,但是有经验的数据科学家可能仍然会发现回顾本节的价值!
用学术术语来说,机器学习算法的健壮性是一个属性,它描述了当应用于数据集而不是训练它的数据集时,你的算法有多有效。
健壮性测试是任何环境下机器学习方法的核心部分。k-fold 交叉验证等验证技术的重要性以及在为最简单的上下文开发模型时使用测试是机器学习算法易受数据变化影响的结果。
大多数数据集包含信号和噪声。噪音可能是可预测的(因此更容易管理),也可能是随机的,难以处理。数据集可能包含或多或少的噪声。通常,具有或多或少的可预测噪声的数据集在去除该噪声的相同数据集上更难训练和测试(可以容易地测试)。
当一个人在给定的数据集上训练了一个模型时,几乎不可避免的是,这个模型是基于信号和噪声来学习的。过拟合的概念通常用于描述一个模型,该模型非常适合给定的数据集,以至于它学会了基于信号和噪声进行预测,这使得它对其他样本的预测能力不如拟合不太精确的模型。
训练模型的部分目标是尽可能减少任何局部噪声对学习的影响。保留一组数据进行测试的验证技术的目的是确保在训练期间对噪声的任何学习只发生在训练集本地的噪声上。训练误差和测试误差之间的差异可以用来理解模型实现之间的过度拟合程度。
我们已经在第 1 章、无监督机器学习中应用了交叉验证。测试过拟合模型的另一种有用的方法是以抖动的形式直接向训练数据集中添加随机噪声。2015 年 10 月,亚历山大·安希金通过卡格尔笔记本引入了这项技术,并提供了一个非常有趣的测试。概念简单;通过添加抖动并查看训练数据的预测精度,我们可以区分过度拟合的模型(随着我们添加抖动,其训练误差将更快增加)和拟合良好或拟合不良的模型:
在这种情况下,我们能够绘制抖动测试的结果,以轻松识别模型是否过度抖动。从非常强的初始位置开始,随着少量抖动的增加,overfit 模型的性能通常会迅速下降。对于拟合较好的模型,增加抖动时的性能损失会大大降低,在低水平的增加抖动时,模型的过拟合程度尤其明显(拟合较好的模型往往优于过拟合的模型)。
让我们看看如何实现过度拟合的抖动测试。我们用一个熟悉的分数,accuracy_score,定义为正确预测的类标签比例,作为考试评分的依据。抖动是通过简单地向数据添加随机噪声(使用np.random.normal)来定义的,噪声量由可配置的scale参数定义:
from sklearn.metrics import accuracy_score
def jitter(X, scale):
if scale > 0:
return X + np.random.normal(0, scale, X.shape)
return X
def jitter_test(classifier, X, y, metric_FUNC = accuracy_score, sigmas = np.linspace(0, 0.5, 30), averaging_N = 5):
out = []
for s in sigmas:
averageAccuracy = 0.0
for x in range(averaging_N):
averageAccuracy += metric_FUNC( y, classifier.predict(jitter(X, s)))
out.append( averageAccuracy/averaging_N)
return (out, sigmas, np.trapz(out, sigmas))
allJT = {}
给定一个分类器、训练数据和一组目标标签,jitter_test本身就是定义为正常 sklearn 分类的包装器。然后调用分类器,根据首先调用jitter操作的数据版本进行预测。
此时,我们将开始创建大量数据集来运行抖动测试。我们将使用 sklearn 的make_moons数据集,通常用作可视化聚类和分类算法性能的数据集。这个数据集由两个类组成,它们的数据点形成交错的半圆。通过向make_moons添加不同数量的噪声并使用不同数量的样本,我们可以创建一系列示例来运行抖动测试:
import sklearn
import sklearn.datasets
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
Xs = []
ys = []
#low noise, plenty of samples, should be easy
X0, y0 = sklearn.datasets.make_moons(n_samples=1000, noise=.05)
Xs.append(X0)
ys.append(y0)
#more noise, plenty of samples
X1, y1 = sklearn.datasets.make_moons(n_samples=1000, noise=.3)
Xs.append(X1)
ys.append(y1)
#less noise, few samples
X2, y2 = sklearn.datasets.make_moons(n_samples=200, noise=.05)
Xs.append(X2)
ys.append(y2)
#more noise, less samples, should be hard
X3, y3 = sklearn.datasets.make_moons(n_samples=200, noise=.3)
Xs.append(X3)
ys.append(y3)
完成后,我们接着创建一个plotter对象,我们将使用该对象直接根据输入数据显示模型的性能:
def plotter(model, X, Y, ax, npts=5000):
xs = []
ys = []
cs = []
for _ in range(npts):
x0spr = max(X[:,0])-min(X[:,0])
x1spr = max(X[:,1])-min(X[:,1])
x = np.random.rand()*x0spr + min(X[:,0])
y = np.random.rand()*x1spr + min(X[:,1])
xs.append(x)
ys.append(y)
cs.append(model.predict([x,y]))
ax.scatter(xs,ys,c=list(map(lambda x:'lightgrey' if x==0 else 'black', cs)), alpha=.35)
ax.hold(True)
ax.scatter(X[:,0],X[:,1],
c=list(map(lambda x:'r' if x else 'lime',Y)),
linewidth=0,s=25,alpha=1)
ax.set_xlim([min(X[:,0]), max(X[:,0])])
ax.set_ylim([min(X[:,1]), max(X[:,1])])
return
我们将使用 SVM 分类器作为抖动测试的基础模型:
import sklearn.svm
classifier = sklearn.svm.SVC()
allJT[str(classifier)] = list()
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(11,13))
i=0
for X,y in zip(Xs,ys):
classifier.fit(X,y)
plotter(classifier,X,y,ax=axes[i//2,i%2])
allJT[str(classifier)].append (jitter_test(classifier, X, y))
i += 1
plt.show()
抖动测试为评估模型过拟合提供了一种有效的手段,其性能与交叉验证相当;事实上,Minushkin 提供的证据表明,作为衡量模型拟合质量的工具,它的表现可以超过交叉验证。
这两种减轻过度拟合的工具在您的算法一次性处理数据或者底层趋势变化不大的情况下都能很好地工作。对于大多数单数据集问题(如大多数学术或网络存储库数据集)或底层趋势变化缓慢的数据问题来说,情况确实如此。
然而,在许多情况下,建模中涉及的数据可能会随着时间的推移在一个或几个维度上发生变化。这可能是因为数据采集方法的改变,通常是因为使用了新的仪器或技术。例如,自 2005 年以来的十年间,普通设备捕获的视频数据在分辨率和质量(以及大小!)的数据有所增加。无论您是使用视频帧本身,还是使用文件大小作为参数,您都将观察到特性的性质、质量和分布的显著变化。
或者,数据集变量的变化可能是由潜在趋势的差异引起的。度量和维度的经典数据模式概念又回来了,因为我们可以通过考虑哪些维度影响我们的度量来更好地理解数据变化是如何受到影响的。
关键的例子是时间。根据具体情况,许多变量会受到星期几、月份或季节变化的影响。在许多情况下,一个有用的选择可能是参数化这些变量(正如我们在上一章中所讨论的,诸如 one-hot 编码之类的技术可以帮助我们的算法学习解析这样的趋势),特别是如果我们处理的是容易预测的周期性趋势(例如,一年中某个月份对给定位置围巾销售的影响)并且容易建模的话。
更成问题的类型的时间序列趋势是非周期性变化。就像前面的摄像机例子一样,某些类型的时间序列趋势会不可逆转地发生变化,而且变化的方式可能不容易预测。来自软件的遥测往往会受到发射遥测时软件实时构建的质量和功能的影响。随着构建随时间变化,遥测发送的值和从这些值创建的变量可能会在一夜之间以难以预测的方式发生根本变化。
人类行为是许多数据集中非常重要的因素,它有助于周期性和非周期性地变化。人们更多地在季节性假期购物,但也会根据新的社会或技术发展永久地改变他们的购物习惯。
这里增加的一些复杂性不仅来自于单个变量及其分布受时间序列趋势影响的事实,还来自相关因素及其相关变量之间的关系将如何变化。变量之间的关系可能会以可量化的方式发生变化。一个例子是,对于人类来说,身高和体重是两个变量,它们的关系因时间和地点而异。我们可以使用身体质量指数特征来跟踪这种关系,当在不同时间段或不同位置进行采样时,它显示出不同的分布。
此外,变量可以以另一种严肃的方式变化;也就是说,它们对高性能建模算法的重要性可能会随着时间而变化!某些变量的值在某些时间段高度相关,但在其他时间段相关性较低。例如,考虑气候和天气变量如何影响农业市场。对于一些作物和经营这些作物的公司来说,这些变量在一年的大部分时间里都相当不重要。然而,在作物生长和收获的时候,它们变得至关重要。更复杂的是,这些因素的重要性还与位置(和当地气候)有关。
建模的挑战显而易见。对于经过一次训练并在新数据上再次运行的模型,管理数据更改可能会带来严重的挑战。对于基于新输入数据动态重新计算的模型,随着变量分布和关系的变化以及可用变量在生成有效解决方案时变得或多或少有价值,数据变化仍然会产生问题。
在您的 ML 应用程序中,成功管理数据变更的部分关键是识别变更可能影响特性分布、关系和特性重要性的维度(也有共同的过失),模型将尝试了解这些维度。
一旦您了解了数据中哪些因素可能会影响过度拟合,您就可以更好地开发有效管理这些因素的解决方案。
尽管如此,构建一个能够解决任何潜在问题的单一模型仍将极具挑战性。对此的简单回答是,如果一个人面临严重的数据更改问题,解决方案可能不是试图用单一模型来解决它们!在下一节中,我们将研究集成方法来提供更好的答案。
识别建模风险因素
虽然在许多情况下,随着时间的推移,识别哪些元素会给模型带来风险是非常简单的,但是使用结构化的过程进行识别会有所帮助。本节简要描述了一些启发式方法和技术,您可以使用它们来筛选模型中的数据变更风险。
大多数数据科学家都有一个数据字典,用于一般用途或自动化应用的数据集。如果数据或应用程序很复杂,这种情况尤其可能发生,但是保存数据字典通常是一种很好的做法。在识别风险因素时,您可以做的一些最有效的工作是浏览这些功能,并根据不同的风险类型对它们进行标记。
我倾向于使用的一些标签包括:
- 纵向变量:该参数是否会由于纵向趋势而在很长一段时间内发生变化,而这些趋势在您现有的训练数据范围内并不完全可见?最明显的例子是生态季节,它影响着人类行为的许多领域,以及许多依赖于一些更基本的气候变量的事物。其他纵向趋势包括财政年度和工作月份,但扩展到包括与您的调查领域相关的许多其他纵向趋势。新 iPhone 机型的生命周期或田鼠的种群流动可能是一个重要的纵向因素,这取决于你的工作性质。
- 缓慢变化:随着时间的推移,这个分类参数有可能获得新的值吗?这个概念是从数据仓库最佳实践中借用来的。经典意义上缓慢变化的维度将获得新的参数代码(例如,当一家新店开业或一个新案例被识别时)。如果管理不当或者出现的数量足够多,这些可以完全抛弃你的模型。缓慢变化的数据的另一个影响是,它会开始影响你的特性的分布,这可能会更难处理。这可能会对模型的有效性产生重大影响。
- 关键参数:数据值监控和决策边界/回归方程重新计算的组合通常可以很好地处理一定数量的缓慢变化的数据和季节性方差,但是如果您看到意外大量的新案例或案例类型,特别是当它们影响您的模型严重依赖的变量时,请考虑采取行动。因此,也要确保你知道你的解决方案最依赖哪些变量!
以这种方式标记的过程是有帮助的(不仅仅是作为你自己记忆的输出),主要是因为它帮助你做以下事情:
- 组织你的期望,并为你监控准备的发展制定一个清单。如果您不能至少跟踪您的纵向变量和缓慢变化的参数变化,那么除了重新计算时支持的参数变化及其(可能缓慢下降的)性能度量之外,您实际上对模型的任何输出都是盲目的。
- 调查缓解措施(例如,改进的规范化或额外的参数,这些参数编码了数据变化的维度)。在许多方面,缓解和添加参数是处理数据更改的最佳解决方案。
- 使用构建的数据集设置稳健性测试,其中您的风险特征被故意改变以模拟数据变化。在这些条件下对你的模型进行压力测试,找出它到底能承受多大的方差。有了这些信息,您可以轻松地将自己设置为使用您的监控值作为早期警报系统;一旦数据变化超过某个安全阈值,您就知道模型性能会下降多少。
管理模型稳健性的策略
我们已经讨论了许多有效的集成技术,这些技术使我们能够平衡对高性能和健壮模型的双重需求。然而,在我们阐述和使用这些技术的过程中,我们必须决定如何以及何时降低模型的性能以提高健壮性。
事实上,本章的一个共同主题是如何平衡创建一个有效的、高性能的模型的冲突目标,同时又不会使这个模型过于不灵活而无法响应数据变化。到目前为止,我们看到的许多解决方案都要求我们权衡一种结果和另一种结果,这并不理想。
在这一点上,值得我们从更广的角度来看待我们的选择,并借鉴互补的技术。在不断发展的商业环境中,对稳健的、高性能的统计模型的需求既不是新的,也不是未得到处理的;信用风险建模等领域在不断变化的领域中应用统计建模的历史悠久,并且已经开发出有效的决策管理方法以取得成功。数据科学家可以通过使用这些既定技术来帮助组织我们自己的模型,从而将其中一些技术转化为我们自己的利益。
一种有效的方法是 冠军/挑战者,一种以测试为中心的方法,包括运行多个并行模型配置。除了其输出被应用的模型(用于指导业务活动或信息报告),冠军/挑战者方法培训一个或多个替代模型配置。
通过维护和监控多个模型,可以安排在替代模型性能超过当前模型时替换当前模型。这通常是通过维护所有模型的性能评分过程并观察结果来完成的,这样就可以手动决定是否以及何时切换到挑战者。
虽然最简单的实现可能涉及到在性能超过主模型时立即切换到挑战者,但这很少实现,因为特定挑战者模型存在暴露于局部最小值的风险(例如,一周中的某一天或一年中的某一月的局部趋势)。花费大量时间评估挑战者模型是正常的,尤其是在敏感应用程序之前。在复杂的真实案例中,人们甚至可能希望通过向有希望的挑战者提供治疗案例的样本来进行额外的测试,以确定它是否对冠军产生了显著的提升。
除了简单的“取代挑战者”继任规则,还有一些创新的空间。基于投票的方法非常常见,其中训练好的集合的顶级子集在个案的基础上提供分数,这些分数被视为(加权或未加权)投票。另一种方法是使用“T2”投票系统,即每个投票人按照偏好对候选方案进行排序。在集合的上下文中,人们通常会给每个单独模型的预测分配一个与其逆秩相等的点值(保持每个模型独立!).然后人们可以将这些投票组合起来(通常尝试一系列不同的权重)以产生一个结果。
投票可以在大量模型的情况下相当好地执行,但是取决于特定的建模环境和因素,例如不同投票者的相似性。正如我们在本章前面所讨论的,使用皮尔逊相关系数等测试来确保您的模型集既有性能又不相关是至关重要的。
人们可能发现特定类别的输入数据(例如,具有特定分段标签的用户)被给定的挑战者更有效地对待,并且可能实现一个案例路由系统,其中多个冠军处理不同的用户子组。这种方法与增强集成的好处有些重叠,但可以通过分离关注点来帮助生产环境。然而,维护多个冠军将增加您的数据团队的监控和监督负担,因此如果不是完全必要的话,最好避免这种选择。
需要解决的一个主要问题是我们如何对我们的模型进行评分,尤其是因为存在直接的实际挑战。特别是,考虑到类标签(用于指导正确性)通常不可用,很难在实际环境中比较多个模型。在预测环境中,这个问题由于冠军模型的预测通常用于采取改变预测事件的行动而变得更加复杂。这项活动使得很难断言挑战者模型的预测会有怎样的表现;根据冠军的预测采取行动,我们无法确认我们模型的结果!
最常见的实施过程是为每个挑战者模型提供一个统计上可行的输入数据样本,然后比较每种方法的升力。这种方法固有地限制了一些建模问题可以支持的挑战者的数量。另一个选择是从任何治疗活动中只留下一个统计上可行的样本,并使用它来创建一个单一的回归测试。这项测试适用于冠军和挑战者的整套模型,为比较提供了有意义的基础。
这种方法的缺点是,无论为测试用例生成正确的类标签需要多长时间,到一个更有效的模型的变化总是会跟踪数据的变化。虽然在许多情况下,这并不严重(冠军模型在生成精确模型所需的时间内保持不变),但在基础条件与模型的训练时间相比变化迅速的背景下,它可能会出现问题。
注
模型训练时间和数据变化频率之间的关系值得简单评论一下。它并不总是如此明确地陈述,但是应用机器学习环境中的典型目标是将训练时间与数据变化频率的因子减少到尽可能小的值。从最坏的情况来看,如果训练一个模型所需的时间长于该模型精确的时间长度(并且该比率等于或大于 1),那么您的模型将永远不会生成可以直接驱动当前动作的当前结果。一般而言,高比率应促进审查和调整活动(要么调查在较低置信度下更快的分数交付是否带来更多价值,要么调整可控环境变量的变化速度)。
这个比率变得越小,你的团队就有越多的余地来应用你的模型的输出来驱动行动和产生价值。根据这个比率在您的建模环境中的变化和可量化程度,它可以作为您的自动化建模解决方案的健康度量在您的组织中推广。
这些替代模型可能只是下一个性能最好的集合配置;他们可能是老型号,留在周围观察。在复杂的操作中,一些挑战者被配置为处理不同的假设场景(例如,如果该地区的温度比预期低 2°C 怎么办或如果销售额明显低于预期怎么办)。这些模型可能是在与主模型相同的数据上训练的,或者是在模拟假设情景的故意扭曲或准备好的数据上训练的。
更多的挑战者往往更好(提供改进的健壮性和性能),前提是挑战者不都是同一主题的微小变化。挑战者模型还为创新和测试提供了一个安全的场所,同时观察有效的挑战者可以提供有用的见解,了解您的冠军团队对一系列可能的环境变化有多强大。
您在本节中学习应用的技术为我们提供了工具,可以将我们现有的模型工具包应用到不断发展的环境中的实际应用中。本章还讨论了将 ML 模型应用于生产时可能出现的复杂情况;样本之间或跨维度的数据变化将导致我们的模型变得越来越无效。通过彻底解开数据变化的概念,我们变得能够更好地描述这种风险,并认识到它可能出现在哪里以及如何出现。
一章的剩余部分专门介绍了提高模型鲁棒性的技术。我们讨论了如何通过查看底层数据来识别模型降级风险,并讨论了一些有用的启发式方法。我们从现有的决策管理方法中学习和使用 Champion/Challenger,这是一个在包括应用机器学习在内的环境中有着悠久历史的备受关注的过程。冠军/挑战者帮助我们在良性竞争中组织和测试多个模型。结合有效的性能监控,模型替代的主动战术计划将为您提供更快、更可控的模型生命周期和质量管理,同时提供大量有价值的运营见解。
进一步阅读
也许最广泛和信息最丰富的合奏和合奏类型之旅是由卡格勒的竞争对手特里克里昂在 mlwave.com/kaggle-ense…
关于 Netflix 获奖模式《务实的混沌》的讨论,请参考http://www . stat . OSU . edu/~ dmsl/grand Prize 2009 _ BPC _ bellkor . pdf。关于网飞对不断变化的商业环境如何让这种 100 万美元的模式变得多余的解释,请参考网飞理工大学的博客。
关于将随机森林集合应用于商业环境的演练,为所有重要的诊断图表和推理提供了大量空间,请考虑 Arshavir Blackwell 的博客,网址为https://citizen net . com/blog/2012/11/10/random-forests-ensembles-and-performance-metrics/。
关于随机森林的更多信息,我发现 scikit-learn 文档很有帮助:http://sci kit-learn . org/stable/modules/generated/sklearn . ensemble . randomforestclaider . html。
在xgboost.readthedocs.io/en/latest/m…的 XGBoost 文档中提供了一个关于梯度增强树的很好的介绍。
有关 Alexander Guschin 参加 Otto 产品分类挑战赛的报道,请参考 No Free Hunch 博客:http://blog . kaggle . com/2015/06/09/Otto-产品-分类-获奖者-面试-第二名-alexander-guschin/ 。
Alexander Minushkin 的过拟合抖动测试描述在https://www . kaggle . com/miniushkin/introduction-kaggle-scripts/抖动-过拟合测试-笔记本中。
总结
在这一章中,我们涉及了很多方面。我们从引入集成开始,集成是竞争机器学习环境中一些最强大和最受欢迎的技术。我们结合专家知识和实际例子,介绍了将集成应用于机器学习项目所需的理论和代码。
此外,本章还专门用一节来讨论当您一次运行几周或几个月的模型时出现的独特注意事项。我们讨论了数据变化意味着什么,如何识别它,以及如何考虑防范它。我们特别考虑了如何创建并行运行的模型集的问题,您可以根据模型集中的季节变化或性能漂移在这些模型集之间进行切换。
在我们回顾这些技术的过程中,我们花了大量时间研究现实世界中的例子,具体目的是了解最佳数据科学家所需的创造性思维和广泛的知识。
这本书中的技术已经达到了这样一个程度,有了技术知识、可以重新应用的代码和对可能性的理解,你真的能够接受任何数据建模挑战。**
九、其他 Python 机器学习工具
在前面八章的过程中,我们研究并应用了一系列技术,这些技术帮助我们丰富和建模许多应用程序的数据。
我们使用 Python 库的组合来处理这些章节中的内容,特别是 NumPy 和 antao,而其他库是在我们需要访问特定算法时使用的。我们没有花太多时间讨论工具方面还存在哪些选项,这些工具的独特优势是什么,或者我们为什么会感兴趣。
这最后一章的主要目标是突出一些您可以使用的其他关键库和框架。这些工具简化了创建和应用模型的过程。本章介绍了这些工具,演示了它们的应用,并提供了大量关于进一步阅读的建议。
成功解决数据科学挑战和成功成为数据科学家的一个主要贡献者是对算法和库的最新发展有很好的理解。作为专业人士,数据科学家往往高度依赖他们使用的数据质量,但拥有最佳可用工具也非常重要。
在本章中,我们将回顾数据科学家最近可用的一些最佳工具,确定它们提供的好处,并讨论如何在一致的工作过程中,将它们与本书前面讨论的工具和技术一起应用。
替代发展工具
在过去的几年里,出现了许多新的机器学习框架,它们在工作流方面具有优势。通常,这些框架高度关注特定的用例或目标。这使得它们非常有用,甚至可能是必备的工具,但这也意味着您可能需要使用多个工作流改进库。
随着越来越多的新 Python ML 项目被点亮以解决特定的工作流挑战,值得讨论两个库,它们增加了我们现有的工作流,并加速或改进了我们在前面几章中所做的工作。在本章中,我们将介绍千层面和 TensorFlow ,讨论每个库的代码和功能,并确定为什么每个框架都值得作为工具集的一部分来考虑。
千层面介绍
让我们面对它;有时候用 Python 创建模型的时间比我们希望的要长。然而,它们对于更复杂的模型来说可能是有效的,并提供了很大的好处(例如图形处理器加速和可配置性)。当处理简单的情况时,类似于 Anano 的库可能相对复杂。这是不幸的,因为我们经常希望使用简单的模型,例如,当我们建立基准时。
千层面是一个由深度学习和音乐数据挖掘研究人员团队开发的图书馆,作为 Anano 的界面。它是专门为确定一个特定的目标而设计的——允许快速有效地建立新模型的原型。
这个焦点决定了如何创建 Lasagne,以一种比用本机代码编写的相同操作简单得多且更容易理解的方式调用函数并返回表达式或数据类型。
在这一节中,我们将看一下 Lasagne 底层的概念模型,应用一些 Lasagne 代码,并了解该库为我们现有的实践添加了什么。
了解千层面
千层面使用层的概念进行操作,这是机器学习中常见的概念。层是一组神经元和操作规则,它们接受输入并生成分数、标签或其他转换。神经网络通常充当一组层,在一端输入数据,在另一端输出值(尽管实现方式各不相同)。
在深度学习环境中,开始将单个层视为一等公民已经变得非常流行。传统上,在机器学习工作中,网络将仅使用几个参数规格(如节点数、偏差和权重值)从层建立。
近年来,寻求额外优势的科学家们开始对单个层的配置越来越感兴趣。如今,在高级机器学习环境中,看到包含子模型和转换输入的层并不罕见。如今,即使是要素也可能根据需要跳过图层,新要素可能会在模型的中途添加到图层中。作为这种改进的一个例子,考虑谷歌用来解决图像识别挑战的卷积神经网络架构。这些网络在层级别进行了广泛的改进,以提高性能。
因此,千层面将图层视为其基本模型组件是有意义的。千层面为模型创建过程增加的是快速直观地将不同层堆叠到模型中的能力。你可以简单地在lasagne.layers内调用一个类,将一个类堆叠到你的模型上。这方面的代码非常高效,如下所示:
l0 = lasagne.layers.InputLayer(shape=X.shape)
l1 = lasagne.layers.DenseLayer(
l0, num_units=10, nonlinearity=lasagne.nonlinearities.tanh)
l2 = lasagne.layers.DenseLayer(l1, num_units=N_CLASSES, nonlinearity=lasagne.nonlinearities.softmax)
在三个简单的语句中,我们使用简单且可配置的函数创建了网络的基本结构。
这段代码使用三层创建一个模型。层l0调用InputLayer类,作为我们模型的输入层。该图层基于输入的预期形状(使用shape参数定义),将我们的输入数据集转换为张量。
接下来的层l1和l2都是完全连接(密集)的层。图层l2定义为输出图层,单位数等于类数,而l1使用相同的DenseLayer类创建10单位的隐藏图层。
除了配置DenseLayer类可用的标准参数(权重、偏差、单位计数和非线性类型)之外,还可以使用使用不同类别的完全不同的网络类型。千层面为一系列常见的层提供分类,包括密集层、卷积层和汇集层、循环层、归一化层和噪声层等。此外,还有一个特殊用途的层类,它提供了一系列附加功能。
当然,如果需要比这些类提供的更多的定制,用户可以很容易地定义自己的图层类型,并将其与其他千层面类结合使用。然而,对于大多数原型开发和快速迭代开发环境来说,这是大量的预先准备的功能。
千层面提供了类似简洁的界面来定义网络的损失计算:
true_output = T.ivector('true_output')
objective = lasagne.objectives.Objective(l2, loss_function=lasagne.objectives.categorical_crossentropy)
loss = objective.get_loss(target=true_output)
这里定义的loss函数是许多可用函数中的一个,包括平方误差、二进制和多类情况下的铰链损失以及crossentropy函数。还提供了用于验证的准确性评分功能。
有了这两个组件,一个loss功能和一个网络架构,我们再次拥有了训练网络所需的一切。为此,我们需要编写更多的代码:
all_params = lasagne.layers.get_all_params(l2)
updates = lasagne.updates.sgd(loss, all_params, learning_rate=1)
train = theano.function([l0.input_var, true_output], loss, updates=updates)
get_output = theano.function([l0.input_var], net_output)
for n in xrange(100):
train(X, y)
这段代码利用theano功能来训练我们的示例网络,使用我们的loss功能来迭代训练给定的一组输入数据。
张量流简介
当我们回顾谷歌在第四章卷积神经网络中对 卷积神经网络 ( CNN )的看法时,我们发现了一个令人费解的多层野兽。如何创建和监控此类网络的问题变得越来越重要,因为网络在层数和复杂性上不断扩展,以应对更复杂的挑战。
为了应对这一挑战,谷歌的机器智能研究组织开发并分发了一个名为 TensorFlow 的库,该库的存在是为了能够更容易地细化和建模非常复杂的机器学习模型。
TensorFlow 通过提供两个主要好处来做到这一点;一个清晰简单的编程接口(在本例中是一个 Python API)到熟悉的结构上(例如 NumPy 对象),以及强大的诊断和图形可视化工具,例如 TensorBoard ,以实现对数据架构的明智调整。
了解张量流
TensorFlow 使数据科学家能够将数据转换操作设计为跨计算图的流。该图可以扩展和修改,同时可以广泛调整单个节点,从而实现对单个层或模型组件的详细细化。TensorFlow 工作流通常包括两个阶段。其中的第一个阶段被称为构建阶段,在此期间组装图表。
在构建阶段,我们可以使用针对 Tensorflow 的 Python API 编写代码。像 Lasagne 一样,TensorFlow 提供了一个相对简单的界面来编写网络层,只需要我们在创建层之前指定权重和偏差。以下示例显示了在创建(使用一行代码)卷积层和简单的最大池层之前,权重和偏差变量的初始设置。此外,我们使用tf.placeholder为输入数据生成占位符变量。
x = tf.placeholder(tf.float32, shape=[None, 784])
y_ = tf.placeholder(tf.float32, shape=[None, 10])
W = tf.Variable(tf.zeros([5, 5, 1, 32]))
b = tf.Variable(tf.zeros([32]))
h_conv = tf.nn.relu(conv2d(x_image, W) + b)
h_pool = max_pool_2x2(h_conv)
这个结构可以扩展到包括一个softmax输出层,就像我们对千层面所做的那样。
W_out = tf.Variable(tf.zeros([1024,10]))
B_out = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(h_conv, W_out) + b_out)
同样,我们可以看到在迭代时间上比直接在 antao 和 Python 库中编写有显著的改进。由于是用 C++编写的,TensorFlow 还提供了优于 Python 的性能提升,在执行时间上具有优势。
接下来,我们需要训练和评估我们的模型。在这里,我们需要写一点代码来定义我们用于训练的loss函数(在这种情况下是交叉熵),用于验证的accuracy函数和优化方法(在这种情况下是最陡梯度下降)。
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
接下来,我们可以简单地开始迭代运行我们的模型。这一切简洁明了:
sess.run(tf.initialize_all_variables())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i%100 == 0:
train_accuracy = accuracy.eval(feed_dict={
x:batch[0], y_: batch[1], keep_prob: 1.0})
print("step %d, training accuracy %g"%(i, train_accuracy))
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
print("test accuracy %g"%accuracy.eval(feed_dict={
x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
使用张量流迭代改进我们的模型
甚至从前面部分的单个例子中,我们应该能够认识到张量流给表带来了什么。它为开发复杂的体系结构和训练方法的任务提供了一个简单的界面,使我们更容易访问我们在本书前面学习过的算法。
然而,正如我们所知,开发初始模型只是模型开发过程的一小部分。我们通常需要反复测试和剖析我们的模型,以提高它们的性能。然而,这往往是一个领域,我们的工具在单一的库或技术中不太统一,测试和监控解决方案在不同的模型中不太一致。
TensorFlow 致力于解决如何在迭代过程中更好地洞察我们的模型的问题,即所谓的模型开发的执行阶段。在执行阶段,我们可以利用 TensorFlow 团队提供的工具来探索和改进我们的模型。
也许这些工具中最重要的是 TensorBoard,它为我们构建的模型提供了一个可探索的可视化表示。TensorBoard 提供了几种功能,包括显示基本模型信息(包括测试和/或培训的每次迭代期间的性能测量)的仪表板。
此外,TensorBoard 仪表板提供了较低层次的信息,包括每个模型层的权重、偏差和激活值的值范围图;迭代过程中非常有用的诊断信息。访问这些数据的过程非常简单,而且非常有用。
除此之外,张量积提供了给定模型的张量流的详细图形。张量是一个 n 维的数据数组(在这种情况下,由 n 个特征组成);当我们使用术语输入数据集时,我们往往会想到这一点。应用于张量的一系列运算被称为张量流,在张量流中,这是一个基本概念,原因很简单,也很有说服力。当细化和调试机器学习模型时,重要的是即使在很低的级别上也要有关于模型及其操作的信息。
TensorBoard 图以可变的细节显示了模型的结构。从这个初始视图中,可以深入到模型的每个组件和连续的子元素中。在这种情况下,我们能够查看在第二个网络层的丢弃功能中发生的具体操作。我们可以看到发生了什么,并确定下一次迭代需要调整什么。
这种级别的透明度是不寻常的,当我们想要调整模型组件时,尤其是当模型元素或层表现不佳时(例如,从显示层元参数值的 TensorBoard 图或从整体网络性能来看),这种透明度非常有帮助。
可以从事件日志中创建 TensorBoards,并在运行 TensorFlow 时生成。这使得在使用 TensorFlow 的日常开发过程中,很容易获得 TensorBoards 的好处。
截至 2016 年 4 月下旬,DeepMind 团队加入了谷歌大脑团队和其他一系列研究人员和开发人员使用 TensorFlow。通过让 TensorFlow 开源并免费提供,谷歌承诺继续支持 TensorFlow,将其作为模型开发和完善的强大工具。
知道何时使用这些库
在这一章的一两点,我们可能遇到了*的问题好吧,那么,你为什么不一开始就教我们这个库呢?*当本章呈现出让生活变得更轻松的完美界面时,公平地问一下我们为什么要花时间在蚂蚁函数和其他低级信息中挖掘。
自然,我主张使用现有的最佳工具,尤其是对于原型任务,在这些任务中,工作的价值更多的是理解你所处的大致范围,或者识别特定的问题类别。值得一提的是,在本书的前面,没有使用这两个库展示内容的三个原因。
第一个原因是这些工具只能让你走这么远。他们可以做很多事情,同意,所以根据领域和该领域的问题的性质,一些数据科学家可能能够依靠他们来满足大多数深度学习需求。当然,除了一定程度的性能和问题复杂性之外,您还需要了解在 antio 中构建模型需要什么,从头开始创建自己的评分函数,或者利用本书中描述的其他技术。
决定关注教学底层实现的另一部分是关于相关技术的发展成熟度。在这一点上,千层面和 TensorFlow 绝对值得大家讨论和推荐。在此之前,当这本书的大部分内容被写出来时,在本章中讨论图书馆的风险更大。基于 antio 的项目很多(本章没有讨论的一些更突出的框架有 Keras 、 Blocks 和 T7】派尔恩 2
即使是现在,不同的库和工具完全有可能在一两年后成为讨论的主题或默认的工作环境。这一领域的发展速度极快,这主要是由于关键公司和研究团体的影响,他们不得不随着旧工具达到其有用的极限而不断构建新工具……或者只是变得清楚如何做得更好。
老实说,从更低的层面挖掘的另一个原因是,这是一本复杂的书。它把理论和代码放在一起,用代码来教授理论。抽象出算法是如何工作的,并简单地讨论如何应用它们来破解一个特定的例子可能很有诱惑力。本章中讨论的工具使实践者能够在一些问题上获得非常好的分数,而不必理解被调用的函数。我的观点是,这不是一个很好的培养数据科学家的方法。
如果你要处理微妙而困难的数据问题,你需要能够修改和定义自己的算法。你需要了解如何选择合适的解决方案。要做这些事情,你需要本书提供的细节,甚至更具体的信息,由于(页面)空间和时间的限制,我没有提供。在这一点上,你可以灵活而有知识地应用深度学习算法。
同样,认识到这些工具做得好或不好也很重要。目前,Lasagne 非常适合这个用例,在这个用例中,一个新的模型正在被开发用于基准测试或者早期通过,优先考虑的应该是迭代速度和获得结果。
与此同时,TensorFlow 适合模型的后期开发寿命。当轻松的收益消失,需要花费大量时间调试和改进模型时,TensorFlow 相对快速的迭代是一个明显的优势,但 TensorBoard 提供的诊断工具带来了巨大的附加值。
因此,这两个库在您的工具集中都有一席之地。根据手头问题的性质,这些库和更多库将被证明是有价值的资产。
进一步阅读
千层面用户指南非常全面,值得一读。在lasagne.readthedocs.io/en/latest/i…找到。
同样,可以在https://www . TensorFlow . org/versions/r 0 . 9/get _ start/index . html找到 TensorFlow 教程。
总结
在这最后一章中,我们离开了之前关于算法、配置和诊断的讨论,转而考虑在实现深度学习算法时改善我们体验的工具。
我们发现了使用千层面的优势,千层面是一个与人工智能的接口,旨在加速和简化我们模型的早期原型。同时,我们检查了 TensorFlow,这是谷歌开发的帮助深度学习模型调整和优化的库。TensorFlow 以最少的努力为我们提供了模型性能的显著可见性,并使诊断和调试复杂的深层模型结构的任务不那么具有挑战性。
这两种工具在我们的过程中都有自己的位置,每一种都适合于一组特定的问题。
在这本书的整个过程中,我们已经浏览并回顾了一系列先进的机器学习技术。我们从一个理解一些基本算法和概念的位置,到自信地使用一个非常流行、强大和受欢迎的工具集。
然而,除了技术之外,这本书试图教一个更进一步的概念,一个更难教和更难学的概念,但它支撑了机器学习的最佳表现。
机器学习领域发展非常迅速。这种速度在几乎每周发布在学术期刊或行业白皮书上的新的和改进的分数中显而易见。从 MNIST 这样的训练例子如何从被视为有意义的挑战迅速转变为 Iris 数据集的深度学习版本玩具问题就可以看出这一点。与此同时,该领域进入下一个重大挑战;CIFAR-10,CIFAR-100。
同时,磁场循环运动。像 Yann LeCun 这样的学者在 80 年代提出的概念正在复兴,因为计算架构和资源的增长使得它们的使用比大规模的真实数据更可行。为了最大限度地使用许多最新的技术,有必要理解几十年前定义的概念,这些概念本身是在更久以前定义的其他概念的基础上定义的。
这本书试图平衡这些问题。了解前沿和现有技术至关重要;理解将定义新技术的概念或两三年后做出的调整同样重要。
然而,最重要的是,这本书让你了解了这些架构和方法的延展性。在数据科学实践的顶端一直看到的一个概念是,特定问题的最佳解决方案是特定问题的解决方案。
这就是为什么顶级的卡格尔竞赛获胜者会进行大量的特性准备并调整他们的架构。这就是为什么 TensorFlow 被写为允许清晰地看到一个人的架构的粒度属性。拥有熟练调整实现或组合算法的知识和技能是真正掌握机器学习技术的必要条件。
通过本书中回顾的许多技术和例子,我希望关于数据问题的思考方式以及对操作和配置这些算法的信心已经传递给了作为一名实践数据科学家的你。本书中许多推荐的进一步阅读的例子很大程度上是为了进一步扩展这些知识,并帮助你发展本书中教授的技能。
除此之外,我祝你在模型构建和配置方面一切顺利。我希望你能亲身体会到这个领域是多么令人愉快和值得!