上下位关系自动检测方法(论文复现)

上下位关系自动检测方法(论文复现)

本文所涉及所有资源均在传知代码平台可获取

文章目录

概述

本文复现论文提出的文本中上位词检测方法,在自然语言处理中,上下位关系(Is-a Relationship)表示的是概念(又称术语)之间的语义包含关系。其中,上位词(Hypernym)表示的是下位词(Hyponym)的抽象化和一般化,而下位词则是对上位词的具象化和特殊化。举例来说:"水果"是"苹果"、"香蕉"、"橙子"等的上位词,"汽车"、"电动车"、"自行车"等则是"交通工具"的下位词。在自然语言处理任务中,理解概念之间的上下位关系对于诸如词义消歧、信息检索、自动问答、语义推理等任务都具有重要意义

文本中上位词检测方法,即从文本中提取出互为上下位关系的概念。现有的无监督上位词检测方法大致可以分为两类------基于模式的方法和基于分布模型的方法:

(1)基于模式的方法:其主要思想是利用特定的词汇-句法模式来检测文本中的上下位关系。例如,我们可以通过检测文本中是否存在句式"【词汇1】是一种【词汇2】"或"【词汇1】,例如【词汇2】"来判断【词汇1】和【词汇2】间是否存在上下位关系。这些模式可以是预定义的,也可以是通过机器学习得到的。然而,基于模式的方法存在一个众所周知的问题------极端稀疏性,即词汇必须在有限的模式中共同出现,其上下位关系才能被检测到。

(2)基于分布模型的方法:基于大型文本语料库,词汇可以被学习并表示成向量的形式。利用特定的相似度度量,我们可以区分词汇间的不同关系。

在该论文中,作者研究了基于模式的方法和基于分布模型的方法在几个上下位关系检测任务中的表现,并发现简单的基于模式的方法在常见的数据集上始终优于基于分布模型的方法。作者认为这种差异产生的原因是:基于模式的方法提供了尚不能被分布模型准确捕捉到的重要上下文约束

算法原理
Hearst 模式

作者使用如下模式来捕捉文本中的上下位关系

通过对大型语料库使用模式捕捉候选上下位词对并统计频次,可以计算任意两个词汇之间存在上下位关系的概率

上下位关系得分

设 p(x,y)p (x ,y )是词汇 xx 和 yy 分别作为下位词和上位词出现在预定义模式集合 PP 中的频率,p−(x)p −(x )是 xx 作为任意词汇的下位词出现在预定义模式中的频率,p+(y)p +(y )是 yy 作为任意词汇的上位词出现在预定义模式中的频率。作者定义正逐点互信息(Positive Point-wise Mutual Information)作为词汇间上下位关系得分的依据

由于模式的稀疏性,部分存在上下位关系的词对并不会出现在特定的模式中。为了解决这一问题,作者利用PPMI得分矩阵的稀疏表示来预测任意未知词对的上下位关系得分。PPMI得分矩阵定义如下

其中 m=|{x|(x,y)\in P\or(y,x)\in P}|。

对矩阵 MM 做奇异值分解可得 M=UΣVTM =U ΣV**T,然后我们可以通过下式计算出上下位关系 spmi 得分

其中 uxu**x 和 vyv**y 分别是矩阵 UU 和 VV 的第 xx 行和第 yy 行,ΣrΣr 是对 ΣΣ 的 rr 截断(即除了最大的 rr 个元素其余全部置零)

核心逻辑

具体的核心逻辑如下所示

bash 复制代码
import spacy
import json
from tqdm import tqdm
import re
from collections import Counter
import numpy as np
import math

nlp = spacy.load("en_core_web_sm")

def clear_text(text):
    """对文本进行清理"""
    # 这里可以添加自己的清理步骤
    # 删去交叉引用标识,例如"[1]"
    pattern = r'\[\d+\]'
    result = re.sub(pattern, '', text)
    return result

def split_sentences(text):
    """将文本划分为句子"""
    doc = nlp(text)
    sentences = [sent.text.strip() for sent in doc.sents]
    return sentences

