[实战Google深度学习框架]Tensorflow(7)自然语言处理

本篇blog主要以code+markdown的形式介绍tf这本实战书。(建议使用jupyter来学习)

第九章 自然语言处理

  • 9.1 语言模型的背景知识

  • 9.2 神经语言模型

  • 9.3 神经网络机器翻译

9.1 语言模型的背景知识

9.1.1 语言模型简介

  1. 把句子看成单词的序列,语言模型可以表示为一个计算p(w_1, w_2, w_3, ...,w_m)的模型。
  2.  生成自然语言文本的应用依赖语言模型来优化输出文本的流畅性。
  3. Seq2Seq模型可以看做是一个条件语言模型(Condational Language Model)
  • 一个句子可以看作成一个单词序列

                                                                 S=(w_1,w_2,w_3,w_4,...,w_m)

  • m为句子的长度,那么它的概率可以表示为

                                                         p(S) = p(w_1,w_2,w_3,w_4,...,w_m) \\ \/\quad \quad \;\,\quad=p(w_1)p(w_2|w_1)p(w_3|w_1,w_2)\cdot \cdot \cdot p(w_m|w_1,w_2,w_3,\cdot \cdot \cdot ,w_{m-1})

                                                         p(w_m|w_1,w_2,w_3,\cdot \cdot \cdot ,w_{m-1}): 表示已知前m-1个单词时,第m个单词为w_m的条件概率。

        假设一门语言的词汇量为V,将p(w_m|w_1,w_2,w_3,\cdot \cdot \cdot ,w_{m-1})的所有参数保存在一个模型里,需要V^m个参数,为了顾及这些参数的取值,常见的方法有n-gram模型、决策树、最大熵模型、条件随机场、神经网络模型等。这里先用n-gram作为介绍,n-gram模型做出了一个有限历史假设:当前单词的出现概率仅仅与前面的n-1个单词相关,因此以上公式可以近似为:

                                                        p(S)=p(w_1,w_2,w_3,\cdot \cdot \cdot ,w_m) = \prod _{i}^{m}p(w_i|w_{i-n+1},\cdot \cdot \cdot w_{m-1})

n指的是当前单词的个数。n-gram模型的参数一般采用最大似然估计(Maximum Likelihood Estimation, MLE)方法计算:

                                                        p(w_i|w_{i-n+1},\cdot \cdot \cdot ,w_{i-1})=\frac{C(w_{i-n+1},\cdot \cdot \cdot ,w_{i-1},w_i)}{C(w_{i-n+1},\cdot \cdot \cdot ,w_{i-1})}

C(X)表示单词序列X在训练语料中出现的次数。

 

9.1.2 语言模型的评价方法

  • 语言模型效果好坏常用的复杂度(perplexity)

                                                     perplexity(S)=p(w_1,w_2,w_3,\cdot \cdot \cdot ,w_m)^{-1/m} \\ \/\ \qquad \qquad \qquad \;\;\;\,\,=\sqrt[m]{\frac{1}{p(w_1,w_2,w_3,\cdot \cdot \cdot ,w_m)}} \\ \/\ \qquad \qquad \qquad \;\;\;\,\,=\sqrt[m]{\prod ^{m}_{i=1} \frac{1}{p(w_i|w_1,\cdot \cdot \cdot ,w_{i-1})}}

perplexity实际是计算每一个单词得到的概率倒数的几何平均,可以理解为平均分支系数(average branching factor),即是模型预测下一个词时的平均可选择数量。

  • 目前在PTB(Penn Tree Bank)数据集上最好的模型的perplexity为47.7。

通常来说采用perplexity的对数表达形式:

                                                   log(perplexity(S)) = -\frac{1}{m}\sum_{i=1}^{m}logp(w_i|w_1,\cdot \cdot \cdot ,w_{i-1})

  • 在数学上, log perplexity可以堪称是真实分布和预测分布之间的交叉熵。假设x是一个离散变量,u(x)和v(x)诗两个与x香港的概率分布,那么u和v时间的交叉熵定义是分布u下-log(v(x))的期望值:

                                                 H(u,v)=E_u[-logv(x)]=-\sum _x u(x)log(v(x))

把x看作单词,u(x)为每个位置上的真实分布,v(x)的模型的预测分布p(w_i|w_1,\cdot \cdot \cdot ,w_{i-1})

在给定上文w_1,w_2,\cdot \cdot \cdot ,w_{i-1}条件下,语料中出现单词的概率为1,出现其他单词概率均为0。

                                               u(x|w_1,\cdot \cdot \cdot ,w_{i-1})=\left\{\begin{matrix} 1, &x=w_i \\ 0, &x\neq w_i \end{matrix}\right.

                                      H(u,v)=-\sum _x u(x)logv(x) \\ \/ \qquad \qquad \;\;\, = -\frac{1}{m}\sum_{i=1}^{m}(\sum_xu(x|w_1,\cdot \cdot \cdot ,w_{i-1})logp(x|w_1,\cdot \cdot \cdot ,w_{i-1})) \\ \/ \qquad \qquad \;\;\, =-\frac{1}{m}\sum^{m}_{i=1}(1.0\times logp(w_i|w_1,\cdot \cdot \cdot ,w_{i-1}) \\ \/ \qquad \qquad \qquad \qquad\;\;\;\;\; + \sum_{x\neq w_i}0.0\times logp(x|w_1,\cdot \cdot \cdot ,w_{i-1}) ) \\ \/ \qquad \qquad \;\;\, = -\frac{1}{m}\sum ^m_{i=1}logp(w_i|w_1,\cdot \cdot \cdot ,w_{i-1}) \\ \/ \qquad \qquad \;\;\, =log(perplexity(S))

import tensorflow as tf
# 假设词汇表的大小为3,语料包含两个单词“2 0”
word_labels = tf.constant([2, 0])

# 假设模型对两个单词预测时,产生的logit分别是[2.0, -1.0, 3.0]和[1.0, 0.0, -0.5]
# 注意这里的logit不是概率,因此它们不是0.0-1.0范围之间的数字。
# 则需要调用prob=tf.nn.softmax(logits)。但这里计算交叉熵的函数直接输入logits
predict_logits = tf.constant([[2.0, -1.0, 3.0], [1.0, 0.0, -0.5]])

# 使用sparse_softmax_cross_entropy_with_logits计算交叉熵。
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels = word_labels, logits= predict_logits)

# 运行程序,计算loss的结果
sess = tf.Session()
sess.run(loss)

# sparse_softmax_cross_entropy_with_logits与上面函数相似
word_prob_distribution = tf.constant([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]])
loss = tf.nn.softmax_cross_entropy_with_logits(
    labels = word_prob_distribution, logits = predict_logits)
# 运行结果
sess.run(loss)

# 由于sparse_softmax_cross_entropy_with_logits允许提供一个概率分布,一种叫label_smoothing的技巧是将正确数据的概率设比为一个比1.0略高的值
word_prob_smooth = tf.constant([[0.01, 0.01, 0.98], [0.98, 0.01, 0.01]])
loss = tf.nn.softmax_cross_entropy_with_logits(
    labels = word_prob_smooth, logits = predict_logits)

sess.run(loss)

 

9.2 神经语言模型

使用循环神经网络,对每个时刻输入·为句子的单词w_i,而每个时刻的输出为一个概率分布。

9.2.1 PTB数据集的预处理

在网站上下载PTB数据为了将文本转换为模型可以读入的单词序列,先将10000个词汇分别映射到0-9999之间的整数编号。

  • 下面的辅助程序首先按照词频顺序为每个词汇分配一个编号,然后将词汇表保存到一个vocab文件中。
