反向传播(BPTT)与循环神经网络(RNN)文本预测

来源:互联网 发布:rs232转网络 编辑:程序博客网 时间:2024/05/12 00:13

反向传播(BPTT)与循环神经网络(RNN)文本预测

BPTT 与 RNN文本预测

参考博客:

  • http://www.wildml.com/2015/10/recurrent-neural-networks-tutorial-part-3-backpropagation-through-time-and-vanishing-gradients/
  • http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-2-implementing-a-language-model-rnn-with-python-numpy-and-theano/

参考论文:
- A guide to recurrent neural networks and backpropagation

本文介绍简单Recurrent Neural Networks(RNN)的基本训练算法BACKPROPAGATION THROUGH TIME (BPTT),并用 python2.7 实现RNN的文本预测。

1. RNN训练方法:BACKPROPAGATION THROUGH TIME (BPTT)

基本RNN结构如下所示:

简单的RNN

stt 时刻的隐藏状态,它表示网络的记忆单元,由前一时刻的隐藏状态和当前时刻的输入来确定,ot 表示 t 时刻的输出,它与隐藏状态有关:

st=f(Uxt+Wst1),假定ftanh
ot=g(Vst),假定 gsoftmax 函数。

U,W,V 分别是输入与隐藏状态、先前隐藏状态与当前隐藏状态、隐藏状态与输出之间的连接权重,可分别叫做输入权重、循环权重、输出权重。

BPTT与传统的反向传播算法基本相同,包含如下三个步骤:
(1) 前向传播计算输出 o 和隐藏状态 s
(2) 反向传播计算误差 δ ,表示模型目标函数 E 对加权输入 nett=(Uxt+Wst1) 的偏导;(不同的是,在BPTT中,δ 的传播沿两个方向,分别为从输出层传递至输入层,以及沿时间 t 反向传播)
(3) 求解模型目标函数 E 对权重 U,W,V 的偏导数。

(1) 前向计算

利用如下两式从输入层 xt 计算每层的隐藏状态 s 和 输出 o
st=f(Uxt+Wst1),假定ftanh
ot=g(Vst),假定 gsoftmax 函数;

(2) 误差 δ 的计算

nett=(Uxt+Wst1) 可以看到,目标函数对 net 的偏导数与时间 t 相关,因此需要求解每个时刻 t 的误差 δt

每一时刻 tδ 从输出层传递至输入层的计算
对于输出层 L
由于 ot=g(Vst) ,则 δLt=Eotg(Vst) ,其中 ytt 时刻的真实输出值;
对于其它层 l
由于 st=f(Uxt+Wst1) ,误差向输入层传播,可得 (δlt)T=(δl+1t)TUf(netlt)

δ 沿时间 t 的反向传播
对于某一隐藏状态层,由 st=f(Uxt+Wst1) 可得 δtδt1 的关系为:
δTt1=δTtWdiag(f(nett1))

(3) 权重偏导数的计算

模型目标函数 E 对输出权重 V 的偏导数:
EV=g(Vst)st

模型目标函数 E 对循环权重 W 的偏导数:
EW=ΣtEtWE 对循环权重 W 的偏导数等于各时刻的偏导数之和。

其中 t 时刻 :
EtW=δTtst1

模型目标函数 E 对输入权重 U 的偏导数:
EU=ΣtEtUE 对输入权重 U 的偏导数等于各时刻的偏导数之和。

其中 t 时刻 :
EtU=δTtxt

2. RNN预测文本实战

本文采用dataset available on Google’s BigQuery的前10000条评论数据作为RNN的训练数据,然后基于训练后的RNN,生成新的评论文本。

(1) 数据预处理

文本标记

首先,将每个训练数据进行词语标记。比如 ‘who cares what you think ?’ 标记为[‘who’, ‘cares’, ‘you’, ‘think’, ‘?’]
这里会用到自然语言处理包nltk中的相关函数。

去除不常用词,标记词典中不存在的词

有些词语在所有句子中可能仅出现过几次,而词典的长度不能过大,否则会加长训练时间。因此,一种简单的做法是将不常用词从词典中去除。

对于词典中不存在的词,可统一标记为 ‘UNKNOWN_TOKEN’,并将 ‘UNKNOWN_TOKEN’ 加入词典。

标记每个样本句的开头和结尾

为便于训练,分别用’START_TOKEN’ 和 ‘END_TOKEN’ 标记每句话的开头和结尾,并将’START_TOKEN’ 和 ‘END_TOKEN’ 加入词典。

将文本转化为词向量

训练前,需将文本转化为数值向量,可将词典中的每个词进行编号。
比如,一个样本为 [‘START_TOKEN’, ‘who’, ‘cares’, ‘what’, ‘you’, ‘think’, ‘?’, ‘END_TOKEN’],其对于的数值向量为 [0,98,1938,53,10,72,19,1],则输入 x=[0,98,1938,53,10,72,19] ,输出 y=[98,1938,53,10,72,19,1] ,各包含7个时刻,即 T=7

(2) RNN实现

文本预处理,获取训练样本

