相似词处理
1. 简介
同义词挖掘一般有三种思路,借助已有知识库,上下文相关性,文本相似度。
1.1 知识库
可以借助已有知识库得到需要同义词,比如说《哈工大信息检索研究室同义词词林扩展版》和 HowNet,其中《词林》文件数据如下。
Aa01A01= 人 士 人物 人士 人氏 人选
Aa01A02= 人类 生人 全人类
Aa01A03= 人手 人员 人口 人丁 口 食指
Aa01A04= 劳力 劳动力 工作者
Aa01A05= 匹夫 个人
Aa01A06= 家伙 东西 货色 厮 崽子 兔崽子 狗崽子 小子 杂种 畜生 混蛋 王八蛋 竖子 鼠辈 小崽子
Aa01A07= 者 手 匠 客 主 子 家 夫 翁 汉 员 分子 鬼 货 棍 徒
Aa01A08= 每人 各人 每位
Aa01A09= 该人 此人
以上两个知识库是人工编辑的,毕竟数量有限,我们还可以借助众包知识库百科词条获取同义词,比如百度百科,如下图所示,在百度百科搜索“凤梨”,我们可以看到在返回页面结果中的 info box中有一个属性为“别称”,别称中就是凤梨的同义词。除此之外,在百科词条的开头描述中,有如下描述“又称”、“俗称”也是同义词,我们可以利用爬虫把这些词爬下来。
百度搜索和谷歌搜索等搜索工具一般都有重定向页,这也可以帮助我们去挖掘同义词。
使用知识库挖掘同义词的优点是简单易得,而且准确率也高,缺点就是知识库覆盖率有限,不是每个细分领域都有。对于金融、医疗、娱乐等领域都需要各自的知识库。
1.2 上下文相关性
利用上下文相关性挖掘同义词也比较好理解,如果两个词的上下文越相似的话,那么这两个词是同义词的概率就越大。使用词向量挖掘同义词是比较常见的做法,比如使用word2vector训练得到词向量,然后再计算余弦相似度,取最相似的top k个词,就得到了同义词。
word2vector是无监督学习,而且本质上来说它是一个语言模型,词向量只是它的副产品,并不是直接用来挖掘同义词。有篇paper发明了弱监督的同义词挖掘模型DPE,也取得了不错的效果。DPE模型流程如下图,一共分为两个阶段,第一阶段跟word2vector差不多,也是训练词向量,只不过DPE是一种graph embedding的思路,首先从语料中构建语义共现网络,然后通过对网络的边采样训练词向量。第二阶段通过弱监督训练一个打分函数,对输入的一对词判断属于同义词的概率。
基于上下文相关性的同义词挖掘方法的优点是能够在语料中挖掘大量的同义词,缺点是训练时间长,而且挖掘的同义词很多都不是真正意义上的同义词需要人工筛选。这种方法对于词频较高的词效果较好。
1.3 文本相似度
对于这一对同义词“阿里巴巴网络技术有限公司”和“阿里巴巴网络公司”直接去计算上下文相似度可能不太有效,那一种直观的方法是直接计算这两个词的文本相似度,比如使用编辑距离(Levenshtein distance)或者 LCS(longest common subsequence),如果两个词的文本相似度大于阈值的话我们就认为他们是同义词的关系。在这里推荐一个计算文本相似度的Java开源项目,基本上文本相似度算法应有尽有。[ 文本相似度算法 ]
基于文本相似度同义词挖掘方法的优点是计算简单,不同于word2vector,这种方法不需要使用很大的语料,只要这个词出现过一次就可以发现同义词关系。这种方法的缺点是有时候不太靠谱,会挖掘出很多错误的同义词,尤其是当两个词比较短的情况下,比如“周杰伦”和“周杰”,就可能会被认为是同义词。所以这种方法适用于一些较长的文本,特别是专业词汇,术语。
2. 句子相似度计算
2.1 编辑距离
编辑距离,英文叫做 Edit Distance,又称 Levenshtein 距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
例如我们有两个字符串:string 和 setting,如果我们想要把 string 转化为 setting,需要这么两步:
第一步,在 s 和 t 之间加入字符 e。
第二步,把 r 替换成 t。
所以它们的编辑距离差就是 2,这就对应着二者要进行转化所要改变(添加、替换、删除)的最小步数。
安装:pip3 install distance
import distance
str1 = "公司地址是哪里"
str2 = "公司在什么位置"
def edit_distance(s1, s2):
return distance.levenshtein(s1, s2)
print(edit_distance(str1, str2))
想要获取相似的文本的话可以直接设定一个编辑距离的阈值来实现,如设置编辑距离为 2
def edit_distance(s1, s2):
return distance.levenshtein(s1, s2)
strings = [
'你在干什么',
'你在干啥子',
'你在做什么',
'你好啊',
'我喜欢吃香蕉'
]
target = '你在干啥'
results = list(filter(lambda x: edit_distance(x, target) <= 2, strings))
print(results)
# ['你在干什么', '你在干啥子']
2.2 杰卡德系数计算
杰卡德系数,英文叫做 Jaccard index, 又称为 Jaccard 相似系数,用于比较有限样本集之间的相似性与差异性。Jaccard 系数值越大,样本相似度越高。
实际上它的计算方式非常简单,就是两个样本的交集除以并集得到的数值,当两个样本完全一致时,结果为 1,当两个样本完全不同时,结果为 0。
算法非常简单,就是交集除以并集,下面我们用 Python 代码来实现一下:
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
def jaccard_similarity(s1, s2):
def add_space(s):
return ' '.join(list(s))
# 将字中间加入空格
s1, s2 = add_space(s1), add_space(s2)
# 转化为TF矩阵
cv = CountVectorizer(tokenizer=lambda s: s.split())
corpus = [s1, s2]
vectors = cv.fit_transform(corpus).toarray()
# 获取词表内容
ret = cv.get_feature_names()
print(ret)
# 求交集
numerator = np.sum(np.min(vectors, axis=0))
# 求并集
denominator = np.sum(np.max(vectors, axis=0))
# 计算杰卡德系数
return 1.0 * numerator / denominator
s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(jaccard_similarity(s1, s2))
2.3 Word2Vec计算
顾名思义,其实就是将每一个词转换为向量的过程。
Word2Vec的词向量模型是训练的维基百科的中文语库,这里模型有250维和50维,向量维度越大模型越大,计算越复杂,正常使用时,需要小的模型,发现50维的也差不多.
流程:
- 对句子进行拆词
- 去除无用的分词
- 计算句子平均词向量
- 余弦相似度
对句子进行拆词:Python提供了很对可用库,自行选择
去除无用的分词:删除没用的语气词等,为的是减少对计算句子平均词向量的影响。
计算句子平均词向量用的是AVG-W2V,计算句子平均词向量,所以02步尤为重要
余弦相似度:
余弦相似度 np.linalg.norm(求范数)(向量的第二范数为传统意义上的向量长度
dist1=float(np.dot(vec1,vec2)/(np.linalg.norm(vec1)*np.linalg.norm(vec2)))
def key_words_ask_method(sentence1, sentence2):
'''
因为无论是#1:AVG-W2V 2:AVG-W2V-TFIDF 都需要求得平均值,
除数:决定整个数据的大小 被除数:影响平均值
所以 分词的标准很重要,可通过自定义词典、停用词 和 语义分析进行适当处理
'''
vec1 = sentence_to_vec(sentence1)
vec2 = sentence_to_vec(sentence2)
# 零向量直接返回
if (vec1 == np.zeros(WORD_VECTOR_DIM)).all() == True or (vec2 == np.zeros(WORD_VECTOR_DIM)).all() == True:
return "不符合相似"
# score = cos(vec1, vec2)
# print(score)
# if score < COSINE_CRITICAL_VALUE:
# return "1"
# else:
# return "0"
# 余弦相似度 np.linalg.norm(求范数)(向量的第二范数为传统意义上的向量长度
dist1=float(np.dot(vec1,vec2)/(np.linalg.norm(vec1)*np.linalg.norm(vec2)))
print("score:", dist1)
if dist1 > 0.92:
return "两个句子相似"
else:
return "两个句子不相似"
参考: