【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors

作业来源:https://github.com/xixiaoyao/CS224n-winter-together

1. 写在前面

这篇文章是CS224N课程的第一个大作业, 主要是对词向量做了一个探索, 并直观的感受了一下词嵌入或者词向量的效果。 这个作业不难, 感兴趣的可以玩一下。这里简单的记录一下我探索的一个过程。 这篇文章基于第一节课的笔记理论【NLP CS224N笔记】Lecture 1 - Introduction and Word Vectors

这个大作业分为两部分, 第一部分是基于计数的单词词向量,这个的灵感就是在相似的上下文中我们一般会使用意思相似的单词(同义词),因此,意思相近的单词会通过上下文的方式在一起出现。通过检查这些上下文,我们可以尝试把单词用词向量的方式表示出来,一种简单的方式就是依赖于单词在一起出现的次数, 所以就得到了一种叫做共现矩阵的策略,这是一个基于单词频数的词向量矩阵, 所以第一部分主要看看这个共现矩阵应该怎么算。 而第二部分,是基于词向量的预测, 是利用了已经训练好的一个词向量矩阵去介绍一下怎么进行预测, 比如可视化这些词向量啊, 找同义词或者反义词啊,实现单词的类比关系啊等等。 下面就来一一简单的看看吧。

大纲如下

  • 实验前的准备工作(导入包和语料库)
  • Part1: Count-Based Word Vectors
  • Part2: Prediction-Based Word Vectors

Ok, let’s go!

2. 实验前的准备工作

做实验之前,我们要导入用到的包:

import sys
assert sys.version_info[0]==3
assert sys.version_info[1] >= 5

from gensim.models import KeyedVectors  # KeyedVectors:实现实体(单词、文档、图片都可以)和向量之间的映射。每个实体由其字符串id标识。
from gensim.test.utils import datapath
import pprint     #  输出的更加规范易读
import matplotlib.pyplot as plt  
plt.rcParams['figure.figsize'] = [10, 5]  #  plt.rcParams主要作用是设置画的图的分辨率,大小等信息
import nltk
nltk.download('reuters')    # 这个可以从GitHub下载, 网址:https://github.com/nltk/nltk_data/tree/gh-pages/packages/corpora
from nltk.corpus import reuters
import numpy as np
import random
import scipy as sp
from sklearn.decomposition import TruncatedSVD
from sklearn.decomposition import PCA

START_TOKEN = ''
END_TOKEN = ''

np.random.seed(0)
random.seed(0)

这里面的Reuters是路透社(商业和金融新闻)语料库, 是一个词库, 语料库包含10788个新闻文档,共计130万词。这些文档跨越90个类别,分为train和test,我们这次需要用其中的一个类别(crude)里面的句子。

这里说一下这个词库导入过程中我这边出现的问题, 如果是直接运行这两行代码:

import nltk
nltk.download('reuters')    # 这个可以从GitHub下载, 网址:https://github.com/nltk/nltk_data/tree/gh-pages/packages/corpora

我这边会报错:
在这里插入图片描述
所以这个语料库我是先从GitHub上进行的下载, 然后再导入进去。 如果也遇到了这个问题, 可以尝试单独下载这个语料库nltk_data, 进入之后, 找到retuters.zip, 点击下载。 当然如果点击下载后再报一个错误:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第1张图片
这个错误就是即使展开详情这块也发现没法访问,这个的解决方式就是在chrome浏览器地址栏输入chrome://net-internals/#hsts,找到 delete domain security policies 项,输入域名:github.com (注意这个地方输入的是无法访问的那个网址, 这里是拿github.com做个演示, 这次实际上是raw.githubbusercontent.com),再点击delete。就可以正常访问了。 但是我这边竟然还是没法连接raw.githubusercontent.com。所以就通过
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第2张图片
这样, 就可以下载预料库了, 下载下来之后, 再简单说一下保存:保存的话这几个位置选一个:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第3张图片
还要切记一点的是得先建一个"corpora"文件夹, 放这里面
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第4张图片
这样就OK了。 然后就是采用下面的函数,导入这个语料库:

def read_corpus(category="crude"):
    """ Read files from the specified Reuter's category.
        Params:
            category (string): category name
        Return:
            list of lists, with words from each of the processed files
    """
    files = reuters.fileids(category)    # 类别为crude文档
    # 每个文档都转化为小写, 并在开头结尾加标识符
    return [[START_TOKEN] + [w.lower() for w in list(reuters.words(f))] + [END_TOKEN] for f in files]

这个是导入语料库的函数, 简单的进行了一下预处理, 就是在每句话的前面和后面各加了一个标识符,表示句子的开始和结束,然后把每个单词分开。 下面导入并看一下效果:

# pprint模块格式化打印
# pprint.pprint(object, stream=None, indent=1, width=80, depth=None, *, compact=False)
# width:控制打印显示的宽度。默认为80个字符。注意:当单个对象的长度超过width时,并不会分多行显示,而是会突破规定的宽度。
# compact:默认为False。如果值为False,超过width规定长度的序列会被分散打印到多行。如果为True,会尽量使序列填满width规定的宽度。
reuters_corpus = read_corpus()
pprint.pprint(reuters_corpus[:1], compact=True, width=100)  # compact 设置为False是一行一个单词

每个句子处理后长这样:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第5张图片
有了这个准备工作之后, 就可以看看两个部分了。

3. PART 1: Count-Based Word Vectors

这部分的灵感上面已经说过, 共现矩阵是实现这种词向量的一种方式, 我们看看共现矩阵是什么意思? 共现矩阵计算的是单词在某些环境下一块出现的频率, 对于共现矩阵, 原文描述是这样的:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第6张图片
上面的话其实就是这样的一个意思, 要想建立共现矩阵,我们需要先为单词构建一个词典, 然后共现矩阵的行列都是这个词典里的单词, 看下面这个例子:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第7张图片
上面基于这两段文档构建出的共现矩阵长这样, 这个是怎么构建的? 首先就是根据两个文档的单词构建一个词典, 这里面的数就是两两单词在上下文中共现的频率, 比如第一行, START和all一起出现了两次, 这就是因为两个文档里面START的窗口中都有all。 同理第二行all的那个, 我们也固定一个窗口, 发现第一个文档里面all左边是START, 右边是that, 第二个文档all左边是START, 右边是is, 那么=2, =1, =1。 下面的都是同理了。

我们就是要构建这样的一个矩阵来作为每个单词的词向量, 当然这个还不是最终形式, 因为可能词典很大的话维度会特别高, 所以就相当了降维技术, 降维之后的结果就是每个单词的词向量。 这个里面使用的降维是SVD, 原理这里不说, 这里使用了Truncated SVD, 具体的实现是调用了sklearn中的包。

所以我们就有了下面的这样一个思路框架:

  1. 对于语录料库中的文档单词, 得先构建一个词典(唯一单词且排好序)
  2. 然后我们就是基于词典和语料库,为每个单词构建词向量, 也就是共现矩阵
  3. 对共现矩阵降维,就得到了最终的词向量
  4. 可视化

好了,基于上面的思路开始实现:

3.1 为语料库中的单词构建词典

我们知道词典就是记录所有的单词, 但是单词唯一且有序。 那么实现这个词典的思路就是我遍历每一篇文档,先获得所有的单词, 然后去掉重复的, 然后再排序就搞定, 当然还得记录字典里的单词总数。 基于这个思路, 就有了下面的代码实现:

# 计算出语料库中出现的不同单词,并排序。
def distinct_words(corpus):
    """ Determine a list of distinct words for the corpus.
        Params:
            corpus (list of list of strings): corpus of documents
        Return:
            corpus_words (list of strings): list of distinct words across the corpus, sorted (using python 'sorted' function)
            num_corpus_words (integer): number of distinct words across the corpus
    """
    corpus_words = []
    num_corpus_words = -1
    
    # ------------------
    # Write your implementation here.
    # 首先得把所有单词放到一个列表里面, 然后用set去重, 然后排序
    for everylist in corpus:
        corpus_words.extend(everylist)
    corpus_words = sorted(set(corpus_words))
    num_corpus_words = len(corpus_words)
    # ------------------

    return corpus_words, num_corpus_words

这里只是用了一种获得单词列表的方式, 还可以用列表推导式的方式:

flattened_list = [word for every_list in corpus for word in every_list]  # 展平成一维
corpus_words = sorted(set(flattened_list))  # set去重,sorted排序
num_corpus_words = len(corpus_words)  # 字典总数

词典建成, 下面就是构建共现矩阵了。

3.2 构建共现矩阵

这个依然是简单说一下思路, 上面已经说了共现矩阵的原理了, 就是记录一块出现的频数嘛, 那么具体实现是咋样的呢?

首先我们得定义一个M矩阵, 也就是共现矩阵, 大小就是行列都是词典的单词个数(上面图片一目了然), 然后还得定义一个字典单词到索引的映射, 因为我们统计的时候是遍历真实文档, 而填矩阵的时候是基于字典,这两个是基于同一个单词进行联系起来的, 所以我们需要获得真实文档中单词在字典里面的索引才能去填矩阵。 所以有了下面这几行代码:

def compute_co_occurrence_matrix(corpus, window_size=4):
    """ Compute co-occurrence matrix for the given corpus and window_size (default of 4).
    
        Note: Each word in a document should be at the center of a window. Words near edges will have a smaller
              number of co-occurring words.
              
              For example, if we take the document "START All that glitters is not gold END" with window size of 4,
              "All" will co-occur with "START", "that", "glitters", "is", and "not".
    
        Params:
            corpus (list of list of strings): corpus of documents
            window_size (int): size of context window
        Return:
            M (numpy matrix of shape (number of corpus words, number of corpus words)): 
                Co-occurence matrix of word counts. 
                The ordering of the words in the rows/columns should be the same as the ordering of the words given by the distinct_words function.
            word2Ind (dict): dictionary that maps word to index (i.e. row/column number) for matrix M.
    """
    words, num_words = distinct_words(corpus)   # 单词已经去重或者排好序  
    M = None
    word2Ind = {}
    
    # ------------------
    # Write your implementation here.
    word2Ind = {k: v for (k, v) in zip(words, range(num_words))}
    M = np.zeros((num_words, num_words))

接下来就是填充共现矩阵了, 思路是这样子, 我们遍历每一篇文档, 对于每一篇文档, 我们遍历每个单词, 对于每个单词, 我们先获得在字典中的索引, 然后去找以这个单词为中心词的窗口范围,这样就找到了这个单词的上下文, 然后对于每个上下文单词, 在共现矩阵里面计数就可以了。 所以这里每个单词会有两个索引, 一个是字典里面的索引, 一个是文档里面的索引, 前者是为了把一起共现的单词次数填充到共现矩阵里面, 后者是为了找到上下文。 下面的代码接上面(注释感觉写的挺明白了):

	# 接下来是遍历语料库 对于每一篇文档, 我们得遍历每个单词
    # 对于每个单词, 我们得找到窗口的范围, 然后再去遍历它窗口内的每个单词
    # 对于这每个单词, 我们就可以在我们的M词典中进行计数, 但是要注意每个单词其实有两个索引
    # 一个是词典里面的索引, 一个是文档中的索引, 我们统计的共现频率是基于字典里面的索引, 
    # 所以这里涉及到一个索引的转换
    
    # 首先遍历语料库
    for every_doc in corpus:
        for cword_doc_ind, cword in enumerate(every_doc):  # 遍历当前文档的每个单词和在文档中的索引
            # 对于当前的单词, 我们先找到它在词典中的位置
            cword_dic_ind = word2Ind[cword]
            
            # 找窗口的起始和终止位置  开始位置就是当前单词的索引减去window_size, 终止位置
            # 是当前索引加上windo_size+1, 
            window_start = cword_doc_ind - window_size
            window_end = cword_doc_ind + window_size + 1
            
            # 有了窗口, 我们就要遍历窗口里面的每个单词, 然后往M里面记录就行了
            # 但是还要注意一点, 就是边界问题, 因为开始单词左边肯定不够窗口大小, 结束单词
            # 右边肯定不够窗口大小, 所以遍历之后得判断一下是不是左边后者右边有单词
            for j in range(window_start, window_end):
                # 前面两个条件控制不越界, 最后一个条件控制不是它本身
                if j >=0 and j < len(every_doc) and j != cword_doc_ind:
                    # 想办法加入到M, 那么得获取这个单词在词典中的位置
                    oword = every_doc[j]   # 获取到上下文单词
                    oword_dic_ind = word2Ind[oword]
                    # 加入M
                    M[cword_dic_ind, oword_dic_ind] += 1
    # ------------------

    return M, word2Ind

通过上面的代码, 就实现了共现矩阵的构建。 下面就简单了, 实现降维

3.3 降到k维

降维直接调用的包sklearn.decomposition.TruncatedSVD.

def reduce_to_k_dim(M, k=2):
    """ Reduce a co-occurence count matrix of dimensionality (num_corpus_words, num_corpus_words)
        to a matrix of dimensionality (num_corpus_words, k) using the following SVD function from Scikit-Learn:
            - http://scikit-learn.org/stable/modules/generated/sklearn.decomposition.TruncatedSVD.html
    
        Params:
            M (numpy matrix of shape (number of corpus words, number of corpus words)): co-occurence matrix of word counts
            k (int): embedding size of each word after dimension reduction
        Return:
            M_reduced (numpy matrix of shape (number of corpus words, k)): matrix of k-dimensioal word embeddings.
                    In terms of the SVD from math class, this actually returns U * S
    """    
    n_iters = 10     # Use this parameter in your call to `TruncatedSVD`
    M_reduced = None
    print("Running Truncated SVD over %i words..." % (M.shape[0]))
    
        # ------------------
        # Write your implementation here.
    svd = TruncatedSVD(n_components=k, n_iter=n_iters, random_state=2020)
    M_reduced = svd.fit_transform(M)
        # ------------------

    print("Done.")
    return M_reduced

这个就不用解释了, 通过降维就可以得到每个单词的词嵌入向量, 我们可以通过下面的代码可视化一下, 这里介绍matplotlib的画图文档https://matplotlib.org/gallery/index.html:

def plot_embeddings(M_reduced, word2Ind, words):
    """ Plot in a scatterplot the embeddings of the words specified in the list "words".
        NOTE: do not plot all the words listed in M_reduced / word2Ind.
        Include a label next to each point.
        
        Params:
            M_reduced (numpy matrix of shape (number of unique words in the corpus , k)): matrix of k-dimensioal word embeddings
            word2Ind (dict): dictionary that maps word to indices for matrix M
            words (list of strings): words whose embeddings we want to visualize
    """

    # ------------------
    # Write your implementation here.
    # 遍历句子, 获得每个单词的x,y坐标
    for word in words:
        word_dic_index = word2Ind[word]
        x = M_reduced[word_dic_index][0]
        y = M_reduced[word_dic_index][1]
        plt.scatter(x, y, marker='x', color='red')
        # plt.text()给图形添加文本注释
        plt.text(x+0.0002, y+0.0002, word, fontsize=9)  # # x、y上方0.002处标注文字说明,word标注的文字,fontsize:文字大小
    plt.show()
    # ------------------

3.4 把上面的过程综合起来:

简单的回忆下上面过程, 首先是读入数据, 然后计算共现矩阵, 然后是降维, 最后是可视化:

reuters_corpus = read_corpus()
M_co_occurrence, word2Ind_co_occurrence = compute_co_occurrence_matrix(reuters_corpus)
M_reduced_co_occurrence = reduce_to_k_dim(M_co_occurrence, k=2)

# Rescale (normalize) the rows to make them each of unit-length
M_lengths = np.linalg.norm(M_reduced_co_occurrence, axis=1)
M_normalized = M_reduced_co_occurrence / M_lengths[:, np.newaxis] # broadcasting

words = ['barrels', 'bpd', 'ecuador', 'energy', 'industry', 'kuwait', 'oil', 'output', 'petroleum', 'venezuela']
plot_embeddings(M_normalized, word2Ind_co_occurrence, words)

结果如下:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第8张图片
还是可以看出点相近来哈,比如oil和energy, peteroleum与industry等。这就是第一部分的内容啦。

4. PART 2: Prediction-Based Word Vectors

4.1 可视化Word2Vec训练的词嵌入

这一部分其实是利用了一个用Word2Vec技术训练好的词向量矩阵去测试一些有趣的效果, 看看词向量到底是干啥用的。 所以用gensim包下载了一个词向量矩阵:

def load_word2vec():
    """ Load Word2Vec Vectors
        Return:
            wv_from_bin: All 3 million embeddings, each lengh 300
    """
    import gensim.downloader as api
    wv_from_bin = api.load("word2vec-google-news-300")
    vocab = list(wv_from_bin.vocab.keys())
    print("Loaded vocab size %i" % len(vocab))
    return wv_from_bin

当然这行代码运行时间很长。 有了这个代码,我们就能得到一个基于Word2Vec训练好的词向量矩阵(和上面我们的M矩阵是类似的,只不过得到的方式不同), 接下来就是进行降维并可视化词嵌入:

def get_matrix_of_vectors(wv_from_bin, required_words=['barrels', 'bpd', 'ecuador', 'energy', 'industry', 'kuwait', 'oil', 'output', 'petroleum', 'venezuela']):
    """ Put the word2vec vectors into a matrix M.
        Param:
            wv_from_bin: KeyedVectors object; the 3 million word2vec vectors loaded from file
        Return:
            M: numpy matrix shape (num words, 300) containing the vectors
            word2Ind: dictionary mapping each word to its row number in M
    """
    import random
    words = list(wv_from_bin.vocab.keys())
    print("Shuffling words ...")
    random.shuffle(words)
    words = words[:10000]       # 选10000个加入
    print("Putting %i words into word2Ind and matrix M..." % len(words))
    word2Ind = {}
    M = []
    curInd = 0
    for w in words:
        try:
            M.append(wv_from_bin.word_vec(w))
            word2Ind[w] = curInd
            curInd += 1
        except KeyError:
            continue
    for w in required_words:
        try:
            M.append(wv_from_bin.word_vec(w))
            word2Ind[w] = curInd
            curInd += 1
        except KeyError:
            continue
    M = np.stack(M)
    print("Done.")
    return M, word2Ind

# -----------------------------------------------------------------
# Run Cell to Reduce 300-Dimensinal Word Embeddings to k Dimensions
# Note: This may take several minutes
# -----------------------------------------------------------------
M, word2Ind = get_matrix_of_vectors(wv_from_bin)
M_reduced = reduce_to_k_dim(M, k=2)         # 减到了2维

words = ['barrels', 'bpd', 'ecuador', 'energy', 'industry', 'kuwait', 'oil', 'output', 'petroleum', 'venezuela']
plot_embeddings(M_reduced, word2Ind, words)

结果如下:
【NLP CS224N笔记】Assignment 1 - Exploring Word Vectors_第9张图片

4.2 余弦相似性

我们已经得到了每个单词的词向量表示, 那么怎么看两个单词的相似性程度呢? 余弦相似性是一种方式, 公式如下:
s = p ⋅ q ∣ ∣ p ∣ ∣ ∣ ∣ q ∣ ∣ ,  where  s ∈ [ − 1 , 1 ] s = \frac{p \cdot q}{||p|| ||q||}, \textrm{ where } s \in [-1, 1] s=pqpq, where s[1,1]
这个详细的的可以参考: Cosine Similarity

基于这个方式,我们就可以找到单词的多义词, 同义词,反义词还能实现单词的类比推理等好玩的事情。所以下面主要是介绍一下实现这些好玩事情的方法,毕竟这里是直接调用的gensim的函数。

比如, 我们找和某个单词最相近的10个单词:
可以使用gensim里面的most_similar函数, GenSim documentation

# 找和energy最相近的10个单词
wv_from_bin.most_similar("energy")

##结果
[('renewable_energy', 0.6721636056900024),
 ('enery', 0.6289607286453247),
 ('electricity', 0.6030439138412476),
 ('enegy', 0.6001754403114319),
 ('Energy', 0.595537006855011),
 ('fossil_fuel', 0.5802257061004639),
 ('natural_gas', 0.5767925381660461),
 ('renewables', 0.5708995461463928),
 ('fossil_fuels', 0.5689164996147156),
 ('renewable', 0.5663810968399048)]

再比如, 为我们可以找同义词和反义词:

w1 = "man"
w2 = "king"
w3 = "woman"
w1_w2_dist = wv_from_bin.distance(w1, w2)
w1_w3_dist = wv_from_bin.distance(w1, w3)

print("Synonyms {}, {} have cosine distance: {}".format(w1, w2, w1_w2_dist))
print("Antonyms {}, {} have cosine distance: {}".format(w1, w3, w1_w3_dist))

## 结果:
Synonyms man, king have cosine distance: 0.7705732733011246
Antonyms man, woman have cosine distance: 0.2335987687110901

还可以实现类比关系:
比如: China : Beijing = Japan : ?, 那么我们可以用下面的代码求这样的类别关系, 注意下面的positive和negative里面的单词顺序, 我们求得?其实和Japan和Beijing相似, 和China远。

# Run this cell to answer the analogy -- man : king :: woman : x
pprint.pprint(wv_from_bin.most_similar(positive=['Bejing', 'Japan'], negative=['China']))

## 结果:
[('Tokyo', 0.6124968528747559),
 ('Osaka', 0.5791803598403931),
 ('Maebashi', 0.5635818243026733),
 ('Fukuoka_Japan', 0.5362966060638428),
 ('Nagoya', 0.5359445214271545),
 ('Fukuoka', 0.5319067239761353),
 ('Osaka_Japan', 0.5298740267753601),
 ('Nagano', 0.5293833017349243),
 ('Taisuke', 0.5258569717407227),
 ('Chukyo', 0.5195443034172058)]

5. 总结

在这里简单的小总一下, 第一次大作业相对来说可能是热身阶段, 所以难度上不是那么的大, 不过还是挺有意思的, 并且还学习到了一个共现矩阵求解词向量的方式, 当然, 第二节课中还会讲到这个思想, 所以第一部分就是讲了一个求解词向量的方式, 这个是基于统计的方式, 而第二部分是Word2Vec训练好的词向量, 演示了一下可以做什么事情。

今天的内容就是这些了, 这次实验里面下载语料库这块如果遇到了问题, 可以尝试单独下下来, 然后再做。 去第二节课了,继续Rush!

你可能感兴趣的:(NLP自然语言处理)