NLP入门九词义消岐WSD的简介与实现

53次阅读

共计 8022 个字符,预计需要花费 21 分钟才能阅读完成。

词义消岐简介

  词义消岐,英文名称为 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),欢迎大家关注哦~~

正文完
 0