Dallas98

词向量

在自然语言处理(NLP)中,词向量将单词按照含义编码成向量,从而更好地进行语言建模和特征学习。词向量经常作为下游 NLP 任务的基本组件,出现在文本分类、翻译、问答、推荐等各种任务模型中;此外,我们也可以通过多种方式,训练自己的词向量。(for more, see here

通过本次实验,你将进一步体会词向量的特性,以及词向量模型的加载使用。

实验环境

你可以使用任何你自己熟悉的 python 环境来完成实验,我们提供了 conda 环境配置文件 environment.yml,里面给出了一些必要的环境依赖,请确保你安装了这些依赖。如果你需要使用其他 python 扩展包可自行安装。(本次实验二所需的环境依赖在实验一的环境中都有,因此你也可以跳过下面的虚拟环境创建步骤,直接启用实验一的环境进行实验。)

这里我们推荐使用 conda 来创建管理 python 虚拟环境。

  1. 下载安装 conda。你可以选择安装已经预装了许多常用扩展的 Anaconda 或者没有任何预装的 Miniconda

  2. 打开 Terminal(Windows 可使用 Anaconda Prompt) 并进入当前文件目录,输入以下命令来创建一个名为 nlplab2 的虚拟环境并启用。

conda env create -f environment.yml
conda activate nlplab2
  1. 使用 jupyter-notebook 打开 lab2.ipynb 文件开始下面的实验。
jupyter-notebook lab2.ipynb

实验过程

加载词向量

加载预训练好的词向量。这里我们使用 GLoVe 在中文维基百科语料训练的词向量,词向量维度为 50,词汇量 83W+。为了减少后续的计算时间,我们使用 pickle 模块将词向量模型保存到二进制文件,你可以在 data 目录下看到它。运行下面的代码加载处理后的 50 维词向量。

Notes:

  • 选择 GLoVe 词向量是由于它在词语类比任务上有着更优的特性。
  • GloVe 官网 提供的都是英文的预训练词向量,中文词向量需要自己用语料库训练。具体需要你到维基中文下载网页 zhwiki 下载中文维基百科语料 xml 文件;完成解析抽取、繁体化简、符号停用词过滤、分词等相关处理;下载 GloVe 官方源码 编译预训练。
  • 此处无需你完成整个词向量训练过程,我们已替你训练好,直接加载即可。
import pickle
import numpy as np

with open(\'data/glove.zh.50.pickle\', mode=\'rb\') as f:
    word2vec_map = pickle.load(f)
word2index = dict(zip(word2vec_map.keys(), range(len(word2vec_map))))
index2word = dict(zip(word2index.values(), word2index.keys()))
word_matrix = np.array(list(word2vec_map.values()))
del word2vec_map
print(\'len(word2index):\', len(word2index))
print(\'len(index2word):\', len(index2word))
print(\'word_matrix.shape:\', word_matrix.shape)
len(word2index): 831144
len(index2word): 831144
word_matrix.shape: (831144, 50)

这里我们得到三个变量:

  • word2index:一个字典,以词(str)为 key,以一个 [0, 831144) 间的整数(int)为 value,词语到下标的映射;
  • index2word:一个字典,以 [0, 831144) 间的整数(int)为 key,以对应的词(str)为 value,下标到词语的映射;
  • word_matrix:一个维度为 (831144, 50) 的 numpy.ndarray 矩阵,第 i 行为下标为 i 的词对应的词向量。
    因此,一个词语 word 的词向量可以通过 word_matrix[word2index[word]] 取得,例如:
word_matrix[word2index[\'中科大\']]
array([ 0.476694,  0.012429,  1.334308,  0.833337,  0.422843,  0.404001,
       -0.286563,  0.277617,  0.78868 ,  0.235541,  0.119859,  0.210273,
       -0.294097, -0.157735,  0.008777,  0.902468, -0.244504,  0.116923,
        0.847188, -0.549439,  0.086888, -0.533772, -0.649807, -0.1344  ,
        0.6017  , -0.178695, -0.423312, -0.3307  ,  0.364139, -0.012755,
        0.011284,  0.570228,  0.600845,  0.154449, -0.293372,  0.421692,
        0.274871,  0.700706, -0.160058,  0.345218, -1.173997, -0.345282,
       -0.490619, -0.787566,  0.912973, -0.113094,  0.091646, -0.316972,
        0.962285,  0.372473])

one-hot编码就是保证每个样本中的单个特征只有1位处于状态1,其他的都是0。独热编码即 One-Hot Encoding,又称一位有效编码,其方法是使用N位状态寄存器来对N个状态进行编码,每个状态都由他独立的寄存器位,并且在任意时候,其中只有一位有效。one-hot向量将类别变量转换为机器学习算法易于利用的一种形式的过程,这个向量的表示为一项属性的特征向量,也就是同一时间只有一个激活点(不为0),这个向量只有一个特征是不为0的,其他都是0,特别稀疏。

相比较于词的独热编码向量,fastText,GloVe等词向量包含有更多的信息,也可以更好的表达不同词语间的相似关系。

下面我们来看看如何使用这些词向量来计算两个词语间的相似程度。

余弦相似度(Cosine Similarity)

为了衡量两个词语间的相似程度,我们需要一种合适的计算两个向量间相似程度的方法。给定两个向量 \(\mathbf{u}\)\(\mathbf{v}\),余弦相似度定义为:

\[\text{CosineSimilarity}(\mathbf{u}, \mathbf{v}) = cos(\theta) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \|\mathbf{v}\|} = \frac{\sum_{i=1}^d u_i v_i}{\sqrt{\sum_{i=1}^d u_i^2} \sqrt{\sum_{i=1}^d v_i^2}} \]

其中 \(\mathbf{u} \cdot \mathbf{v}\) 为两个向量 \(\mathbf{u}\)\(\mathbf{v}\) 间的点积,\(\theta\) 为两个向量 \(\mathbf{u}\)\(\mathbf{v}\) 间的夹角,\(\|\mathbf{u}\|\) 是向量 \(\mathbf{u}\) 的 L2 范数,\(d\) 为向量的维数。

余弦相似度的大小取决于两个向量间的夹角的大小,如果 \(\mathbf{u}\)\(\mathbf{v}\) 非常相似余弦相似度的值就非常接近1。

下面请你来实现函数 cosine_similarity 用以计算向量间的余弦相似度。

注意,上面公式给出的是两个向量间余弦相似度的计算过程,也可以认为是一个样本间的计算,即向量 \(\mathbf{u}\) 和向量 \(\mathbf{v}\) 计算得到一个浮点数结果 \(w\)。但为了后续可以进行快速的并行计算,这里要求你的实现可以进行批量处理,即多个不同的向量 \(\mathbf{u}\) 组成的矩阵 \(\mathbf{U} \in \mathbb{R}^{m \times d}\) 和多个不同的向量 \(\mathbf{v}\) 组成的矩阵 \(\mathbf{V} \in \mathbb{R}^{n \times d}\) 计算的到一个结果矩阵 \(\mathbf{W} \in \mathbb{R}^{m \times n}\),结果矩阵 \(\mathbf{W}\) 的第 \(i\) 行第 \(j\) 列的数值 \(w_{ij}\) 为矩阵 \(\mathbf{U}\) 的第 \(i\) 行向量 \(\mathbf{u}_i\) 和矩阵 \(\mathbf{V}\) 的第 \(j\) 行向量 \(\mathbf{v}_j\) 的余弦相似度值。

Assignment Notes:

  • 仔细阅读下面几个 numpy 函数的说明文档,这些函数可能对你有帮助:

  • 如果你对 numpy 的广播(Broadcasting)机制不了解,可以参阅这些资料:

  • 为了保证你的实现是向量化的批量运算,请不要使用任何 for 循环来逐项计算。合理使用上面参考资料中的方法你可以只用一行代码就完成计算。

def cosine_similarity(u, v):
    \'\'\'
    计算向量间的余弦相似度。

    Args:
        u (numpy.ndarray): 维度为(m, d)的矩阵;
        v (numpy.ndarray): 维度为(n, d)的矩阵。

    Returns:
        (numpy.ndarray): 维度为(m, n)的矩阵,此矩阵第i行第j列的数值为矩阵u的第i行
            和矩阵v的第j行这两个向量依据上面的定义计算出的余弦相似度。
    \'\'\'
    ###### 开始 ######
    
    ### np.dot 求点积
    ### np.linalg.norm 求范数
    
    a = u
    b = v.T
    c = np.dot(a,b)
    d = np.linalg.norm(a,ord=2,axis=1,keepdims=True)
    e = np.linalg.norm(b,ord=2,axis=0,keepdims=True)
    mm = np.dot(d,e)
    return np.divide(c,mm)
    ###### 结束 ######

运行下面的测试,你应该得到如下结果:

mat1.shape: (3, 50)
mat2.shape: (2, 50)
mat3.shape: (3, 2)
array([[0.73747626, 0.3755256 ],
       [0.68657858, 0.40745768],
       [0.13122132, 0.75253873]])
father1 = word_matrix[word2index[\'爸爸\']]
mother1 = word_matrix[word2index[\'妈妈\']]
school1 = word_matrix[word2index[\'学校\']]

father2 = word_matrix[word2index[\'老爸\']]
school2 = word_matrix[word2index[\'校园\']]

mat1 = np.array([father1, mother1, school1])
mat2 = np.array([father2, school2])
print(\'mat1.shape:\', mat1.shape)
print(\'mat2.shape:\', mat2.shape)

mat3 = cosine_similarity(mat1, mat2)
print(\'mat3.shape:\', mat3.shape)
mat3
mat1.shape: (3, 50)
mat2.shape: (2, 50)
mat3.shape: (3, 2)





array([[0.73747626, 0.3755256 ],
       [0.68657858, 0.40745768],
       [0.13122132, 0.75253873]])

可以看到,这个例子中结果矩阵第1行第1列(“爸爸”和“老爸”的相似度)以及第3行第2列(“学校”和“校园”相似度)都有相对较高的余弦相似度数值。

最相似的词语

有了词向量间相似度的度量方法,我们便可以看看与某个词最相似的词语都有哪些。下面请你来实现 top_n_similarity 函数,给定一个词,返回与其最相似的 n 个词及其相似度。任意一个词一定和它自己是最相似的,请将自身排除在外。

Assignment Notes:

  • 想想如何充分利用你上面实现的可进行高效批量运算的 cosine_similarity 函数;
  • 仔细阅读下面的 numpy 函数的说明文档,这些函数可能对你有帮助:
  • 实现合理的话单次调用用时应该在数秒钟,甚至不到1秒钟。
def top_n_similarity(word, n, word2index, index2word, word_matrix):
    \'\'\'
    给定一个词,依据词向量间的余弦相似度找到与其最相似的前n个词并返回。

    Args:
        word (str): 给定的词;
        n (int): 需要返回的最相似词的个数;
        word2index (dict[str, int]): 词语到下标的映射;
        index2word (dict[int, str]):下标到词语的映射;
        word_matrix (numpy.ndarray):词向量组成的矩阵。

    Returns:
        (dict[str, float]): 找到的n个词及其相似度,一个以词为key,以相似度为
            value的字典。
    \'\'\'
    if word not in word2index:
        raise ValueError(\'Word {} not in the vocabulary.\'.format(word))
    ###### 开始 ######
    
    mydict=dict()
    #取出该单词的词向量,并转换为1维向量
    word1 = word_matrix[word2index[word]]
    mat1 = np.array([word1])
    #mat1 = np.expand_dims(word1,axis=0)
    
    #用该向量与原词组方程求相似度,得到该单词与所有词的相似度向量
    ans=cosine_similarity(mat1,word_matrix)
    
    #对该向量进行排序,得到其下标排序结果
    ans1=np.argsort(-ans)
    
    #将向量降维成数组
    ans=np.squeeze(ans)
    ans1=np.squeeze(ans1)
    
    for i in range(n):
        mydict[index2word[ans1[i+1]]]=ans[ans1[i+1]]
    return mydict
    
    ###### 结束 ######

运行下面的测试,你应该得到如下结果:

{\'维也纳\': 0.8026402573563235,
 \'柏林\': 0.789617648409123,
 \'法国巴黎\': 0.788222576783984,
 \'法国\': 0.783072750843146,
 \'布鲁塞尔\': 0.7728576184665334,
 \'里昂\': 0.7655693234781406,
 \'伦敦\': 0.76422517970406,
 \'斯特拉斯堡\': 0.7464075209702475,
 \'日内瓦\': 0.7349832595389842,
 \'路易\': 0.7206136726368307}
%%time
top_n_similarity(\'巴黎\', 10, word2index, index2word, word_matrix)
Wall time: 651 ms





{\'维也纳\': 0.8026402573563235,
 \'柏林\': 0.7896176484091229,
 \'法国巴黎\': 0.7882225767839838,
 \'法国\': 0.7830727508431462,
 \'布鲁塞尔\': 0.7728576184665333,
 \'里昂\': 0.7655693234781408,
 \'伦敦\': 0.7642251797040602,
 \'斯特拉斯堡\': 0.7464075209702475,
 \'日内瓦\': 0.7349832595389842,
 \'路易\': 0.7206136726368307}

下面请发挥你的想象,任意选取你感兴趣的词语,看看哪些词与它相似,以及返回的结果是否合理。英文请使用小写。

word = \'太阳\'
top_n_similarity(word, 10, word2index, index2word, word_matrix)
{\'地球\': 0.8346179490851815,
 \'金星\': 0.8221919998313647,
 \'星\': 0.8163960486983992,
 \'月亮\': 0.8154737751842294,
 \'天空\': 0.7928664125367684,
 \'行星\': 0.750207872730994,
 \'火星\': 0.7497908278425455,
 \'一颗\': 0.746478634718834,
 \'宇宙\': 0.7345393772157445,
 \'星星\': 0.7298035233569619}

词语类比任务(Word Analogy Task)

在词语类比任务中,我们要完成类似这样一个任务:a 对于 b 相当于 c 对于____。举例来说,我们要完成一句话,巴黎对于法国相当于北京对于____

巴黎与法国两个词之间是有语义关系的,巴黎是法国的首都,那么根据第三个词北京,我们可以推断出空白处应该填中国。

cDOmfx.jpg

根据上图中的关系我们可以得到:

\[\mathbf{v}_{法国} - \mathbf{v}_{巴黎} \approx \mathbf{v}_{中国} - \mathbf{v}_{北京} \]

即:

\[\mathbf{v}_{法国} - \mathbf{v}_{巴黎} + \mathbf{v}_{北京} \approx \mathbf{v}_{中国} \]

因此,对于我们需要找到,a 对于 b 相当于 c 对于____这样一个任务,我们需要找到最适合的 \(d\) 使 \(\mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_c\)\(\mathbf{v}_d\) 的相似度最高。这里我们同样使用余弦相似度来进行相似性度量。即:

\[d = \underset{w \in \mathbb{V}}{\mathrm{arg\,max}} \, \, \text{CosineSimilarity}(\mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_c, \mathbf{v}_w) \]

其中 \(\mathbb{V}\) 为整个词汇表。

下面需要你来实现 complete_analogy_task 函数并用它来完成词语类比任务。该函数接受词语 a, b, c 为参数,返回相似度最高的 d,同时还有以下要求:

  • 为了方便分析与理解,这里要求函数返回相似度最高的前3个候选词及其相似度。
  • 我们不希望返回的词是 a, b, c 三个词本身,但有时候计算得到的相似度最高的前几个候选词中可能有 a, b, c 三者中的一个或几个,因此请将 a, b, c 三个词本身排除在外。

Assignment Notes:

  • 如果将 \(\mathbf{v}_b - \mathbf{v}_a + \mathbf{v}_c\) 当作一个词语的词向量,这里需要做的和 top_n_similarity 是很相似的。
def complete_analogy_task(
    word_a, word_b, word_c, word2index, index2word, word_matrix
):
    \'\'\'
    完成词语类比任务。给定已知的三个词 a, b, c,找到最适合的3个候选词 d 使得
    (v_b - v_a) 和 (v_d - v_c) 间的余弦相似度最小。

    Args:
        word_a (str): 词语 a;
        word_b (str): 词语 b;
        word_c (str):词语 c;
        word2index (dict[str, int]): 词语到下标的映射;
        index2word (dict[int, str]):下标到词语的映射;
        word_matrix (numpy.ndarray):词向量组成的矩阵。

    Returns:
        (dict[str, float]): 最合适的3候选词,以词为key,以相似度为value。
    \'\'\'
    for word in (word_a, word_b, word_c):
        if word not in word2index:
            raise ValueError(
                \'Word {} not in the vocabulary.\'.format(word)
            )
    ###### 开始 ######
    
    mydict= dict()
    wordlist =[]
    #取出该单词的词向量,并转换为1维向量
    word1 = word_matrix[word2index[word_a]]
    word2 = word_matrix[word2index[word_b]]
    word3 = word_matrix[word2index[word_c]]
    wordlist.append(word_a)
    wordlist.append(word_b)
    wordlist.append(word_c)
    mat1 = np.array([word2-word1+word3])
    
    #用该向量与原词组方程求相似度,得到该单词与所有词的相似度向量
    ans=cosine_similarity(mat1,word_matrix)
    
    #对该向量进行排序,得到其下标排序结果
    ans1=np.argsort(-ans)
    
    #将向量将维成数组
    ans=np.squeeze(ans)
    ans1=np.squeeze(ans1)
    
    k=0
    for i in range(6):
        p=index2word[ans1[i]]
        if p not in wordlist:
            mydict[p]=ans[ans1[i]]
            k+=1
            if(k==3):
                break
    return mydict
    
    ###### 结束 ######

运行下面的测试用例,你应该得到:

"巴黎":"法国" = "北京":"中国" 	前3候选: {\'中国\': 0.9035901571740452, \'上海\': 0.7863853529317479, \'中华人民共和国\': 0.7673241689100596}
"法国":"巴黎" = "中国":"北京" 	前3候选: {\'北京\': 0.8777837078974607, \'上海\': 0.7822645566195217, \'杭州\': 0.7708022456031641}
"北京":"中国" = "东京":"日本" 	前3候选: {\'日本\': 0.9126536930088033, \'概要\': 0.8140997356991334, \'条目\': 0.7620412947366628}
"哥哥":"爸爸" = "姐姐":"妈妈" 	前3候选: {\'妈妈\': 0.9105166131659955, \'爱上\': 0.8414034985959645, \'我家\': 0.8085360695344918}
"man":"king" = "woman":"queen" 	前3候选: {\'queen\': 0.7812693110741713, \'princess\': 0.7761106573413572, \'lady\': 0.7394024785261167}
"man":"男人" = "woman":"女人" 	前3候选: {\'女人\': 0.8282728834284955, \'爱上\': 0.7446765414784068, \'漂亮\': 0.7270981887885872}
"夏天":"summer" = "冬天":"winter" 	前3候选: {\'winter\': 0.827960010826369, \'spring\': 0.8133394777339396, \'nights\': 0.7302998615344509}
"猫":"cat" = "狗":"dog" 	前3候选: {\'dog\': 0.673225726288403, \'bell\': 0.6685053095960879, \'avbe\': 0.6440587722819132}
def show_cases(cases):
    for case in cases:
        result = complete_analogy_task(*case, word2index, index2word, word_matrix)
        print(\'"{}":"{}" = "{}":"{}"\'.format(
            case[0], case[1], case[2], list(result.keys())[0]
        ), \'\t前3候选:\', result)

test_cases = [
    (\'巴黎\', \'法国\', \'北京\'),
    (\'法国\', \'巴黎\', \'中国\'),
    (\'北京\', \'中国\', \'东京\'),
    (\'哥哥\', \'爸爸\', \'姐姐\'),
    (\'man\', \'king\', \'woman\'),
    (\'man\', \'男人\', \'woman\'),
    (\'夏天\', \'summer\', \'冬天\'),
    (\'猫\', \'cat\', \'狗\'),
]
show_cases(test_cases)
"巴黎":"法国" = "北京":"中国" 	前3候选: {\'中国\': 0.9035901571740453, \'上海\': 0.7863853529317478, \'中华人民共和国\': 0.7673241689100596}
"法国":"巴黎" = "中国":"北京" 	前3候选: {\'北京\': 0.8777837078974606, \'上海\': 0.7822645566195219, \'杭州\': 0.7708022456031642}
"北京":"中国" = "东京":"日本" 	前3候选: {\'日本\': 0.9126536930088031, \'概要\': 0.8140997356991332, \'条目\': 0.7620412947366628}
"哥哥":"爸爸" = "姐姐":"妈妈" 	前3候选: {\'妈妈\': 0.9105166131659955, \'爱上\': 0.8414034985959644, \'我家\': 0.8085360695344919}
"man":"king" = "woman":"queen" 	前3候选: {\'queen\': 0.7812693110741713, \'princess\': 0.7761106573413571, \'lady\': 0.7394024785261168}
"man":"男人" = "woman":"女人" 	前3候选: {\'女人\': 0.8282728834284955, \'爱上\': 0.7446765414784068, \'漂亮\': 0.7270981887885872}
"夏天":"summer" = "冬天":"winter" 	前3候选: {\'winter\': 0.827960010826369, \'spring\': 0.8133394777339394, \'nights\': 0.7302998615344509}
"猫":"cat" = "狗":"dog" 	前3候选: {\'dog\': 0.673225726288403, \'bell\': 0.668505309596088, \'avbe\': 0.6440587722819131}

下面请发挥你的想象尝试不同的例子,看看你是否可以得到合理的结果。请至少测试10个例子,英文请使用小写。

your_test_cases = [
    (\'苹果\', \'mac\', \'微软\'),
    (\'nba\', \'篮球\', \'nfl\'),
    (\'四川\', \'火锅\', \'北京\'),
    (\'爷爷\', \'奶奶\', \'外公\'),
    (\'爷爷\', \'奶奶\', \'爸爸\'),
    (\'c\', \'编译\', \'python\'),
    (\'英语\', \'english\', \'数学\'),
    (\'王子\', \'公主\', \'男\'),
]
show_cases(your_test_cases)
"苹果":"mac" = "微软":"windows" 	前3候选: {\'windows\': 0.8910806551309494, \'microsoft\': 0.8529114256029701, \'xp\': 0.8379023732611227}
"nba":"篮球" = "nfl":"排球" 	前3候选: {\'排球\': 0.7005484502888687, \'橄榄球\': 0.6665796989079577, \'垒球\': 0.6358227473678834}
"四川":"火锅" = "北京":"双合盛" 	前3候选: {\'双合盛\': 0.5892615817795352, \'李太白\': 0.5601195437064146, \'汉堡\': 0.5514640741701642}
"爷爷":"奶奶" = "外公":"姑姑" 	前3候选: {\'姑姑\': 0.7294645937903153, \'外婆\': 0.7193083976760439, \'洪贞恩\': 0.7190164658997995}
"爷爷":"奶奶" = "爸爸":"妈妈" 	前3候选: {\'妈妈\': 0.8551136273568736, \'爱上\': 0.847611952873073, \'老公\': 0.8176522434879868}
"c":"编译" = "python":"perl" 	前3候选: {\'perl\': 0.7076964643456336, \'zodb\': 0.6825362384237011, \'cweb\': 0.6784671475955981}
"英语":"english" = "数学":"mathematics" 	前3候选: {\'mathematics\': 0.757443123039894, \'mathematical\': 0.6710243658734777, \'高等数学\': 0.6644685799868281}
"王子":"公主" = "男":"女" 	前3候选: {\'女\': 0.8330239176078883, \'女主角\': 0.7256086005422543, \'嫁\': 0.702171130631091}

分类:

技术点:

相关文章: