文本相似度,一件可大可小的事情

2020-06-24 10:38:16 浏览数 (2)

问题出现,为什么需要文本相似度

于小文是一个普通程序员,业余的时候会出于做一些自己的网站,最近他做了一个问答社区,就是大家有什么问题都可以在上面问,然后也会有热心网友来解答的网站。

最近他发现一个问题,很多人在提问的时候都会重复,所以他希望有一个功能,就是假设新问题跟已经有的问题相似,就直接推给答案,就避免了重复提问和解答,以及找到解答的时间了。

那么问题就很清楚的定义了,新问题(文本)与已有的问题(文本)之间怎么算重复问题?

我们能否通过设计一个相似度函数,通过调用 Similar(新问题,老问题),把每个老问题都计算一边,就判断出是否相似。

于小文心里开始盘算怎么做。

相似度的实现思考

第一种思考

于小文发现“日本多大”,“日本大小”,我们把第一个问题进行一些步骤的修改,就可以变成第二个问题。

例如我们去掉第一个词的“多”字,变成“日本大”,然后我们再最后插入一个“小”字,就变成了“日本大小”。

也就是说一个文本总能通过进行N种如增加一个字、删掉一个字、修改一个字等等这样的方法就能变成另一个方法。

那么显然两个文本越相似,需要进行的这样的操作就越少。然后于小文查了一下发现这种方法被称为:Edit Distance

代码语言:javascript复制
>>> # !pip install editdistance
>>> import editdistance
>>> editdistance.eval('日本多大', '日本大小')
2

第二种思考

假设我们说文本“日本多大的面积”,和文本“日本面积多大”,一个想法是,这两个文本中有很多字一样,那么我们假设文本是由单个的字组成的集合,那么最简单的的方法是,判断两个集合交集来多少。我们知道集合中的元素是不能重复的,也就是说两个文本中相同字符的数量除以全部字符的数量,不就是一个相似度了吗?似乎这个想法不错。

代码语言:javascript复制
a = '日本多大的面积'
b = '日本面积多大'
len(set(a) & set(b)) / len(set(a) | set(b))

0.8571428571428571

后来于小文上网查了下,发现这种方法有个很高大上的名字,叫:Jaccard Distance

中文是有词,但又未分词的

我们知道英文大部分情况是以空格分割的一个一个单词,但是中文并没有,中文只是有时候会有一些标点符号而已。那么就带来一个问题,比如说有人想问时间,那么“日本时间”和“本日时间”显然不是一个时间,但是如果按照刚才的算法,它们相似度就是100%了。

所以我们有两种可能的方法:

  1. 用某个方法将中文分词切割,不以字符为最小单位,以词为最小单位
  2. 我们发现字符和字符之间的关联,是有意义的,比如我们把“日本时间”,每两个字连一起,就是有了“日本,本时,时间”,其中“日本”和“时间”都是有意义的,而如果把“本日时间”也这样分割得到的就是“本日,日时,时间”,也就是本来100%的相似度,在2元字符组合的情况下只有一个词“时间”相同了。我们把这种方法一般称为2-gram,相对的,也有3-gram,或者N-gram。

因为分词需要引入额外的分词工具,所以于小文决定先试试2的效果,然后于小文很喜欢正则表达式的各种技巧,决定装个逼。

代码语言:javascript复制
>>> re.findall('(?=(.{2}))', '日本时间')
['日本', '本时', '时间']
>>> re.findall('(?=(.{2}))', '本日时间')
['本日', '日时', '时间']
>>> 
>>> a = set(re.findall('(?=(.{2}))', '日本时间'))
>>> b = set(re.findall('(?=(.{2}))', '本日时间'))
>>> len(a & b) / len(a | b)
0.2

用2-gram方法分了5个词,只有“时间”是一样的,所以相似度一下来就从100%降到了20%

词与词不同权

于小文发现了一个问题,有些问题中有很多“废话”,例如

  • 日本的面积到底有多大?
  • 日本的大小?
  • 我家的面积到底有多大?

显然第一个问题和第二个问题是一个问题,但是如果按照上面的算法,夏然第一个问题和第三个问题重复的词更多。

于小文脑海中的第一个想法:去掉“没用的”词,例如去掉“到底”,“有”,“的”,这种经常在问题中出现但是又没什么实际意义的词。

如果这么做确实能解决一部分问题,不过引申出另外的问题,一个是很难判断哪些词真的没用,例如“游戏打到底是什么?”,这里的“到底”就似乎很有用。另一个问题是,这些词可能有很多很多,人工添加很累。

然后于小文发现,有用的词往往是不常见的,没用的词往往是常见的,例如“的”,“有”都很常见,但是“日本”,“面积”在日常说话中并不常见。

所以一个显而易见的方法就是给不常见的词一个很高的权重,常见的词给一个低权重。这个权重只要通过对所有已有的问题都统计一下就可以了,可以以中文分词为基础也可以以2-gram为基础,如果完全没出现的词可以给一个比较高的权重。

于小文的另一个发现,是问题中,经常提及的词,应该是重要的,例如一个问题是:“switch到底哪好了我发现好多人都买了switch”,这里switch提到了两次,应该是一个比较重要的词。如果我们仅仅用所有问题统计的词权重就忽略了问题(当前文本)本身中的特性(权重),于是决定把这两个特性结合一下。

然后于小文搜了一下发现这个叫TFIDF。

语义相似度

于小文发现有些问题中的重要词完全不一样,但是确是指一个东西?

有人问:“厕所怎么装修”,有人问“洗手间怎么装修”,“洗手间”和“厕所”应该是一个意思,吧?但是显然它们从文本上没有任何相似性。

也就是说这两个词从“字符”级别上完全没相似性,但是从“含义”上有相似性。

那么怎么判断这个“含义”相似性呢?

还是先来朴素的想法,于小文最容易想到的方法是,我维护一个列表,这里面列出所有的能找到的同义词,例如WoW可能跟魔兽世界是一个词,厕所和跟洗手间,然后在判断问题相似性的时候把此表中出现的词都尝试一下,也许每次选取一个在遍历尝试过程中最高的相似度就好。

这个方法显然是有问题的,一个最直接的问题是同义词并没有那么好找,而且人们也在不断的发明新词。

必须,得换一个高级点的思路了……

高级思路警告。

假设我们有一个神奇的函数,例如叫他T,T可以把输入的词,都转换为一个真实且唯一的含义的某个符号,也就是说它有一个如下的性质:

T(厕所) = T(洗手间)

因为厕所和洗手间的含义相同,它们经过T转换后的符号也肯定相同。

那么T还有什么性质呢,就是我们从句子的其他词(上下文)中是可以预测到T的。

那么怎么找到T呢?这里需要一个填空题一样的技巧。

例如我们说填空题:“我来到了海边,放眼望去一片()”,括号里面填什么?

我们假设答案是填“蓝”或者“蓝色”吧,因为填这两个都可以满足含义,所以我们可以知道 T(蓝) = T(蓝色)。

我们把这类填空题也定义为一个函数,例如叫V,所以可以得到下面的关系:

V(我来到了海边,放眼望去一片) = T(蓝色) = T(蓝)

当然在实际上我们其实并不需要完全等于,所以粗略的也可以把上面修改为:

V(我来到了海边,放眼望去一片) ≈ T(蓝色) ≈ T(蓝)

也就是说我们假设找到了函数T和V,就可以让“蓝色”、“蓝”,归一到一个一致或至少差不多的符号上,这样就可以用这两个字符之间的相似度来代替它们的字符相似度,也就是可以让“厕所”和“洗手间”等价。

我们把“差不多的符号”,修改为“差不多相似”的向量,把T函数定义为一个从符号集合到向量集合的单射函数,把函数V定义为某种线性或非线性函数。而这两个函数都是可以通过机器学习的反向传播算法得到的。

那么为什么需要填空题呢,因为填空题不需要太多额外的处理,只要我们从一个完整句子挖空一个词就可以了,非常容易得到。

而且我们也不需要设计T(蓝色) ≈ T(蓝)的部分,只要我们通过训练能使得函数T和函数V满足:

V(我来到了海边,放眼望去一片) ≈ T(蓝色)

V(我来到了海边,放眼望去一片) ≈ T(蓝)

自然可以得到 T(蓝色) ≈ T(蓝)

以上是Word2vector的某种解释。

V(我来到了海边,放眼望去一片) ≈ T(蓝) 可以理解为基于V得到T(蓝)的概率,我们一般把这样构建词之间概率关系称为语言模型。

相似度的扩展

分类

于小文遇到一个问题,有很多人不仅仅是为了提问,也是会去看看别人问的问题都有什么。这个时候最好能把问题做一些分类。

但是于小文只会用相似度,于是想,什么是分类?

假设我们有分类A、B、C,新来一个问题x,那么所谓分类不就是看x跟A、B、C哪个相似?或者说跟A、B、C里面的哪些问题相似?

所以一个最朴素的想法是这样的:假设问题x跟A、B、C里面所有的问题都比较一下相似度,然后看排前n个最相似的问题都是分别属于A、B、C哪些里面的,投个票,就好了。

这个算法基本上是K-Nearest-Neighbour

聚类

那么我现在没有A、B、C这些个分类啊?

一个方法是于小文辛苦一点,自己先把问题都过一遍,按照某个标准,分个几万个问题,先把A、B、C都先大概分出来。

但是于小文很懒,真的很懒。

代码语言:javascript复制
相似的问题总是相似的 —— 于小文

我们假设我们要把问题分成10类,假设我们每类先随机放入一个问题。第11个问题,看它跟这10个分类里面的问题哪个最相似,哪个最相似就放入里面,这样不断的往里放。当然还要注意就是分类里面已有的问题,如果它其实跟本身分类的问题都不太相似,反而跟别的分类的一些问题更相似,就挑出来放到别的分类里面。

重复上面两个过程,不就是把相似的都放一起了?

然后于小文只需要大概看看每个分类,给一些明确的分类起个名字就好了

这个算法基本上是K-means

搜索

搜索和聚类、分类其实差不多,我们可以认为语义搜索从某种意义上来说,是拿搜索中的“查询语句”去找已有资料库中的最相似的资料。

这方面首先我们需要有一种判断相似度的方法,其次是可能构建分层次的搜索方法。

例如先进行粗力度的大范围搜索,例如用annoy库,或者jina这样的引擎。

然后可以再进行某些细粒度的排序,例如用某种Siamese Networks的复杂模型,最终得到结果。

实体相似度

“大家给我推荐个笔记本呗?能玩游戏的”

“大家给我推荐个笔记本呗?要书写流畅的”

上面两个笔记本是一个意思吗?当然是有可能的了,只靠纸笔也是可以玩龙与地下城跑团游戏的,也有笔记本因为带有触摸屏幕也是涉及到书写是否流畅的。但是,显然更可能的是,第一个问题是希望推荐笔记本电脑,第二个是希望推荐书写用的纸质笔记本。

那么怎么区别上面两个问题呢,那就需要我们首先知道我们要对比的是“笔记本”这个实体,在这个基础上判断两句话的相似度。因为相似度实际上是我们的目标定义的,例如我们可以说两个句子都是推荐东西时就是相似的,这样定义的话,上面两句话其实是相似的。我们也可以定义,两个句子提到的实体是一致的时候才是相似的,这就需要对具体实体进行判断了。

假设我们对实体进行相似度判断,就需要这个实体的上下文,在这里可以简单理解为句子里的其他词汇,因为这些词汇可以帮助我们定义这个实体到底是什么,所以是我们所需的上下文。

这个任务也很类似实体链接(Entity Linking)和分面情感分析(Aspect-base Sentiment Analysis)。

广义语义相似度

广义语义相似度,于小文发明的词?

当我们把语义相似度扩展到广义,比如说文字的语义相似度可以用一个复杂的函数(模型)表达的时候,相似度实际上就可以是很多东西了。

例如我们说文本分类模型,本质上是对新来的样本,是模型去判断它和学过的样本的相似度对比并最终打分、聚合而得到的结果。

例如我们说记忆与联想,本质上是我们在脑中进行某种相似度搜索,而得到的答案。

0 人点赞