word2vec相似度计算_无监督句向量生成USIF算法来计算语义相似度

在NLP领域比较重要的就是语义相似度计算,可用于非常多方面的应用,比如搜索、智能问答系统、多轮对话、基于内容的推荐系统召回模块等。能够在语义相似度任务这些领域会有巨大提升。

像搜索领域中用到的elasticsearch分布式高性能搜索工具中用到的BM25算法,是通过词频和逆文档形成的稀疏矩阵来计算相似度。这种方法没有考虑到句子之间的语义关系,只是考虑到词频带来的影响。BM25是tf-idf的改进版,考虑到词频无上限增长问题,来抑制词频过大带来的影响,还使用了文档归一化,将句子归一化到平均长度,减弱句子长度带来的扰动。但是这种算法也适用于信息召回,并不能识别到搜索的重要程度。最后肯定还是需要对于搜索内容进行一个排序。

在工业界领域中像有监督的训练数据获取困难,往往需要标注很多数据才能使相似度效果变佳。人工标注的成本也是很高,在一部分中小公司对于不太愿意来承担这一部分的成本。这时候无监督的句向量生成就比较适用了。无监督句向量生成不仅仅是在训练时候只需要cpu即可完成,而且在计算的时候只用cpu也是很快能够计算完成。

有监督算法比较依赖于标注数据,但是标注数据需要大量人工成本,实现语义相似度计算的模型也有很多,比如孪生网络、DSSM算法、Bert等。是Bert做语义相似度有几种做法,比如将要对比的句子拼接在一起输入Bert然后接一层sigmoid激活函数的全连接或者将Bert进行孪生网络的方式使用孪生Bert,然后权重共享等操作来实现。不过一般用比较多的可能是前两种。非常不推荐直接使用Bert的句向量来做余弦相似度计算,效果非常差,具体原因我后面再写篇文章来分析分析,这里就不做过多的赘述了。

无监督算法现在是有几种方法比如通过词向量直接相加平均、词向量通过tf-idf进行加权、词向量使用sif算法加权等。词向量直接相加平均没有考虑到训练样本中词频带来的影响和句子长度带来的影响。tf-idf加权却是考虑到词频带来的影响,但是效果提升不是特别大。2016年提出来SIF加权算法,是当时顶会最好的一篇无监督句向量生成的算法,使用随机游走来推算句子的生成概率。我今天要讲的也就是他的改进版USIF算法,是2018年ACL顶会最好的一篇无监督句向量生成算法。

USIF算法和SIF算法思想上差不了太多,如果没看过SIF算法的朋友可以去看一下SIF算法。都是通过随机游走来推算出句向量生成的一个概率公式,最后化简成一个加权的公式。

bad7f9fedea69f38f7b8d31808c488ad.png

这是USIF论文的名字,感兴趣的可以直接去看看原论文。我接下来讲解下这篇论文。

一、加权公式推导

论文中通过随机游走对于单词w在t时刻产生的概率由对数线性模型给出一个公式

3286555e096b9382a8758dbac16b815f.png
公式1

假设句向量ct在句子生成的整个过程中变化不大,将贯穿所有时间步的句向量序列{ct}替换为单个句向量cs。

则给出一个句子向量cs生成一个单词w的概率为

198bd32dc89ad7049a627c3259f7806c.png
公式2

是句子向量,其中
为标量超参数,V为词汇表,
参数组成的线性组合。句子embedding可以定义为生成句子的句向量
最大后验估计。论文中假设
的潜在语义空间是均匀分布的,则分配函数
对于所有的
是相同的则允许他被一个常数Z代替。

d7b3d6d1f9db10bf9a8893e03c77707b.png
公式3

由于Z不能计算,a是未知的,a是一个需要调优的超参数。这种加权方案称为平滑逆频率(SIF),并对更频繁的单词给予较低的权重。使用语料库中所有{

}的第一个主成分作为常用语篇向量
的估计。然后通过在公共分量上减去加权平均值的投影得到最终的话语向量
(公共分量去除)

