RNN教程2-用python、numpy和theano实现RNN

在这个第2部分,我们将用python实现一个完整的RNN网络。用到numpy和theano这两个库。

RNN翻译教程目录:

英文出处

1.语言模型

我们的目标是用RNN网络来训练出一个语言模型。简单来说下语言模型是怎么回事,假如我们有一个句子,这个句子由m个单词组成,语言模型允许我们,在观测到这个句子时,能够预测下一个单词是什么(也就是一个单词在此基础上出现的概率),也就是条件概率:
条件概率

举个例子。有一个句子“He went to buy some chocolate”,这个句子是怎么生成的呢?我们可以将其看作给定了”He”这个条件之后出现”went”的概率,乘以给定了”He went”这个条件之后出现”to”的概率,乘以…(以此类推)给定了”He went to buy some”这个条件之后出现”chocolate”的概率。

为什么我们要将概率应用到这上面来呢?

首先,这个模型可以用来当作一个评分模型,比如,在机器翻译系统中,通常会生成多个待选的结果,这时候,我们可以选择那个概率最高的句子作为输出。

再者,这个语言模型还有一个很酷的性质,那就是我们可以生成新的文本,Andrej Karparthy的一篇讲述RNN的有效性的博文里面提到,我们可以通过基于RNN的语言模型来生成新的文本,从莎士比亚的诗集到linux的源码,都是可以的。

我们需要注意到,上面提到的概率模型需要用到一个句子里面的所有文字信息,而很多模型是做不到记忆那么多前置信息的。虽然RNN在理论上是为这个而生,但是,它在长期记忆方面的能力还是有所欠缺的。这个后面会继续探讨。

下面开始讲基于RNN的语言模型的代码实现。

2.训练数据和预处理

为了训练这个语言模型,我们需要训练数据。就好像我们学讲话一样,都是通过大量的练习,才会慢慢形成后来的讲话习惯。

幸运的是,这个模型的实现并不需要任何的人工标记。训练数据,我们选择的是reddit上的15000个较长的评论(原文放在bigtable上面数据集链接貌似已经失效,感兴趣的可以去搜一下,也可以用别的语料来训练,原理是一样的)。我们期望能实现一个模型,能够生成类似于reddit的这些评论的文字。好了,先来预处理一下训练数据。

2.1 分词

我们需要把原始语料用的每一段文字切分成句子,然后每一个句子再切分成单词。英文的分词比较简单,可以直接用NLTK(http://www.nltk.org/)的分词系统,里面word_tokenizesent_tokenize这两个方法就足够了。当然NLTK也支持中文接口,可以参看这篇文章:在NLTK中使用斯坦福中文分词器

2.2 过滤掉低频词

在我们的语料库里面,有的单词只出现一到两次,我们最好把这些低频词给去掉,因为如果词汇太多的话,训练会非常慢。而且,对于这些低频词来说,我们没有足够的上下文信息来支撑他们的训练。这类似我们人类的学习,要学习一个词的意义,我们需要在更多的语境里面看到它们。

在代码里面vocabulary_size代表着词汇表的规模(我将词汇表的大小设置为8000,代表着8000个最常出现的单词,当然你也可以更改啦)。对于词汇表里没有的单词,我们将它设置为UNKNOWN_TOKEN。举个例子,如果我们的词汇表里面没有nonlinearities这个单词,那么句子“nonlineraties are important in neural networks”就会被表示成“UNKNOWN_TOKEN are important in Neural Networks”,UNKNOWN_TOKEN这个词也是在词汇表里面的。最后,当我们需要预测单词的时候,如果预测出来的单词是UNKNOWN_TOKEN的话,我们可以用选择词汇表之外的任意一个单词来替代它,又或者,干脆我们就不要生成含有UNKNOWN_TOKEN的文本。

2.3 开始和结束标记

这个模型是用来生成文本的,文本该怎么开头,又怎么结束呢?为了解决这个问题,我们可以用两个特殊的标记来代表开头和结束。对于每一组训练数据,我们在句子的开头增加SENTENCE_START这个标记,在句子的结尾增加SENTENCE_END这个标记。

2.4 建立训练数据矩阵

循环神经网络RNN的输入都是向量,而不是我们数据集里面的字符串,所以我们需要将数据集的字符串映射成向量。在代码里,用的是这两个方法:index_to_wordword_to_index,单词和索引之间可以相互映射。比如,”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]