首先实现 tokenFile2vector 类,tokenFile.py

import numpy as npimport nltk, itertools, csvTXTCODING = 'utf-8'unknown_token = 'UNKNOWN_TOKEN'start_token = 'START_TOKEN'end_token = 'END_TOKEN'# 解析评论文件为数值向量class tokenFile2vector:    def __init__(self, file_path, dict_size):        self.file_path = file_path        self.dict_size = dict_size    # 将文本拆成句子,并加上句子开始和结束标志    def _get_sentences(self):        sents = []        with open(self.file_path, 'rb') as f:            reader = csv.reader(f, skipinitialspace=True)            # 去掉表头             reader.next()            # 解析每个评论为句子            sents = itertools.chain(*[nltk.sent_tokenize(x[0].decode(TXTCODING).lower()) for x in reader])            sents = ['%s %s %s' % (start_token, sent, end_token) for sent in sents]            print 'Get {} sentences.'.format(len(sents))            return sents    # 得到每句话的单词,并得到字典及字典中每个词的下标    def _get_dict_wordsIndex(self, sents):        sent_words = [nltk.word_tokenize(sent) for sent in sents]        word_freq = nltk.FreqDist(itertools.chain(*sent_words))        print 'Get {} words.'.format(len(word_freq))        common_words = word_freq.most_common(self.dict_size-1)        # 生成词典        dict_words = [word[0] for word in common_words]        dict_words.append(unknown_token)        # 得到每个词的下标,用于生成词向量        index_of_words = dict((word, ix) for ix, word in enumerate(dict_words))        return sent_words, dict_words, index_of_words    # 得到训练数据    def get_vector(self):        sents = self._get_sentences()        sent_words, dict_words, index_of_words = self._get_dict_wordsIndex(sents)        # 将每个句子中没包含进词典dict_words中的词替换为unknown_token        for i, words in enumerate(sent_words):            sent_words[i] = [w if w in dict_words else unknown_token for w in words]        X_train = np.array([[index_of_words[w] for w in sent[:-1]] for sent in sent_words])        y_train = np.array([[index_of_words[w] for w in sent[1:]] for sent in sent_words])        return X_train, y_train, dict_words, index_of_words

基于以上实现的 tokenFile2vector 类,获得训练样本 X_train, y_train:

file_path = r'/results-20170508-103637.csv'dict_size = 8000myTokenFile = tokenFile2vector(file_path, dict_size)X_train, y_train, dict_words, index_of_words = myTokenFile.get_vector()

实现RNN

首先生成 myRNN类,myrnn.py

import tokenFileimport numpy as np# 输出单元激活函数def softmax(x):    x = np.array(x)    max_x = np.max(x)    return np.exp(x-max_x) / np.sum(np.exp(x-max_x))class myRNN:    def __init__(self, data_dim, hidden_dim=100, bptt_back=4):        # data_dim: 词向量维度,即词典长度; hidden_dim: 隐单元维度; bptt_back: 反向传播回传时间长度        self.data_dim = data_dim        self.hidden_dim = hidden_dim        self.bptt_back = bptt_back        # 初始化权重向量 U, W, V; U为输入权重; W为递归权重; V为输出权重        self.U = np.random.uniform(-np.sqrt(1.0/self.data_dim), np.sqrt(1.0/self.data_dim),                                    (self.hidden_dim, self.data_dim))        self.W = np.random.uniform(-np.sqrt(1.0/self.hidden_dim), np.sqrt(1.0/self.hidden_dim),                                    (self.hidden_dim, self.hidden_dim))        self.V = np.random.uniform(-np.sqrt(1.0/self.hidden_dim), np.sqrt(1.0/self.hidden_dim),                                    (self.data_dim, self.hidden_dim))    # 前向传播    def forward(self, x):        # 向量时间长度        T = len(x)        # 初始化状态向量, s包含额外的初始状态 s[-1]        s = np.zeros((T+1, self.hidden_dim))        o = np.zeros((T, self.data_dim))        for t in xrange(T):            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]    # 预测输出            def predict(self, x):        o, s = self.forward(x)        pre_y = np.argmax(o, axis=1)        return pre_y    # 计算损失, softmax损失函数, (x,y)为多个样本    def loss(self, x, y):        cost = 0                for i in xrange(len(y)):            o, s = self.forward(x[i])            # 取出 y[i] 中每一时刻对应的预测值            pre_yi = o[xrange(len(y[i])), y[i]]            cost -= np.sum(np.log(pre_yi))        # 统计所有y中词的个数, 计算平均损失        N = np.sum([len(yi) for yi in y])        ave_loss = cost / N        return ave_loss    # 求梯度, (x,y)为一个样本    def bptt(self, x, y):        dU = np.zeros(self.U.shape)        dW = np.zeros(self.W.shape)        dV = np.zeros(self.V.shape)        o, s = self.forward(x)        delta_o = o        delta_o[xrange(len(y)), y] -= 1        for t in np.arange(len(y))[::-1]:            # 梯度沿输出层向输入层的传播            dV += delta_o[t].reshape(-1, 1) * s[t].reshape(1, -1)  # self.data_dim * self.hidden_dim            delta_t = delta_o[t].reshape(1, -1).dot(self.V) * ((1 - s[t-1]**2).reshape(1, -1)) # 1 * self.hidden_dim            # 梯度沿时间t的传播            for bpt_t in np.arange(np.max([0, t-self.bptt_back]), t+1)[::-1]:                dW += delta_t.T.dot(s[bpt_t-1].reshape(1, -1))                dU[:, x[bpt_t]] = dU[:, x[bpt_t]] + delta_t                delta_t = delta_t.dot(self.W.T) * (1 - s[bpt_t-1]**2)        return [dU, dW, dV]    # 计算梯度       def sgd_step(self, x, y, learning_rate):        dU, dW, dV = self.bptt(x, y)        self.U -= learning_rate * dU        self.W -= learning_rate * dW        self.V -= learning_rate * dV    # 训练RNN      def train(self, X_train, y_train, learning_rate=0.005, n_epoch=5):        losses = []        num_examples = 0        for epoch in xrange(n_epoch):               for i in xrange(len(y_train)):                self.sgd_step(X_train[i], y_train[i], learning_rate)                num_examples += 1            loss = self.loss(X_train, y_train)            losses.append(loss)            print 'epoch {0}: loss = {1}'.format(epoch+1, loss)            # 若损失增加,降低学习率            if len(losses) > 1 and losses[-1] > losses[-2]:                learning_rate *= 0.5                print 'decrease learning_rate to', learning_rate

