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

1 前言

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

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

2 词袋方法

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

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

基于TF-IDF词袋方法

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

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

词袋方法缺陷

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

3 相似度计算

在进行句子相似度计算时,有很多可选方法。本文选择了最基本的余弦(cosine)相似度方法,其背后的思想是计算两个句子向量的夹角,夹角越小,相似度越高。假定V_1V_2是两个句子的向量,维度都为n,即V_1=(x_1,x_2,..,x_n)V_2=(y_1,y_2,..,y_n),它们的余弦值等于
cos(V_1,V_2)=\frac{\sum_{i=1}^{n}{(x_i*y_i)}}{\sqrt{\sum_{i=1}^{n}{(x_i)^2}}*\sqrt{\sum_{i=1}^{n}{(y_i)^2}}}=\frac{V_1.V_2}{||V_1||*||V_2||}

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能得到一定程度的处理,这类方法下次再介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容