3.构建RNN

关于RNN的基本介绍可以看第一篇教程

RNN结构图
RNN结构图

让我们具体的看看RNN怎么应用在我们的语言模型上。输入x就是一系列单词,每一个x_{t}就是一个单独的单词。但是,为了矩阵乘法运算能够起作用,我们不能使用上面例子所说的单词索引表达方法(比如这个[51, 27, 16, 10, 856, 53, 25, 34, 69, 1]
)。那么我们用什么呢?依旧是one-hot表达方法。one-hot很简单,比如我们上面说的单词表的大小为8000,某个词的索引是36,那么用one-hot来表示这个词的话,我们可以表示成一个长度为8000的数组,其中第36位为1,其它位均为0.

因此,每一个x_{t}(单词)都会是一个长度为8000的数组向量,而每一个x就会是一个矩阵,每一行代表一个单词。我们将会在构建神经网络的代码里展示这种变形,而不是在预处理的时候。对应的,o_{t}也是类似的形式,是个8000维的数组,理论上,o_{t}也是一个one-hot表示,而实际上,在我们的神经网络里,训练的结果是–o_{t}会被表示成8000个概率值,每一个概率对应着输出这个词的可能性,我们要选择概率最大的词语作为输出。

让我们重新看看公式:

    \[ s_{t}=\tanh(Ux_{t}+Ws_{t-1}) \]

    \[ o_{t}=\rm softmax(Vs_{t}) \]

我发现一个有用的经验,那就是把每一层的矩阵、向量大小给写出来,有助于你理解整个神经网络的结构。我们假设记忆单元之间连接的神经元数量H=100(称之为隐藏层)。隐藏层越大,可以学到的模式就更复杂,当然需要的计算量也更大。下面就是整个结构的大小:

结构大小

请记住,U,V和W是我们这个模型的核心,也就是神经网络的权重参数,它们最终就是我们从训练数据中学习到的东西。因此,我们需要学习的参数数量为2HC+H^2,在具体的这个网络中,C=8000,H=100,代进去结果就是1610000。注意一下,由于xt是one-hot向量(一位为1,其它均为0),所以第一步它和U的全连接乘法,本质上没什么计算量,只是一个选择Ucolumn的过程。因此,主要的矩阵乘法计算在Vs_{t}这一步。这也是我们希望词汇表越小越好的原因。

有了以上的准备,我们开始实现过程吧。

3.1 初始化

我们从一个初始化所有权重的RNN类开始,我将其命名为RNNNumpy,因为我们使用Theano来实现的。初始化U,V,W是有一点小技巧(tricky)的,我们不能将它们全都置为0,因为这样的话,所有层的计算都会变成对称的。我们需要先随机赋值。很多研究表明,一个初始化数值是会对最后的训练结果产生影响。

事实证明,最好的初始化方法,取决于我们用的是哪种激活函数(在我们的例子里是tanh),另外,还有一种推荐的方法,那就是将权重随机初始化,范围在[-\frac{1}{\sqrt{n}},\frac{1}{\sqrt{n}}]之内,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,先不用管它,我们在后面会解释。

3.2 前向传播

接下来,让我们实现前向传播算法:

def forward_propagation(self, x):
    # The total number of time steps
    T = len(x)
    # During forward propagation we save all hidden states in s because need them later.
    # We add one additional element for the initial hidden, which we set to 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]