import codecs
import collections
from operator import itemgetter

RAW_DATA = "C:/Users/gdtop/Desktop/simple-examples2/data/ptb.train.txt"
VOCAB_OUTPUT = 'ptb.vocab'            # 输出词汇表文件

counter = collections.Counter()       # 统计单词出现频率count
with codecs.open(RAW_DATA, "r", "utf-8") as f:  # 每一行读取
    for line in f:
        for word in line.strip().split():       # 切割分词
            counter[word] += 1

# 按照词频顺序对单词排序
sorted_word_to_cnt = sorted(counter.items(),
                           key = itemgetter(1),
                           reverse = True)
sorted_words = [x[0] for x in sorted_word_to_cnt]

# 在文本换行处加上句子结束符<eos>,预先加入词汇表
sorted_words = [",eos"] + sorted_words

'''
除了<eos>还需要将低频<unk>和句子起始符<sos>加入词汇表,并且删除低频词汇,因为在PTB数据中已经替换了低频词汇,所以不需要这一步
sorted_words = ["<unk>","<sos>","<eos>"] + sorted_words
if len(sorted_words)>10000:
sorted_words = sorted_words[:10000]
'''
with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output:
    for word in sorted_words:
        file_output.write(word + "\n")
print(sorted_words)
[',eos', 'the', '<unk>', 'N', 'of', 'to', 'a', 'in', ... , 'guterman', 'ssangyong', 'ipo', 'wachter']

 

  • 在确定了词汇表之后,把文件转化为单词编号,每个单词的编号就是它在词汇文件中的行号。
import codecs
import sys

RAW_DATA = "C:/Users/gdtop/Desktop/simple-examples/data/ptb.train.txt"
VOCAB = "ptb.vocab"                # 上面生成的词汇表文件
OUTPUT_DATA = "ptb.train"                 # 将单词替换问单词编号后的输出文件

# 读取词汇表,并建立词汇到单词编号的映射
with codecs.open(VOCAB, 'r', 'utf-8') as f_vocab:
    vocab = [w.strip() for w in f_vocab.readlines()]
word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))}

# 如果发现了被删除的低频词,则替换为"<unk>
def get_id(word):
    return word_to_id[word] if word in word_to_id else word_to_id["<unk>"]

fin = codecs.open(RAW_DATA, "r", "utf-8")
fout = codecs.open(OUTPUT_DATA, "w", "utf-8")
for line in fin: 
    words = line.strip().split() + ["<eos>"] # 读取单词并添加<eos>结束符
    # 将每个单词替换为词汇表的编号 注意这里' '.join空格作为连接标志
    out_line = ' '.join([str(get_id(w)) for w in words]) + '\n'
    fout.write(out_line)
fin.close()
fout.close()

代码注意:

1.这里在文件IO上用到了codecs,它与常规的f=open(file_name,access_mode = 'r',buffering = -1)优势在于

input文件(gbk, utf-8...)   ----decode----->   unicode  -------encode------> output文件(gbk, utf-8...), 最终返回的是unicode,不容易出现编码上的问题。

import codecs
fw = codecs.open('test1.txt','r','utf-8')
fw.write(line2)

codecs.open( filepath, method, encoding)

  • filepath--文件路径
  • method--打开方式,r为读,w为写,rw为读写,a附加到文件末尾(如果需要以二进制方式打开文件,需要在mode后面加上字符"b",比如"rb""wb"等)
  • encoding--文件的编码,中文文件使用utf-8

 

2. 常用对字符串的操作

  • Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。
str = "00000003210Runoob01230000000"
print str.strip( '0' )     # 去除首尾字符 0   3210Runoob0123

str2 = "   Runoob      "   # 去除首尾空格     Runoob
print str2.strip()
str = "123abcrunoob321"
print (str.strip( '12' ))  # 字符序列为 12    3abcrunoob3
  • Python split() 通过指定分隔符对字符串进行切片,如果参数 num 有指定值,则仅分隔 num 个子字符串,返回分割后的字符串列表
str.split(str="", num=string.count(str)).

# str -- 分隔符,默认为所有的空字符,包括空格、换行(\n)、制表符(\t)等。
# num -- 分割次数。
  • sorted() 函数对所有可迭代的对象进行排序操作。(注意与sort区别是对可迭代对象)
sorted(iterable[, cmp[, key[, reverse]]])

'''
    iterable -- 可迭代对象。
    cmp -- 比较的函数,这个具有两个参数,参数的值都是从可迭代对象中取出,此函数必须遵守的规则为,大                于则返回1,小于则返回-1,等于则返回0。
    key -- 主要是用来进行比较的元素,只有一个参数,具体的函数的参数就是取自于可迭代对象中,指定可迭    代对象中的一个元素来进行排序。
    reverse -- 排序规则,reverse = True 降序 , reverse = False 升序(默认)。
'''

3. Python join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。

这里有一段是 out_line = '  '.join([str(get_id(w)) for w in words]) + '\n'

注意这里的空格是作为两个字对应数字中间的连接符号

 

通常来说使用TFRecords来提高读写效率。

 

 9.2.2 PTB数据的batching方法

文本数据中,由于每个句子的长度不同,所以在进行batching时需要采取一些特殊操作。最常见是使用填充(padding)将同一个batch的句子长度补齐。

import tensorflow as tf
import numpy as np

TRAIN_DATA = "ptb.train"
TRAIN_BATCH_SIZE = 20
TRAIN_NUM_STEP = 35

# 从文件读取数据,并返回包含单词编号的数组
def read_data(file_path):
    with open(file_path, "r") as fin: 
        # 将整个文档读进一个长字符串
        id_string = ''.join([line.strip() for line in fin.readlines()])
    # 将读取的单词编号转为整数
    id_list = [int(w) for w in id_string.split()]
    return id_list

def make_batches(id_list, batch_size, num_step):
    # 计算总的batch数量。每一个batch包含的单词数量是batch_sieze * num_step
    num_batches = (len(id_list) - 1) // (batch_size * num_step)
    
    # 将数据整理成一个维度为[batch_size, num_batches * num_step]的二维数组
    data = np.array(id_list[: num_batches * batch_size * num_step])
    data = np.reshape(data, [batch_size, num_batches * num_step])
    # 沿着第二个维度将数据切分成num_batches个batch,存入一个数组
    data_batches = np.split(data, num_batches, axis = 1)
    
    # 重复上一个操作,这里得到的是RNN每一步输出所需要预测的下一个单词
    label = np.array(id_list[1 : num_batches * batch_size * num_step + 1])
    label = np.reshape(label, [batch_size, num_batches * num_step])
    label_batches = np.split(label, num_batches, axis = 1)
    
    # 返回一个长度为num_batches的数组,每一项包括一个data矩阵和一个label矩阵
    return list(zip(data_batches, label_batches))


