fast.ai 机器学习笔记(四)(1)https://developer.aliyun.com/article/1482643
基线和二元组:简单、良好的情感和主题分类
这种技术最初是在 2012 年提出的。Chris Manning 是斯坦福大学出色的自然语言处理研究人员,而 Sida Wang 我不认识,但我认为他很棒,因为他的论文很棒。他们基本上提出了这个想法。他们所做的是将其与其他方法在其他数据集上进行比较。其中一件事是他们尝试了 IMDB 数据集。这里是大二元组上的朴素贝叶斯 SVM:
正如你所看到的,这种方法胜过了他们研究的其他基于线性的方法,以及他们研究的一些受限玻尔兹曼机的神经网络方法。如今,有更好的方法来做这个,事实上在深度学习课程中,我们展示了我们在 Fast AI 刚刚开发的最新成果,可以达到 94%以上的准确率。但是特别是对于一种简单、快速、直观的线性技术来说,这还是相当不错的。你会注意到,当他们这样做时,他们只使用了二元组。我猜这是因为我看了他们的代码,发现它相当慢且难看。我找到了一种更优化的方法,正如你所看到的,所以我们能够使用三元组,因此我们得到了更好的结果,我们的准确率是 91.8%,而不是 91.2%,但除此之外,它是相同的。哦,他们还使用了支持向量机,在这种情况下几乎与逻辑回归相同,所以有一些细微的差异。所以我认为这是一个相当酷的结果。
我要提一下,在课堂上你看到的是我经过多周甚至多个月的研究得出的结果。所以我不希望你认为这些东西是显而易见的。完全不是。就像阅读这篇论文,论文中没有描述为什么他们使用这个模型,它与其他模型有何不同,为什么他们认为它有效。我花了一两周的时间才意识到它在数学上等同于普通的逻辑回归,然后又花了几周的时间才意识到区别实际上在于正则化。这有点像机器学习,我相信你从你参加的 Kaggle 竞赛中已经注意到了。就像你提出了一千个好主意,其中 999 个无论你有多么自信它们会很棒,最终都会变成垃圾。然后最终在四周后,其中一个终于奏效,给了你继续度过另外四周的痛苦和挫折的热情。这是正常的。而且我可以确定,我所认识的机器学习领域最优秀的从业者都有一个共同的特点,那就是他们非常顽强,也被称为固执和执着,这绝对是我似乎拥有的声誉,可能是公平的,还有另一点,他们都是非常擅长编码的。他们非常擅长将他们的想法转化为新的代码。对我来说,几个月前通过这个工作是一个非常有趣的经历,试图至少弄清楚为什么这个当时的最新成果存在。
更好的版本:NBSVM++ [43:31]
所以一旦我弄清楚了,我实际上能够在此基础上进行改进,并且我会向你展示我做了什么。这就是我非常幸运能够使用 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。
嵌入[1:05:17]
我想谈谈嵌入。我说假装它是线性的,实际上我们可以假装它是线性的。让我向你展示我们可以多么地假装它是线性的,就像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 号码,你不能将其视为分类变量。因为那将是一个巨大的嵌入矩阵,而且每样东西只出现一次,或者是距离最近商店的公里数到小数点后两位,你也不会将其作为分类变量。
这是他们在这次比赛中都使用的经验法则。事实上,如果我们滚动到他们的选择,这是他们的做法:
他们的连续变量是真正连续的东西,比如到竞争对手的公里数,温度等。而其他一切,基本上,他们都视为分类变量。
今天就到这里。下次,我们将结束这个话题。我们将看看如何将这个转化为神经网络,并总结一下。到时见!
机器学习 1:第 12 课
原文:
medium.com/@hiromi_suenaga/machine-learning-1-lesson-12-6c2512e005a3
译者:飞龙
来自机器学习课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 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
是一个很好的练习方法。
fast.ai 机器学习笔记(四)(3)https://developer.aliyun.com/article/1482646