使用 LSTM 计算语言模型的复杂度

简单来说,语言模型的目的是为了计算一个句子的出现概率。把句子看成是单词的序列,于是语言模型需要计算的就是P(w1,w2,⋯,wm) P ( w 1 , w 2 , ⋯ , w m ) 。利用语言模型,可以确定哪个单词序列出现的可能性更大,或者给定若干个单词,可以预测下一个最可能出现的词语。假设输入的拼音串‘xianzaiquna’,它的输出可以是‘西安在去哪’,也可以是‘现在去哪’。根据语言常识可以知道,转换成第二个的概率更高。语言模型就可以得到后者的概率大于前者,因此在大多数情况下,转换成后者比较合理。
那么如何计算一个句子的概率呢?首先一个句子可以被看成一个单词序列:S=(w1,w2,⋯,wm) S = ( w 1 , w 2 , ⋯ , w m ) ,那么,它的概率可以表示为



P(S)=P(w1)P(w2|w1)P(w3|w1,w2)⋯P(wm|w1,w2,⋯,wm−1)(1) (1) P ( S ) = P ( w 1 ) P ( w 2 | w 1 ) P ( w 3 | w 1 , w 2 ) ⋯ P ( w m | w 1 , w 2 , ⋯ , w m − 1 )

通常,我们并不会使用上面的公式,我们会认为一个词汇出现的概率仅仅和前面的 n 个词汇相关,并不是和前面出现的所有词汇相关,即所谓的 n-gram 模型。n-gram 模型分为 unigram, bigram, trigram, 对应的 n 为1,2,3。n-gram 一般采用最大似然估计 (maximum likelihood estimation, MLE) 的方法计算,公式如下:



P(S)=P(w1,w2,⋯,wm)=∏imp(wi−n+1,⋯,wi−1)(2)(3) (2) P ( S ) = P ( w 1 , w 2 , ⋯ , w m ) (3) = ∏ i m p ( w i − n + 1 , ⋯ , w i − 1 )



P(wm|w1,w2,⋯,wm−1)=C(wi−n+1,⋯,wi−1,wi)C(wi−n+1,⋯,wi−1)(4) (4) P ( w m | w 1 , w 2 , ⋯ , w m − 1 ) = C ( w i − n + 1 , ⋯ , w i − 1 , w i ) C ( w i − n + 1 , ⋯ , w i − 1 )

C(X) 表示单词序列 X 在训练语料中出现的次数。训练语料的规模越大,参数估计的结果越可靠。但是,通常我们获得语料都不是足够大的,及时有几十 G 的语料,依然无法保证包含所有的词汇。如果只是按照上面的方法,那些没有在训练语料中出现的单词序列会被直接计算为0。0有一个特征的性质,与任何数相乘都为0。所以,在使用最大似然估计方法时,都需要加入平滑避免参数取值为0,将统计为0的数,设置为一个非常非常小的数,如0.000001。

语言模型效果好坏的常用评价指标:复杂度 Perplexity

Perplexity 值刻画的就是通过某一语言模型估计一句话出现的概率。注意,是一句话出现的概率,不是一个词汇在这句话中出现的概率 。比如,当已经知道 (w1,w2,⋯,wm) ( w 1 , w 2 , ⋯ , w m ) 这句话出现在语料之中,那么通过语言模型计算得到的这句话的概率越高越好,也就是 Perplexity 值越小越好。公式为:



Perplexity(S)=P(w1,w2,⋯,wm)−1m=1P(w1,w2,⋯,wm)‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾√m=∏i=1m1P(wi|w1,w2,⋯,wi−1)‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾⎷m(5)(6)(7) (5) P e r p l e x i t y ( S ) = P ( w 1 , w 2 , ⋯ , w m ) − 1 m (6) = 1 P ( w 1 , w 2 , ⋯ , w m ) m (7) = ∏ i = 1 m 1 P ( w i | w 1 , w 2 , ⋯ , w i − 1 ) m

如果一个语言模型的 perplexity 是89,就表示,平均情况下,模型预测下一个词时,有89个词可能地可以作为下一个词的合理选择。
相比于直接计算 Perplexity 的值,我们更多的时候会使用它的 log 形式,这样计算代价更小,在计算机中,计算多个数的乘除比加减运算更加耗费资源。



log(Perplexity(S))=−1m∑P(wi|w1,w2,⋯,wi−1)(8) (8) l o g ( P e r p l e x i t y ( S ) ) = − 1 m ∑ P ( w i | w 1 , w 2 , ⋯ , w i − 1 )

