循环神经网络不合理的有效性 | Ilya Sutskever’s Top 30 Reading List
作者:karpathy | 日期:2015年5月21日
循环神经网络(Recurrent Neural Networks,RNN)身上有一种近乎魔法的东西。我至今还清楚地记得,第一次训练循环网络是在做图像描述(Image Captioning)的时候。只经过几十分钟的训练,我那个“婴儿模型”(超参数几乎是随手选的)就已经开始生成一些看起来相当不错、而且几乎说得通的图像描述了。有时候,模型的简单程度与它所能产生的结果质量之间的比例,会远远超出你的预期,而那次正是如此。
当时之所以令人震惊,是因为当时的普遍认知认为 RNN 非常难训练(而随着经验的积累,我反而得出了相反的结论)。时间快进大约一年:我已经开始频繁地训练 RNN,并多次亲眼见证了它们的力量与鲁棒性。尽管如此,它们生成的结果仍然不断以新的方式让我感到惊讶。这篇文章的目的,就是与你分享其中的一些“魔法”。
我们将训练 RNN,让它们以“逐字符”的方式生成文本,并思考一个问题:这到底是怎么做到的?
顺便一提,在发布这篇文章的同时,我也在 GitHub 上发布了一份代码,它可以用多层 LSTM 训练字符级语言模型。你只需要提供一大段文本,它就会学会一次生成一个字符的文本。你也可以用它复现下面的实验。不过我们有点超前了——在此之前,RNN 到底是什么?
循环神经网络
序列。 根据你的背景不同,你可能会问:循环神经网络究竟特别在哪里?传统的“普通神经网络”(以及卷积神经网络)有一个明显的限制:它们的接口过于受限——只能接受一个固定长度的向量作为输入(例如一张图像),并输出一个固定长度的向量(例如类别概率)。
不仅如此,这类模型完成这种映射时,所需的计算步骤数量也是固定的(比如网络层数)。循环神经网络真正令人兴奋的核心原因在于:它们可以在向量序列上进行操作——输入是序列,输出是序列,或者在最一般的情况下,两者都是序列。下面的几个例子可以让这一点更具体一些:
图中每个矩形表示一个向量,箭头表示函数(例如矩阵乘法)。红色表示输入向量,蓝色表示输出向量,绿色向量表示 RNN 的状态(稍后会详细解释)。从左到右分别是:
1)不使用 RNN 的普通处理方式,从固定长度输入到固定长度输出(例如图像分类);
2)序列输出(例如图像描述:输入一张图像,输出一个由单词组成的句子);
3)序列输入(例如情感分析:对一个句子进行正面或负面的分类);
4)序列输入 + 序列输出(例如机器翻译:RNN 读入一段英文句子,然后输出一段法文句子);
5)同步的序列输入与输出(例如视频分类,我们希望为视频中的每一帧打标签)。
注意,在所有这些情况下,序列长度都没有被预先限定,因为循环变换(绿色部分)是固定的,可以根据需要重复应用任意多次。
正如你所预期的那样,这种“序列处理”方式比起一开始就被固定计算步骤数量所束缚的网络要强大得多,因此对那些希望构建更智能系统的人来说也更具吸引力。此外,正如我们稍后会看到的,RNN 会通过一个固定(但可学习)的函数,将当前输入向量与自身的状态向量结合起来,生成新的状态向量。从编程的角度来看,这可以理解为:在运行一个固定的程序,程序有输入,也有内部变量。
从这个角度看,RNN 本质上是在描述“程序”。事实上,人们已经证明,RNN 在理论上是图灵完备的——也就是说,在合适的权重设置下,它们可以模拟任意程序。但就像神经网络的通用逼近定理一样,你不应该过度解读这一点。事实上,忘了我刚才说的这句话吧。
如果说训练普通神经网络是在对“函数”进行优化,那么训练循环神经网络,就是在对“程序”进行优化。
在没有序列的情况下进行序列化处理
你可能会觉得,输入或输出是序列的情况其实并不常见,但有一个非常重要的观点需要意识到:即使你的输入和输出都是固定长度的向量,你依然可以使用这种强大的形式,将它们以“序列”的方式来处理。
例如,下图展示了 DeepMind 两篇非常漂亮的论文中的结果。左边的例子中,一个算法学习到了一个循环网络策略,用来在图像上移动它的注意力;具体来说,它学会了从左到右读取门牌号码(Ba 等人)。右边的例子中,一个循环网络通过学习在画布上逐步添加颜色,来生成数字图像(Gregor 等人):
RNN 学会读取门牌号码
RNN 学会绘制门牌号码。
这里的关键结论是:即便你的数据本身并不是序列形式,你仍然可以构建并训练出强大的模型,让它们以序列化的方式来处理这些数据。 你所学习到的,其实是一些有状态的程序,用来处理固定大小的数据。
RNN 的计算方式
那么,这些东西到底是怎么运作的呢?从本质上看,RNN 拥有一个极其简单的接口:它接收一个输入向量 x,并输出一个向量 y。然而,关键在于,这个输出向量不仅受当前输入的影响,还受你在过去所有时间步中输入过的内容的影响。
如果用类的形式来写,RNN 的接口只有一个 step 函数:
rnn = RNN()
y = rnn.step(x) # x 是输入向量,y 是 RNN 的输出向量
RNN 类内部包含一些状态,每次调用 step 时,这些状态都会被更新。在最简单的情况下,这个状态由一个隐藏向量 h 组成。下面是一个“普通 RNN(Vanilla RNN)”中 step 函数的实现示例:
class RNN:
# ...
def step(self, x):
# 更新隐藏状态
self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
# 计算输出向量
y = np.dot(self.W_hy, self.h)
return y
上面描述的是一个普通 RNN 的前向传播过程。这个 RNN 的参数包括三个矩阵:W_hh、W_xh 和 W_hy。隐藏状态 self.h 会被初始化为零向量。np.tanh 实现了一个非线性函数,它会将激活值压缩到 [-1, 1] 的范围内。
我们可以简单看一下它是如何工作的:tanh 里的两项中,一项基于前一时刻的隐藏状态,另一项基于当前输入。在 NumPy 中,np.dot 表示矩阵乘法。这两部分通过加法相互作用,然后再经过 tanh 的压缩,得到新的状态向量。
如果你更习惯数学表示法,也可以把隐藏状态的更新写成:
hₜ = tanh(W_hh · hₜ₋₁ + W_xh · xₜ)
其中 tanh 是逐元素应用的。
我们用随机数初始化 RNN 的矩阵参数,而训练过程中最主要的工作,就是找到一组合适的矩阵,使得模型的行为符合我们的期望——这种期望通常通过某个损失函数来表达,该损失函数刻画了你希望在给定输入序列 x 时,输出 y 应该呈现出怎样的形式。
变得更深
RNN 依然是神经网络,而在深度学习的世界里,一切在做对的前提下,都会随着“变深”而变得更好。举例来说,我们可以构造一个两层的循环网络:
y1 = rnn1.step(x)
y = rnn2.step(y1)
换句话说,我们有两个独立的 RNN:第一个 RNN 接收输入向量,第二个 RNN 接收第一个 RNN 的输出作为自己的输入。对这两个 RNN 来说,它们并不“知道”彼此的存在——对它们而言,一切都只是向量的输入与输出,以及在反向传播过程中流经各个模块的梯度。
稍微复杂一点
我想简单提一下,在实际应用中,大多数人使用的并不是上面描述的这种形式,而是一种稍有不同、名为长短期记忆网络(Long Short-Term Memory,LSTM) 的结构。LSTM 是一种特殊的循环网络,它在实践中通常表现得更好,这得益于它更强大的状态更新公式,以及更友好的反向传播特性。
我不会在这里展开细节,但我之前关于 RNN 所说的一切,对 LSTM 依然完全成立,唯一的区别在于,用来计算状态更新的数学形式(也就是那一行 self.h = ...)变得稍微复杂了一些。接下来,我会在文中交替使用 “RNN” 和 “LSTM” 这两个术语,但本文中的所有实验,实际上使用的都是 LSTM。
字符级语言模型
现在,我们已经对 RNN 是什么、为什么它们令人兴奋、以及它们是如何工作的有了一个整体认识。接下来,我们将把这些概念落地到一个有趣的应用上:训练字符级语言模型。
具体来说,我们会给 RNN 一大段文本,让它学习这样一个概率分布:在给定前面若干字符的情况下,下一个字符最有可能是什么。学会这一点之后,我们就可以让模型一次生成一个字符,从而生成新的文本。
作为一个示例,假设我们的词汇表中只有四个字符:“h、e、l、o”,并希望在训练序列 “hello” 上训练一个 RNN。这个训练序列实际上包含了 4 个独立的训练样本:
1)在上下文是 “h” 的情况下,下一个字符很可能是 “e”;
2)在上下文是 “he” 的情况下,下一个字符很可能是 “l”;
3)在上下文是 “hel” 的情况下,下一个字符也很可能是 “l”;
4)在上下文是 “hell” 的情况下,下一个字符很可能是 “o”。
具体来说,我们会使用 1-of-k 编码(也称为独热编码)将每个字符编码成一个向量:向量中只有一个位置是 1,其余位置都是 0,这个位置对应字符在词汇表中的索引。我们把这些向量一个接一个地通过 step 函数输入到 RNN 中。随后,我们会得到一系列输出向量(在这个例子中是 4 维的,每一维对应一个字符),我们将这些输出解释为:RNN 当前认为“下一个字符是各个候选字符”的置信度。
下面是一张示意图:
一个示例 RNN,输入层和输出层都是 4 维,隐藏层包含 3 个单元(神经元)。这张图展示了当 RNN 接收字符 “hell” 作为输入时,在前向传播过程中各层的激活情况。输出层中包含的是 RNN 对“下一个字符”的置信度(词汇表为 “h, e, l, o”);我们希望绿色的数值较高,而红色的数值较低。
举例来说,在第一个时间步中,当 RNN 看到字符 “h” 时,它给“下一个字符是 h”的置信度为 1.0,“e”为 2.2,“l”为 -3.0,“o”为 4.1。由于在我们的训练数据中(字符串 “hello”)下一个正确字符是 “e”,我们希望提高它的置信度(绿色),并降低其他字符的置信度(红色)。同样地,在这 4 个时间步中的每一个,我们都有一个希望网络赋予更高置信度的目标字符。
由于 RNN 完全由可微分的操作组成,我们可以运行反向传播算法(这本质上就是微积分中链式法则的递归应用),来计算应该朝哪个方向调整每一个权重,才能提高正确目标字符(绿色粗体数字)的得分。接着,我们执行一次参数更新,让每个权重沿着这个梯度方向微小地移动一点。
如果在参数更新之后,再次把相同的输入送入 RNN,我们会发现:正确字符的得分(例如第一个时间步中的 “e”)会略微升高(例如从 2.2 变成 2.3),而错误字符的得分会略微降低。我们不断重复这个过程,直到网络收敛,并且它的预测结果与训练数据一致——也就是说,它总是能够预测出正确的下一个字符。
从更技术的角度看,我们是在对每一个输出向量同时使用标准的 Softmax 分类器(也常被称为交叉熵损失)。RNN 使用小批量随机梯度下降进行训练,我个人比较喜欢使用 RMSProp 或 Adam 这类“按参数自适应学习率”的方法来稳定训练过程。
另外请注意:当字符 “l” 第一次作为输入时,目标字符是 “l”,但第二次输入 “l” 时,目标字符却是 “o”。因此,RNN 不能只依赖当前输入字符本身,而必须使用它的循环连接来记住上下文,才能完成这个任务。
在测试阶段,我们向 RNN 输入一个字符,并得到一个关于“下一个字符可能是什么”的概率分布。我们从这个分布中进行采样,然后把采样得到的字符再输入回 RNN,生成下一个字符。重复这个过程,就可以不断地采样生成文本了。
接下来,让我们在不同的数据集上训练 RNN,看看会发生什么。
RNN 的趣味实验
下面的 5 个字符级模型示例,全部使用我在 GitHub 上发布的代码进行训练。在每一种情况下,输入都是一个包含文本的单一文件,我们训练一个 RNN 来预测序列中的下一个字符。
Paul Graham 生成器
我们先用一个较小的英文数据集来做一次合理性检查。我最喜欢的一个有趣数据集,是将 Paul Graham 的所有文章拼接在一起。基本想法是:这些文章中蕴含着大量智慧,但不幸的是,Paul Graham 本人是一个相对“生成速度较慢”的文本来源。如果我们能按需采样创业智慧,那岂不是很棒?这正是 RNN 能派上用场的地方。
将最近大约 5 年的所有 PG 文章拼接起来,我们得到一个大约 1MB 的文本文件,也就是大约 100 万个字符(顺便一提,这在字符级语言模型中被认为是一个非常小的数据集)。技术细节:我们训练了一个两层 LSTM,每层 512 个隐藏节点(大约 350 万个参数),并在每一层之后使用 0.5 的 dropout。训练时使用 100 个样本一批,并采用长度为 100 个字符的截断反向传播。使用这些设置,在一张 TITAN Z GPU 上训练一个 batch 大约需要 0.46 秒(如果把 BPTT 长度减半到 50 个字符,几乎不会损失性能,但时间可以减少一半)。不多说了,下面直接来看一个从 RNN 中采样得到的文本片段:
显然,上面的内容短时间内还不可能取代 Paul Graham 本人,但请记住:RNN 必须完全从零开始学习英语,而且使用的是一个非常小的数据集(包括逗号、撇号和空格该放在哪里)。我也很喜欢它学会了支持自己论点的方式(例如上面的 “[2]”)。有时候它还会说出一些似乎带着一丝洞见的话,比如:“a company is a meeting to think to investors”。如果你想看更多内容,这里有一个 5 万字符的采样示例链接。
http://cs.stanford.edu/people/karpathy/char-rnn/pg.txt
温度(Temperature)
在采样时,我们还可以调节 Softmax 的温度。将温度从 1 降低到某个更小的值(例如 0.5),会让 RNN 的预测变得更加自信,但同时也更加保守。相反,更高的温度会带来更大的多样性,但代价是错误更多(例如拼写错误)。特别是,当温度被设置得非常接近 0 时,模型会给出它认为最可能的输出,例如 Paul Graham “最有可能说的话”:
看起来我们已经陷入了一个关于创业的无限循环。
Shakespeare
我们已经看到模型可以学会拼写英文单词。那么,如果数据中包含更多结构和风格,会发生什么呢?为了检验这一点,我下载了莎士比亚的全部作品,并将它们拼接成一个单一的文件(4.4MB)。现在我们可以负担得起训练一个更大的网络,在这个实验中,我们尝试了一个三层的 RNN,每一层都有 512 个隐藏节点。在训练网络几个小时之后,我们可以得到如下的采样结果:
PANDARUS:
Alas, I think he shall be come approached and the day
When little srain would be attain'd into being never fed,
And who is but a chain and subjects of his death,
I should not sleep.
Second Senator:
They are away this miseries, produced upon my soul,
Breaking and strongly should be buried, when I perish
The earth and thoughts of many states.
DUKE VINCENTIO:
Well, your wit is in the care of side and that.
Second Lord:
They would be ruled after this chamber, and
my fair nues begun out of the fact, to be conveyed,
Whose noble souls I'll have the heart of the wars.
Clown:
Come, sir, I will make did behold your worship.
VIOLA:
I'll drink it.
请记住,RNN 所知道的只有字符,因此它既会生成说话人的名字,也会生成台词内容。有时我们还会得到相当长的独白段落,例如:
VIOLA:
Why, Salisbury must find his flesh and thought
That which I am not aps, not a man and in fire,
To show the reining of the raven and the wars
To grace my hand reproach within, and not a fair are hand,
That Caesar and my goodly father's world;
When I was heaven of presence and our fleets,
We spare with hours, but cut thy council I am great,
Murdered and by thy master's ready there
My power to give thee but so much as hell:
Some service in the noble bondman here,
Would show him to her wine.
KING LEAR:
O, if you were a feeble sight, the courtesy of your law,
Your sight and several breath, will wear the gods
With his heads, and my hands are wonder'd at the deeds,
So drop upon your lordship's head, and your opinion
Shall be against your honour.
我几乎已经无法从这些样本中辨认出真正的莎士比亚了。如果你喜欢莎士比亚,你可能会喜欢查看一个更长的采样示例。
http://cs.stanford.edu/people/karpathy/char-rnn/shakespear.txt。
当然,你也可以使用提供的代码,在不同温度设置下生成无限多的你自己的样本。
Wikipedia
我们已经看到,LSTM 可以学会拼写单词,并复制一般性的句法结构。接下来,我们进一步提高难度,在结构化的 Markdown 数据上进行训练。具体来说,我们使用了 Hutter Prize 提供的 100MB 原始维基百科数据集来训练一个 LSTM。按照 Graves 等人的做法,我使用前 96MB 作为训练集,其余部分作为验证集,并让模型连续训练了一整夜。现在,我们已经可以对维基百科文章进行采样了。下面是一些有趣的片段。首先是一些基本的 Markdown 输出:
Naturalism and decision for the majority of Arab countries' capitalide was grounded
by the Irish language by [[John Clair]], [[An Imperial Japanese Revolt]], associated
with Guangzham's sovereignty. His generals were the powerful ruler of the Portugal
in the [[Protestant Immineners]], which could be said to be directly in Cantonese
Communication, which followed a ceremony and set inspired prison, training. The
emperor travelled back to [[Antioch, Perth, October 25|21]] to note, the Kingdom
of Costa Rica, unsuccessful fashioned the [[Thrales]], [[Cynth's Dajoard]], known
in western [[Scotland]], near Italy to the conquest of India with the conflict.
Copyright was the succession of independence in the slop of Syrian influence that
was a famous German movement based on a more popular servicious, non-doctrinal
and sexual power post. Many governments recognize the military housing of the
[[Civil Liberalization and Infantry Resolution 265 National Party in Hungary]],
that is sympathetic to be to the [[Punjab Resolution]]
(PJS)[http://www.humah.yahoo.com/guardian.
cfm/7754800786d17551963s89.htm Official economics Adjoint for the Nazism, Montgomery
was swear to advance to the resources for those Socialism's rule,
was starting to signing a major tripad of aid exile.]]
如果你在想,上面出现的某些网址实际上并不存在——没错,模型只是“幻觉”出了它们。另外请注意,模型学会了正确地打开和关闭括号。模型还学会了大量结构化的 Markdown,例如有时会生成标题、列表等内容:
{ { cite journal | id=Cerling Nonforest Department|format=Newlymeslated|none } }
''www.e-complete''.
'''See also''': [[List of ethical consent processing]]
== See also ==
*[[Iender dome of the ED]]
*[[Anti-autism]]
===[[Religion|Religion]]===
*[[French Writings]]
*[[Maria]]
*[[Revelation]]
*[[Mount Agamul]]
== External links==
* [http://www.biblegateway.nih.gov/entrepre/ Website of the World Festival. The labour of India-county defeats at the Ripper of California Road.]
==External links==
* [http://www.romanology.com/ Constitution of the Netherlands and Hispanic Competition for Bilabial and Commonwealth Industry (Republican Constitution of the Extent of the Netherlands)]
有时,模型还会突然进入一种生成随机但语法正确的 XML 的模式:
<page>
<title>Antichristtitle>
<id>865id>
<revision>
<id>15900676id>
<timestamp>2002-08-03T18:14:12Ztimestamp>
<contributor>
<username>Parisusername>
<id>23id>
contributor>
<minor />
<comment>Automated conversioncomment>
<text xml:space="preserve">#REDIRECT [[Christianity]]text>
revision>
page>
模型完全编造了时间戳、ID 等内容。但请注意,它能够在正确的层级结构中关闭对应的标签。
http://cs.stanford.edu/people/karpathy/char-rnn/wiki.txt
如果你有兴趣,这里有一个更长的采样示例可以查看。
代数几何(LaTeX)
上述结果表明,模型在学习复杂的句法结构方面表现得相当不错。受到这些结果的鼓舞,我和我的实验室同事决定进一步深入结构化领域,并设法获得了一本关于代数堆栈/代数几何的书。我们下载了其原始的 LaTeX 源文件(一个 16MB 的文件),并训练了一个多层 LSTM。令人惊讶的是,采样得到的 LaTeX 几乎可以编译。我们只需要手动修复少量问题,就能得到看起来相当可信的数学内容,这非常令人震撼:
下面是另一个示例:
正如你在上面看到的那样,模型有时会尝试生成 LaTeX 图表,但显然它并没有真正理解这些图表。我也很喜欢它在某些地方选择跳过证明的做法。当然,请记住,LaTeX 具有一种相对困难的结构化句法格式,就连我自己也还没有完全掌握。举例来说,下面是一段未经编辑的模型原始输出:
\begin{proof}
We may assume that $\mathcal{I}$ is an abelian sheaf on $\mathcal{C}$.
\item Given a morphism $\Delta : \mathcal{F} \to \mathcal{I}$
is an injective and let $\mathfrak q$ be an abelian sheaf on $X$.
Let $\mathcal{F}$ be a fibered complex. Let $\mathcal{F}$ be a category.
\begin{enumerate}
\item \hyperref[setain-construction-phantom]{Lemma}
\label{lemma-characterize-quasi-finite}
Let $\mathcal{F}$ be an abelian quasi-coherent sheaf on $\mathcal{C}$.
Let $\mathcal{F}$ be a coherent $\mathcal{O}_X$-module. Then
$\mathcal{F}$ is an abelian catenary over $\mathcal{C}$.
\item The following are equivalent
\begin{enumerate}
\item $\mathcal{F}$ is an $\mathcal{O}_X$-module.
\end{lemma}
这个来自一个相对不错模型的样本,展示了几种常见错误。例如,模型开启了一个证明环境\begin{proof},却用引理环境将其结束\end{lemma}。这类问题通常需要我们手动修复,很可能是由于依赖关系过于长期造成的:当模型完成证明时,它已经忘记了自己最初是在写证明还是引理。类似地,它开启了一个枚举环境\begin{enumerate},却忘记将其关闭。我们观察到,随着模型规模和质量的提升,这类错误会变得不那么常见,但它们仍然会出现。
Linux 源代码
我希望把结构化数据推到极限,因此在最后一个挑战中,我决定使用代码。具体来说,我收集了 Linux GitHub 仓库中的所有源文件和头文件,并将它们拼接成一个单一的巨大文件(474MB 的 C 代码)(我最初只打算使用内核源码,但那本身只有大约 16MB)。随后,我在几天的时间里训练了多个“刚好能装进我的 GPU”的三层 LSTM。这些模型大约有 1000 万个参数,这在 RNN 模型中仍然算是偏小的规模。结果非常有趣:
/*
* Increment the size file of the new incorrect UI_FILTER group information
* of the size generatively.
*/
static int indicate_policy(void)
{
int error;
if (fd == MARN_EPT) {
/*
* The kernel blank will coeld it to userspace.
*/
if (ss->segment < mem_total)
unblock_graph_and_set_blocked();
else
ret = 1;
goto bail;
}
segaddr = in_SB(in.addr);
selector = seg / 16;
setup_works = true;
for (i = 0; i < blocks; i++) {
seq = buf[i++];
bpf = bd->bd.next + i * search;
if (fd) {
current = blocked;
}
}
rw->name = "Getjbbregs";
bprm_self_clearl(&iv->version);
regs->new = blocks[(BPF_STATS << info->historidac)] | PFMR_CLOBATHINC_SECONDS << 12;
return segtable;
}
整体来看,这些代码看起来相当不错。当然,我并不认为它真的能够通过编译,但当你滚动浏览这些生成的代码时,会强烈地感觉自己正在看一个庞大的 C 代码库。请注意,RNN 会不时地在代码中随机插入注释。它在避免语法错误方面也表现得非常好,例如能够正确使用字符串、指针表示法等。它也能正确地打开和关闭花括号,并且学会了相当不错的代码缩进。
一种常见错误是:它无法持续跟踪变量名。它经常使用未定义的变量,声明却从未使用的变量,或者返回一个不存在的变量。下面再看几个示例,展示 RNN 学到的更广泛的操作类型:
/*
* If this error is set, we will need anything right after that BSD.
*/
static void action_new_function(struct s_stat_info *wb)
{
unsigned long flags;
int lel_idx_bit = e->edd, *sys & ~((unsigned long) *FIRST_COMPAT);
buf[0] = 0xFFFFFFFF & (bit << 4);
min(inc, slist->bytes);
printk(KERN_WARNING "Memory allocated %02x/%02x, "
"original MLL instead\n"),
min(min(multi_run - s->len, max) * num_data_in),
frame_pos, sz + first_seg);
div_u64_w(val, inb_p);
spin_unlock(&disk->queue_lock);
mutex_unlock(&s->sock->mutex);
mutex_unlock(&func->mutex);
return disassemble(info->pending_bh);
}
static void num_serial_settings(struct tty_struct *tty)
{
if (tty == tty)
disable_single_st_p(dev);
pci_disable_spool(port);
return 0;
}
static void do_command(struct seq_file *m, void *v)
{
int column = 32 << (cmd[2] & 0x80);
if (state)
cmd = (int)(int_state ^ (in_8(&ch->ch_flags) & Cmd) ? 2 : 1);
else
seq = 1;
for (i = 0; i < 16; i++) {
if (k & (1 << 1))
pipe = (in_use & UMXTHREAD_UNCCA) +
((count & 0x00000000fffffff8) & 0x000000f) << 8;
if (count == 0)
sub(pid, ppc_md.kexec_handle, 0x20000000);
pipe_set_bytes(i, 0);
}
/* Free our user pages pointer to place camera if all dash */
subsystem_info = &of_changes[PAGE_SIZE];
rek_controls(offset, idx, &soffset);
/* Now we want to deliberately put it to device */
control_check_polarity(&context, val, 0);
for (i = 0; i < COUNTER; i++)
seq_puts(s, "policy ");
}
请注意,在第二个函数中,模型比较了一个变量与其自身tty == tty,这在逻辑上始终为真。另一方面,至少这一次变量tty确实存在于作用域中。在最后一个函数里,请注意该函数没有返回任何值,而这恰好是正确的,因为它的函数签名是 void。然而,前两个函数同样被声明为 void,却返回了值。这再次体现了由长期依赖引发的一类常见错误。
有时,模型会决定“开始一个新文件”。这通常是非常有趣的部分:模型会先逐字符地复述 GNU 许可证,接着采样一些 include 语句,生成一些宏,然后一头扎进代码之中:
【/*
* Copyright (c) 2006-2010, Intel Mobile Communications. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
*
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define REG_PG vesa_slot_addr_pack
#define PFM_NOCOMP AFSR(0, load)
#define STACK_DDR(type) (func)
#define SWAP_ALLOCATE(nr) (e)
#define emulate_sigs() arch_get_unaligned_child()
#define access_rw(TST) asm volatile("movd %%esp, %0, %3" : : "r" (0)); \
if (__type & DO_READ)
static void stat_PC_SEC __read_mostly offsetof(struct seq_argsqueue, \
pC>[1]);
static void
os_prefix(unsigned long sys)
{
#ifdef CONFIG_PREEMPT
PUT_PARAM_RAID(2, sel) = get_state_state();
set_pid_sum((unsigned long)state, current_state_str(),
(unsigned long)-1->lr_full; low;
}
这里有太多有趣的细节可以讨论——我几乎可以仅凭这一部分就写一整篇博客文章。这里我就先打住,如果你有兴趣,可以查看一个更长的采样示例。
http://cs.stanford.edu/people/karpathy/char-rnn/linux.txt
生成婴儿名字
我们再来一个轻松的实验。我们给 RNN 输入一个包含 8000 个婴儿名字的大型文本文件,每行一个名字。然后让 RNN 生成新的名字。下面是一些示例,只展示那些不出现在训练数据中的名字(大约 90% 都是新的):
你可以在其他地方看到更多示例。我个人最喜欢的一些包括一些看起来非常离谱或极简的名字。“Baby” (haha), “Killie”, “Char”, “R”, “More”, “Mars”, “Hi”, “Saddie”, “With” 和 “Ahbort”。
当然,你可以想象,这在写小说或者给一家新的创业公司起名时,可能会非常有用。
理解正在发生的事情
我们已经看到,在训练结束时得到的结果可能令人印象深刻,但这一切究竟是如何运作的呢?下面我们通过两个快速实验,简单窥探一下内部机制。
训练过程中样本的演化
首先,观察模型在训练过程中的采样文本如何演化,是一件很有趣的事情。举例来说,我用托尔斯泰的《战争与和平》训练了一个 LSTM,并在训练过程中每进行 100 次迭代就生成一次采样。在第 100 次迭代时,模型生成的只是随机杂糅的字符:
不过请注意,它至少已经开始意识到单词之间由空格分隔。只是有时会插入两个空格。同时,它也还不知道逗号几乎总是后面跟着一个空格。在第 300 次迭代时,我们可以看到模型开始理解引号和句号的用法:
此时,单词已经被空格分隔,模型也开始掌握句号出现在句尾的规律。在第 500 次迭代时:
模型现在已经学会拼写最短、最常见的一些词,例如“we”“He”“His”“Which”“and”等。在第 700 次迭代时,我们开始看到越来越多看起来像英语的文本出现:
在第 1200 次迭代时,模型已经开始使用引号以及问号、感叹号,更长的单词也已经被学会:
最终,在大约第 2000 次迭代时,我们开始看到拼写正确的单词、引号、人名等内容:
由此呈现出的整体图景是:模型首先发现的是单词与空格的整体结构,随后迅速开始学习具体的词汇;先从短词开始,最终过渡到更长的词。跨越多个单词的主题与语义(以及更一般意义上的长期依赖关系)则要到很晚才开始显现。
可视化预测与 RNN 中“神经元”的激活
另一种有趣的可视化方式,是观察模型对下一个字符的预测分布。在下面的可视化中,我们向一个维基百科 RNN 模型输入验证集中的字符数据(以蓝色/绿色行显示),并在每个字符下方,用红色展示模型对下一个字符给出的前五个预测。预测结果根据概率大小着色(深红表示非常可能,白色表示不太可能)。例如,可以注意到,在某些字符序列中,模型对下一个字符极为确信(例如在 “http://www.” 这样的序列中)。
输入的字符序列(蓝色/绿色)还根据 RNN 隐藏表示中某个随机选取的神经元的激活程度进行着色。你可以将其理解为:绿色表示高度激活,蓝色表示激活程度较低(对于熟悉 LSTM 细节的人来说,这些是隐藏状态向量中位于 [-1, 1] 区间的值)。直观上看,这是在可视化 RNN “大脑”中某个神经元在读取输入序列时的放电强度。不同的神经元可能会关注不同的模式;下面我们将看到我发现并认为比较有趣或可解释的四个例子(当然,也有许多并不可解释的):
这个示例中高亮的神经元似乎对 URL 非常敏感:当模型处在 URL 内部时,它会强烈激活,而在 URL 之外则关闭。LSTM 很可能使用这个神经元来记住自己是否位于一个 URL 之中。
这里高亮的神经元在 RNN 处于 [[ ]] 这种 Markdown 环境内部时会高度激活,而在其外部则关闭。有趣的是,这个神经元在看到第一个 [ 之后并不会立刻激活,而是要等到第二个 [ 出现后才会开启。判断模型已经看到了一个还是两个 [ 的任务,很可能是由另一个神经元完成的。
在这个例子中,我们看到一个神经元在 [[ ]] 环境内部呈现出近似线性的变化。换句话说,它的激活为 RNN 提供了一个在该结构内部的“时间对齐坐标系”。RNN 可能会利用这一信息,根据自己在该结构中处于较早还是较晚的位置,来调节某些字符的生成概率。
这里是另一个具有非常局部行为的神经元:它平时相对安静,但在 “www” 序列中的第一个 “w” 之后会迅速关闭。RNN 可能利用这个神经元来计数自己在 “www” 序列中已经走到了哪一步,从而判断是否应该继续生成 “w”,或者开始生成 URL 的其余部分。
当然,上述结论在一定程度上仍然是直觉性的,因为 RNN 的隐藏状态是一个高维、分布式的表示。这些可视化是通过自定义的 HTML/CSS/JavaScript 实现的,如果你想做类似的事情,可以查看相关示意材料。
我们还可以通过省略最可能的预测,只保留文本并用颜色表示某个单元的激活,从而进一步压缩这种可视化方式。可以看到,除了大量并不具备可解释性的单元之外,大约有 5% 的单元学会了相当有趣、而且可以理解的“算法”:
再次强调,这里最美妙的一点在于:我们从未在任何地方硬编码过诸如“在预测下一个字符时,记住自己是否处于引号内部可能会有用”之类的规则。我们只是用原始数据训练了 LSTM,而它自行决定这是一个值得追踪的量。换句话说,在训练过程中,它的某个单元逐渐被调谐成了一个“引号检测单元”,因为这有助于它更好地完成最终任务。这是深度学习模型(更一般地说,端到端训练)威力来源的一个最清晰、也最有说服力的例子之一。
源代码
希望我已经让你相信,训练字符级语言模型是一件非常有趣的事情。你可以使用我在 GitHub 上发布的 char-rnn 代码来训练你自己的模型(采用 MIT 许可证)。它接收一个大型文本文件,并训练一个字符级模型,之后你就可以从中进行采样。如果你有一块 GPU 会很有帮助,否则在 CPU 上训练的速度大约会慢一个数量级。不管怎样,如果你用自己的数据训练出了有趣的结果,欢迎告诉我!如果你在 Torch/Lua 的代码库中迷路了,请记住,它本质上只是我之前提到的那个大约 100 行的最小示例的一个更复杂版本。
简短的题外话
这份代码是用 Torch 7 编写的,而 Torch 最近已经成了我最喜欢的深度学习框架。我是在过去几个月里才开始使用 Torch/Lua,这个过程并不轻松(我花了不少时间翻看 Torch 在 GitHub 上的底层源码,也在他们的 Gitter 聊天室里提问),但一旦上手,它在灵活性和速度方面都非常出色。我过去也使用过 Caffe 和 Theano,而我认为 Torch 虽然并不完美,但在抽象层次的设计和整体理念上,比其他框架做得更好。
在我看来,一个高效的深度学习框架应当具备以下几个特征:
-
• 一个对 CPU/GPU 透明的张量库,提供大量功能(切片、数组/矩阵运算等) -
• 一个完全独立的脚本语言代码层(理想情况下是 Python),用于在张量之上实现所有深度学习相关内容(前向/反向传播、计算图等) -
• 能够方便地共享预训练模型(Caffe 在这一点上做得很好,而其他框架则不太行) -
• 最重要的是,不要有编译步骤(或者至少不要像 Theano 那样)
深度学习的发展趋势正在走向规模更大、结构更复杂的网络,这些网络往往在时间维度上展开成复杂的计算图。如果每次修改都需要花很长时间进行编译,那么开发效率将会严重受损。其次,一旦进行编译,就会牺牲可解释性,以及记录日志和调试的能力。如果在模型开发完成后,能够选择性地对计算图进行一次编译,用于生产环境的效率优化,那是可以接受的。
延伸阅读
在文章结束之前,我还想把循环神经网络放到一个更广阔的背景中,简单勾勒一下当前的一些研究方向。近年来,RNN 在深度学习领域引发了大量关注和兴奋。与卷积神经网络类似,它们已经存在了几十年,但直到最近,其全部潜力才开始被广泛认可,这在很大程度上得益于计算资源的不断增长。下面是一些近期发展的简要概述(这绝不是一个完整的列表,其中很多工作都可以追溯到 20 世纪 90 年代,具体可参考相关论文中的“相关工作”部分)。
在自然语言处理与语音领域,RNN 已经能够将语音转录为文本,执行机器翻译,生成手写文本,当然也被广泛用作强大的语言模型(包括字符级和词级)。目前来看,词级模型通常比字符级模型表现得更好,但这很可能只是暂时的。
在计算机视觉领域,RNN 也正在迅速变得无处不在。例如,我们已经看到 RNN 被用于帧级视频分类、图像描述(包括我自己的工作以及许多其他研究)、视频描述,以及最近出现的视觉问答。我个人在计算机视觉中最喜欢的一篇 RNN 相关论文是《视觉注意力的循环模型》,原因既包括其高层方向(通过多次“凝视”对图像进行序列化处理),也包括其底层建模方式(使用 REINFORCE 学习规则,这是强化学习中策略梯度方法的一种特例,使得模型能够学习不可微分的计算过程,例如在图像中移动注意力焦点)。我相信,这种将卷积神经网络用于原始感知、再在其之上叠加 RNN 注意力策略的混合模型,会在感知任务中变得非常普遍,尤其是在那些超越简单目标分类的复杂任务中。
在归纳推理、记忆和注意力方向,另一个极其令人兴奋的研究方向,正是针对普通循环网络的局限性展开的。一方面,RNN 并不具备真正的归纳能力:它们非常擅长记忆序列,但并不总能以令人信服的方式进行泛化(稍后我会给出一些更具体的指引)。另一方面,RNN 将表示规模与每一步的计算量不必要地绑定在一起。例如,如果你将隐藏状态向量的维度翻倍,由于矩阵乘法的存在,每一步的浮点运算量将会增加到原来的四倍。理想情况下,我们希望能够维持一个巨大的表示或记忆(例如包含整个维基百科,或大量中间状态变量),同时保持每个时间步的计算量固定。
朝着这个方向迈出的第一个令人信服的例子,是 DeepMind 提出的神经图灵机。这篇论文勾勒了一条路径:模型可以在一个大型的外部存储阵列与一小组进行实际计算的内部寄存器之间执行读写操作(可以将其理解为“工作记忆”)。更关键的是,神经图灵机引入了一种非常有趣的记忆寻址机制,它通过一种柔性、完全可微的注意力模型来实现。事实证明,柔性注意力是一个极其强大的建模工具,它也被用于机器翻译中的对齐与翻译联合学习,以及用于(玩具级别的)问答任务的记忆网络。事实上,我甚至愿意直言:
注意力机制,是近年来神经网络结构中最有趣的一项创新。
当然,我不想在这里深入太多细节。用于记忆寻址的柔性注意力方案的优点在于,它保持了模型的完全可微性;但不幸的是,它牺牲了效率,因为所有可能被关注的内容,都会在某种程度上被关注。你可以将这理解为:在 C 语言中声明了一个指针,但它并不指向某一个具体地址,而是定义了一个覆盖整个内存空间的概率分布,对这个指针进行解引用时,返回的是所有地址内容的加权和——这显然是一个代价极高的操作。
正因为如此,许多研究者开始用硬注意力模型来替代柔性注意力模型,即在每一步中采样一个特定的记忆块来进行读写(例如对某一个存储单元执行一次明确的读或写操作),而不是对所有单元同时进行“部分读写”。这种模型在哲学上更加令人信服,也更具可扩展性和效率,但不幸的是,它是不可微分的。这就需要借助强化学习领域中的技术(例如 REINFORCE),而在强化学习中,人们早已习惯处理不可微分的交互过程。这一方向仍然是正在进行中的研究工作,硬注意力模型已经在多项研究中得到探索,例如堆栈增强的循环网络、强化学习神经图灵机,以及用于图像描述的“展示、注意与讲述”模型。
人物
如果你想进一步阅读关于 RNN 的资料,我推荐 Alex Graves、Ilya Sutskever 以及 Tomas Mikolov 的博士论文。如果你想了解更多关于 REINFORCE,或者更一般意义上的强化学习与策略梯度方法(REINFORCE 正是其中的一个特例),可以参考 David Silver 的课程,或者 Pieter Abbeel 的相关课程。
代码
如果你想亲手训练 RNN,我听到很多人对 Keras 或 Passage(基于 Theano)评价不错;也可以使用本文随附发布的 Torch 代码,或者我之前写的一个基于 NumPy 的示例代码,它实现了高效、批量化的 LSTM 前向与反向传播。你也可以看看我基于 NumPy 的 NeuralTalk 项目,它使用 RNN/LSTM 为图像生成描述,或者 Jeff Donahue 提供的一个基于 Caffe 的实现。
结论
我们已经学习了什么是循环神经网络,它们如何工作,以及为什么它们会变得如此重要;我们在多个有趣的数据集上训练了字符级语言模型;也初步了解了 RNN 的发展方向。你完全可以期待,在 RNN 这一领域还将出现大量创新,我相信它们会成为智能系统中普遍而关键的组成部分。
最后,给这篇文章加一点“元”元素:我用这篇博客文章本身的源文件训练了一个 RNN。不幸的是,这篇文章只有大约 4.6 万个字符,我写的数据还不足以真正喂饱一个 RNN,但它在低温设置下生成的一个较为“典型”的样本是:
我已经用 RNN 进行了训练并且它能够工作,但通过程序计算得到的,是 RNN 与 RNN 以及 RNN 的计算结果。
是的,这篇文章讨论的是 RNN 以及它们为何如此有效,而现在这件事本身似乎也被验证了。
下次再见!
https://karpathy.github.io/2015/05/21/rnn-effectiveness/
如果觉得内容不错,欢迎你点一下「在看」,或是将文章分享给其他有需要的人^^
相关好文推荐:
复杂动力学第一定律 | Ilya Sutskever’s Top 30 Reading List
赋权于民:大语言模型如何逆转技术扩散的范式 | karpathy
“通用智能根本不存在”?Yann LeCun 与 Demis Hassabis 正面开撕

0条留言