注意,在函数的最后,不仅仅返回了输出层,还返回了隐藏状态层。因为我们需要用他们来计算梯度。每一个o_{t}都是一个8000维的向量,代表着输出每一个词的概率,我们只想要概率最高的那个词,所以,下面用一个predict函数来实现:

def predict(self, x):
    # Perform forward propagation and return index of the highest score
    o, s = self.forward_propagation(x)
return np.argmax(o, axis=1)

让我们来试运行一下

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个值,对应着词典中的每一个词可能是下一个单词的概率。当然,现在还没有开始训练,所有的值都是随机的。

然后下面看下predict函数:

predictions = model.predict(X_train[10])
print predictions.shape
print predictions

它给我们返回了45个结果,对应着词典的索引:

(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]

3.3 计算误差

为了训练我们的网络,我们需要需要量化误差,我们将这种量化的函数称为损失函数L,而我们的目标则是找到最优的U,V,W,使得误差最小。一个经常使用的误差函数叫做 cross-entropy loss(交叉熵损失函数)(关于交叉熵损失函数有一篇中文博客可以看下:http://blog.csdn.net/u012162613/article/details/44239919)。假设我们有N个训练样本(注意,这里不是指N个句子,而是训练样本中所有的单词的个数,因为对于这个模型而言,每输入一个单词,会对应一个输出),还有C个类别(也就是词典的大小,8000),损失函数里面有两个参数:预测输出o和实际标注y,如下式:

    \[ L(y,o)=-\frac{1}{N}\sum_{n\subseteq N}y_n\log{o_n} \]

上面的式子看上去好像有点复杂,但我们仔细看一下,它本质其实就是在度量预测输出值o和实际标注值的差距。原始的交叉熵损失函数
也是这么干的,他有一个重要的结论就是输出值和实际值越接近,整个损失函数就越小。(再重新具体看一下这个式子,yn是一个8000维的one-hot向量,和上面【构建RNN】步骤里面红色字一样,它只是起一个选择的作用,而o_{n}是一个概率,假如o_{n}越接近1,那么\log{o_{n}}就越接近0,损失函数的值就越小,这和上面的解释是吻合的。举个例子,比如y_{n}=[0,0,1],o_{n}=[0.1,0.4,0.5],\sum_{n\subseteq N}y_n\log{o_n}=1\times \log{0.5}= \log{0.5},看到没有,其他的0我们是可以忽略的,而且如果预测越精准(概率趋于1),\sum_{n\subseteq N}y_n\log{o_n}\approx \log{1}=0,也就是损失几乎为0)

下面两个函数就是具体总损失,以及平均损失。

def calculate_total_loss(self, x, y):
    L = 0
    # For each sentence...
    for i in np.arange(len(y)):
        o, s = self.forward_propagation(x[i])
        # We only care about our prediction of the "correct" words
        correct_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(correct_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

3.4 用SGD算法和BPTT算法训练RNN模型

记住,我们想要通过训练数据,训练出能够使得损失最小的U,V,W。最常见的做法是SGD(Stochastic gradient descent)随机梯度下降。SGD背后的原理非常简单,我们循环遍历所有的训练样本,在每一次的循环当中,我们使得参数(也就是这里的U,V,W)的沿着某个方向下降,从而能让损失减少。这里所说的方向,就是由损失的梯度决定的:\frac{\partial L}{\partial U},\frac{\partial L}{\partial V},\frac{\partial L}{\partial W}

SGD算法还需要一个学习率( learning rate),这个学习率定义了我们在每一次迭代中,要跨多大的步子(学习率越大,学习速度越快,但容易“穿越“,导致找不到最优解;学习率越小,学习速度越慢)。这个算法不仅仅是应用在神经网络上,它在很多传统的机器学习算法上面都有大量的应用。关于这个算法,你可以深入的去了解,也有很多研究是针对它的,这是一篇教程:http://cs231n.github.io/optimization-1/

再回到我们的问题上来,我们该如何计算上面所提到的梯度呢?在传统的神经网络中,我们使用的是反向传播算法,而在RNN中,我们使用BPTT(Backpropagation Through Time )算法,用中文直白的理解就是,跨越时间的反向传播算法。为什么在RNN里面就不一样了呢?因为在这个模型当中,所有时间步的参数是共享权重的,而每一个输出的梯度,不仅仅取决于当前时间步的计算结果,还取决于在此之前所有时间步的计算结果。当然了,我们解决这个问题也是使用链式法则。关于反向传播算法,可以参看我之前写的【从梯度下降到反向传播(附计算例子)】,也可以参看这两篇英文博客:

http://cs231n.github.io/optimization-2/
http://colah.github.io/posts/2015-08-Backprop/

现在,暂且把BPTT算法当作一个黑盒子来使用吧,在下一篇教程中,我会详细介绍这个算法的。我们来看下代码实现,bptt函数最后会返回三个梯度,用于更新权重:

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]

3.5 梯度检查

当你在实现一个反向传播算法的时候,你可以同时做一个梯度检查,来检验你的算法是否正确。检查的背后原理也很简单,那就是从导数的定义出发,也就是下面的这个式子:

导数的定义

下面是实现代码:

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)

