整个NLP重新学习(二)

  • 学自贪心科技的贪心训练营

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
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值