利用TF-IDF进行句子相似度计算

1 前言

在NLP机器学习任务中,一个首要的步骤就是将词向量化,也称为词编码。对于词编码,目前主要存在两类方法,一是词袋方法,二是分布式表示;前者又称为 one-hot 编码,是传统的经典方法。分布式表示(distribution representation)是现在流行的方法,也是深度学习必要步骤,对应着embedding layer。

今天主要分享如何利用词袋的方法向量化,然后做句子相似度计算任务。虽然词袋方法已不是主流,但它背后的思想也是分布式表示方法的基础,理解它的原理,有助于我们更好理解现在各种各样新的embedding方法。

2 词袋方法

基本思想:
step1:先统计语料库生成一个有序词表,假设词表大小为N(即词的数量);
step2:接着初始化一个长度N的零向量(),长度N代表着词和句子向量后的维度;
step3:最后,对一个词,检索它在词表的索引,然后将索引对应的初始向量维度值变为1,生成一个one-hot向量即为该词的向量,若词的索引为3,则词向量为,第4维度为1,其他维度都为0;对于一个句子,遍历句子中的每个词,按词编码的方式将初始向量对应的维度变为1,若句子有3个词,对应词表的索引为0,1,3,这句子向量为,第0,1,3维度为1,其他维度都为0。

举个例子:假设有个有序词表为['临床','表现','及','实验室','检查','即可','做出','诊断'],长度为8。对于‘临床’词来说,在词表的索引为0,则词向量为; 对于‘实验室’词来说,在词表的索引为3,则词向量为;对于“检查做出诊断为:”句子来说,分词后为“检查/做出/诊断/为/:”,每个词在词表对应的索引为4,6,7,后面两个词不在词表中,不考虑,最后句子向量为。在实际情况,一般词表很大,大到可能百万级别,当然也会去掉没太意义的词,如停用词或标点符号。

基于TF-IDF词袋方法

今天实现句子向量化,是基于TF-IDF的词袋方法。该方法很简单,就是在词袋基本思想上,将向量中的1值用该词的tf-idf的值代替,这样的好处就是一句话中不同词的权重是不一样的,重要的词所在维度取得值应该更大些,而tf-idf值就是衡量一个词的重要性。

同样,也有将1用词的词频(tf值)来代替,与基于TF-IDF词袋方法是一致的,但TF-IDF的值比TF值更具有代表性。

词袋方法缺陷

不管词袋方法如何优化,但有一个明显的缺陷:就是编码后的句子向量失去了原有词的顺序,换句话来说就是,丢弃了词的上下文信息,而这在很多NLP任务中是很重要的信息,尤其序列标注任务。也是因为这个问题,才有了现在主流的分布式表征方法,后续详细介绍分布式表征的词向量方法。

3 相似度计算

在进行句子相似度计算时,有很多可选方法。本文选择了最基本的余弦(cosine)相似度方法,其背后的思想是计算两个句子向量的夹角,夹角越小,相似度越高。假定和是两个句子的向量,维度都为n,即,,它们的余弦值等于

4 实验

任务说明:利用TF-IDF词袋方法,进行句子相似度计算。

