问题出现,为什么需要文本相似度
于小文是一个普通程序员,业余的时候会出于做一些自己的网站,最近他做了一个问答社区,就是大家有什么问题都可以在上面问,然后也会有热心网友来解答的网站。
最近他发现一个问题,很多人在提问的时候都会重复,所以他希望有一个功能,就是假设新问题跟已经有的问题相似,就直接推给答案,就避免了重复提问和解答,以及找到解答的时间了。
那么问题就很清楚的定义了,新问题(文本)与已有的问题(文本)之间怎么算重复问题?
我们能否通过设计一个相似度函数,通过调用 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%了。
所以我们有两种可能的方法:
- 用某个方法将中文分词切割,不以字符为最小单位,以词为最小单位
- 我们发现字符和字符之间的关联,是有意义的,比如我们把“日本时间”,每两个字连一起,就是有了“日本,本时,时间”,其中“日本”和“时间”都是有意义的,而如果把“本日时间”也这样分割得到的就是“本日,日时,时间”,也就是本来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)。
广义语义相似度
广义语义相似度,于小文发明的词?
当我们把语义相似度扩展到广义,比如说文字的语义相似度可以用一个复杂的函数(模型)表达的时候,相似度实际上就可以是很多东西了。
例如我们说文本分类模型,本质上是对新来的样本,是模型去判断它和学过的样本的相似度对比并最终打分、聚合而得到的结果。
例如我们说记忆与联想,本质上是我们在脑中进行某种相似度搜索,而得到的答案。