【论文阅读】From Word Embeddings To Document Distances

论文介绍的WMD(Word Mover's Distance)是一种基于词嵌入(word embedding)计算两个文本之间距离的方法。

本文跳过词嵌入的介绍,直接进入WMD的实现过程。

 

词的相似性

假设我们有一个包含$n$个词的词典库,用word2vec训练好的这$n$个词的矩阵为:

$$X \in R^{d*n} \tag{1}$$

矩阵$X$中的第$i$列,$x_i$是一个$d$维向量,表示第$i$个单词的词向量。

这样两个词之间的相似性可以通过词向量之间的欧氏距离(Euclidean distance)来表示:

$$c(i, j) = ||x_i - x_j||_2 \tag{2}$$

这里,第$i$个词和第$j$个词之间的相似性记为$c(i, j)$,即等于两者的词向量$x_i$和$x_j$之间的欧氏距离。

 

文本间距

对于一篇文本,我们可以用归一化的nBOW(normalized bag-of-words)向量$d$表示:

$$d = (d_1, d_2, \cdots, d_i, \cdots, d_n) \tag{3}$$

$$d_i = \frac{c_i}{\sum_{j=1}^{n} c_j}, \,\,\, i = 1, 2, \cdots, n \tag{4}$$

这里的$c_i$表示第$i$个词在文本出现的次数,统计所有词的出现次数,我们可以计算出每个词出现的比率$d_i, \,\,\, i = 1, 2, \cdots, n$。

现在有两篇文本,它们的nBOW表示分别为$d$和$d'$。

对于文本之间的距离度量,我们先定义一个矩阵$T \in R^{n*n}$,按照我自己的理解,矩阵$T$中一项$T_{ij} \geq 0$按是$d$中的第$i$个词有多大的可能转换到$d'$中的第$j$个词(原文: Let  $T ∈ R^{n×n}$ be a (sparse) flow matrix where $T_{ij} ≥ 0$ denotes how much of word $i$ in $d$ travels to word $j$ in $d'$)。

为了使$d$全部翻译成$d'$,需要满足一下条件,输出流$\sum_j T_{ij} = d_i$,输入流$\sum_i T_{ij} = d_j'$

我们可以这样定义两个文本之间的距离:$d$中的每一个词与$d'$中的每一个词的之间的相似性的加权和,即

$$\sum_{i,j} T_{ij} c(i, j)$$

$\sum_{i,j} T_{ij} c(i, j)$的最小取值就是论文中给出的WMD(Word move distance)。

\begin{align*}
& \min_{T \geq 0} \sum_{i,j} T_{ij} c(i, j) \\
& s.t. \,\,\,\,\,\, \sum_{j=1}^n T_{ij} = d_i \,\, \forall i \in \{1, \cdots, n\} \\
& \sum_{i=1}^n T_{ij} = d_j' \,\, \forall j \in \{1, \cdots, n\} \tag{5}
\end{align*}

 

快速计算文本间距

理论上,由于WMD的最优问题的时间复杂度是$O(p^3 log p)$,这里$p$表示$d$中有多个词,重复不计。

因为对计算大文本间的WMD需要的时间是非常大,所以,有必要采取一些trick,近似得到我们要计算的目标。 

WCD

我们知道,一个文本$d$可以用nBOW向量表示,$d = (d_1, d_2, \cdots, d_i, \cdots, d_n)$,向量中的第$i$个词可以用词库中第$i$个词在这个文本中出现次数与文本总词数(去除停用词)之比。

由word2vec得到词向量矩阵$X = (x_1, x_2, \cdots, x_i, \cdots, x_n)$,其中$x_i$表示词库中第$i$个词的词向量。

那么$d_i * x_i$表示这个本文中第i个词的词向量的加权($d_i$)表示。

所以,文本$d$可以用向量$Xd = \sum_i d_i x_i$表示。

定义$||Xd - Xd'||_2$为两个文本的WCD(Word centroid distance)。

由三角形不等式知,对于$p-norm$(p范数)有