def main():
    train_batches = make_batches(read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
    
if __name__ == "__main__":
    main()

 

9.2.3 基于循环神经网络的神经语言模型

NLP应用中主要多了两个层: 词向量层(embedding)softmax层

  • 词向量层——每一个单词用一个实数向量表示

将词汇表嵌入到一个固定维度的实数空间。主要有两大作用:

  1. 降低输入的维度。如果不适用词向量而是以one-hot vector形式输入循环神经网络,那么输入的维度将和词汇量大小相同,通常在10000以上,而词向量维度在200-1000之间。
  2. 增加语义信息。简单的单词编号不包含任何语义信息,词向量将稀疏的编号转化为稠密的向量。比如猫或者狗带来的影响可能是相似的,这样的模型下,猫和狗的词向量取值可能是相近的。

假设词向量维度是EMB_SIZE,词汇表大小为VOCAB_SIZE,那么所有单词可以放入大小为VOCAB_SIZE x EMB_SIZE的矩阵内。

embedding = tf.get_variable("embedding",[VOCAB_SIZE, EMB_SIZE])

# 输出的矩阵比输入的数据多一个维度,一般输入的大小是batch_size x num_steps,
# 而输出的input_embedding维度是batch_size x num_steps x EMB_SIZE
input_embedding = tf.nn.embedding_lookup(embedding, input_data)
  • Softmax层——将循环神经网络的输出转化为一个单词表中每个单词的输出概率

1. 使用一个线性映射将循环神经网络的输出映射为一个维度与词汇表大小相同的向量,这一步叫logits。

# 定义线性映射用到的参数
# HIDDEN_SIZE是循环神经网络的隐藏状态维度,VOCAB_SIZE是词汇表大小
weight = tf.get_variable("embedding",[HIDDEN_SIZE, VOCAB_SIZE])
bias = tf.get_variabel("bias", [VOCAB_SIZE])

# 计算线性映射
# output是RNN的输出,其维度为[batch_size * num_steps, HIDDEN_SIZE]
logits = tf.nn.bias_add(tf.matmul(output, weight), bias)

2. 调用Softmax方法将logits转化为和为1的概率

# probs的维度与logits的维度相同
probs = tf.nn.softmax(logits)

模型不关心概率的具体取值,而更关心最终的log perplexity

loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
    labels = tf.reshape(self.targets, [-1]), logits = logits)

 

通过共享参数减少参数数量

softmax层和词向量层都与词汇表大小 VOCAB_SIZE成正比,并且往往都很多。

我们可以共享词向量层和Softmax层的参数,不仅可以大幅减少参数量,还能提高模型效果。

 

完整代码

# coding: utf-8
import numpy as np
import tensorflow as tf

1.设置参数

TRAIN_DATA = "C:/Users/gdtop/Desktop/simple-examples/data/ptb.train.txt"          # 训练数据路径。
EVAL_DATA = "C:/Users/gdtop/Desktop/simple-examples/data/ptb.valid.txt"           # 验证数据路径。
TEST_DATA = "C:/Users/gdtop/Desktop/simple-examples/data/ptb.test.txt"            # 测试数据路径。
HIDDEN_SIZE = 300                 # 隐藏层规模。
NUM_LAYERS = 2                    # 深层循环神经网络中LSTM结构的层数。
VOCAB_SIZE = 10000                # 词典规模。
TRAIN_BATCH_SIZE = 20             # 训练数据batch的大小。
TRAIN_NUM_STEP = 35               # 训练数据截断长度。

EVAL_BATCH_SIZE = 1               # 测试数据batch的大小。
EVAL_NUM_STEP = 1                 # 测试数据截断长度。
NUM_EPOCH = 5                     # 使用训练数据的轮数。
LSTM_KEEP_PROB = 0.9              # LSTM节点不被dropout的概率。
EMBEDDING_KEEP_PROB = 0.9         # 词向量不被dropout的概率。
MAX_GRAD_NORM = 5                 # 用于控制梯度膨胀的梯度大小上限。
SHARE_EMB_AND_SOFTMAX = True      # 在Softmax层和词向量层之间共享参数。

2. 定义模型

# 通过一个PTBModel类来描述模型,这样方便维护循环神经网络中的状态。
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]。
        self.input_data = tf.placeholder(tf.int32, [batch_size, num_steps])
        self.targets = tf.placeholder(tf.int32, [batch_size, num_steps])
        
        # 定义使用LSTM结构为循环体结构且使用dropout的深层循环神经网络。
        dropout_keep_prob = LSTM_KEEP_PROB if is_training else 1.0
        lstm_cells = [
            tf.nn.rnn_cell.DropoutWrapper(
                tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE),
                output_keep_prob=dropout_keep_prob)
            for _ in range(NUM_LAYERS)]     
        cell = tf.nn.rnn_cell.MultiRNNCell(lstm_cells)            
        
        # 初始化最初的状态,即全零的向量。这个量只在每个epoch初始化第一个batch
        # 时使用。
        self.initial_state = cell.zero_state(batch_size, tf.float32)

        # 定义单词的词向量矩阵。
        embedding = tf.get_variable("embedding", [VOCAB_SIZE, HIDDEN_SIZE])
        
        # 将输入单词转化为词向量。
        inputs = tf.nn.embedding_lookup(embedding, self.input_data)
        
        # 只在训练时使用dropout。
        if is_training:
            inputs = tf.nn.dropout(inputs, EMBEDDING_KEEP_PROB)
 
        # 定义输出列表。在这里先将不同时刻LSTM结构的输出收集起来,再一起提供给
        # softmax层。
        outputs = []
        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()
                cell_output, state = cell(inputs[:, time_step, :], state)
                outputs.append(cell_output) 
        # 把输出队列展开成[batch, hidden_size*num_steps]的形状,然后再
        # reshape成[batch*numsteps, hidden_size]的形状。
        output = tf.reshape(tf.concat(outputs, 1), [-1, HIDDEN_SIZE])
 
        # Softmax层:将RNN在每个位置上的输出转化为各个单词的logits。
        if SHARE_EMB_AND_SOFTMAX:
            weight = tf.transpose(embedding)
        else:
            weight = tf.get_variable("weight", [HIDDEN_SIZE, VOCAB_SIZE])
        bias = tf.get_variable("bias", [VOCAB_SIZE])
        logits = tf.matmul(output, weight) + bias
        
        # 定义交叉熵损失函数和平均损失。
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
            labels=tf.reshape(self.targets, [-1]),
            logits=logits)
        self.cost = tf.reduce_sum(loss) / batch_size
        self.final_state = state
        
        # 只在训练模型时定义反向传播操作。
        if not is_training: return

        trainable_variables = tf.trainable_variables()
        # 控制梯度大小,定义优化方法和训练步骤。
        grads, _ = tf.clip_by_global_norm(
            tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM)
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        self.train_op = optimizer.apply_gradients(
            zip(grads, trainable_variables))

 

3.定义数据和训练过程。

# 使用给定的模型model在数据data上运行train_op并返回在全部数据上的perplexity值。
def run_epoch(session, model, batches, train_op, output_log, step):
    # 计算平均perplexity的辅助变量。
    total_costs = 0.0
    iters = 0
    state = session.run(model.initial_state) 
    # 训练一个epoch。
    for x, y in batches:
        # 在当前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})
        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)))
        step += 1

    # 返回给定模型在给定数据上的perplexity值。
    return step, np.exp(total_costs / iters)


# 从文件中读取数据,并返回包含单词编号的数组。
def read_data(file_path):
    with open(file_path, "r") as fin:
        # 将整个文档读进一个长字符串。
        id_string = ' '.join([line.strip() for line in fin.readlines()])
    id_list = [int(w) for w in id_string.split()]  # 将读取的单词编号转为整数
    return id_list


def make_batches(id_list, batch_size, num_step):
    # 计算总的batch数量。每个batch包含的单词数量是batch_size * num_step。
    num_batches = (len(id_list) - 1) // (batch_size * num_step)

    # 如9-4图所示,将数据整理成一个维度为[batch_size, num_batches * num_step]
    # 的二维数组。
    data = np.array(id_list[: num_batches * batch_size * num_step])
    data = np.reshape(data, [batch_size, num_batches * num_step])
    # 沿着第二个维度将数据切分成num_batches个batch,存入一个数组。
    data_batches = np.split(data, num_batches, axis=1)

    # 重复上述操作,但是每个位置向右移动一位。这里得到的是RNN每一步输出所需要预测的
    # 下一个单词。
    label = np.array(id_list[1 : num_batches * batch_size * num_step + 1]) 
    label = np.reshape(label, [batch_size, num_batches * num_step])
    label_batches = np.split(label, num_batches, axis=1)  
    # 返回一个长度为num_batches的数组,其中每一项包括一个data矩阵和一个label矩阵。
    return list(zip(data_batches, label_batches))

