NLP筆記(1) -- 基于規(guī)則的模型和基于概率的模型

寫(xiě)在前面

  • 最近在學(xué)習(xí)NLP的課程,深深感到自身的不足。同班的一位同學(xué)用微信公眾號(hào)的方式對(duì)課程進(jìn)行回顧,我打算向這位同學(xué)學(xué)習(xí),將課程的感悟和遇到的問(wèn)題整理起來(lái),方便以后復(fù)習(xí),也可以加深自己的理解,希望自己可以堅(jiān)持下去。
  • 下面的代碼,基本來(lái)自我的NLP課程作業(yè),當(dāng)然大部分都是模仿老師寫(xiě)的,使用Python完成,感興趣的可以去我的github上面查看:https://github.com/LiuPineapple/Learning-NLP/tree/master/Assignments/lesson-01
  • 作者水平有限,如果有文章中有錯(cuò)誤的地方,歡迎指正!如有侵權(quán),請(qǐng)聯(lián)系作者刪除。

Rule Based Model(基于規(guī)則的模型)

??個(gè)人理解是,語(yǔ)言是有規(guī)則的,常見(jiàn)的諸如“主謂賓”、“定狀補(bǔ)”都是一種語(yǔ)言規(guī)則。而我們要做的,就是給程序創(chuàng)建一個(gè)規(guī)則,在規(guī)則下增加一些語(yǔ)料,從而達(dá)到我們所謂的“讓機(jī)器說(shuō)話(huà)”的目的。

1.創(chuàng)建語(yǔ)法規(guī)則

hero = """
hero = 自我介紹 臺(tái)詞 詢(xún)問(wèn)
自我介紹 = 寒暄* 我是 名字 | 寒暄* 我是 外號(hào)
寒暄* = null | 寒暄 寒暄*
寒暄 = 你好, | 很高興認(rèn)識(shí)你, | 大唐歡迎你,
名字 = 李白。 | 鐘馗。 | 李元芳。
外號(hào) = 劍仙。 | 地府判官。 | 王都密探。
臺(tái)詞 = 大河之劍天上來(lái)! | 對(duì)付魑魅魍魎,乃是強(qiáng)迫癥最佳療法! | 密探的小本本上羞答答,人生太復(fù)雜!
詢(xún)問(wèn) = 你是來(lái)跟我爭(zhēng)天下第一的嗎? | 你是什么鬼? | 你說(shuō)的每一句話(huà)都將作為呈堂證供!"""

??由于筆者比較喜歡玩王者榮耀,所以做了一個(gè)王者榮耀英雄自我介紹的規(guī)則,整體規(guī)則是一個(gè)字符串,其中規(guī)則之間用空格分開(kāi),語(yǔ)料之間用“|”分開(kāi)?,F(xiàn)在的規(guī)則是無(wú)法直接用的,因?yàn)樗亲址男问?,我們定義一個(gè) create_grammer()函數(shù),將字符串形式的語(yǔ)法規(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('|')]#對(duì)于單個(gè)單詞,s.split()也可以去掉空格
    return grammer
hero_grammer = create_grammer(hero)
hero_grammer

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

  1. Python split()方法 https://www.runoob.com/python/att-string-split.html,返回一個(gè)列表。
  2. Python strip()方法 https://www.runoob.com/python/att-string-strip.html,返回一個(gè)字符串。
  3. 第四行,if not line.strip()里面if not后面如果跟的是空的,那么就相當(dāng)于跟了一個(gè)False,非空則相當(dāng)于跟了一個(gè)True。
    結(jié)果如下:
{'hero': [['自我介紹', '臺(tái)詞', '詢(xún)問(wèn)']],
 '自我介紹': [['寒暄*', '我是', '名字'], ['寒暄*', '我是', '外號(hào)']],
 '寒暄*': [['null'], ['寒暄', '寒暄*']],
 '寒暄': [['你好,'], ['很高興認(rèn)識(shí)你,'], ['大唐歡迎你,']],
 '名字': [['李白。'], ['鐘馗。'], ['李元芳。']],
 '外號(hào)': [['劍仙。'], ['地府判官。'], ['王都密探。']],
 '臺(tái)詞': [['大河之劍天上來(lái)!'], ['對(duì)付魑魅魍魎,乃是強(qiáng)迫癥最佳療法!'], ['密探的小本本上羞答答,人生太復(fù)雜!']],
 '詢(xún)問(wèn)': [['你是來(lái)跟我爭(zhēng)天下第一的嗎?'], ['你是什么鬼?'], ['你說(shuō)的每一句話(huà)都將作為呈堂證供!']]}

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() 函數(shù) https://www.runoob.com/python/func-number-choice.html,返回一個(gè)隨機(jī)項(xiàng)。
  2. Python join()方法https://www.runoob.com/python/att-string-join.html,返回一個(gè)字符串。
  3. 最后一行列表生成式的使用可以參考Python中列表生成式中的if和else
  4. 這段代碼中函數(shù)有可能會(huì)不斷調(diào)用自身,因此第四行非常重要,否則會(huì)無(wú)限循環(huán)下去。