$${||x||}_p = {(\sum_{i=1}^n {|x_i|}^p)}^{1/p} \tag{6}$$

当$p=2$时,$2-norm$表示的是欧氏距离。

对于WMD,我们有

\begin{align*}
\sum_{i=1, j=1}^n T_{ij} c(i,j)
&= \sum_{i=1, j=1}^n T_{ij} {||x_i - x_j'||}_2 \\
&= \sum_{i=1, j=1}^n {||T_{ij}(x_i - x_j')||}_2 \\
&\geq {||\sum_{i=1, j=1}^n T_{ij}(x_i - x_j')||}_2 \\
&= {||\sum_{i=1}^n {(\sum_{j=1}^n T_{ij}) x_i} - \sum_{j=1}^n {(\sum_{i=1}^n T_{ij}) x_j}||}_2 \\
&= {||\sum_{i=1}^n {d_i x_i} - \sum_{j=1}^n {d_j' x_j'}||}_2 \\
&= {||Xd - Xd'||}_2 \tag{7}
\end{align*}

所以,WCD是WMD的一个下界,时间复杂度是$O(dp)$,我们可以通过快速计算WCD近似得到WMD。

 

RWMD

为了进一步提升WMD的下界,我们可以通过下WMD计算的约束条件放宽松些,论文中给出的是放弃约束$\sum_{i=1}^n T_{ij} = d_j' \,\,\, \forall i \in \{1, \cdots, n\}$

同时定义$T_{ij}^{\ast}$

$$T_{ij}^{\ast} = \begin{cases}
& d_i,  \text{ if } j = argmin_j c(i,j) \\
& 0,  \text{ otherwise }
\end{cases} \tag{8}$$

$$j^{\ast} = argmin_j c(i, j) \tag{9}$$

所以有

\begin{align*}
\sum_{i,j} T_{ij} c(i, j)
&\geq \sum_{j} T_{ij} c(i, j^{\ast}) \\
&= c(i, j^\ast ) \sum_{j} T_{ij} \\
&= c(i, j^\ast ) d_i \\
&= \sum_j T_{ij}^{\ast} c(i, j) \tag{10}
\end{align*}

 

Prefetch and prune

这样我们就得到了WMD两个下界,WCD和RWMD。如下图,根据原文的随机测试的统计结果,我们看到RWMD比WCD更加接近WMD的值。

 我们可以综合利用WCD,RWMD,WMD,查找给定文本在文本集中距离最近的k个文本时:

  1. 首先把文本集中文本按照它们到给定文本的WCD距离由小到大排序;
  2. 计算前k个文本的WMD距离,记第k近邻的文本的WMD为$d_{WMD}^k$;
  3. 对于文本集中剩下的每一个文本,计算其到给定文本的RWMD,记为$d_{RWMD}^i,i = k+1, k+2, \cdots$,如果$d_{RWMD}^i > d_{WMD}^k$,则把它剪除(pruned),否则,计算这个文本到给定文本的WMD距离,如果有需要,则用当前文本替换第k个最近邻文本(prefetch)。

 原文指出,由于RWMD非常接近WMD的值,在一些数据集上,步骤$3$的通过计算RWMD距离,可以剪除95%的文本,大大节省了整个的查找时间。

代码实现

由式$(5)$知,WMD是一个带约束的线性规划问题(LP),下面给出简单实现WMD的代码。

### 准备工作,下载词向量文件--glove.6B.zip https://nlp.stanford.edu/projects/glove/

'''
scipy.optimize._linprog def linprog(c: int,
            A_ub: Optional[int] = None,
            b_ub: Optional[int] = None,
            A_eq: Optional[int] = None,
            b_eq: Optional[int] = None,
            bounds: Optional[Iterable] = None,
            method: Optional[str] = 'simplex',
            callback: Optional[Callable] = None,
            options: Optional[dict] = None) -> OptimizeResult

矩阵A:就是约束条件的系数(等号左边的系数)
矩阵B:就是约束条件的值(等号右边)
矩阵C:目标函数的系数值
'''

# 下载词向量文件glove.6B.zip https://nlp.stanford.edu/projects/glove/
from gensim.scripts.glove2word2vec import glove2word2vec
from gensim.models import KeyedVectors
from collections import defaultdict, Counter
from scipy import optimize as opt
import numpy as np

# 读取Glove文件,这里使用维度为100的词向量。
def load_embedding():
    glovefile = "glove.6B.100d.txt"
    word2vecfile = "word2vec.txt"

    with open(glovefile, 'r', encoding = 'utf-8') as f:
        texts = f.readlines()
    token2id = defaultdict(lambda: -1)

    texts = [txt.split() for txt in texts]

    tokens = [txt[0] for txt in texts]
    embeddings = [txt[1:] for txt in texts]

    for i, t in enumerate(tokens):
        token2id[t] = i

    embedding_mat = np.array(embeddings).astype(float)
    # print(embedding_mat.shape)
    return embedding_mat, token2id

# 计算文本间的WMD
def WMD(sent1, sent2, embedding_mat, token2id):
    # 预处理文本,获取文本的nBOW表示
    n, _ = embedding_mat.shape
    s1 = list(map(lambda x: x.lower(), sent1.split()))
    s2 = list(map(lambda x: x.lower(), sent2.split()))

    c = Counter(s1)
    d1 = np.array([0.0 for i in range(n)])
    for k, v in c.items():
        d1[token2id[k]] = float(v)
    d1 = d1 / d1.sum()
    

    c = Counter(s2)
    d2 = np.array([0.0 for i in range(n)])
    for k, v in c.items():
        d2[token2id[k]] = float(v)
    d2 = d2 / d2.sum()
    
    # 计算向量之差的2-norm作为向量间的距离
    def get_2norm_distance(x1, x2):
        s = x1 - x2
        dist = np.linalg.norm(s, ord = 2)
        return dist
    
    s1 = list(set(s1))
    s2 = list(set(s2))
    
    # 定义T_ij的系数向量
    n, m = len(s1), len(s2)
    T = np.array([0.0 for _ in range(n*m)])
    for i in range(n * m):
            T[i] = get_2norm_distance(embedding_mat[token2id[s1[int(i / m)]]], embedding_mat[token2id[s2[int(i % m)]]]) 
    
    # 定义等式约束的左边系数矩阵
    a1 = np.ones((n, m))
    t1 = np.zeros((n * n * m)).reshape(n, -1)
    for i in range(n):
        for j in range(m):
            t1[i, i * m + j] = a1[i][j]

    a2 = np.ones((m, n))
    t2 = np.zeros((m * n * m)).reshape(m, -1)
    for i in range(m):
        for j in range(n):
            t2[i, j * m + i] = a2[i][j]

    a = np.vstack((t1, t2))
    
    # 定义等式约束的右边
    const1 = np.array([d1[token2id[c]] for c in s1])
    const2 = np.array([d2[token2id[c]] for c in s2])
    b = np.hstack((const1, const2))

    
    lim = (0, None)
    bounds = tuple([lim for _ in range(n * m)])
    res = opt.linprog(T, None, None, a, b, bounds = bounds)
    wmd_dist = np.dot(T, res["x"])
    return wmd_dist

if __name__ == '__main__':
    s1 = "Obama speaks to the media in Illinois"
    s2 = "The President greets the press in Chicago"
    s3 = "The band gave a concert in Japan"
    embedding_mat, token2id = load_embedding()
    print('The WMD between s1 and s2 is: ', WMD(s1, s2, embedding_mat, token2id))
    print('The WMD between s1 and s3 is: ', WMD(s1, s3, embedding_mat, token2id))

  

运行结果

The WMD between s1 and s2 is:  3.4892724527705172
The WMD between s1 and s3 is:  4.544411509507881

Reference

[1]  Triangle inequality

[2]  MINIMUM COST NETWORK FLOWS

posted on 2020-06-05 18:38  ClementCJ  阅读(461)  评论(2)    收藏  举报

导航