Word2Vec详解
创始人
2024-03-16 07:49:19

Word2Vec

基本思想:通过训练将每一个词映射成一个固定长度的向量,所有向量构成一个词向量空间,每一个向量(单词)可以看作是向量空间中的一个点,意思越相近的单词距离越近。

在这里插入图片描述

如何把词转换为向量?

通常情况下,我们可以维护一个查询表。表中每一行都存储了一个特定词语的向量值,每一列的第一个元素都代表着这个词本身,以便于我们进行词和向量的映射(如“我”对应的向量值为 [0.3,0.5,0.7,0.9,-0.2,0.03] )。给定任何一个或者一组单词,我们都可以通过查询这个excel,实现把单词转换为向量的目的,这个查询和替换过程称之为Embedding Lookup。

在这里插入图片描述

然而在进行神经网络计算的过程中,需要大量的算力,常常要借助特定硬件(如GPU)满足训练速度的需求。GPU上所支持的计算都是以张量(Tensor)为单位展开的,因此在实际场景中,我们需要把Embedding Lookup的过程转换为张量计算:

在这里插入图片描述

如何让向量具有语义信息?

在自然语言处理研究中,科研人员通常有一个共识:使用一个单词的上下文来了解这个单词的语义,比如:

“苹果手机质量不错,就是价格有点贵。”
“这个苹果很好吃,非常脆。”
“菠萝质量也还行,但是不如苹果支持的APP多。”

在上面的句子中,我们通过上下文可以推断出第一个“苹果”指的是苹果手机,第二个“苹果”指的是水果苹果,而第三个“菠萝”指的应该也是一个手机。在自然语言处理领域,使用上下文描述一个词语或者元素的语义是一个常见且有效的做法。我们可以使用同样的方式训练词向量,让这些词向量具备表示语义信息的能力。

2013年,Mikolov提出的经典word2vec算法就是通过上下文来学习语义信息。word2vec包含两个经典模型:CBOW(Continuous Bag-of-Words)和Skip-gram。

CBOW:通过上下文的词向量推理中心词。
Skip-gram:根据中心词推理上下文。

在这里插入图片描述

CBOW模型

在这里插入图片描述

输入层: 一个形状为C×V的one-hot张量,其中C代表上下文中词的个数,通常是一个偶数,我们假设为4;V表示词表大小,我们假设为5000,该张量的每一行都是一个上下文词的one-hot向量表示。
隐藏层: 一个形状为V×N的参数张量W1,一般称为word-embedding,N表示每个词的词向量长度,我们假设为128。输入张量和word embedding W1进行矩阵乘法,就会得到一个形状为C×N的张量。综合考虑上下文中所有词的信息去推理中心词,因此将上下文中C个词相加得一个1×N的向量,是整个上下文的一个隐含表示。
输出层: 创建另一个形状为N×V的参数张量,将隐藏层得到的1×N的向量乘以该N×V的参数张量,得到了一个形状为1×V的向量。最终,1×V的向量代表了使用上下文去推理中心词,每个候选词的打分,再经过softmax函数的归一化,即得到了对中心词的推理概率:

在这里插入图片描述

Skip-gram模型

在这里插入图片描述

**Input Layer(输入层):**接收一个one-hot张量 V∈R1×vocab_size
作为网络的输入,假设vocab_size为5000。
**Hidden Layer(隐藏层):**将张量V乘以一个word embedding张量W1∈Rvocab_size×embed_size ,假设embed_size为128,并把结果作为隐藏层的输出,得到一个形状为R1×embed_size的张量,里面存储着当前句子中心词的词向量。
**Output Layer(输出层):**将隐藏层的结果乘以另一个word embedding张量W2∈Rembed_size×vocab_size,得到一个形状为R1×vocab_size的张量。这个张量经过softmax变换后,就得到了使用当前中心词对上下文的预测结果。根据这个softmax的结果,我们就可以去训练词向量模型。

Skip-gram在实际操作中,使用一个滑动窗口(一般情况下,长度是奇数),从左到右开始扫描当前句子。每个扫描出来的片段被当成一个小句子,每个小句子中间的词被认为是中心词,其余的词被认为是这个中心词的上下文。

Skip-gram实现

1.数据处理:

首先下载数据集处理语料,

# 下载语料用来训练word2vec
download()
# 读取text8数据
corpus = load_text8()
# 对语料进行预处理(分词)并把所有英文字符都转换为小写
corpus = data_preprocess(corpus)
# 构造词典,统计每个词的频率,并根据频率将每个词转换为一个整数id
word2id_freq, word2id_dict, id2word_dict = build_dict(corpus)
vocab_size = len(word2id_freq)
#保存word2id_freq, word2id_dict, id2word_dict到本地
f_save = open('word2id_freq.pkl', 'wb')
pickle.dump(word2id_freq, f_save)
f_save.close()
f_save = open('word2id_dict.pkl', 'wb')
pickle.dump(word2id_dict, f_save)
f_save.close()
f_save = open('id2word_dict.pkl', 'wb')
pickle.dump(id2word_dict, f_save)
f_save.close()
# 把语料转换为id序列
corpus = convert_corpus_to_id(corpus, word2id_dict)
# 使用二次采样算法(subsampling)处理语料,强化训练效果,遗弃频率高的词
corpus = subsampling(corpus, word2id_freq)

