机器学习深度学习个⼈进阶⽇志-基于Tensorflow的“作诗机器
⼈”完整版
今天完成Tensorflow的第⼆个项⽬学习与实战——“作诗机器⼈”,也就是能够⾃动⽣成古诗,并且可以⽣成藏头诗,感觉⽐较有意思。其基本原理就是我上⼀篇博客中的“基于RNN的语⾔模型”,这个项⽬相当于是对这个模型的实战。
⾸先介绍实验平台和数据。
实验平台:
tensorflow 0.12
python 3.5
数据集可以从上⾯提供的链接博客中下载,数据集长这个样⼦:
代码及我个⼈理解的注释:
import collections
import numpy as np
import tensorflow as tf
#数据集,⽂件应放在项⽬⽬录下
poetry_file=''
poetrys=[]
#with-open结构打开⽂件,可以⾃动关闭⽂件
#⽐较⼀下读表格csv⽂件时⽤到的sd_csv(),⽂本⽂件⽤open()
with open(poetry_file,'r',encoding='utf-8') as f:
# f是⼀个可以迭代的对象⽀持for line in f
for line in f:
try:
# strip() ⽅法⽤于移除字符串头尾指定的字符(默认为空格)
#通过“:”来从⼀⾏中分开了题⽬和内容,通过观察数据集这⾥似乎有点问题
tensorflow版本选择
title,content=line.strip().split(':')
#替换掉空格
place(' ','')
#如果有特殊字符的话不参与训练
if '_' in content or '(' in content or '(' in content or '《' in content or '[' in content:
continue
#对content的长度进⾏了限制 <5似乎好理解 79哪⾥来的??
if len(content)<5 or len(content)>79:
continue
#content是个字符串,这个操作是为啥⽬前没懂
content='['+content+']'
#添加到训练集中,注意这时候每个数据是string类型,两边有[]包围
poetrys.append(content)
except Exception as e:
#pass就是什么也不做,只是为了防⽌语法错误
pass
#s通过上⾯的代码,将数据集放在了poetrys这个列表中,⽽且只是放了content
#sorted(iterable[, cmp[, key[, reverse]]])
#key后⾯的lambda是⼀个匿名函数参数为line 这个Line是从poetrys中取出的 line的值len(line)就是⽐较的元素默认升序poetrys = sorted(poetrys,key=lambda line:len(line))
print('唐诗总数: ',len(poetrys))
all_words=[]
for poetry in poetrys:
#poetry是⼀个string类型,可迭代对象
#poetry是⼀个string类型,可迭代对象
#z这是⼀个列表相加的操作,相当于两个列表合并
#⽐如:a = [1,2,3],b = [4,5,6],c = a+b,c的结果:[1,2,3,4,5,6]
all_words+=[word for word in poetry]
#所以这个all_words就是把所有的word弄成⼀块了
#Counter类:为hashable对象计数,是字典的⼦类。引⼊⾃2.7。
#Counter类的⽬的是⽤来跟踪值出现的次数。它是⼀个⽆序的容器类型,以字典的键值对形式存储,其中元素作为key,其计数作为value。counter = collections.Counter(all_words)
# item()⽅法,转为(elem, cnt)格式的列表
#所以x就表⽰⼀个(elem, cnt),x[1]也就是cnt的值了,加上负号后,这样原本默认的升序排序变成了降序
#排序结果是⼀个(elem, cnt)格式的列表,且按字出现的频率由⾼到低
count_pairs=sorted(counter.items(),key=lambda x:-x[1])
#zip() 函数⽤于将可迭代的对象作为参数,将对象中对应的元素打包成⼀个个元组,然后返回由这些元组组成的列表。
#利⽤ * 号操作符,可以将元组解压为列表
#所以words是⼀个[[]]格式的容器,内层[]是[elem,cnt],且所有的[]按字出现频率由⾼到低排序
#第⼆个返回值没看懂
words,_=zip(*count_pairs)
#依旧没看懂
#暂时认为words变成了⼀个字的列表
words=words[:len(words)]+(' ',)
#range返回⼀个从1到len(words)的列表
#通过zip函数打包成[(word1,1),(word2,2)...]形式
#通过dict函数变成了字典{word1:1,word2:2..},这⾥的1,2其实表明了word出现的频率排名
word_num_map=dict(zip(words,range(len(words))))
#get返回指定键的值,也就是word的数字ID值,默认返回words⼤⼩,
#该匿名函数的参数为word,返回指定word在words中的位置
to_num=lambda word:word_(word,len(words))
#map()函数接收两个参数,⼀个是函数,⼀个是序列,map将传⼊的函数依次作⽤到序列的每个元素,并把结果作为新的list返回。poetrys_vector=[list(map(to_num,poetry)) for poetry in poetrys]
#这样得到的结果就是得到了⼀个[[]]⼆维列表,⾥⾯⼀维是每个poetry形成的list
#实现了⽂字的向量化
#通常训练模型都是分batch的
batch_size=64
#块数
n_chunk=len(poetrys_vector)//batch_size
x_batches=[]
y_batches=[]
for i in range(n_chunk):
start_index=i*batch_size
end_index=start_index+batch_size
#取出batches训练集
batches=poetrys_vector[start_index:end_index]
#lengh为最长的poetry的长度
length=max(map(len,batches))
#返回给定要求(形状、数据类型)填充的矩阵
#⽤空格所对应的num填充,所以明⽩了之前的代码为什么words=words[:len(words)]+(' ',)
#空格对应的Num应该是len(words),也就是字的个数
xdata=np.full((batch_size,length),word_num_map[' '],np.int32)
for row in range(batch_size):
#batches[row]是⼀个列表,记录了batches中第row个诗句对应的向量如[33,285,675,...]
xdata[row,:len(batches[row])]=batches[row]
py(xdata)
ydata[:,:-1]=xdata[:,1:]
"""
xdata            ydata
[6,2,4,6,9]      [2,4,6,9,9]
[1,4,2,8,5]      [4,2,8,5,5]
"""
#为什么这么做,去理解语⾔模型的原理
x_batches.append(xdata)
y_batches.append(ydata)
#通过上⾯的代码,x_batches变成了⼀个⼆维列表,⾥⾯的列表元素就是⼀个batch⼤⼩的训练数据,上⾯所有的代码就是数据的预处理过程#定义RNN⽹络
#定义RNN⽹络
#定义占位符作为数据输⼊
input_data=tf.placeholder(tf.int32,[batch_size,None])
output_targets=tf.placeholder(tf.int32,[batch_size,None])
#参数:模型类型、RNN隐藏层神经元个数,RNN隐藏层层数,默认是2层隐藏层的128个神经元的⽹络
def neural_network(model='lstm',rnn_size=128,num_layers=2):
#cell-fun为类
if model=='rnn':
cell__cell.BasicRNNCell
elif model=='gru':
cell__cell.GRUCell
elif model=='lstm':
cell__cell.BasicLSTMCell
#调⽤类的构造器,cell为类的实例
#创建rnn层,run_size个神经元
#官⽅建议state_is_tuple=True,此时,输⼊和输出的states为c(cell状态)和h(输出)的⼆元组,还记得LSTM的内部结构吗
#输⼊、输出、cell状态的维度相同,都是 batch_size * num_units,
#建议直接读源代码
cell =cell_fun(rnn_size,state_is_tuple=True)
# MultiRNNCell也是⼀个类
# 参数cells: list of RNNCells that will be composed in this order.,是⼀个列表,这⾥写作[cell]*num_layers
# *操作符去构建list中的重复元素,[cell]*num_layers⽣成的是[cell,cell]
cell = _cell.MultiRNNCell([cell] * num_layers, state_is_tuple=True)
# cell为多层的RNN
# 状态tensor初始化为0
initial_state = _state(batch_size, tf.float32)
with tf.variable_scope('rnnlm'):
#定义隐藏层与输出层连接权重
#run_size也就是隐藏层神经元个数(输出个数),连接len(words)+1个输出层神经元,此处为何+1,应该是给其他单词留着
softmax__variable("softmax_w",[rnn_size,len(words)+1])
softmax__variable("softmax_b",[len(words)+1])
with tf.device("/cpu:0"):
#embedding技术:X所属空间的单词映射为到Y空间的多维向量
#word embedding,就是到⼀个映射或者函数,⽣成在⼀个新的空间上的表达
#所有的embedding都是随机初始化,然后在训练过程中不断更新embedding矩阵的值。
#因为我们input embedding后的结果就直接输⼊给了第⼀层cell,所以我们就确定下来embedding matrix shape=[vocab_size, hidden_units_size]
embedding = tf.get_variable("embedding",[len(words)+1,rnn_size])
#此处得到的inputs是embedding矩阵
bedding_lookup(embedding,input_data)
#TensorFlow提供了⼀个tf.nn.dynamic_rnn函数,使⽤该函数就相当于调⽤了n次call函数。即通过{h0,x1, x2, …., xn}直接得{h1,h2…,hn}。
outputs,last_dynamic_rnn(cell,inputs,initial_state=initial_state,scope='rnnlm')
#得到的outputs就是time_steps步⾥所有的输出。它的形状为(batch_size, time_steps, cell.output_size)。state是最后⼀步的隐状态,它的形状为(batch_size, cell.s    shape(outputs,[-1,rnn_size])
#output⼤⼩为batch_size*run_size
#softmax_w⼤⼩为run_size*(len(words)+1)
#logits⼤⼩为 batch_size*(len(words)+1)
logits=tf.matmul(output,softmax_w)+softmax_b
#probs就是我们预测下⼀个词是什么的概率分布
softmax(logits)
return logits,last_state,probs,cell,initial_state
#训练⽹络
def train_neural_network():
logits, last_state,probs, _, _ = neural_network()
#前⾯定义过output_targets=tf.placeholder(tf.int32,[batch_size,None])
targets = tf.reshape(output_targets, [-1])
"""
这个函数⽤于计算所有examples(假设⼀句话有n个单词,⼀个单词及单词所对应的label就是⼀个example,所有example就是⼀句话中所有单词)的加权交叉熵损失    logits参数是⼀个2D Tensor构成的列表对象,每⼀个2D Tensor的尺⼨为[batch_size x num_decoder_symbols],
函数的返回值是⼀个1D float类型的Tensor,尺⼨为batch_size,其中的每⼀个元素代表当前输⼊序列example的交叉熵。
进⼀步理解:
logits 的shape = [batch_size, vocab_size], vocab_size是(分类)类别的个数
targets 的shape = [batch_size]
sequence_loss_by_example的做法是,针对logits中的batch_size个examples中的每⼀个example,
sequence_loss_by_example的做法是,针对logits中的batch_size个examples中的每⼀个example,
对所有vocab_size个预测结果,得出预测值最⼤的那个类别,与target中的值相⽐较计算Loss值
因此返回值就是⼀个batch_size⼤⼩的float类型的tensor,代表了交叉熵损失
"""
#
loss = tf.nn.seq2seq.sequence_loss_by_example([logits], [targets], [tf.ones_like(targets, dtype=tf.float32)],
len(words))
#计算损失均值
cost = tf.reduce_mean(loss)
learning_rate = tf.Variable(0.0, trainable=False)
#返回所有已定义的可以训练的变量列表
tvars = tf.trainable_variables()
#gradients是求梯度的函数
"""
Gradient Clipping的引⼊是为了处理gradient explosion或者gradients vanishing的问题。当在⼀次迭代中权重的更新过于迅猛的话,很容易导致loss divergence。G
具体的细节是
1.在solver中先设置⼀个clip_gradient
2.在前向传播与反向传播之后,我们会得到每个权重的梯度diff,这时不像通常那样直接使⽤这些梯度进⾏权重更新,⽽是先求所有权重梯度的平⽅和sumsq_diff,3.最后将所有的权重梯度乘以这个缩放因⼦,这时得到的梯度才是最后的梯度信息。
这样就保证了在⼀次迭代更新中,所有权重的梯度的平⽅和在⼀个设定范围以内,这个范围就是clip_gradient.
tf.clip_by_global_norm
tf.clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None)
通过权重梯度的总和的⽐率来截取多个张量的值。
t_list 是梯度张量, clip_norm 是截取的⽐率, 这个函数返回截取过的梯度张量和⼀个所有张量的全局范数。
t_list[i] 的更新公式如下:
t_list[i] * clip_norm / max(global_norm, clip_norm)
其中global_norm = sqrt(sum([l2norm(t)**2 for t in t_list]))
global_norm 是所有梯度的平⽅和,如果 clip_norm > global_norm ,就不进⾏截取
"""
grads, _ = tf.clip_by_global_adients(cost, tvars), 5)
optimizer = tf.train.AdamOptimizer(learning_rate)
train_op = optimizer.apply_gradients(zip(grads, tvars))
with tf.Session() as sess:
#版本0.12
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver(tf.all_variables())
for epoch in range(10):
#tf.assign(A, new_number): 这个函数的功能主要是把A的值变为new_number
#在这⾥⽤于动态调整学习率
sess.run(tf.assign(learning_rate, 0.002 * (0.97 ** epoch)))
n = 0
for batche in range(n_chunk):
train_loss, _, _ = sess.run([cost, last_state, train_op],
feed_dict={input_data: x_batches[n], output_targets: y_batches[n]})
n += 1
print(epoch, batche, train_loss)
if epoch % 5 == 0:
saver.save(sess, './dule', global_step=epoch)
train_neural_network()
下⾯是使⽤模型⽣成古诗: