NLP入門(九)詞義消岐(WSD)的簡介與實現
詞義消岐簡介
詞義消岐,英文名稱為Word Sense Disambiguation,英語縮寫為WSD,是自然語言處理(NLP)中一個非常有趣的基本任務。
那麼,什麼是詞義消岐呢?通常,在我們的自然語言中,不管是英語,還是中文,都有多義詞存在。這些多義詞的存在,會讓人對句子的意思產生混淆,但人通過學習又是可以正確地區分出來的。
以“小米”這個詞為例,如果僅僅只是說“小米”這個詞語,你並不知道它實際指的到底是小米科技公司還是穀物。但當我們把詞語置於某個特定的語境中,我們能很好地區分出這個詞語的意思。比如,
雷軍是小米的創始人。
在這個句子中,我們知道這個“小米”指的是小米科技公司。比如
我今天早上喝了一碗小米粥。
在這個句子中,“小米”指的是穀物、農作物。
所謂詞義消岐,指的是在特定的語境中,識別出某個歧義詞的正確含義。
那麼,詞義消岐有什麼作用呢?詞義消岐可以很好地服務於語言翻譯和智慧問答領域,當然,還有許多應用有待開發~
詞義消岐實現
在目前的詞義消岐演算法中,有不少原創演算法,有些實現起來比較簡單,有些想法較為複雜,但實現的效果普遍都不是很好。比較經典的詞義消岐的演算法為Lesk演算法,該演算法的想法很簡單,通過對某個歧義詞構建不同含義的語料及待判別句子中該詞語與語料的重合程度來實現,具體的演算法原理可參考網址:https://en.wikipedia.org/wiki/Lesk_algorithm .
我們以詞語“火箭”為例,選取其中的兩個義項(同一個詞語的不同含義):NBA球隊名 和 燃氣推進裝置 ,如下:
獲取語料
首先,我們利用爬蟲爬取這兩個義項的百度百科網頁,以句子為單位,只要句子中出現該詞語,則把這句話加入到這個義項的預料中。爬蟲的完整Python程式碼如下:
import requests from bs4 import BeautifulSoup from pyltp import SentenceSplitter class WebScrape(object): def __init__(self, word, url): self.url = url self.word = word # 爬取百度百科頁面 def web_parse(self): headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \ (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'} req = requests.get(url=self.url, headers=headers) # 解析網頁,定位到main-content部分 if req.status_code == 200: soup = BeautifulSoup(req.text.encode(req.encoding), 'lxml') return soup return None # 獲取該詞語的義項 def get_gloss(self): soup = self.web_parse() if soup: lis = soup.find('ul', class_="polysemantList-wrapper cmn-clearfix") if lis: for li in lis('li'): if '<a' not in str(li): gloss = li.text.replace('▪', '') return gloss return None # 獲取該義項的語料,以句子為單位 def get_content(self): # 傳送HTTP請求 result = [] soup = self.web_parse() if soup: paras = soup.find('div', class_='main-content').text.split('\n') for para in paras: if self.word in para: sents = list(SentenceSplitter.split(para)) for sent in sents: if self.word in sent: sent = sent.replace('\xa0', '').replace('\u3000', '') result.append(sent) result = list(set(result)) return result # 將該義項的語料寫入到txt def write_2_file(self): gloss = self.get_gloss() result = self.get_content() print(gloss) print(result) if result and gloss: with open('./%s_%s.txt'% (self.word, gloss), 'w', encoding='utf-8') as f: f.writelines([_+'\n' for _ in result]) def run(self): self.write_2_file() # NBA球隊名 #url = 'https://baike.baidu.com/item/%E4%BC%91%E6%96%AF%E6%95%A6%E7%81%AB%E7%AE%AD%E9%98%9F/370758?fromtitle=%E7%81%AB%E7%AE%AD&fromid=8794081#viewPageContent' # 燃氣推進裝置 url = 'https://baike.baidu.com/item/%E7%81%AB%E7%AE%AD/6308#viewPageContent' WebScrape('火箭', url).run()
利用這個爬蟲,我們爬取了“火箭”這個詞語的兩個義項的語料,生成了火箭_燃氣推進裝置.txt檔案和火箭_NBA球隊名.txt檔案,這兩個檔案分別含有361和171個句子。以火箭_燃氣推進裝置.txt檔案為例,前10個句子如下:
火箭技術的飛速發展,不僅可提供更加完善的各類導彈和推動相關科學的發展,還將使開發空間資源、建立空間產業、空間基地及星際航行等成為可能。
火箭技術是一項十分複雜的綜合性技術,主要包括火箭推進技術、總體設計技術、火箭結構技術、控制和制導技術、計劃管理技術、可靠性和質量控制技術、試驗技術,對導彈來說還有彈頭制導和控制、
1903年,俄國的К.E.齊奧爾科夫斯基提出了製造大型液體火箭的設想和設計原理。
火箭有很多種,原始的火箭是用引火物附在弓箭頭上,然後射到敵人身上引起焚燒的一種箭矢。
“長征三號丙”火箭是在 “長征三號乙”火箭的基礎上, 減少了兩個助推器並取消了助推器上的尾翼。
火箭與導彈有什麼區別
為了能夠在未來大規模的將人類送入太空,不可能依賴傳統的火箭和飛船。
火箭V2火箭
探測高層大氣的物理特徵(如氣壓、溫度、溼度等)和現象的探空火箭。
可一次發射一發至數十發火箭彈。
實現演算法
我們以句子為單位進行詞義消岐,即輸入一句話,識別出該句子中某個歧義詞的含義。筆者使用的演算法比較簡單,是以TF-IDF為權重的頻數判別。以句子
賽季初的時候,火箭是眾望所歸的西部決賽球隊。
為例,對該句子分詞後,去掉停用詞(stopwords),然後分別統計除了“火箭”這個詞以外的TF-IDF值,累加起來,比較在兩個義項下這個值的大小即可。
實現這個演算法的完整Python程式碼如下:
import os
import jieba
from math import log2
# 讀取每個義項的語料
def read_file(path):
with open(path, 'r', encoding='utf-8') as f:
lines = [_.strip() for _ in f.readlines()]
return lines
# 對示例句子分詞
sent = '賽季初的時候,火箭是眾望所歸的西部決賽球隊。'
wsd_word = '火箭'
jieba.add_word(wsd_word)
sent_words = list(jieba.cut(sent, cut_all=False))
# 去掉停用詞
stopwords = [wsd_word, '我', '你', '它', '他', '她', '了', '是', '的', '啊', '誰', '什麼','都',\
'很', '個', '之', '人', '在', '上', '下', '左', '右', '。', ',', '!', '?']
sent_cut = []
for word in sent_words:
if word not in stopwords:
sent_cut.append(word)
print(sent_cut)
# 計算其他詞的TF-IDF以及頻數
wsd_dict = {}
for file in os.listdir('.'):
if wsd_word in file:
wsd_dict[file.replace('.txt', '')] = read_file(file)
# 統計每個詞語在語料中出現的次數
tf_dict = {}
for meaning, sents in wsd_dict.items():
tf_dict[meaning] = []
for word in sent_cut:
word_count = 0
for sent in sents:
example = list(jieba.cut(sent, cut_all=False))
word_count += example.count(word)
if word_count:
tf_dict[meaning].append((word, word_count))
idf_dict = {}
for word in sent_cut:
document_count = 0
for meaning, sents in wsd_dict.items():
for sent in sents:
if word in sent:
document_count += 1
idf_dict[word] = document_count
# 輸出值
total_document = 0
for meaning, sents in wsd_dict.items():
total_document += len(sents)
# 計算tf_idf值
mean_tf_idf = []
for k, v in tf_dict.items():
print(k+':')
tf_idf_sum = 0
for item in v:
word = item[0]
tf = item[1]
tf_idf = item[1]*log2(total_document/(1+idf_dict[word]))
tf_idf_sum += tf_idf
print('%s, 頻數為: %s, TF-IDF值為: %s'% (word, tf, tf_idf))
mean_tf_idf.append((k, tf_idf_sum))
sort_array = sorted(mean_tf_idf, key=lambda x:x[1], reverse=True)
true_meaning = sort_array[0][0].split('_')[1]
print('\n經過詞義消岐,%s在該句子中的意思為 %s .' % (wsd_word, true_meaning))
輸出結果如下:
['賽季', '初', '時候', '眾望所歸', '西部', '決賽', '球隊']
火箭_燃氣推進裝置:
初, 頻數為: 2, TF-IDF值為: 12.49585502688717
火箭_NBA球隊名:
賽季, 頻數為: 63, TF-IDF值為: 204.6194333469459
初, 頻數為: 1, TF-IDF值為: 6.247927513443585
時候, 頻數為: 1, TF-IDF值為: 8.055282435501189
西部, 頻數為: 16, TF-IDF值為: 80.88451896801904
決賽, 頻數為: 7, TF-IDF值為: 33.13348038429679
球隊, 頻數為: 40, TF-IDF值為: 158.712783770034
經過詞義消岐,火箭在該句子中的意思為 NBA球隊名 .
測試
接著,我們對上面的演算法和程式進行更多的測試。
輸入句子為:
三十多年前,戰士們在戈壁灘白手起家,建起了我國的火箭發射基地。
輸出結果為:
['三十多年', '前', '戰士', '們', '戈壁灘', '白手起家', '建起', '我國', '發射', '基地']
火箭_燃氣推進裝置:
前, 頻數為: 2, TF-IDF值為: 9.063440958888354
們, 頻數為: 1, TF-IDF值為: 6.05528243550119
我國, 頻數為: 3, TF-IDF值為: 22.410959804340102
發射, 頻數為: 89, TF-IDF值為: 253.27878721862933
基地, 頻數為: 7, TF-IDF值為: 42.38697704850833
火箭_NBA球隊名:
前, 頻數為: 3, TF-IDF值為: 13.59516143833253
們, 頻數為: 1, TF-IDF值為: 6.05528243550119
經過詞義消岐,火箭在該句子中的意思為 燃氣推進裝置 .
輸入句子為:
對於馬刺這樣級別的球隊,常規賽只有屈指可數的幾次交鋒具有真正的意義,今天對火箭一役是其中之一。
輸出結果為:
['對於', '馬刺', '這樣', '級別', '球隊', '常規賽', '只有', '屈指可數', '幾次', '交鋒', '具有', '真正', '意義', '今天', '對', '一役', '其中', '之一']
火箭_燃氣推進裝置:
只有, 頻數為: 1, TF-IDF值為: 7.470319934780034
具有, 頻數為: 5, TF-IDF值為: 32.35159967390017
真正, 頻數為: 2, TF-IDF值為: 14.940639869560068
意義, 頻數為: 1, TF-IDF值為: 8.055282435501189
對, 頻數為: 5, TF-IDF值為: 24.03677461028802
其中, 頻數為: 3, TF-IDF值為: 21.16584730650357
之一, 頻數為: 2, TF-IDF值為: 14.11056487100238
火箭_NBA球隊名:
馬刺, 頻數為: 1, TF-IDF值為: 7.470319934780034
球隊, 頻數為: 40, TF-IDF值為: 158.712783770034
常規賽, 頻數為: 14, TF-IDF值為: 73.4709851882102
只有, 頻數為: 1, TF-IDF值為: 7.470319934780034
對, 頻數為: 10, TF-IDF值為: 48.07354922057604
之一, 頻數為: 1, TF-IDF值為: 7.05528243550119
經過詞義消岐,火箭在該句子中的意思為 NBA球隊名 .
輸入句子為:
姚明是火箭隊的主要得分手之一。
輸出結果為:
['姚明', '火箭隊', '主要', '得分手', '之一']
火箭_燃氣推進裝置:
主要, 頻數為: 9, TF-IDF值為: 51.60018906552445
之一, 頻數為: 2, TF-IDF值為: 14.11056487100238
火箭_NBA球隊名:
姚明, 頻數為: 18, TF-IDF值為: 90.99508383902142
火箭隊, 頻數為: 133, TF-IDF值為: 284.1437533641371
之一, 頻數為: 1, TF-IDF值為: 7.05528243550119
經過詞義消岐,火箭在該句子中的意思為 NBA球隊名 .
輸入的句子為:
從1992年開始研製的長征二號F型火箭,是中國航天史上技術最複雜、可靠性和安全性指標最高的運載火箭。
輸出結果為:
['從', '1992', '年', '開始', '研製', '長征二號', 'F', '型', '中國', '航天史', '技術', '最', '複雜', '、', '可靠性', '和', '安全性', '指標', '最高', '運載火箭']
火箭_燃氣推進裝置:
從, 頻數為: 6, TF-IDF值為: 29.312144604353264
1992, 頻數為: 1, TF-IDF值為: 6.733354340613827
年, 頻數為: 43, TF-IDF值為: 107.52982410441274
開始, 頻數為: 5, TF-IDF值為: 30.27641217750595
研製, 頻數為: 25, TF-IDF值為: 110.28565614316162
長征二號, 頻數為: 37, TF-IDF值為: 159.11461253349566
F, 頻數為: 7, TF-IDF值為: 40.13348038429679
中國, 頻數為: 45, TF-IDF值為: 153.51418105769093
技術, 頻數為: 27, TF-IDF值為: 119.10850863461454
最, 頻數為: 2, TF-IDF值為: 7.614709844115208
、, 頻數為: 117, TF-IDF值為: 335.25857156467714
可靠性, 頻數為: 5, TF-IDF值為: 30.27641217750595
和, 頻數為: 76, TF-IDF值為: 191.22539545388003
安全性, 頻數為: 2, TF-IDF值為: 14.940639869560068
運載火箭, 頻數為: 95, TF-IDF值為: 256.28439093389505
火箭_NBA球隊名:
從, 頻數為: 5, TF-IDF值為: 24.42678717029439
1992, 頻數為: 2, TF-IDF值為: 13.466708681227654
年, 頻數為: 52, TF-IDF值為: 130.0360663588247
開始, 頻數為: 2, TF-IDF值為: 12.11056487100238
中國, 頻數為: 4, TF-IDF值為: 13.64570498290586
最, 頻數為: 3, TF-IDF值為: 11.422064766172813
、, 頻數為: 16, TF-IDF值為: 45.847326025938756
和, 頻數為: 31, TF-IDF值為: 77.99983235618791
最高, 頻數為: 8, TF-IDF值為: 59.76255947824027
經過詞義消岐,火箭在該句子中的意思為 燃氣推進裝置 .
輸入句子為:
到目前為止火箭已經在休斯頓進行了電視宣傳,並在大街小巷豎起廣告欄。
輸出結果為:
['到', '目前為止', '已經', '休斯頓', '進行', '電視', '宣傳', '並', '大街小巷', '豎起', '廣告欄']
火箭_燃氣推進裝置:
到, 頻數為: 11, TF-IDF值為: 39.19772273088667
已經, 頻數為: 2, TF-IDF值為: 13.466708681227654
進行, 頻數為: 14, TF-IDF值為: 68.39500407682429
並, 頻數為: 11, TF-IDF值為: 49.17351928258037
火箭_NBA球隊名:
到, 頻數為: 6, TF-IDF值為: 21.38057603502909
已經, 頻數為: 2, TF-IDF值為: 13.466708681227654
休斯頓, 頻數為: 2, TF-IDF值為: 14.940639869560068
進行, 頻數為: 2, TF-IDF值為: 9.770714868117755
並, 頻數為: 5, TF-IDF值為: 22.351599673900168
經過詞義消岐,火箭在該句子中的意思為 燃氣推進裝置 .
總結
對於筆者的這個演算法,雖然有一定的效果,但是也不總是識別正確。比如,對於最後一個測試的句子,識別的結果就是錯誤的,其實“休斯頓”才是識別該詞語義項的關鍵詞,但很遺憾,在筆者的演算法中,“休斯頓”的權重並不高。
對於詞義消岐演算法,如果還是筆者的這個思路,那麼有以下幾方面需要改進:
- 語料大小及豐富程度;
- 停用詞的擴充;
- 更好的演算法。
筆者的這篇文章僅作為詞義消岐的簡介以及簡單實現,希望能對讀者有所啟發~
注意:本人現已開通微信公眾號: Python爬蟲與演算法(微訊號為:easy_web_scrape), 歡迎大家關注