大家好,又见面了,我是你们的朋友全栈君。
反向传播(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结构如下所示:
st 是 t 时刻的隐藏状态,它表示网络的记忆单元,由前一时刻的隐藏状态和当前时刻的输入来确定, ot 表示 t 时刻的输出,它与隐藏状态有关:
st
tanh
ot=g(Vst) ,假定 g 为
softmax
U,W,V 分别是输入与隐藏状态、先前隐藏状态与当前隐藏状态、隐藏状态与输出之间的连接权重,可分别叫做输入权重、循环权重、输出权重。
BPTT与传统的反向传播算法基本相同,包含如下三个步骤:
(1) 前向传播计算输出 o 和隐藏状态
s
(2) 反向传播计算误差 δ ,表示模型目标函数 E 对加权输入
nett=(Uxt+Wst−1)
(3) 求解模型目标函数
E
(1) 前向计算
利用如下两式从输入层 xt 计算每层的隐藏状态 s 和 输出
o
st = f(Uxt+Wst−1) ,假定 f 为
tanh
ot=g(Vst) ,假定 g 为
softmax
(2) 误差 δ 的计算
和 nett=(Uxt+Wst−1) 可以看到,目标函数对 net 的偏导数与时间 t 相关,因此需要求解每个时刻
t
每一时刻 t ,
δ
对于输出层 L :
由于
ot=g(Vst)
对于其它层
l
由于 st = f(Uxt+Wst−1) ,误差向输入层传播,可得 (δlt)T=(δl+1t)TUf′(netlt) 。
δ 沿时间 t 的反向传播
对于某一隐藏状态层,由
st
δTt−1=δTtWdiag(f′(nett−1)) 。
(3) 权重偏导数的计算
模型目标函数 E 对输出权重
V
∂E∂V=g′(Vst)∗st
模型目标函数 E 对循环权重
W
∂E∂W=Σt∂Et∂W , E 对循环权重
W
其中 t 时刻 :
∂Et∂W=δTtst−1
模型目标函数 E 对输入权重
U
∂E∂U=Σt∂Et∂U , E 对输入权重
U
其中 t 时刻 :
∂Et∂U=δ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 np
import nltk, itertools, csv
TXTCODING = '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 = 8000
myTokenFile = tokenFile2vector(file_path, dict_size)
X_train, y_train, dict_words, index_of_words = myTokenFile.get_vector()
实现RNN
首先生成 myRNN类,myrnn.py
import tokenFile
import 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.97211993132 epoch 2: loss = 8.93082011501 epoch 3: loss = 6.7136525424 epoch 4: loss = 6.21936677548 epoch 5: loss = 6.00813231779 epoch 6: loss = 5.87637648866 epoch 7: loss = 5.78373455074 epoch 8: loss = 5.71807642521 epoch 9: loss = 5.63435329796 epoch 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中可以发现,梯度的求解过程中存在这’消失的梯度’或’爆炸的梯度’问题。
消失的梯度:
比如,在 δTt−1=δTtWdiag(f′(nett−1)) 中,若 f 为 tanh (或 sigmoid ),其将函数值限定在 [−1,1] ( sigmoid 为 [0,1] ),将其导数限定在 [0,1] ( sigmoid 为 [0,1/4] ),因此,误差 δt 沿时间 t 的长时间传播将导致其值趋于0,从而使相应的权重梯度趋于0,造成梯度消失的现象,失去学习效果。
爆炸的梯度:
当
Wdiag(f′(nett−1))
梯度消失问题解决方法:
目前已有一些解决梯度消失的方法,比如,合适的权值初始化;使用
ReLU
更有效且受关注的解决方法是采用 Long Short-Term Memory (LSTM) 或 Gated Recurrent Unit (GRU) 结构,他们专为解决梯度消失而生。
RNN的介绍先到这里,后续跟进。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/152316.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...