3.6 SGD算法的实现

有了上面的准备工作,我们就可以使用SGD算法来更新权重了。我倾向于用两个步骤来实现它:1. sgd_step方法:在一个batch上更新权重。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

搞定!

4.用TheanoGPU来训练我们的神经网络

原文作者在之前有写过关于Theano的一个教程(http://www.wildml.com/2015/09/speeding-up-your-neural-network-with-theano-and-the-gpu/),这里就不多赘述了,作者用theano实现了上面的神经网络,可以在github上面淘到:https://github.com/dennybritz/rnn-tutorial-rnnlm

使用示例:

np.random.seed(10)
model = RNNTheano(vocabulary_size)
%timeit model.sgd_step(X_train[10], y_train[10], 0.005)

显然速度会得到大量的提升。

如果你的电脑性能不太好的话,可能训练上好几天都不行。为此,我放出我自己预训练的theano模型:https://github.com/dennybritz/rnn-tutorial-rnnlm/blob/master/data/trained-model-theano.npz

使用方法:

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)

5.生成文本

现在已经有了模型了,我们来看看生成文本的效果:

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)

下面是我挑选的几个生成的句子(我人工为首字母加上了大写)

  • Anyway, to the city scene you’re an idiot teenager.
  • What ? ! ! ! ! ignore!
  • Screw fitness, you’re saying: https
  • Thanks for the advice to keep my thoughts around girls.
  • Yep, please disappear with the terrible generation.

瞧瞧上面生成的句子,有一些有意思的东西值得注意。这个模型成功的学习到了语法,逗号和句号都基本放对位置了,有时候它还能模仿一些网络用语还有符号表情。

然而!!大部分生成的句子都是没有什么实际意义的,又或者有一些语法错误(上面几句是我挑的比较好的了)。分析一下原因。

首先可能是因为我们的训练时间还不够,或者训练数据不够。看上去这个原因很充分,但它其实并不是最主要的原因。

最主要的原因在于模型本身:我们的这个原始RNN模型不能学习到相隔几个词之外的依赖关系。这很奇怪,理论上这个模型就是为了长期依赖而生的,但实际上它还是表现不佳。

幸运的是,对于为什么RNN的训练那么困难已经不太难理解了(可以看这论文:http://arxiv.org/abs/1211.5063)。

下一篇教程,我们将会详细的探索BPTT算法,还会阐述一下梯度消失问题。这给了我们动力去探索更复杂的RNN模型,比如LSTM,这个NLP任务中表现很好的模型。别担心,至此为止你学到的东西,应用到LSTM上面也是一样的!

发表评论

电子邮件地址不会被公开。 必填项已用*标注