N-gram 语言模型

(这里用于备份, 原文见 https://sunoonlee.github.io/2017/03/ngram/ )

什么是语言模型

给定词表 V,一个句子可以看做词的序列 $x_1 x_2 … x_n$ ($x_i \in V$). 将句子出现的概率记为 $p(x_1, x_2, … x_n)$,这样一个联合概率分布就是语言模型。

语言的词表非常庞大,比如汉语的词表在10万量级。而上述联合分布有 $|V|^n$ 种取值, 这样的模型大而稀疏,不便于计算。为了使语言模型更紧凑,可以引入马尔可夫假设。

马尔可夫假设

首先将联合分布分解为条件概率的连乘:

$$ p(x_1, x_2, … x_n) $$
$$ = p(x_1) p(x_2|x_1) p(x_3|x_1, x_2) ... p(x_n|x_1, x_2, ..., x_{n-1}) $$

在这些条件概率里, 每一个词的概率需要考虑它前面的所有词. 而实际上, 相隔太远的两个词关联很弱.

马尔可夫假设是指,假定每个词出现的概率只跟它前面的少数几个词有关。比如,二阶马尔科夫假设只考虑前面两个词,相应的语言模型是三元模型。引入了马尔可夫假设的语言模型,也可以叫做马尔可夫模型。

有了马尔可夫模型,怎样生成句子?可按照以下三个步骤:

  1. 初始化 i = 1, 以及 $ x_0 = x_1 = * $ (*为句首标识符)
  2. 根据条件概率分布生成下一个词,循环执行直到 3
  3. 如果生成的词为句末标识符,就得到了一个句子

如何断句

断句问题在英文中有些麻烦 (见wikipedia),原因之一是 . 的意义模糊,除了作为句号,还可以是小数点或省略号,也可能出现在缩略词、URL、邮箱里。NLTK 里提供了英文断句工具。中文的断句相对轻松一些,句号基本没有歧义。

三元模型及其缺陷

三元模型虽然"linguistically naive", 但在实际中很有用。《数学之美》中说,当 N 从 3 开始继续增加时,语言模型的效果提升不显著,但资源耗费增加得很快;Google 的罗塞塔翻译系统和语音搜索系统使用的是四元模型,存储需要 500 台以上的服务器!(这是2014年数据)

三元模型的参数可以表示为 q(w|u,v)。我们需要依据语料样本进行参数估计, 比较自然的是极大似然估计:q(w|u,v) = c(u,v,w)/c(u,v),其中 c 表示样本中的出现次数。

语言模型从 N 元到三元,参数个数虽然已经大大减少,但仍然是个天文数字。按《数学之美》里的估计:汉语词汇量大致20万,那么三元模型的参数约有 $8\times 10^{15}$ 个,而互联网上的全部中文内容加起来只有 $10^{13}$ 量级。因此,采用极大似然估计的三元模型会遇到严重的问题:大部分的 q(w|u,v) 都会是零. 这是低估了大量罕见三元组的概率,因为它们虽然未在语料中出现,但实际中仍有可能出现。解决方法是对模型进行平滑化处理。

语言模型的平滑化

平滑化主要的原理大致可理解为,在必要的时候退化到低阶的模型,即一元、二元模型。常用的方法有两类,一是低阶与高阶的线性插值,二是对所有非零概率打折,匀出一小部分概率给未出现的词串。具体可见参考资料。

一个 N-gram 语言模型例子

接下来我们可以尝试一个简单的例子: 读取语料并建立 count-based n-gram 语言模型, 然后用它来生成句子. 完整代码见 Github. 这个例子没有做平滑化处理.

读取语料并进行预处理

选择《张爱玲作品集》作为语料输入,约 250 万字。对语料进行这样的预处理:

  • 。!? 作为断句符号. 基于断句结果, 在句首加入自定义 token, 这样语言模型可以学习到如何生成句首词.
  • 忽略部分类型的符号,包括\n、空格、引号、书名号等。
    • 之所以忽略引号、书名号、括号等成对符号,是担心生成语句时容易出现不配对的符号,看起来会很奇怪(后来试验的确如此)。
    • 忽略引号的另一个原因是,右引号常常放在上一条提到的断句符号之后,这种情况下,右引号实际上成为句末,处理这种情况会增加复杂性。

统计 ngram 出现次数及概率

这里可以使用 Python 标准库 collections 里的两个好用的工具: Counterdefaultdict.

def count_ngram(segments, N):
    """统计 N-gram 出现次数"""
    dct = defaultdict(Counter)
    for i in range(N-1, len(segments)):
        context = tuple(segments[i-N+1:i])
        word = segments[i]
        dct[context][word] += 1
    return dct

统计了出现次数后,可以将其除以相应 context 的总次数,得到估计的条件概率 P(word|context)。至此就得到了一个 count based N-gram 语言模型。

def to_prob(dct):
    """将次数字典转换为概率字典"""
    prob_dct = dct.copy()
    for context, count in prob_dct.items():
        total = sum(count.values())
        for word in count:
            count[word] /= total  # works in Python 3
    return prob_dct

用语言模型试着生成句子

生成单个词的方法,是用一个累计的概率去跟随机数 r 比较,直到这个概率大于 r 为止。

def generate_word(context):
    """根据 context 及条件概率,随机生成 word"""
    r = random()
    psum = 0
    for word, prob in prob_dct[context].items():
        psum += prob
        if psum > r:
            return word

生成句子的时候,每个句子需要预先设好最初的 context,好让程序能生成句首词,然后逐步平移 context 即可。

生成效果随 N 的变化

N 取 2~5 时生成的句子样例分别见 Github 目录 下的 generated_Ngram.txt.

可以看出,N=2 时句子基本是混乱的, N 变大时句子更为通顺,但 N>3 时,直接照搬来的原句也较多。这种趋势大致可概括为:从「火星文」到「鹦鹉学舌」。应该是语料规模太小,不足以支撑起一个具有泛化能力的语言模型。


参考:

  1. Michael Collins, Course notes for NLP, Chapter 1: language modelling
  2. 吴军,《数学之美》第3章:统计语言模型

你可能感兴趣的:(N-gram 语言模型)