文章目录
- 学自贪心科技的贪心训练营
NLP训练营学习记录(二)
语言模型
Noisy Channel Model
噪声信道模型。我们都是到在信号传递的过程中会产生噪声,而噪声信道模型的作用是通过被噪声后的信号推导源信号。源信号可能有多个信号,选择其中概率最大的那个。
表达式:
p(sourece | noisy) = P(noisy | sourece) * P(sourece)
P(noisy | sourece) 是转换模型
P(sourece) 是语言模型
这个公式在上一文里的拼写纠错处有详细的推导与解释。
噪声信道模型使用场景:将一个信号源转换成文本的时候用。
- 语音识别
- 机器翻译
- 拼写纠错
- OCR
- 密码破解
Language Model
语言模型的作用:用来判断一句话是否是人话。
例如已经训练好的一个概率语言模型,那么模型就可以得到 P(今天是周日) > P(今天周日是)。也就是“今天是周日”这句话是人话的概率大于"今天周日是"的概率。
那么我们的目的就是构造这样一个LM,使得可以正确判断人话。
句子是由许多次组成的,而这些词并不是独立分布的,所以一个句子的概率就是组成它的词的联合分布概率。
Chain Rule(链式法则)
ChainRule 可以将联合分布用条件概率表示,链式法则是LM的数学基础。
P(A, B) = P(A | B) * P(B)
= P(B | A) * P(A)
# 四个词的句子的语言模型
P(A, B, C, D) = P(A) * P(B | A) * P(C | A, B) * P(D | A, B, C)
则这些条件概率就是模型的参数P(A) 、P(B | A)、 P(C | A, B)、 P(D | A, B, C)。而这里只是四个词,对于整个语料库的模型需要统计每个词的条件概率。使用链式法则作为语言模型缺点就是参数太多。
具体操作是,我们通过遍历语料库然后计算出来各个词的条件概率,存储起来。
那么怎么计算参数的条件概率呢?
例如,休息这个词的条件概率
P(休息 | 今天,是,春节,我们,都)
例如,语料库中有这样的句子:
[......
[今天是春节我们都休息],
......
[今天是春节我们都吃饺子],
......]
那么
P(休息 | 今天,是,春节,我们,都) = 1 / 2
“今天是春节我们都”频数为2,“今天是春节我们都休息”频数为1
越长的句子在语料库中出现的次数越少,这就会导致稀疏性增强。
链式法则作为语言模型的缺点:
- 参数太多
- 长句稀疏性太强
- 而词一旦稀疏,整个句子就意义了,因为其概率为0
马尔科夫假设(Markov Assumption)
马尔科夫假设作用:马尔科夫假设可以用来解决链式法则中长句条件概率稀疏性的问题。
马尔科夫假设为:一个词的出现与它邻近的词相关
与它邻近的一个词相关:first order markov assumption
P(休息 | 今天,是,春节,我们,都) ≈ P(休息 | 都)
与它邻近的两个词相关,second order markov assumption
P(休息 | 今天,是,春节,我们,都) ≈ P(休息 | 我们,都)
这样我们从原来语料库中需要找的一个长句 “今天是春节我们都休息”,变成只用找一个短句 “P(都休息)” 就可以表示这个参数。
而 “都休息” 这个短语在整个语料库中可能有非常多,所以可以解决稀疏性问题。
马尔科夫假设中与它邻近的词越多,则它的表示越不准确。
可以看到马尔科夫假设就是N-Gram的一个雏形。
将马尔科夫链应用于一个语言模型中:
first order markov assumption:
P(今天是春节我们都休息) = P(今天) * P(是 | 今天) * P(春节 | 是) * P(我们 | 春节)
* P(都 | 我们) * P(休息 | 都)
Language Model
链式法则和马尔科夫假设都是Gram语言模型的一个数学概念(也就是Gram = 链式法则+马尔科夫假设)。而在机器学习中将他们重新定义成Gram模型。
Unigram
Unigram 假设各个变量之间相互独立,
所以有以下形式:
P(今天,是,春节,我们,都,休息) = P(今天) * P(是) * P(春节) * P(我们) * P(都) * P(休息)
Unigram由于各个变量之间相互独立,则两句同词不同序的句子通过语言模型得到相同的结果。
Bigram
Bigram 就是 first order markov assumption。
P(今天是春节我们都休息) = P(今天) * P(是 | 今天) * P(春节 | 是) * P(我们 | 春节)
* P(都 | 我们) * P(休息 | 都)
N-gram
N-gram 认为一个词的出现与它前面n个词有关,当 n=1 时就是Bigram。
N一般为 3 模型就很不错了。
构造语言模型
以 Unigram 为例:
corpus = [["今天", "的", "天气", "很好", "啊"],
["我", "很", "想", "出去", "运动"],
["但", "今天", "上午", "有", "运动"],
["训练营", "明天", "才", "开始"]]
word_freq = {"今天": 2/19, "的": 1/19, "开始": 1 .......}
Model("今天开始训练营课程") = word_freq["今天"] * word_freq["开始"] * word_freq["训练营"] * word_freq["课程"] = 2/19
以 Bigram 为例:
corpus = [["明天", "是"],
["明天", "我们"],
["明天", "上课"],
["昨天", "是", "好天气"],
["昨天", "是"],
["明天", "天气"]]
word_freq = {"明天":4/13, "是":{"明天":1/3, "昨天":2/3}, "好天气":{"是":1}, .......}
Model("明天是好天气") = word_freq["明天"] * word_freq["是"]["明天"] * word_freq["好天气"]["是"] = 4/13 * 1/3 * 1 = 4/39
缺点:当输入中有语料库中没有的词的时候整个句子的概率为0。也就是句子无意义了。
解决方法:平滑化
平滑化 Smoothing
在语言模型中,如果碰到语料库中没有遇到过的词,那么其概率是0。使得整个句子的概率为0。但是在正常情况下我们不希望整个句子变得无意义。所以需对其进行平滑化,使没出现过的词无限接近于0但不为0。
Add-one Smoothing
也叫 Laplace Smoothing。
例如在之前我们使用 Bigram 时,一个参数为:
PMLE(Wi|Wi-1) = c(Wi-1, Wi) / c(Wi-1)
使用 Add-one 平滑化后,参数变为:
PAdd-1(Wi|Wi-1) = c(Wi-1, Wi) + 1 / c(Wi-1) + V
V为词典大小。
为什么分母要加V呢?
为了归一化,使得所有概率都在0-1之间,保证所有以wi-1为前提的词的概率和为1。这样才符合概率分布。
例如:词典库大小为17,在语料库中“今天上午”出现了两次,“今天”在整个语料库中也只有两次,在语料库中再没有以“今天”为前提出现的词了。
那么 P(上午|今天) = 3 / 19
而词典中的其他词 P 都等于 1 / 19,这样的词有多少个呢?有V-1个,16个,3/19 + 16/19 = 1。
加一个常量对于语言模型是不影响的。
Add-K Smoothing
使用 Add-K 平滑化后,参数变为:
PAdd-K(Wi|Wi-1) = c(Wi-1, Wi) + K / c(Wi-1) + K*V
怎么选择k值呢?使用优化的思路,将k看成模型的超参数。
将k放到验证集上计算困惑度。选择困惑度最小的那个k。
例如:k=1,然后跑一边验证集得到一个perplexity。然后k=2,查看当前perplexity是否小于当前最小困惑度。如果k长时间困惑度不减小,则停止。
Interpolation(插值法)
Interpolation 的核心思路:在计算Trigram的时候同时考虑Unigram和Bigram和Trigram出现的频次。
例如:在语料库中:
C(今天,天气) = 0
C(今天,使得) = 0
C(天气) = 3
C(今天) = 5
C(使得) = 0
P(天气|今天) = c(今天,天气) / c(今天) = 0
P(使得|今天) = c(今天,使得) / c(今天) = 0
从上述可以看到 “今天天气” 与 “今天使得” 的概率相同,但是在现实中真的相等吗?我们可以看到 “天气” 是有很多的,所以 “今天天气” 应该比 “今天使得” 要大。但是计算出来却是相等的。为了解决这个问题引入Interpolation。
Pinter(Wn|Wn-2, Wn-1) = lambda1 * P(Wn|Wn-2, Wn-1)
+ lambda2 * P(Wn|Wn-1)
+ lambda3 * P(Wn)
lambda1 + lambda2 + lambda3 = 1
也就是 Trigram 的结果是 Trigram、Bigram、Unigram三个值的加权平均。
Good-Turning Smoothing
MLE:MLE方法就是之前我们使用的,我们根据已知数据预测未来数据的一个分布情况。并不会根据未来数据变动现有概率分布。
- 没有出现过的单词的概率:PMLE = 0
- 解释:对于语料库中没有出现过的单词的概率,我们预测它未来不会出现,即0概率
- 出现过的单词的概率:PMLE = c / N
- 解释:对于语料库中出现过的单词的概率,我们预测下一个单词是它的概率为 c / N,c 为该单词的词频,N为总单词的词频
Good-Turning Smoothing:GT对于未出现的单词是有一个预测的,未来未出现的的单词的概率分布会影响现有的概率分布。
- 对没有出现过的单词的概率:PGT = N1 / N
- 解释:对于语料库中没有出现过的单词的概率,我们使用只出现过1词的词的概率和来近似没出现过的词的概率。N1:只出现过1次的词的总数,N:所有单词的词频
- 对于出现过的单词的概率:PGT = ((c+1) * Nc+1) / (Nc * N)
- 解释:对于语料库中出现过的单词的概率,因为要分配给没出现过的新单词概率,所有现有单词的概率分布变小了。c:该单词出现的频数,Nc+1:出现过c+1次的词的总数
Good-Turning的缺点:我们可以看出在GT中,当前出现过的单词的概率依赖于出现过c+1词的词数,但是当前单词出现的频数c越多,我们不一定有Nc+1的词。
对于上述没有出现过的词,我们可以使用机器学习去拟合,也就是以r为横坐标(r为出现r次的词)以Nr为纵坐标(Nr为出现r次的词数)
评估语言模型
使用困惑度评估语言模型:Perplexity = 2-(x)(x是average log likelihood)
x 越大模型越好。困惑度越小越好。
例如:
已经训练好的Bigram:
P(天气 | 今天) = 0.01
P(今天) = 0.002
P(很好 | 天气) = 0.1
P(适合 | 很好) = 0.01
P(出去 | 适合) = 0.02
P(运动 | 出去) = 0.1
测试集语料如下:
今日 天气 很好 适合 出去 运动
x = log(Model(test)).mean()
= log(P(今天)*P(天气|今天)*P(很好|天气)*P(适合|很好)*P(出去|适合)*P(运动|出去)).mean()
= log(P(今天))+log(P(天气|今天))+log(P(很好|天气))+log(P(适合|很好))+log(P(出去|适合))+log(P(运动|出去)) / 6
= log(0.002) - 2 - 1 - 2 + log(0.002) - 1
x 越大说明模型越适配语料。
Perplexity只是一种评估标准,在不同的场景下使用不同的评估标准。其他评估标准有召回率等
语言模型实现
import jieba
import json
import pandas as pd
from tqdm import tqdm
import re
from datetime import datetime
from collections import OrderedDict
import math
1 将自定义词库添加到jieba词库中
print("加载中文词库......")
time = datetime.now()
word_pd = pd.read_excel("Project1/data/综合类中文词库.xlsx", names=["word", "pos", "count"], header=None)
for index, row in tqdm(word_pd.iterrows()):
jieba.add_word(row["word"], freq=row["count"], tag=row["pos"])
print(word_pd.head())
print("加载完成,耗时 {}s".format((datetime.now()-time).seconds))
加载中文词库......
298032it [00:37, 7860.56it/s]
word pos count
0 酢 9 @ 237692
1 做做事 120 v 191456
2 做做饭 134 n 95350
3 做做 210 v 223109
4 做作 208 a 34124
加载完成,耗时 60s
2 加载停用词库
def get_stopwords(stop_path):
stop_words = []
with open(stop_path, "r", encoding="utf-8") as f:
while True:
line = f.readline()
stop_words.append(line.strip())
if not line:
break
return stop_words
stop_words = get_stopwords("StopWords/hit_stopwords.txt")
3 加载数据集
class MyData():
def __init__(self, data_path):
self.data_path = data_path
def __iter__(self):
self.file = open(self.data_path, "r", encoding="utf-8")
while True:
line = self.file.readline().strip()
if not line:
break
yield json.loads(line)
def __del__(self):
self.file.close()
def get_data(json_iter, stop_words):
for j_i in json_iter:
for content in j_i["conversation"]:
content = content.replace(" ", "")
content = re.sub("\[.*?\]", "", content)
yield [word for word in jieba.cut(content) if word not in stop_words]
data = MyData("Project1/data/train.txt")
data = get_data(data, stop_words)
['你好', '今天', '是', '几号', '了']
['你好', '今天', '是', '2018', '年', '1', '月', '18', '日']
['谢谢', '你', '我', '都', '给', '忙', '忘记', '了']
['哈哈', '我', '还', '知道', '今天', '是', '周杰伦', '的', '生日', '呢']
4 构建语言模型参数
# P(x) = {"x": count}
# P(y|x) = {"x": {"y": 1}}
# P(z|x, y) = {"x": {"y": {"z": 1}}}
# {"word1": {"count": 1, "word1作为前词word2": {"count": 2, "word1、word2作为前词word3": {"count": 3}}}}
def generate_model(data):
model, total = {}, 0
for line in data:
for index, word in enumerate(line):
total += 1
# 如果当前词不在模型中
if word not in model:
model[word] = {"count":0}
model[word]["count"] += 1
if index < len(line)-1:
# 如果前一个词不在{word:{}}中
if line[index + 1] not in model[word]:
model[word][line[index + 1]] = {"count": 0}
model[word][line[index + 1]]["count"] += 1
if index < len(line)-2:
if line[index + 2] not in model[word][line[index + 1]]:
model[word][line[index + 1]][line[index + 2]] = {"count": 0}
model[word][line[index + 1]][line[index + 2]]["count"] += 1
return model, total
param, total = generate_model(data)
with open("LanguageModelParam.json", "w") as f:
json.dump(param, f, indent=4)
5 Language Model
class Model():
def __init__(self, param, text_total):
self.param = param
self.V = jieba.dt.total
self.text_total = text_total
def get_param(self, word, next_word=None, nnext_word=None):
try:
if next_word == None and nnext_word == None:
return self.param[word]["count"]
elif next_word != None and nnext_word == None:
return self.param[word][next_word]["count"]
elif next_word != None and nnext_word != None:
return self.param[word][next_word][nnext_word]["count"]
except(KeyError):
return 0
def add_one_smoothing(self, c1, c2):
'''
P(x|y)
c1: yx出现的频数
c2: y出现的频数
'''
return (c1+1) / (c2 + self.V)
class UniGramModel(Model):
def __init__(self, param, text_total, stop_words):
super(UniGramModel, self).__init__(param, text_total)
self.stop_words = stop_words
def forward(self, sentence):
words = list(jieba.cut(sentence))
s = 0
for index, word in enumerate(words):
if word not in self.stop_words:
# print(word, self.get_param(word))
s += -math.log(self.get_param(word) / self.text_total)
return s
class BiGramModel(Model):
def __init__(self, param, text_total, stop_words):
super(BiGramModel, self).__init__(param, text_total)
self.stop_words = stop_words
def forward(self, sentence):
words = list(jieba.cut(sentence))
s = 0
for index, word in enumerate(words):
if word not in self.stop_words:
if index < 1:
s += -math.log(self.get_param(word) / self.text_total)
else:
s += -math.log(self.add_one_smoothing(self.get_param(words[index - 1], word), self.get_param(words[index - 1])))
return s
model = UniGramModel(param, total, stop_words)
print(model.forward("你好吗"))
print(model.forward("你吗好"))
11.086545186872108
12.605608555461487
model = BiGramModel(param, total, stop_words)
print(model.forward("你好吗"))
print(model.forward("你吗好"))
27.71904321227393
50.30684931780861