乐者为王

Do one thing, and do it well.

《超级阅读术》读书笔记

在读这本书前,我已经看过多本关于读书方法的书。所以对于这本书,我并不想从头到尾地去阅读,而是采用“略读”的方式来进行。

首先看书名页。书名页通常称“扉页”或“内封”,上面印有完整的书名、著作者和出版社的名称,背面印有图书在版编目数据、版本记录、版权说明等。作者是斋藤孝;类别是读书方法;2016年第1版第1次印刷,是新出版的书,经典程度未知。如果一本书多次出版多次印刷的话,说明该书比较经典,值得阅读。

然后看前言。作者在这里告诉我们:这是个极速变化的社会,职场人士如果只具备学生时代所掌握的知识和信息,将不能应付所发生的一切。要在这个残酷的竞争社会中生存下去,并且比别人更胜一筹,就需要掌握正确的读书方法——比别人读得更多、比别人读得更精、读后马上就能用于工作。

接着是研究目录页。其实前言中已经对目录进行过概括:

本书由以下几个部分构成:

  • 序言:详细说明为什么职场人士需要具备读书的技能
  • 第一章:介绍培养读书习惯的秘诀
  • 第二章:介绍以大量读书为目的的速读全技能
  • 第三章:介绍以提高读书质量为目的的精读全技能
  • 第四章:介绍将读书效率最大化的选书全技能
  • 第五章:介绍将读书收获的知识用以工作的全技能
  • 附录:介绍值得当下职场人土阅读的50本书

我只要对着上面的列表查看目录中具体的章节名称,以决定哪些章节是我感兴趣、要仔细阅读的。最终确定第二章的“速读”、第三章的“精读”以及第五章的“输出”要花点时间认真阅读。

以下是我的部分读书摘抄:

佛教用语中有一个词叫“慈悲”。这里“慈”的本意是给朋友和同胞带来利益和安乐,“悲”的本意是帮助朋友和同胞脱离和去除苦难。用现在的话来说,不就是服务于人的意思吗?

读书驱动人的想象力,这意味着读者与作者在进行共同创作。就好象站在作者所写的脚本的基础上来自任电影导演那样,读者本身也在创造作品。

书读得越多,你的知识就越丰富,对内容的理解就越快,读书时付出的辛苦也就越少。道理在于,书是通过知识来阅读的,而读书的过程,又有着积累知识的实质性意义。数量上的积累,会带来质量上的变化,即从量变达到质变。那么,应该怎样做才好呢?集中精力读几本你自己想读的领域里的书,这样你就能相对高效率地积累起该领域的相关知识,在读完六七本书之后,你对该领域的知识便能有八成的了解。最终,对哪怕第一次读到的书,你都只要去理解剩下的两成即可,你变得能在很短的时间内读完一本书。

复制和粘贴,只是将互联网上的信息拼凑起来的行为,根本不需要动脑。其结果,不但不能积累起真正的知识,就连自己写的文章也没有进入自己的大脑。

苹果公司创始人史蒂夫·乔布斯于2005年在斯坦福大学的毕业典礼上所作的“点与点”演讲中说到:“你不可能预先把这些点点滴滴穿在一起,唯有在未来某天回顾时,你才会明白那些点点滴滴是如何串联在一起的。所以你得相信,你现在所体会的东西,将来必然会以某种形式连接在一起。”在我们读书的时候,也不能预先把点点滴滴穿在一起。但是,读一本书,便意味着能切实地让自己内心拥有一个点。我们无法知道能否马上学以致用,但我们必须读书。这么做,是为让自己拥有更多的点。坚持这么做,必定有那么一天,点与点会串联在一起。

总结:泛泛而谈,比较空洞,没有什么阅读价值。

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

英文原文: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)的技术,其中人们完全习惯于不可微分的相互作用的概念。这项工作现在还在进展中,但是这些“硬”注意力模型已经被探索过,例如,使用栈增强循环网络的推理算法模式强化学习神经图灵机Show Attend and Tell

研究者。如果你想详细研究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以及它的效果如何,很明显它工作良好:)下次再见!

额外链接:

视频:

讨论:

回复:

如何验证邮箱是否存在?

事情的起因是想要注销某个免费的163邮箱,但是查找资料后发现:

163免费邮箱不支持直接注销。

