乐者为王

Do one thing, and do it well.

循环神经网络不可思议的效用

英文原文:http://karpathy.github.io/2015/05/21/rnn-effectiveness/

循环神经网络(RNN)有些神奇的地方。我仍然记得我为图像标注而训练我的首个循环网络的情景。我的第一个幼稚模型(带有相当随意选择的超参数)在经过几十分钟的训练后就开始在图像的有意义的边缘产生非常漂亮的描述。有时候,模型的简单与你从中得到的结果的质量之间的比例超过你的预期,这次就是一个例子。为什么当时这个结果如此令人震惊?普遍的看法是,RNN应该是很难训练的(随着更多的实践,我实际上得到了相反的结论)。在这之后的1年:我一直在训练RNN,并多次见证它的能力和健壮性,但它的神奇输出仍然让我感到有趣。这篇文章将与你分享一些RNN的魔法。

我们将训练RNN让它一个字符一个字符地生成文本,然后思考“这怎么可能?”

顺便说一句,我会把这篇文章涉及的代码发布到GitHub上,它可以让你基于多层LSTM来训练字符级别的语言模型。你给它输入大量的文本,它将学会生成类似的文本。你也可以用它来复现我下面的实验。那么RNN究竟是什么呢?

循环神经网络

序列。Vanilla神经网络(还有卷积网络)最大的局限性在于它的API太受约束:它接受固定大小的向量作为输入(例如图像),然后产生固定大小的向量作为输出(例如不同分类的概率)。不仅如此:这些模型使用固定数量的计算步骤(例如模型中的层数)执行这种映射。循环神经网络如此令人兴奋的主要原因是它允许我们对向量的序列进行操作:输入中的序列、输出中的序列、或者最普遍情况下的输入输出序列。下面是几个具体的示例:

diags

每个矩形都是一个向量,箭头代表函数(例如矩阵乘法)。输入向量为红色,输出向量为蓝色,绿色向量是RNN的状态。从左到右是:

  1. 没有RNN的Vanilla处理模式,从固定大小的输入到固定大小的输出(例如图像分类)。
  2. 序列输出(例如图像标注,输入图像然后输出一段文字序列)。
  3. 序列输入(例如情感分析,给定的文字被分类为表达正面或者负面情感)。
  4. 序列输入和序列输出(例如机器翻译:RNN读取英语句子然后以法语的形式输出)。
  5. 同步序列输入和输出(例如视频分类,对视频的每个帧打标签)。

注意,每个案例都没有对序列长度进行预先规定,因为循环变换(绿色)是固定的,且根据需要可以多次使用。

正如你预想的那样,与使用固定数量的计算步骤的固定网络相比,序列组织方法的操作要更为强大。RNN通过固定的(但可以学习的)函数把输入向量与其状态向量结合起来以产生新的状态向量。这在编程术语中可以被解释为,运行一个具有某些输入和一些内部变量的固定程序。从这个角度看,RNN本质上是在描述程序。事实上,就它可以模拟任意程序(使用恰当的权值向量)而言,RNN是图灵完备的。但是类似于神经网络的通用近似定理,你不用过于关注其中的细节。

如果训练Vanilla神经网络是对函数进行优化,那么训练循环网络就是对程序进行优化。

序列缺失下的序列化处理。你可能会想,将序列作为输入或输出是相当少见的,但重要的是要认识到,即使输入或输出是固定向量,你仍然可以使用这种强大的形式体系以序列化的方式对它们进行处理。例如,下图显示的结果来自DeepMind的两篇非常不错的论文。在左边,算法学习一种循环网络策略,可以将它的注意力集中在图像周围。具体地说,就是它学习从左到右阅读门牌号码(Ba et al.)。在右边,循环网络通过学习在画布上序列化地添加颜色来生成数字图像(Gregor et al.)。

house-read house-generate

左边:RNN学习阅读门牌号码。右边:RNN学习绘制门牌号码。

言外之意就是,即使数据不是序列的形式,你仍然可以制定和训练出强大的模型来学习序列化地处理它。你是在学习处理固定大小数据的有状态程序。

RNN计算。那么这些是如何工作的呢?主要是RNN有个看似简单的API:它接收输入向量x,然后给出输出向量y。然而最重要的是,该输出向量的内容不仅受到刚才输入的影响,还受到过去整个历史输入的影响。写成类的话,RNN的API由单个step函数构成。

1
2
rnn = RNN()
y = rnn.step(x)  # x是输入向量,y是RNN的输出向量

每当step函数被调用时,RNN的某些内部状态就会被更新。在最简单的案例中,这个状态由单个隐藏向量h构成。以下是Vanilla RNN中step函数的实现:

1
2
3
4
5
6
7
8
class RNN:
  # ...
  def step(self, x):
    # update the hidden state
    self.h = np.tanh(np.dot(self.W_hh, self.h) + np.dot(self.W_xh, x))
    # compute the output vector
    y = np.dot(self.W_hy, self.h)
    return y

上面的代码详细说明了vanilla RNN的前向传播。这个RNN的参数是3个矩阵:W_hh、W_xh、W_hy。隐藏状态self.h被初始化为零向量。np.tanh函数实现一个非线性,将激活值压缩到范围[-1, 1]。注意代码是如何工作的:tanh内有两个条件,一是基于前面的隐藏状态,一是基于当前的输入。在NumPy中,np.dot是矩阵乘法。两个中间变量相加,然后被tanh压缩为新的状态向量。如果你更享受数学公式,我们也可以将隐藏状态写成:equation

我们用随机数初始化RNN的矩阵,训练中的大部分工作是寻找那些能够产生期望行为的矩阵,通过一些损失函数来度量,这些函数表示对于输入序列x你偏好什么类型的输出y。

深度网络。RNN是神经网络,如果你进行深度学习并且开始像叠煎饼一样堆叠模型,它将会工作得越来越好(如果做得正确的话)。例如,我们可以通过以下方式建立一个2层的循环网络:

1
2
y1 = rnn1.step(x)
y = rnn2.step(y1)

换句话说,我们有两个独立的RNN:一个RNN接收输入向量,而第二个RNN以第一个RNN的输出作为其输入。RNN其实并不关心这些——它们都只是向量的进出,以及在反向传播期间某些梯度流经每个模块。

需要指明的是,在实践中我们大多数人使用略有不同的长短期记忆(LSTM)网络。LSTM是一种特殊类型的循环网络,由于其强大的更新方程和一些吸引人的动态反向传播机制,它在实践中的效果略好一些。除了用于更新计算(self.h=...)的数学形式变得有点复杂外,其它所有都与本文介绍的RNN的内容完全相同。从这里开始,我会混合使用术语RNN和LSTM,但是本文中的所有实验都是用LSTM完成的。

字符级别的语言模型

现在我们已经知道RNN是什么,为什么它如此令人兴奋,以及它是如何工作的。我们现在就用它来实现一个有趣的应用:我们将训练RNN字符级别的语言模型。也就是说,我们给RNN巨量的文本,然后让其建模,根据序列中以前的字符序列给出下一个字符的概率分布。这将允许我们一次一个字符地生成新文本。

作为示例,假设我们拥有只有四个字符“helo”的词汇表,然后想用训练序列“hello”训练一个RNN。这个训练序列实际上是4个独立的训练示例的来源:

  1. “h”出现时下一个字符最有可能是“e”。
  2. “he”出现时下一个字符最有可能是“l”。
  3. “hel”出现时下一个字符最有可能是“l”。
  4. “hell”出现时下一个字符最有可能是“o”。

具体来说,我们会使用1-of-k编码方式(即除对应字符为1外其余都为0)将每个字符编码成一个向量,并且使用step函数将它们一次一个地喂给RNN。然后,我们观察四维输出向量(每个字符一维)序列,我们将其解释为RNN当前分配给序列中下次到来的每个字符的置信度。以下是示意图:

