美文网首页
聊天机器人代码

聊天机器人代码

作者: 潇萧之炎 | 来源:发表于2019-06-10 08:46 被阅读0次
#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')

相关文章

网友评论

      本文标题:聊天机器人代码

      本文链接:https://www.haomeiwen.com/subject/hrkaxctx.html