构造数据,假设有一个中心词c和一个上下文词正样本tp。在Skip-gram的理想实现里,需要最大化使用c推理tp的概率。在使用softmax学习时,需要最大化tp的推理概率,同时最小化其他词表中词的推理概率。之所以计算缓慢,是因为需要对词表中的所有词都计算一遍。然而我们还可以使用另一种方法,就是随机从词表中选择几个代表词,通过最小化这几个代表词的概率,去近似最小化整体的预测概率。比如,先指定一个中心词(如“人工”)和一个目标词正样本(如“智能”),再随机在词表中采样几个目标词负样本(如“日本”,“喝茶”等)。对于目标词正样本,我们需要最大化它的预测概率;对于目标词负样本,我们需要最小化它的预测概率。通过这种方式,我们就可以完成计算加速。上述做法,我们称之为负采样。实现如下:

# 构造数据,准备模型训练
# max_window_size代表了最大的window_size的大小,程序会根据max_window_size从左到右扫描整个语料
# negative_sample_num代表了对于每个正样本,我们需要随机采样多少负样本用于训练,
# 一般来说,negative_sample_num的值越大,训练效果越稳定,但是训练速度越慢。
def build_data(corpus, word2id_dict, word2id_freq, max_window_size=3, negative_sample_num=4):# 使用一个list存储处理好的数据dataset = []# 从左到右,开始枚举每个中心点的位置for center_word_idx in range(len(corpus)):# 以max_window_size为上限,随机采样一个window_size,这样会使得训练更加稳定window_size = random.randint(1, max_window_size)# 当前的中心词就是center_word_idx所指向的词center_word = corpus[center_word_idx]# 以当前中心词为中心,左右两侧在window_size内的词都可以看成是正样本positive_word_range = (max(0, center_word_idx - window_size), min(len(corpus) - 1, center_word_idx + window_size))positive_word_candidates = [corpus[idx] for idx in range(positive_word_range[0], positive_word_range[1] + 1) ifidx != center_word_idx]# 对于每个正样本来说,随机采样negative_sample_num个负样本,用于训练for positive_word in positive_word_candidates:# 首先把(中心词,正样本,label=1)的三元组数据放入dataset中,# 这里label=1表示这个样本是个正样本dataset.append((center_word, positive_word, 1))# 开始负采样i = 0while i < negative_sample_num:negative_word_candidate = random.randint(0, vocab_size - 1)if negative_word_candidate not in positive_word_candidates:# 把(中心词,正样本,label=0)的三元组数据放入dataset中,# 这里label=0表示这个样本是个负样本dataset.append((center_word, negative_word_candidate, 0))i += 1return dataset

2.网络定义

import paddle
from paddle.nn import Embedding
import paddle.nn.functional as F
import paddle.nn as nn#定义skip-gram训练网络结构
#使用paddlepaddle的2.0.0版本
#一般来说,在使用paddle训练的时候,我们需要通过一个类来定义网络结构,这个类继承了paddle.nn.layer
class SkipGram(nn.Layer):def __init__(self, vocab_size, embedding_size, init_scale=0.1):# vocab_size定义了这个skipgram这个模型的词表大小# embedding_size定义了词向量的维度是多少# init_scale定义了词向量初始化的范围,一般来说,比较小的初始化范围有助于模型训练super(SkipGram, self).__init__()self.vocab_size = vocab_sizeself.embedding_size = embedding_size# 使用Embedding函数构造一个词向量参数# 这个参数的大小为:[self.vocab_size, self.embedding_size]# 数据类型为:float32# 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样self.embedding = Embedding(num_embeddings = self.vocab_size,embedding_dim = self.embedding_size,weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(low=-init_scale, high=init_scale)))# 使用Embedding函数构造另外一个词向量参数# 这个参数的大小为:[self.vocab_size, self.embedding_size]# 这个参数的初始化方式为在[-init_scale, init_scale]区间进行均匀采样self.embedding_out = Embedding(num_embeddings = self.vocab_size,embedding_dim = self.embedding_size,weight_attr=paddle.ParamAttr(initializer=paddle.nn.initializer.Uniform(low=-init_scale, high=init_scale)))# 定义网络的前向计算逻辑# center_words是一个tensor(mini-batch),表示中心词# target_words是一个tensor(mini-batch),表示目标词# label是一个tensor(mini-batch),表示这个词是正样本还是负样本(用0或1表示)# 用于在训练中计算这个tensor中对应词的同义词,用于观察模型的训练效果def forward(self, center_words, target_words, label):# 首先,通过self.embedding参数,将mini-batch中的词转换为词向量# 这里center_words和eval_words_emb查询的是一个相同的参数# 而target_words_emb查询的是另一个参数center_words_emb = self.embedding(center_words)target_words_emb = self.embedding_out(target_words)# 我们通过点乘的方式计算中心词到目标词的输出概率,并通过sigmoid函数估计这个词是正样本还是负样本的概率。word_sim = paddle.multiply(center_words_emb, target_words_emb)word_sim = paddle.sum(word_sim, axis=-1)word_sim = paddle.reshape(word_sim, shape=[-1])pred = F.sigmoid(word_sim)# 通过估计的输出概率定义损失函数,注意我们使用的是binary_cross_entropy_with_logits函数# 将sigmoid计算和cross entropy合并成一步计算可以更好的优化,所以输入的是word_sim,而不是predloss = F.binary_cross_entropy_with_logits(word_sim, label)loss = paddle.mean(loss)# 返回前向计算的结果,飞桨会通过backward函数自动计算出反向结果。return pred, loss

