(这里用于备份, 原文见 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}) $$
在这些条件概率里, 每一个词的概率需要考虑它前面的所有词. 而实际上, 相隔太远的两个词关联很弱.
马尔可夫假设是指,假定每个词出现的概率只跟它前面的少数几个词有关。比如,二阶马尔科夫假设只考虑前面两个词,相应的语言模型是三元模型。引入了马尔可夫假设的语言模型,也可以叫做马尔可夫模型。
有了马尔可夫模型,怎样生成句子?可按照以下三个步骤:
- 初始化 i = 1, 以及 $ x_0 = x_1 = * $ (*为句首标识符)
- 根据条件概率分布生成下一个词,循环执行直到 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
里的两个好用的工具: Counter
和 defaultdict
.
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 时,直接照搬来的原句也较多。这种趋势大致可概括为:从「火星文」到「鹦鹉学舌」。应该是语料规模太小,不足以支撑起一个具有泛化能力的语言模型。
参考:
- Michael Collins, Course notes for NLP, Chapter 1: language modelling
- 吴军,《数学之美》第3章:统计语言模型