下面来做一个实验,用RNN预测语言模型,并让它输出一句话,具体业务描述如下。
先让RNN学习一段文字,之后模型可以根据我们的输入再自动预测后面的文字。同时将模型预测出来的文字当成输入,再放到模型里,模型就会预测出下一个文字,这样循环下去,可以看到RNN能够输出一句话。
那么RNN是怎么样来学习这段文字呢?这里将整段文字都看成一个个的序列。在模型里预设值只关注连续的4个序列,这样在整段文字中,每次随意拿出4个连续的文字放到模型里进行训练,然后把第5个连续的值当成标签,与输出的预测值进行loss的计算,形成一个可训练的模型,通过优化器来迭代训练。
实例描述
通过让RNN网络对一段文字的训练学习来生成模型,最终可以使用机器生成的模型来表达自己的意思。下面看看具体实现过程。
1、准备样本
这个环节很简单,随便复制一段话放到txt里即可。在例子中使用的样本如下:
在尘世的纷扰中,只要心头悬挂着远方的灯光,我们就会坚持不懈地走,理想为我们灌注了精神的蕴藉。所以,生活再平凡、再普通、再琐碎,我们都要坚持一种信念,默守一种精神,为自己积淀站立的信心,前行的气力。
这是笔者随意下载的一段文字,把该段文字放到代码同级目录下,起名为wordstest.txt
。
1.定义基本工具函数
具体的基本工具函数与语音识别例子差不多,都是与文本处理相关的,首先引入头文件,然后定义相关函数,其中get_ch_lable
函数从文件里获取文本,get_ch_lable_v
函数将文本数组转换成向量。具体如下。
代码9-25 rnnwordtest
# -*- coding: utf-8 -*-
import numpy as np
import tensorflow as tf
from tensorflow.contrib import rnn
import random
import time
from collections import Counter
start_time = time.time()
def elapsed(sec):
if sec < 60:
return str(sec) + " sec"
elif sec < (60 * 60):
return str(sec / 60) + " min"
else:
return str(sec / (60 * 60)) + " hr"
# Target log path
tf.reset_default_graph()
training_file = 'wordstest.txt'
# 中文多文件
def readalltxt(txt_files):
labels = []
for txt_file in txt_files:
target = get_ch_lable(txt_file)
labels.append(target)
return labels
# 中文字
def get_ch_lable(txt_file):
labels = ""
with open(txt_file, 'rb') as f:
for label in f:
labels = labels + label.decode('utf-8')
# labels = labels + label.decode('gb2312')
return labels
# 优先转文件里的字符到向量
def get_ch_lable_v(txt_file, word_num_map, txt_label=None):
words_size = len(word_num_map)
to_num = lambda word: word_num_map.get(word, words_size)
if txt_file != None:
txt_label = get_ch_lable(txt_file)
labels_vector = list(map(to_num, txt_label))
return labels_vector
2.样本预处理
样本预处理工作主要是读取整体样本,并存放到training_data里,获取全部的字表words,并生成样本向量wordlabel和与向量对应关系的word_num_map。具体代码如下。
代码 rnnwordtest(续)
training_data = get_ch_lable(training_file)
print("Loaded training data...")
counter = Counter(training_data)
words = sorted(counter)
words_size = len(words)
word_num_map = dict(zip(words, range(words_size)))
print('字表大小:', words_size)
wordlabel = get_ch_lable_v(training_file, word_num_map)
2、构建模型
本例中使用多层RNN模型,后面接入一个softmax
分类,对下一个字属于哪个向量进行分类,这里认为一个字就是一类。整个例子步骤如下。
1.设置参数定义占位符
学习率为0.001,迭代10000次,每1000次输出一次中间状态。每次输入4个字,来预测第5个字。
网络模型使用了3层的LSTM RNN,第一层为256个cell,第二层和第三层都是512个cell。
代码 rnnwordtest(续)
# 定义参数
learning_rate = 0.001
training_iters = 10000
display_step = 1000
n_input = 4
n_hidden1 = 256
n_hidden2 = 512
n_hidden3 = 512
# 定义占位符
x = tf.placeholder("float", [None, n_input, 1])
wordy = tf.placeholder("float", [None, words_size])
代码中定义了两个占位符x和wordy,其中,x代表输入的4个连续文字,wordy则代表一个字,由于用的是字索引向量的one_hot编码,所以其大小为words_size,代表总共的字数。
2.定义网络结构
将x形状变换并按找时间序列裁分,然后放入3层LSTM网络,最终通过一个全连接生成words_size个节点,为后面的softmax做准备。具体代码如下。
代码 rnnwordtest(续)
x1 = tf.reshape(x, [-1, n_input])
x2 = tf.split(x1, n_input, 1)
# 2-layer LSTM,每层有 n_hidden 个units
rnn_cell = rnn.MultiRNNCell([rnn.LSTMCell(n_hidden1), rnn.LSTMCell(n_hidden2), rnn.LSTMCell(n_hidden3)])
# 通过RNN得到输出
outputs, states = rnn.static_rnn(rnn_cell, x2, dtype=tf.float32)
# 通过全连接输出指定维度
pred = tf.contrib.layers.fully_connected(outputs[-1], words_size, activation_fn=None)
3.定义优化器
优化器同样使用AdamOptimizer,loss使用的是softmax的交叉熵,正确率是统计one_hot中索引对应的位置相同的个数。
代码 rnnwordtest(续)
# 定义loss与优化器
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=pred, labels=wordy))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss)
# 模型评估
correct_pred = tf.equal(tf.argmax(pred, 1), tf.argmax(wordy, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
4.训练模型
在训练过程中同样添加保存检查点功能。在session中每次随机取一个偏移量,然后取后面4个文字向量当作输入,第5个文字向量当作标签用来计算loss。
代码 rnnwordtest(续)
savedir = "log/rnnword/"
saver = tf.train.Saver(max_to_keep=1) # 生成saver
# 启动session
with tf.Session() as session:
session.run(tf.global_variables_initializer())
step = 0
offset = random.randint(0, n_input + 1)
end_offset = n_input + 1
acc_total = 0
loss_total = 0
kpt = tf.train.latest_checkpoint(savedir)
print("kpt:", kpt)
startepo = 0
if kpt != None:
saver.restore(session, kpt)
ind = kpt.find("-")
startepo = int(kpt[ind + 1:])
print(startepo)
step = startepo
while step < training_iters:
# 随机取一个位置偏移
if offset > (len(training_data) - end_offset):
offset = random.randint(0, n_input + 1)
inwords = [[wordlabel[i]] for i in range(offset, offset + n_input)] # 按照指定的位置偏移获得后4个文字向量,当作输入
inwords = np.reshape(np.array(inwords), [-1, n_input, 1])
out_onehot = np.zeros([words_size], dtype=float)
out_onehot[wordlabel[offset + n_input]] = 1.0
out_onehot = np.reshape(out_onehot, [1, -1]) # 所有的字都变成onehot
_, acc, lossval, onehot_pred = session.run([optimizer, accuracy, loss, pred],
feed_dict={x: inwords, wordy: out_onehot})
loss_total += lossval
acc_total += acc
if (step + 1) % display_step == 0:
print("Iter= " + str(step + 1) + ", Average Loss= " + \
"{:.6f}".format(loss_total / display_step) + ", AverageAccuracy= " + \
"{:.2f}%".format(100 * acc_total / display_step))
acc_total = 0
loss_total = 0
in2 = [words[wordlabel[i]] for i in range(offset, offset + n_input)]
out2 = words[wordlabel[offset + n_input]]
out_pred = words[int(tf.argmax(onehot_pred, 1).eval())]
print("%s - [%s] vs [%s]" % (in2, out2, out_pred))
saver.save(session, savedir + "rnnwordtest.cpkt", global_step=step)
step += 1
offset += (n_input + 1) # 调整下一次迭代使用的偏移量
print("Finished!")
saver.save(session, savedir + "rnnwordtest.cpkt", global_step=step)
print("Elapsed time: ", elapsed(time.time() - start_time))
由于检查点文件是建立在log/rnnword/目录下的,所以在运行程序之前需要先在代码文件的当前目录下依次建立log/rnnword/文件夹(有兴趣的读者可以改成自动创建)。运行代码,训练模型得到如下输出:
……
Type is unsupported, or the types of the items don't match field type in
CollectionDef.
'dict' object has no attribute 'name'
Iter= 9000, Average Loss=0.585445, Average Accuracy=79.10%
['注','了','精','神'] - [的]vs[了]
WARNING:tensorflow:Error encountered when serializing LAYER_NAME_UIDS.
Type is unsupported, or the types of the items don't match field type in
CollectionDef.
'dict' object has no attribute 'name'
Iter= 10000, Average Loss= 0.409709, Average Accuracy= 85.60%
['平','凡','、','再'] - [普]vs[普]
WARNING:tensorflow:Error encountered when serializing LAYER_NAME_UIDS.
Type is unsupported, or the types of the items don't match field type in
CollectionDef.
'dict' object has no attribute 'name'
******ebook converter DEMO Watermarks*******
Finished!
WARNING:tensorflow:Error encountered when serializing LAYER_NAME_UIDS.
Type is unsupported, or the types of the items don't match field type in
CollectionDef.
'dict' object has no attribute 'name'
Elapsed time: 1.3609554409980773 min
迭代10000
次的正确率达到了85%。达到了模型基本可用的状态。当然这只是个例子,读者可以尝试在模型中添加全连接及更多节点的LSTM或是更深层的LSTM来优化识别率,并且当学习的字数变多时,还会有更强大的拟合功能。
5.运行模型生成句子
启用一个循环,等待输入文字,当收到输入的文本后,通过eval计算onehot_pred节点,并进行文字的转义,得到预测文字。接下来将预测文字再循环输入模型中,预测下一个文字。代码中设定循环32次,输出32个文字。
代码9-25 rnnwordtest(续)
while True:
prompt = "请输入%s个字: " % n_input
sentence = input(prompt)
inputword = sentence.strip()
if len(inputword) != n_input:
print("您输入的字符长度为:", len(inputword), "请输入4个字")
continue
try:
inputword = get_ch_lable_v(None, word_num_map, inputword)
for i in range(32):
keys = np.reshape(np.array(inputword), [-1, n_input, 1])
onehot_pred = session.run(pred, feed_dict={x: keys})
onehot_pred_index = int(tf.argmax(onehot_pred, 1).eval())
sentence = "%s%s" % (sentence, words[onehot_pred_index])
inputword = inputword[1:]
inputword.append(onehot_pred_index)
print(sentence)
except:
print("该字我还没学会")
运行代码,输出如下:
请输入4个字: 生活平凡
生活平凡,要坚持一种信念,默守一种精神,为自己积淀站立的信心,默守一种精
在本例中,输入了“生活平凡”4个字,可以看到神经网络自动按照这个开头开始往下输出句子,看起来语句还算通顺。