使用 LSTM 计算 Perplexity

获取数据

加压缩文件夹,Data 文件夹中的就是 PTB 数据。

wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz

源代码

language_model.py
import numpy as np
import tensorflow as tf
import reader
import os

os.environ['CUDA_VISIBLE_DEVICES'] = '1'

# 1. 定义相关的参数
DATA_PATH = "./dataset/ptb_data"
# 隐藏层规模
HIDDEN_SIZE = 200
# LSTM 结构的层数
NUM_LAYERS = 2
# 词典规模,加上语句结束标识符和 rare word 标识符总共一万个单词
VOCAB_SIZE = 10000

LEARNING_RATE = 1.0
# batch 大小
TRAIN_BATCH_SIZE = 20
# 训练数据截断长度
TRAIN_NUM_STEP = 35

# 测试时不需要使用截断,所以可以将测试数据看成一个超长的序列
# 测试数据 batch 的大小
EVAL_BATCH_SIZE = 1
# 测试数据截断长度
EVAL_NUM_STEP = 1
# 使用训练数据的轮数
NUM_EPOCH = 2
# 保留的比例
KEEP_PROB = 0.5
# 控制梯度膨胀的参数
MAX_GRAD_NORM = 5


# 2. 定义一个类来描述模型结构。
class PTBModel(object):
    def __init__(self, is_training, batch_size, num_steps):
        # 记录使用的 batch 大小和截断长度
        self.batch_size = batch_size
        self.num_steps = num_steps

        # 定义输入层。
        # 输入维度为 batch_size * num_steps, 这和 ptb_iterator 函数输出的训练数据 batch 一致
        self.input_data = tf.placeholder(tf.int32, [batch_size, num_steps])
        # 定义预期输出。维度和 ptb_iterator 函数输出的正确答案维度一致
        self.targets = tf.placeholder(tf.int32, [batch_size, num_steps])

        # 定义使用LSTM结构及训练时使用dropout。
        lstm_cell = tf.contrib.rnn.BasicLSTMCell(HIDDEN_SIZE, state_is_tuple=False)
        if is_training:
            lstm_cell = tf.contrib.rnn.DropoutWrapper(lstm_cell, output_keep_prob=KEEP_PROB)
        # 多层 LSTM 结构
        cell = tf.contrib.rnn.MultiRNNCell([lstm_cell] * NUM_LAYERS, state_is_tuple=False)

        # 初始化最初的状态。全零的向量
        self.initial_state = cell.zero_state(batch_size, tf.float32)
        # 将原本单词ID转为单词向量。因为总共有 VOCAB_SIZE 个单词,每个单词向量的维度为 HIDDEN_SIZE,
        # embedding 参数的维度为 VOCAB_SIZE * HIDDEN_SIZE
        embedding = tf.get_variable("embedding", [VOCAB_SIZE, HIDDEN_SIZE])

        # 将原本 batch_size * num_steps 个单词 ID 转化为单词向量,转化后的输入层维度为 batch_size * num_steps * HIDDEN_SIZE
        inputs = tf.nn.embedding_lookup(embedding, self.input_data)

        # 只在训练时使用 dropout
        if is_training:
            inputs = tf.nn.dropout(inputs, KEEP_PROB)

        # 定义输出列表。 在这里先将不同时刻 LSTM 结构的输出收集起来,再通过一个全连接层得到最终的输出。
        outputs = []
        # state 存储不同 batch 中 LSTM 的状态,将其初始化为 0
        state = self.initial_state
        with tf.variable_scope("RNN"):
            for time_step in range(num_steps):
                if time_step > 0: tf.get_variable_scope().reuse_variables()
                # 从输入数据中获取当前时刻获得输入并传入 LSTM 结构
                cell_output, state = cell(inputs[:, time_step, :], state)
                # 将当前输出加入输出队列
                outputs.append(cell_output)

        # 把输出队列展开成[batch_size, num_steps * hidden_size] 的形状,
        # 然后再 reshape 成 [batch_size * num_steps, hidden_size] 的形状。
        output = tf.reshape(tf.concat(outputs, 1), [-1, HIDDEN_SIZE])
        # 将从 LSTM 中得到的输出再经过一个全连接层得到最后的预测结果,最终的预测结果在每一个时刻上都是一个长度为 VOCAB_SIZE 的数组,
        # 经过 softmax 层之后表示下一个位置是不同单词的概率。
        weight = tf.get_variable("weight", [HIDDEN_SIZE, VOCAB_SIZE])
        bias = tf.get_variable("bias", [VOCAB_SIZE])
        logits = tf.matmul(output, weight) + bias

        # 定义交叉熵损失函数和平均损失。
        # TensorFlow 提供了 sequence_loss_by_example 函数来计算一个序列的交叉熵的和
        loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example(
            # 预测的结果
            [logits],
            # 期待的正确答案,这里将 [batch_size, num_steps] 二维数组压缩成一维数组
            [tf.reshape(self.targets, [-1])],
            # 损失的权重。在这里所有的权重都为1,也就是说不同 batch 和不同时刻的重要程度是一样的。
            [tf.ones([batch_size * num_steps], dtype=tf.float32)])
        # 计算得到每个 batch 的平均损失
        self.cost = tf.reduce_sum(loss) / batch_size
        self.final_state = state

        # 只在训练模型时定义反向传播操作。
        if not is_training: return
        trainable_variables = tf.trainable_variables()

        # 控制梯度大小,定义优化方法和训练步骤。
        # 通过 clip_by_global_norm 函数控制梯度的大小,避免梯度膨胀的问题
        # tf.clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None)
        # To perform the clipping, the values t_list[i] are set to: t_list[i] * clip_norm / max(global_norm, clip_norm)
        # where: global_norm = sqrt(sum([l2norm(t)**2 for t in t_list]))
        # If clip_norm > global_norm then the entries in t_list remain as they are,
        # otherwise they're all shrunk by the global ratio.
        # Any of the entries of t_list that are of type None are ignored.
        grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM)
        optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
        self.train_op = optimizer.apply_gradients(zip(grads, trainable_variables))