4. 获得ptb文件

import codecs
import collections
from operator import itemgetter
import sys

def get_id(word,word_to_id):


    # 如果发现了被删除的低频词,则替换为"<unk>
    
    return word_to_id[word] if word in word_to_id else word_to_id["<unk>"]

def get_vocab(RAW_DATA):
    # RAW_DATA = "C:/Users/gdtop/Desktop/simple-examples/data/ptb.train.txt"
    VOCAB_OUTPUT = 'ptb.vocab'            # 输出词汇表文件

    counter = collections.Counter()       # 统计单词出现频率count
    with codecs.open(RAW_DATA, "r", "utf-8") as f:  # 每一行读取
        for line in f:
            for word in line.strip().split():       # 切割分词
                counter[word] += 1

    # 按照词频顺序对单词排序
    sorted_word_to_cnt = sorted(counter.items(),
                               key = itemgetter(1),
                               reverse = True)
    sorted_words = [x[0] for x in sorted_word_to_cnt]

    # 在文本换行处加上句子结束符<eos>,预先加入词汇表
    sorted_words = ["<eos>"] + sorted_words

    '''
    除了<eos>还需要将低频<unk>和句子起始符<sos>加入词汇表,并且删除低频词汇,因为在PTB数据中已经替换了低频词汇,所以不需要这一步
    sorted_words = ["<unk>","<sos>","<eos>"] + sorted_words
    if len(sorted_words)>10000:
    sorted_words = sorted_words[:10000]
    '''
    with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output:
        for word in sorted_words:
            file_output.write(word + "\n")
        #print("success")
    
    return VOCAB_OUTPUT
        
    # OUTPUT_DATA = "ptb.train"                 # 将单词替换问单词编号后的输出文件

def ptb_write(RAW_DATA,OUTPUT_DATA,VOCAB_OUTPUT):
    
    # 读取词汇表,并建立词汇到单词编号的映射
    with codecs.open(VOCAB_OUTPUT, 'r', 'utf-8') as f_vocab:
        vocab = [w.strip() for w in f_vocab.readlines()]
    word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))}
    
    fin = codecs.open(RAW_DATA, "r", "utf-8")
    fout = codecs.open(OUTPUT_DATA, "w", "utf-8")
    for line in fin: 
        words = line.strip().split() + ["<eos>"] # 读取单词并添加<eos>结束符
        # 将每个单词替换为词汇表的编号
        out_line = ' '.join([str(get_id(w,word_to_id)) for w in words]) + '\n'
        fout.write(out_line)
        #print(out_line)
    fin.close()
    fout.close()

def get_ptb(RAW_DATA, OUTPUT_DATA):
    VOCAB_OUTPUT = get_vocab(RAW_DATA)
    ptb_write(RAW_DATA,OUTPUT_DATA,VOCAB_OUTPUT)

5. 主函数。