結(jié)果如下:

generate(hero_grammer,'hero')
'大唐歡迎你,很高興認(rèn)識(shí)你,我是鐘馗。密探的小本本上羞答答,人生太復(fù)雜!你是什么鬼?'

3.生成多句話(huà)

接下來(lái),我們寫(xiě)一個(gè)generate_n()函數(shù),使得同時(shí)生成多句話(huà):

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

同時(shí)生成5句話(huà),結(jié)果如下:

generate_n(5,hero_grammer,'hero')
大唐歡迎你,很高興認(rèn)識(shí)你,大唐歡迎你,我是劍仙。大河之劍天上來(lái)!你說(shuō)的每一句話(huà)都將作為呈堂證供!
我是李白。密探的小本本上羞答答,人生太復(fù)雜!你是來(lái)跟我爭(zhēng)天下第一的嗎?
大唐歡迎你,我是李元芳。大河之劍天上來(lái)!你說(shuō)的每一句話(huà)都將作為呈堂證供!
很高興認(rèn)識(shí)你,我是王都密探。大河之劍天上來(lái)!你是什么鬼?
你好,我是李元芳。對(duì)付魑魅魍魎,乃是強(qiáng)迫癥最佳療法!你說(shuō)的每一句話(huà)都將作為呈堂證供!

??使用上面寫(xiě)的代碼,我們可以生成足夠多的句子。但同時(shí)我們也可以發(fā)現(xiàn),并不是每一句話(huà)的邏輯都通順,那么如何判斷到底哪個(gè)句子是更合適的呢?這時(shí)候就需要用到第二個(gè)模型了——Probability Based Model(基于概率的模型)。

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

??個(gè)人理解是,我們首先選擇一個(gè)語(yǔ)料庫(kù),接下來(lái),我們計(jì)算每一個(gè)句子在語(yǔ)料庫(kù)中出現(xiàn)的概率,認(rèn)為其中概率最高的是最合理的句子,并作為最后輸出的句子。那么如何去計(jì)算某一個(gè)句子出現(xiàn)的概率呢,我們需要引入N-Gram模型。

N-Gram模型

??我們知道句子是由一個(gè)又一個(gè)詞構(gòu)成的,假設(shè)某個(gè)句子s,是由按特定順序排列的詞w_1,w_2,w_3,...,w_m構(gòu)成。那么就有:
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)
??但是這個(gè)概率并不容易計(jì)算,為了簡(jiǎn)化計(jì)算,我們這里引入馬爾科夫假設(shè),即假設(shè)一個(gè)詞出現(xiàn)的概率,至于其后面的一個(gè)詞有關(guān),而與其他的詞無(wú)關(guān),也即是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)
??類(lèi)似的,我們還可以得出3-Gram,4-Gram以及最簡(jiǎn)單的1-Gram模型等等,這里不再一一列舉。

1.導(dǎo)入語(yǔ)料庫(kù)

這里使用的語(yǔ)料庫(kù)是一些電影評(píng)論,文件儲(chǔ)存在我的電腦桌面上。

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) 函數(shù) ,取一個(gè)DataFrame的前n項(xiàng),n默認(rèn)為5。
  2. 編碼形式需要自己試驗(yàn)得到合適的,這里是'utf-8'

2.語(yǔ)料處理

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)必誅吳京比這句話(huà)還要意淫一百倍'

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]
['感覺(jué)', '挺', '搞笑', '的', '戰(zhàn)狼', '2', '里', '吳京', '這么', '能']

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

  1. python tolist()方法,將數(shù)組或矩陣轉(zhuǎn)化為列表。
  2. 正則表達(dá)式 re.findall 能夠以列表的形式返回能匹配的子串,這里用于去除評(píng)論中的各種符號(hào)啊,可以理解為一種數(shù)據(jù)清洗。
  3. 這里使用jieba分詞。jieba.cut(string)得到的是一個(gè)生成器(generator),要使用list()生成列表。
  4. 最后得到的Token 是一個(gè)包含原來(lái)的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內(nèi)建的一個(gè)集合模塊,非常有用。
  2. Counter是一個(gè)計(jì)數(shù)器,Counter(Token)會(huì)生成一個(gè)字典,使用.most_common(n)選擇元素出現(xiàn)頻率最高的n個(gè),這里是選取語(yǔ)料庫(kù)中最常出現(xiàn)的10個(gè)詞。
  3. TOKEN_2_GRAM是把原本相鄰的兩個(gè)詞和在一起組成的新列表。
  4. ''.join(TOKEN[i:i+2])TOKEN[i]+TOKEN[i+1]在這里作用相同。

