寫在前面
- 最近在學(xué)習(xí)NLP的課程,深深感到自身的不足宝与。同班的一位同學(xué)用微信公眾號的方式對課程進(jìn)行回顧冶匹,我打算向這位同學(xué)學(xué)習(xí),將課程的感悟和遇到的問題整理起來嚼隘,方便以后復(fù)習(xí),也可以加深自己的理解飞蛹,希望自己可以堅(jiān)持下去。
- 下面的代碼桩皿,基本來自我的NLP課程作業(yè),當(dāng)然大部分都是模仿老師寫的泄隔,使用Python完成,感興趣的可以去我的github上面查看:https://github.com/LiuPineapple/Learning-NLP/tree/master/Assignments/lesson-01
- 作者水平有限佛嬉,如果有文章中有錯誤的地方,歡迎指正暖呕!如有侵權(quán),請聯(lián)系作者刪除湾揽。
Rule Based Model(基于規(guī)則的模型)
??個人理解是,語言是有規(guī)則的库物,常見的諸如“主謂賓”贷帮、“定狀補(bǔ)”都是一種語言規(guī)則戚揭。而我們要做的撵枢,就是給程序創(chuàng)建一個規(guī)則,在規(guī)則下增加一些語料锄禽,從而達(dá)到我們所謂的“讓機(jī)器說話”的目的。
1.創(chuàng)建語法規(guī)則
hero = """
hero = 自我介紹 臺詞 詢問
自我介紹 = 寒暄* 我是 名字 | 寒暄* 我是 外號
寒暄* = null | 寒暄 寒暄*
寒暄 = 你好沃但, | 很高興認(rèn)識你, | 大唐歡迎你绽慈,
名字 = 李白辈毯。 | 鐘馗。 | 李元芳谆沃。
外號 = 劍仙。 | 地府判官唁影。 | 王都密探。
臺詞 = 大河之劍天上來据沈! | 對付魑魅魍魎,乃是強(qiáng)迫癥最佳療法锌介! | 密探的小本本上羞答答,人生太復(fù)雜孔祸!
詢問 = 你是來跟我爭天下第一的嗎? | 你是什么鬼崔慧? | 你說的每一句話都將作為呈堂證供穴墅!"""
??由于筆者比較喜歡玩王者榮耀惶室,所以做了一個王者榮耀英雄自我介紹的規(guī)則玄货,整體規(guī)則是一個字符串拇涤,其中規(guī)則之間用空格分開誉结,語料之間用“|”分開∪辏現(xiàn)在的規(guī)則是無法直接用的惩坑,因?yàn)樗亲址男问揭舶荩覀兌x一個 create_grammer()
函數(shù),將字符串形式的語法規(guī)則變成字典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
后面如果跟的是空的臀玄,那么就相當(dāng)于跟了一個False,非空則相當(dāng)于跟了一個True健无。
結(jié)果如下:
{'hero': [['自我介紹', '臺詞', '詢問']],
'自我介紹': [['寒暄*', '我是', '名字'], ['寒暄*', '我是', '外號']],
'寒暄*': [['null'], ['寒暄', '寒暄*']],
'寒暄': [['你好,'], ['很高興認(rèn)識你睬涧,'], ['大唐歡迎你,']],
'名字': [['李白畦浓。'], ['鐘馗。'], ['李元芳讶请。']],
'外號': [['劍仙。'], ['地府判官夺溢。'], ['王都密探。']],
'臺詞': [['大河之劍天上來风响!'], ['對付魑魅魍魎,乃是強(qiáng)迫癥最佳療法状勤!'], ['密探的小本本上羞答答,人生太復(fù)雜持搜!']],
'詢問': [['你是來跟我爭天下第一的嗎?'], ['你是什么鬼葫盼?'], ['你說的每一句話都將作為呈堂證供!']]}
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() 函數(shù) https://www.runoob.com/python/func-number-choice.html,返回一個隨機(jī)項(xiàng)蟆盹。
- Python join()方法https://www.runoob.com/python/att-string-join.html,返回一個字符串日缨。
- 最后一行列表生成式的使用可以參考Python中列表生成式中的if和else
- 這段代碼中函數(shù)有可能會不斷調(diào)用自身,因此第四行非常重要匣距,否則會無限循環(huán)下去。
結(jié)果如下:
generate(hero_grammer,'hero')
'大唐歡迎你毅待,很高興認(rèn)識你归榕,我是鐘馗尸红。密探的小本本上羞答答刹泄,人生太復(fù)雜!你是什么鬼特石?'
3.生成多句話
接下來,我們寫一個generate_n()
函數(shù)姆蘸,使得同時生成多句話:
def generate_n(n,gram,target):
for i in range(n):
print(generate(gram,target))
同時生成5句話,結(jié)果如下:
generate_n(5,hero_grammer,'hero')
大唐歡迎你逞敷,很高興認(rèn)識你,大唐歡迎你推捐,我是劍仙。大河之劍天上來牛柒!你說的每一句話都將作為呈堂證供!
我是李白焰络。密探的小本本上羞答答符喝,人生太復(fù)雜闪彼!你是來跟我爭天下第一的嗎?
大唐歡迎你畏腕,我是李元芳。大河之劍天上來!你說的每一句話都將作為呈堂證供削解!
很高興認(rèn)識你,我是王都密探铭污。大河之劍天上來!你是什么鬼嘹狞?
你好,我是李元芳磅网。對付魑魅魍魎,乃是強(qiáng)迫癥最佳療法簸喂!你說的每一句話都將作為呈堂證供!
??使用上面寫的代碼喻鳄,我們可以生成足夠多的句子跟啤。但同時我們也可以發(fā)現(xiàn)诽表,并不是每一句話的邏輯都通順隅肥,那么如何判斷到底哪個句子是更合適的呢?這時候就需要用到第二個模型了——Probability Based Model(基于概率的模型)腥放。
Probability Based Model(基于概率的模型)
??個人理解是,我們首先選擇一個語料庫秃症,接下來,我們計(jì)算每一個句子在語料庫中出現(xiàn)的概率岗仑,認(rèn)為其中概率最高的是最合理的句子,并作為最后輸出的句子荠雕。那么如何去計(jì)算某一個句子出現(xiàn)的概率呢稳其,我們需要引入N-Gram模型炸卑。
N-Gram模型
??我們知道句子是由一個又一個詞構(gòu)成的,假設(shè)某個句子s盖文,是由按特定順序排列的詞構(gòu)成。那么就有:
??但是這個概率并不容易計(jì)算洒敏,為了簡化計(jì)算返帕,我們這里引入馬爾科夫假設(shè)桐玻,即假設(shè)一個詞出現(xiàn)的概率荆萤,至于其后面的一個詞有關(guān),而與其他的詞無關(guān)链韭,也即是2-Gram模型:
??類似的,我們還可以得出3-Gram敞峭,4-Gram以及最簡單的1-Gram模型等等,這里不再一一列舉殖蚕。
1.導(dǎo)入語料庫
這里使用的語料庫是一些電影評論,文件儲存在我的電腦桌面上睦疫。
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) 函數(shù) ,取一個DataFrame的前n項(xiàng)鞭呕,n默認(rèn)為5。
- 編碼形式需要自己試驗(yàn)得到合適的葫松,這里是'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]
'犯我中華者雖遠(yuǎn)必誅吳京比這句話還要意淫一百倍'
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]
['感覺', '挺', '搞笑', '的', '戰(zhàn)狼', '2', '里', '吳京', '這么', '能']
上段代碼中有些地方需要注意:
- python tolist()方法,將數(shù)組或矩陣轉(zhuǎn)化為列表咕娄。
- 正則表達(dá)式 re.findall 能夠以列表的形式返回能匹配的子串,這里用于去除評論中的各種符號啊,可以理解為一種數(shù)據(jù)清洗圣勒。
- 這里使用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內(nèi)建的一個集合模塊庇绽,非常有用。
- Counter是一個計(jì)數(shù)器瞧掺,
Counter(Token)
會生成一個字典,使用.most_common(n)
選擇元素出現(xiàn)頻率最高的n個,這里是選取語料庫中最常出現(xiàn)的10個詞肠缔。 - TOKEN_2_GRAM是把原本相鄰的兩個詞和在一起組成的新列表。
-
''.join(TOKEN[i:i+2])
和TOKEN[i]+TOKEN[i+1]
在這里作用相同明未。
??接下來壹蔓,我們定義兩個函數(shù)趟妥,分別計(jì)算單個詞在語料庫中出現(xiàn)的概率和兩個詞相鄰在語料庫中出現(xiàn)的頻率佣蓉。如果某個詞在語料庫中不存在,我們就假定它的頻率是1/語料庫的長度
勇凭。保證了每個詞都有概率,防止后面概率相除時出現(xiàn)分母為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
??下面我們定義函數(shù),來計(jì)算某個句子出現(xiàn)的概率贞让,這里使用了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
??現(xiàn)在萬事俱備喳张,我們已經(jīng)有了生成句子的函數(shù)和判斷句子合理性的函數(shù)美澳,下面我們定義一個函數(shù)摸航,來從多個句子中選擇概率最高的那個。
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中酱虎,萬物皆對象擂涛,函數(shù)也可以作為另一個函數(shù)的參數(shù)輸入读串。
- Python enumerate() 函數(shù)撒妈,https://www.runoob.com/python/python-func-enumerate.html
-
sorted()
返回一個排序后的副本,可以接收一個函數(shù)作為排序依據(jù)狰右。
我們一次產(chǎn)生15個句子,選擇最合理的棋蚌,最后的輸出結(jié)果如下:
generate_best(hero,'hero','\n','=',get_probability,15)
'我是李白。對付魑魅魍魎脱拼,乃是強(qiáng)迫癥最佳療法!你是什么鬼熄浓?'
感覺還可以省撑,我們現(xiàn)在換一個語言規(guī)則:
host = """
host = 寒暄 報(bào)數(shù) 詢問 業(yè)務(wù)相關(guān) 結(jié)尾
報(bào)數(shù) = 我是 數(shù)字 號 ,
數(shù)字 = 單個數(shù)字 | 數(shù)字 單個數(shù)字
單個數(shù)字 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
寒暄 = 稱謂 打招呼 | 打招呼
稱謂 = 人稱 ,
人稱 = 先生 | 女士 | 小朋友
打招呼 = 你好 | 您好
詢問 = 請問你要 | 您需要
業(yè)務(wù)相關(guān) = 玩玩 具體業(yè)務(wù)
玩玩 = null
具體業(yè)務(wù) = 喝酒 | 打牌 | 打獵 | 賭博
結(jié)尾 = 嗎赌蔑?
"""
生成10個句子竟秫,并選擇最合理的句子如下:
generate_best(host,'host','\n','=',get_probability,10)
'女士,你好我是314號,您需要打牌嗎?'
??感覺效果還可以肥败,完成整個過程還是給作者帶來了一定的滿足感哈哈哈。當(dāng)然馒稍,這個過程也存在缺陷,我認(rèn)為纽谒,缺陷主要存在于以下兩個方面:
- 2-gram模型的假設(shè)本身與實(shí)際情況有一定差異,只是一個簡化假設(shè)鼓黔。
- 選取判斷句子合理性的語料庫來自電影影評不见,判斷出來的其實(shí)是最有可能出現(xiàn)在影評中的句子崔步,可能會對判斷結(jié)果造成偏差。
因此井濒,選取更合理的假設(shè)與模型,增加其他方面的語料都是提升準(zhǔn)確度的方法眼虱。
最后席纽,歡迎大家訪問我的GitHub查看更多代碼:https://github.com/LiuPineapple
??歡迎大家訪問我的簡書主頁查看更多文章:http://www.reibang.com/u/31e8349bd083