def main():
    # 定义初始化函数。
    initializer = tf.random_uniform_initializer(-0.05, 0.05)
    
    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("language_model", 
                           reuse=None, initializer=initializer):
        train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)

    # 定义测试用的循环神经网络模型。它与train_model共用参数,但是没有dropout。
    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_batches = make_batches(
            read_data(TRAIN_DATA), TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
        eval_batches = make_batches(
            read_data(EVAL_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP)
        test_batches = make_batches(
            read_data(TEST_DATA), EVAL_BATCH_SIZE, EVAL_NUM_STEP)

        step = 0
        for i in range(NUM_EPOCH):
            print("In iteration: %d" % (i + 1))
            step, train_pplx = run_epoch(session, train_model, train_batches,
                                         train_model.train_op, True, step)
            print("Epoch: %d Train Perplexity: %.3f" % (i + 1, train_pplx))

            _, eval_pplx = run_epoch(session, eval_model, eval_batches,
                                     tf.no_op(), False, 0)
            print("Epoch: %d Eval Perplexity: %.3f" % (i + 1, eval_pplx))

        _, test_pplx = run_epoch(session, eval_model, test_batches,
                                 tf.no_op(), False, 0)
        print("Test Perplexity: %.3f" % test_pplx)

if __name__ == "__main__":
    main()

注意:

书中未把原始的数据转换成单词编号后的文件,这里我自己写了第四步。

 

9.3 神经网络机器翻译

最基础的机器翻译算法——Seq2Seq模型,介绍机器翻译的数据集以及数据预处理方法,而且还有一个s2s的重要改进——注意力机制(attention)。

9.3.1 机器翻译背景与Seq2Seq模型介绍

       模型的训练步骤可分为预处理、词对齐、短语对齐、抽取短语特征(feature)、训练语言模型、学习特征权重。Seq2Seq模型的基本思想——使用一个循环神经网络读取这个编码,将其解压为目标语言的一个句子。这两个循环神经网络分别称为编码器(Encoder)和解码器(Decoder)

       对于seq2seq的decoder,它在训练阶段和预测阶段对rnn的输出的处理可能是不一样的,比如在训练阶段可能对rnn的输出不处理,直接用target的序列作为下时刻的输入,如上图一。而预测阶段会将rnn的输出当成是下一时刻的输入,因为此时已经没有target序列可以作为输入了,如上图二。

       解码器结构与语言模型基本相同:输入为单词的词向量,输出为softmax产生的单词概率,损失函数为log perplexity。解码器的第一个输入时一个特殊的输入<sos>(Start-Of-Sentence)字符,每一步预测的单词时训练数据的目标句子,预测序列的结尾是<eos>字符。

 

9.3.2 机器翻译文本数据的预处理

机器翻译领域重要数据集是WMT数据集,选取中英的图标下载数据。下面以中英翻译为例,数据集包含21万个句子对,内容是TED演讲字幕。

下载的文本没有经过预处理,特别是切词,,但是每个英文单词和标点符号之间紧密相连,这里需要用一些独立的工具来进行切词操作。使用moses,下载地址。# perl moses_tokenizer.perl -no-escape -l en < ./train.raw.en> train.txt.en

 

1.参数设置。

import tensorflow as tf
# 假设输入数据已经用9.2.1小节中的方法转换成了单词编号的格式。
SRC_TRAIN_DATA = "./train.en"        # 源语言输入文件。
TRG_TRAIN_DATA = "./train.zh"        # 目标语言输入文件。
CHECKPOINT_PATH = "./seq2seq_ckpt"   # checkpoint保存路径。  

HIDDEN_SIZE = 1024                   # LSTM的隐藏层规模。
NUM_LAYERS = 2                       # 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000               # 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                # 目标语言词汇表大小。
BATCH_SIZE = 100                     # 训练数据batch的大小。
NUM_EPOCH = 5                        # 使用训练数据的轮数。
KEEP_PROB = 0.8                      # 节点不被dropout的概率。
MAX_GRAD_NORM = 5                    # 用于控制梯度膨胀的梯度大小上限。
SHARE_EMB_AND_SOFTMAX = True         # 在Softmax层和词向量层之间共享参数。

MAX_LEN = 50   # 限定句子的最大单词数量。
SOS_ID  = 1    # 目标语言词汇表中<sos>的ID。

2.读取训练数据并创建Dataset。

# 使用Dataset从一个文件中读取一个语言的数据。
# 数据的格式为每行一句话,单词已经转化为单词编号。
def MakeDataset(file_path):
    dataset = tf.data.TextLineDataset(file_path)
    # 根据空格将单词编号切分开并放入一个一维向量。
    dataset = dataset.map(lambda string: tf.string_split([string]).values)
    # 将字符串形式的单词编号转化为整数。
    dataset = dataset.map(
        lambda string: tf.string_to_number(string, tf.int32))
    # 统计每个句子的单词数量,并与句子内容一起放入Dataset中。
    dataset = dataset.map(lambda x: (x, tf.size(x)))
    return dataset

# 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和
# batching操作。
def MakeSrcTrgDataset(src_path, trg_path, batch_size):
    # 首先分别读取源语言数据和目标语言数据。
    src_data = MakeDataset(src_path)
    trg_data = MakeDataset(trg_path)
    # 通过zip操作将两个Dataset合并为一个Dataset。现在每个Dataset中每一项数据ds
    # 由4个张量组成:
    #   ds[0][0]是源句子
    #   ds[0][1]是源句子长度
    #   ds[1][0]是目标句子
    #   ds[1][1]是目标句子长度
    dataset = tf.data.Dataset.zip((src_data, trg_data))

    # 删除内容为空(只包含<EOS>)的句子和长度过长的句子。
    def FilterLength(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        src_len_ok = tf.logical_and(
            tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN))
        trg_len_ok = tf.logical_and(
            tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
        return tf.logical_and(src_len_ok, trg_len_ok)
    dataset = dataset.filter(FilterLength)
    
    # 从图9-5可知,解码器需要两种格式的目标句子:
    #   1.解码器的输入(trg_input),形式如同"<sos> X Y Z"
    #   2.解码器的目标输出(trg_label),形式如同"X Y Z <eos>"
    # 上面从文件中读到的目标句子是"X Y Z <eos>"的形式,我们需要从中生成"<sos> X Y Z"
    # 形式并加入到Dataset中。
    def MakeTrgInput(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
        return ((src_input, src_len), (trg_input, trg_label, trg_len))
    dataset = dataset.map(MakeTrgInput)

    # 随机打乱训练数据。
    dataset = dataset.shuffle(10000)

    # 规定填充后输出的数据维度。
    padded_shapes = (
        (tf.TensorShape([None]),      # 源句子是长度未知的向量
         tf.TensorShape([])),         # 源句子长度是单个数字
        (tf.TensorShape([None]),      # 目标句子(解码器输入)是长度未知的向量
         tf.TensorShape([None]),      # 目标句子(解码器目标输出)是长度未知的向量
         tf.TensorShape([])))         # 目标句子长度是单个数字
    # 调用padded_batch方法进行batching操作。
    batched_dataset = dataset.padded_batch(batch_size, padded_shapes)
    return batched_dataset

 

9.3.3 Seq2Seq模型的代码实现

  • 使用一个双层的LSTM作为循环神经网络的主体,并且在Softmax层和词向量层之间共享参数。
  • 增加了一个循环神经网络作为编码器。
  • 使用Dataset动态读取数据,而不是直接将所有数据读入数据。
  • 每个batch完全独立。
  • 每训练200步将模型参数保存到一个checkpoint中。

3.定义翻译模型。

# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
           for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(NUM_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
           self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
           self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    # 在forward函数中定义模型的前向计算图。
    # src_input, src_size, trg_input, trg_label, trg_size分别是上面
    # MakeSrcTrgDataset函数产生的五种张量。
    def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
        batch_size = tf.shape(src_input)[0]
    
        # 将输入和输出单词编号转为词向量。
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)
        trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)
        
        # 在词向量上进行dropout。
        src_emb = tf.nn.dropout(src_emb, KEEP_PROB)
        trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB)

        # 使用dynamic_rnn构造编码器。
        # 编码器读取源句子每个位置的词向量,输出最后一步的隐藏状态enc_state。
        # 因为编码器是一个双层LSTM,因此enc_state是一个包含两个LSTMStateTuple类
        # 张量的tuple,每个LSTMStateTuple对应编码器中的一层。
        # enc_outputs是顶层LSTM在每一步的输出,它的维度是[batch_size, 
        # max_time, HIDDEN_SIZE]。Seq2Seq模型中不需要用到enc_outputs,而
        # 后面介绍的attention模型会用到它。
        with tf.variable_scope("encoder"):
            enc_outputs, enc_state = tf.nn.dynamic_rnn(
                self.enc_cell, src_emb, src_size, dtype=tf.float32)

        # 使用dyanmic_rnn构造解码器。
        # 解码器读取目标句子每个位置的词向量,输出的dec_outputs为每一步
        # 顶层LSTM的输出。dec_outputs的维度是 [batch_size, max_time,
        # HIDDEN_SIZE]。
        # initial_state=enc_state表示用编码器的输出来初始化第一步的隐藏状态。
        with tf.variable_scope("decoder"):
            dec_outputs, _ = tf.nn.dynamic_rnn(
                self.dec_cell, trg_emb, trg_size, initial_state=enc_state)

        # 计算解码器每一步的log perplexity。这一步与语言模型代码相同。
        output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
        logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
            labels=tf.reshape(trg_label, [-1]), logits=logits)

        # 在计算平均损失时,需要将填充位置的权重设置为0,以避免无效位置的预测干扰
        # 模型的训练。
        label_weights = tf.sequence_mask(
            trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32)
        label_weights = tf.reshape(label_weights, [-1])
        cost = tf.reduce_sum(loss * label_weights)
        cost_per_token = cost / tf.reduce_sum(label_weights)
        
        # 定义反向传播操作。反向操作的实现与语言模型代码相同。
        trainable_variables = tf.trainable_variables()

        # 控制梯度大小,定义优化方法和训练步骤。
        grads = tf.gradients(cost / tf.to_float(batch_size),
                             trainable_variables)
        grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NORM)
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        train_op = optimizer.apply_gradients(
            zip(grads, trainable_variables))
        return cost_per_token, train_op

4.训练过程和主函数。

# 使用给定的模型model上训练一个epoch,并返回全局步数。
# 每训练200步便保存一个checkpoint。
def run_epoch(session, cost_op, train_op, saver, step):
    # 训练一个epoch。
    # 重复训练步骤直至遍历完Dataset中所有数据。
    while True:
        try:
            # 运行train_op并计算损失值。训练数据在main()函数中以Dataset方式提供。
            cost, _ = session.run([cost_op, train_op])
            if step % 10 == 0:
                print("After %d steps, per token cost is %.3f" % (step, cost))
            # 每200步保存一个checkpoint。
            if step % 200 == 0:
                saver.save(session, CHECKPOINT_PATH, global_step=step)
            step += 1
        except tf.errors.OutOfRangeError:
            break
    return step

def main():
    # 定义初始化函数。
    initializer = tf.random_uniform_initializer(-0.05, 0.05)

    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None, 
                           initializer=initializer):
        train_model = NMTModel()
  
    # 定义输入数据。
    data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE)
    iterator = data.make_initializable_iterator()
    (src, src_size), (trg_input, trg_label, trg_size) = iterator.get_next()
 
    # 定义前向计算图。输入数据以张量形式提供给forward函数。
    cost_op, train_op = train_model.forward(src, src_size, trg_input,
                                            trg_label, trg_size)

    # 训练模型。
    saver = tf.train.Saver()
    step = 0
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(NUM_EPOCH):
            print("In iteration: %d" % (i + 1))
            sess.run(iterator.initializer)
            step = run_epoch(sess, cost_op, train_op, saver, step)
if __name__ == "__main__":
    main()

 

上面程序完成了机器翻译的训练步骤,下面讲解如何从checkpoint中读取模型并对一个新的句子翻译。对新输入的句子翻译称为解码(decoding)。