def extract_noun_phrases(text):
    """从文本中抽取出术语"""
    doc = nlp(text)
    terms = []
    # 遍历句子中的名词性短语(例如a type of robot)
    for chunk in doc.noun_chunks:
        term_parts = []
        for token in list(chunk)[-1::]:
            # 以非名词且非形容词,或是代词的词语为界,保留右半部分(例如robot)
            if token.pos_ in ['NOUN', 'ADJ'] and token.dep_ != 'PRON':
                term_parts.append(token.text)
            else:
                break
        if term_parts != []:
            term = ' '.join(term_parts)
            terms.append(term)
    return terms

def term_lemma(term):
    """将术语中的名词还原为单数"""
    lemma = []
    doc = nlp(term)
    for token in doc:
        if token.pos_ == 'NOUN':
            lemma.append(token.lemma_)
        else:
            lemma.append(token.text)
    return ' '.join(lemma)

def find_co_occurrence(sentence, terms, patterns):
    """找出共现于模板的术语对"""
    pairs = []
    # 两两之间匹配
    for hyponym in terms:
        for hypernym in terms:
            if hyponym == hypernym:
                continue
            for pattern in patterns:
                # 将模板中的占位符替换成候选上下位词
                pattern = pattern.replace('__HYPONYM__', re.escape(hyponym))
                pattern = pattern.replace('__HYPERNYM__', re.escape(hypernym))
                # 在句子中匹配
                if re.search(pattern, sentence) != None:
                    # 将名词复数还原为单数
                    pairs.append((term_lemma(hyponym), term_lemma(hypernym)))
    return pairs

def count_unique_tuple(tuple_list):
    """统计列表中独特元组出现次数"""
    counter = Counter(tuple_list)
    result = [{"tuple": unique, "count": count} for unique, count in counter.items()]
    return result

def find_rth_largest(arr, r):
    """找到第r大的元素"""
    rth_largest_index = np.argpartition(arr, -r)[-r]
    return arr[rth_largest_index]

def find_pairs(corpus_file, patterns, disable_tqdm=False):
    """读取文件并找出共现于模板的上下位关系术语对"""
    pairs = []
    # 按行读取语料库
    lines = corpus_file.readlines()
    for line in tqdm(lines, desc="Finding pairs", ascii=" 123456789#", disable=disable_tqdm):
        # 删去首尾部分的空白字符
        line = line.strip()
        # 忽略空白行
        if line == '':
            continue
        # 清理文本
        line = clear_text(line)
        # 按句处理
        sentences = split_sentences(line)
        for sentence in sentences:
            # 抽取出句子中的名词性短语并分割成术语
            candidates_terms = extract_noun_phrases(sentence)
            # 找出共现于模板的术语对
            pairs = pairs + find_co_occurrence(sentence, candidates_terms, patterns)
    return pairs