# 3. 使用给定的模型model在数据data上运行train_op并返回在全部数据上的perplexity值
def run_epoch(session, model, data, train_op, output_log, epoch_size):
    # 计算 perplexity 的辅助变量
    total_costs = 0.0
    iters = 0
    state = session.run(model.initial_state)

    # 训练一个epoch。
    for step in range(epoch_size):
        # 获取一个 batch 的数据
        x, y = session.run(data)
        # 在当前 batch 上运行 train_op 并计算损失值。交叉熵损失函数计算的就是下一个单词为给定单词的概率。
        cost, state, _ = session.run([model.cost, model.final_state, train_op],
                                     {model.input_data: x, model.targets: y, model.initial_state: state})
        # 将不同时刻、不同 batch 的概率加起来就可以得到第二个 perplexity 公式等号右边的部分,再将这个和做指数运算就可以得到 perplexity 值。
        total_costs += cost
        iters += model.num_steps

        if output_log and step % 100 == 0:
            print("After %d steps, perplexity is %.3f" % (step, np.exp(total_costs / iters)))
    # 返回给定模型在给定数据上的 perplexity 值
    return np.exp(total_costs / iters)


# 4. 定义主函数并执行。
def main():

    # 获取原始数据
    train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH)

    # 计算一个epoch需要训练的次数
    train_data_len = len(train_data)
    train_batch_len = train_data_len // TRAIN_BATCH_SIZE
    train_epoch_size = (train_batch_len - 1) // TRAIN_NUM_STEP

    valid_data_len = len(valid_data)
    valid_batch_len = valid_data_len // EVAL_BATCH_SIZE
    valid_epoch_size = (valid_batch_len - 1) // EVAL_NUM_STEP

    test_data_len = len(test_data)
    test_batch_len = test_data_len // EVAL_BATCH_SIZE
    test_epoch_size = (test_batch_len - 1) // EVAL_NUM_STEP

    initializer = tf.random_uniform_initializer(-0.05, 0.05)
    # 定义训练用的 NN
    with tf.variable_scope("language_model", reuse=None, initializer=initializer):
        train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)

    # 定义测试用的 NN
    with tf.variable_scope("language_model", reuse=True, initializer=initializer):
        eval_model = PTBModel(False, EVAL_BATCH_SIZE, EVAL_NUM_STEP)

    # 训练模型。
    with tf.Session() as session:
        tf.global_variables_initializer().run()

        train_queue = reader.ptb_producer(train_data, train_model.batch_size, train_model.num_steps)
        eval_queue = reader.ptb_producer(valid_data, eval_model.batch_size, eval_model.num_steps)
        test_queue = reader.ptb_producer(test_data, eval_model.batch_size, eval_model.num_steps)

        coord = tf.train.Coordinator()
        threads = tf.train.start_queue_runners(sess=session, coord=coord)

        # 使用训练数据训练模型
        for i in range(NUM_EPOCH):
            print("In iteration: %d" % (i + 1))
            run_epoch(session, train_model, train_queue, train_model.train_op, True, train_epoch_size)

            valid_perplexity = run_epoch(session, eval_model, eval_queue, tf.no_op(), False, valid_epoch_size)
            print("Epoch: %d Validation Perplexity: %.3f" % (i + 1, valid_perplexity))

        # 使用测试数据测试模型效果
        test_perplexity = run_epoch(session, eval_model, test_queue, tf.no_op(), False, test_epoch_size)
        print("Test Perplexity: %.3f" % test_perplexity)

        coord.request_stop()
        coord.join(threads)


