#encoding=utf8
import pdb
import random
import copy
import numpy as np
import tensorflow as tf
import data_utils
class S2SModel(object):
def __init__(self,
source_vocab_size, # 16*190000,字典的长度6865。编码器输入
target_vocab_size, # 16*190000,目标值6865。如果是翻译,那么两种语言的vocab_size语料长度是不一样的。 #解码器输出
buckets, # 桶目录
size, # lsTm后面连多少个隐层神经元512
dropout, # 隐层dropout率
num_layers, # 多少层的lstm,我们一般选1
max_gradient_norm, # 最大梯度值,防止梯度爆炸。一般最大是50,超过50就当50来说,我们这里是5
batch_size, # 每次多少个batch,训练是64,测试是1
learning_rate, # 学习率,万三之三
num_samples, # embeding个 512,采样512个词作为损失函数来训练输出。不是传统的做embeding用的
# 也就是tf.app.flags.DEFINE_integer(
# 'num_samples',
# 512, # 做softmax时,6000个维度(就是总的汉字的个数),从6000里随机采样512个(采概率最大的)
# '分批softmax的样本量')
forward_only=False, # false是训练,true则是只预测不训练
dtype=tf.float32): # 数据类型
# 上面都是形参,等用到这个方法的时候,传入的是实参
# init member variales
# 加个self,就是把这些属性都给这个类
self.source_vocab_size = source_vocab_size #字典维度
self.target_vocab_size = target_vocab_size #字典维度
self.buckets = buckets # 5_15, 10_20, 15_25, 20_30
self.batch_size = batch_size # 16, 32, 64等
self.learning_rate = learning_rate # 万分之一或千分之一
# 先做Dropout,再去乘以2
# LSTM cells :lstm单元属性
# 链接: https://blog.csdn.net/xierhacker/article/details/78772560
cell = tf.contrib.rnn.BasicLSTMCell(size) # 隐层神经元的个数,512
# 对上一行的cell去做Dropout的,在外面裹一层DropoutWrapper
cell = tf.contrib.rnn.DropoutWrapper(cell, output_keep_prob=dropout) # lstm
# 构建双层lstm网络,只是一个双层的lstm,不是双层的seq2seq
cell = tf.contrib.rnn.MultiRNNCell([cell] * num_layers)
output_projection = None #这里没说多少个,输出层,理论上就是字典维度个
softmax_loss_function = None #softmax暂无定义,softmax其实有好几种,到底要哪一个softmax没说
# 如果vocabulary太大,我们还是按照vocabulary来sample的话,内存会爆
if num_samples > 0 and num_samples < self.target_vocab_size: # 用512个样本来,512<6865
print('开启投影:{}'.format(num_samples)) # 构造采样损失函数的参数
# lstm的w和b已经封装在接口里了,这里找不到。
# 这里的w和b是从 512的隐层 到 6865的输出层(就和之前的一样)
# 在6000多维的向量上去找,谁最大,再去查那个位置,是谁
w_t = tf.get_variable(#获取一个已经存在的变量或者创建一个新的变量
"proj_w",#name
[self.target_vocab_size, size], # 构造lstm神经网络,shape=[6865,512],把每个6865维的向量变成512维的向量
dtype=dtype
)
w = tf.transpose(w_t) # 经过转置之后的, shape=[512,6865]
b = tf.get_variable(
"proj_b",
[self.target_vocab_size],# shape=[6865]
dtype=dtype
)
output_projection = (w, b) #完成输出层的构造,隐层512到输出层6865做全连接,结合图来看
# 这里,这一串代码是么有被运行的,因为我们做的是预测,做预测不需要softmax,直接找argmax,损失才有softmax
# 这个函数写在构造函数里面,只是为了调用里面的参数方便,其实也可以放在外面
# 训练的时候需要,测试的时候不需要,测试的时候,我们直接找位置最大的值
# 最好要做归一化
def sampled_loss(labels, logits):
# labels是y值,已知的, logits是y^值,是预测的
# 构造采样损失函数,bp算法更新映射权重
labels = tf.reshape(labels, [-1, 1])#拉伸成一列,有batch_size行,比如可以有64行
# 因为选项有选fp16的训练,这里统一转换为fp32
local_w_t = tf.cast(w_t, tf.float32)
local_b = tf.cast(b, tf.float32)
local_inputs = tf.cast(logits, tf.float32)
return tf.cast(
#
tf.nn.sampled_softmax_loss(
#embedding W
weights=local_w_t,
biases=local_b,
labels=labels, #真实序列值,每次一个
inputs=local_inputs, #预测出来的值,y^,每次一个
num_sampled=num_samples, #因为输出高维度都是6865,所以采样512个词作为损失函数来训练输出
num_classes=self.target_vocab_size # 原始字典维度,就是6865,采样之后分成多少类
#采样的输出还是6865维的,但它的计算过程是512维,所以要传num_classes,也就是画的那个图,最后输出还是6865维的
),
dtype
)
softmax_loss_function = sampled_loss # 定义了softmax的函数,sampled_loss:别名,简单语义
# 前面是属性,后面才是调用它。seq2seq_f,这个才是核心部分
def seq2seq_f(encoder_inputs, decoder_inputs, do_decode):#真正的建模开始
# 只执行embeding,不执行attention
# encoder_inputs:比如3*6000,就是timestep*维度
# Encoder.先将cell进行deepcopy,因为seq2seq模型是两个相同的模型,但是模型参数不共享,
# 所以encoder和decoder要使用两个不同的RnnCell
tmp_cell = copy.deepcopy(cell)#深拷贝,复制一个完全一模一样的出来,给一个新的内存地址
# 因为整个seq2seq_f是有两个lstm单元组成的
#cell: RNNCell常见的一些RNNCell定义都可以用.
#num_encoder_symbols: source的vocab_size大小,用于embedding矩阵定义
#num_decoder_symbols: target的vocab_size大小,用于embedding矩阵定义
#embedding_size: embedding向量的维度
#num_heads: Attention头的个数,就是使用多少种attention的加权方式,
# 用更多的参数来求出几种attention向量
#output_projection: 输出的映射层,因为decoder输出的维度是output_size, 所以想要得到num_decoder_symbols
# 对应的词还需要增加一个映射层,参数是W和B,W:[output_size, num_decoder_symbols],b:[num_decoder_symbols]
#feed_previous: 是否将上一时刻输出作为下一时刻输入,一般测试的时候置为True,
# 此时decoder_inputs除了第一个元素之外其他元素都不会使用。
#initial_state_attention: 默认为False, 初始的attention是零;若为True,将从initial state和attention states开始。
#tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
#encoder_inputs,
#decoder_inputs,
#cell,
#num_encoder_symbols,
#num_decoder_symbols,
#embedding_size,
#num_heads=1,
#output_projection=None,
#feed_previous=False,
#dtype=None,
#scope=None,
#initial_state_attention=False)
return tf.contrib.legacy_seq2seq.embedding_attention_seq2seq(
encoder_inputs,# tensor of input seq 30
decoder_inputs,# tensor of decoder seq 30
tmp_cell,#自定义的cell,可以是GRU/LSTM, 设置multilayer等
num_encoder_symbols=source_vocab_size,# 词典大小 40000 #输入词库的大小6000,编码阶段字典的维度
num_decoder_symbols=target_vocab_size,# 目标词典大小 40000 #输入词库的大小6000,解码阶段字典的维度
embedding_size=size,# embedding 维度,512
num_heads=20, #选20个也可以,精确度会高点
# num_heads就是attention机制,选一个就是一个head去连,选5个就是5个头去连
# 因为上下句之间,哪个和哪个对应无法确定,所以可以变动
output_projection=output_projection,# 输出层。不设定的话输出维数可能很大(取决于词表大小),设定的话投影到一个低维向量
feed_previous=do_decode,# 是否执行的EOS,是否允许输入中间c传进来,do_decode是个布尔型变量
dtype=dtype
)
# inputs,有三个序列:编码阶段的输入,解码阶段的输入(因为解码阶段也需要输入),解码阶段的输出
self.encoder_inputs = []
self.decoder_inputs = []
self.decoder_weights = []
#decoder_weights 是一个于decoder_outputs大小一样的0-1矩阵,该矩阵将目标序列长度以外的其他位置填0
#encoder_inputs 这个列表对象中的每一个元素表示一个占位符,其名字分别为encoder0, encoder1,…,encoder39,encoder{i}
# 的几何意义是编码器在时刻i的输入。
# buckets中的最后一个是最大的(即第“-1”个)
for i in range(buckets[-1][0]): #buckets[-1][0] 是 取[(5,15), (10,20), (15,25), (20,30)]中的20
self.encoder_inputs.append(tf.placeholder(
tf.int32,
shape=[None],
name='encoder_input_{}'.format(i)
))
# 输出比输入大 1,这是为了保证下面的targets可以向左shift 1位
for i in range(buckets[-1][1] + 1): # 31
self.decoder_inputs.append(tf.placeholder(
tf.int32,
shape=[None],
name='decoder_input_{}'.format(i)
))
self.decoder_weights.append(tf.placeholder( # decoder_weights:解码器权重?
dtype,
shape=[None],
name='decoder_weight_{}'.format(i)
))
# target_weights 是一个与 decoder_outputs 大小一样的 0-1 矩阵。该矩阵将目标序列长度以外的其他位置填充为标量值 0。
# Our targets are decoder inputs shifted by one.
# 主要功能的入口
targets = [
self.decoder_inputs[i + 1] for i in range(buckets[-1][1])
]
# 跟language model类似,targets变量是decoder inputs平移一个单位的结果,
#encoder_inputs: encoder的输入,一个tensor的列表。列表中每一项都是encoder时的一个词(batch)。
#decoder_inputs: decoder的输入,同上
#targets: 目标值,与decoder_input只相差一个<EOS>符号,int32型
#weights: 目标序列长度值的mask标志,如果是padding则weight=0,否则weight=1
#buckets: 就是定义的bucket值,是一个列表:[(5,10), (10,20),(20,30)...]
#seq2seq: 定义好的seq2seq模型,可以使用后面介绍的embedding_attention_seq2seq,embedding_rnn_seq2seq,basic_rnn_seq2seq等
#softmax_loss_function: 计算误差的函数,(labels, logits),默认为sparse_softmax_cross_entropy_with_logits
#per_example_loss: 如果为真,则调用sequence_loss_by_example,返回一个列表,其每个元素就是一个样本的loss值。如果为假,则调用sequence_loss函数,对一个batch的样本只返回一个求和的loss值,具体见后面的分析
#name: Optional name for this operation, defaults to "model_with_buckets".
if forward_only:# 测试阶段 seq2seq_f --> softmax_loss_function --> model_with_buckets
self.outputs, self.losses = tf.contrib.legacy_seq2seq.model_with_buckets(
self.encoder_inputs,
self.decoder_inputs,
targets,
self.decoder_weights,
buckets,
lambda x, y: seq2seq_f(x, y, True),
softmax_loss_function=softmax_loss_function
)
if output_projection is not None:
for b in range(len(buckets)):
self.outputs[b] = [ tf.matmul(output, output_projection[0] ) + output_projection[1] for output in self.outputs[b] ]
#biase= [ tf.matmul(output, output_projection[0] ) + output_projection[1] for output in self.outputs[b] ]
#temp=[]
#for output in self.outputs[b]:
#temp.append( tf.matmul(output, output_projection[0] ) + output_projection[1])
#self.outputs[b] =temp
else:#训练阶段
#将输入长度分成不同的间隔,这样数据的在填充时只需要填充到相应的bucket长度即可,不需要都填充到最大长度。
#比如buckets取[(5,10), (10,20),(20,30)...](每个bucket的第一个数字表示source填充的长度,
#第二个数字表示target填充的长度,eg:‘我爱你’-->‘I love you’,应该会被分配到第一个bucket中,
#然后‘我爱你’会被pad成长度为5的序列,‘I love you’会被pad成长度为10的序列。其实就是每个bucket表示一个模型的参数配置),
#这样对每个bucket都构造一个模型,然后训练时取相应长度的序列进行,而这些模型将会共享参数。
#其实这一部分可以参考现在的dynamic_rnn来进行理解,dynamic_rnn是对每个batch的数据将其pad至本batch中长度最大的样本,
#而bucket则是在数据预处理环节先对数据长度进行聚类操作。明白了其原理之后我们再看一下该函数的参数和内部实现:
#encoder_inputs: encoder的输入,一个tensor的列表。列表中每一项都是encoder时的一个词(batch)。
#decoder_inputs: decoder的输入,同上
#targets: 目标值,与decoder_input只相差一个<EOS>符号,int32型
#weights: 目标序列长度值的mask标志,如果是padding则weight=0,否则weight=1
#buckets: 就是定义的bucket值,是一个列表:[(5,10), (10,20),(20,30)...]
#seq2seq: 定义好的seq2seq模型,可以使用后面介绍的embedding_attention_seq2seq,embedding_rnn_seq2seq,basic_rnn_seq2seq等
#softmax_loss_function: 计算误差的函数,(labels, logits),默认为sparse_softmax_cross_entropy_with_logits
#per_example_loss: 如果为真,则调用sequence_loss_by_example,返回一个列表,其每个元素就是一个样本的loss值。如果为假,则调用sequence_loss函数,对一个batch的样本只返回一个求和的loss值,具体见后面的分析
#name: Optional name for this operation, defaults to "model_with_buckets".
#tf.contrib.legacy_seq2seq.model_with_buckets(encoder_inputs,
#decoder_inputs,
#targets,
#weights,
#buckets,
#seq2seq,
#softmax_loss_function=None,
#per_example_loss=False,
#name=None)
self.outputs, self.losses = tf.contrib.legacy_seq2seq.model_with_buckets(
# self.outputs: 预测值
self.encoder_inputs,
self.decoder_inputs,
targets,
self.decoder_weights, # 权重,目标序列长度值的mask标志,如果是padding则weight=0,否则weight=1
buckets,
lambda x, y: seq2seq_f(x, y, False),
# lambda匿名函数,x, y是函数入口,也就是输入;seq2seq_f(x, y, False)是函数体,也就是输出
softmax_loss_function=softmax_loss_function
)
params = tf.trainable_variables()
opt = tf.train.AdamOptimizer(
learning_rate=learning_rate
)
if not forward_only:# 只有训练阶段才需要计算梯度和参数更新
self.gradient_norms = []
self.updates = []
for output, loss in zip(self.outputs, self.losses):# 用梯度下降法优化
gradients = tf.gradients(loss, params) # gradients是求出来的梯度
clipped_gradients, norm = tf.clip_by_global_norm(gradients,max_gradient_norm)# 梯度截断,防止梯度爆炸
# https://blog.csdn.net/u013713117/article/details/56281715
# tf.clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None)
# 通过权重梯度的总和的比率来截取多个张量的值。t_list是梯度张量, clip_norm是截取的比率,
# 这个函数返回截取过的梯度张量和一个所有张量的全局范数
self.gradient_norms.append(norm)
self.updates.append(opt.apply_gradients(
zip(clipped_gradients, params) # 反向更新
))
# self.saver = tf.train.Saver(tf.all_variables())
self.saver = tf.train.Saver(
tf.global_variables(),
write_version=tf.train.SaverDef.V2
)
def step(
self,
session,
encoder_inputs,
decoder_inputs,
decoder_weights,
bucket_id,
forward_only
):
encoder_size, decoder_size = self.buckets[bucket_id]
if len(encoder_inputs) != encoder_size:
raise ValueError(
"Encoder length must be equal to the one in bucket,"
" %d != %d." % (len(encoder_inputs), encoder_size)
)
if len(decoder_inputs) != decoder_size:
raise ValueError(
"Decoder length must be equal to the one in bucket,"
" %d != %d." % (len(decoder_inputs), decoder_size)
)
if len(decoder_weights) != decoder_size:
raise ValueError(
"Weights length must be equal to the one in bucket,"
" %d != %d." % (len(decoder_weights), decoder_size)
)
input_feed = {}
for i in range(encoder_size):
input_feed[self.encoder_inputs[i].name] = encoder_inputs[i]
for i in range(decoder_size):
input_feed[self.decoder_inputs[i].name] = decoder_inputs[i]
input_feed[self.decoder_weights[i].name] = decoder_weights[i]
# 理论上decoder inputs和decoder target都是n位
# 但是实际上decoder inputs分配了n+1位空间
# 不过inputs是第[0, n),而target是[1, n+1),刚好错开一位
# 最后这一位是没东西的,所以要补齐最后一位,填充0
# input和output来自于同一个序列
last_target = self.decoder_inputs[decoder_size].name #最后一个输入
input_feed[last_target] = np.zeros([self.batch_size], dtype=np.int32)
if not forward_only:
output_feed = [
self.updates[bucket_id], # 跟新后的梯度
self.gradient_norms[bucket_id], # 梯度的二范数
self.losses[bucket_id] # 函数的损失值
]
output_feed.append(self.outputs[bucket_id][i]) # 把不同的桶
else:
output_feed = [self.losses[bucket_id]] # 当前桶的损失
for i in range(decoder_size):
output_feed.append(self.outputs[bucket_id][i]) # 指定桶的不同输出的值
####################################################从这下面开始run,之前都是搭架子
outputs = session.run(output_feed, input_feed) # 把input和output合并
if not forward_only:
# 训练
return outputs[1], outputs[2], outputs[3:]
else:
# 测试阶段
return None, outputs[0], outputs[1:]
def get_batch_data(self, bucket_dbs, bucket_id):
# 将batch的字符调用的data_utils里面的函数转换成数值
data = []
data_in = []
bucket_db = bucket_dbs[bucket_id]
for _ in range(self.batch_size):
ask, answer = bucket_db.random()
data.append((ask, answer))
data_in.append((answer, ask))
return data, data_in
def get_batch(self, bucket_dbs, bucket_id, data):
encoder_size, decoder_size = self.buckets[bucket_id]
# bucket_db = bucket_dbs[bucket_id]
encoder_inputs, decoder_inputs = [], []
for encoder_input, decoder_input in data: # 获取的文字就存放在data里
# encoder_input, decoder_input = random.choice(data[bucket_id])
# encoder_input, decoder_input = bucket_db.random()
#把输入句子转化为id
encoder_input = data_utils.sentence_indice(encoder_input)
decoder_input = data_utils.sentence_indice(decoder_input)
# Encoder
# 七个字,encoder_size =10第二个桶,所以剩余的3个用PAD_ID来补
encoder_pad = [data_utils.PAD_ID] * ( encoder_size - len(encoder_input) )
# 加起来做翻转 10个字也就是 PAD PAD PAD ?吗了吃你天今
encoder_inputs.append(list(reversed(encoder_input + encoder_pad)))
# Decoder
# decoder_size=20 ,len(decoder_input)=0,2:一个是go,一个是eos,decoder_pad_size=18
decoder_pad_size = decoder_size - len(decoder_input) - 2
# 1(GO_ID)+0+1(EOS_ID)+18(PAD_ID)=20
decoder_inputs.append(
[data_utils.GO_ID] + decoder_input +
[data_utils.EOS_ID] +
[data_utils.PAD_ID] * decoder_pad_size
)
batch_encoder_inputs, batch_decoder_inputs, batch_weights = [], [], []
# batch encoder,先编码
for i in range(encoder_size):#10
batch_encoder_inputs.append(np.array(
[encoder_inputs[j][i] for j in range(self.batch_size)],#第j行第i列,batch_size这里是1
dtype=np.int32
))
# batch decoder
for i in range(decoder_size):#size和input是有区别的
batch_decoder_inputs.append(np.array(
[decoder_inputs[j][i] for j in range(self.batch_size)],
dtype=np.int32
))
batch_weight = np.ones(self.batch_size, dtype=np.float32)
# batch_size是训练的时候是64,每一个字被映射成了一个数值
# 我们这里是测试,batch_size=1
for j in range(self.batch_size): # 按列排序,j =1
if i < decoder_size - 1: # decoder_size=19
target = decoder_inputs[j][i + 1] # 由0-18变成了1-19
if i == decoder_size - 1 or target == data_utils.PAD_ID:
batch_weight[j] = 0.0 # decoder最后一个和pad_id时候的ids都是0,有真实的预测值是1
# batch_weight可能就是那个选择y时的权重
batch_weights.append(batch_weight)
return batch_encoder_inputs, batch_decoder_inputs, batch_weights
import os
import sys # 打断点
import math
import time
import numpy as np
import tensorflow as tf
import data_utils # 分桶模型在里面
import s2s_model # 模型 包含了get_batch 和 get_batch_data
import os # 文件夹的增删改查
#FLAGS.learning_rate
# 这是另外一种保存方式
# tf里专门用来保存模型参数的
tf.app.flags.DEFINE_float( #保存参数的数据类型
'learning_rate', #保存参数的名称
0.0003, #保存参数的值
'学习率' #help文件,帮助文件
)
tf.app.flags.DEFINE_float(
'max_gradient_norm',
5.0, # 截断梯度值
'梯度最大阈值'
)
tf.app.flags.DEFINE_float(
'dropout',
1.0,
'每层输出DROPOUT的大小'
)
tf.app.flags.DEFINE_integer(
'batch_size',
64,
'小批量梯度下降的批量大小'
)
tf.app.flags.DEFINE_integer(
'size',
512,
'LSTM每层神经元数量'
)
tf.app.flags.DEFINE_integer(
'num_layers',
2,
'LSTM的层数'
)
tf.app.flags.DEFINE_integer(
'num_epoch',
5000, # 原来写的是10000轮
'训练几轮'
)
tf.app.flags.DEFINE_integer(
'num_samples',
512, # 做softmax时,6000个维度(就是总的汉字的个数),从6000里随机采样512个(采概率最大的)
'分批softmax的样本量'
)
tf.app.flags.DEFINE_integer(
'num_per_epoch',
1000,
'每轮训练多少随机样本'
)
# sqlite3
tf.app.flags.DEFINE_string(
'buckets_dir',
'./bucket_dbs',
'sqlite3数据库所在文件夹'
)
tf.app.flags.DEFINE_string(
'model_dir',
'./model',
'模型保存的目录'
)
tf.app.flags.DEFINE_string(
'model_name',
'model3',
'模型保存的名称'
)
tf.app.flags.DEFINE_boolean(
'use_fp16',
False,
'是否使用16位浮点数(默认32位)'
)
tf.app.flags.DEFINE_integer(
'bleu', # 评测聊天机器人效果 NLTK.bleu() (0,1)趋近于1,代表相似度越高,模型越好。
# NLTK是自然语言处理包。NLTK.bleu([y],[y^])输入两个序列,返回0-1。算的其实就是两个文本之间的欧式距离,可用于论文查重
-2, # 这里视频中写的是0
'是否测试bleu'
)
tf.app.flags.DEFINE_boolean(
'test',
True,
'是否在测试' # false就是在训练
)
# config.json就是根据上面的描述自动生成的
FLAGS = tf.app.flags.FLAGS # 只打印内存地址
buckets = data_utils.buckets # list里面放的元组
def create_model(session, forward_only):
"""建立模型"""
dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
model = s2s_model.S2SModel(
data_utils.dim, # 6865,编码器输入的语料长度
data_utils.dim, # 6865,解码器输出的语料长度
buckets, # buckets就是那四个桶,data_utils.buckets,直接在data_utils写的一个变量,就能直接被点出来
FLAGS.size, # 隐层神经元的个数512
FLAGS.dropout, # 隐层dropout率,dropout不是lstm中的,lstm的几个门里面不需要dropout,没有那么复杂。是隐层的dropout
FLAGS.num_layers, # lstm的层数,这里写的是2
FLAGS.max_gradient_norm, # 5,最大截断梯度,防止梯度爆炸
FLAGS.batch_size, # 64,等下要重新赋值,预测就是1,训练就是64
FLAGS.learning_rate, # 0.003万分之三
FLAGS.num_samples, # 512,用作负采样。为什么选512:长尾效应
forward_only, # 只传一次
dtype
)
return model
def train():
"""训练模型"""
# 准备数据
print('准备数据')
# './bucket_dbs', 'sqlite3数据库所在文件夹'
bucket_dbs = data_utils.read_bucket_dbs(FLAGS.buckets_dir)
bucket_sizes = []
for i in range(len(buckets)):
bucket_size = bucket_dbs[i].size
bucket_sizes.append(bucket_size)
print('bucket {} 中有数据 {} 条'.format(i, bucket_size))
total_size = sum(bucket_sizes)
print('共有数据 {} 条'.format(total_size))
# 开始建模与训练
with tf.Session() as sess:
# 构建模型
model = create_model(sess, False)
# 初始化变量
sess.run(tf.global_variables_initializer())
buckets_scale = [
sum(bucket_sizes[:i + 1]) / total_size
for i in range(len(bucket_sizes))
]
# 开始训练
metrics = ' '.join([
'\r[{}]',
'{:.1f}%',
'{}/{}',
'loss={:.3f}',
'{}/{}'
])
bars_max = 20
with tf.device('/gpu:0'):
for epoch_index in range(1, FLAGS.num_epoch + 1600):
print('Epoch {}:'.format(epoch_index))
time_start = time.time()
epoch_trained = 0
batch_loss = []
while True:
# 选择一个要训练的bucket
random_number = np.random.random_sample()
bucket_id = min([
i for i in range(len(buckets_scale))
if buckets_scale[i] > random_number
])
data, data_in = model.get_batch_data(
bucket_dbs,
bucket_id
)
encoder_inputs, decoder_inputs, decoder_weights = model.get_batch(
bucket_dbs,
bucket_id,
data
)
_, step_loss, output = model.step(
sess,
encoder_inputs,
decoder_inputs,
decoder_weights,
bucket_id,
False
)
epoch_trained += FLAGS.batch_size
batch_loss.append(step_loss)
time_now = time.time()
time_spend = time_now - time_start
time_estimate = time_spend / (epoch_trained / FLAGS.num_per_epoch)
percent = min(100, epoch_trained / FLAGS.num_per_epoch) * 100
bars = math.floor(percent / 100 * bars_max)
sys.stdout.write(metrics.format(
'=' * bars + '-' * (bars_max - bars),
percent,
epoch_trained, FLAGS.num_per_epoch,
np.mean(batch_loss),
data_utils.time(time_spend), data_utils.time(time_estimate)
))
sys.stdout.flush()
if epoch_trained >= FLAGS.num_per_epoch:
break
print('\n')
if not os.path.exists(FLAGS.model_dir):
os.makedirs(FLAGS.model_dir)
if epoch_index%800==0:
model.saver.save(sess, os.path.join(FLAGS.model_dir, FLAGS.model_name))
def test_bleu(count):
"""测试bleu"""
from nltk.translate.bleu_score import sentence_bleu
from tqdm import tqdm
# 准备数据
print('准备数据')
bucket_dbs = data_utils.read_bucket_dbs(FLAGS.buckets_dir)
bucket_sizes = []
for i in range(len(buckets)):
bucket_size = bucket_dbs[i].size
bucket_sizes.append(bucket_size)
print('bucket {} 中有数据 {} 条'.format(i, bucket_size))
total_size = sum(bucket_sizes)
print('共有数据 {} 条'.format(total_size))
# bleu设置0的话,默认对所有样本采样
if count <= 0:
count = total_size
buckets_scale = [
sum(bucket_sizes[:i + 1]) / total_size
for i in range(len(bucket_sizes))
]
with tf.Session() as sess:
# 构建模型
model = create_model(sess, True)
model.batch_size = 1
# 初始化变量
sess.run(tf.initialize_all_variables())
model.saver.restore(sess, os.path.join(FLAGS.model_dir, FLAGS.model_name))
total_score = 0.0
for i in tqdm(range(count)):
# 选择一个要训练的bucket
random_number = np.random.random_sample()
bucket_id = min([
i for i in range(len(buckets_scale))
if buckets_scale[i] > random_number
])
data, _ = model.get_batch_data(
bucket_dbs,
bucket_id
)
encoder_inputs, decoder_inputs, decoder_weights = model.get_batch(
bucket_dbs,
bucket_id,
data
)
_, _, output_logits = model.step(
sess,
encoder_inputs,
decoder_inputs,
decoder_weights,
bucket_id,
True
)
outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
ask, _ = data[0]
all_answers = bucket_dbs[bucket_id].all_answers(ask)
ret = data_utils.indice_sentence(outputs)
if not ret:
continue
references = [list(x) for x in all_answers]
score = sentence_bleu(
references,
list(ret),
weights=(1.0,)
)
total_score += score
print('BLUE: {:.2f} in {} samples'.format(total_score / count * 10, count))
def test():
print("开始测试数据")
class TestBucket(object):
def __init__(self, sentence):#构造函数,并没有传入实参,这几行是一个类,放到外面也是可以的
self.sentence = sentence
def random(self):
return sentence, ''
with tf.Session() as sess:
# 构建模型
model = create_model(sess, True) #传入模型超参数 梯度更新的是参数,梯度不更新的都是超参数
model.batch_size = 1 #原来是64,这边是测试,只能输入1
# 初始化变量
sess.run(tf.global_variables_initializer())
#加载模型,上面建立的模型是个架子,这边需要从目录中去加载模型的参数
#也就是model3.data-00000-of-00001,这个里面装的就是模型的参数
#下面这一行是整个模型最浪费 时间的一段,时间复杂度最高
model.saver.restore(sess, os.path.join(FLAGS.model_dir, FLAGS.model_name))
#加载模型比较耗时
# 开始输入一个句子,并将它读进来,读进来之后,按照桶将句子分,按照模型输出,然后去查字典。
# 将预测值打印出来,同时再打印"> ",要求再输入一句话
sys.stdout.write("> ")
# 存起来
sys.stdout.flush()
#逐行去读
sentence = sys.stdin.readline()
while sentence:
#获取最小的分桶id,b从0-3,找大于我们输入的词的桶,选最小的那个桶
# buckets[b][0]:循环过程 5_15的桶,指的是,上一句话<5个字,下一句话小于15个字
# 先输入一句话:你今天吃饭了吗,看看几个字7个,在bucket_5_15, 10-20,15-25,20-30中哪一个符合
# 后三个符合,所以buckets[b]集合中包含后三个,在这三个中选择最小的一个,也就是10-20,节约空间
bucket_id = min([ b for b in range(len(buckets)) if buckets[b][0] > len(sentence) ])
#输入句子处理,包含句子数字向量化 TestBucket是字典,data就是{ask,answer}
data, _ = model.get_batch_data( {bucket_id: TestBucket(sentence)}, bucket_id )
# data [('天气\n', '')],也就是问答对,但是现在只有问,没有答
#输入句子处理,包含句子数字向量化
# encoder_inputs=1*10,decoder_inputs=1*20 decoder_weights=1*20
encoder_inputs, decoder_inputs, decoder_weights = model.get_batch( {bucket_id: TestBucket(sentence)}, bucket_id, data )
# 这个方法主要输出的是预测值output_logits 是y^预测值
_, _, output_logits = model.step(sess,encoder_inputs,decoder_inputs,decoder_weights, bucket_id,True)
# 根据结果索引出最大值为预测结果
outputs = [int(np.argmax(logit, axis=1)) for logit in output_logits]
# 将这个索引值转化为一句话,ids转化为字
ret = data_utils.indice_sentence(outputs)
print(ret)
print("> ", end="")
sys.stdout.flush()
sentence = sys.stdin.readline()
def main(_):
if FLAGS.bleu > -1:
test_bleu(FLAGS.bleu) #评估nlp测试效果
elif FLAGS.test:
test()
else:
train()
# run的时候会运行 def main(_):这个函数
if __name__ == '__main__':
np.random.seed(0)
tf.set_random_seed(0)
tf.app.run()
# run的是main函数
#!/usr/bin/env python3
#encoding=utf8
import os
import sys
import json
import math
import shutil
import pickle
import sqlite3
from collections import OrderedDict, Counter
import numpy as np
from tqdm import tqdm
def with_path(p):
current_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(current_dir, p)
DICTIONARY_PATH = 'db/dictionary.json'
EOS = '<eos>'
UNK = '<unk>'
PAD = '<pad>' # 桶装不满的用pad去代替
GO = '<go>' # 开始
# 标签是:“L1,L2,L3,L4”
# 则解码器输入将为:[_G0,L1,L2,L3,L4,_PAD]
# 目标标签为:[L1,L2,L3,L4,_END,_PAD]
# 我一般是逗号放到句子后面的……
# 不过这样比较方便屏蔽某一行,如果是JS就不用这样了,因为JS的JSON语法比较松,允许多余逗号
buckets = [
(5, 15)
, (10, 20)
, (15, 25)
, (20, 30)
]
def time(s):
ret = ''
if s >= 60 * 60:
h = math.floor(s / (60 * 60))
ret += '{}h'.format(h)
s -= h * 60 * 60
if s >= 60:
m = math.floor(s / 60)
ret += '{}m'.format(m)
s -= m * 60
if s >= 1:
s = math.floor(s)
ret += '{}s'.format(s)
return ret
def load_dictionary():
with open(with_path(DICTIONARY_PATH), 'r', encoding='UTF-8') as fp:
dictionary = [EOS, UNK, PAD, GO] + json.load(fp)
index_word = OrderedDict() #反向字典
word_index = OrderedDict()
for index, word in enumerate(dictionary): #enumerate是枚举,所以第0个就是eos
index_word[index] = word
word_index[word] = index
dim = len(dictionary)
return dim, dictionary, index_word, word_index
"""
def save_model(sess, name='model.ckpt'):
import tensorflow as tf
if not os.path.exists('model'):
os.makedirs('model')
saver = tf.train.Saver()
saver.save(sess, with_path('model/' + name))
def load_model(sess, name='model.ckpt'):
import tensorflow as tf
saver = tf.train.Saver()
saver.restore(sess, with_path('model/' + name))
"""
dim, dictionary, index_word, word_index = load_dictionary()
print('dim: ', dim)
EOS_ID = word_index[EOS]
UNK_ID = word_index[UNK]
PAD_ID = word_index[PAD]
GO_ID = word_index[GO]
class BucketData(object):
def __init__(self, buckets_dir, encoder_size, decoder_size):
self.encoder_size = encoder_size
self.decoder_size = decoder_size
self.name = 'bucket_%d_%d.db' % (encoder_size, decoder_size)
self.path = os.path.join(buckets_dir, self.name)
self.conn = sqlite3.connect(self.path)
self.cur = self.conn.cursor()
sql = '''SELECT MAX(ROWID) FROM conversation;'''
self.size = self.cur.execute(sql).fetchall()[0][0]
# 获取最大的行号,[(23547,)]
def all_answers(self, ask):
"""找出所有数据库中符合ask的answer
"""
sql = '''SELECT answer FROM conversation WHERE ask = '{}';'''.format(ask.replace("'", "''"))
ret = []
for s in self.cur.execute(sql):
ret.append(s[0])
return list(set(ret))
def random(self):
while True:
# 选择一个[1, MAX(ROWID)]中的整数,读取这一行
rowid = np.random.randint(1, self.size + 1)
sql = '''SELECT ask, answer FROM conversation WHERE ROWID = {};'''.format(rowid)
ret = self.cur.execute(sql).fetchall()
if len(ret) == 1:
ask, answer = ret[0]
if ask is not None and answer is not None:
return ask, answer
def read_bucket_dbs(buckets_dir):
#buckets = [
#(5, 15)
#, (10, 20)
#, (15, 25)
#, (20, 30)
#]
ret = []
for encoder_size, decoder_size in buckets:
bucket_data = BucketData(buckets_dir, encoder_size, decoder_size)
ret.append(bucket_data)
return ret
def sentence_indice(sentence):
# 问答对的数值化
# 遍历这句话中的每一个字,然后正向字典,返回索引,最后返回索引的集合
# 这里是把每一个字,变成一个值,而不是词向量,没有用到词嵌入
ret = []
for word in sentence:
if word in word_index:
ret.append(word_index[word])
else:
ret.append(word_index[UNK]) # 聊天机器人项目中,dictionary是个list类型
return ret
def indice_sentence(indice):
# 不管是问还是答,将数值转化为句子,碰到eos则停,eos本身不做转换
ret = []
for index in indice:
word = index_word[index]
if word == EOS:
break
if word != UNK and word != GO and word != PAD:
ret.append(word)
return ''.join(ret)
def vector_sentence(vector):
return indice_sentence(vector.argmax(axis=1))
def generate_bucket_dbs(
input_dir,
output_dir,
buckets,
tolerate_unk=1
):
pool = {}
word_count = Counter() # word_count是计数器
def _get_conn(key):
if key not in pool:
if not os.path.exists(output_dir):
os.makedirs(output_dir)
name = 'bucket_%d_%d.db' % key
path = os.path.join(output_dir, name)
conn = sqlite3.connect(path)
cur = conn.cursor()
cur.execute("""CREATE TABLE IF NOT EXISTS conversation (ask text, answer text);""")
conn.commit()
pool[key] = (conn, cur)
return pool[key]
all_inserted = {}
for encoder_size, decoder_size in buckets:
key = (encoder_size, decoder_size)
all_inserted[key] = 0
# 从input_dir列出数据库列表
db_paths = []
for dirpath, _, filenames in os.walk(input_dir): # walk 循环遍历
for filename in (x for x in sorted(filenames) if x.endswith('.db')):
db_path = os.path.join(dirpath, filename)
db_paths.append(db_path)
# 对数据库列表中的数据库挨个提取
for db_path in db_paths:
print('读取数据库: {}'.format(db_path))
conn = sqlite3.connect(db_path)
c = conn.cursor()
def is_valid(s):
unk = 0
for w in s:
if w not in word_index:
unk += 1
if unk > tolerate_unk:
return False
return True
# 读取最大的rowid,如果rowid是连续的,结果就是里面的数据条数
# 比SELECT COUNT(1)要快
total = c.execute('''SELECT MAX(ROWID) FROM conversation;''').fetchall()[0][0]
# total得到的是 [(137000,)], fetchall得到的就是137000
ret = c.execute('''SELECT ask, answer FROM conversation;''')
wait_insert = []
def _insert(wait_insert):
if len(wait_insert) > 0:
for encoder_size, decoder_size, ask, answer in wait_insert:
key = (encoder_size, decoder_size)
conn, cur = _get_conn(key)
cur.execute("""INSERT INTO conversation (ask, answer) VALUES ('{}', '{}');""".
format(ask.replace("'", "''"), answer.replace("'", "''")))
all_inserted[key] += 1
for conn, _ in pool.values():
conn.commit()
wait_insert = []
return wait_insert
for ask, answer in tqdm(ret, total=total):
if is_valid(ask) and is_valid(answer):
for i in range(len(buckets)):
# 依次匹配第一个桶,第二个桶...匹配上之后,就break
encoder_size, decoder_size = buckets[i]
if len(ask) <= encoder_size and len(answer) < decoder_size:
word_count.update(list(ask))
word_count.update(list(answer))
wait_insert.append((encoder_size, decoder_size, ask, answer))
if len(wait_insert) > 10000000:
wait_insert = _insert(wait_insert)
break
word_count_arr = [(k, v) for k, v in word_count.items()]
# 注意:word_count是计数器,wait_insert是存放的集合[]
# break跳出整个for循环,continue是结束本次循环,执行下次循环
# 构建字典表word_count_arr [('畹', 188), ('华', 7890), ('吾', 465), ('侄', 726), ('你', 1267564), ('接', 32198)...]
word_count_arr = sorted(word_count_arr, key=lambda x: x[1], reverse=True)
# 分桶wait_insert (5, 15, '畹华吾侄', '你接到这封信的时候'), (10, 20, '是祖传的鸳鸯蝴蝶烟', '这一次你死定了!'),
wait_insert = _insert(wait_insert)
return all_inserted, word_count_arr
if __name__ == '__main__':
print('generate bucket dbs')
# 来源数据库目录
db_path = ''
if len(sys.argv) >= 2 and os.path.exists(sys.argv[1]):
db_path = sys.argv[1]
if not os.path.isdir(db_path):
print('invalid db source path, not dir')
exit(1)
elif os.path.exists('./db'):
db_path = './db'
else:
print('invalid db source path')
exit(1)
# 输出目录
target_path = './bucket_dbs_test'
# 不存在就建
if not os.path.exists(target_path):
os.makedirs(target_path)
elif os.path.exists(target_path) and not os.path.isdir(target_path):
print('invalid target path, exists but not dir')
exit(1)
elif os.path.exists(target_path) and os.path.isdir(target_path):
shutil.rmtree(target_path)
os.makedirs(target_path)
# 生成
all_inserted, word_count_arr = generate_bucket_dbs(db_path, target_path, buckets, 1 )
# 导出字典
# print('一共找到{}个词'.format(len(word_count_arr)))
# with open('dictionary_detail.json', 'w') as fp:
# json.dump(word_count_arr, fp, indent=4, ensure_ascii=False)
# with open('dictionary.json', 'w') as fp:
# json.dump([x for x, _ in word_count_arr], fp, indent=4, ensure_ascii=False)
# 输出词库状况
#for key, inserted_count in all_inserted.items():
#print(key)
#print(inserted_count)
#print('done')
网友评论