charseq

这个RNN示例具有4维输入和输出层,以及3个单位(神经元)的隐藏层。该示意图显示了当RNN把字符“hell”当作输入时前向传播中的激活值。输出层包含RNN为下一个字符分配的置信度(词汇表是“h,e,l,o”)。我们希望绿色数值尽可能高,红色数值尽可能低。

例如,我们可以看到,在第1个时间步骤中,当RNN看到字符“h”时,它将下一个可能出现字符的置信度分别设成“h”为1,“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”,但第二次为“o”。因此,RNN不能单独依赖输入,必须使用其循环连接来跟踪上下文以实现此任务。

在测试的时候,我们喂给RNN一个字符,并得到下次可能到来的字符的分布。我们从这个分布中取样,然后将其反馈给RNN以获得下一个字符。重复这个过程你就会得到文本!现在让我们在不同的数据集上训练RNN,看看会发生什么。

为了进一步说明,出于教育目的我还写过使用Python/NumPy的最小字符级别的RNN语言模型。它只有大约100行左右,如果你更擅长阅读代码而不是文本,希望它能对上述内容给出一个简洁、具体和有用的总结。现在我们将深入实例结果,它由更高效的Lua/Torch代码库产生。

RNN的乐趣

以下5个示例的字符模型都使用我在GitHub上发布的代码进行训练。每个案例中的输入都是单个文本文件,我们将训练RNN来预测序列中的下一个字符。

Paul Graham生成器

让我们先尝试用一个小的英文数据集作为完整性检查。我最喜欢的数据集是Paul Graham的文集。基本想法是,这些文章中有很多的智慧,但不幸的是,Paul Graham的写作速度比较慢。如果我们可以根据需要生成创业智慧的样本,岂不美哉?这时就轮到RNN出场了。

合并Paul Graham过去5年的所有文章,我们可以得到大约1MB的文本文件,或者说大约100万个字符(顺便提一句,这是个非常小的数据集)。技术:训练一个2层的LSTM,含有512个隐藏节点(约350万个参数),每层之后有0.5的dropout。我们将通过每批次100个实例和长度为100个字符的截断式沿时间反向传播来训练。使用这些设置,每个批次在TITAN Z GPU上耗时大约0.46秒(这可以通过性能代价微不足道的50个字符的BPTT,即Backpropagation Through Time让耗时减半)。言归正传,让我们看看来自RNN的样本:

The surprised in investors weren’t going to raise money. I’m not the company with the time there are all interesting quickly, don’t have to get off the same programmers. There’s a super-angel round fundraising, why do you can do. If you have a different physical investment are become in people who reduced in a startup with the way to argument the acquirer could see them just that you’re also the founders will part of users’ affords that and an alternation to the idea. [2] Don’t work at first member to see the way kids will seem in advance of a bad successful startup. And if you have to act the big company too.

好吧,显然上面的样本暂时还无法取代Paul Graham,但是别忘了,RNN必须使用一个小的数据集(包括逗号,撇号和空格)完全从零开始学习英语。我也很喜欢它学会通过引用来支持自己的论点(例如上面的[2])。有时候它会说一些有洞见的话,比如“a company is a meeting to think to investors”。如果你想看到更多,这里有5万个字符的样本。

温度。我们还可以在取样的时候调节Softmax的温度。将温度从1降至更低的数值(例如0.5)可以让RNN有更高的置信度,但其样本也会更保守。相反地,较高的温度将会带来更多的多样性,但是会以更多的错误为代价(例如拼写错误等)。特别是,将温度设置得非常接近于0时就会得到与Paul Graham更相似的言论:

is that they were all the same thing that was a startup is that they were all the same thing that was a startup is that they were all the same thing that was a startup is that they were all the same

看起来我们已经陷入了创业的死循环。

莎士比亚

