原文:
medium.com/@hiromi_suenaga/machine-learning-1-lesson-11-7564c3c18bbb
译者:飞龙
协议:CC BY-NC-SA 4.0
来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和 Rachel 给了我这个学习的机会。
这个想法是我们有一些数据(x),然后我们对这些数据做一些操作,例如,我们用一个权重矩阵乘以它(f(x))。然后我们对这个结果做一些操作,例如,我们通过 softmax 或 sigmoid 函数处理它(g(f(x)))。然后我们对这个结果做一些操作,比如计算交叉熵损失或均方根误差损失(h(g(f(x))))。这将给我们一些标量。这里没有隐藏层。这有一个线性层,一个非线性激活函数是 softmax,一个损失函数是均方根误差或交叉熵。然后我们有我们的输入数据。
例如[1:16],如果非线性激活函数是 sigmoid 或 softmax,损失函数是交叉熵,那就是逻辑回归。那么我们如何计算对权重的导数?
为了做到这一点,基本上我们使用链式法则:
因此,为了对权重求导,我们只需使用那个确切的公式计算对 w 的导数[3:29]。然后如果我们在这里进一步,有另一个带有权重 w2 的线性层,现在计算对所有参数的导数没有区别。我们仍然可以使用完全相同的链式法则。
所以不要把多层网络想象成在不同时间发生的事情。它只是函数的组合。所以我们只需使用链式法则一次计算所有导数。它们只是在函数的不同部分出现的一组参数,但微积分并没有不同。所以计算对 w1 和 w2 的导数,你现在可以称之为 w,说 w1 就是所有这些权重。
那么你将会得到一个参数列表[5:26]。这里是 w1,可能是某种高阶张量。如果是卷积层,它将是一个三阶张量,但我们可以展开它。我们将把它变成一个参数列表。这里是 w2。这只是另一个参数列表。这是我们的损失,是一个单一的数字。因此,我们的导数就是同样长度的向量。改变 w 的值会对损失产生多大影响?你可以把它想象成一个函数,比如y = ax1 + bx2 + c,然后问对 a、b 和 c 的导数是多少?你会得到三个数字:对 a、b 和 c 的导数。就是这样。如果对那个权重求导,那个权重,…
为了到达这一点,在链式法则中,我们必须计算像雅可比这样的导数,当你进行矩阵乘法时,你现在得到的是一个权重矩阵和输入向量,这些是来自前一层的激活,还有一些新的输出激活。所以现在你必须说对于这个特定的权重,改变这个特定的权重如何改变这个特定的输出?改变这个特定的权重如何改变这个特定的输出?等等。所以你最终会得到这些更高维度的张量,显示每个权重如何影响每个输出。然后当你到达损失函数时,损失函数将有一个均值或和,所以它们最终会被加起来。
尝试手动计算或逐步考虑这一点让我有点疯狂,因为你倾向于像…你只需要记住,对于每个权重对于每个输出,你都必须有一个单独的梯度。
一个很好的方法是学习使用 PyTorch 的.grad
属性和.backward
方法,并手动查阅 PyTorch 教程。这样你就可以开始设置一些具有向量输入和向量输出的计算,然后输入.backward
,然后输入grad
并查看它。然后对只有 2 或 3 个项目的输入和输出向量进行一些非常小的操作,比如加 2 或其他操作,看看形状是什么,确保它是有意义的。因为向量矩阵微积分在严格意义上并没有为你在高中学到的任何概念引入新的概念。但是对这些形状如何移动有了一定的感觉需要大量的练习。好消息是,你几乎永远不必担心这个。
笔记本 / Excel
我们正在讨论使用这种逻辑回归进行 NLP。在达到这一点之前,我们正在讨论使用朴素贝叶斯进行 NLP。基本思想是我们可以取一个文档(例如电影评论),并将其转换为一个词袋表示,其中包含每个单词出现的次数。我们称单词的唯一列表为词汇表。我们使用 sklearn 的 CountVectorizer 自动生成词汇表,他们称之为“特征”,并创建词袋表示,所有这些袋表示的整体称为术语文档矩阵。
我们有点意识到,我们可以通过简单地平均积极评论中单词“this”出现的次数来计算积极评论包含单词“this”的概率,我们可以对消极评论做同样的事情,然后我们可以取它们的比率,得到一个结果,如果大于一,则表示该单词在积极评论中出现得更频繁,如果小于一,则表示该单词在消极评论中出现得更频繁。
然后我们意识到,使用贝叶斯规则并取对数,我们基本上可以得到一个结果,我们可以将这些(下面突出显示的)的对数加起来,再加上类别 1 与类别 0 的概率比的对数,最终得到一个可以与零进行比较的结果[11:32]。如果结果大于零,我们可以预测文档是积极的,如果结果小于零,我们可以预测文档是消极的。这就是我们的贝叶斯规则。
我们从数学的第一原理开始做了这个,我认为我们都同意“朴素”在朴素贝叶斯中是一个很好的描述,因为它假设了独立性,而这显然是不正确的。但这是一个有趣的起点,当我们实际上到达这一点时,我们计算了概率的比率并取了对数,现在不是将它们相乘,当然,我们必须将它们相加。当我们实际写下这个时,我们意识到哦,这只是一个标准的权重矩阵乘积加上一个偏差:
然后我们意识到,如果这不是很好的准确率(80%),为什么不通过说,嘿,我们知道其他计算一堆系数和一堆偏差的方法,即在逻辑回归中学习它们来改进呢?换句话说,这是我们用于逻辑回归的公式,那么为什么我们不只是创建一个逻辑回归并拟合它呢?它会给我们同样的结果,但与基于独立性假设和贝叶斯规则的理论上正确的系数和偏差不同,它们将是实际上在这些数据中最好的系数和偏差。这就是我们的结论。
这里的关键见解是,几乎所有的机器学习最终都变成了一棵树或一堆矩阵乘积和非线性[13:54]。一切似乎最终都归结为相同的事情,包括贝叶斯规则。然后事实证明,无论该函数中的参数是什么,它们都比基于理论计算更好地学习。事实上,当我们实际尝试学习这些系数时,我们得到了 85%的准确率。
然后我们注意到,与其采用整个术语文档矩阵,我们可以只取单词存在或不存在的一和零。有时这样做同样有效,但后来我们实际尝试了另一种方法,即添加正则化。通过正则化,二值化方法结果略好一些。
然后正则化是我们取损失函数,再次,让我们从 RMSE 开始,然后我们将讨论交叉熵。损失函数是我们的预测减去我们的实际值,将其相加,取平均再加上一个惩罚。
具体来说,这是 L2 惩罚。如果这是 w 的绝对值,那就是 L1 惩罚。我们还注意到,我们实际上并不关心损失函数本身,我们只关心它的导数,这实际上是更新权重的东西,因此因为这是一个总和,我们可以分别对每个部分求导,所以惩罚的导数只是 2aw。因此,我们了解到,尽管这些在数学上是等价的,但它们有不同的名称。这个版本(2aw)被称为权重衰减,这个术语在神经网络文献中使用。
Excel
另一方面,交叉熵只是另一个损失函数,就像均方根误差一样,但它专门设计用于分类。这是一个二元交叉熵的例子。假设这是我们的“是猫还是狗?”所以说isCat
是 1 还是 0。而Preds
是我们的预测,这是我们神经网络的最终层的输出,一个逻辑回归等等。
然后我们所做的就是说好吧,让我们取实际值乘以预测的对数,然后加上 1 减去实际值乘以 1 减去预测的对数,然后取整个东西的负值。
我建议你们尝试写出这个 if 语句版本,希望你们现在已经做到了,否则我将为你们揭示。所以这是:
我们如何将这写成一个 if 语句?
if y == 1:
return -log(ŷ)
else:
return -log(1-ŷ)
所以关键的洞察是 y 有两种可能性:1 或 0。所以很多时候数学会隐藏关键的洞察,我认为这里发生了,直到你真正思考它可以取什么值。所以这就是它所说的。要么给我:-log(ŷ)
,要么给我:-log(1-ŷ)
好的,那么多类别版本就是同样的事情,但你说的不仅仅是 y == 1
,而是 y == 0, 1, 2, 3, 4, 5 . . .
,例如 [19:26]。所以这个损失函数有一个特别简单的导数,另外一个你可以在家里尝试的东西是想一想在它之前加上一个 sigmoid 或 softmax 之后导数是什么样子。结果会是非常好的导数。
人们使用均方根误差用于回归和交叉熵用于分类的原因有很多,但大部分都可以追溯到最佳线性无偏估计的统计概念,基于可能性函数的结果表明这些函数具有一些良好的统计性质。然而,实际上,尤其是均方根误差的性质可能更多是理论上的而不是实际的,实际上,现在使用绝对偏差而不是平方偏差的和通常效果更好。所以在实践中,机器学习中的一切,我通常都会尝试两种。对于特定的数据集,我会尝试两种损失函数,看哪一个效果更好。当然,如果是 Kaggle 竞赛的话,那么你会被告知 Kaggle 将如何评判,你应该使用与 Kaggle 评估指标相同的损失函数。
所以这真的是关键的洞察 [21:16]。让我们不要使用理论,而是从数据中学习。我们希望我们会得到更好的结果。特别是在正则化方面,我们确实得到了。然后我认为这里的关键正则化洞察是让我们不要试图减少模型中的参数数量,而是使用大量的参数,然后使用正则化来找出哪些实际上是有用的。
笔记本
然后我们进一步说,鉴于我们可以通过正则化做到这一点,让我们通过添加二元组和三元组来创建更多。例如 by vast
、by vengeance
这样的二元组,以及 by vengeance .
、by vera miles
这样的三元组。为了让事情运行得更快一些,我们将其限制为 800,000 个特征,但即使使用完整的 70 百万个特征,它的效果也一样好,而且速度并没有慢多少。
veczr = CountVectorizer(
ngram_range=(1,3),
tokenizer=tokenize,
max_features=800000
)
trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)
trn_term_doc.shape
'''
(25000, 800000)
'''
vocab = veczr.get_feature_names()
vocab[200000:200005]
'''
['by vast', 'by vengeance', 'by vengeance .', 'by vera', 'by vera miles']
'''
所以我们使用了完整的 n-grams 集合为训练集和验证集创建了一个术语文档矩阵。现在我们可以继续说我们的标签是训练集标签如前所述,我们的自变量是二值化的术语文档矩阵如前所述:
y=trn_y
x=trn_term_doc.sign()
val_x = val_term_doc.sign()
p = x[y==1].sum(0)+1
q = x[y==0].sum(0)+1
r = np.log((p/p.sum())/(q/q.sum()))
b = np.log(len(p)/len(q))
然后让我们对其进行逻辑回归拟合,并进行一些预测,我们得到了 90% 的准确率:
m = LogisticRegression(C=0.1, dual=True)
m.fit(x, y);
preds = m.predict(val_x)
(preds.T==val_y).mean()
'''
0.90500000000000003
'''
所以看起来很不错。
让我们回到我们的朴素贝叶斯。在我们的朴素贝叶斯中,我们有这个术语文档矩阵,然后对于每个特征,我们正在计算如果它是类别 1 出现的概率,如果它是类别 0 出现的概率,以及这两者的比率。
在我们实际基于的论文中,他们将 p(f|1)
称为 p,将 p(f|0)
称为 q,将比率称为 r。
然后我们说让我们不要把这些比率作为矩阵乘法中的系数。而是,尝试学习一些系数。也许开始用一些随机数,然后尝试使用随机梯度下降找到稍微更好的系数。
所以你会注意到这里一些重要的特征。r向量是一个秩为 1 的向量,其长度等于特征的数量。当然,我们的逻辑回归系数矩阵也是秩为 1 且长度等于特征数量的。我们说它们是计算相同类型的东西的两种方式:一种基于理论,一种基于数据。所以这里是r中的一些数字:
r.shape, r
'''
((1, 800000),
matrix([[-0.05468, -0.161 , -0.24784, ..., 1.09861, -0.69315, -0.69315]]))
'''
记住它使用对数,所以这些小于零的数字代表更有可能是负数的东西,而大于零的数字可能是正数。所以这里是 e 的幂次方。所以这些是我们可以与 1 而不是 0 进行比较的数字:
np.exp(r)
'''
matrix([[ 0.94678, 0.85129, 0.78049, ..., 3\. , 0.5 , 0.5 ]])\
'''
我要做一些希望看起来很奇怪的事情。首先,我会说我们要做什么,然后我会尝试描述为什么这很奇怪,然后我们会讨论为什么它可能并不像我们最初想的那么奇怪。所以这就是我们要做的事情。我们将取我们的术语文档矩阵,然后将其乘以r。这意味着,我可以在 Excel 中做到这一点,我们将说让我们抓取我们的术语文档矩阵中的所有内容,并将其乘以向量r中的等值。所以这就像是一个广播的逐元素乘法,而不是矩阵乘法。
所以这是术语文档矩阵乘以r的值,换句话说,在术语文档矩阵中出现零的地方,在乘以版本中也出现零。而在术语文档矩阵中每次出现一个的地方,等效的r值出现在底部。所以我们并没有真正改变太多。我们只是将一个变成了其他东西,即来自该特征的r。所以我们现在要做的是,我们将使用这些独立变量,而不是在我们的逻辑回归中。
所以在这里。x_nb
(x 朴素贝叶斯版本)是x
乘以r
。现在让我们使用这些独立变量进行逻辑回归拟合。然后对验证集进行预测,结果我们得到了一个更好的数字:
x_nb = x.multiply(r)
m = LogisticRegression(dual=True, C=0.1)
m.fit(x_nb, y);
val_x_nb = val_x.multiply(r)
preds = m.predict(val_x_nb)
(preds.T==val_y).mean()
'''
0.91768000000000005
'''
让我解释为什么这可能会令人惊讶。这是我们的独立变量(下面突出显示),然后逻辑回归得出了一些系数集(假设这些是它恰好得出的系数)。
现在我们可以说,好吧,让我们不使用这组独立变量(x_nb
),而是使用原始的二值化特征矩阵。然后将所有系数除以r中的值,数学上我们将得到完全相同的结果。
所以我们有了我们的独立变量的 x 朴素贝叶斯版本(x_nb
),以及一组权重/系数(w1
),它发现这是一个用于进行预测的好系数集。但是 x_nb 简单地等于x
乘以(逐元素)r
。
换句话说,这(xnb·w1
)等于x*r·w1
。所以我们可以只改变权重为r·w1
,得到相同的数字。这应该意味着我们对独立变量所做的更改不应该有任何影响,因为我们可以在不进行这种改变的情况下计算出完全相同的结果。所以问题就在这里。为什么会有所不同呢?为了回答这个问题,你需要考虑哪些数学上不同的事情。为什么它们不完全相同?提出一些假设,也许我们实际上得到了更好的答案的一些原因。要弄清楚这一点,首先我们需要弄清楚为什么会有不同的答案?这是微妙的。
讨论
它们受到正则化的影响是不同的。我们的损失等于基于预测和实际值的交叉熵损失加上我们的惩罚:
所以如果你的权重很大,那么惩罚(aw²
)就会变大,而且会淹没掉交叉熵部分(x.e.(xw, y)
)。但那实际上是我们关心的部分。我们实际上希望它是一个很好的拟合。所以我们希望尽可能少地进行正则化。所以我们希望权重更小(我有点把“更少”和“更小”这两个词用得有点等同,这不太公平,我同意,但想法是接近零的权重实际上是不存在的)。
这就是问题所在。我们的r值,我不是一个贝叶斯怪胎,但我仍然要使用“先验”这个词。它们有点像一个先验 - 我们认为不同级别的重要性和这些不同特征的积极或消极可能是这样的。我们认为“坏”可能与负面更相关,而不是“好”。所以我们之前的隐含假设是我们没有先验,换句话说,当我们说平方权重(w²)时,我们是在说非零权重是我们不想要的。但实际上我想说的是,与朴素贝叶斯的期望不同是我不想做的事情。除非你有充分的理由相信其他情况,否则只有与朴素贝叶斯先验有所不同。
这实际上就是这样做的。我们最终说我们认为这个值可能是 3。所以如果你要把它变得更大或更小,那将会导致权重的变化,从而使平方项增加。所以如果可以的话,就让所有这些值保持与现在大致相似。这就是现在惩罚项正在做的事情。当我们的输入已经乘以r时,它在说惩罚那些与我们的朴素贝叶斯先验有所不同的事物。
问题:为什么只与 r 相乘,而不是像 r²这样,这次方差会高得多呢?因为我们的先验来自一个实际的理论模型。所以我说我不喜欢依赖理论,但如果我有一些理论,那么也许我们应该将其作为我们的起点,而不是假设一切都是相等的。所以我们的先验说,嘿,我们有这个叫做朴素贝叶斯的模型,朴素贝叶斯模型说,如果朴素贝叶斯的假设是正确的,那么r就是这个特定公式中的正确系数。这就是我们选择它的原因,因为我们的先验是基于那个理论的。
这是一个非常有趣的见解,我从未真正看到过。这个想法是我们可以使用这些传统的机器学习技术,通过将我们的理论期望纳入我们给模型的数据中,赋予它们这种贝叶斯感。当我们这样做时,这意味着我们就不必那么经常进行正则化了。这很好,因为我们经常进行正则化…让我们试试吧!
记住,在 sklearn 逻辑回归中,C
是正则化惩罚的倒数。所以我们通过使其变小(1e-5
)来增加大量的正则化。
这真的会影响我们的准确性,因为现在它非常努力地降低这些权重,损失函数被需要减少权重的需求所压倒。而现在使其具有预测性似乎完全不重要。所以通过开始并说不要推动权重降低,以至于最终忽略这些项,而是推动它们降低,以便尝试消除那些忽略了我们基于朴素贝叶斯公式期望的差异的权重。这最终给我们带来了一个非常好的结果
论文
这种技术最初是在 2012 年提出的。Chris Manning 是斯坦福大学出色的自然语言处理研究人员,而 Sida Wang 我不认识,但我认为他很棒,因为他的论文很棒。他们基本上提出了这个想法。他们所做的是将其与其他方法在其他数据集上进行比较。其中一件事是他们尝试了 IMDB 数据集。这里是大二元组上的朴素贝叶斯 SVM:
正如你所看到的,这种方法胜过了他们研究的其他基于线性的方法,以及他们研究的一些受限玻尔兹曼机的神经网络方法。如今,有更好的方法来做这个,事实上在深度学习课程中,我们展示了我们在 Fast AI 刚刚开发的最新成果,可以达到 94%以上的准确率。但是特别是对于一种简单、快速、直观的线性技术来说,这还是相当不错的。你会注意到,当他们这样做时,他们只使用了二元组。我猜这是因为我看了他们的代码,发现它相当慢且难看。我找到了一种更优化的方法,正如你所看到的,所以我们能够使用三元组,因此我们得到了更好的结果,我们的准确率是 91.8%,而不是 91.2%,但除此之外,它是相同的。哦,他们还使用了支持向量机,在这种情况下几乎与逻辑回归相同,所以有一些细微的差异。所以我认为这是一个相当酷的结果。
我要提一下,在课堂上你看到的是我经过多周甚至多个月的研究得出的结果。所以我不希望你认为这些东西是显而易见的。完全不是。就像阅读这篇论文,论文中没有描述为什么他们使用这个模型,它与其他模型有何不同,为什么他们认为它有效。我花了一两周的时间才意识到它在数学上等同于普通的逻辑回归,然后又花了几周的时间才意识到区别实际上在于正则化。这有点像机器学习,我相信你从你参加的 Kaggle 竞赛中已经注意到了。就像你提出了一千个好主意,其中 999 个无论你有多么自信它们会很棒,最终都会变成垃圾。然后最终在四周后,其中一个终于奏效,给了你继续度过另外四周的痛苦和挫折的热情。这是正常的。而且我可以确定,我所认识的机器学习领域最优秀的从业者都有一个共同的特点,那就是他们非常顽强,也被称为固执和执着,这绝对是我似乎拥有的声誉,可能是公平的,还有另一点,他们都是非常擅长编码的。他们非常擅长将他们的想法转化为新的代码。对我来说,几个月前通过这个工作是一个非常有趣的经历,试图至少弄清楚为什么这个当时的最新成果存在。
所以一旦我弄清楚了,我实际上能够在此基础上进行改进,并且我会向你展示我做了什么。这就是我非常幸运能够使用 PyTorch 的原因,因为我能够创建出我想要的定制化内容,并且通过使用 GPU 也非常快速。这就是 Fast AI 版本的 NBSVM。实际上,我的朋友 Stephen Merity 是一位在自然语言处理领域出色的研究人员,他将其命名为 NBSVM++,我觉得这很可爱,所以这就是,尽管没有 SVM,但是它是一个逻辑回归,但正如我所说,几乎完全相同。
所以首先让我向你展示代码。一旦我弄清楚这是我能想到的最好的线性词袋模型的方法,我将其嵌入到 Fast AI 中,这样你只需写几行代码就可以了。
sl=2000
# Here is how we get a model from a bag of words
md = TextClassifierData.from_bow(
trn_term_doc,
trn_y,
val_term_doc,
val_y, sl
)
所以代码基本上是,嘿,我想为文本分类创建一个数据类,我想从词袋(from_bow
)中创建它。这是我的词袋(trn_term_doc
),这是我们的标签(trn_y
),这是验证集的相同内容,并且每个评论最多使用 2000 个独特的单词,这已经足够了。
然后从那个模型数据中,构建一个学习器,这是 Fast AI 对基于朴素贝叶斯点积的模型的泛化,然后拟合该模型。
learner = md.dotprod_nb_learner()
learner.fit(0.02, 1, wds=1e-6, cycle_len=1)
'''
[ 0\. 0.0251 0.12003 0.91552]
'''
learner.fit(0.02, 2, wds=1e-6, cycle_len=1)
'''
[ 0\. 0.02014 0.11387 0.92012]
[ 1\. 0.01275 0.11149 0.92124]
'''
learner.fit(0.02, 2, wds=1e-6, cycle_len=1)
'''
[ 0\. 0.01681 0.11089 0.92129]
[ 1\. 0.00949 0.10951 0.92223]
'''
经过 5 个时代,我的准确率已经达到了 92.2。所以现在已经远远超过了线性基准(在原始论文中)。所以让我给你展示一下那段代码。
所以代码非常简短。就是这样。这看起来也非常熟悉。这里有一些小调整,假装这个写着Embedding
的东西实际上写着Linear
。我马上会展示给你看 embedding。所以我们基本上有一个线性层,特征的数量作为行,记住,sklearn 特征意味着基本上是单词的数量。然后对于每个单词,我们将创建一个权重,这是有道理的——逻辑回归,每个单词有一个权重。然后我们将它乘以r值,所以每个单词,我们有一个r值每个类。所以我实际上做了这个,这样可以处理不仅仅是正面和负面,还可以找出是哪个作者创作了这个作品——例如可能有五六个作者。
基本上我们使用这些线性层来得到权重和r的值,然后我们取权重乘以r,然后相加。所以这只是一个简单的点积,就像我们为任何逻辑回归所做的那样,然后进行 softmax。我们为了获得更好的结果所做的非常小的调整是这个+self.w_adj
:
我添加的东西是,这是一个参数,但我几乎总是使用这个默认值 0.4。那么这是做什么的呢?这再次改变了先验。如果你考虑一下,即使我们将这个r乘以文档矩阵作为它们的自变量,你真的想从一个问题开始,好的,惩罚项仍然在将w
推向零。
那么w
为零意味着什么?如果我们的系数都是 0 会怎么样?
当我们将这个矩阵与这些系数相乘时,我们仍然得到零。所以权重为零最终会说“我对这个事情是正面还是负面没有意见。”另一方面,如果它们都是 1,那么基本上就是说我的意见是朴素贝叶斯系数是完全正确的。所以我说零几乎肯定不是正确的先验。我们不应该真的说如果没有系数,那就意味着忽略朴素贝叶斯系数。1 可能太高了,因为我们实际上认为朴素贝叶斯只是答案的一部分。所以我尝试了几个不同的数据集,基本上是说取这些权重并加上一些常数。所以零在这种情况下会变成 0.4。换句话说,正则化惩罚将权重推向这个值而不是零。我发现在许多数据集中,0.4 效果非常好且非常稳健。再次,基本思想是在使用简单模型从数据中学习的同时,尽可能地融入我们的先验知识。所以结果是,当你说让权重矩阵的零实际上意味着你应该使用大约一半的r值时,这比权重应该全部为零的先验效果更好。
问题:w
是表示所需正则化量的点吗?w
是权重。所以x = ((w+self.w_adj)*r/self.r_adj).sum(1)
正在计算我们的激活。我们计算我们的激活等于权重乘以r,然后求和。所以这只是我们的正常线性函数。被惩罚的是我的权重矩阵。这就是受到惩罚的地方。所以通过说,嘿,你知道,不要只使用w
—— 使用w+0.4
。0.4(即self.w_adj
)不受惩罚。它不是权重矩阵的一部分。因此,权重矩阵实际上免费获得了 0.4。
问题:通过这样做,即使经过正则化,每个特征都会获得一些形式的最小权重吗?不一定,因为它最终可能会为一个特征选择一个系数为-0.4
,这将表示“你知道,即使朴素贝叶斯说对于这个特征r应该是什么,我认为你应该完全忽略它”。
休息期间有几个问题。第一个是关于这里正在发生的事情的总结:
这里有w
加上权重调整乘以r:
所以通常,我们所做的是说逻辑回归基本上是wx
(我将忽略偏差)。然后我们将其更改为rx·w
。然后我们说让我们先做x·w
这部分。这里的这个东西,我实际上称之为 w,这可能很糟糕,实际上是w
乘以x
:
所以,我没有r(x·w)
,我有w·x
加上一个常数乘以r。所以这里的关键思想是正则化希望权重为零,因为它试图减少Σw²。所以我们所说的是,好吧,我们希望将权重推向零,因为这是我们的默认起点期望。所以我们希望处于这样一种情况,即如果权重为零,那么我们有一个对我们来说在理论上或直观上有意义的模型。这个模型(r(x·w)
),如果权重为零,对我们来说没有直观意义。因为它在说,嘿,将所有东西乘以零会消除一切。我们实际上在说“不,我们实际上认为我们的r是有用的,我们实际上想保留它。”所以,让我们取(x·w)
并加上 0.4。所以现在,如果正则化器将权重推向零,那么它将将总和的值推向 0.4。
因此,它将整个模型推向 0.4 倍r。换句话说,如果您将所有权重一起正则化到 0.4 倍r,那么我们的默认起点是说“是的,您知道,让我们使用一点r。这可能是一个好主意。”这就是这个想法。这个想法基本上是当权重为零时会发生什么。您希望那是有意义的,否则正则化权重朝着那个方向移动就不是一个好主意。
第二个问题是关于 n-grams。所以 n-gram 中的 N 可以是 uni,bi,tri,等等。1,2,3,等等个 grams。所以“This movie is good”有四个 unigrams:This
,movie
,is
,good
。它有三个 bigrams:This movie
,movie is
,is good
。它有两个 trigrams:This movie is
,movie is good
。
问题:您介意回到w_adj
或0.4
的内容吗?我在想这种调整会不会损害模型的可预测性,因为想象一下极端情况,如果不是 0.4,如果是 4,000,那么所有系数基本上会是…?确切地说。因此,我们的先验需要有意义。这就是为什么它被称为 DotProdNB,因此先验是我们认为朴素贝叶斯是一个好的先验的地方。因此,朴素贝叶斯认为r = p/q是一个好的先验,我们不仅认为这是一个好的先验,而且我们认为rx+b是一个好的模型。这就是朴素贝叶斯模型。换句话说,我们期望系数为 1 是一个好的系数,而不是 4,000。具体来说,我们认为零可能不是一个好的系数。但我们也认为也许朴素贝叶斯版本有点过于自信。所以也许 1 有点高。因此,我们相当确定,假设朴素贝叶斯模型是适当的,正确的数字在 0 和 1 之间。
问题继续:但我在想的是只要不是零,您就会将那些应该为零的系数推到非零的地方,并使高系数与零系数之间的差异变小。嗯,但是您看,它们本来就不应该是零。它们应该是r。请记住,这是在我们的前向函数中,所以这是我们正在计算梯度的一部分。所以基本上是说,好吧,您仍然可以将 self.w 设置为您喜欢的任何值。但是正则化器希望它为零。所以我们所说的是,好吧,如果您希望它为零,那么我将尝试使零给出一个合理的答案。
没有人说 0.4 对于每个数据集都是完美的。我尝试了一些不同的数据集,并发现在 0.3 和 0.6 之间有一些最佳值。但我从未发现一个比零更好的数据集,这并不奇怪。我也从未发现一个更好的数据集。因此,这个想法是一个合理的默认值,但这是另一个您可以玩耍的参数,我有点喜欢。这是另一件您可以使用网格搜索或其他方法来找出对您的数据集最佳的东西。实际上,关键在于在这个模型之前的每个模型,据我所知,都隐含地假设它应该为零,因为它们没有这个参数。顺便说一句,我实际上还有第二个参数(r_adj=10
),它是我对 r 做的相同的事情,实际上是通过一个参数除以 r,我现在不会太担心,但这是另一个您可以用来调整正则化性质的参数。最终,我是一个实证主义者,而不是一个理论家。我认为这似乎是一个好主意。几乎所有我认为是一个好主意的事情最终都被证明是愚蠢的。这个特定的想法在这个数据集上给出了良好的结果,也在其他一些数据集上给出了良好的结果。
**问题:**我仍然对w + w_adj
感到困惑。你提到我们执行w + w_adj
是为了不让系数设为零,我们对先验赋予了一些重要性。但你也说过学习的效果可能是w
被设为负值,这可能会使w + w_adj
为零。所以如果我们允许学习过程确实将系数设为零,那为什么这与只有w
不同呢?因为正则化。因为我们通过Σw²对其进行惩罚。换句话说,我们在说,你知道,如果忽略r是最好的选择,那将会花费你(Σw²)。你将不得不将w
设为负数。所以只有在这显然是一个好主意的情况下才这样做。除非这显然是一个好主意,否则你应该将其保留在原处。这是唯一的原因。今天我们所做的所有事情基本上都是为了最大化我们从正则化中获得的优势,并且说正则化将我们推向某种默认假设,几乎所有的机器学习文献都假设默认假设是所有事物都是零。我在说的是,从理论上讲这是有道理的,而从经验上讲,事实证明你应该决定你的默认假设是什么,这将给你带来更好的结果。
**问题继续:**那么可以这样说,在某种程度上你是在前往将所有系数设为零的过程中设置了一个额外的障碍,如果确实值得的话,它将能够做到这一点吗?是的,确实如此。所以我会说,没有这个默认障碍,使系数非零是障碍。现在我要说的是,不,障碍是使系数不等于 0.4r。
**问题:**所以这是 w²乘以某个常数的总和。如果常数是,比如说 0.1,那么权重可能不会趋向于零。那么我们可能就不需要权重衰减了?如果常数的值为零,那么就没有正则化。但如果这个值大于零,那么就会有一些惩罚。而且可以推测,我们将其设置为非零是因为我们过拟合了。所以我们想要一些惩罚。所以如果有一些惩罚,那么我的观点是我们应该惩罚那些与我们的先验不同的事物,而不是惩罚那些与零不同的事物。我们的先验是事物应该大致等于r。
我想谈谈嵌入。我说假装它是线性的,实际上我们可以假装它是线性的。让我向你展示我们可以多么地假装它是线性的,就像nn.Linear
,创建一个线性层。
这是我们的数据矩阵,这是我们的系数r如果我们正在进行r版本。所以如果我们将r放入列向量中,那么我们可以通过系数对数据矩阵进行矩阵乘法。
因此,这个自变量矩阵乘以这个系数矩阵的矩阵乘法将给我们一个答案。所以问题是,好吧,为什么 Jeremy 没有写nn.Linear
?为什么 Jeremy 写了nn.Embedding
?原因是,如果你回忆一下,我们实际上并不是这样存储的。因为这实际上是宽度为 800,000,高度为 25,000。所以我们实际上是这样存储的:
我们的存储方式是,这个词袋包含哪些单词索引。这是一种稀疏的存储方式。它只列出每个句子中的索引。鉴于此,我现在想要执行我刚刚向你展示的那种矩阵乘法,以创建相同的结果。但我想要从稀疏表示中执行。这基本上是一种独热编码:
这有点像一个虚拟矩阵版本。它有一个单词“this”吗?它有一个单词“movie”吗?等等。所以如果我们采用简单版本的有没有单词“this”(即 1, 0, 0, 0, 0, 0),然后我们将其乘以r,那么它只会返回第一个项目:
总的来说,一个独热编码向量乘以一个矩阵等同于查找该矩阵中的第 n 行。 所以这只是说找到第 0、第一个、第二个和第五个系数:
它们完全相同。 在这种情况下,每个特征只有一个系数,但实际上我这样做的方式是为每个类别的每个特征都有一个系数。 所以在这种情况下,类别是正面和负面。 所以我实际上有 r 正面 (p/q),r 负面 (q/r):
在二进制情况下,显然同时拥有两者是多余的。 但是如果是像这个文本的作者是谁? 是 Jeremy,Savannah 还是 Terrence? 现在我们有三个类别,我们想要三个 r
的值。 所以做这个稀疏版本的好处是,你可以查找第 0、第一个、第二个和第五个。
再次强调,从数学上讲,这与乘以一个独热编码矩阵是相同的。 但是,当输入稀疏时,效率显然要高得多。 因此,这个计算技巧在数学上与乘以一个独热编码矩阵是相同的,而不是概念上类似于。 这被称为嵌入。 我相信大多数人可能已经听说过嵌入,比如词嵌入:Word2Vec,GloVe 等。 人们喜欢把它们说成是这种令人惊叹的新复杂神经网络东西。 但事实并非如此。 嵌入意味着通过简单的数组查找来加快乘以一个独热编码矩阵的过程。 这就是为什么我说你可以把这个想象成说 self.w = nn.Linear(nf+1, 1)
:
因为它实际上做的是相同的事情。 它实际上是一个具有这些维度的矩阵。 这是一个线性层,但它期望我们要给它的输入实际上不是一个独热编码矩阵,而是一个整数列表 —— 每个项目的每个单词的索引。 所以你可以看到 Fast AI 中的 forward
函数自动获取(对于 DotProdNB leaner)特征索引(feature_idx
):
所以它们自动来自稀疏矩阵。 Numpy 使得很容易只需抓取这些索引。 换句话说,我们在这里(feat_idx
)有一个包含这个文档中的 800,000 个单词索引的列表。 所以这里(self.w(feat_idx)
)说的是查找我们的嵌入矩阵中的每一个,该矩阵有 800,000 行,并返回你找到的每一个东西。 从数学上讲,这与乘以一个独热编码矩阵是相同的。 这就是所有嵌入的含义。 这意味着我们现在可以处理构建任何类型的模型,比如任何类型的神经网络,其中我们的输入可能是非常高基数的分类变量。 然后我们只需将它们转换为介于零和级别数之间的数字代码,然后我们可以学习一个线性层,就好像我们已经对其进行了独热编码,而实际上并没有构建独热编码版本,也没有进行矩阵乘法。 相反,我们将只存储索引版本并简单地进行数组查找。 因此,回流的梯度基本上是在独热编码版本中,所有为零的东西都没有梯度,因此回流的梯度只会更新我们使用的嵌入矩阵的特定行。 这对于自然语言处理非常重要,就像在这里一样,我想创建一个 PyTorch 模型,该模型将实现这个非常简单的方程。
如果没有这个技巧,那意味着我要输入一个 25,000 x 80,000 元素的数组,这将有点疯狂。 所以这个技巧让我写下了这个。 我只是用 Embedding 替换了 Linear,用一些只输入索引而不是输入独热编码的东西来替换那个,就这样。 然后它继续工作,所以现在每个时代的训练时间大约是一分钟。
现在我们可以把这个想法应用到不仅仅是语言,而是任何东西上。 例如,预测杂货店商品的销售情况。
问题:我们实际上并没有查找任何东西,对吧?我们只是看到了那个带有索引的数组表示?所以我们正在查找。现在存储的词袋的表示不再是 1 1 1 0 0 1,而是 0 1 2 5。因此,我们实际上必须进行矩阵乘法。但是我们不是进行矩阵乘法,而是查找第零个东西,第一个东西,第二个东西和第五个东西。
问题继续:这意味着我们仍然保留了独热编码矩阵吗?不,我们没有。这里没有使用独热编码矩阵。目前没有突出显示独热编码矩阵。我们目前突出显示的是索引列表和权重矩阵中的系数列表:
所以现在我们要做的是更进一步,我们要说根本不使用线性模型,让我们使用一个多层神经网络。让我们的输入可能包括一些分类变量。这些分类变量,我们将只将其作为数值索引。因此,这些的第一层不会是一个普通的线性层,它们将是一个嵌入层,我们知道在数学上它的行为与线性层完全相同。因此,我们的希望是现在我们可以使用这个来为任何类型的数据创建一个神经网络。
笔记本
几年前在 Kaggle 上有一个名为 Rossmann 的竞赛,这是一个德国的杂货连锁店,他们要求预测他们商店中商品的销售情况。这包括分类和连续变量的混合。在 Guo/Berkhahn 的这篇论文中,他们描述了他们的第三名作品,这比第一名作品简单得多,但几乎一样好,但简单得多,因为他们利用了这个所谓的实体嵌入的想法。在论文中,他们认为他们发明了这个,实际上早些时候由 Yoshua Bengio 和他的合著者在另一个 Kaggle 竞赛中写过。尽管如此,我觉得 Guo 在描述这个如何在许多其他方面使用上走得更远,所以我们也会谈论这个。
笔记本在深度学习存储库中,因为我们在深度学习课程中讨论了一些深度学习特定方面,在这门课程中,我们主要将讨论特征工程,我们还将讨论这个嵌入的想法。
让我们从数据开始。所以数据是,2015 年 7 月 31 日,第 1 号店开业。他们正在进行促销活动。有学校假期。不是国家假期,他们卖出了 5263 件商品。这是他们提供的关键数据。所以目标显然是在没有销售信息的测试集中预测销售额。他们还告诉你,对于每家商店,它是某种特定类型的,销售某种特定种类的商品,其最近的竞争对手距离一定距离,竞争对手在 2008 年 9 月开业,还有一些关于促销的更多信息,我不知道这意味着什么。就像许多 Kaggle 竞赛一样,他们允许您下载外部数据集,只要您与其他竞争者分享。他们还告诉您每家商店所在的州,因此人们下载了德国不同州的名称,他们为德国每周下载了一些谷歌趋势数据。我不知道他们得到了什么具体的谷歌趋势,但是有的。对于每个日期,他们下载了一堆温度信息。就是这样。
这里一个有趣的见解是,Rossmann 可能在某种程度上犯了一个错误,设计这个比赛是一个可以使用外部数据的比赛。因为实际上,你并不能知道下周的天气或下周的谷歌趋势。但当你参加 Kaggle 比赛时,你并不在乎这些。你只是想赢,所以你会利用一切可以得到的。
首先让我们谈谈数据清理。在这个获得第三名的参赛作品中,实际上并没有进行太多的特征工程,特别是按照 Kaggle 的标准,通常每一个细节都很重要。这是一个很好的例子,展示了使用神经网络可以取得多大的成就,这让我想起了昨天我们谈到的索赔预测比赛,获胜者没有进行任何特征工程,完全依赖深度学习。房间里的笑声,我猜,是来自那些在比赛中进行了一点点特征工程的人们
顺便提一下,我发现在比赛中努力工作,然后比赛结束了你没有赢得比赛。然后获胜者出来说这就是我赢得比赛的方法。这是你学到最多的时候。有时候这种情况发生在我身上,我会想,哦,我想到了那个,我试过了,然后我回去发现我那里有个 bug,我没有正确测试,然后我意识到,哦,好吧,我真的需要学会以不同的方式测试这个东西。有时候就像,哦,我想到了那个,但我假设它不会起作用,我真的要记住在做任何假设之前检查一切。你知道,有时候就像,哦,我没有想到那个技术,哇,现在我知道它比我刚刚尝试的一切都要好。否则,如果有人说,嘿,你知道这是一个非常好的技术,你会说好的。但是当你花了几个月的时间尝试做某事,然后别人用那个技术做得更好时,那就相当有说服力了。所以这有点困难,我站在你面前说这里有一堆我用过的技术,我赢得了一些 Kaggle 比赛,我得到了一些最先进的结果。但是当这些信息传达给你时,已经是二手信息了。所以尝试一些东西真的很棒。而且尤其是在深度学习课程中,我注意到,我的一些学生尝试了我说的这个技术,他们第二天就进入了 Kaggle 比赛的前十名,他们说,好的,这算是非常有效。Kaggle 比赛有很多原因是有帮助的。但其中一个最好的方式是比赛结束后发生的事情,所以对于现在即将结束的比赛,确保你观看论坛,看看人们在分享解决方案方面分享了什么,如果你想了解更多,可以自由地问问获胜者,嘿,你能告诉我更多关于这个或那个吗。人们通常很乐意解释。然后最好是尝试自己复制一下。这可以变成一个很棒的博客文章或很棒的内核,可以说,某某说他们使用了这个技术,这里是这个技术的一个非常简短的解释,这里是一点代码展示它是如何实现的,这里是结果展示你可以得到相同的结果。这也可以是一个非常有趣的写作。
数据尽可能易于理解总是很好的。因此,在这种情况下,来自 Kaggle 的数据使用各种整数表示假期。我们可以只使用一个布尔值来表示是否是假期。所以只需清理一下:
train.StateHoliday = train.StateHoliday!='0'
test.StateHoliday = test.StateHoliday!='0'
我们有很多不同的表需要将它们全部合并在一起。我有一种用 Pandas 合并事物的标准方法。我只是使用了 Pandas 的合并函数,具体来说我总是进行左连接。左连接是保留左表中的所有行,你有一个关键列,将其与右侧表中的关键列匹配,然后合并那些也存在于右表中的行。
def join_df(left, right, left_on, right_on=None, suffix='_y'):
if right_on is None:
right_on = left_on
return left.merge(
right,
how='left',
left_on=left_on,
right_on=right_on,
suffixes=("", suffix)
)
我总是进行左连接的关键原因是,在进行连接之后,我总是检查右侧是否有现在为空的内容:
store = join_df(store, store_states, "Store")
len(store[store.State.isnull()])
因为如果是这样,那就意味着我漏掉了一些东西。我没有在这里展示,但我也检查了行数在之前和之后是否有变化。如果有变化,那就意味着右侧表不是唯一的。所以即使我确定某件事是真的,我也总是假设我搞砸了。所以我总是检查。
我可以继续将州名合并到天气中:
weather = join_df(weather, state_names, "file", "StateName")
如果你看一下谷歌趋势表,它有这个周范围,我需要将其转换为日期以便加入它:
在 Pandas 中这样做的好处是,Pandas 让我们可以访问所有的 Python。例如,在系列对象内部,有一个.str
属性,可以让你访问所有的字符串处理函数。就像.cat
让你访问分类函数一样,.dt
让你访问日期时间函数。所以现在我可以拆分该列中的所有内容。
googletrend['Date']=googletrend.week.str.split(' - ',expand=True)[0]
googletrend['State']=googletrend.file.str.split('_', expand=True)[2]
googletrend.loc[googletrend.State=='NI', "State"] = 'HB,NI'
使用这些 Pandas 函数非常重要,因为它们将被向量化,加速,通常通过 SIMD 至少通过 C 代码,以便运行得又快又顺利。
和往常一样,让我们为我们的日期添加日期元数据:
add_datepart(weather, "Date", drop=False)
add_datepart(googletrend, "Date", drop=False)
add_datepart(train, "Date", drop=False)
add_datepart(test, "Date", drop=False)
最后,我们基本上是在对所有这些表进行去规范化。我们将把它们全部放入一个表中。因此,在谷歌趋势表中,它们主要是按州划分的趋势,但也有整个德国的趋势,所以我们将整个德国的趋势放入一个单独的数据框中,以便我们可以加入它:
trend_de = googletrend[googletrend.file == 'Rossmann_DE']
因此,我们将有这个州的谷歌趋势和整个德国的谷歌趋势。
现在我们可以继续为训练集和测试集同时加入。然后检查两者都没有空值。
store = join_df(store, store_states, "Store")
len(store[store.State.isnull()])
'''
0
'''
joined = join_df(train, store, "Store")
joined_test = join_df(test, store, "Store")
len(joined[joined.StoreType.isnull()]),len(joined_test[joined_test.StoreType.isnull()])
'''
(0, 0)
'''
joined = join_df(joined, googletrend, ["State","Year", "Week"])
joined_test = join_df(joined_test, googletrend, ["State","Year", "Week"])
len(joined[joined.trend.isnull()]),len(joined_test[joined_test.trend.isnull()])
'''
(0, 0)
'''
joined = joined.merge(trend_de, 'left', ["Year", "Week"], suffixes=('', '_DE'))
joined_test = joined_test.merge(trend_de, 'left', ["Year", "Week"], suffixes=('', '_DE'))
len(joined[joined.trend_DE.isnull()]),len(joined_test[joined_test.trend_DE.isnull()])
'''
(0, 0)
'''
joined = join_df(joined, weather, ["State","Date"])
joined_test = join_df(joined_test, weather, ["State","Date"])
len(joined[joined.Mean_TemperatureC.isnull()]),len(joined_test[joined_test.Mean_TemperatureC.isnull()])
'''
(0, 0)
'''
我的合并函数,如果有两列是相同的,我将左侧的后缀设置为空,这样它就不会影响名称,右侧设置为_y
。
在这种情况下,我不想要任何重复的内容,所以我只是浏览并删除了它们:
for df in (joined, joined_test):
for c in df.columns:
if c.endswith('_y'):
if c in df.columns:
df.drop(c, inplace=True, axis=1)
for df in (joined,joined_test):
df['CompetitionOpenSinceYear'] = \
df.CompetitionOpenSinceYear.fillna(1900).astype(np.int32)
df['CompetitionOpenSinceMonth'] = \
df.CompetitionOpenSinceMonth.fillna(1).astype(np.int32)
df['Promo2SinceYear'] = \
df.Promo2SinceYear.fillna(1900).astype(np.int32)
df['Promo2SinceWeek'] = \
df.Promo2SinceWeek.fillna(1).astype(np.int32)
这家商店的主要竞争对手自某个日期以来一直开业。因此,我们可以使用 Pandas 的to_datetime
,我传入年、月和日。所以这将给我们一个错误,除非它们都有年和月,所以我们将缺失的部分填充为 1900 年和 1 月(见上文)。而我们真正想知道的是这家商店在这个特定记录时已经开业多久了,所以我们可以进行日期相减:
for df in (joined,joined_test):
df["CompetitionOpenSince"] = \
pd.to_datetime(dict(
year=df.CompetitionOpenSinceYear,
month=df.CompetitionOpenSinceMonth,
day=15
))
df["CompetitionDaysOpen"] = \
df.Date.subtract(df.CompetitionOpenSince).dt.days
现在如果你考虑一下,有时竞争对手的开业时间晚于这一行,所以有时会是负数。而且可能没有意义有负数(即将在 x 天后开业)。现在话虽如此,我绝不会在没有先运行包含它和不包含它的模型的情况下放入这样的东西。因为我们对数据的假设往往是不正确的。在这种情况下,我没有发明任何这些预处理步骤。我写了所有的代码,但它都是基于第三名获奖者的 GitHub 存储库。因此,知道在 Kaggle 竞赛中获得第三名需要做什么,我相当肯定他们会检查每一个这些预处理步骤,并确保它实际上提高了他们的验证集分数。
for df in (joined,joined_test):
df.loc[df.CompetitionDaysOpen<0, "CompetitionDaysOpen"] = 0
df.loc[df.CompetitionOpenSinceYear<1990,"CompetitionDaysOpen"]=0
[1:30:44]
因此,我们将创建一个神经网络,其中一些输入是连续的,而另一些是分类的。这意味着在我们的神经网络中,我们基本上会有这种初始权重矩阵。我们将有这个输入特征向量。一些输入将只是普通的连续数字(例如最高温度,到最近商店的公里数),而另一些将被有效地独热编码。但我们实际上不会将其存储为独热编码。我们实际上会将其存储为索引。
因此,神经网络模型需要知道这些列中的哪些应该基本上创建一个嵌入(即哪些应该被视为独热编码),哪些应该直接输入到线性层中。当我们到达那里时,我们将告诉模型哪个是哪个,但实际上我们需要提前考虑哪些我们想要视为分类变量,哪些是连续变量。特别是,我们要将其视为分类的东西,我们不希望创建比我们需要的更多的类别。让我告诉你我的意思。
这次比赛的第三名决定将比赛开放的月数作为一个他们要用作分类变量的东西。为了避免创建比需要的更多的类别,他们将其截断到 24 个月。他们说,超过 24 个月的任何东西,截断到 24 个。因此,这里是比赛开放的唯一值,从零到 24。这意味着将会有一个嵌入矩阵,基本上会有一个嵌入向量,用于尚未开放的事物(0),用于一个月开放的事物(1),依此类推。
for df in (joined,joined_test):
df["CompetitionMonthsOpen"] = df["CompetitionDaysOpen"]//30
df.loc[df.CompetitionMonthsOpen>24,"CompetitionMonthsOpen"] = 24
joined.CompetitionMonthsOpen.unique()
'''
array([24, 3, 19, 9, 0, 16, 17, 7, 15, 22, 11, 13, 2, 23, 12, 4, 10, 1, 14, 20, 8, 18, 6, 21, 5]
'''
现在,他们绝对可以将其作为一个连续变量来处理[1:33:14]。他们本可以只是在这里放一个数字,表示开放了多少个月,然后将其视为连续变量,直接输入到初始权重矩阵中。但我发现,显然这些竞争对手也发现了,尽可能地将事物视为分类变量是最好的。这样做的原因是,当你通过一个嵌入矩阵传递一些内容时,意味着每个级别可以被完全不同地处理。例如,在这种情况下,某物是否开放了零个月或一个月是非常不同的。因此,如果你将其作为连续变量输入,神经网络将很难找到具有这种巨大差异的功能形式。这是可能的,因为神经网络可以做任何事情。但如果你不让它变得容易。另一方面,如果你使用嵌入,将其视为分类变量,那么零和一将有完全不同的向量。因此,尤其是在你有足够的数据时,尽可能地将列视为分类变量是一个更好的主意。当我说尽可能时,基本上意味着基数不要太高。因此,如果这是每一行上唯一不同的销售 ID 号码,你不能将其视为分类变量。因为那将是一个巨大的嵌入矩阵,而且每样东西只出现一次,或者是距离最近商店的公里数到小数点后两位,你也不会将其作为分类变量。
这是他们在这次比赛中都使用的经验法则。事实上,如果我们滚动到他们的选择,这是他们的做法:
他们的连续变量是真正连续的东西,比如到竞争对手的公里数,温度等。而其他一切,基本上,他们都视为分类变量。
今天就到这里。下次,我们将结束这个话题。我们将看看如何将这个转化为神经网络,并总结一下。到时见!
原文:
medium.com/@hiromi_suenaga/machine-learning-1-lesson-12-6c2512e005a3
译者:飞龙
协议:CC BY-NC-SA 4.0
来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和 Rachel 给了我这个学习的机会。
视频 / 笔记本
我想今天我们可以完成在这个 Rossmann 笔记本中的工作,看一下时间序列预测和结构化数据分析。然后我们可能会对我们学到的一切进行一个小小的复习,因为信不信由你,这就是结尾。关于机器学习没有更多需要知道的东西,只有你将在下个学期和余生中学到的一切。但无论如何,我没有别的要教的了。所以我会做一个小小的复习,然后我们将涵盖课程中最重要的部分,那就是思考如何正确、有效地使用这种技术,以及如何让它对社会产生积极影响的方式。
上次,我们谈到了这样一个想法,当我们试图构建这个 CompetitionMonthsOpen 派生变量时,实际上我们将其截断为不超过 24 个月,我们谈到了原因,因为我们实际上希望将其用作分类变量,因为分类变量,由于嵌入,具有更多的灵活性,神经网络可以如何使用它们。所以这就是我们离开的地方。
for df in (joined,joined_test):
df["CompetitionMonthsOpen"] = df["CompetitionDaysOpen"]//30
df.loc[df.CompetitionMonthsOpen>24, "CompetitionMonthsOpen"]= 24
joined.CompetitionMonthsOpen.unique()
'''
array([24, 3, 19, 9, 0, 16, 17, 7, 15, 22, 11, 13, 2, 23, 12, 4, 10, 1, 14, 20, 8, 18, 6, 21, 5])
'''
让我们继续进行下去。因为这个笔记本中发生的事情可能适用于你处理的大多数时间序列数据集。正如我们所讨论的,虽然我们在这里使用了df.apply
,但这是在每一行上运行一段 Python 代码,速度非常慢。所以只有在找不到可以一次对整列进行操作的矢量化 pandas 或 numpy 函数时才这样做。但在这种情况下,我找不到一种方法可以在不使用任意 Python 的情况下将年份和周数转换为日期。
还值得记住这个 lambda 函数的概念。每当你尝试将一个函数应用到某个东西的每一行或张量的每个元素时,如果没有已经存在的矢量化版本,你将不得不调用像DataFrame.apply
这样的东西,它将运行你传递给每个元素的函数。所以这基本上是函数式编程中的映射,因为很多时候你想要传递给它的函数是你只会使用一次然后丢弃的东西。使用这种 lambda 方法非常常见。所以这个 lambda 是为了告诉df.apply
要使用什么而创建的函数。
for df in (joined,joined_test):
df["Promo2Since"] = pd.to_datetime(df.apply(
lambda x: Week(x.Promo2SinceYear, x.Promo2SinceWeek).monday(),
axis=1
).astype(pd.datetime))
df["Promo2Days"] = df.Date.subtract(df["Promo2Since"]).dt.days
我们也可以用不同的方式来写这个 [3:16]。以下两个单元格是相同的:
一种方法是定义函数(create_promo2since(x)
),然后通过名称传递它,另一种方法是使用 lambda 在现场定义函数。所以如果你不熟悉创建和使用 lambda,练习和玩弄df.apply
是一个很好的练习方法。
让我们来谈谈这个持续时间部分,起初可能看起来有点具体,但实际上并不是。我们要做的是看三个字段:“促销”
、“州假期”
、“学校假期”
所以基本上我们有一个表格:
对于每个商店,对于每个日期,那个商店在那天有促销活动
那个地区的那家店铺在那天有学校假期吗
那个地区的那家店铺在那天有州假期吗
这些事情是事件。带有事件的时间序列非常常见。如果你正在查看石油和天然气钻探数据,你试图说的是通过这根管道的流量,这里是一个代表何时触发了某个警报的事件,或者这里是一个钻头卡住的事件,或者其他。所以像大多数时间序列一样,某种程度上会倾向于代表一些事件。事件发生在某个时间点本身就很有趣,但很多时候时间序列也会显示事件发生前后的情况。例如,在这种情况下,我们正在进行杂货销售预测。如果即将到来一个假期,销售额在假期前后很可能会更高,在假期期间会更低,如果这是一个城市店铺的话。因为你要在离开前备货带东西,然后回来时,你就得重新填满冰箱,例如。虽然我们不必进行这种特征工程来专门创建关于假期前后的特征,但是神经网络,我们能够给神经网络提供它需要的信息,它就不必学习这些信息。它学习的越少,我们就能够利用我们已有的数据做更多事情,利用我们已有的规模架构做更多事情。因此,即使对于神经网络这样的东西,特征工程仍然很重要,因为这意味着我们将能够利用我们拥有的有限数据取得更好的结果,利用我们拥有的有限计算能力取得更好的结果。
因此,这里的基本思想是,当我们的时间序列中有事件时,我们希望为每个事件创建两个新列[7:20]:
还有多久这个事件再次发生。
上次那个事件发生已经多久了。
换句话说,距离下一个州假期还有多久,距离上一个州假期已经多久了。所以这不是我知道存在的库或任何其他东西。所以我手工写在这里。
def get_elapsed(fld, pre):
day1 = np.timedelta64(1, 'D')
last_date = np.datetime64()
last_store = 0
res = []
for s,v,d in zip(
df.Store.values,
df[fld].values,
df.Date.values
):
if s != last_store:
last_date = np.datetime64()
last_store = s
if v: last_date = d
res.append(((d-last_date).astype('timedelta64[D]') / day1))
df[pre+fld] = res
因此,重要的是,我需要按店铺来做这个。所以我想说,对于这家店铺,上次促销是什么时候(即自上次促销以来多长时间),下次促销还有多长时间,例如。
我要做的是这样的。我将创建一个小函数,它将接受一个字段名,然后我将依次传递Promo
、StateHoliday
和SchoolHoliday
。让我们以学校假期为例。所以我们说字段等于学校假期,然后我们说get_elapsed('SchoolHoliday', 'After')
。让我告诉你这将会做什么。我们首先按店铺和日期排序。现在当我们循环遍历时,我们将在店铺内循环遍历。所以店铺#1,1 月 1 日,1 月 2 日,1 月 3 日,依此类推。
fld = 'SchoolHoliday'
df = df.sort_values(['Store', 'Date'])
get_elapsed(fld, 'After')
df = df.sort_values(['Store', 'Date'], ascending=[True, False])
get_elapsed(fld, 'Before')
当我们循环遍历每家店铺时,我们基本上会说这一行是学校假期还是不是[8:56]。如果是学校假期,那么我们将跟踪名为last_date
的变量,表示我们看到学校假期的最后日期。然后我们将追加到我们的结果中自上次学校假期以来的天数。
zip
的重要性[9:26]有一些有趣的特性。其中一个是使用zip。我实际上可以通过编写for row in df.iterrows():
然后从每行中获取我们想要的字段来更简单地编写这个。结果表明,这比我现在的版本慢 300 倍。基本上,遍历 DataFrame 并从行中提取特定字段具有很多开销。更快的方法是遍历 numpy 数组。因此,如果您取一个 Series(例如df.Store
),然后在其后添加.values
,那么就会获取该系列的 numpy 数组。
这里有三个 numpy 数组。一个是商店 ID,一个是fld
是什么(在这种情况下,那是学校假期),还有日期。因此,现在我想要循环遍历每个列表的第一个、第二个和第三个。这是一个非常常见的模式。我基本上在我写的每个笔记本中都需要做类似的事情。而要做到这一点的方法就是使用 zip。因此,zip
意味着逐个循环遍历这些列表。然后这里是我们可以从第一个列表、第二个列表和第三个列表中获取元素的地方:
因此,如果您还没有使用 zip 进行过多尝试,那么这是一个非常重要的函数需要练习。就像我说的,我几乎在我写的每个笔记本中都使用它——每次您都必须同时循环遍历一堆列表。
因此,我们将循环遍历每个商店、每个学校假期和每个日期。
问题:它是否循环遍历了所有可能的组合?不是。只有 111、222 等。
因此,在这种情况下,我们基本上想要说让我们获取第一个商店、第一个学校假期和第一个日期。因此,对于商店 1,1 月 1 日,学校假期是真还是假。因此,如果是学校假期,我会通过记录上次看到学校的日期来跟踪这一事实,并附加自上次学校假期以来的时间长度。如果商店 ID 与上一个商店 ID 不同,那么我现在已经到了一个全新的商店,这种情况下,我基本上必须重置一切。
问题:对于我们没有最后一个假期的第一个点会发生什么?是的,所以我只是将其设置为一些任意的起始点(np.datetime64()
),最终会得到,我记不清了,要么是最大的日期,要么是最小的日期。您可能需要在之后用缺失值或零替换它。不过好处是,由于 ReLU 的存在,神经网络很容易截断极端值。因此,在这种情况下,我没有对其做任何特殊处理。我最终得到了这些像负十亿日期时间戳,但仍然可以正常工作。
接下来要注意的是,我需要对训练集和测试集进行一些处理。在前一节中,我实际上添加了一个循环,对训练 DataFrame 和测试 DataFrame 进行以下操作:
对于每个数据框中的每个单元格,我都进行了以下操作:
接下来,有一系列单元格我首先要为训练集和测试集运行。在这种情况下,我有两个不同的单元格:一个将 df 设置为训练集,一个将其设置为测试集。
我使用的方法是,我只运行第一个单元格(即跳过 df=test[columns]
),然后运行下面的所有单元格,这样就可以对训练集进行全部操作。然后我回来运行第二个单元格,然后运行下面的所有单元格。因此,这个笔记本不是设计为从头到尾顺序运行的。但是它被设计为以这种特定方式运行。我提到这一点是因为这可能是一个有用的技巧。当然,你可以将下面的所有内容放在一个函数中,将数据框传递给它,并在测试集上调用一次,在训练集上调用一次。但我更喜欢有点实验,更交互地看着每一步。因此,这种方式是在不将其转换为函数的情况下在不同数据框上运行某些内容的简单方法。
如果我按店铺和日期排序,那么这就是在追踪上次发生某事的时间[15:11]。因此 d - last_date
最终会告诉我距离上次学校假期有多少天:
现在如果我按日期降序排列并调用完全相同的函数,那么它会告诉我距离下一个假期还有多久:
所以这是一个很好的技巧,可以将任意事件时间添加到你的时间序列模型中。例如,如果你现在正在进行厄瓜多尔杂货比赛,也许这种方法对其中的各种事件也会有用。
为了国家假期,为了促销,我们来做一下:
fld = 'StateHoliday'
df = df.sort_values(['Store', 'Date'])
get_elapsed(fld, 'After')
df = df.sort_values(['Store', 'Date'], ascending=[True, False])
get_elapsed(fld, 'Before')
fld = 'Promo'
df = df.sort_values(['Store', 'Date'])
get_elapsed(fld, 'After')
df = df.sort_values(['Store', 'Date'], ascending=[True, False])
get_elapsed(fld, 'Before')
我们在这里看的下一件事是滚动函数。在 pandas 中,滚动是我们创建所谓的窗口函数的方式。假设我有这样的一些数据。我可以说好,让我们在这个点周围创建一个大约 7 天的窗口。
然后我可以取得那七天窗口内的平均销售额。然后我可以在这里做同样的事情,取得那七天窗口内的平均销售额。
所以如果对每个点都这样做,并连接起那些平均值,你最终会得到一个移动平均值:
移动平均值的更通用版本是窗口函数,即将某个函数应用于每个点周围的一些数据窗口。很多时候,我在这里展示的窗口实际上并不是你想要的。如果你试图构建一个预测模型,你不能将未来作为移动平均的一部分。因此,通常你实际上需要一个在某个点结束的窗口(而不是点位于窗口中间)。那就是我们的窗口函数:
Pandas 允许你使用这里的滚动来创建任意窗口函数:
bwd = df[['Store']+columns].sort_index() \
.groupby("Store").rolling(7, min_periods=1).sum()
fwd = df[['Store']+columns].sort_index(ascending=False) \
.groupby("Store").rolling(7, min_periods=1).sum()
第一个参数表示我想将函数应用到多少个时间步。第二个参数表示如果我处于边缘,换句话说,如果我处于上图的左边缘,你应该将其设置为缺失值,因为我没有七天的平均值,或者要使用的最小时间段数是多少。所以这里,我设置为 1。然后你还可以选择设置窗口在周期的开始、结束或中间。然后在其中,你可以应用任何你喜欢的函数。所以这里,我有我的按店铺每周求和。所以有一个很简单的方法来得到移动平均值或其他内容。
我应该提到,如果你去 Pandas 的时间序列页面,左侧有一个很长的索引列表。这是因为 Wes McKinney 创造了这个,他最初是在对冲基金交易中,我相信。他的工作都是关于时间序列的。所以我认为 Pandas 最初非常专注于时间序列,而且现在它可能仍然是 Pandas 最强大的部分。所以如果你在处理时间序列计算,你绝对应该尝试学习整个 API。关于时间戳、日期偏移、重采样等方面有很多概念性的内容需要理解。但这绝对值得,否则你将手动编写这些循环。这将比利用 Pandas 已经做的事情花费更多时间。当然,Pandas 将为你使用高度优化的向量化 C 代码,而你的版本将在 Python 中循环。所以如果你在处理时间序列的工作,学习完整的 Pandas 时间序列 API 是绝对值得的。它们几乎和其他任何时间序列 API 一样强大。
好的,经过所有这些,你可以看到这些起始值,我提到的 —— 稍微偏向极端。所以你可以看到,9 月 17 日,商店 1 距上次学校假期 13 天。16 日是 12,11,10,依此类推。
我们目前处于促销期。这里,这是促销前一天:
在它的左边,我们在上次促销之后有 9 天等等。这就是我们如何可以向我们的时间序列添加事件计数器的方式,当你在处理时间序列时,这通常是一个好主意。
现在我们已经做到了,我们的数据集中有很多列,所以我们将它们分成分类和连续列。我们将在回顾部分更多地讨论这一点,但这些将是我将为其创建嵌入的所有内容:
而 contin_vars
是我将直接输入模型的所有东西。例如,我们有 CompetitionDistance
,这是到最近竞争对手的距离,最高温度,以及我们有一个分类值 DayOfWeek
。所以这里,我们有最高温度,可能是 22.1,因为德国使用摄氏度,我们有到最近竞争对手的距离,可能是 321.7 公里。然后我们有星期几,也许星期六是 6。前两个数字将直接进入我们要输入神经网络的向量中。我们将在稍后看到,但实际上我们会对它们进行归一化,或多或少。但这个分类变量,我们不会。我们需要将它通过一个嵌入层。所以我们将有一个 7x4 的嵌入矩阵(例如,维度为 4 的嵌入)。这将查找第 6 行以获取四个项目。所以星期六将变成长度为 4 的向量,然后添加到这里。
这就是我们的连续和分类变量将如何工作。
然后我们将所有的分类变量转换为 Pandas 的分类变量,方式与之前相同:
for v in cat_vars:
joined[v] = joined[v].astype('category').cat.as_ordered()
然后我们将应用相同的映射到测试集。如果在训练集中星期六是 6,apply_cats
确保在测试集中星期六也是 6:
apply_cats(joined_test, joined)
对于连续变量,确保它们都是浮点数,因为 PyTorch 期望所有东西都是浮点数。
for v in contin_vars:
joined[v] = joined[v].fillna(0).astype('float32')
joined_test[v] = joined_test[v].fillna(0).astype('float32')
然后这是我使用的另一个小技巧。
idxs = get_cv_idxs(n, val_pct=150000/n)
joined_samp = joined.iloc[idxs].set_index("Date")
samp_size = len(joined_samp); samp_size150000
这两个单元格(上面和下面)都定义了一个叫做joined_samp
的东西。其中一个将它们定义为整个训练集,另一个将它们定义为一个随机子集。所以我的想法是,我在样本上做所有的工作,确保一切都运行良好,尝试不同的超参数和架构。然后当我对此满意时,我会回过头来运行下面这行代码,说,好,现在让整个数据集成为样本,然后重新运行它。
samp_size = n
joined_samp = joined.set_index("Date")
这是一个很好的方法,与我之前向您展示的类似,它让您可以在笔记本中使用相同的单元格首先在样本上运行,然后稍后回来并在完整数据集上运行。
现在我们有了joined_samp
,我们可以像以前一样将其传递给 proc_df 来获取因变量以处理缺失值。在这种情况下,我们传递了一个额外的参数do_scale=True
。这将减去均值并除以标准差。
df, y, nas, mapper = proc_df(joined_samp, 'Sales', do_scale=True)
yl = np.log(y)
这是因为如果我们的第一层只是一个矩阵乘法。这是我们的权重集。我们的输入大约是 0.001 和另一个是 10⁶,例如,然后我们的权重矩阵已经初始化为 0 到 1 之间的随机数。然后基本上 10⁶的梯度将比 0.001 大 9 个数量级,这对优化不利。因此,通过将所有内容标准化为均值为零标准差为 1 开始,这意味着所有的梯度将在同一种规模上。
我们在随机森林中不需要这样做,因为在随机森林中,我们只关心排序。我们根本不关心值。但是对于线性模型和由线性模型层构建而成的东西,即神经网络,我们非常关心规模。因此,do_scale=True
为我们归一化我们的数据。现在,由于它为我们归一化了数据,它会返回一个额外的对象mapper
,其中包含了每个连续变量被归一化时的均值和标准差。原因是我们将不得不在测试集上使用相同的均值和标准差,因为我们需要我们的测试集和训练集以完全相同的方式进行缩放;否则它们将具有不同的含义。
因此,确保您的测试集和训练集具有相同的分类编码、相同的缺失值替换和相同的缩放归一化的细节非常重要,因为如果您没有做对,那么您的测试集根本不会起作用。但是如果您按照这些步骤操作,它将正常工作。我们还对因变量取对数,这是因为在这个 Kaggle 竞赛中,评估指标是均方根百分比误差。均方根百分比误差意味着我们根据我们的答案和正确答案之间的比率受到惩罚。我们在 PyTorch 中没有一个叫做均方根百分比误差的损失函数。我们可以编写一个,但更简单的方法是对因变量取对数,因为对数之间的差异与比率相同。因此,通过取对数,我们可以轻松地得到这个效果。
你会注意到 Kaggle 上绝大多数的回归竞赛要么使用均方根百分比误差,要么使用对数的均方根误差作为他们的评估指标。这是因为在现实世界的问题中,大多数时候,我们更关心比率而不是原始差异。因此,如果您正在设计自己的项目,很可能您会考虑使用因变量的对数。
然后我们创建一个验证集,正如我们之前学到的,大多数情况下,如果你的问题涉及时间因素,你的验证集可能应该是最近的时间段,而不是一个随机子集。所以这就是我在这里做的:
val_idx = np.flatnonzero(
(df.index<=datetime.datetime(2014,9,17)) &
(df.index>=datetime.datetime(2014,8,1)))
当我完成建模并找到一个架构、一组超参数、一定数量的 epochs 以及所有能够很好工作的东西时,如果我想让我的模型尽可能好,我会重新在整个数据集上进行训练 — 包括验证集。现在,至少目前为止,Fast AI 假设你有一个验证集,所以我的一种折中方法是将我的验证集设置为只有一个索引,即第一行:
val_idx=[0]
这样所有的代码都能继续运行,但实际上没有真正的验证集。显然,如果你这样做,你需要确保你的最终训练与之前的完全相同,包括相同的超参数、相同数量的 epochs,因为现在你实际上没有一个正确的验证集来进行检查。
问题:我有一个关于之前讨论过的 get_elapsed 函数的问题。在 get_elapsed 函数中,我们试图找出下一个假期还有多少天。所以每年,假期基本上是固定的,比如 7 月 4 日、12 月 25 日都会有假期,几乎没有变化。那么我们不能从以前的年份查找,然后列出今年将要发生的所有假期吗?也许可以。我的意思是,在这种情况下,我猜对于Promo
和一些假期会改变,比如复活节,这种方法可以适用于所有情况。而且运行起来也不会花太长时间。如果你的数据集太大,导致运行时间太长,你可以在一年内运行一次,然后以某种方式复制。但在这种情况下,没有必要。我总是把我的时间看得比电脑的时间更重要,所以我尽量保持事情尽可能简单。
现在我们可以创建我们的模型。要创建我们的模型,我们必须像在 Fast AI 中一样创建一个模型数据对象。所以一个列模型数据对象只是一个代表训练集、验证集和可选测试集的标准列结构化数据的模型数据对象。
md = ColumnarModelData.from_data_frame(
PATH, val_idx, df,
yl.astype(np.float32),
cat_flds=cat_vars,
bs=128,
test_df=df_test
)
我们只需要告诉它哪些变量应该被视为分类变量。然后传入我们的数据框。
对于我们的每个分类变量,这里是它所拥有的类别数量。因此,对于我们的每个嵌入矩阵,这告诉我们该嵌入矩阵中的行数。然后我们定义我们想要的嵌入维度。如果你在进行自然语言处理,那么需要捕捉一个词的含义和使用方式的所有细微差别的维度数量经验性地被发现大约是 600。事实证明,当你使用小于 600 的嵌入矩阵进行自然语言处理模型时,结果不如使用大小为 600 的好。超过 600 后,似乎没有太大的改进。我会说人类语言是我们建模的最复杂的事物之一,所以我不会指望你会遇到许多或任何需要超过 600 维度的嵌入矩阵的分类变量。另一方面,有些事物可能具有相当简单的因果关系。例如,StateHoliday
——也许如果某事是假日,那么在城市中的商店会有一些行为,在乡村中的商店会有一些其他行为,就是这样。也许这是一个相当简单的关系。因此,理想情况下,当你决定使用什么嵌入大小时,你应该利用你对领域的知识来决定关系有多复杂,因此我需要多大的嵌入。实际上,你几乎永远不会知道这一点。你只知道这一点,因为也许别人以前已经做过这方面的研究并找到了答案,就像在自然语言处理中一样。因此,在实践中,你可能需要使用一些经验法则,并尝试一些经验法则后,你可以尝试再高一点,再低一点,看看哪种方法有帮助。所以这有点像实验。
cat_sz=[
(c, len(joined_samp[c].cat.categories)+1)
for c in cat_vars
]
cat_sz
'''
[('Store', 1116),
('DayOfWeek', 8),
('Year', 4),
('Month', 13),
('Day', 32),
('StateHoliday', 3),
('CompetitionMonthsOpen', 26),
('Promo2Weeks', 27),
('StoreType', 5),
('Assortment', 4),
('PromoInterval', 4),
('CompetitionOpenSinceYear', 24),
('Promo2SinceYear', 9),
('State', 13),
('Week', 53),
('Events', 22),
('Promo_fw', 7),
('Promo_bw', 7),
('StateHoliday_fw', 4),
('StateHoliday_bw', 4),
('SchoolHoliday_fw', 9),
('SchoolHoliday_bw', 9)]
'''
这里是我的经验法则。我的经验法则是看看该类别有多少个离散值(即嵌入矩阵中的行数),并使嵌入的维度为该值的一半。所以如果是星期几,第二个,有八行和四列。这里是(c+1)//2
——列数除以二。但是我说不要超过 50。在这里你可以看到对于商店(第一行),有 116 家商店,只有一个维度为 50。为什么是 50?我不知道。到目前为止似乎效果还不错。你可能会发现你需要一些稍微不同的东西。实际上,对于厄瓜多尔杂货比赛,我还没有真正尝试过调整这个,但我认为我们可能需要一些更大的嵌入大小。但这是可以摆弄的东西。
emb_szs = [(c, min(50, (c+1)//2)) for _,c in cat_sz]
emb_szs
'''
[(1116, 50),
(8, 4),
(4, 2),
(13, 7),
(32, 16),
(3, 2),
(26, 13),
(27, 14),
(5, 3),
(4, 2),
(4, 2),
(24, 12),
(9, 5),
(13, 7),
(53, 27),
(22, 11),
(7, 4),
(7, 4),
(4, 2),
(4, 2),
(9, 5),
(9, 5)]
'''
问题:随着基数大小变得越来越大,您正在创建越来越宽的嵌入矩阵。因此,您是否会因为选择了 70 个参数而极大地增加过拟合的风险,因为模型永远不可能捕捉到数据实际巨大的所有变化[36:44]?这是一个很好的问题,所以让我提醒您一下现代机器学习和旧机器学习之间的区别的黄金法则。在旧的机器学习中,我们通过减少参数数量来控制复杂性。在现代机器学习中,我们通过正则化来控制复杂性。所以简短的答案是不。我不担心过拟合,因为我避免过拟合的方式不是通过减少参数数量,而是通过增加丢弃率或增加权重衰减。现在说到这一点,对于特定的嵌入,没有必要使用比我需要的更多的参数。因为正则化是通过给模型更多的随机数据或实际上对权重进行惩罚来惩罚模型。所以我们宁愿不使用比必要更多的参数。但是在设计架构时,我的一般经验法则是在参数数量方面慷慨一些。在这种情况下,如果经过一些工作后,我们觉得商店实际上似乎并不那么重要。那么我可能会手动去修改它,使其更小。或者如果我真的发现这里的数据不够,我要么过拟合了,要么使用的正则化比我感到舒适的要多,那么您可能会回去。但我总是会从参数慷慨的角度开始。在这种情况下,这个模型表现得相当不错。
好的,现在我们有一个包含每个嵌入矩阵的行数和列数的元组列表[38:41]。所以当我们调用get_learner
来创建我们的神经网络时,这是我们传入的第一件事:
emb_szs
: 我们的每个嵌入有多大
len(df.columns)-len(cat_vars)
: 我们有多少连续变量
[1000,500]
: 每一层要创建多少个激活
[0.001,0.01]
: 每一层使用的丢弃率是多少
m = md.get_learner(
emb_szs,
len(df.columns)-len(cat_vars),
0.04, 1,
[1000,500],
[0.001,0.01],
y_range=y_range
)
m.summary()
然后我们可以继续调用fit
。我们训练了一段时间,得到了大约 0.1 的分数。
m.fit(lr, 1, metrics=[exp_rmspe])
'''
[ 0\. 0.01456 0.01544 0.1148 ]
'''
m.fit(lr, 3, metrics=[exp_rmspe])
'''
[ 0\. 0.01418 0.02066 0.12765]
[ 1\. 0.01081 0.01276 0.11221]
[ 2\. 0.00976 0.01233 0.10987]
'''
m.fit(lr, 3, metrics=[exp_rmspe], cycle_len=1)
'''
[ 0\. 0.00801 0.01081 0.09899]
[ 1\. 0.00714 0.01083 0.09846]
[ 2\. 0.00707 0.01088 0.09878]
'''
所以我尝试在测试集上运行这个,并且上周我把它提交到了 Kaggle,这里是结果[39:25]:
私人分数 0.107,公共分数 0.103。所以让我们看看这将如何进行。让我们从公共排行榜开始:
在 3000 个参赛者中排名第 340。这不太好。让我们试试私人排行榜,排名是 0.107。
哦,第 5 名[40:30]。所以希望您现在在想,哦,有一些 Kaggle 比赛即将结束,我参加了,并且花了很多时间在公共排行榜上取得好成绩。我想知道那是否是个好主意。答案是否定的,那不是。Kaggle 公共排行榜并不是要取代您精心开发的验证集。例如,如果您正在进行冰山比赛(哪些是船只,哪些是冰山),那么他们实际上在公共排行榜中放入了大约 4000 张合成图像,而在私人排行榜中没有放入任何图像。所以这是 Kaggle 测试您的一个非常好的方面,“您是否创建了一个好的验证集并且是否信任它?”因为如果您更信任排行榜的反馈而不是验证反馈,那么当您认为自己排名第 5 时,您可能会发现自己排名第 350。在这种情况下,我们实际上有一个相当不错的验证集,因为正如您所看到的,它大约是 0.1,而我们实际上确实得到了大约 0.1。所以在这种情况下,这个比赛的公共排行榜完全没有用。
问题:那么,公共排行榜的前几名实际上与私人排行榜的前几名有多少对应呢?因为在流失预测挑战中,有 4 个人完全超过了其他人[42:07]。这完全取决于情况。如果他们随机抽取公共和私人排行榜,那么这应该是非常有指示性的。但也可能不是。所以在这种情况下,公共排行榜上的第二名最终赢得了比赛。公共排行榜上的第一名排在第七位。事实上,你可以看到这里的小绿色标记。而另外一个人则跃升了 96 个名次。
如果我们用刚刚看过的神经网络进入,我们将跃升 350 个名次。所以这取决于情况。有时他们会告诉你公共排行榜是随机抽样的。有时他们会告诉你不是。通常你必须通过查看验证集结果和公共排行榜结果之间的相关性来判断它们之间的相关性有多好。有时,如果有 2 或 3 个人远远领先于其他人,他们可能已经发现了某种泄漏或类似的情况。这通常是一种存在某种技巧的迹象。
好的,这就是 Rossmann,这也是我们所有材料的结束。
我们学会了两种训练模型的方法。一种是通过构建树,另一种是使用 SGD。因此,SGD 方法是一种可以训练线性模型或具有非线性层之间的线性层堆叠的模型的方法。而树构建具体将给我们一棵树。然后,我们可以将树构建与装袋结合起来创建随机森林,或者与提升结合起来创建 GBM,或者其他一些略有不同的变体,比如极端随机树。因此,值得提醒自己这些东西是做什么的。所以让我们看一些数据。实际上,让我们具体看看分类数据。因此,分类数据可能看起来有几种可能性。比如我们有邮政编码,所以我们有 94003 作为我们的邮政编码。然后我们有销售额,比如 50。对于 94131,销售额为 22,等等。因此,我们有一些分类变量。我们可以表示这种分类变量的几种方式。一种是只使用数字。也许一开始它不是一个数字。也许根本不是一个数字。也许一个分类变量是像旧金山、纽约、孟买和悉尼这样的城市。但我们可以通过任意决定给它们编号来将其转换为数字。因此,它最终成为一个数字。我们可以使用这种任意的数字。因此,如果发现相邻的邮政编码具有相似的行为,那么邮政编码与销售额的图表可能看起来像这样,例如:
或者,如果相邻的两个邮政编码在任何方面都没有相似的销售行为,你会期望看到更像这样的情况:
有点到处都是。所以有两种可能性。如果我们只是用这种方式对邮政编码进行编码,那么随机森林会做什么是,它会说,好的,我需要找到我的最佳分割点——使两侧的标准差尽可能小或在数学上等效地具有最低均方根误差的分割点。因此,在这种情况下,它可能会选择这里作为第一个分割点,因为在左侧有一个平均值,在另一侧有另一个平均值[48:07]。
然后对于它的第二个分割点,它会说如何分割右侧,它可能会说我会在这里分割,因为现在我们有了这个平均值和这个平均值:
最后,它会说我们如何拆分中间部分,然后它会说好的,我会在中间拆分。所以你可以看到,即使它贪婪地自上而下一次一次地进行拆分,它仍然能够专注于它需要的拆分集合。唯一的原因是如果两半总是完全平衡,那么它就无法做到这一点。但即使发生这种情况,也不会是世界末日。它会在其他地方拆分,下一次,两部分树仍然完全平衡的可能性非常小。因此,在实践中,这完全没问题。
在第二种情况下,它可以做同样的事情。即使一个邮政编码与其相邻的邮政编码之间在数字上没有关系。我们仍然可以看到,如果在这里拆分,一侧的平均值,另一侧的平均值可能大约在这里:
那么接下来它会在哪里拆分?可能是在这里,因为这一侧的平均值,另一侧的平均值在这里。
所以,可以做同样的事情。它将需要更多的拆分,因为它将需要缩小到每个单独的大邮政编码和每个单独的小邮政编码。但这仍然没问题。所以当我们处理为随机森林或 GBM 构建决策树时,我们 tend to encode our variables just as ordinals.
另一方面,如果我们正在做神经网络或者像线性回归或逻辑回归这样的最简单版本,它能做的最好就是(绿色),这一点一点也不好:
而且这个也是一样的:
所以一个序数对于线性模型或将线性和非线性模型堆叠在一起的模型来说并不是一个有用的编码。所以,我们创建一个独热编码。就像这样:
通过这种编码,可以有效地创建一个小直方图,其中每个级别都会有一个不同的系数。这样,它可以做到它需要做的事情。
问题:在什么时候这对你的系统变得太繁琐?几乎从来没有。因为请记住,在现实生活中,我们实际上不需要创建那个矩阵。相反,我们可以只有四个系数,然后进行索引查找,这在数学上等同于在独热编码上进行乘法。所以这不是问题。
有一件事要提到。我知道你们已经学到了更多关于事物的分析解决方案。在像线性回归这样的分析解决方案中,你无法解决这种程度的共线性问题。换句话说,如果不是孟买、纽约或旧金山,你就知道某样东西在悉尼。换句话说,这四个类别中的第四个与其他三个之间存在百分之百的共线性。因此,如果你尝试以这种方式在分析上解决线性回归问题,整个事情就会崩溃。现在请注意,对于 SGD,我们没有这样的问题。像 SGD 为什么会在乎呢?我们只是沿着导数走一步。它会在乎一点,因为最终,共线性的主要问题在于有无限数量的同样好的解决方案。换句话说,我们可以增加左边的所有这些,减少这个。或者减少所有这些,增加这个。它们会平衡。
当存在无限多个好的解决方案时,意味着损失曲面上有很多平坦区域,这可能会使优化变得更加困难。因此,摆脱所有这些平坦区域的真正简单方法是添加一点正则化。因此,如果我们添加了一点权重衰减,比如 1e-7,那么这就表示这些解决方案不再是完全相同的,最好的解决方案是参数最小且彼此最相似的解决方案,这将使其再次成为一个良好的损失函数。
问题:您能澄清一下您提到为什么独热编码不会那么繁琐的那一点吗?当然。如果我们有一个独热编码向量,并且将其乘以一组系数,那么这完全等同于简单地说让我们找到其中值为 1 的那个值。换句话说,如果我们将1000
存储为零,0100
存储为一,0020
存储为二,那么这完全等同于只是说嘿,查找数组中的那个值。
所以我们称这个版本为嵌入。因此,嵌入是一个权重矩阵,您可以将其与独热编码相乘。这只是一个计算快捷方式。但从数学上讲,它是一样的。
解决线性模型的解析方法与使用 SGD 解决的方法之间存在关键差异。使用 SGD,我们不必担心共线性等问题,至少不像解析方法那样。然后使用 SGD 解决线性模型或单层或多层模型与使用树的区别;树会对更少的事情提出异议。特别是,您可以将序数作为您的分类变量,并且正如我们之前学到的,对于树,我们也不必担心对连续变量进行归一化,但是对于这些使用 SGD 训练的模型,我们必须担心。
然后我们还学到了很多关于解释随机森林的知识。如果您感兴趣,您可能会尝试使用相同的技术来解释神经网络。如果您想知道在神经网络中哪些特征很重要,您可以尝试同样的方法;依次对每列进行洗牌,看看它对准确性的影响有多大。这将是您神经网络的特征重要性。然后,如果您真的想玩得开心,认识到,那么,对该列进行洗牌只是计算输出对该输入的敏感性的一种方式,换句话说,就是输出对该输入的导数。因此,也许您可以直接要求 PyTorch 给您输出关于输入的导数,看看是否会得到相同类型的答案。
您可以对偏依赖图做同样的事情。您可以尝试使用神经网络做完全相同的事情;用相同的值替换列中的所有内容,对 1960、1961、1962 进行绘图。我不知道有没有人在以前做过这些事情,不是因为这是火箭科学,而只是可能没有人想到或者不在库中,我不知道。但如果有人尝试过,我认为您会发现它很有用。这将成为一篇很棒的博客文章。甚至如果您想进一步,也可以写成论文。所以这是一个您可以尝试的想法。因此,大多数这些解释技术并不特别适用于随机森林。像树解释器这样的东西当然适用,因为它们都是关于树内部的东西。
问题:在树解释器中,我们正在查看特征的路径及其贡献。在神经网络的情况下,我猜每个激活在其路径上的贡献会是相同的,对吗?是的,也许。我不知道。我还没有考虑过这个。问题继续:我们如何从激活中推断出结论?Jeremy:说“推断”这个词要小心,因为人们通常使用“推断”这个词来特指测试时间的预测。你的意思是对模型进行一种询问。我不确定。我们应该考虑一下。实际上,Hinton 和他的一位学生刚刚发表了一篇关于如何用树来近似神经网络的论文,就是出于这个原因。我还没有看过这篇论文。
问题:在线性回归和传统统计学中,我们关注的一件事是变化的统计显著性之类的东西。所以在考虑树解释器或者瀑布图,我猜这只是一种可视化。我猜这在哪里适用?因为我们可以看到,哦,是的,这看起来很重要,因为它导致了很大的变化,但我们怎么知道它在传统上是否具有统计显著性?所以大多数时候,我不关心传统的统计显著性,原因是现在,统计显著性的主要驱动因素是数据量,而不是实际重要性。而且现在,您构建的大多数模型都会有如此多的数据,以至于每一个微小的事情都会在统计上显著,但其中大多数在实际上并不重要。因此,我的主要关注点是实际重要性,即这种影响的大小是否影响您的业务?在我们处理的数据较少时,统计显著性更为重要。如果您确实需要了解统计显著性,例如,因为您有一个非常小的数据集,因为标记成本很高或者很难收集,或者是一个罕见疾病的医疗数据集,您总是可以通过自助法来获得统计显著性,也就是说,您可以随机重新对数据集进行多次抽样,多次训练您的模型,然后您可以看到预测的实际变化。因此,通过自助法,您可以将任何模型转化为能够给出置信区间的东西。有一篇由 Michael Jordan 撰写的论文,其中有一种被称为小自助袋的技术,实际上将这一点推进了一步,如果您感兴趣,那么这篇论文是值得一读的。
问题:你说如果你在做随机森林时不需要一个独热编码矩阵。如果我们这样做会发生什么,模型会有多糟糕?我们实际上确实这样做了。记得我们有那个最大类别大小,我们确实创建了一个独热编码,我们这样做的原因是我们的特征重要性会告诉我们个别级别的重要性,我们的部分依赖图,我们可以包括个别级别。所以这并不一定会使模型变得更糟,它可能会使它变得更好,但它可能根本不会改变太多。在这种情况下,它几乎没有改变。问题继续:这也是我们在真实数据中注意到的一点,如果基数更高,比如说有 50 个级别,如果你做独热编码,随机森林表现得非常糟糕?Jeremy:是的,没错。这就是为什么在 Fast.AI 中,我们有最大分类大小,因为在某个时候,你的独热编码变量会变得太稀疏。所以我通常在 6 或 7 处截断。另外,当你超过那个点时,它变得不那么有用,因为对于特征重要性来说,要看的级别太多了。问题继续:它是否可以只查看那些不重要的级别,然后将那些显著的特征视为重要?Jeremy:是的,这样也可以。就像一旦基数增加得太高,你基本上就是把你的数据分割得太多了,所以在实践中,你的有序版本可能会更好。
没有时间来审查所有内容,但这是关键概念,然后记住我们可以使用的嵌入矩阵可能不只有一个系数,实际上我们将有几个系数的维度,这对于大多数线性模型来说并不实用。但一旦你有了多层模型,那么现在就创建了一个相当丰富的类别表示,你可以用它做更多的事情。
幻灯片
现在让我们谈谈最重要的部分。我们在这门课程的早期谈到了很多机器学习实际上是有些误导的。人们关注预测准确性,比如亚马逊有一个协同过滤算法用于推荐书籍,最终推荐出它认为你最有可能高评的书。结果他们最终可能会推荐一本你已经有或者你已经知道并且本来就会购买的书,这并没有太大价值。他们应该做的是找出哪本书可以推荐给你,从而改变你的行为。这样,我们实际上最大化了由于推荐而带来的销售增长。所以这种优化影响你的行为与仅仅提高预测准确性之间的区别的想法。提高预测准确性是一个非常重要的区别,在学术界或行业中很少被讨论,令人惊讶。在行业中更多地被讨论,但在大多数学术界中被忽视。所以这是一个非常重要的想法,即最终你的模型的目标,想必是影响行为。所以请记住,我实际上提到了我写的一篇关于这个的论文,在那里我介绍了一种叫做传动系统方法的东西,我谈到了如何将机器学习纳入到我们如何实际影响行为的方式中。这是一个起点,但接下来的问题是,如果我们试图影响行为,我们应该影响什么样的行为,以及如何影响,当我们开始影响行为时可能意味着什么。因为现在很多公司,你最终会在那里工作,都是大公司,你将会构建能够影响数百万人的东西。那意味着什么呢?
实际上我不会告诉你这意味着什么,因为我不知道[1:05:34]。我要做的只是让你意识到一些问题,并让你相信其中两件事:
你应该关心。
这些是重大的当前问题。
我希望你关心的主要原因是因为我希望你想成为一个好人,并向你展示不考虑这些事情会让你成为一个坏人。但如果你不觉得有说服力,我会告诉你这个事实。大众汽车被发现在排放测试中作弊。最终被判刑的人是实施那段代码的程序员。他们只是按照指示去做。所以如果你认为自己只是一个技术人员,只是按照指示去做,那是我的工作。我告诉你,如果你这样做,你可能会因为听从指示而被送进监狱,所以 a)不要只是听从指示,因为你可能会成为一个坏人,b)你可能会被送进监狱。
第二件要意识到的事情是,在当下,你在工作中与二十个人开会,大家都在讨论如何实施这个新功能,每个人都在讨论[1:06:49]。每个人都在说“我们可以这样做,这是一种建模的方式,然后我们可以实施它,这是这些约束条件”,你心里有一部分在想我们是否应该这样做?那不是考虑这个问题的正确时机,因为在那时很难站出来说“对不起,我不确定这是一个好主意”。你实际上需要提前考虑如何处理这种情况。所以我希望你现在考虑这些问题,并意识到当你陷入其中时,你可能甚至意识不到正在发生什么。那只是一个像其他会议一样的会议,一群人在讨论如何解决这个技术问题。你需要能够认识到哦,这实际上是一个具有道德影响的事情。
Rachel 实际上写了所有这些幻灯片。很抱歉她不能在这里展示,因为她对这个问题进行了深入研究。她实际上曾身处困难环境,亲眼目睹这些事情发生,我们知道这是多么困难。但让我给你们一个了解发生的情况。
所以工程师试图解决工程问题并导致问题并不是一件新事。在纳粹德国,IBM,被称为 Hollerith 的集团,Hollerith 是 IBM 的原始名称,它来自于实际发明用于跟踪美国人口普查的打孔卡的人。这是世界上第一次大规模使用打孔卡进行数据收集。这变成了 IBM,所以在这一点上,仍然被称为 Hollerith。因此,Hollerith 向纳粹德国出售了一个打孔卡系统,每张打孔卡都会编码,比如这是犹太人,8,吉普赛人,12,用毒气室处决,6。这里有一张描述如何杀死这些不同人的卡片。因此,一名瑞士法官裁定 IBM 的技术协助促成了纳粹分子的任务,并犯下了反人类罪行。这导致了大约二千万平民的死亡。根据我从犹太虚拟图书馆得到这些图片和引用的观点,他们认为“犹太人民的毁灭变得更不重要,因为 IBM 的技术成就的振奋性质只会因为可以获得的奇幻利润而进一步提高”。这是很久以前的事情,希望你们不会最终在促成种族灭绝的公司工作。但也许你们会。
www.nytimes.com/2017/10/27/world/asia/myanmar-government-facebook-rohingya.html
www.nytimes.com/2017/10/24/world/asia/myanmar-rohingya-ethnic-cleansing.html
也许你们会去 Facebook,他们现在正在促成种族灭绝。我认识在 Facebook 工作的人,他们根本不知道他们在做什么。现在,在 Facebook,罗辛亚人正处于缅甸的种族灭绝中,他们是缅甸的一个穆斯林族群。婴儿被从母亲怀中抢走扔进火里,人们被杀害,数以百千计的难民。在接受采访时,进行这些行为的缅甸将军表示,我们非常感谢 Facebook 让我们知道“罗辛亚假新闻”,这些人实际上不是人类,他们实际上是动物。现在,Facebook 并不是要促成缅甸罗辛亚人的种族灭绝,不是。相反,发生的是他们想要最大化印象和点击量。结果是,Facebook 的数据科学家发现,如果你拿人们感兴趣的东西并向他们提供稍微更极端的版本,你实际上会得到更多的印象,项目经理们说要最大化这些印象,人们点击了,这就产生了这种情况。潜在的影响是非常巨大和全球性的。这实际上正在发生。这是 2017 年 10 月。正在发生。
问题:我只是想澄清这里发生了什么。所以这是在促进虚假新闻或不准确的媒体吗?是的,让我详细介绍一下,发生的事情是在 2016 年中期,Facebook 解雇了它的人类编辑。所以是人类决定如何在你的主页上排序的。这些人被解雇,被机器学习算法取代。所以这些机器学习算法是由像你们这样的数据科学家编写的,他们有清晰的指标,他们试图最大化他们的预测准确性,并且像“我们认为如果我们把这个东西放在比这个东西更高的位置,我们会得到更多的点击”。结果表明,这些用于在 Facebook 新闻源上放置内容的算法倾向于说“哦,人类的本性是我们倾向于点击那些刺激我们观点的东西,因此更极端版本的我们已经看到的东西”。这对于最大化参与度的 Facebook 收入模型来说是很好的,它在他们所有的关键绩效指标上看起来都很好。那时,有一些负面新闻,我不确定 Facebook 现在放在他们的热门部分的东西是否真的那么准确,但从人们在 Facebook 优化的指标的角度来看,这看起来很棒。所以回到 2016 年 10 月,人们开始注意到一些严重的问题。
例如,在美国,将住房针对某些种族是非法的。这是非法的,然而一个新闻机构发现 Facebook 在 2016 年 10 月确实在做这件事。再次强调,并不是因为数据科学团队中的某个人说“让黑人不能住在好社区”。而是,他们发现他们的自动聚类和分割算法发现了一群不喜欢非裔美国人的人,如果你用这种广告针对他们,那么他们更有可能选择这种住房或其他什么。但有趣的是,即使被告知了三次,Facebook 仍然没有解决这个问题。这就是说这些不仅仅是技术问题。它们也是经济问题。当你开始说你得到报酬的事情(也就是广告),你必须改变你构建这些方式的方式,这样你要么使用更多花钱的人,要么减少对少数群体或其他基于算法的人的针对性,这可能会影响收入。我提到这个原因是因为在你的职业生涯中,你会在某个时候发现自己在一次对话中,你会想“我不确定这在道德上是否可以接受”,而你正在与之交谈的人在心里想“这会让我们赚很多钱”,你永远不会成功地进行对话,因为你在谈论不同的事情。所以当你与比你更有经验和更资深的人交谈时,他们可能听起来像他们知道他们在谈论什么,只要意识到他们的激励并不一定会集中在如何成为一个好人上。就像他们并不是在想如何成为一个坏人,但在我看来,你在这个行业中花的时间越多,你对这些事情的麻木程度就越高,比如也许得到晋升和赚钱并不是最重要的事情。
例如,我有很多擅长计算机视觉的朋友,其中一些人已经创建了似乎几乎是为了帮助威权政府监视他们的公民而量身定制的初创公司。当我问我的朋友们,你们有没有考虑过这种方式的使用,他们通常会对我提出的问题感到有点冒犯。但我要求你们考虑这个问题。无论你最终在哪里工作,如果你最终创建了一个初创公司,工具可以被用于善良或邪恶。所以我并不是说不要创建优秀的计算机视觉目标跟踪和检测工具,因为你可以继续创建一个更好的外科手术干预机器人工具包。我只是说要意识到这一点,考虑一下,谈论一下。
所以我发现这个很有趣。这实际上是一个很酷的事情,meetup.com 做了。他们考虑到了这一点。他们实际上考虑到了这一点。他们认为你知道,如果我们建立一个像我们在课堂上学到的协同过滤系统来帮助人们决定去哪个 meetup。它可能会注意到在旧金山,比起女性,更多的男性倾向于参加技术 meetup,因此它可能会开始决定向更多男性推荐技术 meetup。结果是更多男性会参加技术 meetup,结果是当女性参加技术 meetup 时,她们会觉得哦,这里都是男性,我真的不想去技术 meetup。结果是算法会得到新的数据,表明男性更喜欢技术 meetup,所以继续下去。因此,算法的这种初步推动可能会产生这种恶性循环。最终,你可能会得到几乎全是男性的技术 meetup。因此,这种反馈循环是一个微妙的问题,当你考虑构建算法时,你真的要考虑到你正在改变的行为是什么。
所以另一个有点可怕的例子是在这篇论文中,作者描述了美国许多部门现在正在使用预测性执法算法。那么我们去哪里找即将犯罪的人。你知道,算法简单地反馈给你基本上你给它的数据。所以如果你的警察局过去曾经进行过种族歧视,那么它可能会稍微更频繁地建议你去黑人社区检查是否有人犯罪。结果是你的警察会更多地去黑人社区,结果是他们逮捕更多黑人,结果是数据显示黑人社区更不安全。结果是算法告诉警察,也许你应该更频繁地去黑人社区,依此类推。
这不像是未来可能发生的模糊可能性。这是来自认真研究数据和理论的顶尖学者的记录工作。这些严肃的学术工作表明,不,这正在发生。再次,我相信开始创建这种预测性执法算法的人并没有想过如何逮捕更多黑人。希望他们在想,天哪,我希望我的孩子在街上更安全,我该如何创造一个更安全的社会。但他们没有考虑到这种恶性循环。
关于社交网络算法的这篇文章实际上是最近《纽约时报》上我一个朋友 Renee Diresta 写的,她做了一些令人惊讶的事情。她建立了一个第二个 Facebook 账号,一个假账号。那时她对反疫苗运动非常感兴趣,所以她开始关注一些反疫苗者,并访问了一些反疫苗链接。突然间,她的新闻动态开始充斥着反疫苗者的新闻,以及其他一些东西,比如化学尾迹和深层国家阴谋论等等。于是她开始点击这些内容,她点击得越多,Facebook 推荐的极端离谱的阴谋论就越多。所以现在当 Renee 去那个 Facebook 账号时,整个页面都充满了愤怒、疯狂、极端的阴谋论内容。这就是她看到的一切。所以如果这是你的世界,那么在你看来,这就像是一个不断提醒和证明所有这些东西的连续。所以再次强调,这种失控的反馈循环最终告诉缅甸将军们,他们的 Facebook 主页上罗兴亚人是动物、假新闻,以及其他一切。
很多这些也来自偏见。所以让我们具体谈谈偏见。图像软件中的偏见来自数据中的偏见。所以我认识的大多数在谷歌大脑构建计算机视觉算法的人,很少有人是有色人种。所以当他们用家人和朋友的照片来训练算法时,他们用的是很少有色人种的照片。所以当 FaceApp 决定我们要尝试查看很多 Instagram 照片,看看哪些被点赞最多时,他们并没有意识到,答案是浅色面孔。所以他们建立了一个生成模型让你看起来更“性感”,这是实际照片,这是更性感的版本。所以更性感的版本更白,鼻孔更少,看起来更欧洲化。这显然不受欢迎。再次强调,我不认为 FaceApp 的任何人会说“让我们创造一些让人看起来更白的东西”。他们只是用周围的人的一堆图像来训练它。这也有严重的商业影响。他们不得不撤回这个功能。他们受到了大量的负面反弹,这是应该的。
这里有另一个例子。谷歌照片创建了这个照片分类器,飞机、摩天大楼、汽车、毕业典礼和大猩猩。所以想想这对大多数人来说是什么样子。对大多数人来说,他们看到这个,他们不了解机器学习,他们会说“搞什么鬼,谷歌的某个人写了一些代码把黑人称为大猩猩”。这就是它看起来的样子。我们知道那不是发生的事情。我们知道发生的是谷歌计算机视觉专家团队中没有或只有少数有色人种的人建立了一个分类器,使用他们可以获得的所有照片,所以当系统遇到皮肤较黑的人时,它会认为“哦,我之前主要只见过大猩猩中的这种情况,所以我会把它放在那个类别中”。再次强调,数据中的偏见会导致软件中的偏见,商业影响也非常显著。谷歌因此受到了很多负面公关影响,这是应该的。有人在他们的 Twitter 上发布了这张照片。他们说看看谷歌照片刚刚决定做的事情。
你可以想象第一届由人工智能评选的国际选美大赛发生了什么。基本上结果是所有美丽的人都是白人。所以你可以看到这种图像软件中的偏见,这要归功于数据中的偏见,也要归功于构建团队缺乏多样性。
在自然语言处理中也可以看到同样的情况。这里是土耳其语。O 是土耳其语中没有性别的代词。没有他和她之分。但当然在英语中,我们并没有一个广泛使用的无性别单数代词,所以谷歌翻译将其转换为这个。现在有很多人在网上看到这个并且字面上说“那又怎样?”它正确地反馈了英语中的通常用法。我知道这是如何训练的,就像 Word2vec 词向量一样,它是在谷歌新闻语料库、谷歌图书语料库上训练的,它只是告诉我们事情是如何的。从某种角度来看,这是完全正确的。用来创建这种有偏见算法的有偏见数据实际上是人们几十年来写书和报纸文章的实际数据。但这是否意味着这是你想要创建的产品?这是否意味着这是你必须创建的产品?仅仅因为你训练模型的特定方式导致它最终做出这样的结果,这真的是你想要的设计吗?你能想到这可能会产生什么负面影响和反馈循环吗?如果这些事情中的任何一件让你感到不安,那么现在,幸运的是,你有一个新的很酷的工程问题要解决。我如何创建无偏见的自然语言处理解决方案?现在有一些初创公司开始做这个,并开始赚钱。所以这对你来说是机会。就像嘿,这里有一些人因为他们糟糕的模型而创造出了扭曲的社会结果。你可以去建立一些更好的东西。Word2vec 词向量中的偏见的另一个例子是餐厅评论中排名较低的墨西哥餐厅,因为“墨西哥”这个词在美国新闻和书籍中更常与犯罪相关的词语联系在一起。同样,这是一个正在发生的真实问题。
Rachel 实际上对普通的 Word2vec 词向量进行了一些有趣的分析。她基本上把它们拿出来,根据一些在其他地方进行的研究来看这些类比。所以你可以看到,Word2vec 向量方向显示父亲对医生就像母亲对护士一样。男人对计算机程序员就像女人对家庭主妇一样,等等。所以很容易看出这些词向量中包含了什么。它们在我们今天使用的几乎所有自然语言处理软件中都是基础的。
这里有一个很好的例子。ProPublica 实际上在这个领域做了很多有益的工作。现在许多法官都可以访问判决指南软件。因此,判决指南软件告诉法官对于这个个体,我们建议这种类型的刑罚。当然,法官不懂机器学习,所以他们有两个选择,要么按照软件的建议行事,要么完全忽视它,有些人属于每个类别。对于那些属于按照软件建议行事的人,这里是发生的事情。对于那些被标记为高风险的人,实际上没有再次犯罪的人中,白人约四分之一,非裔美国人约一半。所以,那些没有再次犯罪的人中,如果是非裔美国人,他们被标记为高风险的情况几乎是白人的两倍。而那些被标记为低风险但实际上再次犯罪的人中,白人约一半,非裔美国人只有 28%。所以这是数据,我希望没有人会刻意创造这样的情况。但是当你从有偏见的数据开始,数据显示白人和黑人吸食大麻的比率大致相同,但黑人被监禁的频率大约是白人的 5 倍,美国的司法系统的性质,至少目前来看,是不平等的,不公平的。因此,输入到机器学习模型中的数据基本上会支持这种现状。然后由于负面反馈循环,情况只会变得越来越糟。
现在我要告诉你关于这个有趣的事情,研究员阿贝·龚指出了一些问题。以下是一些正在被问到的问题。所以让我们来看一个。你的父亲是否曾被逮捕过?你对这个问题的回答将决定你是否被关押以及关押多久。现在作为一个机器学习研究员,你认为这可能会提高你算法的预测准确性并获得更好的 R²。可能会,但我不知道。也许会。你试试看,哦,我有了更好的 R²。那么这意味着你应该使用它吗?还有另一个问题。你认为因为一个人的父亲是谁而把他关押更长时间是合理的吗?然而,这些实际上是我们现在问罪犯的问题的例子,然后将其放入一个机器学习系统中来决定他们的命运。再次,设计这个的人可能他们是专注于技术卓越,获得 ROC 曲线下面积的最大值,我发现了这些很好的预测因子,让我又多了 0.02。我猜他们没有停下来思考,这样决定谁应该更长时间被关押是否合理。
因此,将这些放在一起,你可以看到这会变得越来越可怕。我们以 Taser 这样的公司为例,Taser 是一种会给你一个大电击的设备,Taser 设法与一些学术研究人员建立了良好的关系,这些研究人员似乎会说他们要他们说的话,以至于现在如果你看数据,结果表明如果你被电击,你可能会死亡的概率相当高。这并不罕见。然而,他们支付给研究这个问题的研究人员一直回来说“哦不,这与电击无关。他们之后立即死亡完全无关。这只是一个偶然事件,事情发生了”。因此,这家公司现在拥有 80%的身体摄像机市场份额。他们开始收购计算机视觉 AI 公司。他们将尝试使用这些警察身体摄像机视频来预测犯罪活动。那意味着什么?这就像是我现在有一些增强现实显示,告诉我电击这个人,因为他们即将做一些坏事?这是一个令人担忧的方向,所以我确信没有人在 Taser 或他们收购的公司中的数据科学家会认为这是他们想要帮助创造的世界,但他们可能会发现自己或你可能会发现自己处于这种讨论的中心,虽然这不是明确讨论的话题,但你内心可能会有一部分在想“我想知道这可能会被如何使用”。我不知道在这种情况下应该做什么才是正确的,因为你可以询问,当然人们会说“不不不”。那你能做什么?你可以要求一些书面承诺,你可以决定离开,你可以开始研究事情的合法性,比如说,至少保护自己的法律情况。我不知道。想一想你会如何应对。
这些是 Rachel 提出的一些问题,作为需要考虑的事情。如果你正在考虑构建一个数据产品或使用模型,如果你正在构建机器学习模型,那是有原因的。你正在尝试做某事。那么数据中可能存在什么偏见?因为无论数据中存在什么偏见,最终都会成为你预测的偏见,潜在地影响你正在影响的行动,潜在地影响你返回的数据,你可能会得到一个反馈循环。
如果构建它的团队不够多样化,你可能会错过什么?例如,Twitter 的一位高级执行官在选举之前很久就警告了 Twitter 存在严重的俄罗斯机器人问题。那时 Twitter 高管团队中唯一的黑人。唯一的一个。不久之后,他们失去了工作。因此,拥有一个更多样化的团队意味着拥有更多样化的观点、信仰、想法和寻找的事物等等。因此,非多样化的团队似乎会犯更多这样的错误。
我们能审计代码吗?它是开源的吗?检查不同群体之间的不同错误率。我们是否可以使用一个极易解释和易于沟通的简单规则?如果出了问题,我们是否有一个处理问题的好方法?
当我们与人们谈论这个问题时,很多人都去找 Rachel 说我对我的组织正在做的事情感到担忧,我该怎么办[1:36:21]?或者我只是担心我的 toxic workplace,我该怎么办?而 Rachel 往往会说你有考虑过离开吗?他们会说我不想失去工作。但实际上,如果你会编程,你是整个人口的 0.3%。如果你会编程和做机器学习,你可能是整个人口的 0.01%。你是极其极其受欢迎的。所以实际上,显然一个组织不希望你觉得自己可以随时离开找另一份工作,这不符合他们的利益。但这绝对是真的。所以我希望你们在这门课程中能够获得足够的自信,认识到你有能力找到工作,特别是一旦你有了第一份工作,第二份工作就会容易很多。所以这很重要,不仅是为了让你觉得你实际上有能力去做出道德行为,而且也很重要意识到如果你发现自己处在一个 toxic 环境中,这是相当普遍的不幸。在湾区尤其存在很多糟糕的科技文化/环境。如果你发现自己处在这样的环境中,最好的做法就是赶紧离开。如果你没有自信认为自己可以找到另一份工作,你就会被困住。所以这真的很重要。很重要的是要知道你在结束这个项目时拥有非常受欢迎的技能,特别是在你有了第一份工作之后,你现在是一个拥有受欢迎技能和在该领域就业记录的人。
问题:这只是一个广泛的问题,你知道人们正在做一些什么来处理数据中的偏见吗[1:38:41]?你知道,这目前是一个有争议的话题,有人试图使用算法方法,他们基本上试图说我们如何识别偏见并将其减去。但我知道的最有效的方法是试图在数据层面上处理它。所以从一个更多元化的团队开始,特别是一个团队包括来自人文学科的人,比如社会学家、心理学家、经济学家,了解反馈循环和对人类行为的影响,他们往往配备了识别和跟踪这类问题的良好工具。然后尝试将解决方案纳入到过程中。但我无法告诉你有一个标准的处理方法,告诉你如何解决它。如果有这样的东西,我们还没有找到。简短的答案是,需要一个多样化的聪明团队意识到问题并努力解决。
评论:这只是针对整个班级的一个普遍性建议,如果你对这些感兴趣,我读过一本很酷的书,Jeremy 你可能听说过,Cathy O’Neil 的《Weapons of Math Destruction》。它涵盖了很多相同的内容[1:40:09]。是的,谢谢你的推荐。Kathy 很棒。她还有一个 TED 演讲。我没能完成这本书。它太令人沮丧了。我只是说“不要再看了”。但是,它确实很好。
好的。就这样了。谢谢大家。对我来说,这真的很紧张。显然,这原本是我和 Rachel 分享的事情。所以我最终做了我一生中最困难的事情之一,那就是独自教授两个人的课程,照顾生病的妻子和一个幼儿,还要学习深度学习课程。而且还要用我刚写的新库来完成所有这些。所以我期待着能好好休息一下。但这一切都是完全值得的。因为你们太棒了。我对你们对我给予的机会作出的反应以及我给予你们的反馈感到非常高兴。所以恭喜。