实验数据:使用上一篇“TF-IDF的理论与实践“(https://www.jianshu.com/p/c55c6cae24ad)中同样的语料库file_corpus,然后从语料库中切分句子,取出现句子频率最高的前10000句子样本集。选取5个样本句子,然后利用相似度来计算出与样本句子最相似的句子。

4.1 生成句子样本集

引入所用的包:

import codecs
import re
import jieba
from scipy import spatial
import json

句子切分

def get_sentence(corpus_dir,sentence_dir):
    
    re_han= re.compile(u"([\u4E00-\u9FD5a-zA-Z0-9]+)")  #the method of cutting text to sentence
    
    file_corpus=codecs.open(corpus_dir,'r',encoding='utf-8')
    file_sentence=codecs.open(sentence_dir,'w',encoding='utf-8')

    st=dict()
    for _,line in enumerate(file_corpus):
        line=line.strip()
        blocks=re_han.split(line)
        for blk in blocks:
            if re_han.match(blk) and len(blk)>10:
                st[blk]=st.get(blk,0)+1

    st=sorted(st.items(),key=lambda x:x[1],reverse=True)
    for s in st[:10000]:
        file_sentence.write(s[0]+'\n')

    file_corpus.close()
    file_sentence.close()

4.2 相似度计算

class ComputeSimilarity():
    def __init__(self,tf_idf_dir):
        self.tf_idf_dir=tf_idf_dir
        self.tf_idf=self.load_dict()
        self.vocab=list(self.tf_idf.keys())
        self.N=len(self.vocab)   
        
    def load_dict(self):
        tf_idf=dict()
        with codecs.open(tf_idf_dir,'r',encoding='utf-8') as f:
            for line in f:
                line=line.split('\t')
                try:
                    assert len(line)==2
                    tf_idf[line[0]]=float(line[1])
                except:
                    pass
        return tf_idf
        
    def similarity(self,s1,s2,use_idf=True):
        v1=np.zeros((1,self.N))
        v2=np.zeros((1,self.N))

        if use_idf:
            for w in jieba.cut(s1):
                if w in self.tf_idf:
                    v=self.tf_idf[w]
                    i=self.vocab.index(w)
                    v1[:,i]+=v

            for w in jieba.cut(s2):
                if w in self.tf_idf:
                    v=self.tf_idf[w]
                    i=self.vocab.index(w)
                    v2[:,i]+=v
        else:
            for w in jieba.cut(s1):
                if w in self.tf_idf:
                    i=self.vocab.index(w)
                    v1[:,i]=1

            for w in jieba.cut(s2):
                if w in self.tf_idf:
                    i=self.vocab.index(w)
                    v2[:,i]=1

        sim=1-spatial.distance.cosine(v1,v2)

        return sim

4.3 样本测试

def test(tf_idf_dir,sentence_dir,test_dir):
    
    cs=ComputeSimilarity(tf_idf_dir)
    ssm=cs.similarity
    
    test_data=[u'临床表现及实验室检查即可做出诊断',
               u'面条汤等容易消化吸收的食物为佳',
               u'每天应该摄入足够的维生素A',
               u'视患者情况逐渐恢复日常活动',
               u'术前1天开始预防性运用广谱抗生素']
    
    model_list=['use_idf','unuse_idf']
    
    file_sentence=codecs.open(sentence_dir,'r',encoding='utf-8')
    train_data=file_sentence.readlines()
    
    for model in model_list:
        if model=='use_idf':
            use_idf=True
        else:
            use_idf=False 
            
        dataset=dict()
        result=dict()
        for s1 in test_data:
            dataset[s1]=dict()
            for s2 in train_data:
                s2=s2.strip()
                if s1!=s2:
                    sim=ssm(s1,s2,use_idf=use_idf)
                    dataset[s1][s2]=dataset[s1].get(s2,0)+sim
        for r in dataset:
            top=sorted(dataset[r].items(),key=lambda x:x[1],reverse=True)
            result[r]=top[0]
            
        with codecs.open(test_dir,'a',encoding='utf-8') as f:
            f.write('--------------The result of %s method------------------\n '%model)
            
            f.write(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=False))
            f.write('\n\n')

    file_sentence.close()

执行代码

if __name__=="__main__":
    sentence_dir=r'E:\jianshu\data\file_sentence.txt'
    tf_idf_dir=r'E:\jianshu\data\tf_idf.txt'
    test_dir=r'E:\jianshu\data\test_result.txt'
    test(tf_idf_dir,sentence_dir,test_dir)

5 结果分析

将跑出的test_result.txt的结果用表格形式展示出来,如下图:

运行结果

从5条样例数据来看,在二者匹配结果一致的情况下(第2,4,5句子),使用tf_idf的方法(use_idf)计算的相似度更高些;通过第1个句子对比看,二者匹配差异在“实验室”这个词,这个词的tf_idf影响了最终的结果,显示use_idf优于后者;通过第2个句子对比看,应该unuse_idf结果更好些,出现这种情况的原因是词“维生素“的tf_idf很高的词且匹配句子重复出现,就导致这个句子匹配度提升,这种情形下是不合理的。

整体来说,tf_idf相似度计算会更合理些,但也不是完美的,当句子的匹配中涉及语义理解的含义时,往往这两种方法都不能很好处理,这时候可以利用word2vec或者bert能得到一定程度的处理,这类方法下次再介绍。

你可能感兴趣的:(利用TF-IDF进行句子相似度计算)