自然語言處理的一些演算法研究和實現(NLTK)
自然語言處理中演算法設計有兩大部分:分而治之 和 轉化 思想。一個是將大問題簡化為小問題,另一個是將問題抽象化,向向已知轉化。前者的例子:歸併排序;後者的例子:判斷相鄰元素是否相同(與排序)。
這次總結的自然語言中常用的一些基本演算法,算是入個門了。
遞迴
使用遞迴速度上會受影響,但是便於理解演算法深層巢狀物件。而一些函數語言程式設計語言會將尾遞迴優化為迭代。
如果要計算n個詞有多少種組合方式?按照階乘定義:n! = n*(n-1)*…*1
def func(wordlist):
length = len(wordlist)
if length==1 :
return 1
else:
return func(wordlist[1:])*length
如果要尋找word下位詞的大小,並且將他們加和。
from nltk.corpus import wordnet as wn
def func(s):#s是WordNet裡面的物件
return 1+sum(func(child) for child in s.hyponyms())
dog = wn.synset('dog.n.01')
print(func(dog))
構建一個字母查詢樹
建立一個巢狀的字典結構,每一級的巢狀包含既定字首的所有單詞。而子查詢樹含有所有可能的後續詞。
def WordTree(trie,key,value):
if key:
first , rest = key[0],key[1:]
if first not in trie:
trie[first] = {}
WordTree(trie[first],rest,value)
else:
trie['value'] = value
WordDict = {}
WordTree(WordDict,'cat','cat')
WordTree(WordDict,'dog','dog')
print(WordDict)
貪婪演算法:不確定邊界自然語言的分割問題(退火演算法的非確定性搜尋)
爬山法是完完全全的貪心法,每次都鼠目寸光的選擇一個當前最優解,因此只能搜尋到區域性的最優值。模擬退火其實也是一種貪心演算法,但是它的搜尋過程引入了隨機因素。模擬退火演算法以一定的概率來接受一個比當前解要差的解,因此有可能會跳出這個區域性的最優解,達到全域性的最優解。
import nltk
from random import randint
#text = 'doyou'
#segs = '01000'
def segment(text,segs):#根據segs,返回切割好的詞連結串列
words = []
last = 0
for i in range(len(segs)):
if segs[i]=='1':#每當遇見1,說明是詞分界
words.append(text[last:i+1])
last = i+1
words.append(text[last:])
return words
def evaluate(text,segs): #計算這種詞分界的得分。作為分詞質量,得分值越小越好(分的越細和更準確之間的平衡)
words = segment(text,segs)
text_size = len(words)
lexicon_size = len(' '.join(list(set(words))))
return text_size + lexicon_size
###################################以下是退火演算法的非確定性搜尋############################################
def filp(segs,pos):#在pos位置擾動
return segs[:pos]+str(1-int(segs[pos]))+segs[pos+1:]
def filp_n(segs,n):#擾動n次
for i in range(n):
segs = filp(segs,randint(0,len(segs)-1))#隨機位置擾動
return segs
def anneal(text,segs,iterations,cooling_rate):
temperature = float(len(segs))
while temperature>=0.5:
best_segs,best = segs,evaluate(text,segs)
for i in range(iterations):#擾動次數
guess = filp_n(segs,int(round(temperature)))
score = evaluate(text,guess)
if score<best:
best ,best_segs = score,guess
score,segs = best,best_segs
temperature = temperature/cooling_rate #擾動邊界,進行降溫
print( evaluate(text,segs),segment(text,segs))
print()
return segs
text = 'doyouseethekittyseethedoggydoyoulikethekittylikethedoggy'
seg = '0000000000000001000000000010000000000000000100000000000'
anneal(text,seg,5000,1.2)
動態規劃
它在自然語言中運用非常廣泛。首先他需要一張表,用來將每一次的子結果存放在查詢表之中。避免了重複計運算元問題!!!
這裡我們討論一個梵文組合旋律的問題。短音節:S,一個長度;長音節:L,兩個長度。所以構建長度為2的方式:{SS,L}。
首先用遞迴的方式編寫一下找到任意音節的函式
def func1(n):
if n==0:
return [""]
elif n==1:
return ["S"]
else:
s = ["S" + item for item in func1(n-1)]
l = ["L" + item for item in func1(n-2)]
return s+l
print(func1(4))
使用動態規劃來實現找到任意音節的函式
之前遞迴十分佔用時間,如果是40個音節,我們需要重複計算632445986次。如果使用動態規劃,我們可以把結果存到一個表中,需要時候呼叫,而不是很坑爹重複計算。
def func2(n):#採用自下而上的動態規劃
lookup = [[""],["S"]]
for i in range(n-1):
s = ["S"+ item for item in lookup[i+1]]
l = ["L" + item for item in lookup[i]]
lookup.append(s+l)
return lookup
print(func2(4)[4])
print(func2(4))
def func3(n,lookup={0:[""],1:["S"]}):#採用自上而下的動態規劃
if n not in lookup:
s = ["S" + item for item in func3(n-1)]
l = ["L" + item for item in func3(n-2)]
lookup[n] = s+l
return lookup[n]#必須返回lookup[n].否則遞迴的時候會出錯
print(func3(4))
對於以上兩種方法,自下而上的方法在某些時候會浪費資源,因為,子問題不一定是解決主問題的必要條件。
NLTK自帶裝飾符:默記
裝飾器
@memoize
會儲存每次函式呼叫時的結果及引數,那麼之後的在呼叫,就不用重複計算。而我們可以只把精力放在上層邏輯,而不是更關注效能和時間(被解決了)
from nltk import memoize
@memoize
def func4(n):
if n==0:
return [""]
elif n==1:
return ["S"]
else:
s = ["S" + item for item in func4(n-1)]
l = ["L" + item for item in func4(n-2)]
return s+l
print(func4(4))
其他的應用
這裡主要介紹一下除了上述兩種主要演算法外,一些小的使用技巧和相關基礎概念。
詞彙多樣性
詞彙多樣性主要取決於:平均詞長(字母個數/每個單詞)、平均句長(單詞個數/每個句子)和文字中沒歌詞出現的次數。
from nltk.corpus import gutenberg
for fileid in gutenberg.fileids():
num_chars = len(gutenberg.raw(fileid))
num_words = len(gutenberg.words(fileid))
num_sents = len(gutenberg.sents(fileid))
num_vocab = len(set(w.lower() for w in gutenberg.words(fileid)))
print(int(num_chars/num_words),int(num_words/num_sents),int(num_words/num_vocab),'from',fileid)
文體差異性
文體差異性可以體現在很多方面:動詞、情態動詞、名詞等等。這裡我們以情態動詞為例,來分析常見情態動詞的在不同文字的差別。
from nltk.corpus import brown
from nltk import FreqDist,ConditionalFreqDist
cfd = ConditionalFreqDist(( genere,word) for genere in brown.categories() for word in brown.words(categories=genere))
genres=['news','religion','hobbies']
models = ['can','could','will','may','might','must']
cfd.tabulate(conditions = genres,samples=models)
隨機語句生成
從《創世紀》中得到所有的雙連詞,根據概率分佈,來判斷哪些詞最有可能跟在給定詞後面。
import nltk
def create_sentence(cfd,word,num=15):
for i in range(num):
print(word,end=" ")
word = cfd[word].max()#查詢word最有可能的字尾
text= nltk.corpus.genesis.words("english-kjv.txt")
bigrams = nltk.bigrams(text)
cfd = nltk.ConditionalFreqDist(bigrams)
print(create_sentence(cfd,'living'))
詞謎問題解決
單詞長度>=3,並且一定有r,且只能出現’egivrvonl’中的字母。
puzzle_word = nltk.FreqDist('egivrvonl')
base_word = 'r'
wordlist = nltk.corpus.words.words()
result = [w for w in wordlist if len(w)>=3 and base_word in w and nltk.FreqDist(w)<=puzzle_word]
#通過FreqDist比較法(比較鍵對應的value),來完成字母只出現一次的要求!!!
print(result)
時間和空間權衡:全文檢索系統
除了研究演算法,分析內部實現外。構造輔助資料結構,可以顯著加快程式執行。
import nltk
def raw(file):
contents = open(file).read()
return str(contents)
def snippet(doc,term):#查詢doc中term的定位
text = ' '*30+raw(doc)+' '*30
pos = text.index(term)
return text[pos-30:pos+30]
files = nltk.corpus.movie_reviews.abspaths()
idx = nltk.Index((w,f) for f in files for w in raw(f).split())
#注意nltk.Index格式
query = 'tem'
while query!='quit' and query:
query = input('>>> input the word:')
if query in idx:
for doc in idx[query]:
print(snippet(doc,query))
else:
print('Not found')