Tensorflow手动编写LSTM单元内部结构实现中文文本分类

前言

在之前使用的LSTM是通过调用了tensorflow的api实现的(详情参考Tensorflow使用LSTM实现中文文本分类(2)),如使用
tf.contrib.rnn.BasicLSTMCell() 用来构建一层lstm单元,
tf.contrib.rnn.MultiRNNCell(cells) 用来将多层lstm单元连接起来。
为了加深对lstm cell内部结构的理解,这里将构建lstm单元的api部分去掉,换成手写lstm内部逻辑。

代码如下

注:代码中有一大段注释,那是原来使用api的代码,下面的是手动编写LSTM单元代码。
代码中实现的是一层lstm,个数等于时间步个数。

# -*- coding:utf-8 -*-

# 构建计算图-lstm
# embeding
# lstm
# fc
# train_op
# 训练流程代码
# 数据集的封装
#   api: next_batch(batch_size): 输出的都是 id
# 词表封装
#   api:sentence2id(text_sentence):将句子转化为id
# 类别的封装:
#   api:category2id(text_category): 将类别转化为id

import tensorflow as tf
import os
import sys
import numpy as np
import math

# 打印出 log
tf.logging.set_verbosity(tf.logging.INFO)

# lstm 需要的参数
def get_default_params():
    return tf.contrib.training.HParams(
        num_embedding_size = 16, # 每个词语的向量的长度

        # 指定 lstm 的 步长, 一个sentence中会有多少个词语
        # 因为执行的过程中是用的minibatch,每个batch之间还是需要对齐的
        # 在测试时,可以是一个变长的
        num_timesteps = 50, # 在一个sentence中 有 50 个词语

        num_lstm_nodes = [32, 32], # 每一层的size是多少
        num_lstm_layers = 2, # 和上句的len 是一致的
        # 有 两层 神经单元,每一层都是 32 个 神经单元

        num_fc_nodes = 32, # 全连接的节点数
        batch_size = 100,
        clip_lstm_grads = 1.0,
        # 控制lstm的梯度,因为lstm很容易梯度爆炸或者消失
        # 这种方式就相当于给lstm设置一个上限,如果超过了这个上限,就设置为这个值
        learning_rate = 0.001,
        num_word_threshold = 10, # 词频太少的词,对于模型训练是没有帮助的,因此设置一个门限
    )


hps = get_default_params() # 生成 参数 对象



# 设置文件路径
train_file = './news_data/cnews.train.seg.txt'
val_file = './news_data/cnews.val.seg.txt'
test_file = './news_data/cnews.test.seg.txt'
vocab_file = './news_data/cnews.vocab.txt' # 统计的词频
category_file = './news_data/cnews.category.txt' # 标签
output_folder = './news_data/run_text_rnn'

if not os.path.exists(output_folder):
    os.mkdir(output_folder)

class Vocab:
    '''
    词表的封装
    '''
    def __init__(self, filename, num_word_threahold):
        # 每一个词,给她一个id,另外还要统计词频。ps:前面带下划线的为私有成员
        self._word_to_id = {}
        self._unk = -1 # 先给 unk 赋值一个 负值,然后根据实际情况在赋值
        self._num_word_theshold = num_word_threahold # 低于 这个值 就忽略掉该词
        self._read_dict(filename) # 读词表方法

    def _read_dict(self, filename):
        '''
        读这个词表
        :param filename: 路径
        :return: none
        '''
        with open(filename, 'r') as f:
            lines = f.readlines()
        for line in lines:
            word, frequency = line.strip('\n').split('\t')
            word = word # 获得 单词
            frequency = int(frequency) # 获得 频率
            if frequency < self._num_word_theshold:
                continue # 门限过滤一下
            idx = len(self._word_to_id) #这里使用了一个id递增的小技巧
            if word == '': # 如果是空格,就把上一个id号给它
                # 如果是 unk的话, 就特殊处理一下
                self._unk = idx
            self._word_to_id[word] = idx
            # 如果 word 存在,就把 idx 当做值,将其绑定到一起
            # 如果 word 在词表中不存在,就把nuk的值赋予它

    def word_to_id(self, word):
        '''
        为单词分配id值
        :param word: 单词
        :return:
        '''
        # 字典.get() 如果有值,返回值;无值,返回默认值(就是第二个参数)
        return self._word_to_id.get(word, self._unk)

    def sentence_to_id(self, sentence):
        '''
        将句子 转换成 id 向量
        :param sentence: 要输入的句子(分词后的句子)
        :return:
        '''
        # 单条句子的id vector
        word_ids = [self.word_to_id(cur_word) for cur_word in sentence.split(' ')]
        # cur_word 有可能不存在,需要使用函数进行过滤一下
        return word_ids

    # 定义几个 访问私有成员属性的方法
    # Python内置的 @ property装饰器就是负责把一个方法变成属性调用的
    @ property
    def unk(self):
        return self._unk

    def size(self):
        return len(self._word_to_id)

