NLP笔记(1) -- 基于规则的模型和基于概率的模型

写在前面

  • 最近在学习NLP的课程,深深感到自身的不足。同班的一位同学用微信公众号的方式对课程进行回顾,我打算向这位同学学习,将课程的感悟和遇到的问题整理起来,方便以后复习,也可以加深自己的理解,希望自己可以坚持下去。
  • 下面的代码,基本来自我的NLP课程作业,当然大部分都是模仿老师写的,使用Python完成,感兴趣的可以去我的github上面查看:https://github.com/LiuPineapple/Learning-NLP/tree/master/Assignments/lesson-01
  • 作者水平有限,如果有文章中有错误的地方,欢迎指正!如有侵权,请联系作者删除。

Rule Based Model(基于规则的模型)

  个人理解是,语言是有规则的,常见的诸如“主谓宾”、“定状补”都是一种语言规则。而我们要做的,就是给程序创建一个规则,在规则下增加一些语料,从而达到我们所谓的“让机器说话”的目的。

1.创建语法规则

hero = """
hero = 自我介绍 台词 询问
自我介绍 = 寒暄* 我是 名字 | 寒暄* 我是 外号
寒暄* = null | 寒暄 寒暄*
寒暄 = 你好, | 很高兴认识你, | 大唐欢迎你,
名字 = 李白。 | 钟馗。 | 李元芳。
外号 = 剑仙。 | 地府判官。 | 王都密探。
台词 = 大河之剑天上来! | 对付魑魅魍魉,乃是强迫症最佳疗法! | 密探的小本本上羞答答,人生太复杂!
询问 = 你是来跟我争天下第一的吗? | 你是什么鬼? | 你说的每一句话都将作为呈堂证供!"""

  由于笔者比较喜欢玩王者荣耀,所以做了一个王者荣耀英雄自我介绍的规则,整体规则是一个字符串,其中规则之间用空格分开,语料之间用“|”分开。现在的规则是无法直接用的,因为它是字符串的形式,我们定义一个 create_grammer()函数,将字符串形式的语法规则变成字典hero_grammer,方便以后使用。

def create_grammer(grammer_str,linesplit = '\n',split = '='):
    grammer = {}
    for line in grammer_str.split(linesplit):
        if  not line.strip(): continue
        exp,stmt = line.split(split)
        grammer[exp.strip()] = [s.split() for s in stmt.split('|')]#对于单个单词,s.split()也可以去掉空格
    return grammer
hero_grammer = create_grammer(hero)
hero_grammer

上段代码中有些地方需要注意:

  1. Python split()方法 https://www.runoob.com/python/att-string-split.html,返回一个列表。
  2. Python strip()方法 https://www.runoob.com/python/att-string-strip.html,返回一个字符串。
  3. 第四行,if not line.strip()里面if not后面如果跟的是空的,那么就相当于跟了一个False,非空则相当于跟了一个True。
    结果如下:
{'hero': [['自我介绍', '台词', '询问']],
 '自我介绍': [['寒暄*', '我是', '名字'], ['寒暄*', '我是', '外号']],
 '寒暄*': [['null'], ['寒暄', '寒暄*']],
 '寒暄': [['你好,'], ['很高兴认识你,'], ['大唐欢迎你,']],
 '名字': [['李白。'], ['钟馗。'], ['李元芳。']],
 '外号': [['剑仙。'], ['地府判官。'], ['王都密探。']],
 '台词': [['大河之剑天上来!'], ['对付魑魅魍魉,乃是强迫症最佳疗法!'], ['密探的小本本上羞答答,人生太复杂!']],
 '询问': [['你是来跟我争天下第一的吗?'], ['你是什么鬼?'], ['你说的每一句话都将作为呈堂证供!']]}

2.生成句子

import random
choice = random.choice
def generate(gram, target):
    if target not in gram: return target # means target is a terminal expression
    expaned = [generate(gram, t) for t in choice(gram[target])]
    return ''.join([e if e != '/n' else '\n' for e in expaned if e != 'null'])

上段代码中有些地方需要注意:

  1. Python choice() 函数 https://www.runoob.com/python/func-number-choice.html,返回一个随机项。
  2. Python join()方法https://www.runoob.com/python/att-string-join.html,返回一个字符串。
  3. 最后一行列表生成式的使用可以参考Python中列表生成式中的if和else
  4. 这段代码中函数有可能会不断调用自身,因此第四行非常重要,否则会无限循环下去。

结果如下:

generate(hero_grammer,'hero')
'大唐欢迎你,很高兴认识你,我是钟馗。密探的小本本上羞答答,人生太复杂!你是什么鬼?'

