本人最近正在学习深度学习以及tensorflow,在此记录一些学习过程中看到的有价值的参考资料,并且写下一点我自己的初步理解。
环境
win10 64+anaconda3(python3.5)+tensorflow0.12.1
关于windows下CUDA等配置,请参考下文:
windows 10 64bit下安装Tensorflow+Keras+VS2015+CUDA8.0 GPU加速
部分参考/推荐资料
tensorflow官方引导文档
Udacity深度学习(即谷歌深度学习公开课)
网友multiangle深度学习笔记
运用TensorFlow处理简单的NLP问题
本人所用anaconda3,ptb位于Anaconda3\Lib\site-packages\tensorflow\models\rnn\ptb目录下,共包含
两个主要文件。其中reader是PTB模型处理数据的工具包。PTBModel、main都位于ptb_word_lm中。
和之前的Tutorial一样,PTB也是分为构建抽象模型和训练两大步骤。
官方文档位于:https://www.tensorflow.org/tutorials/recurrent
配置说明
这份官方代码非常有心的设置了4种不同大小的配置,分别为small,medium、large和test,以small为例:
class SmallConfig(object):
"""Small config."""
init_scale = 0.1 # 相关参数的初始值为随机均匀分布,范围是[-init_scale,+init_scale]
learning_rate = 1.0 # 学习速率,此值还会在模型学习过程中下降
max_grad_norm = 5 # 用于控制梯度膨胀,如果梯度向量的L2模超过max_grad_norm,则等比例缩小
num_layers = 2 # LSTM层数
num_steps = 20 # 分隔句子的粒度大小,每次会把num_steps个单词划分为一句话(但是本模型与seq2seq模型不同,它仅仅是1对1模式,句子长度应该没有什么用处)。
hidden_size = 200 # 隐层单元数目,每个词会表示成[hidden_size]大小的向量
max_epoch = 4 # epochmax_epoch时,lr_decay逐渐减小
max_max_epoch = 13 # 完整的文本要循环的次数
keep_prob = 1.0 # dropout率,1.0为不丢弃
lr_decay = 0.5 # 学习速率衰减指数
batch_size = 20 # 和num_steps共同作用,要把原始训练数据划分为batch_size组,每组划分为n个长度为num_steps的句子。
vocab_size = 10000 # 单词数量(这份训练数据中单词刚好10000种)
另有以下配置,可以设置要选用的config(下面为small)、数据地址、输出存储地址等。
flags = tf.flags
logging = tf.logging
flags.DEFINE_string(
"model", "small",
"A type of model. Possible options are: small, medium, large.")
flags.DEFINE_string("data_path", r'C:\Users\hasee\Desktop\tempdata\lstm\simple-examples\data', "data_path")
flags.DEFINE_string("save_path", r'C:\Users\hasee\Desktop\tempdata\lstm\simple-examples\data\res',
"Model output directory.")
flags.DEFINE_bool("use_fp16", False,
"Train using 16-bit floats instead of 32bit floats")
FLAGS = flags.FLAGS
PTBModel
在class PTBModel的init()中构建了一个抽象LSTM模型。
lstm_cell和initial_state
# 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.
# 注释指的是如果将forget_bias=0.0改为1.0会得到更好的结果,但是这将与论文中的描述不符。
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(size, forget_bias=0.0, state_is_tuple=True)
if is_training and config.keep_prob < 1:
lstm_cell = tf.nn.rnn_cell.DropoutWrapper(
lstm_cell, output_keep_prob=config.keep_prob)
cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True)
self._initial_state = cell.zero_state(batch_size, data_type())
使用BasicLSTMCell构建一个基础LSTM单元,然后根据keep_prob来为cell配置dropout。最后通过MultiRNNCell将num_layers个lstm_cell连接起来。
在LSTM单元中,有2个状态值,分别是c和h。
更多基础知识请见tensorflow笔记:多层LSTM代码分析
答:根据解读tensorflow之rnn ,官方文档给出如下描述:
BasicLSTMCell没有实现clipping,projection layer,peep-hole等一些lstm的高级变种,仅作为一个基本的basicline结构存在,如果要使用这些高级variant要用LSTMCell这个类。
由于我们现在只是想搭建一个基本的lstm-language model模型,现阶段BasicLSTMCell够用。这就是为什么这里用的是BasicLSTMCell这个类而不是别的什么。
embedding
with tf.device("/cpu:0"):
embedding = tf.get_variable(
"embedding", [vocab_size, size], dtype=data_type())
# input_.input_data为外部输入的id形式的数据,通过embedding_lookup()将ids转换为词向量形式inputs。
inputs = tf.nn.embedding_lookup(embedding, input_.input_data)
在这里embedding表示词向量矩阵。此矩阵共有vocab_size行(在这里为10000),每一行都是一个hidden_size维向量,随着模型的训练,embedding内部权值会不断更新,最终可以得到各个词的向量表示。
outputs与loss
这里与基础模型的套路大致一致,但是需要注意一下次数为num_steps的循环,他做的就是rnn的展开,每一次会产生一个output和一个状态
outputs = []
state = self._initial_state
with tf.variable_scope("RNN"):
for time_step in range(num_steps):
if time_step > 0: tf.get_variable_scope().reuse_variables()
# 这个cell(inputs[:, time_step, :], state)会调用tf.nn.rnn_cell.MultiRNNCell中的__CALL__()方法
# TODO __CALL__()的注释说:Run this multi-layer cell on inputs, starting from state.但是还没看该方法实际做了什么
(cell_output, state) = cell(inputs[:, time_step, :], state)
outputs.append(cell_output)
# 下面套路和基础模型一致,y=wx+b
# x=output,y=targets
output = tf.reshape(tf.concat(1, outputs), [-1, size])
softmax_w = tf.get_variable(
"softmax_w", [size, vocab_size], dtype=data_type())
softmax_b = tf.get_variable("softmax_b", [vocab_size], dtype=data_type())
logits = tf.matmul(output, softmax_w) + softmax_b
self._logits=logits
# 将loss理解为一种更复杂的交叉熵形式:与基础模型中的代码类似:
# cross_entropy=tf.reduce_mean(-tf.reduce_sum(y * tf.log(a), reduction_indices=[1]))
loss = tf.nn.seq2seq.sequence_loss_by_example(
[logits],
[tf.reshape(input_.targets, [-1])],
[tf.ones([batch_size * num_steps], dtype=data_type())])
# 上述loss是所有batch上累加的loss,取平均值作为_cost
self._cost = cost = tf.reduce_sum(loss) / batch_size
self._final_state = state
lr与梯度下降
参考解读tensorflow之rnn
在此lstm模型运行过程中需要动态的更新gradient值。
官方文档说明了这种操作:
并给出了一个例子:
# Create an optimizer.
opt = GradientDescentOptimizer(learning_rate=0.1)
# Compute the gradients for a list of variables.
grads_and_vars = opt.compute_gradients(loss, )
# grads_and_vars is a list of tuples (gradient, variable). Do whatever you
# need to the 'gradient' part, for example cap them, etc.
capped_grads_and_vars = [(MyCapper(gv[0]), gv[1]) for gv in grads_and_vars]
# Ask the optimizer to apply the capped gradients.
opt.apply_gradients(capped_grads_and_vars)
模仿这个代码,我们可以写出如下的伪代码:
optimizer = tf.train.AdamOptimizer(learning_rate=self._lr)
# gradients: return A list of sum(dy/dx) for each x in xs.
grads = optimizer.gradients(self._cost, )
clipped_grads = tf.clip_by_global_norm(grads, config.max_grad_norm)
# accept: List of (gradient, variable) pairs, so zip() is needed
self._train_op = optimizer.apply_gradients(zip(grads, ))
此时就差一个不知道了,也就是需要对哪些variables进行求导,答案是:trainable variables:
tvars = tf.trainable_variables()
此时再看官方PTBModel中的代码:
# 在运行过程中想要调整gradient值,就不能直接简单的optimizer.minimize(loss)而是要显式计算gradients
self._lr = tf.Variable(0.0, trainable=False)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars),
config.max_grad_norm)
optimizer = tf.train.GradientDescentOptimizer(self._lr)
self._train_op = optimizer.apply_gradients(
zip(grads, tvars),
global_step=tf.contrib.framework.get_or_create_global_step())
self._new_lr = tf.placeholder(
tf.float32, shape=[], name="new_learning_rate")
self._lr_update = tf.assign(self._lr, self._new_lr)
其中tf.clip_by_global_norm()可用于用于控制梯度爆炸的问题。
梯度爆炸和梯度弥散的原因一样,都是因为链式法则求导的关系,导致梯度的指数级衰减。为了避免梯度爆炸,需要对梯度进行修剪。详见tensorflow笔记:多层LSTM代码分析
main()
main首先要读取并处理数据、配置模型并且控制模型运转。
读取数据、设置config
# 在ptb_raw_data中已经将原始文本转换为id形式
raw_data = reader.ptb_raw_data(FLAGS.data_path)
train_data, valid_data, test_data, vocab_size = raw_data
# 原始数据刚好是10000个单词,所以不需要修改config.vocab_size
# 但是我有试过修改训练数据,所以加上了这句
config = get_config()
config.vocab_size=vocab_size
eval_config = get_config()
eval_config.batch_size = 1
eval_config.num_steps = 1
eval_config.vocab_size=vocab_size
重点关注ptb_raw_data()方法。此方法中有几个关键步骤:
word_to_id = _build_vocab(train_path)
def _build_vocab(filename):
"""
此方法读取原始数据,将换行符替换为,然后根据词频构件一个词汇表并返回。
"""
data = _read_words(filename)
counter = collections.Counter(data)
count_pairs = sorted(counter.items(), key=lambda x: (-x[1], x[0]))
words, _ = list(zip(*count_pairs))
word_to_id = dict(zip(words, range(len(words))))
return word_to_id
def _read_words(filename):
# 在这里讲换行符替换为了
with tf.gfile.GFile(filename, "r") as f:
return f.read().decode("utf-8").replace("\n", "" ).split()
将原始train/valid/test数据转换为id形式
根据上面得到的word_to_id词汇表对原始数据进行转化:
train_data = _file_to_word_ids(train_path, word_to_id)
valid_data = _file_to_word_ids(valid_path, word_to_id)
test_data = _file_to_word_ids(test_path, word_to_id)
生成/训练模型
以train模式为例:
with tf.name_scope("Train"):
# PTBInput中根据config设置好batch_size等,还初始化了input(slice0)以及targetOutput(slice1)
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)
基本是初始化模型的标准套路,但是需要注意PTBInput()
在PTBInput中通过reader.ptb_producer()生成input和targets。
class PTBInput(object):
"""The input data."""
def __init__(self, config, data, name=None):
self.batch_size = batch_size = config.batch_size
self.num_steps = num_steps = config.num_steps
self.epoch_size = ((len(data) // batch_size) - 1) // num_steps
# input是当前slice[batchsize*num_steps],output是下一个slice同样是[batchsize*num_steps]
self.input_data, self.targets = reader.ptb_producer(data, batch_size, num_steps, name=name)
在ptb_producer()中比较有趣的是最后几句:
def ptb_producer(raw_data, batch_size, num_steps, name=None):
# 其他代码与注释
i = tf.train.range_input_producer(epoch_size, shuffle=False).dequeue()
x = tf.slice(data, [0, i * num_steps], [batch_size, num_steps])
y = tf.slice(data, [0, i * num_steps + 1], [batch_size, num_steps])
return x, y
i的本质是range_input_producer()获得的一个FIFOQueue.dequeue()(个人认为近似一个函数),外部调用x和y时就可以通过i不断更新自身的值。因为本模型要做的是预测下一个词,所以在这里y(target)就是x(input)右移一位。
tf中的队列和其他变量一样,是一种有状态的节点,其他节点可以把新元素插入到队列后端(rear),也可以把队列前端(front)的元素删除。有如下例子:
q=tf.FIFOQueue(3,'float')
init=q.enqueue_many(([0.,0.,0.],))
x=q.dequeue()
y=x+1
q_inc=q.enqueue([y])
# 注意,如果不写sess会报错
with tf.Session() as sess:
init.run()
q_inc.run()
q_inc.run()
q_inc.run()
在sess中从队列前端取走一个元素,加上1之后,放回队列的后端。慢慢地,队列的元素的值就会增加,示意图如下:
更多信息请参考TensorFlow 官方文档中文版-线程和队列,
之后循环max_max_epoch次(文本重复次数),循环过程中调整学习率,再调用run_epoch()训练模型。
with sv.managed_session() as session:
for i in range(config.max_max_epoch):
# 修改学习速率大小
lr_decay = config.lr_decay ** max(i + 1 - config.max_epoch, 0.0)
m.assign_lr(session, config.learning_rate * lr_decay)
train_perplexity = run_epoch(session, m, eval_op=m.train_op,verbose=True)
run_epoch()
首先设置需要run获取的数据,如果eval_op不为空,那么调用它并让模型根据预设代码自动优化。
fetches = {
"cost": model.cost,
"final_state": model.final_state,
}
if eval_op is not None:
fetches["eval_op"] = eval_op
for step in range(model.input.epoch_size):
feed_dict = {}
for i, (c, h) in enumerate(model.initial_state):
feed_dict[c] = state[i].c
feed_dict[h] = state[i].h
vals = session.run(fetches, feed_dict)
cost = vals["cost"]
state = vals["final_state"]
costs += cost
iters += model.input.num_steps
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)))
return np.exp(costs / iters)