def spmi_calculate(configs, unique_pairs):
    """基于对共现频率的统计,计算任意两个术语间的spmi得分"""
    # 计算每个术语分别作为上下位词的出现频次
    terms = list(set([pair["tuple"][0] for pair in unique_pairs] + [pair["tuple"][1] for pair in unique_pairs]))
    term_count = {term: {'hyponym_count': 0, 'hypernym_count': 0} for term in terms}
    all_count = 0
    for pair in unique_pairs:
        term_count[pair["tuple"][0]]['hyponym_count'] += pair["count"]
        term_count[pair["tuple"][1]]['hypernym_count'] += pair["count"]
        all_count += pair["count"]
    # 计算PPMI矩阵 
    ppmi_matrix = np.zeros((len(terms), len(terms)), dtype=np.float32)
    for pair in unique_pairs:
        hyponym = pair["tuple"][0]
        hyponym_id = terms.index(hyponym)
        hypernym = pair["tuple"][1]
        hypernym_id = terms.index(hypernym)
        ppmi = (pair["count"] * all_count) / (term_count[hyponym]['hyponym_count'] * term_count[hypernym]['hypernym_count'])
        ppmi = max(0, math.log(ppmi))
        ppmi_matrix[hyponym_id, hypernym_id] = ppmi
    # 对PPMI进行奇异值分解并截断
    r = configs['clip']
    U, S, Vt = np.linalg.svd(ppmi_matrix)
    S[S < find_rth_largest(S, r)] = 0
    S_r = np.diag(S)
    # 计算任意两个术语间的spmi
    paris2spmi = []
    for hyponym_id in range(len(terms)):
        for hypernym_id in range(len(terms)):
            # 同一个术语间不计算得分
            if hyponym_id == hypernym_id:
                continue
            spmi = np.dot(np.dot(U[hyponym_id , :], S_r), Vt[:, hypernym_id]).item()
            # 保留得分大于阈值的术语对
            if spmi > configs["threshold"]:
                hyponym = terms[hyponym_id]
                hypernym = terms[hypernym_id]
                paris2spmi.append({"hyponym": hyponym, "hypernym": hypernym, "spmi": spmi})
    # 按spmi从大到小排序
    paris2spmi = sorted(paris2spmi, key=lambda x: x["spmi"], reverse=True)
    return paris2spmi

if __name__ == "__main__":
    # 读取配置文件
    with open('config.json', 'r') as config_file:
        configs = json.load(config_file)
    # 读取模板
    with open(configs['patterns_path'], 'r') as patterns_file:
        patterns = json.load(patterns_file)
    # 语料库中共现于模板的术语对
    with open(configs['corpus_path'], 'r', encoding='utf-8') as corpus_file:
        pairs = find_pairs(corpus_file, patterns)
    # 统计上下位关系的出现频次
    unique_pairs = count_unique_tuple(pairs)
    with open(configs["pairs_path"], 'w') as pairs_file:
        json.dump(unique_pairs, pairs_file, indent=6, ensure_ascii=True)
    # 计算任意两个术语间的spmi得分
    paris2spmi = spmi_calculate(configs, unique_pairs)
    with open(configs['spmi_path'], 'w') as spmi_file:
        json.dump(paris2spmi, spmi_file, indent=6, ensure_ascii=True)
效果演示

运行脚本main.py,程序会自动检测语料库中存在的上下位关系。运行结果如下所示

使用方式

解压附件压缩包并进入工作目录。如果是Linux系统,请使用如下命令

bash 复制代码
unzip Revisit-Hearst-Pattern.zip
cd Revisit-Hearst-Pattern

代码的运行环境可通过如下命令进行配置

bash 复制代码
pip install -r requirements.txt
python -m spacy download en_core_web_sm

如果希望在本地运行程序,请运行如下命令

bash 复制代码
python main.py

如果希望在线部署,请运行如下命令

bash 复制代码
python main-flask.py

如果希望添加新的模板,请修改文件data/patterns.json。

"HYPONYM "表示下位词占位符;

"HYPERNYM "表示上位词占位符;

其余格式请遵照 python.re 模块的正则表达式要求。

如果希望使用自己的文件路径或改动其他实验设置,请在文件config.json中修改对应参数。以下是参数含义对照表:

文章代码资源点击附件获取

相关推荐
海晨忆9 分钟前
【Vue】v-if和v-show的区别
前端·javascript·vue.js·v-show·v-if
JiangJiang34 分钟前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
1024小神38 分钟前
在GitHub action中使用添加项目中配置文件的值为环境变量
前端·javascript
龙骑utr42 分钟前
qiankun微应用动态设置静态资源访问路径
javascript
Jasmin Tin Wei43 分钟前
css易混淆的知识点
开发语言·javascript·ecmascript
齐尹秦1 小时前
CSS 列表样式学习笔记
前端
wsz77771 小时前
js封装系列(一)
javascript
Mnxj1 小时前
渐变边框设计
前端
用户7678797737321 小时前
由Umi升级到Next方案
前端·next.js
快乐的小前端1 小时前
TypeScript基础一
前端