problem2.ipynb 15.2 KB

线性规划与Word Mover's Distance

WMD在文本分析领域算作是一个比较经典的算法,它可以用来计算两个文本之间的相似度。 比如问答系统中,可以判断一个用户的query跟哪一个知识库里的问题最相近。而且,计算两个文本之间的相似度这个问题是NLP的核心,这也是为什么文本相似度计算这么重要的原因。

背景: 在文本相似度匹配问题上如果使用tf-idf等模型,那这时候假如两个文本中没有出现共同的单词,则计算出来的相似度为0,但我们知道实际上很多时候单词可能不一样,但表示的内容确是类似的。 比如 ”People like this car“, "Those guys enjoy driving that", 虽然没有任何一样的单词,意思确是类似的。 这是WMD算法提出来的初衷。

WMD作为文本相似度计算的一种方法,最早由Matt J. Kusner, Yu Sun, Nicholas I. Kolkin, Kilian Q. Weinberger等人提出。但实际上它的想法极其简单,可以认为是Transportation Problem用在了词向量上, 其核心是线性规划。 对于Transportation问题在课上已经讲过,仍不清楚的朋友可以回顾一下课程的内容。

在Section B里我们需要做两件事情: 1. 实现WMD算法来计算两个字符串之间的距离。 2. WMD的拓展方案

1. WMD算法的实现

具体算法的实现是基于线性规划问题,细节请参考WMD的论文。 核心思想是把第一个句子转换成第二个句子过程中需要花费的最小cost。

<img src="picture1.png" alt="drawing" width="600"/>

线性规划问题即可以写成如下形式:

<img src="picture2.png" alt="drawing" width="500"/>

这里的参数是$T{ij}$, 需要通过LP Solver去解决。$c(i,j)$指的是两个单词之间的距离, $c{i,j}=||x_i-x_j||_2$。 参考: x2=x12+...+xd2 ||x||_2=\sqrt{x_1^2+...+x_d^2}

为了实现WMD算法,首先需要词向量。 在这里,我们就不自己去训练了,直接使用已经训练好的词向量。 请下载训练好的Glove向量:https://nlp.stanford.edu/projects/glove/ 下载其中的 glove.6B.zip, 并使用d=100维的向量。 由于文件较大,需要一些时间来下载。 如果太慢的话,可以找找国内的镜像。

请注意:提交作业时不要上传此文件, 但文件路径请使用我们给定的路径,不要改变。

In [41]:
# 读取Glove文件。 注意: 不要试图修改文件以及路径
!pip install pulp
import numpy as np
from pulp import *

word2id={}
word2vec={}
with open("glove.6B.100d.txt","r",encoding="utf-8") as glovefile:
    for i,each_line in enumerate(glovefile):
        each_vec=each_line.strip().split(' ')
        word2vec[each_vec[0]]=np.array([float(i) for i in each_vec[1:]])
        word2id[each_vec[0]]=i


#分割句子为单个词
def split_sentence(sent):
    split_list=[]
    for each in sent.strip().split():
        if each in word2id:
            split_list.append(each.strip())
    return split_list
#获取单词对应的向量
def get_vector(split_list,word2vec,word2id):
    word_vector={}
    for each in split_list:
        word_vector[word2id[each]]=word2vec[each]#注意要用ID(即词表中的次序)来标识每个词,这样方便表示词在句子中出现的频率
    return word_vector
#计算句子中每个词的词频
def calculate_freq(split_list,word2id):
    freq=np.zeros(len(word2id))
    for each in split_list:
        freq[word2id[each]]=+1
    return freq/freq.sum()
#计算不同句子之间的单词的词向量距离
def cost(v1,v2):
    dist=np.linalg.norm(v1-v2)
    return dist
#基于词向量计算WMD组合
def solve_lp(split_list1,split_list2,vector1,vector2,freq1,freq2):
    list1_id=[word2id[each] for each in set(split_list1)]
    list2_id=[word2id[each] for each in set(split_list2)]
    
    problem=LpProblem("wmp_lp",LpMinimize)
    T=LpVariable.dicts("T",(list1_id,list2_id),lowBound=0,upBound=1)
    problem+=lpSum([T[i][j]*cost(vector1[i],vector2[j]) for i in list1_id for j in list2_id])
    for i in list1_id:
        problem+=lpSum(T[i][j] for j in list2_id)==freq1[i]
    for j in list2_id:
        problem+=lpSum(T[i][j] for i in list1_id)==freq2[j]
    problem.solve()
    return problem.objective.value()
    
    
# TODO: 编写WMD函数来计算两个句子之间的相似度

def WMD (sent1, sent2):
    """
    这是主要的函数模块。参数sent1是第一个句子, 参数sent2是第二个句子,可以认为没有经过分词。
    在英文里,用空格作为分词符号。
    
    在实现WMD算法的时候,需要用到LP Solver用来解决Transportation proboem. 请使用http://cvxopt.org/examples/tutorial/lp.html
    也可以参考blog: https://scaron.info/blog/linear-programming-in-python-with-cvxopt.html
    
    需要做的事情为:
    
    1. 对句子做分词: 调用 .split() 函数即可
    2. 获取每个单词的词向量。这需要读取文件之后构建embedding matrix. 
    3. 构建lp问题,并用solver解决
    
    可以自行定义其他的函数,但务必不要改写WMD函数名。测试时保证WMD函数能够正确运行。
    """
    split1=split_sentence(sent1)
    split2=split_sentence(sent2)
    
    vector1=get_vector(split1,word2vec,word2id)
    vector2=get_vector(split2,word2vec,word2id)
    
    freq1=calculate_freq(split1,word2id)
    freq2=calculate_freq(split2,word2id)
    
    wmd_dist=solve_lp(split1,split2,vector1,vector2,freq1,freq2)
    
    return wmd_dist
    
Out [41]:
Requirement already satisfied: pulp in /anaconda3/lib/python3.7/site-packages (2.0)
Requirement already satisfied: pyparsing>=2.0.1 in /anaconda3/lib/python3.7/site-packages (from pulp) (2.4.0)
In [45]:
## TODO: 自己写至少4个Test cases来测试一下。 比如 print (WMD("people like this car", "those guys enjoy driving that"))

##       
sent1="someone knows that they konw nothing"
sent2="others believe they know everything"
sent3="what do you think of apple"
sent4="I like eating bananas"
print(WMD(sent1,sent2),WMD(sent1,sent3),WMD(sent3,sent4))

Out [45]:
2.7888952428491187 4.186289379043182 5.713011142655212

2. WMD算法的拓展

2.1 从欧式距离到Mahalanobis距离

WMD算法本身不需要任何的标注好的数据,所以它属于无监督学习。 而且在上述的WMD算法里使用的是欧式距离,$c(i,j)=||x_i-x_j||_2$, 那这种距离有什么缺点呢? 其中一个缺点是欧式距离的计算把一个空间里的每一个维度都看成了同样的权重,也就是每一个维度的重要性都是一致的,而且不同维度之间的相关性也没有考虑进来。如果想把这些信息考虑进来,我们则可以使用一个改进版的距离计算叫做Mahalanobis Distance, 距离计算变成 c(i,j)=(xixj)M(xixj) c(i,j)=(x_i-x_j)^{\top}M(x_i-x_j)

这如何去理解呢? Mahalanobis distance可以理解成: 首先我们对原始空间里的样本做了一层线性的转换, 然后在转换后的空间里计算欧式距离。 我们把这个过程写一下: 原始空间里的点为 xi x_i, 然后我们定义一个转换矩阵 L L, 这时候就可以得到 LxiLxj22=L(xixj)22=(L(xixj))L(xixj)=(xixj)LL(xixj)=(xixj)M(xixj) ||Lx_i - Lx_j||_2^2=||L(x_i-x_j)||_2^2=(L(x_i-x_j))^{\top}L(x_i-x_j)=(x_i-x_j)^{\top}L^{\top}L(x_i-x_j)=(x_i-x_j)^{\top}M(x_i-x_j), 相当于把$L^{\top}L$看做是矩阵$M$。这时候很容易看出来矩阵$M$是PSD(positive semidefinite).

假设我们定义了这种距离,这里的M如何选择呢? 当然,这是需要学出来的! 那为了学出M, 必须要有标注好的训练数据,也就需要监督学习场景!

在这个式子里,第一部分代表的是让同一类型的样本的距离变小, 第二部分代表的是不同类型的样本之间要扩大距离。

  • Q1: 这里$\lambda$起到什么作用?

𝜆 的作用在于决定权重的分配,即更重视同一类型还是不同类型的距离

  • Q2: 在目标函数里有$\eta$值,这个值怎么理解? 如果去设定这个值呢?

𝜂其实表示了两个句子的差异度,比如要想让𝜂大,那么𝑑(S𝑢,Sw)应该尽量大,𝑑(S𝑢,S𝑣)应该尽量小 。可以通过对𝜂设定为参数,取不同的数值,选择让L能得到最小值的𝜂。

这里的$d_{u,v}$指的是$s_u$和$s_v$之间的距离, 而且这个距离被定义为:

\begin{equation} d{u, v}=min{T\geq 0}\sum{i,j}^{}T{ij}c(i,j)^u~~~~ s.t. \sum{j=1}^{}T{ij}=di^u, ~~\sum{i=1}^{}T_{ij}=d_j'^v\end{equation}

这里 c(i,j)=(xixj)M(xixj) c(i,j)=(x_i-x_j)^{\top}M(x_i-x_j)。 所以是不是可以察觉到这个问题目标函数里既包含了参数$M$也包含了线性规划问题。

  • Q3: 请试着去理解上述所有的过程,并回答: 优化问题如何解决呢? 请给出解题的思路 (文字适当配合推导过程)。

要想minL,需要min𝑑(S𝑢,S𝑣)。先随机初始化T,然后利用监督数据,使用梯度下降法求解矩阵M,再通过线性规划求解算法求解T。

对于上述问题,其实我们也可以采用不一样的损失函数来求解M。 一个常用的损失函数叫作 “kNN-LOO error”, 相当于把KNN的准确率转换成了smooth differential loss function. 感兴趣的朋友可以参考: https://papers.nips.cc/paper/6139-supervised-word-movers-distance.pdf

以上是优化部分的一个简短的作业,通过这些练习会对优化理论有更清晰的认知。 Good luck for everyone!