1.参数设置。

# 读取checkpoint的路径。9000表示是训练程序在第9000步保存的checkpoint。
CHECKPOINT_PATH = "./seq2seq_ckpt-9000"

# 模型参数。必须与训练时的模型参数保持一致。
HIDDEN_SIZE = 1024                         	# LSTM的隐藏层规模。
NUM_LAYERS = 2                             	# 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000                   	# 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                    	# 目标语言词汇表大小。
SHARE_EMB_AND_SOFTMAX = True            	# 在Softmax层和词向量层之间共享参数。

# 词汇表文件
SRC_VOCAB = "./en.vocab"
TRG_VOCAB = "./zh.vocab"

# 词汇表中<sos>和<eos>的ID。在解码过程中需要用<sos>作为第一步的输入,并将检查
# 是否是<eos>,因此需要知道这两个符号的ID。
SOS_ID = 1
EOS_ID = 2

2.定义NMT模型和解码步骤。

# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
           for _ in range(NUM_LAYERS)])
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(NUM_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
            self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
            self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    def inference(self, src_input):
        # 虽然输入只有一个句子,但因为dynamic_rnn要求输入是batch的形式,因此这里
        # 将输入句子整理为大小为1的batch。
        src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32)
        src_input = tf.convert_to_tensor([src_input], dtype=tf.int32)
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)

        # 使用dynamic_rnn构造编码器。这一步与训练时相同。
        with tf.variable_scope("encoder"):
            enc_outputs, enc_state = tf.nn.dynamic_rnn(
                self.enc_cell, src_emb, src_size, dtype=tf.float32)
   
        # 设置解码的最大步数。这是为了避免在极端情况出现无限循环的问题。
        MAX_DEC_LEN=100

        with tf.variable_scope("decoder/rnn/multi_rnn_cell"):
            # 使用一个变长的TensorArray来存储生成的句子。
            init_array = tf.TensorArray(dtype=tf.int32, size=0,
                dynamic_size=True, clear_after_read=False)
            # 填入第一个单词<sos>作为解码器的输入。
            init_array = init_array.write(0, SOS_ID)
            # 构建初始的循环状态。循环状态包含循环神经网络的隐藏状态,保存生成句子的
            # TensorArray,以及记录解码步数的一个整数step。
            init_loop_var = (enc_state, init_array, 0)

            # tf.while_loop的循环条件:
            # 循环直到解码器输出<eos>,或者达到最大步数为止。
            def continue_loop_condition(state, trg_ids, step):
                return tf.reduce_all(tf.logical_and(
                    tf.not_equal(trg_ids.read(step), EOS_ID),
                    tf.less(step, MAX_DEC_LEN-1)))

            def loop_body(state, trg_ids, step):
                # 读取最后一步输出的单词,并读取其词向量。
                trg_input = [trg_ids.read(step)]
                trg_emb = tf.nn.embedding_lookup(self.trg_embedding,
                                                 trg_input)
                # 这里不使用dynamic_rnn,而是直接调用dec_cell向前计算一步。
                dec_outputs, next_state = self.dec_cell.call(
                    state=state, inputs=trg_emb)
                # 计算每个可能的输出单词对应的logit,并选取logit值最大的单词作为
                # 这一步的而输出。
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight)
                          + self.softmax_bias)
                next_id = tf.argmax(logits, axis=1, output_type=tf.int32)
                # 将这一步输出的单词写入循环状态的trg_ids中。
                trg_ids = trg_ids.write(step+1, next_id[0])
                return next_state, trg_ids, step+1

            # 执行tf.while_loop,返回最终状态。
            state, trg_ids, step = tf.while_loop(
                continue_loop_condition, loop_body, init_loop_var)
            return trg_ids.stack()

3.翻译一个测试句子。

def main():
    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None):
        model = NMTModel()

    # 定义个测试句子。
    test_en_text = "This is a test . <eos>"
    print(test_en_text)
    
    # 根据英文词汇表,将测试句子转为单词ID。
    with codecs.open(SRC_VOCAB, "r", "utf-8") as f_vocab:
        src_vocab = [w.strip() for w in f_vocab.readlines()]
        src_id_dict = dict((src_vocab[x], x) for x in range(len(src_vocab)))
    test_en_ids = [(src_id_dict[token] if token in src_id_dict else src_id_dict['<unk>'])
                   for token in test_en_text.split()]
    print(test_en_ids)

    # 建立解码所需的计算图。
    output_op = model.inference(test_en_ids)
    sess = tf.Session()
    saver = tf.train.Saver()
    saver.restore(sess, CHECKPOINT_PATH)

    # 读取翻译结果。
    output_ids = sess.run(output_op)
    print(output_ids)
    
    # 根据中文词汇表,将翻译结果转换为中文文字。
    with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab:
        trg_vocab = [w.strip() for w in f_vocab.readlines()]
    output_text = ''.join([trg_vocab[x] for x in output_ids])
    
    # 输出翻译结果。
    print(output_text.encode('utf8').decode(sys.stdout.encoding))
    sess.close()

if __name__ == "__main__":
    main()
This is a test . <eos>
[90, 13, 9, 689, 4, 2]
INFO:tensorflow:Restoring parameters from ./seq2seq_ckpt-9000
[  1  10   7   9  12 411 271   6   2]
<sos> 这 是 一 个 测 试 。 <eos>

注意这里我们保存了多个步骤的参数,尝试用第1000步的ckpt来测试结果

This is a test . <eos>
[90, 13, 9, 689, 4, 2]
INFO:tensorflow:Restoring parameters from C:/Users/adward/Desktop/TensorFlow-Google/seq2seq_ckpt-1000
[  1  10  12   7   9  12 108 103  13 456   3  57 128   6   2]
<sos>这个是一个非常有趣的事情。<eos>

 

9.3.4 注意力机制

       编码器将完整句子压缩到一个维度固定的向量,然后解码器根据这个向量生成的句子,当句子较长时,这个中间点向量难以储存足够的信息,就称为这个模型的一个瓶颈。就像人在翻译时候,经常回头查阅原文某处,来提高这个片段的翻译精准度。

       引用一个实际的例子我觉得写得很好,如果拿机器翻译来解释这个分心模型的Encoder-Decoder框架更好理解,比如输入的是英文句子:Tom chase Jerry,Encoder-Decoder框架逐步生成中文单词:“汤姆”,“追逐”,“杰瑞”。在翻译“杰瑞”这个中文单词的时候,分心模型里面的每个英文单词对于翻译目标单词“杰瑞”贡献是相同的,很明显这里不太合理,显然“Jerry”对于翻译成“杰瑞”更重要,但是分心模型是无法体现这一点的,这就是为何说它没有引入注意力的原因。没有引入注意力的模型在输入句子比较短的时候问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因。出自Attention - 之二

这里有一篇个人觉得还不错 自然语言处理中的Attention机制总结

       解码器在解码时每一步将隐藏状态作为查询的输入来“查询”编码器的隐藏状态,在每个输入的位置计算一个反映与查询输入相关程度的权重,再根据这个权重对各输入位置的隐藏状态求加权平均。加权平均后的向量称为“context”,在解码下一个单词时,将context作为额外信息输入到循环神经网络中,这样循环神经网络能时刻读取原文中最相关的信息。

NEURAL MACHINE TRANSLATION BY JOINTLY LEARNING TO ALIGN AND TRANSLATE

这里的S_j是编码器在预测第j个单词时的状态,计算j时刻的context方法如下:

                                                                                  \alpha_{i,j}=\frac{exp(e(h_i,s_j))}{\sum _i exp(e(h_i,s_j))} \\ \/ \quad \quad context_j = \sum_i \alpha _{i,j}h_i