ec9048ae38a6581695817a325d45af81.png
公式4

这是SIF的一个思想,USIF也是根据这个思想进行改进。

a861f0c7faa1d95cc9355693e053238a.png
公式5

有两个x和y两个比较少出现的词,而且x和y词语义不相似,其中x,y是非停用词(即那些具有不可忽视的重要性的词),将词向量

分解为两个不相似的词相加平均。

be0b7cd1101444321edecacc5261b08b.png
公式6

分解可以得到上式,带入SIF的加权函数中可以得到下式:

fe32f42f8e16c4cd42b79db4d711c204.png
公式7

我们对于

进行重新赋值并且保证符合上式的关系:

e3e259c99a3bdb97dcbb1acea67fb524.png
公式8

4965d258cac95181e216a33a0a1eacd0.png
公式9

将结果带入公式1可以得到公式10

5c05331335b52469f83eb9c8ba8eb4d4.png
公式10

为了解决单词向量长度的混淆效应,论文提出了一种随机漫步模型,其中在t时刻观察到单词w的概率,t时刻句子向量

和单词向量
之间的角距离成反比。

776a90e6a487a5c2d87bd5e00431a21c.png
公式11

因此,角距离也可以解释为L2归一化词向量与L2归一化话语向量在单位范围上的最短路径长度。因为角距离范围是

之间,所以除以
是量纲范围归一化到[0,1]区间。

因为时间步句子向量变化不大,公式11带入公式2中可得公式12:

51b87a6c8328e4212a4f303d4fdf2821.png
公式12

我们将某些句子的嵌入定义为生成句子的句子向量的映射估计。假设对可能的句子有一致的先验,映射估计也是对句子的最大似然估计。句子的对数似然值为:

1a066d5e7b52f745ff5e687c888a48e8.png
公式13

为了使对数似然函数最大化,使用泰勒一阶展开式逼近。

27e8680c700386e67344145c7e2a796b.png
公式14

4ef546499b9428a035327f395b27b6f1.png
公式15

公式15在单位范围可近似于得到公式16:

8bc78ba86a8509e5d6e46ff2346ed335.png
公式16

a是单词w偶然产生的概率,而不是由句子向量产生的概率。为了估计a,我们首先考虑一个句子向量cs在随机行走的n步中至少产生一个随机单词w的概率

deb9256d94e61b605e5fa99ba288d925.png
公式17

Z和α都和词表的数量有关,则在单位范围上,潜在句子矢量与单词矢量之间的期望距离为

,所以

ccb322ddae56674287745d19929d2cc2.png

f4bcbfab442d0ddc38e55cc8b806db12.png
公式18

0e9eadc79ba194e3759a56a314e98de3.png
公式19

98314e8f0929faccf9c3cffafd861a27.png
公式20

根据上面式子可以推算公式21:

9bbfce679f714e667c776048dd00d5c7.png
公式21

f43f398f0fdb337236f1622741aaf645.png
公式22

二、减去句子向量投影上的主成分

在SIF算法中提到减去句子第一主成分上的投影,可以减去训练句子中组成的共性表达,提高句子的语义表达能力。这篇USIF算法在此基础上进行改进,理论上去除更多的主成分会增加表达。作者实验中发现超过5的奇异向量不能解释更多的方差。所以论文去除了前五的主成分,并且使用算出来的奇异值进行奇异向量的加权。

71b4fbd18e360741668b75e5095c0f37.png
公式23

句子向量减去前五的主成分公式:

c55eb5a4dca9bf0bfbf89b0555bfe6f9.png
公式24

注意事项:

在计算语义相似度之前先要算出来训练数据中所有句子一起的截断奇异值分解模型,便于在后续语义相似度中应用。

三、USIF算法的代码实现

#*- coding:utf-8 -*- coding

