PyTorch基础——词向量(Word Vector)技术

一、介绍

内容

将接触现代 NLP 技术的基础:词向量技术。

第一个是构建一个简单的 N-Gram 语言模型,它可以根据 N 个历史词汇预测下一个单词,从而得到每一个单词的向量表示。

第二个将接触到现代词向量技术常用的模型 Word2Vec。在实验中将以小说《三体》为例,展示了小语料在 Word2Vec 模型中能够取得的效果。

在最后一个将加载已经训练好的一个大规模词向量,并利用这些词向量来做一些简单的运算和测试,以探索词向量中包含的语义信息。

知识点

  • N-Gram(NPLM) 语言模型
  • Word2Vec 词向量模型
  • 使用 Word2Vec 词向量进行语义运算

二、N-Gram 词向量模型

引入相关包

# 加载必要的程序包
# PyTorch的程序包
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

# 数值运算和绘图的程序包
import numpy as np
import matplotlib.pyplot as plt

import matplotlib

值得注意的是 sklearn 包,在本次实验中将用到其中的 PCA 降维算法。

# 加载机器学习的软件包
from sklearn.decomposition import PCA

#加载‘结巴’中文分词软件包

import jieba

#加载正则表达式处理的包
import re

%matplotlib inline

文本预处理

读入原始文件

下载数据 网盘链接:https://pan.baidu.com/s/1D1YI4h7k-xPpPbFYQx4E8A 提取码:osk5

#读入原始文件

f = open("三体.txt", 'r')
text = str(f.read())
f.close()

text

接下来用“结巴(jieba)”分词工具来分词,并过滤掉所有的标点符号

# 分词
temp = jieba.lcut(text)
words = []
for i in temp:
    #过滤掉所有的标点符号
    i = re.sub("[\s+\.\!\/_,$%^*(+\"\'“”《》?“]+|[+——!,。?、~@#¥%……&*():]+", "", i)
    if len(i) > 0:
        words.append(i)
print(len(words))
words

N-Gram 词向量模型的原理是利用一个人工神经网络来根据前 N 个单词来预测下一个单词,从而得到每个单词的词向量。

# 构建三元组列表.  每一个元素为: ([ i-2位置的词, i-1位置的词 ], 下一个词)
# 我们选择的Ngram中的N,即窗口大小为2
trigrams = [([words[i], words[i + 1]], words[i + 2]) for i in range(len(words) - 2)]
# 打印出前三个元素看看
print(trigrams[:3])

将每个单词进行编码 构造词典

对上面包含所有词汇的列表 word 取一个独立集 set()

根据词典做两个速查表,一个是根据单词索引其编号,一个是根据编号索引单词

# 得到词汇表
vocab = set(words)
print(len(vocab))
# 两个字典,一个根据单词索引其编号,一个根据编号索引单词
# word_to_idx中的值包含两部分,一部分为id,另一部分为单词出现的次数
# word_to_idx中的每一个元素形如:{w:[id, count]},其中w为一个词,id为该词的编号,count为该单词在words全文中出现的次数
word_to_idx = {} 
idx_to_word = {}
ids = 0

# 对全文循环,构建这两个字典
for w in words:
    cnt = word_to_idx.get(w, [ids, 0])
    if cnt[1] == 0:
        ids += 1
    cnt[1] += 1
    word_to_idx[w] = cnt
    idx_to_word[ids] = w

word_to_idx

构造模型并训练

下面将构建一个三层的神经网络模型:

  • 1、输入层:embedding 层。
    • 这一层的作用是:先将输入单词的编号映射为一个 one hot 编码的向量,形如:001000,维度为单词表大小。
    • 然后,embedding 会通过一个线性的神经网络层映射出这个词的向量表示,输出为 embedding_dim。
  • 2、隐藏层:线性层 + 非线性 ReLU。
    • 从 embedding_dim 维度到128维度,然后经过非线性 ReLU 函数
  • 3、输出层:线性层 + Log Softmax。
    • 从 128 维度到单词表大小维度,然后 log softmax 函数,给出预测每个单词的概率。
class NGram(nn.Module):

    def __init__(self, vocab_size, embedding_dim, context_size):
        super(NGram, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)  # 输入层
        self.linear1 = nn.Linear(context_size * embedding_dim, 128) # 隐藏层
        self.linear2 = nn.Linear(128, vocab_size) # 输出层

    def forward(self, inputs):
        #嵌入运算,嵌入运算在内部分为两步:将输入的单词编码映射为one hot向量表示,然后经过一个线性层得到单词的词向量
        #inputs的尺寸为:1*context_size
        embeds = self.embeddings(inputs)
        #embeds的尺寸为: context_size*embedding_dim
        embeds = embeds.view(1, -1)
        #此时embeds的尺寸为:1*embedding_dim
        # 线性层加ReLU
        out = self.linear1(embeds)
        out = F.relu(out)
        #此时out的尺寸为1*128

        # 线性层加Softmax
        out = self.linear2(out)
        #此时out的尺寸为:1*vocab_size
        log_probs = F.log_softmax(out, dim=1)
        return log_probs
    def extract(self, inputs):
        embeds = self.embeddings(inputs)
        return embeds

NPLM 模型的训练是非常非常缓慢的,在训练代码的后面提供了预训练好的模型供大家加载使用。

losses = [] #纪录每一步的损失函数
criterion = nn.NLLLoss() #运用负对数似然函数作为目标函数(常用于多分类问题的目标函数)
model_ng = NGram(len(vocab), 10, 2) #定义NGram模型,向量嵌入维数为10维,N(窗口大小)为2

optimizer = optim.SGD(model_ng.parameters(), lr=0.001) #使用随机梯度下降算法作为优化器

# 因为模型的训练速度非常之慢
# 所以在这里把迭代训练的次数修改为0
# 如果你真的好奇有多慢,欢迎修改这个值,一定不会令你失望的
for epoch in range(0):
    total_loss = torch.Tensor([0])
    for context, target in trigrams:
	# 准备好输入模型的数据,将词汇映射为编码
    context_idxs = [word_to_idx[w][0] for w in context]

    # 包装成PyTorch的Variable
    context_var = Variable(torch.LongTensor(context_idxs))

    # 清空梯度:注意PyTorch会在调用backward的时候自动积累梯度信息,故而每隔周期要清空梯度信息一次。
    optimizer.zero_grad()

    # 用神经网络做计算,计算得到输出的每个单词的可能概率对数值
    log_probs = model_ng(context_var)

    # 计算损失函数,同样需要把目标数据转化为编码,并包装为Variable
    loss = criterion(log_probs, Variable(torch.LongTensor([word_to_idx[target][0]])))

    # 梯度反传
    loss.backward()

    # 对网络进行优化
    optimizer.step()

    # 累加损失函数值
    total_loss += loss.data
losses.append(total_loss)
print('第{}轮,损失函数为:{:.2f}'.format(epoch, total_loss.numpy()[0]))

在这里加载预训练的模型。(数据在网盘中)

model_ng = torch.load('NPLM_Ready.mdl')

将向量投影到二维平面进行可视化

下面首先观察 NPLM 模型的网络结构,以便从 NPLM 的输入层中取出词向量。

print(model_ng)

下面将调用模型的 extract 函数提取出来所有单词的词向量。最后的词向量就存储到了 vec 中。

# 从训练好的模型中提取每个单词的向量
vec = model_ng.extract(Variable(torch.LongTensor([v[0] for v in word_to_idx.values()])))
vec = vec.data.numpy()

但此时获得的词向量仍然是高维度的,想要直观的观察到词汇的分布,还要对它进行降维。可以通过 PCA 降维的方法将 vec 中的向量展示在二维世界中。

# 利用PCA算法进行降维
X_reduced = PCA(n_components=2).fit_transform(vec)


# 绘制所有单词向量的二维空间投影
fig = plt.figure(figsize = (30, 20))
ax = fig.gca()
ax.set_facecolor('white')
ax.plot(X_reduced[:, 0], X_reduced[:, 1], '.', markersize = 1, alpha = 0.4, color = 'black')


# 绘制几个特殊单词的向量
words = ['智子', '地球', '三体', '质子', '科学', '世界', '文明', '太空', '加速器', '平面', '宇宙', '信息']

# 设置中文字体,否则无法在图形上显示中文
zhfont1 = matplotlib.font_manager.FontProperties(fname='./华文仿宋.ttf', size=16)
for w in words:
    if w in word_to_idx:
        ind = word_to_idx[w][0]
        xy = X_reduced[ind]
        plt.plot(xy[0], xy[1], '.', alpha =1, color = 'red')
        plt.text(xy[0], xy[1], w, fontproperties = zhfont1, alpha = 1, color = 'black')

那么,获得的词向量好不好呢?我们通常是去查看在向量上相似的词是否具有相似的语义信息,来验证训练出的词向量是高质量的(包含正确的语义信息)。

# 定义计算cosine相似度的函数
def cos_similarity(vec1, vec2):

    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    norm = norm1 * norm2
    dot = np.dot(vec1, vec2)
    result = dot / norm if norm > 0 else 0
    return result

# 在所有的词向量中寻找到与目标词(word)相近的向量,并按相似度进行排列
def find_most_similar(word, vectors, word_idx):
    vector = vectors[word_to_idx[word][0]]
    simi = [[cos_similarity(vector, vectors[num]), key] for num, key in enumerate(word_idx.keys())]
    sort = sorted(simi)[::-1]
    words = [i[1] for i in sort]
    return words

# 与智子靠近的词汇
find_most_similar('智子', vec, word_to_idx)

似乎在词义上并没有什么相近性,这说明本次实验中的这个 NPLM 模型学出来的词向量并不好。实际上,训练好的词向量需要大规模语料,同时还要训练足够长的时间。在这两点上,实验中的小模型都没有做到,所以训练出来的词向量并不理想。

三、Word2Vec

引入 Word2Vec 相关包

Gensim 是一个面向自然语言处理领域的 Python 包,包含了 Word2Vec、LDA 主题模型等常用的自然语言处理功能的函数库。

#加载Word2Vec的软件包
import gensim as gensim
from gensim.models import Word2Vec
from gensim.models.keyedvectors import KeyedVectors
from gensim.models.word2vec import LineSentence

用小语料训练自己的词向量

首先载入语料库,并进行分词

# 读入文件、分词,形成一句一句的语料
# 注意跟前面处理不一样的地方在于,我们一行一行地读入文件,从而自然利用行将文章分开成“句子”
f = open("三体.txt", 'r')
lines = []
for line in f:
    temp = jieba.lcut(line)
    words = []
    for i in temp:
        #过滤掉所有的标点符号
        i = re.sub("[\s+\.\!\/_,$%^*(+\"\'””《》]+|[+——!,。?、~@#¥%……&*():;‘]+", "", i)
        if len(i) > 0:
            words.append(i)
    if len(words) > 0:
        lines.append(words)

lines 是输入的已经变成列表的单词,size 是拟嵌入向量的维度;window 表示的是上下文窗口大小,也就是 N-gram 模型中的那个 N;min_count 为保留最少多少的低频词,如果它等于 0,那便意味着算法将计算所有词的词向量,无论它的出现次数是多少。

# 调用Word2Vec的算法进行训练。
# 参数分别为:size: 嵌入后的词向量维度;window: 上下文的宽度,min_count为考虑计算的单词的最低词频阈值

model = Word2Vec(lines, size = 20, window = 2 , min_count = 0)

观察小语料 Word2Vec 词向量的效果

首先对词向量进行降维,投影到二维空间

# 首先将词向量使用 PCA 降维,投影到二维空间
rawWordVec = []
word2ind = {}
for i, w in enumerate(model.wv.vocab):
    rawWordVec.append(model[w])
    word2ind[w] = i
rawWordVec = np.array(rawWordVec)
X_reduced = PCA(n_components=2).fit_transform(rawWordVec)

绘制出使用 Word2Vec 训练出的词向量星空

# 绘制星空图
# 绘制所有单词向量的二维空间投影
fig = plt.figure(figsize = (15, 10))
ax = fig.gca()
ax.set_facecolor('black')
ax.plot(X_reduced[:, 0], X_reduced[:, 1], '.', markersize = 1, alpha = 0.3, color = 'white')


# 绘制几个特殊单词的向量
words = ['智子', '地球', '三体', '质子', '科学', '世界', '文明', '太空', '加速器', '平面', '宇宙', '进展','的']

# 设置中文字体,否则无法在图形上显示中文
zhfont1 = matplotlib.font_manager.FontProperties(fname='./华文仿宋.ttf', size=16)
for w in words:
    if w in word2ind:
        ind = word2ind[w]
        xy = X_reduced[ind]
        plt.plot(xy[0], xy[1], '.', alpha =1, color = 'red')
        plt.text(xy[0], xy[1], w, fontproperties = zhfont1, alpha = 1, color = 'yellow')

下面找出与“智子”最相近的 20 个词

model.wv.most_similar('智子', topn = 20)

从上面打印出的相近词中可以观察到,使用小语料训练的 Word2Vec 词向量仍不能达到理想的效果。原因有可能还是语料太小,或者对于词语的预处理工作还需要加强

四、使用预训练的大规模 Word2Vec 词向量

加载大规模词向量

注:以下代码请在空闲内存大于3G的环境实验

下面实验要用到的词向量是由微博、人民日报、上海热线、汽车之家等多处的大量语料训练而成,包含 1366130 个词向量。规模如此巨大的中文词向量是非常难得的。感谢台湾第一家大数据公司 AsiaMiner 的联合创始人 尹相志 老师提供本词向量文件。大家可以自行搜索下载数据。

词向量文件的名字是:vectors.bin,首先将它加载到内存中,因为词向量较多所以加载需要一小点时间。

# 加载词向量
word_vectors = KeyedVectors.load_word2vec_format('vectors.bin', binary=True, unicode_errors='ignore')
len(word_vectors.vocab)

观察大规模词向量空间中的语义信息

仍然是使用 PCA 降维将词向量投影到 2 维空间

# PCA降维
rawWordVec = []
word2ind = {}
for i, w in enumerate(word_vectors.vocab):
    rawWordVec.append(word_vectors[w])
    word2ind[w] = i
rawWordVec = np.array(rawWordVec)
X_reduced = PCA(n_components=2).fit_transform(rawWordVec)

降维后将所有的词向量绘制在“星空”中

# 绘制星空图
# 绘制所有的词汇
fig = plt.figure(figsize = (30, 15))
ax = fig.gca()
ax.set_facecolor('black')
ax.plot(X_reduced[:, 0], X_reduced[:, 1], '.', markersize = 1, alpha = 0.1, color = 'white')

ax.set_xlim([-12,12])
ax.set_ylim([-10,20])


# 选择几个特殊词汇,不仅画它们的位置,而且把它们的临近词也画出来
words = {'徐静蕾','吴亦凡','物理','红楼梦','量子'}
all_words = []
for w in words:
    lst = word_vectors.most_similar(w)
    wds = [i[0] for i in lst]
    metrics = [i[1] for i in lst]
    wds = np.append(wds, w)
    all_words.append(wds)


zhfont1 = matplotlib.font_manager.FontProperties(fname='./华文仿宋.ttf', size=16)
colors = ['red', 'yellow', 'orange', 'green', 'cyan', 'cyan']
for num, wds in enumerate(all_words):
    for w in wds:
        if w in word2ind:
            ind = word2ind[w]
            xy = X_reduced[ind]
            plt.plot(xy[0], xy[1], '.', alpha =1, color = colors[num])
            plt.text(xy[0], xy[1], w, fontproperties = zhfont1, alpha = 1, color = colors[num])

注意到,二维空间上看起来靠近的点不一定在高维空间上也靠近。因此我们不能简单的根据二维空间上的靠近程度来判断词语意思的相近程度,而更应该相信颜色,因为颜色是根据相似度表示出来的。

有趣的词向量语义运算

观察一下求近似词的效果

# 查看相似词
word_vectors.most_similar('物理', topn = 20)
# 女人-男人=?-国王
words = word_vectors.most_similar(positive=['女人', '国王'], negative=['男人'])
words
# 北京-中国=?-俄罗斯
words = word_vectors.most_similar(positive=['北京', '俄罗斯'], negative=['中国'])
words
# 自然科学-物理学=?-政治学
words = word_vectors.most_similar(positive=['自然科学', '政治学'], negative=['物理学'])
words
# 王菲-章子怡=?-汪峰
words = word_vectors.most_similar(positive=['王菲', '汪峰'], negative=['章子怡'])
words
posted @ 2020-01-21 17:10  小萝卜鸭  阅读(3801)  评论(0编辑  收藏  举报