如果连续180天没登录过网易任何产品的帐号,系统将自动清空所有信息和资料,并删除帐号。

那么问题来了,超过上述期限没有登录的话,如何才能验证该邮箱是否真的被销号?直接登录肯定不行,如果那时邮箱正处于冻结状态,你登录不就相当于重新激活吗!或者向该邮箱发送测试邮件,如果投递成功就说明邮箱还没被注销,但这种方法太过麻烦,尤其对于注销多个邮箱的情况。要想优雅地解决这个问题,我们需要从了解电子邮件传输协议开始。

电子邮件传输协议

电子邮件传输协议主要包括SMTP协议、ESMTP协议、POP协议、IMAP协议等,具体的标准可以参考RFC Editor网站下的相关RFC文档。POP3是Post Office Protocol 3的缩写,IMAP是Internet Message Access Protocol的缩写,两者的作用都是收取邮件,关键的区别在于邮件是放在服务器上(IMAP)还是放在本地电脑上(POP3)。SMTP是Simple Mail Transfer Protocol的缩写,ESMTP即Extension-SMTP,是为对付滥用SMTP服务器发邮件而引入的,它和SMTP的主要区别在于增加了发信认证机制。

下图是一个典型的电子邮件收发路径:

email-architecture

在你写完邮件点击“发送”时,你的邮件客户端会将消息发送给SMTP服务器(就如同我们将信交给本地的邮局),然后SMTP服务器根据收信人的地址向DNS服务器查询邮箱地址后缀的MX记录,找到目的邮件服务器(就好像邮局根据收信人的地址选择邮递路线,经过飞机或火车等交通工具到达收信人所在地邮局),然后收信人就可以使用自己的帐号登录POP3服务器收取邮件(就像收信人从邮箱取信)。

由此推断,通过SMTP服务器或者MX服务器能够验证邮箱是否存在。下面我们就来进行测试。

实践

我们先从SMTP服务器开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C:\>telnet smtp.163.com 25
220 163.com Anti-spam GT for Coremail System (163com[20141201])

hello    # SMTP协议没有这个指令
502 Error: command not implemented

helo hi  # 因为每次按键都会被传送到服务器,所以输入错误时不能使用退格键删除,只能换行重新输入
500 Error: bad syntax

helo hi
250 OK

vrfy noname@163.com  # 现在的邮件服务器都使用ESTMP协议,VRFY、EXPN这些指令都已经被禁用或不被支持
502 Error: command not implemented

mail from: <noname@example.com>  # 必须是同域的邮箱才能发邮件
553 Local user only,163 smtp7,C8CowAA3eUZ7jEBaXE8hDg--.38995S2 1514179735

mail from: <noname@163.com>      # 必须登录服务器才能发邮件
553 authentication is required,163 smtp7,C8CowAA3eUZ7jEBaXE8hDg--.38995S3 1514179751

quit
221 Bye

因为邮件服务器使用ESMTP协议的缘故,必须使用同域的其它邮箱登录SMTP服务器后才能验证邮箱是否存在,这显然不是什么好的方法。

再来试试MX服务器,希望不要如此麻烦,否则的话就完蛋了~

首先找到163免费邮箱的MX服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C:\>nslookup -q=mx 163.com
Non-authoritative answer:
163.com MX preference = 10, mail exchanger = 163mx03.mxmail.netease.com
163.com MX preference = 10, mail exchanger = 163mx01.mxmail.netease.com
163.com MX preference = 10, mail exchanger = 163mx02.mxmail.netease.com
163.com MX preference = 50, mail exchanger = 163mx00.mxmail.netease.com

163.com nameserver = ns6.nease.net
163.com nameserver = ns3.nease.net
163.com nameserver = ns4.nease.net
163.com nameserver = ns1.nease.net
163.com nameserver = ns8.166.com
163.com nameserver = ns2.166.com
163.com nameserver = ns5.nease.net
163mx01.mxmail.netease.com      internet address = 220.181.14.138
163mx01.mxmail.netease.com      internet address = 220.181.14.139
163mx01.mxmail.netease.com      internet address = 220.181.14.140
163mx01.mxmail.netease.com      internet address = 220.181.14.141
163mx01.mxmail.netease.com      internet address = 220.181.14.142
163mx01.mxmail.netease.com      internet address = 220.181.14.143
163mx01.mxmail.netease.com      internet address = 220.181.14.135
163mx01.mxmail.netease.com      internet address = 220.181.14.136
163mx01.mxmail.netease.com      internet address = 220.181.14.137