3.生成多句话

接下来,我们写一个generate_n()函数,使得同时生成多句话:

def generate_n(n,gram,target):
    for i in range(n):
        print(generate(gram,target))

同时生成5句话,结果如下:

generate_n(5,hero_grammer,'hero')
大唐欢迎你,很高兴认识你,大唐欢迎你,我是剑仙。大河之剑天上来!你说的每一句话都将作为呈堂证供!
我是李白。密探的小本本上羞答答,人生太复杂!你是来跟我争天下第一的吗?
大唐欢迎你,我是李元芳。大河之剑天上来!你说的每一句话都将作为呈堂证供!
很高兴认识你,我是王都密探。大河之剑天上来!你是什么鬼?
你好,我是李元芳。对付魑魅魍魉,乃是强迫症最佳疗法!你说的每一句话都将作为呈堂证供!

  使用上面写的代码,我们可以生成足够多的句子。但同时我们也可以发现,并不是每一句话的逻辑都通顺,那么如何判断到底哪个句子是更合适的呢?这时候就需要用到第二个模型了——Probability Based Model(基于概率的模型)。

Probability Based Model(基于概率的模型)

  个人理解是,我们首先选择一个语料库,接下来,我们计算每一个句子在语料库中出现的概率,认为其中概率最高的是最合理的句子,并作为最后输出的句子。那么如何去计算某一个句子出现的概率呢,我们需要引入N-Gram模型。

N-Gram模型

  我们知道句子是由一个又一个词构成的,假设某个句子s,是由按特定顺序排列的词w_1,w_2,w_3,...,w_m构成。那么就有:
P(s) = P(w_1w_2w_3...w_m) = P(w_1|w_2w_3...w_m)P(w_2|w_3...w_m)...P(w_{m-1}|w_m)P(w_m)
  但是这个概率并不容易计算,为了简化计算,我们这里引入马尔科夫假设,即假设一个词出现的概率,至于其后面的一个词有关,而与其他的词无关,也即是2-Gram模型:
P(s) = P(w_1w_2w_3...w_m) = P(w_1|w_2)P(w_2|w_3)...P(w_{m-1}|w_m)P(w_m)
  类似的,我们还可以得出3-Gram,4-Gram以及最简单的1-Gram模型等等,这里不再一一列举。

1.导入语料库

这里使用的语料库是一些电影评论,文件储存在我的电脑桌面上。

filename = r'C:\Users\Administrator\Desktop\movie_comments.csv'
import pandas as pd
data = pd.read_csv(filename,encoding = 'utf-8')
data.head()
图片1

上段代码中有些地方需要注意:

  1. DataFrame.head(n) 函数 ,取一个DataFrame的前n项,n默认为5。
  2. 编码形式需要自己试验得到合适的,这里是'utf-8'

2.语料处理

comment = data['comment'].tolist()
import re

def token(string):
    return re.findall('\w+', string)

comments_clean = [''.join(token(str(a))) for a in comment]
comments_clean[5]
'犯我中华者虽远必诛吴京比这句话还要意淫一百倍'

import jieba
def cut_string(string): return list(jieba.cut(string))
comment_words = [cut_string(i) for i in comments_clean]
Token = []
for i in range(len(comment_words)):
    Token += comment_words[i]
Token[500:510]
['感觉', '挺', '搞笑', '的', '战狼', '2', '里', '吴京', '这么', '能']

上段代码中有些地方需要注意:

  1. python tolist()方法,将数组或矩阵转化为列表。
  2. 正则表达式 re.findall 能够以列表的形式返回能匹配的子串,这里用于去除评论中的各种符号啊,可以理解为一种数据清洗。
  3. 这里使用jieba分词。jieba.cut(string)得到的是一个生成器(generator),要使用list()生成列表。
  4. 最后得到的Token 是一个包含原来的comment中所有词的列表。
from collections import Counter
words_count = Counter(Token)
words_count.most_common(10)
[('的', 328262),
 ('了', 102420),
 ('是', 73106),
 ('我', 50338),
 ('都', 36255),
 ('很', 34712),
 ('看', 34022),
 ('电影', 33675),
 ('也', 32065),
 ('和', 31290)]
TOKEN = [str(t) for t in Token]
TOKEN_2_GRAM = [''.join(TOKEN[i:i+2]) for i in range(len(TOKEN[:-1]))]
len(TOKEN)
4490313
len(TOKEN_2_GRAM)
4490312
TOKEN_2_GRAM[:10]
['吴京意淫', '意淫到', '到了', '了脑残', '脑残的', '的地步', '地步看', '看了', '了恶心', '恶心想']

