# -*- coding: utf-8 -*- import time import numpy as np import tensorflow as tf import ptb.reader as reader
flags = tf.app.flags FLAGS = flags.FLAGS
logging = tf.logging
flags.DEFINE_string("save_path", './Out', "Model output directory.") flags.DEFINE_bool("use_fp16", False, "Train using 16-bit floats instead of 32bit floats")
def data_type(): return tf.float16 if FLAGS.use_fp16 else tf.float32
#定义语言模型处理的输入数据的class class PTBInput(object): """The input data.""" #初始化方法 #读取config中的batch_size,num_steps到本地变量。 def __init__(self, config, data, name=None): self.batch_size = batch_size = config.batch_size self.num_steps = num_steps = config.num_steps #num_steps是LSTM的展开步数 #计算每个epoch内需要多好轮训练的迭代 self.epoch_size = ((len(data) // batch_size) - 1) // num_steps #通过ptb_reader获取特征数据input_data和label数据targets self.input_data, self.targets = reader.ptb_producer( data, batch_size, num_steps, name=name)
#定义语言模型的class class PTBModel(object): """The PTB model.""" #训练标记,配置参数,ptb类的实例input_ def __init__(self, is_training, config, input_): self._input = input_
batch_size = input_.batch_size num_steps = input_.num_steps size = config.hidden_size #hidden_size是LSTM的节点数 vocab_size = config.vocab_size #vocab_size是词汇表
# Slightly better results can be obtained with forget gate biases # initialized to 1 but the hyperparameters of the model would need to be # different than reported in the paper. def lstm_cell(): #使用tf.contrib.rnn.BasicLSTMCell设置默认的LSTM单元 return tf.contrib.rnn.BasicLSTMCell( size, forget_bias=0.0, state_is_tuple=True) #state_is_tuple表示接受和返回的state将是2-tuple的形式
attn_cell = lstm_cell #如果训练状态且Dropout的keep_prob小于1,则在前面的lstm_cell之后接一个DropOut层, #这里的做法是调用tf.contrib.rnn.DropoutWrapper函数 if is_training and config.keep_prob < 1: def attn_cell(): return tf.contrib.rnn.DropoutWrapper( lstm_cell(), output_keep_prob=config.keep_prob) #最后使用rnn堆叠函数tf.contrib.rnn.MultiRNNCell将前面构造的lstm_cell多层堆叠得到cell #堆叠次数,为config中的num_layers. cell = tf.contrib.rnn.MultiRNNCell( [attn_cell() for _ in range(config.num_layers)], state_is_tuple=True) #这里同样将state_is_tuple设置为True
#并掉用cell.zero_state设置LSTM单元的初始化状态为0 self._initial_state = cell.zero_state(batch_size, tf.float32) #这里需要注意,LSTM单元可以读入一个单词并结合之前存储的状态state计算下一个单词出现的概率, #并且每次读取一个单词后它的状态state会被更新
#创建网络的词embedding部分,embedding即为将one-hot的编码格式的单词转化为向量的表达形式 #这部分操作在GPU中实现 with tf.device("/cpu:0"): #初始哈embedding矩阵,其行数设置词汇表数vocab_size,列数(每个单词的向量表达的维数)设为hidden_size #hidden_size和LSTM单元中的隐含节点数一致# 在训练过程中,embedding的参数可以被优化和更新。 embedding = tf.get_variable( "embedding", [vocab_size, size], dtype=tf.float32)
#接下来是使用tf.nn.embedding_lookup查询单词对应的向量表达获得inputs inputs = tf.nn.embedding_lookup(embedding, input_.input_data)
#如果为训练状态,则添加一层Dropout if is_training and config.keep_prob < 1: inputs = tf.nn.dropout(inputs, config.keep_prob)
# Simplified version of models/tutorials/rnn/rnn.py's rnn(). # This builds an unrolled LSTM for tutorial purposes only. # In general, use the rnn() or state_saving_rnn() from rnn.py. # # The alternative version of the code below is: # # inputs = tf.unstack(inputs, num=num_steps, axis=1) # outputs, state = tf.nn.rnn(cell, inputs, # initial_state=self._initial_state)
#定义输出outputs outputs = [] state = self._initial_state #首先使用tf.variable_scope将接下来的名称设为RNN with tf.variable_scope("RNN"): #为了控制训练过程,我们会限制梯度在反向传播时可以展开的步数为一个固定的值,而这个步数也是num_steps #这里设置一个循环,长度为num_steps,来控制梯度的传播 for time_step in range(num_steps): #并且从第二次循环开始,我们使用tf.get_variable_scope().reuse_variables()设置复用变量 if time_step > 0: tf.get_variable_scope().reuse_variables() #每次循环内,我们传入inputs和state到堆叠的LSTM单元即(cell)中 #注意,inputs有三个维度,第一个维度是batch中的低级个样本, #第二个维度代表是样本中的第几个单词,第三个维度是单词的向量表达的维度。 #inputs[:, time_step, :]代表所有样板的第time_step个单词 (cell_output, state) = cell(inputs[:, time_step, :], state) #这里我们得到输出cell_output和更新后的state
outputs.append(cell_output) #最后我们将结果cell_output添加到输出列表ouputs中
#将output的内容用tf.contact串联到一起,并使用tf.reshape将其转为一个很长的一维向量 output = tf.reshape(tf.concat(outputs, 1), [-1, size]) #接下来是softmax层,先定义权重softmax_w和偏置softmax_b softmax_w = tf.get_variable( "softmax_w", [size, vocab_size], dtype=tf.float32) softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=tf.float32) #然后使用tf.matmul将输出output乘上权重并加上偏置得到logits logits = tf.matmul(output, softmax_w) + softmax_b #这里直接使用tf.contrib.legacy_seq2seq.sequence_loss_by_example计算输出logits #和targets的偏差
loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example( [logits], [tf.reshape(input_.targets, [-1])], [tf.ones([batch_size * num_steps], dtype=tf.float32)]) # 这里的sequence_loss即target words的averge negative log probability,
self._cost = cost = tf.reduce_sum(loss) / batch_size #然后再使用tf.reduce_sum汇总batch的误差。 self._final_state = state
if not is_training: return #如果此时不是训练状态,直接返回。
#定义学习速率的变量lr,并将其设为不可训练 self._lr = tf.Variable(0.0, trainable=False)
#再使用tf.trainable_variables获取所有可训练的参数tvars tvars = tf.trainable_variables() #针对前面得到的cost,计算tvars的梯度,并用tf.clip_by_global_norm设置梯度的最大范数max_grad_norm grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), config.max_grad_norm) #这即是Gradient Clipping的方法,控制梯度的最大范数,某种程度上起到正则化的效果。 #Gradient Clipping可防止Gradient Explosion梯度爆炸的问题,如果对梯度不加限制, #则可能会因为迭代中梯度过大导致训练难以收敛
#然后定义GradientDescent优化器 optimizer = tf.train.GradientDescentOptimizer(self._lr)
#再创建训练操作_train_op,用optimizer.apply_gradients将前面clip过的梯度应用到所有可训练的参数tvars上, #然后使用tf.contrib.framework.get_or_create_global_step()生成全局统一的训练步数 self._train_op = optimizer.apply_gradients( zip(grads, tvars), global_step=tf.contrib.framework.get_or_create_global_step())
#设置一个_new_lr的placeholder用以控制学习速率。 self._new_lr = tf.placeholder( tf.float32, shape=[], name="new_learning_rate") #同时定义个assign_lr的函数,用以在外部控制模型的学习速率 self._lr_update = tf.assign(self._lr, self._new_lr)
# 同时定义个assign_lr的函数,用以在外部控制模型的学习速率 #方式是将学习速率值传入_new_lr这个place_holder,并执行_update_lr操作完成对学习速率的修改 def assign_lr(self, session, lr_value): session.run(self._lr_update, feed_dict={self._new_lr: lr_value})
#模型定义完毕,再定义定义这个PTBModel class的一些property #python中的@property装饰器可以将返回变量设为只读,防止修改变量引发的问题
#这里定义input,initial_state,cost,lr,final_state,train_op为property,方便外部访问 @property def input(self): return self._input
@property def initial_state(self): return self._initial_state
@property def cost(self): return self._cost
@property def final_state(self): return self._final_state
@property def lr(self): return self._lr
@property def train_op(self): return self._train_op
#接下来定义几种不同大小的模型的参数 #首先是小模型的设置 class SmallConfig(object): """Small config.""" init_scale = 0.1 #网络中权重值的初始Scale learning_rate = 1.0 #学习速率的初始值 max_grad_norm = 5 #前面提到的梯度的最大范数 num_layers = 2 # num_layers是LSTM可以堆叠的层数 num_steps = 20 #是LSTM梯度反向传播的展开步数 hidden_size = 200 #LSTM的隐含节点数 max_epoch = 4 #是初始学习速率的可训练的epoch数,在此之后需要调整学习速率 max_max_epoch = 13 #总共可以训练的epoch数 keep_prob = 1.0 #keep_prob是dorpout层的保留节点的比例 lr_decay = 0.5 #学习速率的衰减速率 batch_size = 20 #每个batch中样板的数量 vocab_size = 10000
#具体每个参数的值,在不同的配置中对比才有意义
#在中等模型中,我们减小了init_state,即希望权重初值不要过大,小一些有利于温和的训练 #学习速率和最大梯度范数不变,LSTM层数不变。 #这里将梯度反向传播的展开步数从20增大到35. #hidden_size和max_max_epoch也相应地增大约3倍;同时,这里开始设置dropout的keep_prob到0.5 #而之前设置1,即没有dropout; #因为学习迭代次数的增大,因此将学习速率的衰减速率lr_decay也减小了。 #batch_size和词汇表vocab_size的大小保持不变 class MediumConfig(object): """Medium config.""" init_scale = 0.05 learning_rate = 1.0 max_grad_norm = 5 num_layers = 2 num_steps = 35 hidden_size = 650 max_epoch = 6 max_max_epoch = 39 keep_prob = 0.5 lr_decay = 0.8 batch_size = 20 vocab_size = 10000
#大型模型,进一步缩小了init_scale并大大放宽了最大梯度范数max_grad_norm到10 #同时将hidden_size提升到了1500,并且max_epoch,max_max_epoch也相应增大了; #而keep_drop也因为模型复杂度的上升继续下降,学习速率的衰减速率lr_decay也进一步减小 class LargeConfig(object): """Large config.""" init_scale = 0.04 learning_rate = 1.0 max_grad_norm = 10 num_layers = 2 num_steps = 35 hidden_size = 1500 max_epoch = 14 max_max_epoch = 55 keep_prob = 0.35 lr_decay = 1 / 1.15 batch_size = 20 vocab_size = 10000
#TstConfig只是测试用,参数都尽量使用最小值,只是为了测试可以完整允许模型 class TstConfig(object): """Tiny config, for testing.""" init_scale = 0.1 learning_rate = 1.0 max_grad_norm = 1 num_layers = 1 num_steps = 2 hidden_size = 2 max_epoch = 1 max_max_epoch = 1 keep_prob = 1.0 lr_decay = 0.5 batch_size = 20 vocab_size = 10000
#定义训练一个epoch数据的函数run_epoch。 def run_epoch(session, model, eval_op=None, verbose=False): """Runs the model on the given data.""" # 记录当前时间,初始化损失costs和迭代数iters。 start_time = time.time() costs = 0.0 iters = 0 state = session.run(model.initial_state) #并执行model.initial_state来初始化状态并获得初始状态
#接着创建输出结果的字典表fetches #其中包括cost和final_state fetches = { "cost": model.cost, "final_state": model.final_state, } #如果有评测操作eval_op,也一并加入fetches if eval_op is not None: fetches["eval_op"] = eval_op
# 接着进行循环训练中,次数为epoch_size for step in range(model.input.epoch_size): feed_dict = {} #在每次循环中,我们生成训练用的feed_dict
for i, (c, h) in enumerate(model.initial_state): feed_dict[c] = state[i].c feed_dict[h] = state[i].h
# 将全部的LSTM单元的state加入feed_dict,然后传入feed_dict并执行 # fetchees对网络进行一次训练,并且拿到cost和state vals = session.run(fetches, feed_dict) cost = vals["cost"] state = vals["final_state"]
#我们累加cost到costs,并且累加num_steps到iters。 costs += cost iters += model.input.num_steps
#我们每完成约10%的epoch,就进行一次结果的展示,依次展示当前epoch的进度, # perplexity(即平均cost的自然常数指数,语言模型性能的重要指标,越低代表模型输出的概率分布在预测样本上越好) #和训练速度(单词/s)
if verbose and step % (model.input.epoch_size // 10) == 10: print("%.3f perplexity: %.3f speed: %.0f wps" % (step * 1.0 / model.input.epoch_size, np.exp(costs / iters), iters * model.input.batch_size / (time.time() - start_time))) # 最后返回perplexity作为函数的结果 return np.exp(costs / iters)
#使用reader.ptb_raw_data直接读取解压后的数据,得到训练数据,验证数据和测试数据 raw_data = reader.ptb_raw_data('./simple-examples/data/') train_data, valid_data, test_data, _ = raw_data
#这里定义训练模型的配置为小型配置 config = SmallConfig() eval_config = SmallConfig() eval_config.batch_size = 1 eval_config.num_steps = 1 #需要注意的是测试配置eval_config需和训练配置一致 #这里将测试配置的batch_size和num_steps修改为1
#创建默认的Graph,并使用tf.random_uniform_initializer设置参数的初始化器 with tf.Graph().as_default(): initializer = tf.random_uniform_initializer(-config.init_scale, config.init_scale)
with tf.name_scope("Train"): #使用PTBInput和PTBModel创建一个用来训练的模型m train_input = PTBInput(config=config, data=train_data, name="TrainInput") with tf.variable_scope("Model", reuse=None, initializer=initializer): m = PTBModel(is_training=True, config=config, input_=train_input) #tf.scalar_summary("Training Loss", m.cost) #tf.scalar_summary("Learning Rate", m.lr)
with tf.name_scope("Valid"): # 使用PTBInput和PTBModel创建一个用来验证的模型mvalid valid_input = PTBInput(config=config, data=valid_data, name="ValidInput") with tf.variable_scope("Model", reuse=True, initializer=initializer): mvalid = PTBModel(is_training=False, config=config, input_=valid_input) #tf.scalar_summary("Validation Loss", mvalid.cost)
with tf.name_scope("Tst"): # 使用PTBInput和PTBModel创建一个用来验证的模型Tst test_input = PTBInput(config=eval_config, data=test_data, name="TstInput") with tf.variable_scope("Model", reuse=True, initializer=initializer): mtst = PTBModel(is_training=False, config=eval_config, input_=test_input)
#其中训练和验证模型直接使用前面的cofig,测试模型则使用前面的测试配置eval_config
sv = tf.train.Supervisor() #使用tf.train.Supervisor创建训练的管理器sv
#并使用sv.managed_session()创建默认的session with sv.managed_session() as session: #再执行训练多个epoch数据的循环 for i in range(config.max_max_epoch): #在每个epoch循环内,我们先计算累计的学习速率衰减值, #这里只需要计算超过max_epoch的轮数,再求lr_decay的超出轮数次幂即可 #然后将初始学习速率乘以累计的衰减,并更新学习速率。 lr_decay = config.lr_decay ** max(i + 1 - config.max_epoch, 0.0) m.assign_lr(session, config.learning_rate * lr_decay)
#在循环内执行一个epoch的训练和验证,并输出当前的学习速率,训练和验证即的perplexity print("Epoch: %d Learning rate: %.3f" % (i + 1, session.run(m.lr))) train_perplexity = run_epoch(session, m, eval_op=m.train_op, verbose=True) print("Epoch: %d Train Perplexity: %.3f" % (i + 1, train_perplexity)) valid_perplexity = run_epoch(session, mvalid) print("Epoch: %d Valid Perplexity: %.3f" % (i + 1, valid_perplexity))
#在完成全部训练之后,计算并输出模型在测试集上的perplexity tst_perplexity = run_epoch(session, mtst) print("Test Perplexity: %.3f" % tst_perplexity) # # if FLAGS.save_path: # print("Saving model to %s." % FLAGS.save_path) # sv.saver.save(session, FLAGS.save_path, global_step=sv.global_step)
if __name__ == "__main__": tf.app.run()
|