lstm实现困惑度
- 困惑度是什么
- 具体实现
- 数据准备
- vocab
- Word2id
- batch_data
- 模型的配置
困惑度是什么
通常在永ngram语言模型的时候,通常用困惑度来描述这个query的通顺程序,ngram是一个统计概率模型。
但是ngram模型有一个缺点,就是通常我们使用的是2-gram或者3-gram,那么对于大于3个字或词以上的信息就不能捕获到了,但是循环神经网络可以将任意长度的信息都捕获到,这也是循环神经网络预测句子的通顺程度的优势。
具体实现
数据准备
这儿采用的是PTB数据集,下载地址在数据。
只需要是使用
ptb.train.txt
ptb.text.txt
ptb.valid.txt
这些 数据是什么呢?他们里面是一些英文的句子,相邻的单词是用空格隔开的,对于不常出现的词用<unk>表示,句子结尾的换行符用<eos>来表示,据统计,train.txt里面的句子一个有10000个单词(加上这两个特殊的字符)。
vocab
为了将这个句子中的单词送入lstm中,那么需要将这些单词id化,也就是将这10000个单词每一个都赋给一个具体的数字来表示它们。在id化之前我们需要给每一个单词都赋予一个数字id。这个py文件叫做generate_vocab.py
具体的代码
# _*_ encoding=utf8 _*_
import codecs
import collections
from operator import itemgetter
import config
counter = collections.Counter()
with codecs.open(config.train_data, 'r', encoding='utf-8') as fr:
for line in fr:
line = line.strip().split()
for word in line:
counter[word] += 1
sorted_word_to_counter = sorted(counter.items(), key=itemgetter(1), reverse=True)
sorted_words = [x[0] for x in sorted_word_to_counter]
sorted_words.insert(0, "<eos>")
with codecs.open(config.VOCAB_OPTPUT, 'w', encoding='utf-8') as fw:
for word in sorted_words:
fw.write(word + "\n")
print("词汇表生成完成")
同时,我们应该准备分离的原则,将配置信息单独提出来,叫config.py
# _*_ encoding=utf8 _*_
# 训练文件
train_data = "data/ptb/ptb.train.txt"
test_data = "data/ptb/ptb.test.txt"
valid_data = "data/ptb/ptb.valid.txt"
# 词汇表的位置
VOCAB_OPTPUT = "data/output/ptb.vocab"
# 训练文件id化
train2id_data = "data/output/ptb.train"
test2id_data = "data/output/ptb.test"
valid2id_data = "data/output/ptb.valid"
# 根据训练文件生成id词典
word_dict = "data/output/word_dict"
# 参数
train_batch_size = 2
train_step_num = 35
现在就可以得到train.txt里面所有的单词的信息,具体信息 如下(部分展示)
<eos>
the
<unk>
N
of
to
a
in
and
's
that
Word2id
接下来就继续上一步没有完成的word2id步骤,需要将训练,测试,验证三个文件都id化。
# _*_ encoding=utf8 _*_
"""
处理好每个单词的统计数据后,再将训练文件没测试文件根据单词表id化
"""
import codecs
import config
with codecs.open(config.VOCAB_OPTPUT, 'r', 'utf-8') as fr:
vocab = {word.strip() for word in fr.readlines()}
word2id = {k:v for (k,v) in zip(vocab,range(len(vocab)))}
# 低频次用unk替换
def get_id(word):
return word2id[word] if word in word2id else word2id['<unk>']
sort_word2id = sorted(word2id.items(),key = lambda x:x[1],reverse = True)
with codecs.open(config.word_dict, "w", encoding="utf-8") as fw:
for (key,value) in sort_word2id:
fw.write(key + "\t" + str(value) + "\n")
with codecs.open(config.train2id_data, "w", encoding="utf-8") as fw:
with codecs.open(config.train_data, "r", encoding="utf-8") as fr:
for index,line in enumerate(fr):
words = line.strip().split() + ["<eos>"]
out_line = ' '.join([str(get_id(word)) for word in words])
fw.write(out_line + "\n")
print("词典生成完成,训练文件id化完成")
with codecs.open(config.test2id_data, "w", encoding="utf-8") as fw:
with codecs.open(config.test_data, "r", encoding="utf-8") as fr:
for index,line in enumerate(fr):
words = line.strip().split() + ["<eos>"]
out_line = ' '.join([str(get_id(word)) for word in words])
fw.write(out_line + "\n")
print("词典生成完成,测试文件id化完成")
with codecs.open(config.valid2id_data, "w", encoding="utf-8") as fw:
with codecs.open(config.valid_data, "r", encoding="utf-8") as fr:
for index,line in enumerate(fr):
words = line.strip().split() + ["<eos>"]
out_line = ' '.join([str(get_id(word)) for word in words])
fw.write(out_line + "\n")
print("词典生成完成,验证文件id化完成")
现在我们可以得到新的三个文件,这三个文件就是最开始的三个数据文件把单词转成数字id的文件,比如一句话“i love u”,经过id化之后,现在的信息就可能变成了12 2 30。
batch_data
在实际训练中,我们是会将数据一个batch一个batch塞进去的,但是每个句子的长度不一样,这儿我们是需要定长输入的,所以一般做法是补padding,也就是将所有的句子补padding,直到和最长的句子一样长。但是在这里,我们是预测困惑度,说白了,也就是根据前面的单词预测后面单词出现的概率,所以我们不能丢掉上下文信息,所以这儿给出了另外一个补padding的方法。
具体做法:将整个文档的句子一次连接起来,当做一个句子来训练,这样就不会丢掉信息了,但是这样的话merge后的句子太长了,并不太现实,所以这儿将merge句子切割成登场的子序列,这样,循环神经网络在处理完一个子序列之后,它的隐藏状态将会复制到下一个序列作为初始值,这样在前向过程中,就相当于一次性读入这整个merge句子,但是反向传播的时候,梯度只会在每一个子序列内部进行传播。
具体实现代码,这人主要干了生成batch和padding的功能。该文件名get_batch.py
# _*_ encoding=utf8 _*_
import numpy as np
import codecs
import config
train_batch_size = config.train_batch_size
train_step_num = config.train_step_num
def read_data(file_path):
with codecs.open(file_path, 'r', encoding='utf-8') as fr:
id_string = ' '.join([line.strip() for line in fr.readlines()])
id_list = [int(w) for w in id_string.split()]
return id_list
def make_batches(id_list, batch_size, num_step):
# 计算总的batch_size数,每一个batch包含的单词数量是batch_size*num_step
num_batch = (len(id_list) - 1) // (batch_size * num_step)
# 把数据整理成[batch_size, num_batches*num_step]
data = np.array(id_list[:num_batch * batch_size * num_step])
data = np.reshape(data, [batch_size, num_batch * num_step])
# 沿第二个维度将数据分成num_batch个batch,存入一个数组
data_batches = np.split(data, num_batch, axis=1)
# 每个位置右移一位,再构造RNN输出,从前面的信息得到下一个单词的信息
label = np.array(id_list[1:num_batch * batch_size * num_step + 1])
label = np.reshape(label, [batch_size, num_batch * num_step])
label_batch = np.split(label, num_batch, axis=1)
# print(data_batches[0])
# 返回RNN的输入和输出
return list(zip(data_batches, label_batch))
# make_batches(read_data(config.train2id_data),train_batch_size,train_step_num)
对于上面这个代码,有几点需要注意的地方,一个是组装batch的地方,一个是生成输入数据和生成输出数据。
模型的配置
在训练之前,定义好训练的参数信息
# _*_ encoding=utf8 _*_
import numpy as np
import tensorflow as tf
import os
from get_batch import read_data,make_batches
import config
tf.app.flags.DEFINE_string('TRAIN_DATA', config.train2id_data ,"训练数据")
tf.app.flags.DEFINE_string('EVAL_DATA', config.test2id_data ,"测试数据")
tf.app.flags.DEFINE_string('TEST_DATA', config.valid2id_data ,"验证数据")
tf.app.flags.DEFINE_integer('HIDDEN_SIZE', 300 ,"影藏层数量")
tf.app.flags.DEFINE_integer('NUM_LAYERS', 2 ,"LSTM层数")
tf.app.flags.DEFINE_integer('VOCAB_SZIE', 10000 ,"词典大小")
tf.app.flags.DEFINE_integer('TRAIN_BATCH_SIZE', 20, 'batch_size的大小')
tf.app.flags.DEFINE_integer('TRAIN_NUM_STEP', 35, '训练数据的截断长度')
tf.app.flags.DEFINE_integer('EVAL_BATCH_SIZE', 1, '测试数据的batch大小')
tf.app.flags.DEFINE_integer('EVAL_NUM_STEP', 1, '测试数据的截断长度')
tf.app.flags.DEFINE_integer('NUM_EPOCH', 5, '训练的轮数')
tf.app.flags.DEFINE_float('LSTM_KEEP_PROB', 0.9, 'LSTM不被dropout的概率')
tf.app.flags.DEFINE_float('EMBEDDING_KEEP_PROB', 0.9, '词向量不被dropout的概率')
tf.app.flags.DEFINE_integer('MAX_GRAD_NORM', 5 ,"用户控制梯度膨胀的梯度大小的上限")
tf.app.flags.DEFINE_boolean('SHARE_EMB_AND_SOFTMAX', True ,"在softmax和词向量层共享参数")
FLAGS = tf.app.flags.FLAGS
模型:model.py
# _*_ encoding=utf8 _*_
import numpy as np
import tensorflow as tf
import os
from get_batch import read_data,make_batches
from model_config import FLAGS
class PTBModel(object):
def __init__(self, is_training, batch_size, num_steps):
self.batch_size = batch_size # 每一个batch有多少个query
self.num_steps = num_steps # 截断长度
# 这人是很好理解的,就是通过一个句子预测该句子是不是应该出现,加上一个batch,就构成了输入和输出。
self.input_data = tf.placeholder(tf.int32, [batch_size,num_steps]) # 定义输入的格式
self.targets = tf.placeholder(tf.int32,[batch_size,num_steps]) # 定义输出的格式
# 这儿是不同的lstm层之间不被dropout的概率,如果是训练过程,就丢掉部分信息,如果不是,则需要全部的新进行传播
dropout_keep_prob = FLAGS.LSTM_KEEP_PROB if is_training else 1.0
# 定义单个基本的LSTM单元。同时指明隐藏层的数量hidden_size,声明lstm的时候只需要什么隐藏层的数量
rnn_cell = tf.nn.rnn_cell.BasicLSTMCell(FLAGS.HIDDEN_SIZE)
# 当state_is_tuple=True的时候,state是元组形式,state=(c,h)。如果是False,那么state是一个由c和h拼接起来的张量,
# state=tf.concat(1,[c,h])。在运行时,返回2值,一个是h,还有一个state
# rnn_cell = tf.nn.rnn_cell.BasicLSTMCell(FLAGS.HIDDEN_SIZE,state_is_tuple=True)
# dropout,就是指网络中每个单元在每次有数据流入时以一定的概率(keep prob)正常工作,否则输出0值。这是一张正则化思想,可以有效防止过拟合,可以理解同一个t时刻,多层cell之间传递信息的时候进行dropout
rnn_cell = tf.nn.rnn_cell.DropoutWrapper(rnn_cell, output_keep_prob = dropout_keep_prob)
# 这儿定义的是两层的lstm,每层都是300个影藏单元,将两层的lstm叠起来了
lstm_cells = [
rnn_cell for _ in range(FLAGS.NUM_LAYERS)
]
# 构建多层的循环神经网络
cell = tf.nn.rnn_cell.MultiRNNCell(lstm_cells)
# 将LSTM的状态初始化全为0 的数组,zero_state这个函数生成全零的初始状态,
# initial_state包含了两个张量的LSTMStateTuple类,其中.c和.h分别是c状态和h状态
# 每次使用一个batch大小的训练样本
# 初始化的c 和 h的状态
# 定义好的cell会依次接收num_steps个输入然后产生最后的state
self.initial_state = cell.zero_state(batch_size, tf.float32)
# 定义单词的词向量矩阵 300维 [vocab_size, embedding_size]
# word embedding步骤,降维,增加word之间的联系
embedding = tf.get_variable("embedding", [FLAGS.VOCAB_SZIE, FLAGS.HIDDEN_SIZE])
# 将单词转化为词向量
inputs = tf.nn.embedding_lookup(embedding, self.input_data)
if is_training:
inputs = tf.nn.dropout(inputs, FLAGS.EMBEDDING_KEEP_PROB)
outputs = []
# 初始的c h状态
state = self.initial_state
# LSTM循环
# 安装文本的顺序向cell输入
# inputs数据结构
with tf.variable_scope("RNN"):
for time_step in range(num_steps):
# 第二次循环开始,会复用tf.get_variable_scope().reuse_variables()设置的变量
if time_step > 0: tf.get_variable_scope().reuse_variables()
# 每次循环,都输入input和state,返回output和更新后的state
cell_output, state = cell(inputs[:, time_step, :], state) #
# output: shape[num_steps][batch,hidden_size]
outputs.append(cell_output)
# axis = 0 代表在第0个维度拼接
# axis = 1 代表在第1个维度拼接
# 把之前outputs展开,成[batch, hidden_size*num_steps],
# 然后 reshape, 成[batch*numsteps, hidden_size]
output = tf.reshape(tf.concat(outputs, 1), [-1, FLAGS.HIDDEN_SIZE])
# 获取embedding的权重 [HIDDEN_SIZE, VOCAB_SZIE]
if FLAGS.SHARE_EMB_AND_SOFTMAX:
weight = tf.transpose(embedding)
else:
weight = tf.get_variable("weight", [FLAGS.HIDDEN_SIZE, FLAGS.VOCAB_SZIE])
bias = tf.get_variable("bias", [FLAGS.VOCAB_SZIE])
# [batch*numsteps,VOCAB_SZIE]
logits = tf.matmul(output, weight) + bias
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=tf.reshape(self.targets, [-1]),
logits=logits
)
self.cost = tf.reduce_sum(loss) / batch_size
self.final_state = state
if not is_training: return
# 获取全部可训练的参数
trainable_variables = tf.trainable_variables()
grads,_ = tf.clip_by_global_norm(
tf.gradients(self.cost, trainable_variables),FLAGS.MAX_GRAD_NORM)
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
self.train_op = optimizer.apply_gradients(zip(grads, trainable_variables))
训练:train.py
# _*_ encoding=utf8 _*_
import numpy as np
import tensorflow as tf
import os
from get_batch import read_data,make_batches
from model import PTBModel
from model_config import FLAGS
# os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
def run_epoch(session, model, batches, train_op, output_log, step):
total_costs = 0.0
iters = 0
state = session.run(model.initial_state)
for x,y in batches:
cost,state,_ = session.run(
[model.cost,model.final_state,train_op],
feed_dict = {model.input_data:x, model.targets:y,
model.initial_state:state}
)
total_costs += cost
iters += model.num_steps
if output_log and step % 100 == 0:
print("After %d steps,perplexity is %.3f" %(step, np.exp(total_costs/iters)))
step += 1
return step,np.exp(total_costs / iters)
def main():
initialzer = tf.random_uniform_initializer(-0.05,0.05)
with tf.variable_scope("language_model", reuse=None,initializer=initialzer):
train_model = PTBModel(True, FLAGS.TRAIN_BATCH_SIZE, FLAGS.TRAIN_NUM_STEP)
with tf.variable_scope("language_model", reuse=True,initializer=initialzer):
eval_model = PTBModel(False, FLAGS.EVAL_BATCH_SIZE, FLAGS.EVAL_NUM_STEP)
with tf.Session() as sess:
tf.global_variables_initializer().run()
train_batches = make_batches(
read_data(FLAGS.TRAIN_DATA), FLAGS.TRAIN_BATCH_SIZE, FLAGS.TRAIN_NUM_STEP
)
eval_batches = make_batches(
read_data(FLAGS.EVAL_DATA), FLAGS.EVAL_BATCH_SIZE, FLAGS.EVAL_NUM_STEP
)
test_batches = make_batches(
read_data(FLAGS.TEST_DATA), FLAGS.EVAL_BATCH_SIZE, FLAGS.EVAL_NUM_STEP
)
step = 0
for i in range(FLAGS.NUM_EPOCH):
print("in iteration :%d " % (i+1))
step,train_pplx = run_epoch(sess, train_model, train_batches,
train_model.train_op,True, step)
print("Epoch: %d Train perplexity:%.3f" % (i+1,train_pplx))
step, eval_pplx = run_epoch(sess, eval_model, eval_batches,
tf.no_op(), False, 0)
print("Epoch: %d Eval perplexity:%.3f" % (i + 1, eval_pplx))
step, test_pplx = run_epoch(sess, eval_model, test_batches,
tf.no_op(), False, 0)
print("Test perplexity:%.3f" % test_pplx)
if __name__ == '__main__':
main()