上段代码中有些地方需要注意:

  1. collections是Python内建的一个集合模块,非常有用。
  2. Counter是一个计数器,Counter(Token)会生成一个字典,使用.most_common(n)选择元素出现频率最高的n个,这里是选取语料库中最常出现的10个词。
  3. TOKEN_2_GRAM是把原本相邻的两个词和在一起组成的新列表。
  4. ''.join(TOKEN[i:i+2])TOKEN[i]+TOKEN[i+1]在这里作用相同。

  接下来,我们定义两个函数,分别计算单个词在语料库中出现的概率和两个词相邻在语料库中出现的频率。如果某个词在语料库中不存在,我们就假定它的频率是1/语料库的长度。保证了每个词都有概率,防止后面概率相除时出现分母为0的情况。

words_count = Counter(TOKEN)
def prob_1(word):
    if word in TOKEN:
        return words_count[word]/len(TOKEN)
    else:
        return 1/len(TOKEN)
words_count_2 = Counter(TOKEN_2_GRAM)
def prob_2(word1,word2):
    if word1+word2 in TOKEN_2_GRAM:
        return words_count_2[word1+word2]/len(TOKEN_2_GRAM)
    else:
        return 1/len(TOKEN_2_GRAM)
prob_2('我们', '在')
2.137936072148216e-05

  下面我们定义函数,来计算某个句子出现的概率,这里使用了2-gram模型。

def get_probability(sentence):
    words = cut(sentence)
    sentence_prob = 1
    
    for i, word in enumerate(words[:-1]):
        next_word = words[i+1]
        probability_1 = prob_1(next_word)
        probability_2 = prob_2(word, next_word)
        
        sentence_prob *= (probability_2 / probability_1)
    sentence_prob *= probability_1
    return sentence_prob
get_probability('今天是个好日子')
1.700447371998775e-11

  现在万事俱备,我们已经有了生成句子的函数和判断句子合理性的函数,下面我们定义一个函数,来从多个句子中选择概率最高的那个。

def generate_best(grammer,target,linesplit,split,model,n): 
    sentences = [generate(create_grammer(grammer,linesplit,split),target) for i in range(n)]
    prob = [model(sentence) for sentence in sentences]
    sens = enumerate(prob)
    sens_sorted = sorted(sens,key=lambda x: x[1],reverse = True)
    return sentences[sens_sorted[0][0]]

上段代码中有些地方需要注意:

  1. 在Python中,万物皆对象,函数也可以作为另一个函数的参数输入。
  2. Python enumerate() 函数,https://www.runoob.com/python/python-func-enumerate.html
  3. sorted()返回一个排序后的副本,可以接收一个函数作为排序依据。

我们一次产生15个句子,选择最合理的,最后的输出结果如下:

generate_best(hero,'hero','\n','=',get_probability,15)
'我是李白。对付魑魅魍魉,乃是强迫症最佳疗法!你是什么鬼?'

感觉还可以,我们现在换一个语言规则:

host = """
host = 寒暄 报数 询问 业务相关 结尾 
报数 = 我是 数字 号 ,
数字 = 单个数字 | 数字 单个数字 
单个数字 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 
寒暄 = 称谓 打招呼 | 打招呼
称谓 = 人称 ,
人称 = 先生 | 女士 | 小朋友
打招呼 = 你好 | 您好 
询问 = 请问你要 | 您需要
业务相关 = 玩玩 具体业务
玩玩 = null
具体业务 = 喝酒 | 打牌 | 打猎 | 赌博
结尾 = 吗?
"""

生成10个句子,并选择最合理的句子如下:

generate_best(host,'host','\n','=',get_probability,10)
'女士,你好我是314号,您需要打牌吗?'

  感觉效果还可以,完成整个过程还是给作者带来了一定的满足感哈哈哈。当然,这个过程也存在缺陷,我认为,缺陷主要存在于以下两个方面:

  1. 2-gram模型的假设本身与实际情况有一定差异,只是一个简化假设。
  2. 选取判断句子合理性的语料库来自电影影评,判断出来的其实是最有可能出现在影评中的句子,可能会对判断结果造成偏差。

因此,选取更合理的假设与模型,增加其他方面的语料都是提升准确度的方法。

最后,欢迎大家访问我的GitHub查看更多代码:https://github.com/LiuPineapple
  欢迎大家访问我的简书主页查看更多文章:https://www.jianshu.com/u/31e8349bd083

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335