这是目前为止我们在这个课程里学到的主要的东西。我们学习了包含有仿射函数组成的三明治夹层(就是矩阵相乘,更通用的叫法),夹层里还有像ReLU这样的非线性运算的神经网络。我们学习了这些计算的结果叫激活值。在计算中,模型需要学习的内容,称之为参数。参数是随机初始化的,或者是从预训练模型里复制过来的,
然后我们用SGD或更快的优化算法训练它们。我们学到了卷积函数是一种仿射函数,它对自相关数据(auto correlated data),比如图片之类的,效果很好。我们还学了batch norm、dropout、 data augmentation、weight decay作为正则化的方法。同时,batch norm也有助于提高模型的训练速度,今天我们学习了Res/dense模块,
然后,今天,我们学了Res/Dense blocks。我们学了很多关于图片分类、回归、嵌入、分类变量和连续变量、协同过率、语言模型、NLP分类的东西。还有图像分割、U-Net和GAN。
好好复习这些类容,确保你对它们很很熟悉。如果你只看了一遍这些视频,你肯定没有熟悉。人们一般要看三遍才能真正理解这些细节。
递归神经网络Recurrent Neural Network (RNN) [1:38:31]
一个还没有学的是RNN。这是最后一个要学的东西。我要用图示的方法来解释RNN。我先给你们看一个只有一个隐藏层的基本的神经网络。
矩形代表一个输入,它的输入尺寸是批次数量(batch_size) x Inputs的数量。箭头代表一个神经网络,比如说一个矩阵乘法,后面跟着ReLU。一个圆圈代表一个激活值。这种情况下,我们只有一组隐藏层的激活值。考虑到单个样本的输出,就是输入的数目。这里(右边第一个箭头)
是一个矩阵,尺寸是Input的数量 x activations的数量。因此,输出的尺寸是batch_size x activations的数量。
知道怎样计算形状是很重要的。通过learn.summary()
查看所有变量/输入/输出的维度。
然后,这里有另一个箭头,这代表它是另外一个层,点乘矩阵加上非线性项(此处不是Relu)。这里,考虑到下一层是输出,所以我们非线性项用的softmax。
三角形代表一个输出。这个点乘矩阵的尺寸是激活值数量x类别数量,所以我们的输出的尺寸是批次数量x类别数量。
再用一次这些符号。
三角形是输出,圆形是激活值(我们也叫它隐藏状态hidden state),矩形是输入。我们来想象我们下,我们要把一个很大的文档拆分成每3个单词一组的集合,对每个集合,用前两个单词,预测第三个单词。如果我们现在有这个数据集,我们可以:
- 把第一个单词作为输入
- 让它经过一个embedding,创建一些激活值
- 让它经过一个矩阵乘法和非线性计算
- 取第二个单词
- 让它经过一个embedding
- 然后我们可以把这两个东西加在一起,或者连接在一起(concatenate)。一般来说,你见到两组激活值在一个图里走到一起,你一般可以选择连接或者相加。这会创建生成一组新的激活值。
- 然后你可以将其送入全连接层,再用softmax函数(归一化函数)来产生一个输出
这就是一个完全标准的全连接的神经网络,多了一个小东西,就是在这里做了连接或者相加,我们要用这个网络来通过前两个单词预测第三个单词。
记住,箭头代表层操作,我在这里删除了细节。因为这里一定是一个仿射函数和一个非线性计算。
再多做些。如果我们要用前3个单词预测第4个单词,要怎样做呢?就是在上次的图片基础上,加一个额外的输入和一个额外的圆圈。
但是我要说的一点是,每次我们从矩形走到圆形中间的过程,我们在做相同的事情,我们在做embedding。就是一种特别的矩阵乘法,其输入是one-hot编码的输入。每次从圆形到圆形的过程,基本上就是取到一个隐藏状态(激活值),并将其变成另一组激活值的过程。这个过程完成后即处于下一词,从圆形到三角形的过程,做的事又不同,是将隐藏状态。激活值转换成输出的过程。因此,我最好还是把这些箭头用不同的颜色标示出来,每个箭头都很可能代表相同的权重矩阵。因为它们做的都是同样的事。那么为什么对每个词都要有不同的嵌入。或者与不同的权重相乘。从这个隐藏状态到这个隐藏状态再到这个,这就是我们要打造的东西。
Human numbers [1:43:11]
现在我们来看下human numbers(手写数字识别),它在lesson7-human-numbers.ipynb里。这是一个我创建的数据集,里面包含了用英文写的从1到9,999的数字。
我们要创建一个可以预测这个文档里下一个单词的语言模型。这只是一个简单示例。这里我们的例子中只有一个文档,内容是一个数字列表。因此我们可以使用一个TextList
来创建用于训练和验证的文本列表。
这里验证集是8,000之后的数字,训练集是1到8,000。我们可以把它们合并,转成一个data bunch。
这样我们就只有一个文档,train[0]通过.text得到文本列表的内容。这是它的前80个字符,以特殊标记(special token)的'xxbos'开头。所有以xx开头的是fastai 的特殊token,bos(beginning of stream)是stream的起始标记。它的意思是说这是文档的起点,在NLP里,知道文档在哪开始很有用,这样你的模型知道从哪里开始识别。
验证集有13,000个token。就是13,000个单词或者标点符号,因为每个用空格隔开的部分都是一个标记。
我们设的批次大小(batch size)是64,这里有个属性叫bptt,默认值是70,bptt,我们简单提过‘bptt’ (“back prop through time”)。它的意思是随时间反向传播,即序列的长度。因此,对64个文档片段中的每一个,我们把它分成70个单词为一组分开,每次处理一组。
我们要做的就是,取出含13,000个token的整个字符串作为验证集,然后将其分成64个大小差不多的部分。人们经常把这个理解错。我不是说“各部分的长度是64”,不是这样。它们是“64个长度差不多的片段”。因此,文档的第一个1/64的片段是第一部分,第二个1/64的分段是第二部分,然后对于64个分段中的每一个,再将其分成长度为70个词的小片,因此每一批,对于13000个标记,一共可以分成多少批?总数除以批大小,再除以70,大约是2.9批,即3批。让我们用一个迭代器(iter)做数据加载。取第1,2,3批数据,包括x和y。
我们把3批数据的元素个数加起来,得到一个略低于总个数(13017)的值,因为分了3批之后,最后还剩一点。但剩下的又不足以成为新的一批。你以后会经常遇到这一类的东西,各种形状、大小、内容、迭代。你可以看到,这里是95×64,之前说过是70×64
这是因为语言模型的数据加载器,将bptt(序列)稍微打乱一下。做了一下洗牌,使数据更加随机化,这有利于模型。这里你可以看到第一批中的x,记住,所有这些英文文本已经被数值化了,这是第一批的y。你可以看到x1是[2,18,10,11,8.....],y1是[18,10,11,8...]
y1与x1有一位数字的偏移,因为这正是语言模型所要求的的,我们要预测下一词。因此2之后是18,18之后应该是10.你可以得到数据集的vocab属性,
vocab有一个textify方法(文本化),所以如果我们用textify查看同样的内容,它会去vocab中查找相应的文本。在这里你可以看到xxbos eight thousand one,y里没有了'xxbos',只是eight thousand one
。所以xxbos
之后是eight
,之后是thousand
,之后是one
。在eight thousand twenty three
之后,就是x2,注意这里,我们总是看第0列,这代表着第一个批次(第一个mini batch),之后是eight thousand twenty four
,然后是x3,一直到eight thousand forty
。
然后早从头开始,但是取第一列,也就是第二批(x1[1])。现在我们继续。这里从eight thousand forty
跳到eight thousand forty six
,这是因为上一个mini-batch不完整。这意味着每一个mini-batch 会与之前的mini-batch连接,这样可以直接从x1[0]过渡到x2[0],每个mini batch和前一个mini batch连接在一起。你可以直接从x1[0]到x2[0],继续eight thousand twenty three
、eight thousand twenty four
。第一列也是如此,你会看到前后也是互相连接的。所有的mini-batch都是相互连接的。
这是数据,我们可以用show_batch来查看它。
这是我们的模型,它做了我们在图里看到的事情:
把代码拷到了这里:
它包含一个embedding(绿色箭头),一个隐藏层到隐藏层的过程(棕色箭头),一个隐藏状态到输出的过程。每个标颜色的箭头都对应一个矩阵。在forward过程中,我们取第一个输入x[0]
,让它通过输入到隐藏状态层(绿色箭头),得到第一组激活值h。假设还有第2个单词,因为有时在一批的结尾可能就没有第2个词了。这里我们假设有第2个词,把它加到h上,x[1]的结果,完成绿色的箭头(i_h
)。然后我们可以说,好了,我们的新h是这两个相加的结果,完成从隐藏层到隐藏层的橙色箭头,然后ReLU激活,然后batch norm。然后,对这第二个单词,做相同的事情。最后蓝色箭头调用h_o()
。
这就是怎样把图转成代码。没有什么新东西。现在让我们来做,我们可以在学习器里验证,得到训练到46%的准确率。现在来看看这个代码,其实很糟糕,有很多重复的代码。作为程序员,看到重复的代码时,怎么办?我们要对代码进行重构,把这一段写成循环,现在写成了循环的形式,现在遍历x
中的每个xi
。猜猜看,这就是RNN了,RNN只是重构,不是什么新东西,现在这就是一个RNN,
现在将这个结构图重构为这个结构图,
这是相同的结构图, 我只是用循环代替了它。它们做同样的事,这是代码。
它的init()函数也一样,完全一样,只是这里出现了循环,开始前,我必须确保我有一堆零作为初始值。当然,训练出的结果是一样的,
接下来,你可能会想到,循环的一个好处是,现在代码可以在不同的情况下运行,即使我不是从前三个单词预测第四个单词,而是从前八个单词预测第九个 ,也没有问题,任意长度的序列都可以,让我们将bptt提高到20,现在可以这样做了。
然后,不仅仅是从前N-1个词预测第N个词,让我们试着从第一个词预测第二个词,从第三个词预测第四个词,以此类推。因为,之前,看一下损失函数,之前模型结果比较的对象只是序列的最后一个单词,这很浪费,因为序列里有很多个单词。我们来把x的每个单词和y里的每个单词做比较。要做到这个,我们要改变原来的结构图,
循环的最后不再是一个三角形,现在三角形在循环中。换句话说,每个循环之后,预测,再循环,预测,循环,预测.....
代码在这里,和之前的代码一样,但我创建了一个数组,每经过一个循环,我把h_o(h)
的结果添加到数组末尾,n个输入,我得到n个输出,我预测了每个单词,
之前的准确率是39%,现在是34%。为什么变差了?因为我在预测第二个单词时,只有一个单词可以用。当我预测第三个单词时,有两个单词的状态可以用。这是一个更难的解决的问题。一个明显的解决办法是,关键在这里:
这一步每次将状态重置为0,开始了另一个bptt序列,现在不这样做,我们保留h,然后就可以把每个批次连接到上一个batch,不想图像分类那样,发生了重排。我们取原来的模型,复制,但是把h的创建转移到构造函数里。
就像这样,它现在是self.h,
是完全相同的代码,只是最后h改成了self.h,两段代码做着同样的事,它现在并没有抛弃中间的状态,所以现在结果变好了,最后的准确率达到了44%。
这就是RNN。你总是希望保留中间的状态。但要记住,RNN没什么特别的,它就是一个正常的全连接神经网络,只是你重构了一个循环。
在每个循环最后,你可以不单单输出一个结果,你可以把它输出到另外一个RNN里。
你可以从一个RNN进到另外一个RNN。这很好,因为我们有了更多层来计算,它的效果预期会更好。
要做到这个,我们要再重构更多代码。我们取Model3
的代码,用PyTorch内置的等效代码替换它,可以这样写:
nn.RNN
就是做循环。我们还有相同的embedding、相同的输出、相同的batch norm、相同的h初始化,但是没有了循环。RNN的一个好处是你现在就可以定义你想要多少层。当然,得到的准确率是一样的。在这里,我准备用2层,
但要注意,这个图,没有循环的版本:
我们继续把BPTT设为20,所以这里有20层。我们从可视化函数可视化的论文里知道,深度网络有着糟糕的凹凸不平的的损失表面。所以,当你创建很长的时间尺度和很多层的神经网络时,训练变得不可能了,你可以用一些技巧,其中之一就是添加跳过连接。
但人们经常不再单单把这些(绿色箭头和橙色箭头)加在一起,而是用一个mini神经网络决定要保持多少绿色的箭头,多少橙色的箭头。当你这样做时,你得到了叫GRU或LSTM的东西,至于是哪个,这取决于这个小网络的细节。我们会在课程第二部分学习这些网络的细节。说实话,它们不是很重要。
现在,我们可以创建一个GRU来替代。它的功能和我们之前的很像,只是它可以在更深的网络里处理更长的序列。我们用两层试试。
哇,准确率到了75%,这就是RNN。我讲它的主要原因,是要揭开最后的一个神秘的东西。这是深度学习里最简单的魔法之一。它只是重构了一个全连接网络。不要让RNN吓倒你。使用它时,你有一个输入序列,一个输出序列,长度都为n,我们将其用于语言模型,也可以用于其他任务,例如,输出序列可以是,对于每个单词,输出可能是是否有敏感的东西,而决定是否做匿名化。比如说,这是否是隐私数据,也可能是这个词的词性标注。或者可能是这个词该如何构词,等等。这些被称为序列标注任务。你可以使用相同的方法,解决几乎所有的序列标注任务,你也可以完成我之前的课里做的,构建一个语言模型,你可以去掉h_o
,用一个标准的分类器,然后就可以做自然语言处理分类了。就像前面看到的,得到最领先的结果,甚至在长文件上也行。这是一种非常有价值的技术。而且完全不神秘。
这就是深度学习,至少在我看来实用的部分就是这些。只看一遍,你不会全部掌握。我不建议你第一次看得很慢,在第一遍就全部掌握。你可以回过来再看一遍,慢慢来,有些地方你可能回觉得“噢,我现在知道他在讲什么了”。然后,你可以实现你以前做不到的东西,你可以比以前更深入。一定要再看一遍。并且,看的同时要写代码,不只是自己写,还要把它提交到github上。无论你是不是觉得它们是好代码。你写代码并且分享它这件事本身就是有巨大意义的,如果你在论坛上告诉人们,“嘿,我写了这些代码。它不是很棒,但这是我的第一次努力。你们看有什么问题吗” 。人们会说“噢,这里做得很好。但是,这里,你可以用这个库,这会省些时间”。你会从同伴那里学到很多。
你们已经注意到了,我开始介绍越来越多的论文。课程第二部分会有很多论文,现在可以开始阅读一些已经介绍过的论文。对那些推导、定理、引理之类的东西,你可以跳过它们。我就是这样的。它们对你做深度学习实践来说没有什么影响。那些说为什么我们要解决这个问题、这些结果是什么之类的部分就非常有意义。然后试着写些文章,
不是用来给Geoffrey Hinton和Yann LeCun看的文章,是你希望你自己写下并阅读的英文文章,就像你在六个月前写的那样。因为六个月前,你的读者中有比像Geoffrey Hinton和Yann LeCun这样的读者更多的人。这些人是你最了解的。你知道他们需要什么。
去寻求帮助,也帮助别人。告诉我们你成功的故事。可能最重要的是和其它人一起学。如果有社区交流,人们会学得更好。可以办一个读书聚会,参与meetup,创建学习小组,做些项目。还是一样,这些不需要做出特别令人惊奇的成绩。只需要做些你觉得能让世界变得更好些的东西,或者你觉得能让你两岁的孩子看到后能开心的东西,或者在你的兄弟来看你在做什么时,你想展示给他们的东西,等等。去做一些东西,把它做完,然后再试着让它更好些。
比如我今天下午看到的Elon Musk的推文生成器。它读了很多老的推文,创建了一个Elon Musk的语言模型,然后产生这些新的推文,比如 "Humanity will also have an option to publish on its own journey as an alien civilization. it will always like all human being." "Mars is no longer possible," "AI will definitely be the central intelligence agency."
这很棒,我喜欢这个。我喜欢Dave Smith写的“这是我第一次提交。感谢在八周里教会一个做金融的人怎样构建一个app”。我觉得这很棒。我觉得这个项目会受到很多热心的关注。这会改变未来的社会发展方向吗?大概不会。但Elon Musk可能会看到这个,然后想“噢,可能我要重新考虑我说话的方式了”。我觉得这很棒。所以,去创建一些东西,提交到这里,花点时间去做它。
或者参与到fastai里来。这个fastai项目,有很事情要做。你可以帮助写文档、做测试,这些可能听起来有点无聊。但你会惊奇地发现这并不无聊。拿一段没有文档的代码,研究它,理解它,在论坛上问Sylvain和我,发生了什么?为什么这样写?我们会发给你我们在实现的论文。编写一个测试需要深刻地理解机器学习里的这部分内容来知道它应该怎样运行。这是很有意思的。
Stats Bakman 创建了这个很好的Dev Projects Index,你可以到论坛上fastai开发项目板块,找到正在进行的,你想参加的项目。
创建一个学习小组。Deena已经在一月创建了一个旧金山学习小组,创建学习小组很简单,到论坛里,找到你所在时区的板块,添加一个文章,说“我们来成立一个学习小组吧”。但是要给人们一个Googel sheet之类的东西来登记,真正做些事情。
一个很好的例子是Pierre,他在巴西组织了上一期课程的学习小组,做得很好。他不断贴出人们一起学习深度学习、创建wiki、创建项目的照片,很棒的经历。
在课程第二部分,我们会学习所有这些有趣的东西。在实践上,深入fastai代码,理解我们是怎样构建它的。我们会过一遍代码。我们构造它们的时候,在每个阶段,都创建notebook来学习我们在做什么,我们会看到软件开发过程。我们会讲做研究的过程,怎样读学术论文,怎样把它从数学符号变成代码。然后是一堆各种类型的没有学过的模型。这会从深度学习实践进入到实际的前沿研究。
提问(Ask Jeremy Anything) [2:05:26]
我们在线上有AMA(ask me anything)活动,我们有时间讲几个社区提问最多的问题。
提问:第一个是Jeremy的请求,尽管它不是被提问最多的问题。你的典型的一天是怎样的,你怎样在这么多事情上分配时间?
我总是听到这个问题,所以我觉得我需要回答下,有些人为这个投票了。来到我们学习小组的人总是会被我的没有条理和没有效率震惊,我总是听到人们说“哇,我以前觉得你是深度学习的模范,我想看看怎样能变得像你一样,现在,我不太确定你是什么样的人了”。对我来说,这与你做得是否开心有关,我没有做过很多计划。我只是坚持去完成我开了头的工作。如果你得不到乐趣,就很难继续下去,因为在深度学习里有很多挫折,这不像写一个web app,就是做授权、检查、后台服务检查、用户凭证、检查,你在做流程。另外一边,对GAN这样的东西,是这样的:它没有效果、它没有效果、它没有效果、它还是没有效果、它还是没有效果,直到“哦,天,这太神奇了。这是一只猫”。它是这样的东西。我们没有那么规律的反馈。所以,你需要对这有兴趣。所以,我的日子有点,你明白的,另外,我不参加什么会议,我不打电话,我不喝咖啡,我不看电视,我不玩电脑游戏。我花很多时间陪家人,花很多时间锻炼,花很多时间阅读、写代码、做我喜欢的事。主要就是去把事情做完,彻底地完成它。你完成了80%,但还没有创建一个README,安装过程还有点麻烦,github上99%的项目都是这样。你会看到README里写着:“TODO:完成基线实验文档......”。不要做这样的人。彻底完成一些东西,可以和一些人一起做,把它做完。
提问:什么是最让你兴奋的、有前途的深度学习/机器学习的东西?你去年讲过你不是强化学习的粉丝。现在你还是这样觉得吗?
和三年前开始做这个课程时一样,我还是认为是那些都是关于迁移学习。它的价值没有被充分认识,还没有被充分研究。每次我们把迁移学习用到其他东西时,它就会变好很多。我们在NLP上用迁移学习的论文改变了NLP发展的方向,被《纽约时报》报道了,只是一件不值一提的小事。那是我们一起拼凑到一起的文章。所以我仍然对迁移学习感到兴奋。我对强化学习仍然没有兴趣 。我没有看到它被普通人用在普遍的任务上。对那些可以被很简单很快速解决的问题,它是一个难以置信的低效的方法。它会有它擅长的领域,但是不会用到大多数人日常工作中。
提问:对于要参加2019年第二部分课程的人,在课程开始前,你建议做什么学习实践呢?
就是写代码。是的,就是一直写代码。我知道这完全可行,我听到有人学到这里还没有写任何代码。如果你是这样的,那没关系。你只要再学一遍,这次要写代码。看看输入的形状,看看输出,要知道怎样取一个mini batch,看看它的平均数和标准差,把它画出来。你们有很多资料。如果你能自己从头写出这些notebook,我说的从头写是说用fastai库,不是完全从头写,你会成为头部梯队的一员,因为你能自己做所有这些东西,这非常非常难得。这会让你对part2有充分的准备。
提问:你认为未来fastai库会怎样,比如说在五年内?
像我说过的,我不做什么计划,很随便,所以...我们唯一的计划是,fast.ai做为一个组织,来让深度学习成为一个普通人做普通工作时能使用的工具。只要我们还需要写代码,就做不到这个,因为世界上99.8%的人不会写代码。所以主要的目标会是,让人们不再用一个库,而是能用上一个不需要用户写代码的软件。并且,也不再需要一个像这个一样的非常长的、很困难的课程。所以,我希望能不需要这个课程、不需要写代码,我希望能做到这样,那你们可以只做有用的东西,能快速地、简单地做。这会在五年之内做到吗?可能要更长。
好了。希望能在课程第二部分看到你们所有人。谢谢。