简单来说,语言模型的目的是为了计算一个句子的出现概率。把句子看成是单词的序列,于是语言模型需要计算的就是 P(w1,w2,⋯,wm) P ( w 1 , w 2 , ⋯ , w m ) 。利用语言模型,可以确定哪个单词序列出现的可能性更大,或者给定若干个单词,可以预测下一个最可能出现的词语。假设输入的拼音串‘xianzaiquna’,它的输出可以是‘西安在去哪’,也可以是‘现在去哪’。根据语言常识可以知道,转换成第二个的概率更高。语言模型就可以得到后者的概率大于前者,因此在大多数情况下,转换成后者比较合理。
那么如何计算一个句子的概率呢?首先一个句子可以被看成一个单词序列: S=(w1,w2,⋯,wm) S = ( w 1 , w 2 , ⋯ , w m ) ,那么,它的概率可以表示为
通常,我们并不会使用上面的公式,我们会认为一个词汇出现的概率仅仅和前面的 n 个词汇相关,并不是和前面出现的所有词汇相关,即所谓的 n-gram 模型。n-gram 模型分为 unigram, bigram, trigram, 对应的 n 为1,2,3。n-gram 一般采用最大似然估计 (maximum likelihood estimation, MLE) 的方法计算,公式如下:
C(X) 表示单词序列 X 在训练语料中出现的次数。训练语料的规模越大,参数估计的结果越可靠。但是,通常我们获得语料都不是足够大的,及时有几十 G 的语料,依然无法保证包含所有的词汇。如果只是按照上面的方法,那些没有在训练语料中出现的单词序列会被直接计算为0。0有一个特征的性质,与任何数相乘都为0。所以,在使用最大似然估计方法时,都需要加入平滑避免参数取值为0,将统计为0的数,设置为一个非常非常小的数,如0.000001。
Perplexity 值刻画的就是通过某一语言模型估计一句话出现的概率。注意,是一句话出现的概率,不是一个词汇在这句话中出现的概率 。比如,当已经知道 (w1,w2,⋯,wm) ( w 1 , w 2 , ⋯ , w m ) 这句话出现在语料之中,那么通过语言模型计算得到的这句话的概率越高越好,也就是 Perplexity 值越小越好。公式为:
如果一个语言模型的 perplexity 是89,就表示,平均情况下,模型预测下一个词时,有89个词可能地可以作为下一个词的合理选择。
相比于直接计算 Perplexity 的值,我们更多的时候会使用它的 log 形式,这样计算代价更小,在计算机中,计算多个数的乘除比加减运算更加耗费资源。
加压缩文件夹,Data 文件夹中的就是 PTB 数据。
wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
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()
# 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", "" ).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