e(h_i,s_j)是计算原文中各单词与当前解码器状态的相关度函数

                                                                               e(h,s)=U tanh(Vh+Ws)

U,V,W是模型的参数,在计算第j步的context的向量,contex被加入j+1时刻作为循环层的输入。

  • 编码器采用了一个双向循环网络,因为在解码器通过注意力查询一个单词时候,也需要知道单词周围的信息。
  • 取消了编码器和解码器之间的连接。

 

训练模型代码

1.参数设置。

# 假设输入数据已经用9.2.1小节中的方法转换成了单词编号的格式。
SRC_TRAIN_DATA = "C:/Users/adward/Desktop/TensorFlow-Google/train.en"          # 源语言输入文件。
TRG_TRAIN_DATA = "C:/Users/adward/Desktop/TensorFlow-Google/train.zh"          # 目标语言输入文件。
CHECKPOINT_PATH = "C:/Users/adward/Desktop/TensorFlow-Google/attention_ckpt"   # checkpoint保存路径。  

HIDDEN_SIZE = 1024                     # LSTM的隐藏层规模。
DECODER_LAYERS = 2                     # 解码器中LSTM结构的层数。这个例子中编码器固定使用单层的双向LSTM。
SRC_VOCAB_SIZE = 10000                 # 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                  # 目标语言词汇表大小。
BATCH_SIZE = 100                       # 训练数据batch的大小。
NUM_EPOCH = 5                          # 使用训练数据的轮数。
KEEP_PROB = 0.8                        # 节点不被dropout的概率。
MAX_GRAD_NORM = 5                      # 用于控制梯度膨胀的梯度大小上限。
SHARE_EMB_AND_SOFTMAX = True           # 在Softmax层和词向量层之间共享参数。

MAX_LEN = 50   # 限定句子的最大单词数量。
SOS_ID  = 1    # 目标语言词汇表中<sos>的ID。

2.读取训练数据并创建Dataset。

# 使用Dataset从一个文件中读取一个语言的数据。
# 数据的格式为每行一句话,单词已经转化为单词编号。
def MakeDataset(file_path):
    dataset = tf.data.TextLineDataset(file_path)
    # 根据空格将单词编号切分开并放入一个一维向量。
    dataset = dataset.map(lambda string: tf.string_split([string]).values)
    # 将字符串形式的单词编号转化为整数。
    dataset = dataset.map(
        lambda string: tf.string_to_number(string, tf.int32))
    # 统计每个句子的单词数量,并与句子内容一起放入Dataset中。
    dataset = dataset.map(lambda x: (x, tf.size(x)))
    return dataset

# 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和
# batching操作。
def MakeSrcTrgDataset(src_path, trg_path, batch_size):
    # 首先分别读取源语言数据和目标语言数据。
    src_data = MakeDataset(src_path)
    trg_data = MakeDataset(trg_path)
    # 通过zip操作将两个Dataset合并为一个Dataset。现在每个Dataset中每一项数据ds
    # 由4个张量组成:
    #   ds[0][0]是源句子
    #   ds[0][1]是源句子长度
    #   ds[1][0]是目标句子
    #   ds[1][1]是目标句子长度
    dataset = tf.data.Dataset.zip((src_data, trg_data))

    # 删除内容为空(只包含<EOS>)的句子和长度过长的句子。
    def FilterLength(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        src_len_ok = tf.logical_and(
            tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN))
        trg_len_ok = tf.logical_and(
            tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
        return tf.logical_and(src_len_ok, trg_len_ok)
    dataset = dataset.filter(FilterLength)
    
    # 从图9-5可知,解码器需要两种格式的目标句子:
    #   1.解码器的输入(trg_input),形式如同"<sos> X Y Z"
    #   2.解码器的目标输出(trg_label),形式如同"X Y Z <eos>"
    # 上面从文件中读到的目标句子是"X Y Z <eos>"的形式,我们需要从中生成"<sos> X Y Z"
    # 形式并加入到Dataset中。
    def MakeTrgInput(src_tuple, trg_tuple):
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
        return ((src_input, src_len), (trg_input, trg_label, trg_len))
    dataset = dataset.map(MakeTrgInput)

    # 随机打乱训练数据。
    dataset = dataset.shuffle(10000)

    # 规定填充后输出的数据维度。
    padded_shapes = (
        (tf.TensorShape([None]),      # 源句子是长度未知的向量
         tf.TensorShape([])),         # 源句子长度是单个数字
        (tf.TensorShape([None]),      # 目标句子(解码器输入)是长度未知的向量
         tf.TensorShape([None]),      # 目标句子(解码器目标输出)是长度未知的向量
         tf.TensorShape([])))         # 目标句子长度是单个数字
    # 调用padded_batch方法进行batching操作。
    batched_dataset = dataset.padded_batch(batch_size, padded_shapes)
    return batched_dataset

前两步和之前的PTB模型相似。

3.定义翻译模型。

# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell_fw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.enc_cell_bw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(DECODER_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
           self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
           self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    # 在forward函数中定义模型的前向计算图。
    # src_input, src_size, trg_input, trg_label, trg_size分别是上面
    # MakeSrcTrgDataset函数产生的五种张量。
    def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
        batch_size = tf.shape(src_input)[0]
    
        # 将输入和输出单词编号转为词向量。
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)
        trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)
        
        # 在词向量上进行dropout。
        src_emb = tf.nn.dropout(src_emb, KEEP_PROB)
        trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB)

        # 使用dynamic_rnn构造编码器。
        # 编码器读取源句子每个位置的词向量,输出最后一步的隐藏状态enc_state。
        # 因为编码器是一个双层LSTM,因此enc_state是一个包含两个LSTMStateTuple类
        # 张量的tuple,每个LSTMStateTuple对应编码器中的一层。
        # 张量的维度是 [batch_size, HIDDEN_SIZE]。
        # enc_outputs是顶层LSTM在每一步的输出,它的维度是[batch_size, 
        # max_time, HIDDEN_SIZE]。Seq2Seq模型中不需要用到enc_outputs,而
        # 后面介绍的attention模型会用到它。
        # 下面的代码取代了Seq2Seq样例代码中forward函数里的相应部分。
        with tf.variable_scope("encoder"):
            # 构造编码器时,使用bidirectional_dynamic_rnn构造双向循环网络。
            # 双向循环网络的顶层输出enc_outputs是一个包含两个张量的tuple,每个张量的
            # 维度都是[batch_size, max_time, HIDDEN_SIZE],代表两个LSTM在每一步的输出。
            enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(
                self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size, 
                dtype=tf.float32)
            # 将两个LSTM的输出拼接为一个张量。
            enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1)     

        with tf.variable_scope("decoder"):
            # 选择注意力权重的计算模型。BahdanauAttention是使用一个隐藏层的前馈神经网络。
            # memory_sequence_length是一个维度为[batch_size]的张量,代表batch
            # 中每个句子的长度,Attention需要根据这个信息把填充位置的注意力权重设置为0。
            attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
                HIDDEN_SIZE, enc_outputs,
                memory_sequence_length=src_size)

            # 将解码器的循环神经网络self.dec_cell和注意力一起封装成更高层的循环神经网络。
            attention_cell = tf.contrib.seq2seq.AttentionWrapper(
                self.dec_cell, attention_mechanism,
                attention_layer_size=HIDDEN_SIZE)

            # 使用attention_cell和dynamic_rnn构造编码器。
            # 这里没有指定init_state,也就是没有使用编码器的输出来初始化输入,而完全依赖
            # 注意力作为信息来源。
            dec_outputs, _ = tf.nn.dynamic_rnn(
                attention_cell, trg_emb, trg_size, dtype=tf.float32)

        # 计算解码器每一步的log perplexity。这一步与语言模型代码相同。
        output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
        logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
            labels=tf.reshape(trg_label, [-1]), logits=logits)

        # 在计算平均损失时,需要将填充位置的权重设置为0,以避免无效位置的预测干扰
        # 模型的训练。
        label_weights = tf.sequence_mask(
            trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32)
        label_weights = tf.reshape(label_weights, [-1])
        cost = tf.reduce_sum(loss * label_weights)
        cost_per_token = cost / tf.reduce_sum(label_weights)
        
        # 定义反向传播操作。反向操作的实现与语言模型代码相同。
        trainable_variables = tf.trainable_variables()

        # 控制梯度大小,定义优化方法和训练步骤。
        grads = tf.gradients(cost / tf.to_float(batch_size),
                             trainable_variables)
        grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NORM)
        optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
        train_op = optimizer.apply_gradients(
            zip(grads, trainable_variables))
        return cost_per_token, train_op

 