??接下來(lái),我們定義兩個(gè)函數(shù),分別計(jì)算單個(gè)詞在語(yǔ)料庫(kù)中出現(xiàn)的概率和兩個(gè)詞相鄰在語(yǔ)料庫(kù)中出現(xiàn)的頻率。如果某個(gè)詞在語(yǔ)料庫(kù)中不存在,我們就假定它的頻率是1/語(yǔ)料庫(kù)的長(zhǎng)度。保證了每個(gè)詞都有概率,防止后面概率相除時(shí)出現(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ù),來(lái)計(jì)算某個(gè)句子出現(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('今天是個(gè)好日子')
1.700447371998775e-11

??現(xiàn)在萬(wàn)事俱備,我們已經(jīng)有了生成句子的函數(shù)和判斷句子合理性的函數(shù),下面我們定義一個(gè)函數(shù),來(lái)從多個(gè)句子中選擇概率最高的那個(gè)。

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中,萬(wàn)物皆對(duì)象,函數(shù)也可以作為另一個(gè)函數(shù)的參數(shù)輸入。
  2. Python enumerate() 函數(shù),https://www.runoob.com/python/python-func-enumerate.html
  3. sorted()返回一個(gè)排序后的副本,可以接收一個(gè)函數(shù)作為排序依據(jù)。

我們一次產(chǎn)生15個(gè)句子,選擇最合理的,最后的輸出結(jié)果如下:

generate_best(hero,'hero','\n','=',get_probability,15)
'我是李白。對(duì)付魑魅魍魎,乃是強(qiáng)迫癥最佳療法!你是什么鬼?'

感覺(jué)還可以,我們現(xiàn)在換一個(gè)語(yǔ)言規(guī)則:

host = """
host = 寒暄 報(bào)數(shù) 詢(xún)問(wèn) 業(yè)務(wù)相關(guān) 結(jié)尾 
報(bào)數(shù) = 我是 數(shù)字 號(hào) ,
數(shù)字 = 單個(gè)數(shù)字 | 數(shù)字 單個(gè)數(shù)字 
單個(gè)數(shù)字 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 
寒暄 = 稱(chēng)謂 打招呼 | 打招呼
稱(chēng)謂 = 人稱(chēng) ,
人稱(chēng) = 先生 | 女士 | 小朋友
打招呼 = 你好 | 您好 
詢(xún)問(wèn) = 請(qǐng)問(wèn)你要 | 您需要
業(yè)務(wù)相關(guān) = 玩玩 具體業(yè)務(wù)
玩玩 = null
具體業(yè)務(wù) = 喝酒 | 打牌 | 打獵 | 賭博
結(jié)尾 = 嗎?
"""

生成10個(gè)句子,并選擇最合理的句子如下:

generate_best(host,'host','\n','=',get_probability,10)
'女士,你好我是314號(hào),您需要打牌嗎?'

??感覺(jué)效果還可以,完成整個(gè)過(guò)程還是給作者帶來(lái)了一定的滿(mǎn)足感哈哈哈。當(dāng)然,這個(gè)過(guò)程也存在缺陷,我認(rèn)為,缺陷主要存在于以下兩個(gè)方面:

  1. 2-gram模型的假設(shè)本身與實(shí)際情況有一定差異,只是一個(gè)簡(jiǎn)化假設(shè)。
  2. 選取判斷句子合理性的語(yǔ)料庫(kù)來(lái)自電影影評(píng),判斷出來(lái)的其實(shí)是最有可能出現(xiàn)在影評(píng)中的句子,可能會(huì)對(duì)判斷結(jié)果造成偏差。

因此,選取更合理的假設(shè)與模型,增加其他方面的語(yǔ)料都是提升準(zhǔn)確度的方法。

最后,歡迎大家訪(fǎng)問(wèn)我的GitHub查看更多代碼:https://github.com/LiuPineapple
??歡迎大家訪(fǎng)問(wèn)我的簡(jiǎn)書(shū)主頁(yè)查看更多文章:http://www.itdecent.cn/u/31e8349bd083

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容