Theano 深度学习(二)
原文:
annas-archive.org/md5/39be8fc3387902d01265692ab3d9cda6译者:飞龙
第四章 使用递归神经网络生成文本
在上一章中,你学习了如何将离散输入表示为向量,以便神经网络能够理解离散输入以及连续输入。
许多现实世界的应用涉及可变长度的输入,例如物联网和自动化(类似卡尔曼滤波器,已经更为进化);自然语言处理(理解、翻译、文本生成和图像注释);人类行为重现(文本手写生成和聊天机器人);强化学习。
之前的网络,称为前馈网络,只能对固定维度的输入进行分类。为了将它们的能力扩展到可变长度的输入,设计了一个新的网络类别:递归神经网络(RNN),非常适合用于处理可变长度输入或序列的机器学习任务。
本章介绍了三种著名的递归神经网络(简单 RNN、GRU 和 LSTM),并以文本生成作为示例。 本章涵盖的主题如下:
-
序列的案例
-
递归网络的机制
-
如何构建一个简单的递归网络
-
时间反向传播
-
不同类型的 RNN、LSTM 和 GRU
-
困惑度和词错误率
-
在文本数据上进行训练以生成文本
-
递归网络的应用
递归神经网络的需求
深度学习网络在自然语言处理中的应用是数值化的,能够很好地处理多维数组的浮点数和整数作为输入值。对于类别值,例如字符或单词,上一章展示了一种称为嵌入(embedding)的技术,将它们转换为数值。
到目前为止,所有的输入都是固定大小的数组。在许多应用中,如自然语言处理中的文本,输入有一个语义含义,但可以通过可变长度的序列来表示。
需要处理可变长度的序列,如下图所示:
递归神经网络(RNN)是应对可变长度输入的解决方案。
递归可以看作是在不同时间步长上多次应用前馈网络,每次应用时使用不同的输入数据,但有一个重要区别,那就是存在与过去时间步长的连接,目标是通过时间不断优化输入的表示。
在每个时间步长,隐藏层的输出值代表网络的中间状态。
递归连接定义了从一个状态到另一个状态的转换,给定输入的情况下,以便不断优化表示:
递归神经网络适用于涉及序列的挑战,如文本、声音和语音、手写文字以及时间序列。
自然语言的数据集
作为数据集,可以使用任何文本语料库,例如 Wikipedia、网页文章,甚至是包含代码或计算机程序、戏剧或诗歌等符号的文本;模型将捕捉并重现数据中的不同模式。
在这种情况下,我们使用微型莎士比亚文本来预测新的莎士比亚文本,或者至少是风格上受到莎士比亚启发的新文本;有两种预测层次可以使用,但可以以相同的方式处理:
-
在字符级别:字符属于一个包含标点符号的字母表,给定前几个字符,模型从字母表中预测下一个字符,包括空格,以构建单词和句子。预测的单词不需要属于字典,训练的目标是构建接近真实单词和句子的内容。
-
在单词级别:单词属于一个包含标点符号的字典,给定前几个单词,模型从词汇表中预测下一个单词。在这种情况下,单词有强烈的约束,因为它们属于字典,但句子没有这种约束。我们期望模型更多地关注捕捉句子的语法和意义,而不是字符级别的内容。
在这两种模式下,token 表示字符/单词;字典、字母表或词汇表表示(token 的可能值的列表);
流行的 NLTK 库,一个 Python 模块,用于将文本分割成句子并将其标记化为单词:
conda install nltk
在 Python shell 中,运行以下命令以下载 book 包中的英语分词器:
import nltk
nltk.download("book")
让我们解析文本以提取单词:
from load import parse_text
X_train, y_train, index_to_word = parse_text("data/tiny-shakespear.txt", type="word")
for i in range(10):
print "x", " ".join([index_to_word[x] for x in X_train[i]])
print "y"," ".join([index_to_word[x] for x in y_train[i]])
*Vocabulary size 9000*
*Found 12349 unique words tokens.*
*The least frequent word in our vocabulary is 'a-fire' and appeared 1 times.*
*x START first citizen : before we proceed any further , hear me speak .*
*y first citizen : before we proceed any further , hear me speak . END*
*x START all : speak , speak .*
*y all : speak , speak . END*
*x START first citizen : you are all resolved rather to die than to famish ?*
*y first citizen : you are all resolved rather to die than to famish ? END*
*x START all : resolved .*
*y all : resolved . END*
*x START resolved .*
*y resolved . END*
*x START first citizen : first , you know caius marcius is chief enemy to the people .*
*y first citizen : first , you know caius marcius is chief enemy to the people . END*
*x START all : we know't , we know't .*
*y all : we know't , we know't . END*
*x START first citizen : let us kill him , and we 'll have corn at our own price .*
*y first citizen : let us kill him , and we 'll have corn at our own price . END*
*x START is't a verdict ?*
*y is't a verdict ? END*
*x START all : no more talking o n't ; let it be done : away , away !*
*y all : no more talking o n't ; let it be done : away , away ! END*
或者 char 库:
from load import parse_text
X_train, y_train, index_to_char = parse_text("data/tiny-shakespear.txt", type="char")
for i in range(10):
print "x",''.join([index_to_char[x] for x in X_train[i]])
print "y",''.join([index_to_char[x] for x in y_train[i]])
*x ^first citizen: before we proceed any further, hear me speak*
*y irst citizen: before we proceed any further, hear me speak.$*
*x ^all: speak, speak*
*y ll: speak, speak.$*
*x ^first citizen: you are all resolved rather to die than to famish*
*y irst citizen: you are all resolved rather to die than to famish?$*
*x ^all: resolved*
*y ll: resolved.$*
*x ^resolved*
*y esolved.$*
*x ^first citizen: first, you know caius marcius is chief enemy to the people*
*y irst citizen: first, you know caius marcius is chief enemy to the people.$*
*x ^all: we know't, we know't*
*y ll: we know't, we know't.$*
*x ^first citizen: let us kill him, and we'll have corn at our own price*
*y irst citizen: let us kill him, and we'll have corn at our own price.$*
*x ^is't a verdict*
*y s't a verdict?$*
*x ^all: no more talking on't; let it be done: away, away*
*y ll: no more talking on't; let it be done: away, away!$*
额外的开始标记(START 单词和 ^ 字符)避免了预测开始时产生空的隐藏状态。另一种解决方案是用 初始化第一个隐藏状态。
额外的结束标记(END 单词和 $ 字符)帮助网络学习在序列生成预测完成时预测停止。
最后,out of vocabulary 标记(UNKNOWN 单词)替换那些不属于词汇表的单词,从而避免使用庞大的词典。
在这个示例中,我们将省略验证数据集,但对于任何实际应用程序,将一部分数据用于验证是一个好的做法。
同时,请注意,第二章 中的函数,使用前馈网络分类手写数字 用于层初始化 shared_zeros 和 shared_glorot_uniform,以及来自第三章,将单词编码为向量 用于模型保存和加载的 save_params 和 load_params 已被打包到 utils 包中:
from theano import *
import theano.tensor as T
from utils import shared_zeros, shared_glorot_uniform,save_params,load_params
简单的递归网络
RNN 是在多个时间步上应用的网络,但有一个主要的区别:与前一个时间步的层状态之间的连接,称为隐状态!简单递归网络:
这可以写成以下形式:
RNN 可以展开为一个前馈网络,应用于序列!简单递归网络作为输入,并在不同时间步之间共享参数。
输入和输出的第一个维度是时间,而后续维度用于表示每个步骤中的数据维度。正如上一章所见,某一时间步的值(一个单词或字符)可以通过索引(整数,0 维)或独热编码向量(1 维)表示。前者在内存中更加紧凑。在这种情况下,输入和输出序列将是 1 维的,通过一个向量表示,且该维度为时间:
x = T.ivector()
y = T.ivector()
训练程序的结构与第二章中的用前馈网络分类手写数字相同,只是我们定义的模型与递归模块共享相同的权重,适用于不同的时间步:
embedding_size = len(index_)
n_hidden=500
让我们定义隐状态和输入权重:
U = shared_glorot_uniform(( embedding_size,n_hidden), name="U")
W = shared_glorot_uniform((n_hidden, n_hidden), name="W")
bh = shared_zeros((n_hidden,), name="bh")
以及输出权重:
V = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
by = shared_zeros((embedding_size,), name="by")
params = [U,V,W,by,bh]
def step(x_t, h_tm1):
h_t = T.tanh(U[x_t] + T.dot( h_tm1, W) + bh)
y_t = T.dot(h_t, V) + by
return h_t, y_t
初始状态可以在使用开始标记时设为零:
h0 = shared_zeros((n_hidden,), name='h0')
[h, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, None], truncate_gradient=10)
它返回两个张量,其中第一个维度是时间,第二个维度是数据值(在这种情况下为 0 维)。
通过扫描函数进行的梯度计算在 Theano 中是自动的,并遵循直接连接和递归连接到前一个时间步。因此,由于递归连接,某一特定时间步的错误会传播到前一个时间步,这种机制被称为时间反向传播(BPTT)。
已观察到,在过多时间步后,梯度会爆炸或消失。这就是为什么在这个例子中,梯度在 10 个步骤后被截断,并且错误不会反向传播到更早的时间步。
对于剩余的步骤,我们保持之前的分类方式:
model = T.nnet.softmax(y_pred)
y_out = T.argmax(model, axis=-1)
cost = -T.mean(T.log(model)[T.arange(y.shape[0]), y])
这将在每个时间步返回一个值的向量。
LSTM 网络
RNN 的主要困难之一是捕捉长期依赖关系,这是由于梯度消失/爆炸效应和截断反向传播。
为了克服这个问题,研究人员已经在寻找一长串潜在的解决方案。1997 年设计了一种新型的递归网络,带有一个记忆单元,称为细胞状态,专门用于保持和传输长期信息。
在每个时间步,单元值可以部分通过候选单元更新,并通过门控机制部分擦除。两个门,更新门和忘记门,决定如何更新单元,给定先前的隐藏状态值和当前输入值:
候选单元的计算方式相同:
新的单元状态的计算方式如下:
对于新的隐藏状态,输出门决定要输出单元值中的哪些信息:
其余部分与简单 RNN 保持相同:
该机制允许网络存储一些信息,并在未来比简单 RNN 更远的时间点使用这些信息。
许多 LSTM 设计的变体已经被设计出来,你可以根据你的问题来测试这些变体,看看它们的表现。
在这个例子中,我们将使用一种变体,其中门和候选值同时使用了先前的隐藏状态和先前的单元状态。
在 Theano 中,让我们为以下内容定义权重:
- 输入门:
W_xi = shared_glorot_uniform(( embedding_size,n_hidden))
W_hi = shared_glorot_uniform(( n_hidden,n_hidden))
W_ci = shared_glorot_uniform(( n_hidden,n_hidden))
b_i = shared_zeros((n_hidden,))
- 忘记门:
W_xf = shared_glorot_uniform(( embedding_size, n_hidden))
W_hf = shared_glorot_uniform(( n_hidden,n_hidden))
W_cf = shared_glorot_uniform(( n_hidden,n_hidden))
b_f = shared_zeros((n_hidden,))
- 输出门:
W_xo = shared_glorot_uniform(( embedding_size, n_hidden))
W_ho = shared_glorot_uniform(( n_hidden,n_hidden))
W_co = shared_glorot_uniform(( n_hidden,n_hidden))
b_o = shared_zeros((n_hidden,))
- 单元:
W_xc = shared_glorot_uniform(( embedding_size, n_hidden))
W_hc = shared_glorot_uniform(( n_hidden,n_hidden))
b_c = shared_zeros((n_hidden,))
- 输出层:
W_y = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
b_y = shared_zeros((embedding_size,), name="by")
所有可训练参数的数组:
params = [W_xi,W_hi,W_ci,b_i,W_xf,W_hf,W_cf,b_f,W_xo,W_ho,W_co,b_o,W_xc,W_hc,b_c,W_y,b_y]
要放置在循环中的步进函数:
def step(x_t, h_tm1, c_tm1):
i_t = T.nnet.sigmoid(W_xi[x_t] + T.dot(W_hi, h_tm1) + T.dot(W_ci, c_tm1) + b_i)
f_t = T.nnet.sigmoid(W_xf[x_t] + T.dot(W_hf, h_tm1) + T.dot(W_cf, c_tm1) + b_f)
c_t = f_t * c_tm1 + i_t * T.tanh(W_xc[x_t] + T.dot(W_hc, h_tm1) + b_c)
o_t = T.nnet.sigmoid(W_xo[x_t] + T.dot(W_ho, h_tm1) + T.dot(W_co, c_t) + b_o)
h_t = o_t * T.tanh(c_t)
y_t = T.dot(h_t, W_y) + b_y
return h_t, c_t, y_t
让我们使用扫描操作符创建循环神经网络:
h0 = shared_zeros((n_hidden,), name='h0')
c0 = shared_zeros((n_hidden,), name='c0')
[h, c, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, c0, None], truncate_gradient=10)
门控循环网络
GRU 是 LSTM 的替代方法,它简化了机制,不使用额外的单元:
构建门控循环网络的代码仅需定义权重和 step 函数,如前所述:
- 更新门的权重:
W_xz = shared_glorot_uniform(( embedding_size,n_hidden))
W_hz = shared_glorot_uniform(( n_hidden,n_hidden))
b_z = shared_zeros((n_hidden,))
- 重置门的权重:
W_xr = shared_glorot_uniform(( embedding_size,n_hidden))
W_hr = shared_glorot_uniform(( n_hidden,n_hidden))
b_r = shared_zeros((n_hidden,))
- 隐藏层的权重:
W_xh = shared_glorot_uniform(( embedding_size,n_hidden))
W_hh = shared_glorot_uniform(( n_hidden,n_hidden))
b_h = shared_zeros((n_hidden,))
- 输出层的权重:
W_y = shared_glorot_uniform(( n_hidden, embedding_size), name="V")
b_y = shared_zeros((embedding_size,), name="by")
可训练参数:
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_y, b_y]
步进函数:
def step(x_t, h_tm1):
z_t = T.nnet.sigmoid(W_xz[x_t] + T.dot(W_hz, h_tm1) + b_z)
r_t = T.nnet.sigmoid(W_xr[x_t] + T.dot(W_hr, h_tm1) + b_r)
can_h_t = T.tanh(W_xh[x_t] + r_t * T.dot(W_hh, h_tm1) + b_h)
h_t = (1 - z_t) * h_tm1 + z_t * can_h_t
y_t = T.dot(h_t, W_y) + b_y
return h_t, y_t
循环神经网络:
h0 = shared_zeros((n_hidden,), name='h0')
[h, y_pred], _ = theano.scan(step, sequences=x, outputs_info=[h0, None], truncate_gradient=10)
介绍了主要网络后,我们将看看它们在文本生成任务中的表现。
自然语言性能指标
词错误率 (WER) 或 字符错误率 (CER) 等同于自然语言准确度错误的定义。
语言模型的评估通常通过困惑度来表示,困惑度简单地定义为:
训练损失对比
在训练过程中,学习率在经过一定数量的 epochs 后可能会变强,用于微调。当损失不再减小时,减少学习率将有助于训练的最后步骤。为了减少学习率,我们需要在编译时将其定义为输入变量:
lr = T.scalar('learning_rate')
train_model = theano.function(inputs=[x,y,lr], outputs=cost,updates=updates)
在训练过程中,我们调整学习率,如果训练损失没有改善,则减小学习率:
if (len(train_loss) > 1 and train_loss[-1] > train_loss[-2]):
learning_rate = learning_rate * 0.5
作为第一个实验,让我们看看隐藏层大小对简单 RNN 训练损失的影响:
更多的隐藏单元可以提高训练速度,最终可能表现更好。为了验证这一点,我们应该运行更多的 epochs。
比较不同网络类型的训练,在这种情况下,我们没有观察到 LSTM 和 GRU 有任何改善:
这可能是由于truncate_gradient选项,或者因为问题过于简单,不依赖于记忆。
另一个需要调整的参数是词汇出现在词典中的最小次数。更高的次数会学习到更频繁的词,这样更好。
预测示例
让我们用生成的模型预测一个句子:
sentence = [0]
while sentence[-1] != 1:
pred = predict_model(sentence)[-1]
sentence.append(pred)
print(" ".join([ index_[w] for w in sentence[1:-1]]))
请注意,我们选择最有可能的下一个词(argmax),同时,为了增加一些随机性,我们必须根据预测的概率抽取下一个词。
在 150 个 epoch 时,虽然模型仍未完全收敛到我们对莎士比亚文笔的学习上,我们可以通过初始化几个单词来玩转预测,并看到网络生成句子的结尾:
-
第一市民:一句话,我知道一句话
-
现在怎么了!
-
你不觉得这样睡着了,我说的是这个吗?
-
锡尼乌斯:什么,你是我的主人吗?
-
好吧,先生,来吧。
-
我自己已经做过
-
最重要的是你在你时光中的状态,先生
-
他将不会这样做
-
祈求你,先生
-
来吧,来吧,你
-
乌鸦?
-
我会给你
-
什么,嘿!
-
考虑你,先生
-
不再!
-
我们走吧,或者你的未知未知,我做我该做的事
-
我们现在不是
从这些例子中,我们可以看出,模型学会了正确地定位标点符号,在正确的位置添加句点、逗号、问号或感叹号,从而正确地排序直接宾语、间接宾语和形容词。
原文由短句组成,风格类似莎士比亚。更长的文章,如维基百科页面,以及通过进一步训练并使用验证集来控制过拟合,将生成更长的文本。第十章,使用先进的 RNN 预测时间序列:将教授如何使用先进的 RNN 预测时间序列,并展示本章的进阶版本。
RNN 的应用
本章介绍了简单的 RNN、LSTM 和 GRU 模型。这些模型在序列生成或序列理解中有广泛的应用:
-
文本生成,例如自动生成奥巴马的政治演讲(obama-rnn),例如使用关于工作的话题作为文本种子:
下午好。愿上帝保佑你。美国将承担起解决美国人民面临的新挑战的责任,并承认我们创造了这一问题。他们受到了攻击,因此必须说出在战争最后日子里的所有任务,我无法完成。这是那些依然在努力的人们的承诺,他们将不遗余力,确保美国人民能够保护我们的部分。这是一次齐心协力的机会,完全寻找向美国人民借鉴承诺的契机。事实上,身着制服的男女和我们国家数百万人的法律系统应该是我们所能承受的力量的强大支撑,我们可以增加美国人民的精神力量,并加强我们国家领导层在美国人民生活中的作用。非常感谢。上帝保佑你们,愿上帝保佑美利坚合众国。
你可以在
medium.com/@samim/obama-rnn-machine-generated-political-speeches-c8abd18a2ea0#.4nee5wafe.查看这个例子。 -
文本注释,例如,词性(POS)标签:名词、动词、助词、副词和形容词。
生成手写字:
www.cs.toronto.edu/~graves/handwriting.html -
使用 Sketch-RNN 绘图 (
github.com/hardmaru/sketch-rnn) -
语音合成:递归网络将生成用于生成每个音素的参数。在下面的图像中,时间-频率同质块被分类为音素(或字形或字母):
-
音乐生成:
-
任何序列的分类,如情感分析(积极、消极或中立情感),我们将在第五章中讨论,使用双向 LSTM 分析情感。
-
序列编码或解码,我们将在第六章中讨论,使用空间变换网络进行定位。
相关文章
你可以参考以下链接以获得更多深入的见解:
-
递归神经网络的非理性有效性,Andrej Karpathy,2015 年 5 月 21 日(
karpathy.github.io/2015/05/21/rnn-effectiveness/) -
理解 LSTM 网络,Christopher Colah 的博客,2015 年(
colah.github.io/posts/2015-08-Understanding-LSTMs/) -
使用 LSTM 进行音频分类:连接时序分类与深度语音:端到端语音识别的扩展(
arxiv.org/abs/1412.5567) -
使用递归神经网络的通用序列学习教程:
www.youtube.com/watch?v=VINCQghQRuM -
关于训练递归神经网络的难点,Razvan Pascanu,Tomas Mikolov,Yoshua Bengio,2012 年
-
递归神经网络教程:
-
RNN 简介
-
使用 Python、NumPy 和 Theano 实现 RNN
-
反向传播与时间和梯度消失问题
-
使用 Python 和 Theano 实现 GRU/LSTM RNN,Denny Britz 2015 年,
www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/
-
-
长短时记忆(LONG SHORT-TERM MEMORY),Sepp Hochreiter,Jürgen Schmidhuber,1997 年
概述
递归神经网络提供了处理离散或连续数据的变长输入和输出的能力。
在之前的前馈网络只能处理单一输入到单一输出(一对一方案)的情况下,本章介绍的递归神经网络提供了在变长和定长表示之间进行转换的可能性,新增了深度学习输入/输出的新操作方案:一对多、多对多,或多对一。
RNN 的应用范围广泛。因此,我们将在后续章节中更深入地研究它们,特别是如何增强这三种模块的预测能力,或者如何将它们结合起来构建多模态、问答或翻译应用。
特别地,在下一章中,我们将通过一个实际示例,使用文本嵌入和递归网络进行情感分析。此次还将有机会在另一个库 Keras 下复习这些递归单元,Keras 是一个简化 Theano 模型编写的深度学习库。
第五章:使用双向 LSTM 分析情感
本章更具实用性,以便更好地了解在前两章中介绍的常用循环神经网络和词嵌入。
这也是一个将读者引入深度学习新应用——情感分析的机会,这也是自然语言处理(NLP)的另一个领域。这是一个多对一的方案,其中一系列可变长度的单词必须分配到一个类别。一个类似可以使用这种方案的 NLP 问题是语言检测(如英语、法语、德语、意大利语等)。
虽然上一章展示了如何从零开始构建循环神经网络,但本章将展示如何使用基于 Theano 构建的高级库 Keras,帮助实现和训练使用预构建模块的模型。通过这个示例,读者应该能够判断何时在项目中使用 Keras。
本章将讨论以下几个要点:
-
循环神经网络和词嵌入的回顾
-
情感分析
-
Keras 库
-
双向循环神经网络
自动化情感分析是识别文本中表达的意见的问题。它通常涉及将文本分类为积极、消极和中性等类别。意见是几乎所有人类活动的核心,它们是我们行为的关键影响因素。
最近,神经网络和深度学习方法被用于构建情感分析系统。这些系统能够自动学习一组特征,以克服手工方法的缺点。
循环神经网络(RNN)在文献中已被证明是表示序列输入(如文本)的非常有用的技术。循环神经网络的一种特殊扩展——双向循环神经网络(BRNN)能够捕捉文本中的前后上下文信息。
在本章中,我们将展示一个示例,演示如何使用长短时记忆(LSTM)架构的双向循环神经网络来解决情感分析问题。我们的目标是实现一个模型,给定一段文本输入(即一系列单词),该模型试图预测其是积极的、消极的还是中性的。
安装和配置 Keras
Keras 是一个高级神经网络 API,用 Python 编写,可以在 TensorFlow 或 Theano 上运行。它的开发目的是让实现深度学习模型变得尽可能快速和简单,以便于研究和开发。你可以通过 conda 轻松安装 Keras,如下所示:
conda install keras
在编写 Python 代码时,导入 Keras 会告诉你使用的是哪个后端:
>>> import keras
*Using Theano backend.*
*Using cuDNN version 5110 on context None*
*Preallocating 10867/11439 Mb (0.950000) on cuda0*
*Mapped name None to device cuda0: Tesla K80 (0000:83:00.0)*
*Mapped name dev0 to device cuda0: Tesla K80 (0000:83:00.0)*
*Using cuDNN version 5110 on context dev1*
*Preallocating 10867/11439 Mb (0.950000) on cuda1*
*Mapped name dev1 to device cuda1: Tesla K80 (0000:84:00.0)*
如果你已经安装了 TensorFlow,它可能不会使用 Theano。要指定使用哪个后端,请编写一个 Keras 配置文件 ~/.keras/keras.json:。
{
"epsilon": 1e-07,
"floatx": "float32",
"image_data_format": "channels_last",
"backend": "theano"
}
也可以直接通过环境变量指定 Theano 后端:
KERAS_BACKEND=theano python
请注意,所使用的设备是我们在 ~/.theanorc 文件中为 Theano 指定的设备。也可以通过 Theano 环境变量来修改这些变量:
KERAS_BACKEND=theano THEANO_FLAGS=device=cuda,floatX=float32,mode=FAST_RUN python
使用 Keras 编程
Keras 提供了一套数据预处理和构建模型的方法。
层和模型是对张量的可调用函数,并返回张量。在 Keras 中,层/模块和模型没有区别:一个模型可以是更大模型的一部分,并由多个层组成。这样的子模型作为模块运行,具有输入/输出。
让我们创建一个包含两个线性层、中间加入 ReLU 非线性层并输出 softmax 的网络:
from keras.layers import Input, Dense
from keras.models import Model
inputs = Input(shape=(784,))
x = Dense(64, activation='relu')(inputs)
predictions = Dense(10, activation='softmax')(x)
model = Model(inputs=inputs, outputs=predictions)
model 模块包含用于获取输入和输出形状的方法,无论是单个输入/输出还是多个输入/输出,并列出我们模块的子模块:
>>> model.input_shape
*(None, 784)*
>>> model.get_input_shape_at(0)
*(None, 784)*
>>> model.output_shape
*(None, 10)*
>>> model.get_output_shape_at(0)
*(None, 10)*
>>> model.name
*'sequential_1'*
>>> model.input
*/dense_3_input*
>>> model.output
*Softmax.0*
>>> model.get_output_at(0)
*Softmax.0*
>>> model.layers
*[<keras.layers.core.Dense object at 0x7f0abf7d6a90>, <keras.layers.core.Dense object at 0x7f0abf74af90>]*
为了避免为每一层指定输入,Keras 提出了通过 Sequential 模块编写模型的函数式方法,以构建由多个模块或模型组成的新模块或模型。
以下模型定义与之前展示的模型完全相同,使用 input_dim 来指定输入维度,否则将无法知道该维度并生成错误:
from keras.models import Sequential
from keras.layers import Dense, Activation
model = Sequential()
model.add(Dense(units=64, input_dim=784, activation='relu'))
model.add(Dense(units=10, activation='softmax'))
model 被视为可以是更大模型的一部分的模块或层:
model2 = Sequential()
model2.add(model)
model2.add(Dense(units=10, activation='softmax'))
每个模块/模型/层都可以进行编译,然后使用数据进行训练:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(data, labels)
让我们实践一下 Keras。
SemEval 2013 数据集
让我们从准备数据开始。在本章中,我们将使用在 SemEval 2013 竞赛中用于监督任务的 Twitter 情感分类(消息级别)的标准数据集。该数据集包含 3662 条推文作为训练集,575 条推文作为开发集,1572 条推文作为测试集。该数据集中的每个样本包含推文 ID、极性(正面、负面或中性)和推文内容。
让我们下载数据集:
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_train.zip
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_dev.zip
wget http://alt.qcri.org/semeval2014/task9/data/uploads/semeval2013_task2_test_fixed.zip
unzip semeval2013_task2_train.zip
unzip semeval2013_task2_dev.zip
unzip semeval2013_task2_test_fixed.zip
A 指的是子任务 A,即消息级情感分类 我们本章研究的目标,其中 B 指的是子任务 B 的术语级情感分析。
input 目录不包含标签,仅包含推文。full 目录包含更多级别的分类,主观 或 客观。我们的关注点是 gold 或 cleansed 目录。
让我们使用脚本来转换它们:
pip install bs4
python download_tweets.py train/cleansed/twitter-train-cleansed-A.tsv > sem_eval2103.train
python download_tweets.py dev/gold/twitter-dev-gold-A.tsv > sem_eval2103.dev
python download_tweets.py SemEval2013_task2_test_fixed/gold/twitter-test-gold-A.tsv > sem_eval2103.test
文本数据预处理
正如我们所知,在 Twitter 上常常使用 URL、用户提及和话题标签。因此,我们首先需要按照以下步骤预处理推文。
确保所有的标记(tokens)之间使用空格分隔。每条推文都会被转换为小写字母。
URL、用户提及和话题标签分别被 <url>、<user> 和 <hashtag> 代替。此步骤通过 process 函数完成,它以推文为输入,使用 NLTK 的 TweetTokenizer 进行分词,进行预处理,并返回推文中的词汇(token)集合:
import re
from nltk.tokenize import TweetTokenizer
def process(tweet):
tknz = TweetTokenizer()
tokens = tknz.tokenize(tweet)
tweet = " ".join(tokens)
tweet = tweet.lower()
tweet = re.sub(r'http[s]?://(?:[a-z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-f][0-9a-f]))+', '<url>', tweet) # URLs
tweet = re.sub(r'(?:@[\w_]+)', '<user>', tweet) # user-mentions
tweet = re.sub(r'(?:\#+[\w_]+[\w\'_\-]*[\w_]+)', '<hashtag>', tweet) # hashtags
tweet = re.sub(r'(?:(?:\d+,?)+(?:\.?\d+)?)', '<number>', tweet) # numbers
return tweet.split(" ")
例如,如果我们有推文 RT @mhj: just an example! :D http://example.com #NLP,该函数的处理过程如下:
tweet = 'RT @mhj: just an example! :D http://example.com #NLP'
print(process(tweet))
返回值
[u'rt', u'\<user\>', u':', u'just', u'an', u'example', u'!', u':d', u'\<url\>', u'\<hashtag\>']
以下函数用于读取数据集,并返回一个元组列表,每个元组表示一个样本(推文,类别),其中类别是一个整数,取值为 {0, 1 或 2},定义了情感极性:
def read_data(file_name):
tweets = []
labels = []
polarity2idx = {'positive': 0, 'negative': 1, 'neutral': 2}
with open(file_name) as fin:
for line in fin:
_, _, _, _, polarity, tweet = line.strip().split("\t")
tweet = process(tweet)
cls = polarity2idx[polarity]
tweets.append(tweet)
labels.append(cls)
return tweets, labels
train_file = 'sem_eval2103.train'
dev_file = 'sem_eval2103.dev'
train_tweets, y_train = read_data(train_file)
dev_tweets, y_dev = read_data(dev_file)
现在,我们可以构建词汇表,它是一个字典,用于将每个单词映射到一个固定的索引。以下函数接收一个数据集作为输入,并返回词汇表和推文的最大长度:
def get_vocabulary(data):
max_len = 0
index = 0
word2idx = {'<unknown>': index}
for tweet in data:
max_len = max(max_len, len(tweet))
for word in tweet:
if word not in word2idx:
index += 1
word2idx[word] = index
return word2idx, max_len
word2idx, max_len = get_vocabulary(train_tweets)
vocab_size = len(word2idx)
我们还需要一个函数,将每条推文或一组推文转换为基于词汇表的索引,如果单词存在的话,否则用未知标记(索引 0)替换词汇表外(OOV)的单词,具体如下:
def transfer(data, word2idx):
transfer_data = []
for tweet in data:
tweet2vec = []
for word in tweet:
if word in word2idx:
tweet2vec.append(word2idx[word])
else:
tweet2vec.append(0)
transfer_data.append(tweet2vec)
return transfer_data
X_train = transfer(train_tweets, word2idx)
X_dev = transfer(dev_tweets, word2idx)
我们可以节省一些内存:
del train_tweets, dev_tweets
Keras 提供了一个辅助方法来填充序列,确保它们具有相同的长度,以便一批序列可以通过张量表示,并在 CPU 或 GPU 上对张量进行优化操作。
默认情况下,该方法会在序列开头进行填充,这有助于获得更好的分类结果:
from keras.preprocessing.sequence import pad_sequences
X_train = pad_sequences(X_train, maxlen=max_len, truncating='post')
X_dev = pad_sequences(X_dev, maxlen=max_len, truncating='post')
最后,Keras 提供了一个方法,通过添加一个维度,将类别转换为它们的一热编码表示:
使用 Keras 的 to_categorical 方法:
from keras.utils.np_utils import to_categorical
y_train = to_categorical(y_train)
y_dev = to_categorical(y_dev)
设计模型架构
本示例中的模型主要模块如下:
-
首先,输入句子的单词会被映射为实数向量。这个步骤称为词的向量表示或词嵌入(更多细节,请参见第三章,将单词编码为向量)。
-
然后,使用双向 LSTM 编码器将这组向量表示为一个固定长度的实值向量。这个向量总结了输入句子,并包含基于词向量的语义、句法和/或情感信息。
-
最后,这个向量通过一个 softmax 分类器,将句子分类为正面、负面或中立。
词的向量表示
词嵌入是分布式语义学的一种方法,它将单词表示为实数向量。这种表示具有有用的聚类特性,因为在语义和句法上相关的单词会被表示为相似的向量(参见第三章,将单词编码为向量)。
这一步的主要目的是将每个单词映射到一个连续的、低维的实值向量,这些向量可以作为任何模型的输入。所有单词向量被堆叠成一个矩阵 ;其中,N 是词汇表大小,d 是向量维度。这个矩阵被称为嵌入层或查找表层。嵌入矩阵可以使用预训练模型(如 Word2vec 或 Glove)进行初始化。
在 Keras 中,我们可以简单地定义嵌入层,如下所示:
from keras.layers import Embedding
d = 100
emb_layer = Embedding(vocab_size + 1, output_dim=d, input_length=max_len)
第一个参数表示词汇表大小,output_dim 是向量维度,input_length 是输入序列的长度。
让我们将此层作为输入层添加到模型中,并声明模型为顺序模型:
from keras.models import Sequential
model = Sequential()
model.add(emb_layer)
使用双向 LSTM 进行句子表示
循环神经网络具有表示序列(如句子)的能力。然而,在实际应用中,由于梯度消失/爆炸问题,使用普通的 RNN 学习长期依赖关系是困难的。如前一章所述,长短期记忆(LSTM)网络被设计为具有更持久的记忆(即状态),专门用于保持和传递长期信息,这使得它们在捕捉序列中元素之间的长期依赖关系方面非常有用。
LSTM 单元是本章所用模型的基本组件。
Keras 提供了一种方法 TimeDistributed,用于在多个时间步上克隆任何模型并使其具有递归性。但对于常用的递归单元,如 LSTM,Keras 中已经存在一个模块:
from keras.layers import LSTM
rnn_size = 64
lstm = LSTM(rnn_size, input_shape=(max_len, d))
以下内容相同:
lstm = LSTM(rnn_size, input_dim=d, input_length=max_len)
对于后续层,我们无需指定输入大小(这是因为 LSTM 层位于嵌入层之后),因此我们可以简单地定义 lstm 单元,如下所示:
lstm = LSTM(rnn_size)
最后但同样重要的是,在这个模型中,我们希望使用双向 LSTM。它已经证明能够带来更好的结果,在给定前一个词的情况下捕捉当前词的含义,以及在之后出现的词:
为了让这个单元以双向方式处理输入,我们可以简单地使用 Bidirectional,这是一个针对 RNN 的双向封装器:
from keras.layers import Bidirectional
bi_lstm = Bidirectional(lstm)
model.add(bi_lstm)
使用 softmax 分类器输出概率
最后,我们可以将从 bi_lstm 获得的向量传递给 softmax 分类器,如下所示:
from keras.layers import Dense, Activation
nb_classes = 3
fc = Dense(nb_classes)
classifier = Activation('softmax')
model.add(fc)
model.add(classifier)
现在,让我们打印出模型的摘要:
print(model.summary())
Which will end with the results:
Using Theano backend:
__________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
=========================================================================================
embedding_1 (Embedding) (None, 30, 100) 10000100 embedding_input_1[0][0]
_________________________________________________________________________________________
bidirectional_1 (Bidirectional) (None, 128) 84480 embedding_1[0][0]
__________________________________________________________________________________________
dense_1 (Dense) (None, 3) 387 bidirectional_1[0][0]
__________________________________________________________________________________________
activation_1 (Activation) (None, 3) 0 dense_1[0][0]
=========================================================================================
Total params: 10,084,967
Trainable params: 10,084,967
Non-trainable params: 0
__________________________________________________________________________________________
编译和训练模型
现在,模型已定义,准备好进行编译。要在 Keras 中编译模型,我们需要确定优化器、损失函数,并可选地指定评估指标。如前所述,问题是预测推文是正面、负面还是中立的。这是一个多类别分类问题。因此,在这个示例中使用的损失(或目标)函数是 categorical_crossentropy。我们将使用 rmsprop 优化器和准确率评估指标。
在 Keras 中,您可以找到实现的最先进的优化器、目标函数和评估指标。使用编译函数在 Keras 中编译模型非常简单:
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
我们已经定义并编译了模型,现在它已经准备好进行训练。我们可以通过调用 fit 函数在定义的数据上训练或拟合模型。
训练过程会经过若干次数据集迭代,称为 epochs,可以通过epochs参数来指定。我们还可以使用batch_size参数设置每次训练时输入给模型的实例数。在本例中,我们将使用较小的epochs = 30,并使用较小的批次大小10。我们还可以通过显式地使用validation_data参数输入开发集来在训练过程中评估模型,或者通过validation_split参数选择训练集的一个子集。在本例中,我们将使用之前定义的开发集:
model.fit(x=X_train, y=y_train, batch_size=10, epochs=30, validation_data=[X_dev, y_dev])
评估模型
我们已经在训练集上训练了模型,现在可以评估网络在测试集上的性能。可以使用evaluation()函数来完成这一操作。该函数返回模型在测试模式下的损失值和指标值:
test_file = 'sem_eval2103.test'
test_tweets, y_test = read_data(test_file)
X_test = transfer(test_tweets, word2idx)
del test_twee
X_test = pad_sequences(X_test, maxlen=max_len, truncating='post')
y_test = to_categorical(y_test)
test_loss, test_acc = model.evaluate(X_test, y_test)
print("Testing loss: {:.5}; Testing Accuracy: {:.2%}" .format(test_loss, test_acc))
保存和加载模型
要保存 Keras 模型的权重,只需调用save函数,模型将序列化为.hdf5格式:
model.save('bi_lstm_sentiment.h5')
要加载模型,请使用 Keras 提供的load_model函数,如下所示:
from keras.models import load_model
loaded_model = load_model('bi_lstm_sentiment.h5')
它现在已经准备好进行评估,并且无需重新编译。例如,在相同的测试集上,我们必须获得相同的结果:
test_loss, test_acc = loaded_model.evaluate(X_test, y_test)
print("Testing loss: {:.5}; Testing Accuracy: {:.2%}" .format(test_loss, test_acc))
运行示例
要运行模型,我们可以执行以下命令行:
python bilstm.py
进一步阅读
请参考以下文章:
-
SemEval Sentiment Analysis in Twitter
www.cs.york.ac.uk/semeval-2013/task2.html -
Personality insights with IBM Watson demo
personality-insights-livedemo.mybluemix.net/ -
Tone analyzer
tone-analyzer-demo.mybluemix.net/ -
Keras
keras.io/ -
Deep Speech: 扩展端到端语音识别,Awni Hannun, Carl Case, Jared Casper, Bryan Catanzaro, Greg Diamos, Erich Elsen, Ryan Prenger, Sanjeev Satheesh, Shubho Sengupta, Adam Coates, Andrew Y. Ng, 2014
-
深度递归神经网络语音识别,Alex Graves, Abdel-Rahman Mohamed, Geoffrey Hinton, 2013
-
Deep Speech 2:英语和普通话的端到端语音识别,作者:Dario Amodei, Rishita Anubhai, Eric Battenberg, Carl Case, Jared Casper, Bryan Catanzaro, Jingdong Chen, Mike Chrzanowski, Adam Coates, Greg Diamos, Erich Elsen, Jesse Engel, Linxi Fan, Christopher Fougner, Tony Han, Awni Hannun, Billy Jun, Patrick LeGresley, Libby Lin, Sharan Narang, Andrew Ng, Sherjil Ozair, Ryan Prenger, Jonathan Raiman, Sanjeev Satheesh, David Seetapun, Shubho Sengupta, Yi Wang, Zhiqian Wang, Chong Wang, Bo Xiao, Dani Yogatama, Jun Zhan, Zhenyao Zhu,2015
总结
本章回顾了前几章介绍的基本概念,同时介绍了一种新应用——情感分析,并介绍了一个高层次的库 Keras,旨在简化使用 Theano 引擎开发模型的过程。
这些基本概念包括循环网络、词嵌入、批量序列填充和类别独热编码。为了提高结果,提出了双向递归。
在下一章中,我们将看到如何将递归应用于图像,使用一个比 Keras 更轻量的库 Lasagne,它能让你更顺利地将库模块与自己的 Theano 代码结合。
第六章. 使用空间变换器网络进行定位
本章将 NLP 领域留到后面再回到图像,并展示递归神经网络在图像中的应用实例。在第二章,使用前馈网络分类手写数字中,我们处理了图像分类的问题,即预测图像的类别。在这里,我们将讨论对象定位,这是计算机视觉中的一个常见任务,旨在预测图像中对象的边界框。
而第二章,使用前馈网络分类手写数字通过使用线性层、卷积层和非线性激活函数构建的神经网络解决了分类任务,而空间变换器是一个新的模块,基于非常特定的方程,专门用于定位任务。
为了在图像中定位多个对象,空间变换器是通过递归网络构成的。本章借此机会展示如何在Lasagne中使用预构建的递归网络,Lasagne 是一个基于 Theano 的库,提供额外的模块,并通过预构建的组件帮助你快速开发神经网络,同时不改变你使用 Theano 构建和处理网络的方式。
总结来说,主题列表由以下内容组成:
-
Lasagne 库简介
-
空间变换器网络
-
带有空间变换器的分类网络
-
使用 Lasagne 的递归模块
-
数字的递归读取
-
使用铰链损失函数的无监督训练
-
基于区域的对象定位神经网络
使用 Lasagne 的 MNIST CNN 模型
Lasagne 库打包了层和工具,能够轻松处理神经网络。首先,让我们安装最新版本的 Lasagne:
pip install --upgrade https://github.com/Lasagne/Lasagne/archive/master.zip
让我们使用 Lasagne 重新编写第二章,使用前馈网络分类手写数字中的 MNIST 模型:
def model(l_input, input_dim=28, num_units=256, num_classes=10, p=.5):
network = lasagne.layers.Conv2DLayer(
l_input, num_filters=32, filter_size=(5, 5),
nonlinearity=lasagne.nonlinearities.rectify,
W=lasagne.init.GlorotUniform())
network = lasagne.layers.MaxPool2DLayer(network, pool_size=(2, 2))
network = lasagne.layers.Conv2DLayer(
network, num_filters=32, filter_size=(5, 5),
nonlinearity=lasagne.nonlinearities.rectify)
network = lasagne.layers.MaxPool2DLayer(network, pool_size=(2, 2))
if num_units > 0:
network = lasagne.layers.DenseLayer(
lasagne.layers.dropout(network, p=p),
num_units=num_units,
nonlinearity=lasagne.nonlinearities.rectify)
if (num_units > 0) and (num_classes > 0):
network = lasagne.layers.DenseLayer(
lasagne.layers.dropout(network, p=p),
num_units=num_classes,
nonlinearity=lasagne.nonlinearities.softmax)
return network
层包括layer0_input、conv1_out、pooled_out、conv2_out、pooled2_out、hidden_output。它们是通过预构建的模块构建的,例如,InputLayer、Conv2DLayer、MaxPool2DLayer、DenseLayer,以及诸如修正线性单元(rectify)或 softmax 的丢弃层非线性和GlorotUniform的初始化方式。
要连接由模块组成的网络图,将输入符号var与输出var连接,使用以下代码:
input_var = T.tensor4('inputs')
l_input = lasagne.layers.InputLayer(shape=(None, 1, 28, 28), input_var=input_var)
network = mnist_cnn.model(l_input)
prediction = lasagne.layers.get_output(network)
或者使用这段代码:
l_input = lasagne.layers.InputLayer(shape=(None, 1, 28, 28))
network = mnist_cnn.model(l_input)
input_var = T.tensor4('inputs')
prediction = lasagne.layers.get_output(network, input_var)
一个非常方便的功能是,你可以打印任何模块的输出形状:
print(l_input.output_shape)
Lasagne 的get_all_params方法列出了模型的参数:
params = lasagne.layers.get_all_params(network, trainable=True)
for p in params:
print p.name
最后,Lasagne 提供了不同的学习规则,如RMSprop、Nesterov Momentum、Adam和Adagrad:
target_var = T.ivector('targets')
loss = lasagne.objectives.categorical_crossentropy(prediction, target_var)
loss = loss.mean()
updates = lasagne.updates.nesterov_momentum(
loss, params, learning_rate=0.01, momentum=0.9)
train_fn = theano.function([input_var, target_var], loss, updates=updates)
其他所有内容保持不变。
为了测试我们的 MNIST 模型,下载 MNIST 数据集:
wget http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz -P /sharedfiles
训练一个 MNIST 分类器来进行数字分类:
python 1-train-mnist.py
模型参数保存在 model.npz 中。准确率再次超过 99%。
一个定位网络
在 空间变换网络 (STN) 中,想法不是直接将网络应用于输入图像信号,而是添加一个模块来预处理图像,对其进行裁剪、旋转和缩放以适应物体,从而辅助分类:
空间变换网络
为此,STNs 使用一个定位网络来预测仿射变换参数并处理输入:
空间变换网络
在 Theano 中,仿射变换的微分是自动完成的,我们只需通过仿射变换将定位网络与分类网络的输入连接起来。
首先,我们创建一个与 MNIST CNN 模型相差不远的定位网络,用于预测仿射变换的六个参数:
l_in = lasagne.layers.InputLayer((None, dim, dim))
l_dim = lasagne.layers.DimshuffleLayer(l_in, (0, 'x', 1, 2))
l_pool0_loc = lasagne.layers.MaxPool2DLayer(l_dim, pool_size=(2, 2))
l_dense_loc = mnist_cnn.model(l_pool0_loc, input_dim=dim, num_classes=0)
b = np.zeros((2, 3), dtype=theano.config.floatX)
b[0, 0] = 1.0
b[1, 1] = 1.0
l_A_net = lasagne.layers.DenseLayer(
l_dense_loc,
num_units=6,
name='A_net',
b=b.flatten(),
W=lasagne.init.Constant(0.0),
nonlinearity=lasagne.nonlinearities.identity)
在这里,我们只需通过 DimshuffleLayer 向输入数组添加一个通道维度,该维度的值仅为 1。这样的维度添加被称为广播。
池化层将输入图像大小调整为 50x50,这足以确定数字的位置。
定位层的权重初始化为零,偏置则初始化为单位仿射参数;STN 模块在开始时不会产生任何影响,整个输入图像将被传输。
根据仿射参数进行裁剪:
l_transform = lasagne.layers.TransformerLayer(
incoming=l_dim,
localization_network=l_A_net,
downsample_factor=args.downsample)
down_sampling_factor 使我们能够根据输入定义输出图像的大小。在这种情况下,它的值是三,意味着图像将是 33x33——与我们的 MNIST 数字大小 28x28 相差不远。最后,我们简单地将 MNIST CNN 模型添加到分类输出中:
l_out = mnist_cnn.model(l_transform, input_dim=dim, p=sh_drp, num_units=400)
为了测试分类器,让我们创建一些 100x100 像素的图像,带有一些变形和一个数字:
python create_mnist_sequence.py --nb_digits=1
绘制前三个图像(对应 1、0、5):
python plot_data.py mnist_sequence1_sample_8distortions_9x9.npz
运行命令以训练模型:
python 2-stn-cnn-mnist.py
在这里,当数字没有变形时,准确率超过 99%,这通常是仅用简单的 MNIST CNN 模型无法实现的,并且在有变形的情况下,准确率超过 96.9%。
绘制裁剪图像的命令是:
python plot_crops.py res_test_2.npz
它给我们带来了以下结果:
带有变形的情况:
STN 可以被看作是一个模块,可以包含在任何网络中,位于两个层之间的任何地方。为了进一步提高分类结果,在分类网络的不同层之间添加多个 STN 有助于获得更好的结果。
这是一个包含两个分支的网络示例,每个分支都有自己的 SPN,它们在无监督的情况下将尝试捕捉图像的不同部分进行分类:
(空间变换网络论文,Jaderberg 等,2015 年)
应用于图像的递归神经网络
这个想法是使用递归来读取多个数字,而不仅仅是一个:
为了读取多个数字,我们只需将定位前馈网络替换为递归网络,它将输出多个仿射变换,分别对应于每个数字:
从前面的例子中,我们将全连接层替换为 GRU 层:
l_conv2_loc = mnist_cnn.model(l_pool0_loc, input_dim=dim, p=sh_drp, num_units=0)
class Repeat(lasagne.layers.Layer):
def __init__(self, incoming, n, **kwargs):
super(Repeat, self).__init__(incoming, **kwargs)
self.n = n
def get_output_shape_for(self, input_shape):
return tuple([input_shape[0], self.n] + list(input_shape[1:]))
def get_output_for(self, input, **kwargs):
tensors = [input]*self.n
stacked = theano.tensor.stack(*tensors)
dim = [1, 0] + range(2, input.ndim+1)
return stacked.dimshuffle(dim)
l_repeat_loc = Repeat(l_conv2_loc, n=num_steps)
l_gru = lasagne.layers.GRULayer(l_repeat_loc, num_units=num_rnn_units,
unroll_scan=True)
l_shp = lasagne.layers.ReshapeLayer(l_gru, (-1, num_rnn_units))
这将输出一个维度为(None, 3, 256)的张量,其中第一维是批量大小,3 是 GRU 中的步骤数,256 是隐藏层的大小。在这个层的上面,我们仅仅添加一个和之前一样的全连接层,输出三个初始的身份图像:
b = np.zeros((2, 3), dtype=theano.config.floatX)
b[0, 0] = 1.0
b[1, 1] = 1.0
l_A_net = lasagne.layers.DenseLayer(
l_shp,
num_units=6,
name='A_net',
b=b.flatten(),
W=lasagne.init.Constant(0.0),
nonlinearity=lasagne.nonlinearities.identity)
l_conv_to_transform = lasagne.layers.ReshapeLayer(
Repeat(l_dim, n=num_steps), [-1] + list(l_dim.output_shape[-3:]))
l_transform = lasagne.layers.TransformerLayer(
incoming=l_conv_to_transform,
localization_network=l_A_net,
downsample_factor=args.downsample)
l_out = mnist_cnn.model(l_transform, input_dim=dim, p=sh_drp, num_units=400)
为了测试分类器,我们创建一些具有100x100像素的图像,并加入一些扭曲,这次包含三个数字:
python create_mnist_sequence.py --nb_digits=3 --output_dim=100
绘制前三个图像(对应序列296、490、125):
python plot_data.py mnist_sequence3_sample_8distortions_9x9.npz
让我们运行命令来训练我们的递归模型:
python 3-recurrent-stn-mnist.py
*Epoch 0 Acc Valid 0.268833333333, Acc Train = 0.268777777778, Acc Test = 0.272466666667*
*Epoch 1 Acc Valid 0.621733333333, Acc Train = 0.611116666667, Acc Test = 0.6086*
*Epoch 2 Acc Valid 0.764066666667, Acc Train = 0.75775, Acc Test = 0.764866666667*
*Epoch 3 Acc Valid 0.860233333333, Acc Train = 0.852294444444, Acc Test = 0.859566666667*
*Epoch 4 Acc Valid 0.895333333333, Acc Train = 0.892066666667, Acc Test = 0.8977*
*Epoch 53 Acc Valid 0.980433333333, Acc Train = 0.984261111111, Acc Test = 0.97926666666*
分类准确率为 99.3%。
绘制裁剪图:
python plot_crops.py res_test_3.npz
带有共同定位的无监督学习
在第二章中训练的数字分类器的前几层,使用前馈网络分类手写数字作为编码函数,将图像表示为嵌入空间中的向量,就像对待单词一样:
通过最小化随机集的合页损失目标函数,有可能训练空间变换网络的定位网络,这些图像被认为包含相同的数字:
最小化这个和意味着修改定位网络中的权重,使得两个定位的数字比两个随机裁剪的数字更接近。
这是结果:
(空间变换网络论文,Jaderberg 等,2015 年)
基于区域的定位网络
历史上,目标定位的基本方法是使用分类网络在滑动窗口中;它的过程是将一个窗口在每个方向上逐像素滑动,并在每个位置和每个尺度上应用分类器。分类器学习判断目标是否存在且居中。这需要大量的计算,因为模型必须在每个位置和尺度上进行评估。
为了加速这一过程,Fast-R-CNN 论文中的区域提议网络(RPN)由研究员 Ross Girshick 提出,目的是将神经网络分类器的全连接层(如 MNIST CNN)转换为卷积层;事实上,在 28x28 的图像上,卷积层和线性层之间没有区别,只要卷积核的尺寸与输入相同。因此,任何全连接层都可以重写为卷积层,使用相同的权重和适当的卷积核尺寸,这使得网络能够在比 28x28 更大的图像上工作,输出在每个位置的特征图和分类得分。唯一的区别可能来自于整个网络的步幅,步幅可以设置为不同于 1,并且可以很大(例如几个 10 像素),通过将卷积核的步幅设置为不同于 1,以减少评估位置的数量,从而减少计算量。这样的转换是值得的,因为卷积非常高效:
Faster R-CNN:使用区域提议网络实现实时物体检测
已经设计了一种端到端网络,借鉴了解卷积原理,其中输出特征图一次性给出所有的边界框:你只看一次(YOLO)架构预测每个位置可能的 B 个边界框。每个边界框由其坐标(x, y, w, h)按比例表示为回归问题,并具有一个与交并比(IOU)相对应的置信度(概率),该交并比表示该框与真实框之间的重叠程度。类似的方式也提出了 SSD 模型。
最后,在第八章中介绍的分割网络,使用编码-解码网络进行翻译和解释,也可以看作是神经网络实现的目标定位方法。
进一步阅读
你可以进一步参考以下来源以获取更多信息:
-
空间变换网络,Max Jaderberg, Karen Simonyan, Andrew Zisserman, Koray Kavukcuoglu, 2015 年 6 月
-
循环空间变换网络,Søren Kaae Sønderby, Casper Kaae Sønderby, Lars Maaløe, Ole Winther, 2015 年 9 月
-
谷歌街景字符识别,Jiyue Wang, Peng Hui How
-
使用卷积神经网络在野外读取文本,Max Jaderberg, Karen Simonyan, Andrea Vedaldi, Andrew Zisserman, 2014 年
-
使用深度卷积神经网络从街景图像中进行多位数字识别,Ian J. Goodfellow, Yaroslav Bulatov, Julian Ibarz, Sacha Arnoud, Vinay Shet, 2013
-
从谷歌街景图像中识别字符,Guan Wang, Jingrui Zhang
-
《用于自然场景文本识别的合成数据与人工神经网络》,Max Jaderberg,Karen Simonyan,Andrea Vedaldi,Andrew Zisserman,2014 年
-
去掉 R 的 R-CNN,Karel Lenc,Andrea Vedaldi,2015 年
-
Fast R-CNN,Ross Girshick,2015 年
-
Faster R-CNN:基于区域提议网络的实时物体检测,Shaoqing Ren,Kaiming He,Ross Girshick,Jian Sun,2015 年
-
你只需看一次:统一的实时物体检测,Joseph Redmon,Santosh Divvala,Ross Girshick,Ali Farhadi,2015 年 6 月
-
YOLO 实时演示
pjreddie.com/darknet/yolo/ -
YOLO9000:更好、更快、更强,Joseph Redmon,Ali Farhadi,2016 年 12 月
-
SSD:单次多框检测器,Wei Liu,Dragomir Anguelov,Dumitru Erhan,Christian Szegedy,Scott Reed,Cheng-Yang Fu,Alexander C. Berg,2015 年 12 月
-
精确的物体检测和语义分割的丰富特征层次,Ross Girshick,Jeff Donahue,Trevor Darrell,Jitendra Malik,2013 年
-
文本流:一种统一的自然场景图像文本检测系统,Shangxuan Tian,Yifeng Pan,Chang Huang,Shijian Lu,Kai Yu,Chew Lim Tan,2016 年
总结
空间变换器层是一个原创模块,用于定位图像区域、裁剪并调整大小,帮助分类器集中注意图像中的相关部分,从而提高准确性。该层由可微分的仿射变换组成,参数通过另一个模型——定位网络进行计算,并且可以像往常一样通过反向传播进行学习。
使用循环神经单元可以推断出图像中读取多个数字的应用示例。为了简化工作,引入了 Lasagne 库。
空间变换器是众多定位方法中的一种;基于区域的定位方法,如 YOLO、SSD 或 Faster RCNN,提供了最先进的边界框预测结果。
在下一章中,我们将继续进行图像识别,探索如何对包含比数字更多信息的完整图像进行分类,例如室内场景和户外风景的自然图像。与此同时,我们将继续使用 Lasagne 的预构建层和优化模块。
第七章。使用残差网络分类图像
本章介绍了用于图像分类的最先进的深度网络。
残差网络已成为最新的架构,准确性大幅提高,并且更为简洁。
在残差网络之前,已经有很长时间的架构历史,比如AlexNet、VGG、Inception(GoogLeNet)、Inception v2、v3 和 v4。研究人员一直在寻找不同的概念,并发现了一些潜在的规律来设计更好的架构。
本章将涉及以下主题:
-
图像分类评估的主要数据集
-
图像分类的网络架构
-
批量归一化
-
全局平均池化
-
残差连接
-
随机深度
-
密集连接
-
多 GPU
-
数据增强技术
自然图像数据集
图像分类通常包括比 MNIST 手写数字更广泛的物体和场景。它们大多数是自然图像,意味着人类在现实世界中观察到的图像,例如风景、室内场景、道路、山脉、海滩、人类、动物和汽车,而不是合成图像或计算机生成的图像。
为了评估图像分类网络在自然图像上的表现,研究人员通常使用三个主要数据集来比较性能:
-
Cifar-10 数据集包含 60,000 张小图像(32x32),仅分为 10 类,您可以轻松下载:
wget https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz -P /sharedfiles tar xvzf /sharedfiles/cifar-10-python.tar.gz -C /sharedfiles/下面是每个类别的一些示例图像:
Cifar 10 数据集类别及样本
www.cs.toronto.edu/~kriz/cifar.html -
Cifar-100 数据集包含 60,000 张图像,分为 100 类和 20 个超级类别
-
ImageNet 数据集包含 120 万张图像,标注了广泛的类别(1,000 类)。由于 ImageNet 仅供非商业用途,您可以下载 Food 101 数据集,该数据集包含 101 种餐食类别,每个类别有 1,000 张图像:
wget http://data.vision.ee.ethz.ch/cvl/food-101.tar.gz -P /sharedfiles tar xvzf food-101.tar.gz -C /sharedfiles/
在介绍残差架构之前,让我们讨论两种提高分类网络准确度的方法:批量归一化和全局平均池化。
批量归一化
更深的网络,超过 100 层,可以帮助图像分类多个类别。深度网络的主要问题是确保输入流以及梯度能够从网络的一端有效传播到另一端。
然而,网络中的非线性部分饱和,梯度变为零并不罕见。此外,网络中的每一层都必须适应其输入分布的持续变化,这一现象被称为内部协变量偏移。
已知,网络训练时,输入数据经过线性处理以使均值为零、方差为单位(称为网络输入归一化)能加速训练,并且每个输入特征应独立归一化,而不是联合归一化。
要规范化网络中每一层的输入,稍微复杂一些:将输入的均值归零会忽略前一层学习到的偏置,而当方差为单位时,问题更加严重。当该层的输入被归一化时,前一层的参数可能会无限增长,而损失保持不变。
因此,对于层输入规范化,批量归一化层在归一化后重新学习尺度和偏置:
它不使用整个数据集,而是使用批次来计算归一化的统计量,并通过移动平均来接近整个数据集的统计信息,同时进行训练。
一个批量归一化层具有以下好处:
-
它减少了不良初始化或过高学习率的影响
-
它提高了网络的准确性
-
它加速了训练
-
它减少了过拟合,起到正则化模型的作用
引入批量归一化层时,可以移除 dropout,增加学习率,并减少 L2 权重规范化。
小心将非线性激活放在 BN 层之后,并去除前一层的偏置:
l = NonlinearityLayer(
BatchNormLayer(
ConvLayer(l_in,
num_filters=n_filters[0],
filter_size=(3,3),
stride=(1,1),
nonlinearity=None,
pad='same',
W=he_norm)
),
nonlinearity=rectify
)
全局平均池化
传统上,分类网络的最后两层是全连接层和 softmax 层。全连接层输出一个等于类别数量的特征数,softmax 层将这些值归一化为概率,且它们的和为 1。
首先,可以将步幅为 2 的最大池化层替换为步幅为 2 的新的卷积层:全卷积网络的表现更好。
其次,也可以移除全连接层。如果最后一个卷积层输出的特征图数量选择为类别数,全球空间平均池化将每个特征图缩减为一个标量值,表示在不同宏观空间位置上类的得分平均值:
残差连接
虽然非常深的架构(具有许多层)表现更好,但它们更难训练,因为输入信号在层与层之间逐渐减弱。有人尝试在多个阶段训练深度网络。
这种逐层训练的替代方法是向网络中添加一个附加连接,跳过一个块的层,称为恒等连接,它将信号传递而不作任何修改,除了经典的卷积层,称为残差,形成一个残差块,如下图所示:
这样的残差块由六层组成。
残差网络是由多个残差块组成的网络。输入经过第一层卷积处理,然后是批量归一化和非线性激活:
例如,对于由两个残差块组成的残差网络,且第一卷积层有八个特征图,输入图像的大小为 28x28,层的输出形状如下:
InputLayer (None, 1, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
ElemwiseSumLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 8, 28, 28)
ElemwiseSumLayer (None, 8, 28, 28)
BatchNormLayer (None, 8, 28, 28)
NonlinearityLayer (None, 8, 28, 28)
Conv2DDNNLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
ElemwiseSumLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 16, 14, 14)
ElemwiseSumLayer (None, 16, 14, 14)
BatchNormLayer (None, 16, 14, 14)
NonlinearityLayer (None, 16, 14, 14)
Conv2DDNNLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
ElemwiseSumLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
Conv2DDNNLayer (None, 32, 7, 7)
ElemwiseSumLayer (None, 32, 7, 7)
BatchNormLayer (None, 32, 7, 7)
NonlinearityLayer (None, 32, 7, 7)
GlobalPoolLayer (None, 32)
DenseLayer (None, 10)
输出特征图的数量增加,而每个输出特征图的大小减小:这种技术通过减小特征图大小/增加维度的数量保持每层参数数量不变,这是构建网络时常见的最佳实践。
为了增加维度,在三个不同位置进行了三次维度转换,第一次在第一个残差块之前,第二次在 n 个残差块之后,第三次在 2xn 个残差块之后。每个转换之间,过滤器的数量按数组定义:
# 8 -> 8 -> 16 -> 32
n_filters = {0:8, 1:8, 2:16, 3:32}
维度增加是通过相应残差块的第一层进行的。由于输入的形状与输出不同,简单的恒等连接无法与块层的输出拼接,因此用维度投影代替,以将输出的大小调整为块输出的维度。这样的投影可以通过一个1x1的卷积核,步幅为2来实现:
def residual_block(l, transition=False, first=False, filters=16):
if transition:
first_stride = (2,2)
else:
first_stride = (1,1)
if first:
bn_pre_relu = l
else:
bn_pre_conv = BatchNormLayer(l)
bn_pre_relu = NonlinearityLayer(bn_pre_conv, rectify)
conv_1 = NonlinearityLayer(BatchNormLayer(ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(3,3), stride=first_stride,
nonlinearity=None,
pad='same',
W=he_norm)),nonlinearity=rectify)
conv_2 = ConvLayer(conv_1, num_filters=filters, filter_size=(3,3), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)
# add shortcut connections
if transition:
# projection shortcut, as option B in paper
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(2,2), nonlinearity=None, pad='same', b=None)
elif conv_2.output_shape == l.output_shape:
projection=l
else:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', b=None)
return ElemwiseSumLayer([conv_2, projection])
也有一些变种的残差块被发明出来。
一个宽版(Wide-ResNet)残差块是通过增加每个残差块的输出数量来构建的,当它们到达末端时,这个增加是通过一个倍数来实现的:
n_filters = {0:num_filters, 1:num_filters*width, 2:num_filters*2*width, 3:num_filters*4*width}
一个瓶颈版本通过减少每层的参数数量来创建一个瓶颈,它具有降维效果,实施赫布理论 共同发放的神经元会相互连接,并帮助残差块捕获信号中的特定模式:
瓶颈是同时减少特征图大小和输出数量,而不是像之前的做法那样保持每层参数数量不变:
def residual_bottleneck_block(l, transition=False, first=False, filters=16):
if transition:
first_stride = (2,2)
else:
first_stride = (1,1)
if first:
bn_pre_relu = l
else:
bn_pre_conv = BatchNormLayer(l)
bn_pre_relu = NonlinearityLayer(bn_pre_conv, rectify)
bottleneck_filters = filters / 4
conv_1 = NonlinearityLayer(BatchNormLayer(ConvLayer(bn_pre_relu, num_filters=bottleneck_filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
conv_2 = NonlinearityLayer(BatchNormLayer(ConvLayer(conv_1, num_filters=bottleneck_filters, filter_size=(3,3), stride=first_stride, nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
conv_3 = ConvLayer(conv_2, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)
if transition:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(2,2), nonlinearity=None, pad='same', b=None)
elif first:
projection = ConvLayer(bn_pre_relu, num_filters=filters, filter_size=(1,1), stride=(1,1), nonlinearity=None, pad='same', b=None)
else:
projection = l
return ElemwiseSumLayer([conv_3, projection])
现在,完整的三堆残差块网络已经构建完成:
def model(shape, n=18, num_filters=16, num_classes=10, width=1, block='normal'):
l_in = InputLayer(shape=(None, shape[1], shape[2], shape[3]))
l = NonlinearityLayer(BatchNormLayer(ConvLayer(l_in, num_filters=n_filters[0], filter_size=(3,3), stride=(1,1), nonlinearity=None, pad='same', W=he_norm)),nonlinearity=rectify)
l = residual_block(l, first=True, filters=n_filters[1])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[1])
l = residual_block(l, transition=True, filters=n_filters[2])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[2])
l = residual_block(l, transition=True, filters=n_filters[3])
for _ in range(1,n):
l = residual_block(l, filters=n_filters[3])
bn_post_conv = BatchNormLayer(l)
bn_post_relu = NonlinearityLayer(bn_post_conv, rectify)
avg_pool = GlobalPoolLayer(bn_post_relu)
return DenseLayer(avg_pool, num_units=num_classes, W=HeNormal(), nonlinearity=softmax)
用于 MNIST 训练的命令:
python train.py --dataset=mnist --n=1 --num_filters=8 --batch_size=500
这带来了 98%的 top-1 精度。
在 Cifar 10 上,残差网络层数超过 100 层时,需要将批量大小减少到 64,以适应 GPU 的内存:
-
对于 ResNet-110(6 x 18 + 2):
python train.py --dataset=cifar10 --n=18 --num_filters=16 --batch_size=64 -
ResNet-164(6 x 27 + 2):
python train.py --dataset=cifar10 --n=27 --num_filters=16 --batch_size=64 -
宽版 ResNet-110:
python train.py --dataset=cifar10 --n=18 --num_filters=16 --width=4 --batch_size=64 -
使用 ResNet-bottleneck-164:
python train.py --dataset=cifar10 --n=18 --num_filters=16 --block=bottleneck --batch_size=64 -
对于 Food-101,我进一步减少了 ResNet 110 的批量大小:
python train.py --dataset=food101 --batch_size=10 --n=18 --num_filters=16
随机深度
由于信号在层间传播时可能在任何一个残差块中出现错误,随机深度的想法是通过随机移除一些残差块并用恒等连接替代,来训练网络的鲁棒性。
首先,由于参数数量较少,训练速度更快。其次,实践证明它具有鲁棒性,并且能提供更好的分类结果:
密集连接
随机深度通过创建直接连接来跳过一些随机的层。更进一步地,除了移除一些随机层外,另一种实现相同功能的方法是为之前的层添加一个身份连接:
一个密集块(密集连接卷积网络)
至于残差块,一个密集连接的卷积网络由重复的密集块组成,以创建一堆层块:
具有密集块的网络(密集连接卷积网络)
这种架构选择遵循了在第十章中看到的相同原则,使用高级 RNN 预测时间序列,带有高速公路网络:身份连接有助于信息在网络中正确传播和反向传播,从而减少了在层数较高时出现的梯度爆炸/消失问题。
在 Python 中,我们将残差块替换为一个密集连接块:
def dense_block(network, transition=False, first=False, filters=16):
if transition:
network = NonlinearityLayer(BatchNormLayer(network), nonlinearity=rectify)
network = ConvLayer(network,network.output_shape[1], 1, pad='same', W=he_norm, b=None, nonlinearity=None)
network = Pool2DLayer(network, 2, mode='average_inc_pad')
network = NonlinearityLayer(BatchNormLayer(network), nonlinearity=rectify)
conv = ConvLayer(network,filters, 3, pad='same', W=he_norm, b=None, nonlinearity=None)
return ConcatLayer([network, conv], axis=1)
另请注意,批量归一化是逐特征进行的,由于每个块的输出已经归一化,因此不需要第二次归一化。用一个简单的仿射层替代批量归一化层,学习连接归一化特征的尺度和偏置即可:
def dense_fast_block(network, transition=False, first=False, filters=16):
if transition:
network = NonlinearityLayer(BiasLayer(ScaleLayer(network)), nonlinearity=rectify)
network = ConvLayer(network,network.output_shape[1], 1, pad='same', W=he_norm, b=None, nonlinearity=None)
network = BatchNormLayer(Pool2DLayer(network, 2, mode='average_inc_pad'))
network = NonlinearityLayer(BiasLayer(ScaleLayer(network)), nonlinearity=rectify)
conv = ConvLayer(network,filters, 3, pad='same', W=he_norm, b=None, nonlinearity=None)
return ConcatLayer([network, BatchNormLayer(conv)], axis=1)
用于训练 DenseNet-40:
python train.py --dataset=cifar10 --n=13 --num_filters=16 --block=dense_fast --batch_size=64
多 GPU
Cifar 和 MNIST 图像仍然较小,低于 35x35 像素。自然图像的训练需要保留图像中的细节。例如,224x224 的输入大小就非常合适,这比 35x35 大了 40 倍。当具有如此输入大小的图像分类网络有几百层时,GPU 内存限制了批次大小,最多只能处理十几张图像,因此训练一个批次需要很长时间。
要在多 GPU 模式下工作:
-
模型参数是共享变量,意味着在 CPU / GPU 1 / GPU 2 / GPU 3 / GPU 4 之间共享,和单 GPU 模式一样。
-
批次被分成四个部分,每个部分被送到不同的 GPU 进行计算。网络输出在每个部分上计算,梯度被反向传播到每个权重。GPU 返回每个权重的梯度值。
-
每个权重的梯度从多个 GPU 拉回到 CPU 并堆叠在一起。堆叠后的梯度代表了整个初始批次的梯度。
-
更新规则应用于批次梯度,并更新共享的模型权重。
请参见下图:
Theano 稳定版本仅支持每个进程一个 GPU,因此在主程序中使用第一个 GPU,并为每个 GPU 启动子进程进行训练。请注意,前述图像中的循环需要同步模型的更新,以避免每个 GPU 在不同步的模型上进行训练。与其自己重新编程,不如使用 Platoon 框架(github.com/mila-udem/platoon),该框架专门用于在一个节点内跨多个 GPU 训练模型。
另外,值得注意的是,将多个 GPU 上的批量归一化均值和方差同步会更加准确。
数据增强
数据增强是提高分类精度的一个非常重要的技术。数据增强通过从现有样本创建新样本来实现,方法是添加一些抖动,例如:
-
随机缩放
-
随机大小裁剪
-
水平翻转
-
随机旋转
-
光照噪声
-
亮度抖动
-
饱和度抖动
-
对比抖动
这将帮助模型在现实生活中常见的不同光照条件下变得更加鲁棒。
模型每一轮都会发现不同的样本,而不是始终看到相同的数据集。
请注意,输入归一化对于获得更好的结果也很重要。
深入阅读
你可以参考以下标题以获得更多见解:
-
密集连接卷积网络,Gao Huang,Zhuang Liu,Kilian Q. Weinberger 和 Laurens van der Maaten,2016 年 12 月
-
代码灵感来源于 Lasagne 仓库:
-
Inception-v4,Inception-ResNet 和残差连接对学习的影响,Christian Szegedy,Sergey Ioffe,Vincent Vanhoucke 和 Alex Alemi,2016
-
图像识别的深度残差学习,Kaiming He,Xiangyu Zhang 和 Shaoqing Ren,Jian Sun,2015
-
重新思考计算机视觉中的 Inception 架构,Christian Szegedy,Vincent Vanhoucke,Sergey Ioffe,Jonathon Shlens 和 Zbigniew Wojna,2015
-
宽残差网络,Sergey Zagoruyko 和 Nikos Komodakis,2016
-
深度残差网络中的恒等映射,Kaiming He,Xiangyu Zhang,Shaoqing Ren 和 Jian Sun,2016 年 7 月
-
网络中的网络,Min Lin,Qiang Chen,Shuicheng Yan,2013
总结
已经提出了新技术来实现最先进的分类结果,如批量归一化、全局平均池化、残差连接和密集块。
这些技术推动了残差网络和密集连接网络的构建。
使用多个 GPU 有助于训练图像分类网络,这些网络具有多个卷积层、大的感受野,并且批量输入的图像在内存使用上较重。
最后,我们研究了数据增强技术如何增加数据集的大小,减少模型过拟合的可能性,并学习更稳健网络的权重。
在下一章中,我们将看到如何利用这些网络的早期层作为特征来构建编码器网络,以及如何反转卷积以重建输出图像,以进行像素级预测。
第八章. 使用编码解码网络进行翻译与解释
编码解码技术在输入和输出属于同一空间时应用。例如,图像分割是将输入图像转换为新图像,即分割掩码;翻译是将字符序列转换为新的字符序列;问答则是以新的字词序列回答输入的字词序列。
为了应对这些挑战,编码解码网络由两部分对称组成:编码网络和解码网络。编码器网络将输入数据编码成一个向量,解码器网络则利用该向量生成输出,例如翻译、回答输入问题、解释或输入句子或输入图像的注释。
编码器网络通常由前几层组成,这些层属于前面章节中介绍的网络类型,但没有用于降维和分类的最后几层。这样一个截断的网络会生成一个多维向量,称为特征,它为解码器提供一个内部状态表示,用于生成输出表示。
本章分解为以下关键概念:
-
序列到序列网络
-
机器翻译应用
-
聊天机器人应用
-
反卷积
-
图像分割应用
-
图像标注应用
-
解码技术的改进
自然语言处理中的序列到序列网络
基于规则的系统正在被端到端神经网络所取代,因为后者在性能上有所提升。
端到端神经网络意味着网络直接通过示例推断所有可能的规则,而无需了解潜在的规则,如语法和词形变化;单词(或字符)直接作为输入喂入网络。输出格式也是如此,输出可以直接是单词索引本身。网络架构通过其系数负责学习这些规则。
适用于自然语言处理(NLP)的端到端编码解码网络架构是序列到序列网络,如以下图所示:
单词索引通过查找表转换为其在嵌入空间中的连续多维值。这一转换,详见第三章,将单词编码为向量,是将离散的单词索引编码到神经网络能够处理的高维空间中的关键步骤。
然后,首先对输入的单词嵌入执行一堆 LSTM 操作,用以编码输入并生成思维向量。第二堆 LSTM 以这个向量作为初始内部状态,并且期望为目标句子中的每个单词生成下一个单词。
在核心部分,是我们经典的 LSTM 单元步骤函数,包含输入、遗忘、输出和单元门:
def LSTM( hidden_size):
W = shared_norm((hidden_size, 4*hidden_size))
U = shared_norm((hidden_size, 4*hidden_size))
b = shared_zeros(4*hidden_size)
params = [W, U, b]
def forward(m, X, h_, C_ ):
XW = T.dot(X, W)
h_U = T.dot(h_, U)
bfr_actv = XW + h_U + b
f = T.nnet.sigmoid( bfr_actv[:, 0:hidden_size] )
i = T.nnet.sigmoid( bfr_actv[:, 1*hidden_size:2*hidden_size] )
o = T.nnet.sigmoid( bfr_actv[:, 2*hidden_size:3*hidden_size] )
Cp = T.tanh( bfr_actv[:, 3*hidden_size:4*hidden_size] )
C = i*Cp + f*C_
h = o*T.tanh( C )
C = m[:, None]*C + (1.0 - m)[:, None]*C_
h = m[:, None]*h + (1.0 - m)[:, None]*h_
h, C = T.cast(h, theano.config.floatX), T.cast(h, theano.config.floatX)
return h, C
return forward, params
一个简单的闭包比一个类更好。没有足够的方法和参数去写一个类。编写类要求添加许多self,并且每个变量之前都要有一个__init__方法。
为了减少计算成本,整个层栈被构建成一个一步函数,并且递归性被添加到整个栈步骤函数的顶部,该步骤函数会为每个时间步生成最后一层的输出。其他一些实现让每一层都独立递归,这样效率要低得多(慢于两倍以上)。
在X输入的顶部,使用一个掩码变量m,当设置为零时,停止递归:当没有更多数据时,隐藏状态和单元状态保持不变(掩码值为零)。由于输入是批量处理的,因此每个批次中的句子可能有不同的长度,并且借助掩码,所有批次中的句子都可以并行处理,步数与最大句子长度相同。递归会在批次中每行的不同位置停止。
类的闭包是因为模型不能像先前示例那样直接应用于某些符号输入变量:实际上,模型是应用于递归循环内的序列(使用扫描操作符)。因此,在许多高级深度学习框架中,每一层都被设计为一个模块,暴露出前向/反向方法,可以添加到各种架构中(并行分支和递归),正如本示例所示。
编码器/解码器的完整栈步骤函数,放置在它们各自的递归循环内,可以设计如下:
def stack( voca_size, hidden_size, num_layers, embedding=None, target_voca_size=0):
params = []
if embedding == None:
embedding = shared_norm( (voca_size, hidden_size) )
params.append(embedding)
layers = []
for i in range(num_layers):
f, p = LSTM(hidden_size)
layers.append(f)
params += p
def forward( mask, inputs, h_, C_, until_symbol = None):
if until_symbol == None :
output = embedding[inputs]
else:
output = embedding[T.cast( inputs.argmax(axis=-1), "int32" )]
hos = []
Cos = []
for i in range(num_layers):
hs, Cs = layersi
hos.append(hs)
Cos.append(Cs)
output = hs
if target_voca_size != 0:
output_embedding = shared_norm((hidden_size, target_voca_size))
params.append(output_embedding)
output = T.dot(output, output_embedding)
outputs = (T.cast(output, theano.config.floatX),T.cast(hos, theano.config.floatX),T.cast(Cos, theano.config.floatX))
if until_symbol != None:
return outputs, theano.scan_module.until( T.eq(output.argmax(axis=-1)[0], until_symbol) )
return outputs
return forward, params
第一部分是将输入转换为嵌入空间。第二部分是 LSTM 层的堆栈。对于解码器(当target_voca_size != 0时),添加了一个线性层来计算输出。
现在我们有了我们的编码器/解码器步骤函数,让我们构建完整的编码器-解码器网络。
首先,编码器-解码器网络必须将输入编码成内部状态表示:
encoderInputs, encoderMask = T.imatrices(2)
h0,C0 = T.tensor3s(2)
encoder, encoder_params = stack(valid_data.source_size, opt.hidden_size, opt.num_layers)
([encoder_outputs, hS, CS], encoder_updates) = theano.scan(
fn = encoder,
sequences = [encoderMask, encoderInputs],
outputs_info = [None, h0, C0])
为了编码输入,编码栈步骤函数会在每个单词上递归地运行。
当outputs_info由三个变量组成时,扫描操作符认为扫描操作的输出由三个值组成。
这些输出来自编码栈步骤函数,并且对应于:
-
栈的输出
-
栈的隐藏状态,以及
-
栈的单元状态,对于输入句子的每个步骤/单词
在outputs_info中,None表示考虑到编码器将产生三个输出,但只有最后两个会被反馈到步骤函数中(h0 -> h_ 和 C0 -> C_)。
由于序列指向两个序列,scan 操作的步骤函数必须处理四个参数。
然后,一旦输入句子被编码成向量,编码器-解码器网络将其解码:
decoderInputs, decoderMask, decoderTarget = T.imatrices(3)
decoder, decoder_params = stack(valid_data.target_size, opt.hidden_size, opt.num_layers, target_voca_size=valid_data.target_size)
([decoder_outputs, h_vals, C_vals], decoder_updates) = theano.scan(
fn = decoder,
sequences = [decoderMask, decoderInputs],
outputs_info = [None, hS[-1], CS[-1]])
params = encoder_params + decoder_params
编码器网络的最后状态hS[-1]和CS[-1]将作为解码器网络的初始隐藏状态和细胞状态输入。
在输出上计算对数似然度与上一章关于序列的内容相同。
对于评估,最后预测的单词必须输入解码器中,以预测下一个单词,这与训练有所不同,在训练中输入和输出序列是已知的:
在这种情况下,outputs_info中的None可以替换为初始值prediction_start,即start标记。由于它不再是None,该初始值将被输入到解码器的步骤函数中,只要它与h0和C0一起存在。scan 操作符认为每个步骤都有三个先前的值输入到解码器函数(而不是像之前那样只有两个)。由于decoderInputs已从输入序列中移除,因此传递给解码器堆栈步骤函数的参数数量仍然是四个:先前预测的输出值将取代输入值。这样,同一个解码器函数可以同时用于训练和预测:
prediction_mask = theano.shared(np.ones(( opt.max_sent_size, 1), dtype="int32"))
prediction_start = np.zeros(( 1, valid_data.target_size), dtype=theano.config.floatX)
prediction_start[0, valid_data.idx_start] = 1
prediction_start = theano.shared(prediction_start)
([decoder_outputs, h_vals, C_vals], decoder_updates) = theano.scan(
fn = decoder,
sequences = [prediction_mask],
outputs_info = [prediction_start, hS[-1], CS[-1]],
non_sequences = valid_data.idx_stop
)
非序列参数valid_data.idx_stop告诉解码器步骤函数,它处于预测模式,这意味着输入不是单词索引,而是其先前的输出(需要找到最大索引)。
同样,在预测模式下,一次预测一个句子(批量大小为1)。当产生end标记时,循环停止,这得益于解码器堆栈步骤函数中的theano.scan_module.until输出,之后无需再解码更多单词。
用于翻译的 Seq2seq
序列到序列(Seq2seq)网络的第一个应用是语言翻译。
该翻译任务是为计算语言学协会(ACL)的会议设计的,数据集 WMT16 包含了不同语言的新闻翻译。此数据集的目的是评估新的翻译系统或技术。我们将使用德英数据集。
-
首先,预处理数据:
python 0-preprocess_translations.py --srcfile data/src-train.txt --targetfile data/targ-train.txt --srcvalfile data/src-val.txt --targetvalfile data/targ-val.txt --outputfile data/demo First pass through data to get vocab... Number of sentences in training: 10000 Number of sentences in valid: 2819 Source vocab size: Original = 24995, Pruned = 24999 Target vocab size: Original = 35816, Pruned = 35820 (2819, 2819) Saved 2819 sentences (dropped 181 due to length/unk filter) (10000, 10000) Saved 10000 sentences (dropped 0 due to length/unk filter) Max sent length (before dropping): 127 -
训练
Seq2seq网络:python 1-train.py --dataset translation初看之下,你会注意到每个周期的 GPU 时间是445.906425953,因此比 CPU 快十倍(4297.15962195)。
-
训练完成后,将英语句子翻译成德语,加载已训练的模型:
python 1-train.py --dataset translation --model model_translation_e100_n2_h500
用于聊天机器人的 Seq2seq
序列到序列网络的第二个目标应用是问答系统或聊天机器人。
为此,下载 Cornell 电影对话语料库并进行预处理:
wget http://www.mpi-sws.org/~cristian/data/cornell_movie_dialogs_corpus.zip -P /sharedfiles/
unzip /sharedfiles/cornell_movie_dialogs_corpus.zip -d /sharedfiles/cornell_movie_dialogs_corpus
python 0-preprocess_movies.py
该语料库包含大量富含元数据的虚构对话,数据来自原始电影剧本。
由于源语言和目标语言的句子使用相同的词汇表,解码网络可以使用与编码网络相同的词嵌入:
if opt.dataset == "chatbot":
embeddings = encoder_params[0]
对于chatbot数据集,相同的命令也适用:
python 1-train.py --dataset chatbot # training
python 1-train.py --dataset chatbot --model model_chatbot_e100_n2_h500 # answer my question
提高序列到序列网络的效率
在聊天机器人示例中,第一个值得注意的有趣点是输入序列的反向顺序:这种技术已被证明能改善结果。
对于翻译任务,使用双向 LSTM 来计算内部状态是非常常见的,正如在第五章中所看到的,使用双向 LSTM 分析情感:两个 LSTM,一个按正向顺序运行,另一个按反向顺序运行,两个并行处理序列,它们的输出被连接在一起:
这种机制能够更好地捕捉给定未来和过去的信息。
另一种技术是注意力机制,这是下一章的重点。
最后,精细化技术已经开发并在二维 Grid LSTM 中进行了测试,这与堆叠 LSTM 相差不大(唯一的区别是在深度/堆叠方向上的门控机制):
Grid 长短期记忆
精细化的原则是也在输入句子上按两种顺序运行堆栈,顺序进行。这个公式的思想是让编码器网络在正向编码之后重新访问或重新编码句子,并隐式地捕捉一些时间模式。此外,请注意,二维网格提供了更多可能的交互作用来进行这种重新编码,在每个预测步骤重新编码向量,使用之前输出的单词作为下一个预测单词的方向。所有这些改进与更大的计算能力有关,对于这个重新编码器网络,其复杂度为O(n m)(n和m分别表示输入和目标句子的长度),而对于编码-解码网络来说,其复杂度为O(n+m)。
所有这些技术都有助于降低困惑度。当模型训练时,还可以考虑使用束搜索算法,该算法会在每个时间步跟踪多个预测及其概率,而不是仅跟踪一个,以避免一个错误的预测排名第一时导致后续错误预测。
图像的反卷积
在图像的情况下,研究人员一直在寻找作为编码卷积逆操作的解码操作。
第一个应用是对卷积网络的分析与理解,如在第二章中所示,使用前馈网络分类手写数字,它由卷积层、最大池化层和修正线性单元组成。为了更好地理解网络,核心思想是可视化图像中对于网络某个单元最具判别性的部分:在高层特征图中的一个神经元被保持为非零,并且从该激活信号开始,信号会反向传播回二维输入。
为了通过最大池化层重建信号,核心思想是在正向传递过程中跟踪每个池化区域内最大值的位置。这种架构被称为DeConvNet,可以表示为:
可视化和理解卷积网络
信号会被反向传播到在正向传递过程中具有最大值的位置。
为了通过 ReLU 层重建信号,已提出了三种方法:
-
反向传播仅反向传播到那些在正向传递过程中为正的位置。
-
反向 DeConvNet仅反向传播正梯度
-
引导反向传播仅反向传播到满足两个先前条件的位置:在正向传递过程中输入为正,并且梯度为正。
这些方法在下图中进行了说明:
从第一层的反向传播给出了各种类型的滤波器:
然而,从网络的更高层开始,引导反向传播给出了更好的结果:
也可以将反向传播条件化为输入图像,这样将激活多个神经元,并从中应用反向传播,以获得更精确的输入可视化:
反向传播也可以应用于原始输入图像,而不是空白图像,这一过程被谷歌研究命名为Inceptionism,当反向传播用于增强输出概率时:
但反卷积的主要目的是用于场景分割或图像语义分析,其中反卷积被学习的上采样卷积所替代,如SegNet 网络中所示:
SegNet:一种用于图像分割的深度卷积编码器-解码器架构
在反卷积过程中,每一步通常会将较低输入特征与当前特征进行连接,以进行上采样。
DeepMask 网络采取一种混合方法,仅对包含对象的补丁进行反卷积。为此,它在包含对象的 224x224 输入补丁(平移误差±16 像素)上进行训练,而不是完整的图像:
学习分割对象候选
编码器(VGG-16)网络的卷积层有一个 16 倍的下采样因子,导致特征图为 14x14。
一个联合学习训练两个分支,一个用于分割,一个用于评分,判断补丁中对象是否存在、是否居中以及是否在正确的尺度上。
相关分支是语义分支,它将 14x14 特征图中的对象上采样到 56x56 的分割图。上采样是可能的,如果:
-
一个全连接层,意味着上采样图中的每个位置都依赖于所有特征,并且具有全局视野来预测值。
-
一个卷积(或局部连接层),减少了参数数量,但也通过部分视图预测每个位置的分数。
-
一种混合方法,由两个线性层组成,二者之间没有非线性,旨在执行降维操作,如前图所示
输出掩膜随后通过一个简单的双线性上采样层被上采样回原始的 224x224 补丁维度。
为了处理完整的输入图像,可以将全连接层转换为卷积层,卷积核大小等于全连接层的输入大小,并使用相同的系数,这样网络在应用到完整图像时就变成了完全卷积的网络,步长为 16。
随着序列到序列网络通过双向重新编码机制得到改进,SharpMask方法通过在等效尺度上使用输入卷积特征来改善上采样反卷积过程的锐度:
学习细化对象分割
而 SegNet 方法仅通过跟踪最大池化索引产生的上采样图来学习反卷积,SharpMask 方法直接重用输入特征图,这是一种非常常见的粗到细方法。
最后,请记住,通过应用条件随机场(CRF)后处理步骤,可以进一步改善结果,无论是对于一维输入(如文本),还是二维输入(如分割图像)。
多模态深度学习
为了进一步开放可能的应用,编码-解码框架可以应用于不同的模态,例如,图像描述。
图像描述是用文字描述图像的内容。输入是图像,通常通过深度卷积网络编码成一个思想向量。
用于描述图像内容的文本可以从这个内部状态向量中生成,解码器采用相同的 LSTM 网络堆栈,就像 Seq2seq 网络一样:
进一步阅读
请参考以下主题以获取更深入的见解:
-
基于神经网络的序列到序列学习,Ilya Sutskever,Oriol Vinyals,Quoc V. Le,2014 年 12 月
-
使用 RNN 编码器-解码器的短语表示学习用于统计机器翻译,Kyunghyun Cho,Bart van Merrienboer,Caglar Gulcehre,Dzmitry Bahdanau,Fethi Bougares,Holger Schwenk,Yoshua Bengio,2014 年 9 月
-
通过联合学习对齐与翻译的神经机器翻译,Dzmitry Bahdanau,Kyunghyun Cho,Yoshua Bengio,2016 年 5 月
-
神经对话模型,Oriol Vinyals,Quoc Le,2015 年 7 月
-
快速而强大的神经网络联合模型用于统计机器翻译,Jacob Devlin,Rabih Zbib,Zhongqiang Huang,Thomas Lamar,Richard Schwartz,John Mkahoul,2014 年
-
SYSTRAN 的纯神经机器翻译系统,Josep Crego,Jungi Kim,Guillaume Klein,Anabel Rebollo,Kathy Yang,Jean Senellart,Egor Akhanov,Patrice Brunelle,Aurelien Coquard,Yongchao Deng,Satoshi Enoue,Chiyo Geiss,Joshua Johanson,Ardas Khalsa,Raoum Khiari,Byeongil Ko,Catherine Kobus,Jean Lorieux,Leidiana Martins,Dang-Chuan Nguyen,Alexandra Priori,Thomas Riccardi,Natalia Segal,Christophe Servan,Cyril Tiquet,Bo Wang,Jin Yang,Dakun Zhang,Jing Zhou,Peter Zoldan,2016 年
-
Blue:一种自动评估机器翻译的方法,Kishore Papineni,Salim Roukos,Todd Ward,Wei-Jing Zhu,2002 年
-
ACL 2016 翻译任务
-
变色龙在假想对话中的应用:一种理解对话中文本风格协调的新方法,Cristian Danescu-NiculescuMizil 和 Lillian Lee,2011,见:
research.googleblog.com/2015/06/inceptionism-going-deeper-into-neural.html -
通过深度卷积网络和完全连接的 CRFs 进行语义图像分割,Liang-Chieh Chen,George Papandreou,Iasonas Kokkinos,Kevin Murphy,Alan L.,Yuille,2014 年
-
SegNet:一种用于图像分割的深度卷积编码器-解码器架构,Vijay Badrinarayanan,Alex Kendall,Roberto Cipolla,2016 年 10 月
-
R-FCN:基于区域的全卷积网络进行物体检测,Jifeng Dai,Yi Li,Kaiming He,Jian Sun,2016 年
-
学习分割物体候选框,Pedro O. Pinheiro,Ronan Collobert,Piotr Dollar,2015 年 6 月
-
学习优化物体分割,Pedro O. Pinheiro,Tsung-Yi Lin,Ronan Collobert,Piotr Dollàr,2016 年 3 月
-
可视化与理解卷积网络,Matthew D Zeiler,Rob Fergus,2013 年 11 月
-
展示与讲述:神经图像标题生成器,Oriol Vinyals,Alexander Toshev,Samy Bengio,Dumitru Erhan,2014 年
摘要
至于爱情,头到脚的姿势提供了令人兴奋的新可能性:编码器和解码器网络使用相同的层堆栈,但方向相反。
尽管它没有为深度学习提供新的模块,但编码-解码技术非常重要,因为它使得网络能够进行“端到端”训练,也就是说,直接将输入和相应的输出喂入网络,而不需要为网络指定任何规则或模式,也不需要将编码训练和解码训练拆分成两个独立的步骤。
虽然图像分类是一个一对一的任务,情感分析是一个多对一的任务,但编码-解码技术展示了多对多的任务,比如翻译或图像分割。
在下一章中,我们将介绍一种注意力机制,它赋予编码-解码架构专注于输入的某些部分,以便生成更准确的输出的能力。
第九章. 使用注意力机制选择相关的输入或记忆
本章介绍了一种注意力机制,通过这种机制,神经网络能够通过专注于输入或记忆的相关部分来提升其性能。
通过这种机制,翻译、注释、解释和分割等,在前一章中看到的,都能获得更高的准确性。
神经网络的输入和输出也可以与读取和写入外部记忆相关联。这些网络,记忆网络,通过外部记忆增强,并能够决定存储或检索哪些信息,以及从哪里存储或检索。
在本章中,我们将讨论:
-
注意力机制
-
对齐翻译
-
图像中的焦点
-
神经图灵机
-
记忆网络
-
动态记忆网络
可微分的注意力机制
在翻译句子、描述图像内容、注释句子或转录音频时,自然的做法是一次专注于输入句子或图像的某一部分,在理解该部分并转换后,再转向下一部分,按照一定的顺序进行全局理解。
例如,在德语中,在某些条件下,动词出现在句子的末尾。因此,在翻译成英语时,一旦主语被读取和翻译,好的机器翻译神经网络可以将注意力转向句子末尾以找到动词并将其翻译成英语。这种将输入位置与当前输出预测匹配的过程是通过注意力机制实现的。
首先,让我们回到设计了 softmax 层的分类网络(见 第二章, 使用前馈网络分类手写数字),该层输出一个非负权重向量 ,对于输入 X,该向量的和为1:
然后:
分类的目标是使 尽可能接近1(对于正确的类别k),并对其他类别接近零。
但是 是一个概率分布,也可以作为一个权重向量,用来关注在位置k的记忆向量的某些值
:
如果权重集中在位置k,则返回 。根据权重的锐度,输出将更清晰或更模糊。
在特定位置处理向量m值的这个机制就是注意力机制:也就是说,它是线性的、可微的,并且具有反向传播梯度下降,用于特定任务的训练。
更好的使用注意力机制进行翻译
注意力机制的应用范围非常广泛。为了更好地理解,首先让我们通过机器翻译的例子来说明它。注意力机制对齐源句子和目标句子(预测翻译),并避免长句子的翻译退化:
在上一章中,我们讨论了使用编码器-解码器框架的机器翻译,编码器提供给解码器一个固定长度的编码向量c。有了注意力机制,如果每一步的编码循环网络产生一个隐藏状态h i,那么在每个解码时间步t提供给解码器的向量将是可变的,并由以下公式给出:
使用通过 softmax 函数产生的对齐系数:
根据解码器的先前隐藏状态和编码器的隐藏状态
,前一个解码器隐藏状态与每个编码器隐藏状态之间的嵌入式点积产生一个权重,描述它们应该如何匹配:
经过几个训练周期后,模型通过聚焦输入的某个部分来预测下一个词:
为了更好地学习对齐,可以使用数据集中存在的对齐注释,并为由注意力机制产生的权重添加交叉熵损失,这可以在训练的前几个周期中使用。
更好的使用注意力机制对图像进行注释
相同的注意力机制可以应用于图像注释或音频转录任务。
对于图像,注意力机制在每个预测时间步聚焦于特征的相关部分:
展示、关注和讲述:带有视觉注意力的神经图像字幕生成
让我们看一下经过训练的模型在图像上的注意力点:
(Show, Attend and Tell: Neural Image Caption Generation with Visual Attention,Kelvin Xu 等,2015 年)
在神经图灵机中存储和检索信息
注意力机制可以作为在记忆增强网络中访问部分记忆的方式。
神经图灵机中的记忆概念受到了神经科学和计算机硬件的启发。
RNN 的隐藏状态用来存储信息,但它无法存储足够大的数据量并进行检索,即使 RNN 已被增强了一个记忆单元,如 LSTM 中的情况。
为了解决这个问题,神经图灵机(NTM)首先设计了一个外部记忆库和读/写头,同时保留了通过梯度下降进行训练的神奇之处。
读取记忆库是通过对变量记忆库的注意力进行控制,类似于前面例子中对输入的注意力:
这可以通过以下方式进行说明:
而写入记忆库则通过另一个注意力机制将我们的新值分配到记忆的一部分:
描述需要存储的信息,并且是需要删除的信息,并且它们的大小与记忆库相同:
读写头的设计类似于硬盘,其移动性由注意权重和
来想象。
记忆将在每个时间步演变,就像 LSTM 的单元记忆一样;但是,由于记忆库设计得很大,网络倾向于在每个时间步将传入的数据进行存储和组织,干扰比任何经典 RNN 都要小。
与记忆相关的处理过程自然是通过一个递归神经网络(RNN)在每个时间步充当控制器来驱动的:
控制器网络在每个时间步输出:
-
每个读/写头的定位或注意系数
-
写头需要存储或删除的值
原始的 NTM 提出了两种定义头部定位(也称为寻址)的方法,定义为权重:
-
基于内容的定位,用于将相似的内容放置在记忆的同一区域,这对于检索、排序或计数任务非常有用:
-
基于位置的定位,它依赖于头部的先前位置,可以在复制任务中使用。一个门控
定义了先前权重与新生成权重之间的影响,以计算头部的位置。一个偏移权重
定义了相对于该位置的位移量。
最后,一个锐化权重减少了头部位置的模糊:
所有操作都是可微分的。
可能不止两个头,特别是在一些任务中,如对两个存储值的加法运算,单个读取头将会受到限制。
这些 NTM 在任务中表现出比 LSTM 更强的能力,比如从输入序列中检索下一个项目、重复输入序列多次或从分布中采样。
记忆网络
给定一些事实或故事来回答问题或解决问题,促使设计出一种新型网络——记忆网络。在这种情况下,事实或故事被嵌入到一个记忆库中,就像它们是输入一样。为了完成需要排序事实或在事实之间创建转换的任务,记忆网络使用递归推理过程,在多个步骤或跳跃中操作记忆库。
首先,查询或问题q被转换成常量输入嵌入:
而在每个推理步骤中,回答问题的事实X被嵌入到两个记忆库中,其中嵌入系数是时间步长的函数:
为了计算注意力权重:
并且:
选择了带有注意力机制:
每个推理时间步骤的输出随后与身份连接组合,如前所述,以提高递归效率:
一个线性层和分类 softmax 层被添加到最后的:
具有动态记忆网络的情节记忆
另一种设计通过动态记忆网络被引入。首先,N 个事实与分隔符令牌连接在一起,然后通过 RNN 编码:RNN 在每个分隔符处的输出被用作输入嵌入。这样的编码方式更加自然,同时也保留了时间依赖性。问题也通过 RNN 进行编码以生成向量q。
其次,记忆库被替换为情节记忆,依赖于混合了 RNN 的注意力机制,以便保留事实之间的时间依赖关系:
门控 由多层感知器提供,依赖于推理的前一个状态
、问题和输入嵌入
作为输入。
推理过程与 RNN 相同:
以下图片展示了输入和输出之间的相互作用,以计算情节记忆:
问我任何事:自然语言处理的动态记忆网络
为了对这些网络进行基准测试,Facebook 研究通过合成 bAbI 数据集,使用 NLP 工具为一些随机建模的故事创建事实、问题和答案。该数据集由不同的任务组成,用于测试不同的推理技能,例如基于时间、大小或位置的单个、两个或三个事实推理、计数、列举或理解论点之间的关系、否定、动机以及路径查找。
至于机器翻译中的引导对齐,当数据集也包含了导致答案的事实注释时,也可以使用监督训练:
-
注意力机制
-
当推理循环停止时,生成一个停止标记,判断使用的事实数量是否足够回答问题
进一步阅读
您可以参考以下主题以获取更多见解:
-
问我任何事:自然语言处理的动态记忆网络,Ankit Kumar,Ozan Irsoy,Peter Ondruska,Mohit Iyyer,James Bradbury,Ishaan Gulrajani,Victor Zhong,Romain Paulus,Richard Socher,2015 年
-
注意力与增强型循环神经网络,Chris Olah,Shan Carter,2016 年 9 月
distill.pub/2016/augmented-rnns/ -
面向话题的神经机器翻译的引导对齐训练,陈文虎,Evgeny Matusov,Shahram Khadivi,Jan-Thorsten Peter,2016 年 7 月
-
展示、注意与叙述:具有视觉注意力的神经图像字幕生成,Kelvin Xu,Jimmy Ba,Ryan Kiros,Kyunghyun Cho,Aaron Courville,Ruslan Salakhutdinov,Richard Zemel,Yoshua Bengio,2015 年 2 月
-
迈向 AI 完全问题回答:一组先决条件玩具任务,Jason Weston,Antoine Bordes,Sumit Chopra,Alexander M. Rush,Bart van Merriënboer,Armand Joulin,Tomas Mikolov,2015 年
-
记忆网络,Jason Weston,Sumit Chopra,Antoine Bordes,2014 年
-
端到端记忆网络,Sainbayar Sukhbaatar,Arthur Szlam,Jason Weston,Rob Fergus,2015 年
-
神经图灵机,Alex Graves,Greg Wayne,Ivo Danihelka,2014 年
-
深度视觉-语义对齐用于生成图像描述,Andrej Karpathy,Li Fei-Fei,2014
总结
注意力机制是帮助神经网络选择正确信息并集中精力以生成正确输出的聪明选择。它可以直接应用于输入或特征(输入经过几层处理)。在翻译、图像标注和语音识别等任务中,尤其是在输入维度很重要时,准确率得到了提升。
注意力机制引入了增强了外部记忆的新型网络,作为输入/输出,可以从中读取或写入。这些网络已被证明在问答挑战中非常强大,几乎所有自然语言处理任务都可以转化为此类任务:标注、分类、序列到序列,或问答任务。
在下一章,我们将介绍更高级的技巧及其在更一般的递归神经网络中的应用,以提高准确性。