import json
import numpy as np
from sklearn.decomposition import TruncatedSVD
from numba import jit
from gensim.models import KeyedVectors
import pandas as pd
import joblib
class word2prob(object):

    def __init__(self,word_count_file_path):
        file_path = word_count_file_path
        with open(file_path, 'r',encoding='utf-8') as f:
            self.prob = json.load(f)
            total = sum(self.prob.values())


        self.prob = {k: (self.prob[k] / total) for k in self.prob}
        self.min_prob = min(self.prob.values())
        self.count = total

    def __getitem__(self, w):
        return self.prob.get(w.lower(), self.min_prob)

    def __contains__(self, w):
        return w.lower() in self.prob

    def __len__(self):
        return len(self.prob)

    def vocab(self):
        return iter(self.prob.keys())

class word2vec(object):

    def __init__(self,embedding_path):

        self.embedding_path = embedding_path
        vectors = KeyedVectors.load(embedding_path, mmap='r+')    #Vectors(name=embedding_path)
        self.vectors = vectors

    def __getitem__(self, w):
        return self.vectors[w]

    def __contains__(self, w):
        return w in self.vectors

class uSIF(object):
    def __init__(self, vec, prob, n=11, m=5):

        self.vec = vec
        self.m = m

        if not (isinstance(n, int) and n > 0):
            raise TypeError("n should be a positive integer")

        vocab_size = float(len(prob))
        threshold = 1 - (1 - 1 / vocab_size) ** n
        alpha = len([w for w in prob.vocab() if prob[w] > threshold]) / vocab_size
        Z = 0.5 * vocab_size
        self.a = (1 - alpha) / (alpha * Z)

        self.weight = lambda word: (self.a / (0.5 * self.a + prob[word]))

    @jit
    def _to_vec(self, sentence):
        tokens = sentence.split()
        #v_t = np.zeros(300)
        if tokens == []:
            return np.zeros(300) + self.a
        else:
            # for i in tokens:
            #     v_t +=self.vec[i]*self.weight(i)/len(tokens)
            # return v_t
            v_t = np.array([self.vec[t] for t in tokens])
            v_t = v_t * (1.0 / np.linalg.norm(v_t, axis=0))
            v_t = np.array([self.weight(t) * v_t[i, :] for i, t in enumerate(tokens)])
            return np.mean(v_t, axis=0)

    def embed(self, sentences):
        sentences_vectors = self._to_vec(sentences).reshape(1,-1)
        #sentences_vectors = [self._to_vec(s) for s in sentences.split()]
        if self.m == 0:
            return sentences_vectors
        proj = lambda a, b: a.dot(b.transpose())*b
        #svd = TruncatedSVD(n_components=self.m, n_iter=7,random_state=0).fit(sentences_vectors)
        svd = joblib.load(filename="E:/微信文本语料/trigram/svd_n5.pkl")
        for i in range(self.m):
            lambda_i = (svd.singular_values_[i] ** 2) / (svd.singular_values_ ** 2).sum()
            pc = svd.components_[i]
            sentences_vectors = [v_s - lambda_i*proj(v_s, pc) for v_s in sentences_vectors]
        return np.array(sentences_vectors)




四、使用USIF算法给语义相似度提升

我在公司实际项目中应用了USIF算法,并且使用百度千言数据集的训练集作为评测数据。

f378aa3b0930f30b6c481d8ea4c88c4e.png
百度千言数据集的训练集

55af83dd443b034b569d07d6119a4e0c.png
百度千言数据集上评测MAP

在无监督句子表征上USIF虽然能够带来提升,但是还是需要对于词向量训练有着很高的要求,我单使用词向量加权平均可以到达MAP 0.9左右,语料处理和词向量模型和参数调节。我这里使用的是FastText无监督训练词向量,FastText的子词拼接可以一定程度上避免OOV情况产生。

具体的代码和评测数据,过一段时间会放在GitHub上,并在文章中插入链接。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值