博客文章基于Google Tensorflow实战深度学习框架第九章部分内容,手写一遍代码加强模型理解。
代码在pycharm中编写运行
# -*- coding:utf-8 -*-
'''
@Author:zhangy
@Modify:2019.7.5
'''
import codecs
import collections
from operator import itemgetter
#设置中英文类型,得到不同的vocab
DATA_TYPE = 'english'
if DATA_TYPE == 'chinese':
RAW_DATA = 'train.txt.zh'
VOCAB_OUTPUT = 'zh.vocab'
VOCAB_SIZE = 4000
elif DATA_TYPE == 'english':
RAW_DATA = 'train.txt.en'
VOCAB_OUTPUT = 'en.vocab'
VOCAB_SIZE = 10000
#对单词进行计数
counter = collections.Counter()
with codecs.open(RAW_DATA,'r','utf-8') as f:
for line in f:
for word in line.strip().split():
counter[word] += 1
#依据词频进行降序排列
sorted_word_to_cnt = sorted(counter.items(),key=itemgetter(1),reverse=True)
#拿到对应的单词列表
sorted_word_list = [x[0] for x in sorted_word_to_cnt]
#加入句子的起止符号和unknown符号
sorted_word_list = ["","",""] + sorted_word_list
if len(sorted_word_list) > VOCAB_SIZE:
sorted_word_list = sorted_word_list[:VOCAB_SIZE]
with codecs.open(VOCAB_OUTPUT,'w','utf-8') as file_output:
for word in sorted_word_list:
file_output.write(word + '\n')
# -*- coding:utf-8 -*-
'''
@Author:zhangy
@Modify:2019.7.5
'''
import codecs
DATA_TYPE = "english"
if DATA_TYPE == 'chinese':
RAW_DATA = 'train.txt.zh'
VOCAB = 'zh.vocab'
OUTPUT_DATA = 'train.zh'
elif DATA_TYPE == 'english':
RAW_DATA = 'train.txt.en'
VOCAB = 'en.vocab'
OUTPUT_DATA = 'train.en'
with codecs.open(VOCAB,'r','utf-8') as f_vocab:
#把所有单词转换为列表形式
vocab = [w.strip() for w in f_vocab.readlines()]
word_to_id = {k:v for (k,v) in zip(vocab,range(len(vocab)))}
def get_id(word):
return word_to_id[word] if word in word_to_id else word_to_id['']
fin = codecs.open(RAW_DATA,'r','utf-8')
fout = codecs.open(OUTPUT_DATA,'w','utf-8')
for line in fin:
words = line.strip().split() + ['']
out_line = " ".join([str(get_id(w)) for w in words]) + '\n'
fout.write(out_line)
fin.close()
fout.close()
# -*- coding:utf-8 -*-
'''
@Author:zhangy
@Modify:2019.7.5
'''
'''
循环神经网络在读取数据时会将填充位置的内容与其他内容一样纳入计算,因此为了不让填充影响训练,有
两方面需要注意:
1、在读取填充时如果按照循环神经网络处理,例如读取填充后的“B1 B2 0 0”和原始“B1 B2”之后产生的隐藏状态不同,
因此Tensorflow提供了tf.nn.dynamic_ rnn对每一个batch的数据读取两个输入:输入数据的内容(维度为[batch_size,time] 和
输入数据的长度(维度为[time ])。对于输入batch 里的每一条数据,在读取了相应长度的内容后, dynamic_rnn就跳过后
面的输入,直接把前一步的计算结果复制到后面的时刻。这样可以保证padding是否存在不影响模型效果。
另外值得注意的是,使用dyanmic_rnn 时每个batch 的最大序列长度不需要相同。例如在上面的例子中,第一个batch 的维
度是2 ×4 ,而第二个batch 的维度是2×7。在训练中dynamic_mn 会根据每个batch 的最大长度动态展开到需要的层数,这就是
它被称为“ dynamic ”的原因。
2、在设计损失函数时需要特别将填充位置的损失的权重设置为0 ,这样在填充位置产生的预测不会影响梯度的计算。
下面的代码使用tf.data.Dataset. padded_ batch 来进行填充和batching , 并记录每个句子的序列长度以用作
dynamic_rnn 的输入,这里是从磁盘动态读取数据。
'''
import numpy as np
import tensorflow as tf
MAX_LEN = 50 #限定句子的最大单词数量
SOS_ID = 1 #目标语言词汇表中的ID
#使用Dataset从一个文件中读取一个语言的数据,格式为每行一句话,单词已转换为单词编号
def MakeDataset(file_path):
dataset = tf.data.TextLineDataset(file_path)
#根据空格将单词编号切分开并放入一个一维向量
dataset = dataset.map(lambda string:tf.string_split([string]).values)
#将字符串形式的单词编号转换为整数
dataset = dataset.map(lambda string:tf.string_to_number(string,tf.int32))
#统计每个句子的单词数量,并与句子内容一起放入Dataset
dataset = dataset.map(lambda x:(x,tf.size(x)))
return dataset
#从源文件src_path和目标文件trg_path中分别读取数据,进行填充和batch操作
def MakeSrcTrgDataset(src_path,trg_path,batch_size):
#首先分别读取源语言和目标语言数据
src_data = MakeDataset(src_path)
trg_data = MakeDataset(trg_path)
#通过zip操作将两个dataset合并为一个dataset,现在每个dataset中每一项数据ds由4个张量组成
# ds[0][0]是源句子
# ds[0][1]是源句子长度
# ds[1][0]是目标句子
# ds[1][1]是同标句子长度
dataset = tf.data.Dataset.zip((src_data,trg_data))
#删除内容为空(只包含)的句子和长度过长的句子
def FilterLength(src_tuple,trg_tuple):
((src_input,src_len),(trg_input,trg_len)) = (src_tuple,trg_tuple)
src_len_ok = tf.logical_and(tf.greater(src_len,1),tf.less_equal(src_len,MAX_LEN))
trg_len_ok = tf.logical_and(tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
return tf.logical_and(src_len_ok,trg_len_ok)
dataset = dataset.filter(FilterLength)
'''
解码器需要两种格式的目标句子:
1、解码器的输入(trg_input),形如 X Y Z
2、解码器的目标输出(trg_label),形如 X Y Z
从上述操作中的到的句子是'X Y Z '的形式,我们需要从中生成' X Y Z'形式并
加入到dataset中
'''
def MakeTrgInput(src_tuple,trg_tuple):
((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
return ((src_input, src_len), (trg_input, trg_label, trg_len))
dataset = dataset.map(MakeTrgInput)
#随机打乱训练数据
dataset = dataset.shuffle(10000)
#规定填充后的数据维度
padded_shapes = (
(tf.TensorShape([None]), #源句子长度未知
tf.TensorShape([])), #源句子长度是单个数字
(tf.TensorShape([None]), #目标句子(解码输入)是长度未知的向量
tf.TensorShape([None]), #目标句子(解码输出)是长度未知的向量
tf.TensorShape([]))) #目标句子长度是单个数字
#调用padded_batch方法进行batching操作
batched_dataset = dataset.padded_batch(batch_size,padded_shapes)
return batched_dataset
# -*- coding:utf-8 -*-
'''
@Author:zhangy
@Modify:2019.7.5
'''
'''
seq2seq模型训练部分
使用一个双层LSTM作为循环神经网络的主体,且在softmax层和词向量层之间共享参数。相比较语言模型。
seq2seq代码有以下几处变化:
1、增加了一个循环神经网络作为编码器
2、使用Dataset动态读取数据,而不是直接将所有数据读入内存
3、每个batch完全独立,不需要再batch之间传递状态
4、每训练200步便将模型参数保存到一个checkpoint中
'''
import tensorflow as tf
from batching import MakeSrcTrgDataset
SRC_TRAIN_DATA = "train.en" #源语言输入文件
TRG_TRAIN_DATA = "train.zh" #目标语言输入文件
CHECKPOINT_PATH = "model_save_path/seq2seq_ckpt" #checkpoint保存路径
HIDDEN_SIZE = 1024 #LSTM隐藏层规模
NUM_LAYERS = 2 #神经网络中的LSTM结构的层数
SRC_VOCAB_SIZE = 10000 #源语言词汇表大小
TRG_VOCAB_SIZE = 4000 #目标语言词汇表大小
BATCH_SIZE = 100 #训练数据batch大小
NUM_EPOCH = 5 #迭代轮数
KEEP_PROB = 0.8 #节点不被dropout的概率(词向量)
MAX_GRAD_NORM = 5 #用于控制梯度膨胀的梯度大小上限
SHARE_EMB_AND_SOFTMAX = True #在softmax层和词向量层之间共享参数
class NMTMOdel(object):
#在模型的初始化函数中定义模型要使用的变量
def __init__(self):
# 定义编码器和解码器所使用的的LSTM结构
self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
for _ in range(NUM_LAYERS)])
self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
for _ in range(NUM_LAYERS)])
#为源语言和目标语言分别定义词向量
self.src_embedding = tf.get_variable("src_emb",[SRC_VOCAB_SIZE,HIDDEN_SIZE])
self.trg_embedding = tf.get_variable("trg_emb",[TRG_VOCAB_SIZE,HIDDEN_SIZE])
#定义softmax层的变量
if SHARE_EMB_AND_SOFTMAX:
#tf.transpose()用于交换张量的不同维度,如果输入张量是二维,则代表转置
self.softmax_weight = tf.transpose(self.trg_embedding)
else:
self.softmax_weight = tf.get_variable("softmax_weight",[HIDDEN_SIZE,TRG_VOCAB_SIZE])
self.softmax_bias = tf.get_variable("softmax_bias",[TRG_VOCAB_SIZE])
#在forward函数中定义模型的前向计算图
#src_input,src_size,trg_input,trg_label,trg_size是batching.py中MakeSrcTrgDataset函数产生的五种张量
def forward(self,src_input,src_size,trg_input,trg_label,trg_size):
batch_size = tf.shape(src_input)[0]
#将输入和输出单词编号转为词向量
src_emb = tf.nn.embedding_lookup(self.src_embedding,src_input)
trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)
#在词向量上进行dropout
src_emb = tf.nn.dropout(src_emb,KEEP_PROB)
trg_emb = tf.nn.dropout(trg_emb,KEEP_PROB)
'''
使用dynamic_rnn构造编码器,编码器读取目标句子每个位置的词向量,输出最后一步隐藏层的状态
enc_state,因为编码器就是一个双层的LSTM,因此enc_stste是一个包含两个LSTMStateTuple类的tuple,每
个LSTMStateTuple对应编码器中一层的状态。
enc_outputs是顶层LSTM在每一步的输出,维度为[batch_size,max_time,HIDDEN_SIZE],seq2seq模型不会
用到enc_outputs,attention模型会用到。
'''
with tf.variable_scope("encoder"):
enc_outputs,enc_states = tf.nn.dynamic_rnn(
self.enc_cell,src_emb,src_size,dtype=tf.float32)
'''
使用dynamic_rnn构造解码器,解码器读取目标句子每个位置的词向量,输出dec_output为每一步顶层
LSTM的输出,dec_outputs的维度是[batch_size,max_time,HIDDEN_SIZE],
initial_state=enc_state表示用编码器的输出来初始化第一步的隐藏状态。
'''
with tf.variable_scope("decoder"):
dec_outputs,_ = tf.nn.dynamic_rnn(
self.dec_cell,trg_emb,trg_size,initial_state=enc_states)
#计算解码器每一步的log perplexity
output = tf.reshape(dec_outputs,[-1,HIDDEN_SIZE])
logits = tf.matmul(output,self.softmax_weight) + self.softmax_bias
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=tf.reshape(trg_label,[-1]),logits=logits)
#在计算平均损失时,需要将填充位置的权重置为0,以避免无效位置的预测干扰模型训练
label_weights = tf.sequence_mask(
trg_size,maxlen=tf.shape(trg_label)[1],dtype=tf.float32)
label_weights = tf.reshape(label_weights,[-1])
cost = tf.reduce_sum(loss * label_weights)
cost_per_token = cost / tf.reduce_sum(label_weights)
#定义反向传播操作
trainable_variables = tf.trainable_variables()
#控制梯度大小,定义优化方法和步骤
grads = tf.gradients(cost / tf.to_float(batch_size),trainable_variables)
grads,_ = tf.clip_by_global_norm(grads,MAX_GRAD_NORM)
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
train_op = optimizer.apply_gradients(
zip(grads,trainable_variables)
)
return cost_per_token,train_op
'''
使用给定的模型model上训练一个epoch,并返回全局步数,每训练200步便保存一个checkpoint
'''
def run_epoch(session,cost_op,train_op,saver,step):
#训练一个epoch,重复训练步骤直到遍历dataset中所有数据
while True:
try:
#运行train_op并计算损失,训练数据在main()中以Dataset方式提供
cost,_ = session.run([cost_op,train_op])
if step % 10 == 0:
print("After %d step,per token cost is %.3f" % (step,cost))
#每200步保存一个checkpoint
if step % 200 == 0:
saver.save(session,CHECKPOINT_PATH,global_step=step)
step += 1
except tf.errors.OutOfRangeError:
break
return step
def main():
#定义初始化函数
initializer = tf.random_uniform_initializer(-0.05,0.05)
#定义训练用的循环神经网络
with tf.variable_scope("nmt_model",reuse=None,initializer=initializer):
train_model = NMTMOdel()
#定义输入数据
data = MakeSrcTrgDataset(SRC_TRAIN_DATA,TRG_TRAIN_DATA,BATCH_SIZE)
iterator = data.make_initializable_iterator()
(src,src_size),(trg_input,trg_label,trg_size) = iterator.get_next()
#定义前向计算图,输入数据以张量形式提供给forward函数
cost_op,train_op = train_model.forward(src,src_size,trg_input,trg_label,trg_size)
#训练模型
saver = tf.train.Saver()
step = 0
with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(NUM_EPOCH):
print("In iteration: %d" % (i+1))
sess.run(iterator.initializer)
step = run_epoch(sess,cost_op,train_op,saver,step)
if __name__ == '__main__':
main()