3.网络训练:

# 开始训练,定义一些训练过程中需要使用的超参数
batch_size = 512
epoch_num = 3
embedding_size = 200
step = 0
learning_rate = 0.001# 定义一个使用word-embedding查询同义词的函数
# 这个函数query_token是要查询的词,k表示要返回多少个最相似的词,embed是我们学习到的word-embedding参数
# 我们通过计算不同词之间的cosine距离,来衡量词和词的相似度
# 具体实现如下,x代表要查询词的Embedding,Embedding参数矩阵W代表所有词的Embedding
# 两者计算Cos得出所有词对查询词的相似度得分向量,排序取top_k放入indices列表
def get_similar_tokens(query_token, k, embed):W = embed.numpy()x = W[word2id_dict[query_token]]cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9)flat = cos.flatten()indices = np.argpartition(flat, -k)[-k:]indices = indices[np.argsort(-flat[indices])]for i in indices:print('for word %s, the similar word is %s' % (query_token, str(id2word_dict[i])))# 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络
skip_gram_model = SkipGram(vocab_size, embedding_size)# 构造训练这个网络的优化器
adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())# 使用build_batch函数,以mini-batch为单位,遍历训练数据,并训练网络
for center_words, target_words, label in build_batch(dataset, batch_size, epoch_num):# 使用paddle.to_tensor,将一个numpy的tensor,转换为飞桨可计算的tensorcenter_words_var = paddle.to_tensor(center_words)target_words_var = paddle.to_tensor(target_words)label_var = paddle.to_tensor(label)# 将转换后的tensor送入飞桨中,进行一次前向计算,并得到计算结果pred, loss = skip_gram_model(center_words_var, target_words_var, label_var)# 程序自动完成反向计算loss.backward()# 程序根据loss,完成一步对参数的优化更新adam.step()# 清空模型中的梯度,以便于下一个mini-batch进行更新adam.clear_grad()# 每经过100个mini-batch,打印一次当前的loss,看看loss是否在稳定下降step += 1if step % 1000 == 0:print("step %d, loss %.3f" % (step, loss.numpy()[0]))# 每隔10000步,打印一次模型对以下查询词的相似词,这里我们使用词和词之间的向量点积作为衡量相似度的方法,只打印了5个最相似的词并保存网络模型参数和优化器模型参数if step % 10000 == 0:get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)get_similar_tokens('one', 5, skip_gram_model.embedding.weight)get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)paddle.save(skip_gram_model.state_dict(), "text8.pdparams")paddle.save(adam.state_dict(), "adam.pdopt")

在这里插入图片描述
这里step到300000我就结束训练了,可以看到词性已经很接近了。

4.测试训练模型

训练完成后,对任意词都可以基于我们训练出模型计算出跟这个词最接近的词。

# 定义一些训练过程中需要使用的超参数
embedding_size = 200
learning_rate = 0.001# 通过我们定义的SkipGram类,来构造一个Skip-gram模型网络skip_gram_model = SkipGram(vocab_size, embedding_size)# 构造训练这个网络的优化器
adam = paddle.optimizer.Adam(learning_rate=learning_rate, parameters=skip_gram_model.parameters())# 加载网络模型和优化器模型
layer_state_dict = paddle.load("./my_model/text8.pdparams")
opt_state_dict = paddle.load("./my_model/adam.pdopt")skip_gram_model.set_state_dict(layer_state_dict)
adam.set_state_dict(opt_state_dict)# get_similar_tokens('movie', 5, skip_gram_model.embedding.weight)
# get_similar_tokens('one', 5, skip_gram_model.embedding.weight)
# get_similar_tokens('chip', 5, skip_gram_model.embedding.weight)
get_similar_tokens('she', 5, skip_gram_model.embedding.weight)
get_similar_tokens('dog', 5, skip_gram_model.embedding.weight)
get_similar_tokens('apple', 5, skip_gram_model.embedding.weight)
get_similar_tokens('beijing', 5, skip_gram_model.embedding.weight)

结果如下,我没有完全训练完,可以看到效果还可以。

在这里插入图片描述

源码已经上传到GitHub上,github链接:https://github.com/fakerst/Word2Vec-SkipGram 网络有些差,后续会把训练好的模型也进行上传。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...