看起来RNN可以学习拼写英文单词。但是如果数据中有更多的结构和风格呢?为检验这一点,我下载了莎士比亚的所有作品,并将它们合并成单个(4.4MB)文件。现在我们可以训练一个更大的网络,在这个案例中,我们将尝试一个3层的RNN,每层有512个隐藏节点。在训练网络几个小时之后,我们得到如下的样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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只知道字符,所以它会对发言者的名字和内容进行取样。有时候我们也会得到相对延伸的独白短文,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

我几乎不能把这些样本从莎士比亚的原作中辨别出来:)如果你喜欢莎士比亚,你可能会喜欢这10万个字符的样本。当然,你也可以使用我提供的代码在不同温度下生成无限数量的你自己的样本。

维基百科

我们看到,LSTM可以学习拼写单词和复制一般的句法结构。让我们进一步增加难度,在结构化的markdown上面训练它。具体地说,就是使用Hutter Prize 的维基百科原始数据集(100MB)训练一个LSTM。和Graves et al.一样,我使用前面的96MB来训练,剩下的用于验证以及在晚上跑几个模型。我们现在可以对维基百科的文章进行取样!以下是一些有趣的摘录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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还有很多,比如有时候它会创建标题,列表等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{ { 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文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<page>
  <title>Antichrist</title>
  <id>865</id>
  <revision>
    <id>15900676</id>
    <timestamp>2002-08-03T18:14:12Z</timestamp>
    <contributor>
      <username>Paris</username>
      <id>23</id>
    </contributor>
    <minor />
    <comment>Automated conversion</comment>
    <text xml:space="preserve">#REDIRECT [[Christianity]]</text>
  </revision>
</page>

模型生成timestamp,id等等。另外,还要注意到它会以正确的嵌套顺序恰当地关闭相应的标签。如果你有兴趣看到更多,这里有10万个字符的维基百科样本

代数几何(Latex)

以上结果表明,该模型在学习复杂句法结构方面确实相当擅长。这些结果令人印象深刻,我和我的实验室同事(Justin Johnson)决定在结构化的领域进一步推进。我们找到关于代数叠/几何的这本书,下载它的原始Latex源文件(16MB),然后训练一个多层的LSTM。令人惊讶的是,产生的Latex样本几乎是可以编译的。在我们手动修复一些问题后,就得到了看起来似乎合理的数学推论,这是相当惊人的:

latex4

代数几何样本(假的),这里是真正的PDF文件

这是另一份样本:

latex3

更像代数几何了,还出现了图表(右)。

正如你在上面看到的,有时候这个模型试图生成Latex图表,但显然它并不明白图表的具体意思。我也很喜欢它跳过证明的部分(左上角的“Proof omitted”)。当然,Latex有着相对困难的结构化语法格式,甚至我自己都没有完全掌握。例如,这里是一份来自模型的原始样本(未编辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
\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}结束。我们必须手动修复这种问题,它可能是由于依赖关系太长期的缘故:当模型完成proof的时候,它已经忘记是在做proof还是lemma。同样地,它打开一个\begin{enumerate},但却忘记关闭它。我们观察到,在使用更大/更好的模型时,这些问题会变少,但尽管如此,这些问题还是会出现。

Linux源代码

我想把结构化的数据推到极限,所以最后的挑战是我决定使用代码。首先,我从GitHub上的Linux库中提取所有的源文件和头文件,将它们合并成单个巨大的文件(474MB的C代码)(我本来只打算在内核上训练,但是其大小只有约16MB)。然后,我花几天的时间训练出几个大小刚好适合我的GPU的3层LSTM网络。这些模型有大约1000万个参数,对于RNN模型来说仍然很少。结果超级有趣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
 * 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会随机地生成带有注释的代码。它很少会犯语法错误。比如,它能正确地使用字符串、指针表示法等。它也能正确地打开和关闭括号{[,学会很好地缩进代码。一个常见的错误是它不能跟踪变量:它经常使用未定义的变量(例如上面的rw),声明从不使用的变量(例如int error),或者返回不存在的变量。让我们看看更多的示例。这是另外一段代码,显示RNN学到的更多操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
 * 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许可证,包含几个头文件,声明一些宏,然后就开始生成代码部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
 *  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 <linux/kexec.h>
#include <linux/errno.h>
#include <linux/io.h>
#include <linux/platform_device.h>
#include <linux/multi.h>
#include <linux/ckevent.h>

#include <asm/io.h>
#include <asm/prom.h>
#include <asm/e820.h>
#include <asm/system_info.h>
#include <asm/setew.h>
#include <asm/pgproto.h>

#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;
}

这里面有太多有趣的地方需要涉及——仅仅这部分我大概就能写一整篇文章。现在我就不再多说,这里有1MB的Linux代码样本供你欣赏。

生成婴儿名字

让我们尝试一个更好玩的。给RNN提供一个包含8000个婴儿名字的文本文件,每个名字一行(名字从这里获得)。 我们可以把这些名字喂给RNN,然后生成新的名字!以下是一些示例名字,只显示训练数据中没有出现的(90%不会):

Rudi Levette Berice Lussa Hany Mareanne Chrestina Carissy Marylen Hammine Janye Marlise Jacacrie Hendred Romand Charienna Nenotto Ette Dorane Wallen Marly Darine Salina Elvyn Ersia Maralena Minoria Ellia Charmin Antley Nerille Chelon Walmor Evena Jeryly Stachon Charisa Allisa Anatha Cathanie Geetra Alexie Jerin Cassen Herbett Cossie Velen Daurenge Robester Shermond Terisa Licia Roselen Ferine Jayn Lusine Charyanne Sales Sanny Resa Wallon Martine Merus Jelen Candica Wallin Tel Rachene Tarine Ozila Ketia Shanne Arnande Karella Roselina Alessia Chasty Deland Berther Geamar Jackein Mellisand Sagdy Nenc Lessie Rasemy Guen Gavi Milea Anneda Margoris Janin Rodelin Zeanna Elyne Janah Ferzina Susta Pey Castina

你可以在这里看到更多。我最喜欢的名字包括“Baby”(哈哈)、“Killie”、“Char”、“R”、“More”、“Mars”、“Hi”、“Saddie”、“With”和“Ahbort”。这确实很有意思。当然,你还可以在写小说、命名或者给创业公司起名字的时候把它当作相当有用的灵感来源:)

理解到底发生了什么

我们看到,在训练结束时的结果令人印象深刻,但是这些工作是如何进行的呢?让我们跑两个快速的实验来简单地窥视一下。

训练中样本的演变

首先,看看样本文本在模型训练的过程中是如何演变的。例如,我用列夫·托尔斯泰的《战争与和平》来训练LSTM,然后每100次训练就产生一次样本。在第100次迭代时,模型取样的文本是随机杂凑的:

1
2
tyntd-iafhatawiaoihrdemot  lytdws  e ,tfti, astai f ogoh eoase rrranbyne 'nhthnee e
plia tklrgd t o idoe ns,smtt   h ne etie h,hregtrs nigtike,aoaenns lng

但是请注意,至少它开始意识到单词要用空格分开。有时候它偶尔会插入两个空格。它也不知道逗号后面几乎总是有个空格。在迭代300次后,我们看到模型开始学会使用引号和句号:

1
2
3
4
"Tmont thithey" fomesscerliund
Keushey. Thom here
sheulke, anmerenith ol sivh I lalterthend Bleipile shuwy fil on aseterlome
coaniogennc Phe lism thond hon at. MeiDimorotion in ther thize."

这些单词同样被空格分开,模型开始在句子结尾处使用句号。在第500次迭代时:

1
2
we counter. He stutn co des. His stanted out one ofler that concossions and was
to gearang reay Jotrets and with fre colt otf paitt thin wall. Which das stimn

模式已经学会拼写最短和最常见的单词,如“we”、“He”、“His”、“Which”、“and”等。在第700次迭代时,我们开始看到越来越像英语的文本出现:

1
2
3
Aftair fall unsuch that the hall for Prince Velzonski's that me of
her hearly, and behs to so arwage fiving were to it beloge, pavu say falling misfort
how, and Gogition is so overelical and ofter.

在第1200次迭代时,我们看到引号、问号和感叹号的使用。同时还出现了更长的单词:

1
2
"Kite vouch!" he repeated by her
door. "But I would be done and quarts, feeling, then, son is people...."

直到大约第2000次迭代时,我们才开始得到正确拼写的单词、引述、名字等等:

1
2
3
"Why do what that day," replied Natasha, and wishing to himself the fact the
princess, Princess Mary was easier, fed in had oftened him.
Pierre aking his soul came to the packs and drove up his father-in-law women.

从上面的描述可以知道,模型首先是发现单词-空格这样普遍的结构,然后迅速开始学习单词:首先从简短的单词开始,然后是更长的单词。跨越多个单词的话题和主题(以及一般的长期依赖关系)要到很久以后才会出现。

可视化RNN中的预测与神经元激活

另一个有趣的可视化是看看字符的预测分布。在下面的可视化中,我们把验证集的字符数据(蓝色/绿色的行)喂给维基百科RNN模型,然后在每个字符下面可视化(用红色)前5个最有可能的下一个字符。颜色深浅由它们的概率大小决定(所以暗红色被认为是非常可能的,白色是不太可能的)。注意,有时候模型对下一个字符的预测是非常有信心的(例如,模型对http://www.序列中的字符就是)。

输入字符序列(蓝/绿)的颜色深浅取决于RNN隐藏层中随机选择的神经元的激活情况。绿色表示非常兴奋,蓝色表示不太兴奋(对于那些熟悉LSTM细节的人来说,这些是隐藏状态向量中[-1,1]之间的值,也就是经过门限操作和tanh计算的LSTM单元状态)。直观地说,这是在RNN读取输入序列的时候,将它的“大脑”中的一些神经元的激活率可视化。不同的神经元可能在寻找不同的模式。下面我们来看看4个不同的神经元,我认为它们是有趣的或者可解释的:

under1

此图中高亮的神经元似乎对URL感到非常兴奋,在URL之外则不太兴奋。LSTM很可能使用这个神经元来记住它是否在URL内部。

under2

当RNN在[[]]环境内时,此处高亮的神经元变得非常兴奋,在其外部则不太兴奋。有趣的是,神经元在看到字符“[”后不会兴奋,必须等待第二个“[”才能激活。计算模型是否已经看到一个或两个“[”的任务很可能用不同的神经元来完成。

under3

在这里,我们看到神经元在跨越[[]]环境时似乎是线性变化的。换句话说,它的激活值给RNN提供了一个跨越[[]]范围的时间对齐的坐标系统。RNN可以使用这些信息来生成不同的字符,这或多或少可能取决于字符在[[]]范围内出现的早/晚(也许?)。

under4

这里是另一个具有非常局部行为的神经元:它是相对安静的,但在碰到“www”序列中的第一个“w”之后立即变得不太兴奋。RNN可以使用这个神经元来计算它在“www”序列中有多远,以便它可以知道是否应该输出另一个“w”,或者是否应该开始URL。

当然,由于RNN的隐藏状态是极多的、高维的和分散的,所以这些结论有些需要手动调整。这些可视化是由自定义的HTML/CSS/JavaScript生成的,如果你想创建类似的东西,你可以看这里的模板。

我们也可以通过排除最有可能的预测来精简这个可视化,仅仅显现文本,通过单元的激活值来决定颜色深浅。我们可以看到,除了大部分没有做任何解释的单元之外,大约5%的单元最终学会了相当有趣和可解释的算法:

pane1 pane2

此外,在试图预测下一个字符(例如,它可能有助于跟踪你目前是否在引号内)的任何时候我们都不必硬编码,这是多么美妙的一件事情!我们刚刚使用原始数据训练LSTM,它就决定这是个有用的东西需要跟踪。换句话说,其中一个单元在训练中逐渐把自己调整成为引号检测单元,因为这有助于它更好地完成最终任务。这是深度学习模型(更普遍的端到端训练)的能力来自哪里的最干净和最引人注目的示例之一。

源代码

希望我已经让你深信,训练字符级别的语言模型是一个非常有趣的练习。你可以使用我在GitHub上发布的字符RNN代码(采用MIT许可证)来训练自己的模型。它需要一个大的文本文件来训练字符级别的模型,然后你就可以从中取样。此外,如果你有一个GPU的话会更好,否则在CPU上训练会多花大约10倍的时间。不管怎样,如果你用某些数据进行训练并最终得到有趣的结果,请告诉我!如果你迷失在Torch/Lua的代码库中,请记住,它只是这100行要点的更高级版本。

题外话。代码是用Torch 7编写的,它最近已经成为我最喜欢的深度学习框架。我在最近的几个月才开始使用Torch/Lua,它们并不简单(我花了很多时间在GitHub上挖掘原始的Torch代码,并在gitter上询问他们问题以完成工作),但是一旦你掌握了足够的知识,它就会给你带来很大的灵活性和速度提升。我以前同样使用过Caffe和Theano,我认为,虽然Torch还不完美,但是它的抽象层次和哲学要比其它的好。在我看来,一个高效的框架应该具有以下特征:

  1. 具有许多功能的CPU/GPU透明张量库(切片、数组/矩阵操作等)
  2. 一套基于脚本语言(最好是Python)的完全独立的代码库,能够对张量进行操作,实现所有深度学习的东西(前向/反向传播、图形计算等)
  3. 可以轻松地分享预训练的模型(Caffe做的很好,其它的不行)
  4. 最关键的:没有编译步骤(或者至少不要像Theano现在这样)。深度学习的趋势是更大更复杂的网络,它们在复杂图中花费的时间会成倍地增加。不需要长时间编译或开发是非常重要的。其次,编译会丢失可解释性和有效日志/调试的能力。如果有选项可以在图被开发完成后选择编译,那是相当好的。

进一步阅读

在结束这篇文章前,我还想把RNN放到更广泛的背景中,并提供当前研究方向的概述。RNN最近在深度学习领域颇受欢迎。和卷积网络类似,它已经存在了几十年,但是它的全部潜力最近才开始得到广泛的认可,这在很大程度上是由于我们不断增长的计算资源。这里简要概述一些最近的发展情况(肯定不是完整的清单,很多这样的工作可以追溯到20世纪90年代的研究,请参阅相关的工作部分):

在NLP/语音领域,RNN将语音转录为文本,执行机器翻译生成手写文本,当然,它也被用作强大的语言模型(Sutskever et al.)(Graves)(Mikolov et al.)(都是在字符和单词层面)。目前看来,单词级别的模型比字符级别的模型更好,但这肯定是暂时的。

计算机视觉。RNN在计算机视觉中也很快变得普及。例如,将RNN用于帧级别的视频分类图像标注(也包括我自己的工作以及其它许多内容),视频标注以及最近的视觉问答。在计算机视觉论文中,我个人最喜欢的RNN是可视化注意力的循环模型,这是由于其高层次的方向(对图像扫视后的序列化处理)和低层次的建模(REINFORCE学习规则是强化学习中策略梯度方法的一个特例,可以训练出执行不可微分计算的模型(在这种情况下对图像周围进行扫视))。我相信,这种由CNN做原始感知加上RNN在顶部做扫视策略的混合模型将变得普及,特别是在那些不仅仅是对普通视图中某些对象进行分类的更复杂的任务中。

归纳推理、记忆和注意力。另一个极其令人兴奋的研究方向是面向解决Vanilla循环网络的局限性。RNN的一个问题是它不具有归纳性:它能很好地记忆序列,但它不一定总是以正确的方式显示令人信服的泛化符号(稍后我会提供一些证据使其更加具体)。第二个问题是它不必要地将表征大小与每个步骤的计算量相结合。例如,如果将隐藏状态矢量的大小加倍,由于矩阵乘法的原因,每个步骤的FLOPS数量会增加四倍。理想情况下,我们希望保持一个巨大的表征/记忆(例如,包含所有维基百科或许多中间状态变量),同时保持每个时间步骤的计算固定的能力。

在这些方向上,第一个有说服力的示例已经在DeepMind的神经图灵机论文中被建立。论文勾勒出一个模型的路径,该模型可以在大型外部存储阵列和较小的记忆寄存器集(运算发生的地方,可以将其视作我们的工作记忆)之间执行读/写操作。至关重要的是,该论文还特别提出一个非常有趣的记忆寻址机制,该机制是通过(“软”的和完全可微分的)注意力模型来实现的。“软”注意力的概念已被证明是一个强大的建模特性,也被通过共同学习对齐和翻译的神经机器翻译提出用于机器翻译和被记忆网络提出用于(玩具)问答。事实上,我可以这么说:

注意力的概念是近期神经网络中最有趣的架构创新。

现在,我不想讲太多的细节,但是记忆寻址的“软”注意力方案是很方便的,因为它使得模型完全可微分的,但不幸的是会牺牲一些效率,因为所有可以被注意的东西都被注意到了(虽然是“软软地”)。可以将其视作C语言中的指针,它不指向特定地址,而是定义了整个记忆地址,并且解除引用指针返回指向内容的加权和(这是个昂贵的操作!)。这激发了多个作者交换软注意力模型,以便在对某个特定的存储器块进行采样(例如,对某些存储器单元的读/写动作而不是从某种程度上对所有单元格的读/写)进行采样的情况下进行硬注意。这个模型在哲学上更具吸引力,可扩展性和高效性,但不幸的是它也是不可微分的。这就要求使用来自强化学习文献(例如REINFORCE)的技术,其中人们完全习惯于不可微分的相互作用的概念。这是非常正在进行的工作,但是这些“硬”注意力模型已经被探索,例如,推导堆栈增强循环网络的算法模式强化学习神经图灵机显示参加和告诉

研究者。如果你想详细研究RNN,我推荐Alex GravesIlya SutskeverTomas Mikolov的论文。想要知道更多关于REINFORCE和更通用的强化学习和策略梯度方法(REINFORCE是它的一个特例)的内容,可以学习David Silver或者Pieter Abbeel的公开课。

代码。如果你想训练RNN,Theano上的KerasPassage很不错,或者是配套本文的Torch代码,或者是我不久以前写的原始NumPy代码的要点,它实现了一个高效、批量的LSTM前向和反向传播。你也可以看看我的基于NumPy的NeuralTalk,它使用RNN/LSTM来标注图像,或者看看Jeff Donahue的这个Caffe实现

总结

我们已经学习了RNN,它是如何工作的,以及为什么它如此重要。我们还使用几个有趣的数据集来训练RNN字符级别的语言模型,并且我们已经看到RNN是如何进行这个过程的。你可以自信地期待RNN领域的大量创新,我相信它将成为智能系统的普遍和关键组成部分。

最后,为给这篇文章增加一些元素,我使用这篇博文的源文件来训练一个RNN。不幸的是,文章的长度不足以很好地训练RNN。以下是返回的样本(使用低温生成以获得更典型的样本):

1
2
I've the RNN with and works, but the computed with program of the
RNN with and the computed of the RNN with with and the code

是的,这篇文章讲的是RNN以及它的效果如何,很明显它工作良好:)下次再见!

编辑(额外链接):

视频:

讨论:

回复:

Comments