训练RNN

rnn = myRNN(dict_size, hidden_dim=100, bptt_back=4)rnn.train(X_train[:200], y_train[:200], learning_rate=0.005, n_epoch=10)

可得如下结果:

epoch 1: loss = 8.97211993132epoch 2: loss = 8.93082011501epoch 3: loss = 6.7136525424epoch 4: loss = 6.21936677548epoch 5: loss = 6.00813231779epoch 6: loss = 5.87637648866epoch 7: loss = 5.78373455074epoch 8: loss = 5.71807642521epoch 9: loss = 5.63435329796epoch 10: loss = 5.56515764008

(3) 用RNN进行文本预测

基于训练好的RNN模型,我们可以得到下一个词将会是什么,从而生成新的文本。

unknown_token = 'UNKNOWN_TOKEN'start_token = 'START_TOKEN'end_token = 'END_TOKEN'def generate_text(rnn, dict_words, index_of_words):    # dict_words: type list; index_of_words: type dict    sent = [index_of_words[start_token]]    # 预测新词,知道句子的结束(END_TOKEN)    while not sent[-1] == index_of_words[end_token]:        next_probs, _ = rnn.forward(sent)        sample_word = index_of_words[unknown_token]        # 按预测输出分布进行采样,得到新的词        while sample_word == index_of_words[unknown_token]:            samples = np.random.multinomial(1, next_probs[-1])            sample_word = np.argmax(samples)        # 将新生成的有含义的词(即不为UNKNOWN_TOKEN的词)加入句子        sent.append(sample_word)    new_sent = [dict_words[i] for i in sent[1:-1]]    new_sent_str = ' '.join(new_sent)    return new_sent_str

生成文本举例:

sent_str = generate_text(rnn, dict_words, index_of_words)print 'Generate sentence:', sent_str

可得如下类似结果:

例子:it 's this he your wealth decisions roof of .mao dam , many are things a go a issue n't you works that a half

3. RNN中的梯度消失或爆炸问题

上边生成文本的结果可知,RNN生成的文本效果比较poor。一个可能的原因是训练数据不足、预处理不精细等等,然而,更重要的原因是RNN对于句子中跨度较大的词间的依赖关系无能为力。其实,从BPTT中可以发现,梯度的求解过程中存在这’消失的梯度’或’爆炸的梯度’问题。

消失的梯度:

比如,在 δTt1=δTtWdiag(f(nett1)) 中,若 ftanh (或 sigmoid),其将函数值限定在 [1,1] (sigmoid[0,1]),将其导数限定在 [0,1] (sigmoid[0,1/4]),因此,误差 δt 沿时间 t 的长时间传播将导致其值趋于0,从而使相应的权重梯度趋于0,造成梯度消失的现象,失去学习效果。

爆炸的梯度:

Wdiag(f(nett1)) 值非常大时,误差 δt 沿时间 t 的长时间传播将导致其值急剧增大,造成梯度爆炸。与梯度消失相比,梯度爆炸比较易于发现,比如梯度出现 NaN;此外,也可以通过限定梯度的最大阈值来避免梯度爆炸的影响。

梯度消失问题解决方法:

目前已有一些解决梯度消失的方法,比如,合适的权值初始化;使用 ReLU 替代 tanhsigmoid (因为 ReLU 导数为0或1,不太可能造成梯度消失现象)。

更有效且受关注的解决方法是采用 Long Short-Term Memory (LSTM) 或 Gated Recurrent Unit (GRU) 结构,他们专为解决梯度消失而生。

RNN的介绍先到这里,后续跟进。

1 0
原创粉丝点击