class CategoryDict:
    '''
    和 词表的 方法 几乎一样
    '''
    def __init__(self, filename):
        self._category_to_id = {}
        with open(filename, 'r') as f:
            lines = f.readlines()
        for line in lines:
            category = line.strip('\r\n')
            idx = len(self._category_to_id)
            self._category_to_id[category] = idx

    def size(self):
        return len(self._category_to_id)

    def category_to_id(self, category):
        if not category in self._category_to_id:
            raise Exception('%s is not in our category list' % category)
        return self._category_to_id[category]



# 获得 词表 对象
vocab = Vocab(vocab_file, hps.num_word_threshold)
# 词表长度
vocab_size = vocab.size()


# 获得 类别表 对象
category_vocab = CategoryDict(category_file)
# 类别 总数
num_classes = category_vocab.size()


# 封装数据集
class TextDataSet:
    '''
    数据集 封装
    功能: 1、将数据集向量化。2、返回batch
    '''
    def __init__(self, filename, vocab, category_vocab, num_timesteps):
        '''
        封装数据集
        :param filename: 可以是训练数据集、测试数据集、验证数据集等
        :param vocab: 词表 对象
        :param category_vocab: 类别 对象
        :param num_timesteps: 步长 (sentence的总长度)
        '''
        # 将 各个对象 赋值
        self._vocab = vocab
        self._category_vocab = category_vocab
        self._num_timesteps = num_timesteps

        # matrix
        self._inputs = []
        # vector
        self._outputs = []
        # batch 起始点
        self._indicator = 0

        # 将文本数据 解析 成 matrix
        self._parse_file(filename) # 进行解析

    def _parse_file(self, filename):
        tf.logging.info('Loading data from %s', filename)
        with open(filename, 'r') as f:
            lines = f.readlines()
        for line in lines:
            label, content = line.strip('\n').split('\t')

            # 得到 一个 label 的 id
            id_label = self._category_vocab.category_to_id(label)
            # 得到 一个 vector
            id_words = self._vocab.sentence_to_id(content)

            # 需要在每一个minibatch上进行对齐,对 word 进行 对齐 操作
            # 如果 超出了界限,就 截断, 如果 不足,就 填充
            id_words = id_words[0: self._num_timesteps] # 超过了 就 截断
            # 低于 num_timesteps 就填充,也就是说,上一句和下面两句 可以完全并列写,神奇!!
            # 这里的编码方式感觉很巧妙!!!
            padding_num = self._num_timesteps - len(id_words)
            id_words = id_words + [self._vocab.unk for i in range(padding_num)]

            self._inputs.append(id_words)
            self._outputs.append(id_label)

        # 转变为 numpy 类型
        self._inputs = np.asarray(self._inputs, dtype=np.int32)
        self._outputs = np.asarray(self._outputs, dtype=np.int32)
        # 对数据进行随机化
        self._random_shuffle()
        self._num_sample = len(self._inputs)

    def _random_shuffle(self):
        p = np.random.permutation(len(self._inputs))
        self._inputs = self._inputs[p]
        self._outputs = self._outputs[p]

    def next_batch(self, batch_size):
        end_indicator = self._indicator + batch_size
        if end_indicator > len(self._inputs):
            self._random_shuffle()
            self._indicator = 0
            end_indicator = batch_size
        if end_indicator > len(self._inputs):
            raise Exception('batch_size: %d is too large' % batch_size)

        batch_inputs = self._inputs[self._indicator: end_indicator]
        batch_outputs = self._outputs[self._indicator: end_indicator]
        self._indicator = end_indicator
        return batch_inputs, batch_outputs

    def num_samples(self):
        return self._num_sample




# 得到 三个 文本对象,当中都包含了 input 和 label
train_dataset = TextDataSet(train_file, vocab, category_vocab, hps.num_timesteps)
val_dataset = TextDataSet(val_file, vocab, category_vocab, hps.num_timesteps)
test_dataset = TextDataSet(test_file, vocab, category_vocab, hps.num_timesteps)