找到MX服务器后,我们就可以像对待SMTP服务器那样对待它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C:\>telnet 163mx03.mxmail.netease.com 25
220 163.com Anti-spam GT for Coremail System (163com[20141201])

helo hi
250 OK

vrfy noname@163.com              # VRFY指令也处于禁用
502 Error: command not implemented

mail from: <noname@example.com>  # 注意:不同域的邮箱也能发邮件啦
250 Mail OK

rcpt to: <fewfwe>
550 Invalid User: fewfwe

rcpt to: <fewfwe@163.com>        # 邮箱不存在
550 User not found: fewfwe@163.com

rcpt to: <fake@163.com>          # 邮箱存在
250 Mail OK

quit
221 Bye

总结

  • 验证邮箱是否存在的最好方法是在MX服务器上使用RCPT TO指令。
  • 连续180天没登录过的163免费邮箱不会被注销。

写到这里才想起我手里还有以前收集的大批邮箱,即使按照上面的步骤逐个验证也不现实,看来需要找个时间码个程序来自动化批量处理它们。这次就暂时先这样子吧。

使用Scrapy爬取小说(6)

使用“下一页”链接抓取小说

在前文中,我们首先抓取小说目录页面的所有章节链接,然后再使用这些链接分别抓取各个章节的正文内容。这样需要分析两个不同页面的结构。其实,还有个更简单点的方法,只要分析内容页面的结构即可。

在小说的内容页面中通常都会有“上一页”和“下一页”这样的链接。我们只需要先抓取小说的首个章节,然后抽取出它的“下一页”链接,接着抓取这个链接对应的章节,再抽取出这个章节的“下一页”链接,重复这个循环直到不再有“下一页”链接为止。

抽取“下一页”链接的XPath表达式是:

1
//a[text()='下一页']/@href

修改后的novel_spider.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class NovelSpider(scrapy.Spider):
  name = 'novelspider'
  allowed_domains = ['example.com']
  # 把原来的目录页链接改成第一章的链接
  start_urls = ['http://example.com/wuxia/hzlz/ssjx/001.htm']

  def parse(self, response):
    # 有些小说网站的页眉和页脚都有“下一页”链接,我们
    # 不需要管它有几个,只要获取第一个出现的就行。
    link = response.xpath("//a[text()='下一页']/@href").extract_first()
    next_url = response.urljoin(link)
    yield scrapy.Request(next_url, callback=self.parse)  # 注意callback的值

    item = NovelItem()
    title = response.xpath('//center/table/tr[2]/td/text()').extract()
    content = response.xpath('//center/table/tr[4]/td/text()').extract()
    item['title'] = title
    item['content'] = content
    return item

满怀愉悦地运行Spider,结果什么都没有,抓取失败。这是怎么回事?百思不得其解。

郁闷之旅由此开始~

把yield关键词删掉试试?!修改后的代码如下:

1
2
3
link = response.xpath("//a[text()='下一页']/@href").extract_first()
next_url = response.urljoin(link)
scrapy.Request(next_url, callback=self.parse)

结果只输出第一章的内容。

把parse方法改成下面这样呢?

1
2
3
4
5
6
7
8
9
10
11
def parse(self, response):
  item = NovelItem()
  title = response.xpath('//center/table/tr[2]/td/text()').extract()
  content = response.xpath('//center/table/tr[4]/td/text()').extract()
  item['title'] = title
  item['content'] = content
  yield item

  link = response.xpath("//a[text()='下一页']/@href").extract_first()
  next_url = response.urljoin(link)
  return scrapy.Request(next_url, callback=self.parse)

也只能输出第一章的内容。

读到这里可能有看官会疑惑,为什么我会做出上面这两种修改?首先,我知道问题是在yield语句那里。其次,因为在我的印象中,yield的作用是延迟执行后面的语句。但现在从实际执行的情况来看显然不是如此。

真实的yield

查阅资料以后发现,包含yield的函数不再是普通函数,Python解释器会将其视为生成器。我们用个例子来详细说明下。

