原始文章链接
代码所在的github仓库
我的GitHub
有关RNN的一般概述请参考第一部分
中文版:http://www.jianshu.com/p/54364d395186
英文原版:http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/
在这一部分我们将使用python来完整实现一个RNN神经网络,并且使用theano(一个运行在GPU上的一个库)来优化我们的实现,
我将跳过一些对于理解神经网络不那么重要的一些代码,但是不用担心,这些代码都在github上
我们的目标是通过使用RNN神经网络去建立一个语言模型,这句话是什么意思呢,下面来让我们看一下吧
假如说我么有一个由m个单词组成的句子,现在有一个语言模型允许我们去预测这个句子是一个正常句子的概率(在给定的数据集中):
上面这个公式:
w:代表的是句子中的单词
P(wi | w1,…wi-1):指的是在w1,…wi-1是一个正常句子前缀的情况下再加上wi成为一个正常句子的概率
例如:
现在有若干个单词 我 医院 去 吃饭 看病
假设 w1 ,…wi-1 的句子前缀序列是 :我去医院
那么P(看病|我去医院) 的概率就大于P(吃饭|我去医院)的概率
换句话说,一个句子的概率是每个单词概率的乘积,例如,现在有这么一句话:“我去买一些巧克力”,这句话是一句正常话的概率就
等于P(我去买一些)的概率乘以 P(巧克力)的概率。既 P(我去买一些巧克力) = P(我去买一些) * P(巧克力)
现在你是否有点疑惑,难道上面这个方法真的有用吗?我们为什么要分别观察每个句子的概率?
首先,这样的一个模型可以被用作评分机制,例如机器翻译系统,一个机器翻译系统可能对于一个输入的句子可能有多种候选集(即一句话可能有多种翻译),你可以利用这个模型去获取每个句子的模型,从而找到评分最高也就是翻译效果最好的那个句子。根据我们直觉上的感受,最可能的句子也最有可能拥有正确的语法。类似的评分也发生在语音识别项目中
利用这个语言模型我们也可以做一些很酷的事情,因为我们可以预测出给定的一个单词他之前应该出现单词的概率,我们也可以预测出这个单词之后应该出现哪些单词的概率。这是一个生成模型:即给定一个现有的单词序列,我们从预测的单词中抽取出下一个单词。依次重复这个步骤,我们就逐渐会生成一个句子。Andrej Karparthy博士有一篇非常好的文章依次演示了哪些文章分别具有哪些能力,他的模型是训练单个单词(而不是完成的句子或者短语)从而生成任何句子。最终生成的这些句子可能是莎士比亚诗集中的一句话,或者是某句日常交流的内容,甚至可能是一段linux代码。
需要注意的是,在上面的等式中,每一个单词的生成实际上是依靠它前面所有单词的概率的(即每个单词出现的概率是以以前所有单词为条件的)。但是在实际情况中,由于计算机条件(内存和cpu)的限制,有时候并不能完全计算出所有单词的概率。通常情况下,只看这个单词之前的若干个单词的概率。不过在理论情况下,RNN是支持把所有单词的概率都计算一遍的,但是这种情况有点复杂。我们将在稍后的帖子中给出解释。
为了训练我们的模型,我们需要一些文本文件来进行学习。幸运的是,我们并不需要为这些训练数据集做标记(监督学习是需要为文本打标记的)为了获取原始的训练数据,我从google下载了15,000条比较长的评论信息。首先我们需要对这些数据做一些预处理,以便我们计算机能更好的处理。
现在我们已经有了原始的训练语料,但是我们希望以每个词为基础进行训练。这就意味着我们需要对原始句子进行切词。
有的单词可能只在我们的文本中出现了一次或者两次。把这次不常用的词移除掉是一个不错的建议。一个巨大的词表将会使我们的训练变得特别慢。并且对于那些奇缺词来说。并没有特别多的上下文来供我们使用。这样的话,我们并不能学习到这些奇缺词该怎样使用。这与人类的思维方式很像。要真正的理解一个单词的意思,你需要在不同的语境中去学习他。例如 苹果这个单词,我们经常会看到很多关于“苹果”这个单词的句子。例如“我吃了一个苹果”,”我买了一些苹果“,“我拥有一部苹果手机”。我们就知道“苹果”这个单词可能代表的是一种水果,也有可能代表的是一部手机的牌子。现在如果让我们用苹果造一个
句子,我们便知道怎样去造这个句子。但是如果有一个稀缺词,例如“黼黻” 我们没有看到过关于这个单词足够多的句子,所以我们也就没雨办法去学习怎样利用这个单词去造一个句子了。
在我们的代码中,我通过设置vocabulary_size来设置了常见的词汇列表(这个值可以根据你的需要随意的扩大或者缩小),我们会把不在我们词汇列表中的单词通过一个UNKONWN_TOKEN来代替。例如我们的词汇列表中并不包含 “非线性” 这个单词,那么现在有这么一句话 “非线性在神经网络中非常重要” 将会被替换成 “UNKONWN_TOKEN在神经网络中非常重要”。
我们可能想学习一些哪些单词最有可能是一句话的开始单词或者结束单词。(例如 “我” 这个单词就最有可能是一个开始单词,而 “了”,“吗” 就很有可能是一个句子的结束部分。)要做到这一点,我们将会为每一个句子的开始和结束单词安排一个SENTENCE_START标记和一个SENTENCE_END标记。
RNN的输入只能是向量,而不是字符串的形式,所以我们需要把单词映射为向量。我们建立从单词到id的映射,index_to_word 和 word_to_index. 例如 单词 “firendly” 对应的id可能是2001. 比如,”how”,”are”,”you”这3个词可能处于词汇表的第4,100,7733个位置,那么一个训练输入句子“how are you“就会被表达成[0,4,100,7733],其中的0是上面提到的开始标记SENTENCE_START的位置。由于我们是为了训练语言模型,那么对应的输出应该是每个单词往后移动一个位置,对应为[4,100,7733,1],其中的1是上面提到的结束标记SENTENCE_END。下面是实现的代码片段:
vocabulary_size = 8000
unknown_token = "UNKNOWN_TOKEN"
sentence_start_token = "SENTENCE_START"
sentence_end_token = "SENTENCE_END"
# Read the data and append SENTENCE_START and SENTENCE_END tokens
print "Reading CSV file..."
with open('data/reddit-comments-2015-08.csv', 'rb') as f:
reader = csv.reader(f, skipinitialspace=True)
reader.next()
# Split full comments into sentences
sentences = itertools.chain(*[nltk.sent_tokenize(x[0].decode('utf-8').lower()) for x in reader])
# Append SENTENCE_START and SENTENCE_END
sentences = ["%s %s %s" % (sentence_start_token, x, sentence_end_token) for x in sentences]
print "Parsed %d sentences." % (len(sentences))
# Tokenize the sentences into words
tokenized_sentences = [nltk.word_tokenize(sent) for sent in sentences]
# Count the word frequencies
word_freq = nltk.FreqDist(itertools.chain(*tokenized_sentences))
print "Found %d unique words tokens." % len(word_freq.items())
# Get the most common words and build index_to_word and word_to_index vectors
vocab = word_freq.most_common(vocabulary_size-1)
index_to_word = [x[0] for x in vocab]
index_to_word.append(unknown_token)
word_to_index = dict([(w,i) for i,w in enumerate(index_to_word)])
print "Using vocabulary size %d." % vocabulary_size
print "The least frequent word in our vocabulary is '%s' and appeared %d times." % (vocab[-1][0], vocab[-1][1])
# Replace all words not in our vocabulary with the unknown token
for i, sent in enumerate(tokenized_sentences):
tokenized_sentences[i] = [w if w in word_to_index else unknown_token for w in sent]
print "\nExample sentence: '%s'" % sentences[0]
print "\nExample sentence after Pre-processing: '%s'" % tokenized_sentences[0]
# Create the training data
X_train = np.asarray([[word_to_index[w] for w in sent[:-1]] for sent in tokenized_sentences])
y_train = np.asarray([[word_to_index[w] for w in sent[1:]] for sent in tokenized_sentences])
这里是一个输出的例子:
x:
SENTENCE_START what are n’t you understanding about this ? !
[0, 51, 27, 16, 10, 856, 53, 25, 34, 69]
y:
what are n’t you understanding about this ? ! SENTENCE_END
[51, 27, 16, 10, 856, 53, 25, 34, 69, 1]
有关RNN的一般概述请参考第一部分
中文版:http://www.jianshu.com/p/54364d395186
英文原版:http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/
下面让我们来具体看一下一个RNN神经网络的样子,input x代表的是有多个单词组成的句子(就像上面输入的句子那样)。每一个Xt是一个单个的单词。
这里还有一个问题,由于矩阵乘法的原理,我们不能简单的使用一个单词(例如36这个下标)作为输入,而是把它转化为一个向量长度为vocabulary_sized的ont-hot向量。例如下标为36的这个单词所转化的向量:前面35个位置都是0,而在36这个位置上的值为1。因此每一个Xt都应该是一个向量。同时x应该是一个矩阵。每一行代表一个单词。我们将在我们的神经网络中执行这个转化,而不是在数据的预处理中。我们的输入是具有相似的格式。每个Ot是一个大小为vocabulary_size的向量。每一个元素的值表示该单词成为句子中下一个单词的概率
我总是觉得写下向量和矩阵的维数是非常有用的,我们假设我们选择了一个词汇量为C=8000,并且隐藏层H=100.(你可以把隐藏层理解为网络的内存。把隐藏层设置的比较大的好处是让我们可以学习更复杂的模式。但是也会导致额外的计算量。)我们可以得到以下:
上面这个图是非常有价值的,请记住U, V 和 W 是我们想从这个网络中学习的参数。因此,我们需要学习总共个参数,在这个例子中C=8000 H=100 ,带入上面的公式,因此,我们需要学习总共1,610,000个参数 。这个参数的个数是非常大的。这也就告诉我们,我们的模型是有瓶颈的。但是请注意,Xt是一个one-hot向量[一位是1,其他位都是0],所以第一步它和U的全连接乘法,本质上没什么计算量,只是一个选择U的column的过程。因此,我们模型中最大的矩阵乘法是Vst,这就是为什么我们要尽可能减少我们词汇量的做法
有了上面的这些理论,现在是时候展现真正的技术啦,haha
我们首先会初始化一个RNN类来初始化我们的参数。我把这个初始化类叫做RNNNumpy,因为我们后面会实现一个Theano版本的。初始化参数U,V和 W是比较棘手的。我们不能把它们直接初始化为0,因为这会使得所有层的计算都会变成对称的。因此我们必须随机的初始化他们。由于这些初始化参数会对训练结果产生影响,因此人们在这块进行了大量的研究。最终的结果表明,最好的参数初始化依赖于激活函数(即我们例子中的tanh函数),一个推荐的方法是在区间内随机初始化权重,其中n是来自上一层的连接数。这听起来似乎比较复杂,但是不要担心,通常情况下只要你随机给参数赋予一个比较小的随机值。最终得到的效果就会比较好。
class RNNNumpy:
def __init__(self, word_dim, hidden_dim=100, bptt_truncate=4):
# Assign instance variables
self.word_dim = word_dim
self.hidden_dim = hidden_dim
self.bptt_truncate = bptt_truncate
# Randomly initialize the network parameters
self.U = np.random.uniform(-np.sqrt(1./word_dim), np.sqrt(1./word_dim), (hidden_dim, word_dim))
self.V = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (word_dim, hidden_dim))
self.W = np.random.uniform(-np.sqrt(1./hidden_dim), np.sqrt(1./hidden_dim), (hidden_dim, hidden_dim))
在上面的代码中,word_dim是我们词汇表的大小,hidden_dim使我们隐藏层的大小(我们可以自主设置),至于bptt_truncate这个参数是干什么的,目前我们还不需要了解。我们将会在后面介绍。
注意:截止到目前,我们已经完成了初始化的工作,包括初始化参数和初始化数据,现在我们通过前向传播算法来生成一句话。(实际计算的是给定一个开始单词,对应词汇表中的所有单词,作为下一个单词的概率值)
http://blog.csdn.net/cherrylvlei/article/details/75269614
下面,我们来定义我们上面讲到的前向传播概率(预测词的概率)
def forward_propagation(self, x):
# The total number of time steps
T = len(x)
# 在向前传播期间,我们需要保存每一个隐藏层的概率,因为我们在后面需要用到这些状态
# 我们为初始隐藏添加一个额外的参数,我们设置它为0
s = np.zeros((T + 1, self.hidden_dim))
s[-1] = np.zeros(self.hidden_dim)
# The outputs at each time step. Again, we save them for later.
o = np.zeros((T, self.word_dim))
# For each time step...
for t in np.arange(T):
# Note that we are indxing U by x[t]. This is the same as multiplying U with a one-hot vector.
s[t] = np.tanh(self.U[:,x[t]] + self.W.dot(s[t-1]))
o[t] = softmax(self.V.dot(s[t]))
return [o, s]
RNNNumpy.forward_propagation = forward_propagation
在上面的程序中,我们不仅计算返回结果,同时也会计算和保存隐藏状态。稍后我们会使用他们计算梯度,并返回这些隐层的状态避免重复计算。每一个Ot表示的是词汇在我们词汇表中的概率向量。但有时候,例如我们在评估我们的模型的时候,我们想要的是下一个概率最高的词汇。我们把这个函数叫做预测函数:
def predict(self, x):
执行向前传播函数,并且返回概率最高的下标
o, s = self.forward_propagation(x)
return np.argmax(o, axis=1)
RNNNumpy.predict = predict
让我们来执行一下上面的函数,并举一个实例来说明输出结果:
np.random.seed(10)
model = RNNNumpy(vocabulary_size)
o, s = model.forward_propagation(X_train[10])
print o.shape
print o
(45, 8000)
[[ 0.00012408 0.0001244 0.00012603 ..., 0.00012515 0.00012488
0.00012508]
[ 0.00012536 0.00012582 0.00012436 ..., 0.00012482 0.00012456
0.00012451]
[ 0.00012387 0.0001252 0.00012474 ..., 0.00012559 0.00012588
0.00012551]
...,
[ 0.00012414 0.00012455 0.0001252 ..., 0.00012487 0.00012494
0.0001263 ]
[ 0.0001252 0.00012393 0.00012509 ..., 0.00012407 0.00012578
0.00012502]
[ 0.00012472 0.0001253 0.00012487 ..., 0.00012463 0.00012536
0.00012665]]
对于句子里的每一个词语(上面的这个句子有45个词),我们的模型输出了8000个值,对应着词典中的每一个词可能是下一个单词的概率。请注意,因为我们的参数U,V,W是随机初始化的,所以现在这些预测也是完全随机的。下面给出每个单词预测概率最高的每个单词的下标:
predictions = model.predict(X_train[10])
print predictions.shape
print predictions
(45,)
[1284 5221 7653 7430 1013 3562 7366 4860 2212 6601 7299 4556 2481 238 2539
21 6548 261 1780 2005 1810 5376 4146 477 7051 4832 4991 897 3485 21
7291 2007 6006 760 4864 2182 6569 2800 2752 6821 4437 7021 7875 6912 3575]
注意:到目前为止,我们已经能够完成给定一个单词,生成一句话的功能。现在我们需要计算一下损失,也就是计算一下给定一个初始值,我们程序生成的句子和实际真实句子之间的差别有多大。差距越大,说明我们预测的结果越不好。
计算损失
为了训练我们的网络,我们需要一种方法来衡量我们的网络的所造成的错误。我们称之为损失函数L。我们的目标是找到是我们损失函数最小的参数U,V, W。一个常用的方法是交叉熵损失交叉熵损失。如果我们有N个训练的样本(我们文本中的单词)和C个类(我们词向量的大小),那么关于我们预测和真实的类别y的损失有如下的关系:
这个函数看起来有点复杂,其实它所做的事情就是把我们的训练样本相加,并根据我们的预测结果来补充损失。y(正确的结果)和O(预测的结果)之间的距离越大,那么损失就越大。再重新具体看一下这个式子,yn是一个8000维的one-hot向量,它只是起一个选择的作用,而On是一个概率,假如On越接近1,那么logOn就越接近0,损失函数的值就越小,这和上面的解释是吻合的。我们通过函数calculate_loss来实现我们的损失函数:
def calculate_total_loss(self, x, y):
L = 0
#对于每一个句子。。。
for i in np.arange(len(y)):
o, s = self.forward_propagation(x[i])
#We only care about our prediction of the "correct" words
corrent_word_predictions = o[np.arange(len(y[i])), y[i]]
# Add to the loss based on how off we were
L += -1 * np.sum(np.log(currect_word_predictions))
return L
def calculate_loss(self, x, y):
# Divide the total loss by the number of training examples
N = np.sum((len(y_i) for y_i in y))
return self.calculate_total_loss(x,y) / N
RUNNNumpy.calculate_total_loss = calculate_total_loss
RNNNumpy.calculate_loss = calculate_loss
现在让我们退一步想一下,随机预测函数应该还是个什么样的损失。这将给我们一个参照,并且来确定我们的函数实现没有错误。现在我们有C个单词,因此每个单词被预测为正确单词的概率是1/C,因此我们现在得到一个损失函数是关于 :
# Limit to 1000 examples to save time
print "Expected Loss for random predictions: %f" % np.log(vocabulary_size)
print "Actual loss: %f" % model.calculate_loss(X_train[:1000], y_train[:1000])
Expected Loss for random predictions: 8.987197
Actual loss: 8.987440
这两个得到的结果差不多。请记住,评估每个数据集的损失是非常昂贵的操作,如果你有大量的数据集,那么可能会话费你数个小时的时间。
注意:现在我们已经实现了怎样计算损失的代码,但是现在我们计算出来的损失函数的值比较大,说明我们的模型现在效果还特别差,下一步我们需要做的就是通过SGD算法来优化我们的模型,使得最终模型变得越来越来,也就是使得最终生成的句子,越来越像人所说的话。
需要记住的是,我们是希望通过最小化数据集来找到使我们损失函数最小的参数。一个经常使用的方法是SGD。全称是随机梯度下降算法。SGD的原理其实很简单。我们迭代我们的训练样例,并在每次迭代的过程中将参数微调到一个减少误差的方向。这个方向是由损失的梯度决定的:
SGD需要一个学习率,这个学习率定义了每次向梯度下降方向上移动的步长。SGD是一个非常受欢迎的优化方法,不仅适用于神经网络,也适用于机器学习领域。因此,如何使用批处理、并行性处理以及自适应学习率来优化SGD,已经有了很多的研究。尽管SGD背后的思想非常简单,但是怎样使用一种非常有效的方法来实现SGD算法,却是非常复杂的一件事情。如果你想了解更多有关SGD方面的内容,请移步这里(http://cs231n.github.io/optimization-1/)。由于SGD非常受欢迎,因此网上有大量关于其实现的例子。因此在本篇文章中,我就不再大量的重复他们。在这里我将简单实现一个简易版本的SGD,简单到甚至没有做任何的优化。
但是我们如何计算上面公式中提到的那些梯度呢?在传统的神将网络(http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/)中我们通过反向传播算法,来做到这一点。在RNN中我们使用这种方法的改动版本,叫做BPTT。由于网络中每个不同时间状态都共享参数,因此每个输出的梯度结果不仅取决于当前时间的状态,还需要计算前一个时刻的时间状态。如果你了解微积分,那实际上就是应用了链式规则。本教程的下一个部分是详细介绍BPTT的,因此我不会在这里展开去将这一部分。有关反向转播的内容请自动移步到这里(http://colah.github.io/posts/2015-08-Backprop/)和这里(http://cs231n.github.io/optimization-2/)。现在你就当BPTT仍然是一个黑匣子。下面这段函数的作用是输入训练样本(x, y)然后返回我们想要的梯度
def bptt(self, x, y):
T = len(y)
# Perform forward propagation
o, s = self.forward_propagation(x)
# We accumulate the gradients in these variables
dLdU = np.zeros(self.U.shape)
dLdV = np.zeros(self.V.shape)
dLdW = np.zeros(self.W.shape)
delta_o = o
delta_o[np.arange(len(y)), y] -= 1.
# For each output backwards...
for t in np.arange(T)[::-1]:
dLdV += np.outer(delta_o[t], s[t].T)
# Initial delta calculation
delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))
# Backpropagation through time (for at most self.bptt_truncate steps)
for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)
dLdW += np.outer(delta_t, s[bptt_step-1])
dLdU[:,x[bptt_step]] += delta_t
# Update delta for next step
delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)
return [dLdU, dLdV, dLdW]
RNNNumpy.bptt = bptt
梯度检查
无论什么时候,只要你实现了反向传播函数,那么最好也把梯度检查函数也实现了。因为这是验证你反向传播函数是否正确的最好方法。梯度检查背后的思想是参数的倒数等于点的斜率。我们可以通过稍微改变参数然后除以改变来近似的得到:然后我们将我们通过反向传播得到的梯度值和我们用上述方法所估计的梯度值进行比较。如果这两个值之间的差值不是太大,那么说明我们的实现是正确的。因为这个近似值的计算需要我们计算所有参数的损失,因此梯度检查是一个非常昂贵的操作(请记住,在上面的例子中,我们大概有100万个参数)。因此在一个词汇量较小的模型上去执行梯度检查是一个不错的建议:
def gradient_check(self, x, y, h=0.001, error_threshold=0.01):
# Calculate the gradients using backpropagation. We want to checker if these are correct.
bptt_gradients = self.bptt(x, y)
# List of all parameters we want to check.
model_parameters = ['U', 'V', 'W']
# Gradient check for each parameter
for pidx, pname in enumerate(model_parameters):
# Get the actual parameter value from the mode, e.g. model.W
parameter = operator.attrgetter(pname)(self)
print "Performing gradient check for parameter %s with size %d." % (pname, np.prod(parameter.shape))
# Iterate over each element of the parameter matrix, e.g. (0,0), (0,1), ...
it = np.nditer(parameter, flags=['multi_index'], op_flags=['readwrite'])
while not it.finished:
ix = it.multi_index
# Save the original value so we can reset it later
original_value = parameter[ix]
# Estimate the gradient using (f(x+h) - f(x-h))/(2*h)
parameter[ix] = original_value + h
gradplus = self.calculate_total_loss([x],[y])
parameter[ix] = original_value - h
gradminus = self.calculate_total_loss([x],[y])
estimated_gradient = (gradplus - gradminus)/(2*h)
# Reset parameter to original value
parameter[ix] = original_value
# The gradient for this parameter calculated using backpropagation
backprop_gradient = bptt_gradients[pidx][ix]
# calculate The relative error: (|x - y|/(|x| + |y|))
relative_error = np.abs(backprop_gradient - estimated_gradient)/(np.abs(backprop_gradient) + np.abs(estimated_gradient))
# If the error is to large fail the gradient check
if relative_error > error_threshold:
print "Gradient Check ERROR: parameter=%s ix=%s" % (pname, ix)
print "+h Loss: %f" % gradplus
print "-h Loss: %f" % gradminus
print "Estimated_gradient: %f" % estimated_gradient
print "Backpropagation gradient: %f" % backprop_gradient
print "Relative Error: %f" % relative_error
return
it.iternext()
print "Gradient check for parameter %s passed." % (pname)
RNNNumpy.gradient_check = gradient_check
# To avoid performing millions of expensive calculations we use a smaller vocabulary size for checking.
grad_check_vocab_size = 100
np.random.seed(10)
model = RNNNumpy(grad_check_vocab_size, 10, bptt_truncate=1000)
model.gradient_check([0,1,2,3], [1,2,3,4])
到目前为止我们可以为我们的参数来计算梯度了,现在我们来实现SGD,我喜欢把实现SGD这个步骤分两步来实现:
1:定义一个sdg_step函数来计算梯度和执行一个批次的更新。
2:外层循环来迭代我们的训练集,并调整学习率
# Performs one step of SGD.
def numpy_sdg_step(self, x, y, learning_rate):
# Calculate the gradients
dLdU, dLdV, dLdW = self.bptt(x, y)
# Change parameters according to gradients and learning rate
self.U -= learning_rate * dLdU
self.V -= learning_rate * dLdV
self.W -= learning_rate * dLdW
RNNNumpy.sgd_step = numpy_sdg_step
# Outer SGD Loop
# - model: The RNN model instance
# - X_train: The training data set
# - y_train: The training data labels
# - learning_rate: Initial learning rate for SGD
# - nepoch: Number of times to iterate through the complete dataset
# - evaluate_loss_after: Evaluate the loss after this many epochs
def train_with_sgd(model, X_train, y_train, learning_rate=0.005, nepoch=100, evaluate_loss_after=5):
# We keep track of the losses so we can plot them later
losses = []
num_examples_seen = 0
for epoch in range(nepoch):
# Optionally evaluate the loss
if (epoch % evaluate_loss_after == 0):
loss = model.calculate_loss(X_train, y_train)
losses.append((num_examples_seen, loss))
time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print "%s: Loss after num_examples_seen=%d epoch=%d: %f" % (time, num_examples_seen, epoch, loss)
# Adjust the learning rate if loss increases
if (len(losses) > 1 and losses[-1][1] > losses[-2][1]):
learning_rate = learning_rate * 0.5
print "Setting learning rate to %f" % learning_rate
sys.stdout.flush()
# For each training example...
for i in range(len(y_train)):
# One SGD step
model.sgd_step(X_train[i], y_train[i], learning_rate)
num_examples_seen += 1
现在让我们来评估一下训练我们的网络大概需要多久的时间:
np.random.seed(10)
model = RNNNumpy(vocabulary_size)
%timeit model.sgd_step(X_train[10], y_train[10], 0.005)
哦,真是个坏消息,SGD这一步在我的笔记本上大概需要350毫秒。我们大概有80,000个例子在我们的训练集中。所以一次迭代(遍历整个数据集)需要几个小时,多次迭代的话大概的好几天的时间。而且与某些公司所研究的数据集相比,我们的数据集简直就是mini版的。那么我们应该怎么办呢?
不要担心,幸运的是已经有很多的方法来提成我们代码的运行效率。我们可以仍然使用我们的模型来使我们的代码运行的更快。还有一些方法是优化我们的模型以便减少计算成本。或者把两者结合起来使用。研究人员已经确认了很多中方法来使得模型的计算不在那么耗费时间。例如使用分层softmax或者添加投影层去避免大的矩阵乘法(如果想了解更多相关的内容请移步这里和这里)。在这里我会使用第一类的方法来加快我们代码运行的时间。最好的方法就是使用GPU。在这之前,我们使用一个小的数据集,来检查这个损失是否真的减少了:
np.random.seed(10)
# Train on a small subset of the data to see what happens
model = RNNNumpy(vocabulary_size)
losses = train_with_sgd(model, X_train[:100], y_train[:100], nepoch=10, evaluate_loss_after=1)
2015-09-30 10:08:19: Loss after num_examples_seen=0 epoch=0: 8.987425
2015-09-30 10:08:35: Loss after num_examples_seen=100 epoch=1: 8.976270
2015-09-30 10:08:50: Loss after num_examples_seen=200 epoch=2: 8.960212
2015-09-30 10:09:06: Loss after num_examples_seen=300 epoch=3: 8.930430
2015-09-30 10:09:22: Loss after num_examples_seen=400 epoch=4: 8.862264
2015-09-30 10:09:38: Loss after num_examples_seen=500 epoch=5: 6.913570
2015-09-30 10:09:53: Loss after num_examples_seen=600 epoch=6: 6.302493
2015-09-30 10:10:07: Loss after num_examples_seen=700 epoch=7: 6.014995
2015-09-30 10:10:24: Loss after num_examples_seen=800 epoch=8: 5.833877
2015-09-30 10:10:39: Loss after num_examples_seen=900 epoch=9: 5.710718
看起来,我们的实现的最终结果跟我们想的一样,确实损失一直在减少。
我之前写过一个关于Theano的教程,因为运行在Gpu上的代码和上面的代码逻辑啥的都一样。所以在这里我不会再次去重复一遍代码。仅仅定义一个RNNTheano的类,用Theano中响应的计算去代替numpy的计算。代码在这里
np.random.seed(10)
model = RNNTheano(vocabulary_size)
%timeit model.sgd_step(X_train[10], y_train[10], 0.005)
这次,SGD这一步在我的mac上仅仅花费了70ms、在使用GPU的g2.2xlarge Amazon EC2实例上需要23毫秒。有了将近15倍的提高。这就意味着我们整体的训练在几个小时或者一天就可以跑完,而不是需要几天或者一周的时间。虽然依然还得好几个小时的时间,但是对于我们来说,已经是不错的结果了。
为了避免你花费一天的时间训练模型,我们已经帮你预先训练好了一个隐层为50、词汇量大小为8000的一个theano的模型。我迭代训练了20次,大概花费了20多个小时的时间。损失函数的值在训练的时候一直在减少。获取迭代跟多的次数可能会达到更好的模型,但是这意味着也要花费更多的时间。你可以尝试一下哦。你可以在github仓库中的data/train-model-threno.npz中找到模型的参数。通过load_model_parameters_theano方法去加载这些模型的参数:
from utils import load_model_parameters_theano, save_model_parameters_theano
model = RNNTheano(vocabulary_size, hidden_dim=50)
# losses = train_with_sgd(model, X_train, y_train, nepoch=50)
# save_model_parameters_theano('./data/trained-model-theano.npz', model)
load_model_parameters_theano('./data/trained-model-theano.npz', model)
现在我们已经有了一个模型,现在我们用它来生成我们想要的文本,让我们来实现一个辅助函数来生成新的句子:
def generate_sentence(model):
# We start the sentence with the start token
new_sentence = [word_to_index[sentence_start_token]]
# Repeat until we get an end token
while not new_sentence[-1] == word_to_index[sentence_end_token]:
next_word_probs = model.forward_propagation(new_sentence)
sampled_word = word_to_index[unknown_token]
# We don't want to sample unknown words
while sampled_word == word_to_index[unknown_token]:
samples = np.random.multinomial(1, next_word_probs[-1])
sampled_word = np.argmax(samples)
new_sentence.append(sampled_word)
sentence_str = [index_to_word[x] for x in new_sentence[1:-1]]
return sentence_str
num_sentences = 10
senten_min_length = 7
for i in range(num_sentences):
sent = []
# We want long sentences, not sentences with one or two words
while len(sent) < senten_min_length:
sent = generate_sentence(model)
print " ".join(sent)
下面是几个生成的例子,我加了首字母大写:
Yep, please disappear with the terrible generation.
现在来看一下这些生成的句子,他们准确的学会了如何使用语法。并且学会是使用标点符号。
然而,绝大多数生成的句子都是没有意义的,或者存在语法错误。上面几个是我挑出来还不错的句子。其中一个原因可能是我们没有足够的时间或者足够的训练数据集去训练我们的模型。这可能是一个事实,但不是最主要的原因。最主要的原因是我们的RNN网络不能生成完整的句子,因为他不能学习相隔几个步骤之间的单词的联系。这也是RNN网络为什么没有得到普遍的流行的原因。虽然他的理论听起来特别完美。但是实际效果却不怎么完美。我也不知道为什么会这样。
幸运的是,对于训练RNN的困难,阅读这篇博客,你可能就会更好的理解了(https://arxiv.org/abs/1211.5063)。再下一部分的教程中。我们将会详细的为你解释反向传播时间(BPTT)算法。并且演示什么叫做梯度消失的问题。这将会刺激我们去研究更复杂的RNN模型,例如LSTM模型。LSTM模型目前是NLP领域最好的模型。你在本教程中学到的所有内容也将使用与LSTM和其他的RNN模型。所以如果vanilla RNN的训练结果没有你想象的那么好,也不要气馁。
好了本教程结束,翻译也结束啦 哈哈,第一次翻译这类文章,如果那些地方翻译的不好请在评论中指正。大家一起学习交流。感谢大家。