写在前面
- 最近在学习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
上段代码中有些地方需要注意:
- Python split()方法 https://www.runoob.com/python/att-string-split.html,返回一个列表。
- Python strip()方法 https://www.runoob.com/python/att-string-strip.html,返回一个字符串。
- 第四行,
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'])
上段代码中有些地方需要注意:
- Python choice() 函数 https://www.runoob.com/python/func-number-choice.html,返回一个随机项。
- Python join()方法https://www.runoob.com/python/att-string-join.html,返回一个字符串。
- 最后一行列表生成式的使用可以参考Python中列表生成式中的if和else
- 这段代码中函数有可能会不断调用自身,因此第四行非常重要,否则会无限循环下去。
结果如下:
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,是由按特定顺序排列的词构成。那么就有:
但是这个概率并不容易计算,为了简化计算,我们这里引入马尔科夫假设,即假设一个词出现的概率,至于其后面的一个词有关,而与其他的词无关,也即是2-Gram模型:
类似的,我们还可以得出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()
上段代码中有些地方需要注意:
- DataFrame.head(n) 函数 ,取一个DataFrame的前n项,n默认为5。
- 编码形式需要自己试验得到合适的,这里是'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', '里', '吴京', '这么', '能']
上段代码中有些地方需要注意:
- python tolist()方法,将数组或矩阵转化为列表。
- 正则表达式 re.findall 能够以列表的形式返回能匹配的子串,这里用于去除评论中的各种符号啊,可以理解为一种数据清洗。
- 这里使用jieba分词。
jieba.cut(string)
得到的是一个生成器(generator),要使用list()
生成列表。 - 最后得到的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]
['吴京意淫', '意淫到', '到了', '了脑残', '脑残的', '的地步', '地步看', '看了', '了恶心', '恶心想']
上段代码中有些地方需要注意:
- collections是Python内建的一个集合模块,非常有用。
- Counter是一个计数器,
Counter(Token)
会生成一个字典,使用.most_common(n)
选择元素出现频率最高的n个,这里是选取语料库中最常出现的10个词。 - TOKEN_2_GRAM是把原本相邻的两个词和在一起组成的新列表。
-
''.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]]
上段代码中有些地方需要注意:
- 在Python中,万物皆对象,函数也可以作为另一个函数的参数输入。
- Python enumerate() 函数,https://www.runoob.com/python/python-func-enumerate.html
-
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号,您需要打牌吗?'
感觉效果还可以,完成整个过程还是给作者带来了一定的满足感哈哈哈。当然,这个过程也存在缺陷,我认为,缺陷主要存在于以下两个方面:
- 2-gram模型的假设本身与实际情况有一定差异,只是一个简化假设。
- 选取判断句子合理性的语料库来自电影影评,判断出来的其实是最有可能出现在影评中的句子,可能会对判断结果造成偏差。
因此,选取更合理的假设与模型,增加其他方面的语料都是提升准确度的方法。
最后,欢迎大家访问我的GitHub查看更多代码:https://github.com/LiuPineapple
欢迎大家访问我的简书主页查看更多文章:https://www.jianshu.com/u/31e8349bd083