词义消岐简介

  词义消岐,英文名称为Word Sense Disambiguation,英语缩写为WSD,是自然语言处理(NLP)中一个非常有趣的基本任务。
  那么,什么是词义消岐呢?通常,在我们的自然语言中,不管是英语,还是中文,都有多义词存在。这些多义词的存在,会让人对句子的意思产生混淆,但人通过学习又是可以正确地区分出来的。
  以“小米”这个词为例,如果仅仅只是说“小米”这个词语,你并不知道它实际指的到底是小米科技公司还是谷物。但当我们把词语置于某个特定的语境中,我们能很好地区分出这个词语的意思。比如,

在这个句子中,我们知道这个“小米”指的是小米科技公司。比如

在这个句子中,“小米”指的是谷物、农作物。
  所谓词义消岐,指的是在特定的语境中,识别出某个歧义词的正确含义。
  那么,词义消岐有什么作用呢?词义消岐可以很好地服务于语言翻译和智能问答领域,当然,还有许多应用有待开发~

词义消岐实现

  在目前的词义消岐算法中,有不少原创算法,有些实现起来比较简单,有些想法较为复杂,但实现的效果普遍都不是很好。比较经典的词义消岐的算法为Lesk算法,该算法的想法很简单,通过对某个歧义词构建不同含义的语料及待判别句子中该词语与语料的重合程度来实现,具体的算法原理可参考网址:https://en.wikipedia.org/wiki/Lesk_algorithm .
  在下面的部分中,笔者将会介绍自己想的一种实现词义消岐的算法,仅仅是一个想法,仅供参考。
  我们以词语“火箭”为例,选取其中的两个义项(同一个词语的不同含义):NBA球队名燃气推进装置 ,如下:

NLP入门(九)词义消岐(WSD)的简介与实现-LMLPHP

获取语料

  首先,我们利用爬虫爬取这两个义项的百度百科网页,以句子为单位,只要句子中出现该词语,则把这句话加入到这个义项的预料中。爬虫的完整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个句子如下:

实现算法

  我们以句子为单位进行词义消岐,即输入一句话,识别出该句子中某个歧义词的含义。笔者使用的算法比较简单,是以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', '型', '中国', '航天史', '技术', '最', '复杂', '、', '可靠性', '和', '安全性', '指标', '最高', '运载火箭']
火箭_燃气推进装置:
从, 频数为: 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), 欢迎大家关注哦~~

05-12 12:16