# 开始计算图模型 (重点)
def create_model(hps, vocab_size, num_classes):
    '''
    构建lstm
    :param hps: 参数对象
    :param vocab_size:  词表 长度
    :param num_classes:  分类数目
    :return:
    '''
    num_timesteps = hps.num_timesteps # 一个句子中 有 num_timesteps 个词语
    batch_size = hps.batch_size

    # 设置两个 placeholder, 内容id 和 标签id
    inputs = tf.placeholder(tf.int32, (batch_size, num_timesteps))
    outputs = tf.placeholder(tf.int32, (batch_size, ))

    # dropout keep_prob 表示要keep多少值,丢掉的是1-keep_prob
    keep_prob = tf.placeholder(tf.float32, name='keep_prob')

    global_step = tf.Variable(
        tf.zeros([], tf.int64),
        name='global_step',
        trainable = False)  # 可以保存 当前训练到了 哪一步,而且不训练

    # 随机的在均匀分布下初始化, 构建 embeding 层
    embeding_initializer = tf.random_uniform_initializer(-1.0, 1.0)

    # 和 name_scope 作用是一样的,他可以定义指定 initializer
    # tf.name_scope() 和 tf.variable_scope() 的区别 参考:
    # https://www.cnblogs.com/adong7639/p/8136273.html
    with tf.variable_scope('embedding', initializer=embeding_initializer):
        # tf.varialble_scope() 一般 和 tf.get_variable() 进行配合
        # 构建一个 embedding 矩阵,shape 是 [词表的长度, 每个词的embeding长度 ]
        embeddings = tf.get_variable('embedding', [vocab_size, hps.num_embedding_size], tf.float32)

        # 每一个词,都要去embedding中查找自己的向量
        # [1, 10, 7] 是一个句子,根据 embedding 进行转化
        # 如: [1, 10, 7] -> [embedding[1], embedding[10], embedding[7]]
        embeding_inputs = tf.nn.embedding_lookup(embeddings, inputs)
        # 上句的输入: Tensor("embedding/embedding_lookup:0", shape=(100, 50, 16), dtype=float32)
        # 输出是一个三维矩阵,分别是:100 是 batch_size 大小,50 是 句子中的单词数量,16 为 embedding 向量长度


    # lstm 层

    # 输入层 大小 加上 输出层的大小,然后开方
    scale = 1.0 / math.sqrt(hps.num_embedding_size + hps.num_lstm_nodes[-1]) / 3.0
    lstm_init = tf.random_uniform_initializer(-scale, scale)



    def _generate_parames_for_lstm_cell(x_size, h_size, bias_size):
        '''
        生成参数的变量
        :param x_size: x × w  其中 w 的形状
        :param h_size: 上一层 输出h 的形状
        :param bias_size: 偏置的形状
        :return: 各个 变量
        '''
        x_w = tf.get_variable('x_weights', x_size) # 输入x的w权重的值
        h_w = tf.get_variable('h_weights', h_size) # 上一层 输出h 的 值
        b = tf.get_variable('biases', bias_size, initializer=tf.constant_initializer(0.0)) # 偏置的 值

        return x_w, h_w, b




    with tf.variable_scope('lstm_nn', initializer = lstm_init):
        '''
        cells = [] # 保存两个lstm层
        # 循环这两层 lstm
        for i in range(hps.num_lstm_layers):
            # BasicLSTMCell类是最基本的LSTM循环神经网络单元。
            # 输入参数和BasicRNNCell差不多, 设置一层 的 lstm 神经元
            cell = tf.contrib.rnn.BasicLSTMCell(
                hps.num_lstm_nodes[i], # 每层的 节点个数
                state_is_tuple = True # 中间状态是否是一个元组
            )
            cell = tf.contrib.rnn.DropoutWrapper( # 进行 dropout
                cell,
                output_keep_prob = keep_prob # dropout 的 比例
            )
            cells.append(cell)

        cell = tf.contrib.rnn.MultiRNNCell(cells)
        # 该方法的作用是:将两层的lstm 连到一起,比如:上层的输出是下层的输入
        # 此时的cell,已经是一个多层的lstm,但是可以当做单层的来操作,比较简单

        # 保存中间的一个隐含状态,隐含状态在初始化的时候初始化为0,也就是零矩阵
        initial_state = cell.zero_state(batch_size, tf.float32)

        # rnn_outputs: [batch_size, num_timesteps, lstm_outputs[-1](最后一层的输出)]
        # _ 代表的是隐含状态
        rnn_outputs, _ = tf.nn.dynamic_rnn(
            cell, embeding_inputs, initial_state = initial_state
        ) # 现在的rnn_outputs 代表了每一步的输出

        # 获得最后一步的输出,也就是说,最后一个step的最后一层的输出
        last = rnn_outputs[:, -1, :]
        # print(last) Tensor("lstm_nn/strided_slice:0", shape=(100, 32), dtype=float32)


        '''
        # 生成 四组 可变 参数,分别是 遗忘门、输入门、输出门  和 tanh
        # 输入门
        with tf.variable_scope('inputs'):
            ix, ih, ib = _generate_parames_for_lstm_cell( # 以i开头,代表 inputs
                x_size=[hps.num_embedding_size, hps.num_lstm_nodes[0]], # []
                h_size = [hps.num_lstm_nodes[0], hps.num_lstm_nodes[0]],
                bias_size = [1, hps.num_lstm_nodes[0]]
            )
        with tf.variable_scope('outputs'):
            ox, oh, ob = _generate_parames_for_lstm_cell( # 以i开头,代表 inputs
                x_size=[hps.num_embedding_size, hps.num_lstm_nodes[0]], # []
                h_size = [hps.num_lstm_nodes[0], hps.num_lstm_nodes[0]],
                bias_size = [1, hps.num_lstm_nodes[0]]
            )

        with tf.variable_scope('forget'):
            fx, fh, fb = _generate_parames_for_lstm_cell( # 以i开头,代表 inputs
                x_size=[hps.num_embedding_size, hps.num_lstm_nodes[0]], # []
                h_size = [hps.num_lstm_nodes[0], hps.num_lstm_nodes[0]],
                bias_size = [1, hps.num_lstm_nodes[0]]
            )
        with tf.variable_scope('memory'):
            cx, ch, cb = _generate_parames_for_lstm_cell( # 以i开头,代表 inputs
                x_size=[hps.num_embedding_size, hps.num_lstm_nodes[0]], # []
                h_size = [hps.num_lstm_nodes[0], hps.num_lstm_nodes[0]],
                bias_size = [1, hps.num_lstm_nodes[0]]
            )

        # 初始化 隐状态 隐状态的形状 (batch_size, lstm最后一层神经个数)
        state = tf.Variable(tf.zeros([batch_size, hps.num_lstm_nodes[0]]),
                            trainable = False
                            )

        # 每个神经元的输出 形状同上
        h = tf.Variable(tf.zeros([batch_size, hps.num_lstm_nodes[0]]),
                        trainable = False
                        )

        for i in range(num_timesteps): # 按照 词语的数量 进行
            # [batch_size, 1, embed_size]
            embeding_input = embeding_inputs[:, i, :] # 取出 句子中 的 第一个词语(当i为1时)
            #这样每次取出来的 中间的那一维度 就是 1,可以将其合并掉

            # 因为是 只有一个词语,所以将其reshape成 二维
            embeding_input = tf.reshape(embeding_input, [batch_size, hps.num_embedding_size])

            # 遗忘门
            forget_gate = tf.sigmoid(
                # 输入x与w相乘,加上 上一层输出h与hw相乘,在加上,偏置
                # 以下各个门同理
                tf.matmul(embeding_input, fx) + tf.matmul(h, fh) + fb
            )

            # 输入门
            input_gate = tf.sigmoid(
                tf.matmul(embeding_input, ix) + tf.matmul(h, ih) + ib
            )

            # 输出门
            output_gate = tf.sigmoid(
                tf.matmul(embeding_input, ox) + tf.matmul(h, oh) + ob
            )

            # tanh 层
            mid_state = tf.tanh(
                tf.matmul(embeding_input, cx) + tf.matmul(h, ch) + cb
            )


            # c状态 是 上一个单元传入c状态×遗忘门 再加上 输入门×tanh
            state = mid_state * input_gate + state * forget_gate

            h = output_gate * tf.tanh(state)
        last = h # 只需要 最后一个 输出 就可以了
        # 输出 Tensor("lstm_nn/mul_149:0", shape=(100, 32), dtype=float32)
        # 和注释部分的 last 输出 是同样的结果


    # 将最后一层的输出 链接到一个全连接层上
    # 参考链接:https://www.w3cschool.cn/tensorflow_python/tensorflow_python-fy6t2o0o.html
    fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)
    with tf.variable_scope('fc', initializer = fc_init): # initializer 此范围内变量的默认初始值
        fc1 = tf.layers.dense(last,
                              hps.num_fc_nodes,
                              activation = tf.nn.relu,
                              name = 'fc1')
        # 进行 dropout
        fc1_dropout = tf.nn.dropout(fc1, keep_prob)
        # 进行更换 参考:https://blog.csdn.net/UESTC_V/article/details/79121642

        logits = tf.layers.dense(fc1_dropout, num_classes, name='fc2')

    # 没有东西需要初始化,所以可以直接只用name_scope()
    with tf.name_scope('metrics'):
        softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
            logits = logits,
            labels = outputs
        )



        # 该方法 做了三件事:1,labels 做 onehot,logits 计算softmax概率,3. 做交叉熵
        loss = tf.reduce_mean(softmax_loss)

        #
        y_pred = tf.argmax(
            tf.nn.softmax(logits),
            1,
            #output_type = tf.int64
        )


        # 这里做了 巨大 修改,如果问题,优先检查这里!!!!!!
        #print(type(outputs), type(y_pred))
        correct_pred = tf.equal(outputs, tf.cast(y_pred, tf.int32)) # 这里也做了修改
        accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

    with tf.name_scope('train_op'):
        tvars = tf.trainable_variables() # 获取所有可以训练的变量
        for var in tvars:
            tf.logging.info('variable name: %s' % (var.name)) # 打印出所有可训练变量

        # 对 梯度进行 截断.
        # grads是截断之后的梯度
        grads, _ = tf.clip_by_global_norm(
            tf.gradients(loss, tvars), # 在可训练的变量的梯度
            hps.clip_lstm_grads
        ) # 可以 获得 截断后的梯度



        optimizer = tf.train.AdamOptimizer(hps.learning_rate) # 将每个梯度应用到每个变量上去
        train_op = optimizer.apply_gradients(
            zip(grads, tvars), # 将 梯度和参数 绑定起来
            global_step = global_step # 这个参数 等会儿,再好好研究一下
        )


    return ((inputs, outputs, keep_prob),
            (loss, accuracy),
            (train_op, global_step))