Fibonacci数列是个非常简单的递归数列,除第1和第2个数外,任何一个数都可以由前两个数相加得到。下面是输出Fibonacci数列前N个数的测试代码:

1
2
3
4
5
6
7
8
9
def fab(max):
  n = 0
  a, b = 0, 1
  while n < max:
    yield b
    a, b = b, a + b
    n = n + 1

print(fab(5))

猜猜会输出什么?有人以为会打印出Fibonacci数列,但实际上输出的是生成器对象的信息:

1
<generator object fab at 0x7fd41edfcd58>

fab(5)看起来像函数调用,但它其实是返回一个生成器对象。在对其调用next函数之前,它不会执行任何函数代码。虽然仍按函数的流程执行,但每执行到一条yield语句就会中断,并返回一个迭代值,下次执行时从yield处继续执行。看起来就好像一个函数在正常执行的过程中被yield中断了数次,每次中断都会通过yield返回当时的迭代值。

所以正确的打印代码应该这样写(for循环会自动对生成器对象调用next函数):

1
2
for n in fab(5)
  print(n)

也可以手动调用next函数,这样我们就可以更清楚地看到它的执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> f = fab(5)
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

当函数执行结束时,生成器自动抛出StopIteration异常,表示迭代完成。在for循环中,无需处理StopIteration异常,循环会正常结束。

正确的解决方案

明白yield到底起什么作用后,再回过头来看前面的修改。

第2次修改明显是没有思考,胡乱盲目地尝试。它只是创建一个Request对象而已,能有什么用!代码也清楚地表明,parse方法仅会被回调1次,这也是仅能输出第一章内容的原因。现在回想起来 怎么也弄不明白当初为什么会这么写?!

第3次修改的代码首先是解析Response对象,把第一章的标题和内容封装起来返回给Scrapy。因为这个函数是生成器函数,所以Scrapy在输出第一章内容后会继续执行yield item后面的语句,直到函数结束,抛出StopIteration异常。因为有return语句,所以会把返回的值当作StopIteration异常的属性。

第1次修改和第3次修改类似,不同的是它返回“下一页”的Request对象给Scrapy,所以parse方法会被不断地循环回调,直到没有“下一页”为止。

归根结底,第1和第3次修改不能输出所有章节内容的原因是因为StopIteration异常吞掉了return返回的值,所以也要把它改成yield。修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
def parse(self, response):
  item = NovelItem()
  title = response.xpath('//center/table/tr[2]/td/text()').extract()
  content = response.xpath('//center/table/tr[4]/td/text()').extract()
  item['title'] = title
  item['content'] = content
  yield item

  link = response.xpath("//a[text()='下一页']/@href").extract_first()
  next_url = response.urljoin(link)
  yield scrapy.Request(next_url, callback=self.parse)

给代码做个美容:

1
2
3
4
5
6
7
8
9
def parse(self, response):
  yield {
    'title': response.xpath('//center/table/tr[2]/td/text()').extract(),
    'content': response.xpath('//center/table/tr[4]/td/text()').extract(),
  }

  link = response.xpath("//a[text()='下一页']/@href").extract_first()
  next_url = response.urljoin(link)
  yield scrapy.Request(next_url, callback=self.parse)

打完收工。写文章比写代码累多了!

使用Scrapy爬取小说(5)

重构代码以提升健壮性

今天的任务是重构TxtPipeline。

先看下TxtPipeline中负责写文件的代码片段:

1
2
3
4
f = open(filename, 'w')
f.write(title)
f.write(content)
f.close()

我们经常会看到这样的代码,但它存在严重的问题,你能把它找出来吗?

首先,open函数打开文件,并返回文件句柄,该句柄是由操作系统分配的。接着就是调用write方法写文件。最后是调用close方法关闭文件。文件使用完毕后必须关闭,因为文件句柄会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的。

由于文件读写时都有可能产生IOError,一旦出错,后面的close方法就不会被调用。所以,为确保无论是否出错都能正常地关闭文件和释放文件句柄,我们需要使用如下方法来实现:

1
2
3
4
5
6
7
try:
  f = open(filename, 'w')
  f.write(title)
  f.write(content)
finally:
  if f:
    f.close()

但每次都这么写实在太繁琐。其实,我们可以使用with语句来帮我们自动管理文件资源:

1
2
3
with open(filename, 'w') as f:
  f.write(title)
  f.write(content)

这和前面的try-finally是一样的,但是代码更加简洁,而且不必调用close方法。

使用Scrapy爬取小说(4)

重构代码以提升可移植性

在前文中,我们使用range(1, 310)来确定小说章节链接的范围,这很不好。我们编程的时候应该要注意尽量避免代码中的硬编码和魔数,提高代码的可移植性。如果小说章节的链接不是这种连续的数字,或者章节的数量是在逐步增加的,那么这段代码就是无效的,或者会慢慢变得无效。

如何才能把这段代码写得更具可移植性呢?

novel-chapter-urls

我们不必关心章节链接的格式,我们只要知道它是个链接,一定是以<a href="url">text</a>这种形式呈现(如上图所示)。我们也不必关心章节数量是否变化,只要把所有这种形式的链接抓取下来即可。与此对应的XPath表达式是:

1
//center/table[@bordercolorlight]//a/@href

因为页面文档中可能有多个表格,所以要在table后面添加@bordercolorlight属性来指定我们要查找的那个。

那么在Scrapy中如何实现这样的能力呢?以下是具体的代码:

1
2
3
4
5
def parse(self, response):
  links = response.xpath('//center/table[@bordercolorlight]//a/@href').extract()
  for link in links:
    next = response.urljoin(link)
    yield scrapy.Request(next, callback=self.parse_chapter)

使用Scrapy爬取小说(3)

将小说保存到MongoDB

在前文中,我们将小说的每个章节保存为独立的文本文件。今天我们准备把小说内容输出到数据库。对于数据存储,我选择MongoDB。为什么是MongoDB而不是其它?原因是以前没用过,想尝试下。

现在我们已经知道,要把抓取来的数据保存到数据库,只需实现Item Pipeline即可。我们可以仿照前面的实现依葫芦画瓢。

以下是将小说内容保存到MongoDB的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pymongo

class MongoPipeline(object):
  def open_spider(self, spider):
    self.client = pymongo.MongoClient('localhost', 27017)
    self.novel = self.client['novel']
    self.ssjx = self.novel['ssjx']

  def process_item(self, item, spider):
    data = {
      # 标题和内容都是列表类型,必须先转换成字符串
      'title' : ''.join(item['title']),
      'content' : ''.join(item['content']),
    }
    self.ssjx.insert(data)
    return item

  def close_spider(self, spider):
    self.client.close()

将组件添加到novel/settings.py的ITEM_PIPELINES配置中以启用它:

1
2
3
4
ITEM_PIPELINES = {
  'novel.pipelines.TxtPipeline' : 300,
  'novel.pipelines.MongoPipeline' : 400,
}

在项目的根目录中使用下面的指令运行Spider:

1
scrapy crawl novelspider

如果没有问题的话,爬虫会不停地运行,小说的章节内容也会被一个个地保存到数据库。下面的截图是最终的抓取结果:

novel-mongo-gui

使用Scrapy爬取小说(2)

将小说按章节保存为文本文件

在前文中,我们通过-o选项将抓取的小说内容保存成本地文件。虽然它工作的很好,但是有两个缺点:一是把所有小说内容保存到单个文件会导致该文件太大,用文本编辑器打开随机浏览的速度非常慢;二是小说章节不是按照顺序保存的,导致阅读指定的章节内容很不方便。

再写个小工具按章节内容分割小说文件?无需如此麻烦。我们可以在Scrapy中直接将每个章节保存为单独的文本文件。Scrapy中的Item Pipeline就是干这类事情的。看下面的Scrapy架构图:

scrapy-architecture

当Item在Spider中被收集之后,它们会被传递到Item Pipeline,这些Pipeline组件按照一定的顺序执行对Item的处理,同时也决定此Item是否继续通过,或是被丢弃而不再进行处理。

以下是Item Pipeline的一些典型应用:

  • 清理HTML数据
  • 验证爬取的数据
  • 查重
  • 将爬取结果保存到数据库中

编写Item Pipeline

编写自己的Item Pipeline非常简单,每个Item Pipeline都是实现以下方法的Python类:

1
process_item(self, item, spider)

此外,下面的方法是可选实现的:

1
2
open_spider(self, spider)  # 该方法在Spider被开启时调用
close_spider(spider)       # 该方法在Spider被关闭时调用

明白原理后,我们就可以开始编写自己的Item Pipeline。以下就是将小说的每个章节写成单独文本文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TxtPipeline(object):
  def process_item(self, item, spider):
    # 标题和内容都是列表类型,必须先转换成字符串
    title = ''.join(item['title'])
    content = ''.join(item['content'])
    # 使用章节名来创建文件
    # 使用strip()来过滤非法字符r'\/:*?"<>|'
    filename = '{}.txt'.format(title.strip())
    f = open(filename, 'w')
    f.write(title)
    f.write(content)
    f.close()
    return item

启用Item Pipeline

要启用Pipeline组件,你必须将它添加到novel/settings.py的ITEM_PIPELINES配置中,就像下面这样:

1
2
3
ITEM_PIPELINES = {
  'novel.pipelines.TxtPipeline' : 300,
}

Pipeline后面的整数值确定它们的运行顺序,Item按数字从低到高通过每个Pipeline。通常将这些值定义在0-1000范围内。

运行Spider

在项目的根目录中执行如下的命令(因为不再把所有的小说内容保存为单个文件,所有不需要指定-o选项):

1
scrapy crawl novelspider

没有报错的话,等个几分钟,就能看到很多文本文件躺在自己的电脑上面。

novel-txt-list

使用Scrapy爬取小说(1)

这几天正在看《Python网络数据采集》,在这过程中觉得有必要写个爬虫来实践学到的知识。便给自己定个小目标:试着用Scrapy爬取小说《蜀山剑侠传》,并把内容保存到本地文件中。

Scrapy是一个开源的Python数据抓取框架,速度快且强大,而且使用简单,可以很方便地抓取网站页面并从中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。

好吧,废话不多说,让我们直接开干!

创建项目

在抓取之前,必须先创建一个Scrapy项目,可以直接用以下命令生成:

1
scrapy startproject novel

这是新建项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── novel                # 项目模块
│   ├── __init__.py
│   ├── items.py         # 定义爬取的数据
│   ├── middlewares.py   # 定义爬取时的中间件
│   ├── pipelines.py     # 定义数据管道
│   ├── __pycache__
│   ├── settings.py      # 项目的设置文件
│   └── spiders          # 放置爬虫代码的文件夹
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg           # Scrapy部署时的配置文件

分析页面结构

主要分析两个页面。一是小说的目录页面,目的是获取小说所有章节的链接以备抓取。二是任意章节页面,用于爬取其中的标题和正文。

通过观察目录页面的源码可以发现,所有章节的链接都类似NUMBER.htm。其中,NUMBER是3位整数,从001到309。

novel-chapter-urls

使用Firefox浏览器的检查器(Inspector)查看章节页面,尝试把光标放在正文上,你应该可以看到正文周围的蓝色方块(如下图左侧所示),如果你点击这个方块,就可以选中检查器面板中相对应的HTML代码。可以看到小说的标题和正文都在td标签内。

novel-page-inspector

与此对应的XPath表达式分别是:

1
2
//center/table/tr[2]/td/text()  # 标题的XPath路径
//center/table/tr[4]/td/text()  # 正文的XPath路径

需要注意的是,上面XPath表达式里的中括号内的数字为节点索引,是从1开始的,而不是0。

定义爬取的数据

当需要从某个网站抓取信息时,首先是定义我们要爬取的数据。在Scrapy中,可以通过Item来完成。以下是我们定义的Item:

1
2
3
4
5
import scrapy

class NovelItem(scrapy.Item):
  title = scrapy.Field()
  content = scrapy.Field()

编写爬取数据的Spider

现在我们需要添加一个爬虫来真正做点什么。创建文件novel/spiders/novel_spider.py,添加如下内容:

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
import scrapy
from novel.items import NovelItem