训练完成后对模型进行测试

import tensorflow as tf
import codecs
import sys


# 读取checkpoint的路径。9000表示是训练程序在第9000步保存的checkpoint。
CHECKPOINT_PATH = "C:/Users/adward/Desktop/TensorFlow-Google/attention_ckpt-9000"

# 模型参数。必须与训练时的模型参数保持一致。
HIDDEN_SIZE = 1024                          # LSTM的隐藏层规模。
DECODER_LAYERS = 2                          # 解码器中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000                      # 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                       # 目标语言词汇表大小。
SHARE_EMB_AND_SOFTMAX = True                # 在Softmax层和词向量层之间共享参数。

# 词汇表文件
SRC_VOCAB = "C:/Users/adward/Desktop/TensorFlow-Google/en.vocab"
TRG_VOCAB = "C:/Users/adward/Desktop/TensorFlow-Google/zh.vocab"

# 词汇表中<sos>和<eos>的ID。在解码过程中需要用<sos>作为第一步的输入,并将检查
# 是否是<eos>,因此需要知道这两个符号的ID。
SOS_ID = 1
EOS_ID = 2
# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell_fw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.enc_cell_bw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE) 
           for _ in range(DECODER_LAYERS)])

        # 为源语言和目标语言分别定义词向量。   
        self.src_embedding = tf.get_variable(
            "src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
        self.trg_embedding = tf.get_variable(
            "trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])

        # 定义softmax层的变量
        if SHARE_EMB_AND_SOFTMAX:
            self.softmax_weight = tf.transpose(self.trg_embedding)
        else:
            self.softmax_weight = tf.get_variable(
               "weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
        self.softmax_bias = tf.get_variable(
            "softmax_bias", [TRG_VOCAB_SIZE])

    def inference(self, src_input):
        # 虽然输入只有一个句子,但因为dynamic_rnn要求输入是batch的形式,因此这里
        # 将输入句子整理为大小为1的batch。
        src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32)
        src_input = tf.convert_to_tensor([src_input], dtype=tf.int32)
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)

        with tf.variable_scope("encoder"):
            # 使用bidirectional_dynamic_rnn构造编码器。这一步与训练时相同。
            enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(
                self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size, 
                dtype=tf.float32)
            # 将两个LSTM的输出拼接为一个张量。
            enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1)    
        
        with tf.variable_scope("decoder"):
            # 定义解码器使用的注意力机制。
            attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
                HIDDEN_SIZE, enc_outputs,
                memory_sequence_length=src_size)

            # 将解码器的循环神经网络self.dec_cell和注意力一起封装成更高层的循环神经网络。
            attention_cell = tf.contrib.seq2seq.AttentionWrapper(
                self.dec_cell, attention_mechanism,
                attention_layer_size=HIDDEN_SIZE)
   
        # 设置解码的最大步数。这是为了避免在极端情况出现无限循环的问题。
        MAX_DEC_LEN=100

        with tf.variable_scope("decoder/rnn/attention_wrapper"):
            # 使用一个变长的TensorArray来存储生成的句子。
            init_array = tf.TensorArray(dtype=tf.int32, size=0,
                dynamic_size=True, clear_after_read=False)
            # 填入第一个单词<sos>作为解码器的输入。
            init_array = init_array.write(0, SOS_ID)
            # 调用attention_cell.zero_state构建初始的循环状态。循环状态包含
            # 循环神经网络的隐藏状态,保存生成句子的TensorArray,以及记录解码
            # 步数的一个整数step。
            init_loop_var = (
                attention_cell.zero_state(batch_size=1, dtype=tf.float32),
                init_array, 0)

            # tf.while_loop的循环条件:
            # 循环直到解码器输出<eos>,或者达到最大步数为止。
            def continue_loop_condition(state, trg_ids, step):
                return tf.reduce_all(tf.logical_and(
                    tf.not_equal(trg_ids.read(step), EOS_ID),
                    tf.less(step, MAX_DEC_LEN-1)))

            def loop_body(state, trg_ids, step):
                # 读取最后一步输出的单词,并读取其词向量。
                trg_input = [trg_ids.read(step)]
                trg_emb = tf.nn.embedding_lookup(self.trg_embedding,
                                                 trg_input)
                # 调用attention_cell向前计算一步。
                dec_outputs, next_state = attention_cell.call(
                    state=state, inputs=trg_emb)
                # 计算每个可能的输出单词对应的logit,并选取logit值最大的单词作为
                # 这一步的而输出。
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight)
                          + self.softmax_bias)
                next_id = tf.argmax(logits, axis=1, output_type=tf.int32)
                # 将这一步输出的单词写入循环状态的trg_ids中。
                trg_ids = trg_ids.write(step+1, next_id[0])
                return next_state, trg_ids, step+1

            # 执行tf.while_loop,返回最终状态。
            state, trg_ids, step = tf.while_loop(
                continue_loop_condition, loop_body, init_loop_var)
            return trg_ids.stack()

预测

def main():
    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None):
        model = NMTModel()

    # 定义个测试句子。
    test_en_text = "This is a test . <eos>"
    print(test_en_text)
    
    # 根据英文词汇表,将测试句子转为单词ID。
    with codecs.open(SRC_VOCAB, "r", "utf-8") as f_vocab:
        src_vocab = [w.strip() for w in f_vocab.readlines()]
        src_id_dict = dict((src_vocab[x], x) for x in range(len(src_vocab)))
    test_en_ids = [(src_id_dict[token] if token in src_id_dict else src_id_dict['<unk>'])
                   for token in test_en_text.split()]
    print(test_en_ids)

    # 建立解码所需的计算图。
    output_op = model.inference(test_en_ids)
    sess = tf.Session()
    saver = tf.train.Saver()
    saver.restore(sess, CHECKPOINT_PATH)

    # 读取翻译结果。
    output_ids = sess.run(output_op)
    print(output_ids)
    
    # 根据中文词汇表,将翻译结果转换为中文文字。
    with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab:
        trg_vocab = [w.strip() for w in f_vocab.readlines()]
    output_text = ''.join([trg_vocab[x] for x in output_ids])
    
    # 输出翻译结果。
    print(output_text.encode('utf8').decode(sys.stdout.encoding))
    sess.close()

if __name__ == "__main__":
    main()
This is a test . <eos>
[90, 13, 9, 689, 4, 2]
INFO:tensorflow:Restoring parameters from ./attention_ckpt-9000
[  1  10   7   9  12 411 271   6   2]
<sos>这是一个测试。<eos>
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值