placeholders, metrics, others = create_model(
    hps, vocab_size, num_classes
)

inputs, outputs, keep_prob = placeholders
loss, accuracy = metrics
train_op, global_step = others

init_op = tf.global_variables_initializer()
train_keep_prob_value = 0.8
test_keep_prob_value = 1.0

num_train_steps = 100000

# 验证集、测试集 输出函数
def eval_holdout(sess, dataset_for_test, batch_size):
    # 计算出 该数据集 有多少batch
    num_batches = dataset_for_test.num_samples() // batch_size # // 整除 向下取整

    accuracy_vals = []
    loss_vals = []

    for i in range(num_batches):
        batch_inputs, batch_labels = dataset_for_test.next_batch(batch_size)
        accuracy_val, loss_val = sess.run([accuracy, loss],
                                          feed_dict={
                                              inputs: batch_inputs,
                                              outputs: batch_labels,
                                              keep_prob: train_keep_prob_value
                                          }
                                          )
        accuracy_vals.append(accuracy_val)
        loss_vals.append(loss_val)

        return np.mean(accuracy_vals), np.mean(loss_vals)





# train: 99.7%
# valid: 92.7%
# test: 93.2%


with tf.Session() as sess:
    sess.run(init_op)
    for i in range(num_train_steps):
        batch_inputs, batch_labels = train_dataset.next_batch(hps.batch_size)

        outputs_val = sess.run(
            [loss, accuracy, train_op, global_step],
            feed_dict={
                inputs: batch_inputs,
                outputs: batch_labels,
                keep_prob:train_keep_prob_value
            }
        )

        loss_val, accuracy_val, _, global_step_val = outputs_val


        if global_step_val % 200 == 0:
            tf.logging.info(
                'Step: %5d, loss: %3.3f, accuracy: %3.3f'%(global_step_val, loss_val, accuracy_val))

        if global_step_val % 1000 == 0:
            validdata_accuracy, validdata_loss = eval_holdout(sess, val_dataset, hps.batch_size)
            testdata_accuracy, testdata_loss = eval_holdout(sess, test_dataset, hps.batch_size)
            tf.logging.info(
                ' valid_data Step: %5d, loss: %3.3f, accuracy: %3.5f' % (global_step_val, validdata_loss, validdata_accuracy))
            tf.logging.info(
                ' test_data Step: %5d, loss: %3.3f, accuracy: %3.5f' % (global_step_val, testdata_loss, testdata_accuracy))

'''
INFO:tensorflow:Step: 10000, loss: 0.053, accuracy: 0.990
INFO:tensorflow: valid_data Step: 10000, loss: 0.661, accuracy: 0.88000
INFO:tensorflow: test_data Step: 10000, loss: 1.216, accuracy: 0.80000
'''

训练一万次的精度,训练集0.99, 验证集0.88, 测试集0.80。总体来说是没有Tensorflow使用LSTM实现中文文本分类(2)在一万次上的训练精度高。分析原因的话,就是在上一篇中使用的是双层lstm,lstm cell的个数也多,所以学习能力更强吧。

你可能感兴趣的:(tensorflow,Tensorflow学习笔记)