小编说:语言模型是自然语言处理问题中一类最基本的问题,它有着非常广泛的应用,也是理解后文中更加复杂的自然语言处理问题的基础。本文介绍了语言模型的基本概念以及介绍评价语言模型好坏的标准,并给出了使用TensorFlow实现该损失函数的具体代码。 本文选自《TensorFlow:实战Google深度学习框架(第2版)》
- 1 语言模型简介
假设一门语言中所有可能的句子服从某一个概率分布,每个句子出现的概率加起来为1,那么“语言模型”的任务就是预测每个句子在语言中出现的概率。对于语言中常见的句子,一个好的语言模型应得出相对较高的概率;而对于不合语法的句子,计算出的概率则应接近于零。把句子看成单词的序列,语言模型可以表示为一个计算p(w1,w2,w3,…,wm)的模型。语言模型仅仅对句子出现的概率进行建模,并不尝试去“理解”句子的内容含义。比如说,语言模型能告诉我们什么样的句子是常用句子,但无法告诉我们两句话的意思是否相似或者相反。
语言模型有很多应用。很多生成自然语言文本的应用都依赖语言模型来优化输出文本的流畅性。生成的句子在语言模型中的概率越高,说明其越有可能是一个流畅、自然的句子。例如在输入法中,假设输入的拼音串为“xianzaiquna”,输出可能是“西安在去哪”,也可能是“现在去哪”,这时输入法就利用语言模型比较两个输出的概率,得出“现在去哪”更有可能是用户所需要的输出。在统计机器翻译的噪声信道模型(Noisy Channel Model)中,每个候选翻译的概率由一个翻译模型和一个语言模型共同决定,其中的语言模型就起到了在目标语言中挑选较为合理的句子的作用。在《TensorFlow(第2版)》9.3小节中将看到,神经网络机器翻译的Seq2Seq模型可以看作是一个条件语言模型(Conditional Language Model),它相当于是在给定输入的情况下对目标语言的所有句子估算概率,并选择其中概率最大的句子作为输出。
那么如何计算一个句子的概率呢?首先一个句子可以被看成是一个单词序列:
其中m为句子的长度。那么,它的概率可以表示为:
p(wm|w1,w2,w3,…,wm-1)表示,已知前m-1个单词时,第m个单词为wm的条件概率。如果能对这一项建模,那么只要把每个位置的条件概率相乘,就能计算一个句子出现的概率。然而一般来说,任何一门语言的词汇量都很大,词汇的组合更是不计其数。假设一门语言的词汇量为V,如果要将p(wm|w1,w2,w3,…,wm-1)的所有参数保存在一个模型里,将需要Vm个参数,一般的句子长度远远超出了实际可行的范围。为了估计这些参数的取值,常见的方法有n-gram模型、决策树、最大熵模型、条件随机场、神经网络语言模型等。这里先以其中最简单的n-gram模型来介绍语言模型问题。为了控制参数数量,n-gram模型做了一个有限历史假设:当前单词的出现概率仅仅与前面的n-1个单词相关,因此以上公式可以近似为:
n-gram模型里的n指的是当前单词依赖它前面的单词的个数。通常n可以取1、2、3、4,其中n取1、2、3时分别称为unigram、bigram和trigram。n-gram模型中需要估计的参数为条件概率:
假设某种语言的单词表大小为V,那么n-gram模型需要估计的不同参数数量为O(Vn)量级。当n越大时,n-gram模型在理论上越准确,但也越复杂,需要的计算量和训练语料数据量也就越大,因此n取≥4的情况非常少。
n-gram模型的参数一般采用最大似然估计(Maximum Likelihood Estimation,MLE)方法计算:
其中C(X)表示单词序列X在训练语料中出现的次数。训练语料的规模越大,参数估计的结果越可靠。但即使训练数据的规模非常大时,还是有很多单词序列在训练语料中不会出现,这就会导致很多参数为0。举例来说,IBM使用了366M英语语料训练trigram,发现14.7%的trigram和2.2%的bigram在训练中没有出现。为了避免因为乘以0而导致整个句概率为0,使用最大似然估计方法时需要加入平滑避免参数取值为0。使用n-gram建立语言模型的细节不再详细介绍,感兴趣的读者推荐阅读Michael Collins的讲义 。
2 语言模型的评价方法
语言模型效果好坏的常用评价指标是复杂度(perplexity)。在一个测试集上得到的perplexity越低,说明建模的效果越好。计算perplexity值的公式如下:
简单来说,perplexity值刻画的是语言模型预测一个语言样本的能力。比如已经知道(w1,w2,w3,…,wm)这句话会出现在语料库之中,那么通过语言模型计算得到的这句话的概率越高,说明语言模型对这个语料库拟合得越好。
从上面的定义中可以看出,perplexity实际是计算每一个单词得到的概率倒数的几何平均,因此perplexity可以理解为平均分支系数(average branching factor),即模型预测下一个词时的平均可选择数量。例如,考虑一个由0~9这10个数字随机组成的长度为m的序列,由于这10个数字出现的概率是随机的,所以每个数字出现的概率是 。因此,在任意时刻,模型都有10个等概率的候选答案可以选择,于是perplexity就是10(有10个合理的答案)。perplexity的计算过程如下:
目前在PTB(Penn Tree Bank)数据集上最好的语言模型perplexity为47.7 ,也就是说,平均情况下,该模型预测下一个词时,有47.7个词等可能地可以作为下一个词的合理选择。
在语言模型的训练中,通常采用perplexity的对数表达形式:
相比乘积求平方根的方式,使用加法的形式可以加速计算,同时避免概率乘积数值过小而导致浮点数向下溢出的问题。
在数学上,log perplexity可以看成真实分布与预测分布之间的交叉熵(Cross Entropy)。交叉熵描述了两个概率分布之间的一种距离。假设x是一个离散变量,u(x)和v(x)是两个与x相关的概率分布,那么u和v之间交叉熵的定义是在分布u下-log(v(x))的期望值:
把x看作单词,u(x)为每个位置上单词的真实分布,v(x)为模型的预测分布p(wi|w1,…,wi-1),就可以看出log perplexity和交叉熵是等价的。唯一的区别在于,由于语言的真实分布是未知的,因此在log perplexity的定义中,真实分布用测试语料中的取样代替,即认为在给定上文w1,w2,…,wi-1的条件下,语料中出现单词wi的概率为1,出现其他单词的概率均为0。
在神经网络模型中,p(wi|w1,…,wi-1)分布通常是由一个softmax层产生的,这时TensorFlow中提供了两个方便计算交叉熵的函数:tf.nn.softmax_cross_entropy_with_logits和tf.nn.sparse_softmax_cross_entropy_with_logits。两个函数之间的区别可以看下面的例子。
代码语言:javascript复制# 假设词汇表的大小为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的结果是[0.32656264, 0.46436879], 这对应两个预测的
# perplexity损失。
sess = tf.Session()
sess.run(loss)
# 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)
# 运行结果与上面相同:[ 0.32656264, 0.46436879]
sess.run(loss)
# 由于softmax_cross_entropy_with_logits允许提供一个概率分布,因此在使用时有更大
# 的自由度。举个例子,一种叫label smoothing的技巧是将正确数据的概率设为一个比1.0
# 略小的值,将错误数据的概率设为比0.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)
# 运行结果为[ 0.37656265, 0.48936883]
sess.run(loss)