if __name__ == "__main__":
    main()
reader.py
# Copyright 2015 The TensorFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================


"""Utilities for parsing PTB text files."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import collections
import os

import tensorflow as tf


def _read_words(filename):
    with tf.gfile.GFile(filename, "rb") as f:
        return f.read().decode("utf-8").replace("\n", "<eos>").split()


def _build_vocab(filename):
    data = _read_words(filename)

    counter = collections.Counter(data)
    count_pairs = sorted(counter.items(), key=lambda x: (-x[1], x[0]))

    words, _ = list(zip(*count_pairs))
    word_to_id = dict(zip(words, range(len(words))))

    return word_to_id


def _file_to_word_ids(filename, word_to_id):
    data = _read_words(filename)
    return [word_to_id[word] for word in data if word in word_to_id]


def ptb_raw_data(data_path=None):
    """Load PTB raw data from data directory "data_path".
    Reads PTB text files, converts strings to integer ids,
    and performs mini-batching of the inputs.
    The PTB dataset comes from Tomas Mikolov's webpage:
    http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
    Args:
      data_path: string path to the directory where simple-examples.tgz has
        been extracted.
    Returns:
      tuple (train_data, valid_data, test_data, vocabulary)
      where each of the data objects can be passed to PTBIterator.
    """

    train_path = os.path.join(data_path, "ptb.train.txt")
    valid_path = os.path.join(data_path, "ptb.valid.txt")
    test_path = os.path.join(data_path, "ptb.test.txt")

    word_to_id = _build_vocab(train_path)
    train_data = _file_to_word_ids(train_path, word_to_id)
    valid_data = _file_to_word_ids(valid_path, word_to_id)
    test_data = _file_to_word_ids(test_path, word_to_id)
    vocabulary = len(word_to_id)
    return train_data, valid_data, test_data, vocabulary


def ptb_producer(raw_data, batch_size, num_steps, name=None):
    """Iterate on the raw PTB data.
    This chunks up raw_data into batches of examples and returns Tensors that
    are drawn from these batches.
    Args:
      raw_data: one of the raw data outputs from ptb_raw_data.
      batch_size: int, the batch size.
      num_steps: int, the number of unrolls.
      name: the name of this operation (optional).
    Returns:
      A pair of Tensors, each shaped [batch_size, num_steps]. The second element
      of the tuple is the same data time-shifted to the right by one.
    Raises:
      tf.errors.InvalidArgumentError: if batch_size or num_steps are too high.
    """
    with tf.name_scope(name, "PTBProducer", [raw_data, batch_size, num_steps]):
        raw_data = tf.convert_to_tensor(raw_data, name="raw_data", dtype=tf.int32)

        data_len = tf.size(raw_data)
        batch_len = data_len // batch_size
        data = tf.reshape(raw_data[0: batch_size * batch_len],
                          [batch_size, batch_len])

        epoch_size = (batch_len - 1) // num_steps
        assertion = tf.assert_positive(
            epoch_size,
            message="epoch_size == 0, decrease batch_size or num_steps")
        with tf.control_dependencies([assertion]):
            epoch_size = tf.identity(epoch_size, name="epoch_size")

        i = tf.train.range_input_producer(epoch_size, shuffle=False).dequeue()
        x = tf.strided_slice(data, [0, i * num_steps],
                             [batch_size, (i + 1) * num_steps])
        x.set_shape([batch_size, num_steps])
        y = tf.strided_slice(data, [0, i * num_steps + 1],
                             [batch_size, (i + 1) * num_steps + 1])
        y.set_shape([batch_size, num_steps])
        return x, y