class NovelSpider(scrapy.Spider):
  name = 'novelspider'
  allowed_domains = ['example.com']
  start_urls = ['http://example.com/wuxia/hzlz/ssjx/']

  def parse(self, response):
    # 还记得前面分析目录页面时的结果吗:000、001...309。
    for i in range(1, 310):
      # 生成每个章节的绝对链接
      next = response.urljoin('{0:03d}.htm'.format(i))
      # 生成新的请求对象解析小说的标题和正文
      yield scrapy.Request(next, callback=self.parse_chapter)

  def parse_chapter(self, response):
    item = NovelItem()
    title = response.xpath('//center/table/tr[2]/td/text()').extract()
    print('Title is', title)
    content = response.xpath('//center/table/tr[4]/td/text()').extract()
    print('Content is', content)
    item['title'] = title
    item['content'] = content
    return item

运行Spider

完成爬虫后,如何通过它来得到我们想要的结果呢?在项目的根目录中执行如下的命令:

1
scrapy crawl novelspider -o novel.json

没有报错的话,等个几分钟,就能看到一个完整的JSON数据文件躺在自己的电脑上面。

不过如果打开的话,可能只会看到“\uXXXX”这样的乱码,它们都是中文字符的Unicode编码。要直接显示成中文的话,需要在novel/settings.py中添加以下设置:

1
FEED_EXPORT_ENCODING = 'utf-8'

最终的结果如图:

novel-json-chinese

软件版本号的思考

software-version

产品的名称用来表明产品目的,通常在产品的整个生命周期中都使用它。软件产品的版本号则用来表明产品在特定时间段内所拥有的功能集。它们关注的是以何种方式对提供给用户的软件版本进行识别。

版本号用于捕获相关产品修订和变种(通常与可移植性、国际化或者性能特征相关)的信息。目标是采用尽量少的标识符去收集所有信息。遗憾的是,在工业生产中还没有一种标准的版本编号机制,或者说,不存在单一的、通用的算法。更多情况是,需要你针对发布的内容和目标群体做些细微不同的调整。

但不管你的目标群体是什么,完全发布最好通过以下方式进行标识。这是经过不断地反复实践,被各种封闭、开源软件所广泛使用的惯例。

  • 软件的名称。
  • 用x.y.z.build四位元组去捕捉修订信息。
  • 根据需要生成的任意变种信息。

版本编号中的元组定义:

元组 定义
x 主版本号。表示产品的当前主要版本。用来表示提供给客户的产品功能的主要增强。在一个极端的例子中,主版本号的增加用来说明产品现在已经拥有一个全新的功能类。
y 次版本号。表示给产品新增了一些特征,或者是在原来文档中描述的特征上做了重要的修改。用来确定次版本号什么时候需要修改的一个衡量标准就是产品功能说明书。
z 修订版本号。用来表示给产品所做的缺陷维护行为的等级。产品缺陷是在产品的功能说明书中没有定义,并且已经或者可能对产品的使用者造成不利影响的任何行为。缺陷维护可以看作是支持该版本功能说明的一切活动。
build 构建版本号。一般是编译器在编译过程中自动生成。

除z和build的意义比较明确外,对x和y的解释都太笼统。我们需要更加详细的说明以指导我们的开发工作。

修订过的版本编号中的元组定义:

元组 定义
x 主版本号。用于有扩展性的、客户可见的架构上或特性上的改变。以一个管理大型数据库的系统为例,你可能在以下情况时需要定义一个主要的版本发布:
* 改变数据库的结构,导致单纯的升级系统对客户产生比较严重的影响。
* 改变已经发布的API,导致它与前一版本不兼容。
* 删除功能(好的架构师应该删除一些不需要的功能)。
* 持续增加新的功能,例如对新的操作系统的支持。
x的增加也可以是出于纯粹的商业理由。例如,客户的技术支持合同标明,在下个主要版本发布以后,软件可以得到18个月的技术支持。通过增加x,你将强迫客户去升级。
y 次版本号。通常与期望的功能或其它改进措施相关。当市场部门认为这个版本的一系列特性已经通过证实,次要版本号就会增加。决定增加x或y可能会比较随意。市场架构师应该定义触发任何一个增加的事件(定义与x相关的触发事件比定义y要更容易)。
z 修订版本号。主要版本号和次要版本号都相同的维护版本应该彼此兼容。
build 构建版本号。一般是编译器在编译过程中自动生成。

注意:这里的版本编号规则仅适用于发布周期较长的软件产品。如果发布周期很短,像Chrome和Firefox那样,可能就不太适用。

参考资料

  • 语义化版本
  • 《软件发布方法》
  • 《超越软件架构:创建和维护优秀解决方案》