word2vec简单讲解+python gensim库使用

本文深入探讨词向量、word2vec及wordembedding的概念,解析word2vec的CBOW与Skip-Gram模型,阐述哈夫曼编码、负采样及HierarchicalSoftmax在词向量训练中的应用,并通过实例演示gensim库的word2vec模型训练。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

词向量、word2vec、word embedding

首先讲解一下词向量,词向量就是把一个词变成向量形式,这个向量形式可以表示这个词,在模型中直接用词向量作为这个单词的表示作为输入,因为算法模型是不清楚这些单词的,它只知道数字,所有的都是数字,所以词向量就是一个向量,一般是1*n维的行向量,word2vec字面理解就是将word转换为向量,所以word2vec也是用来将单词转换为词向量的一种方式,而另一个名词word embedding是什么意思呢?word embedding翻译过来是词嵌入,理解为将单词嵌入到数字空间中,其实也是用数字来表示单词,嵌入数字可以理解为是词向量的一种,word2vec也是word embedding的一种。

word2vec 的思想

one-hot模型带来的问题是单词及其稀疏,且单词与单词之间无关系,都是独立的个体,百万级别的预料中,肯跟单词就有百万级别,那么one-hot表示的单词维度特别长,占内存且用于计算运算量大,所以有另一种方式就比较好,另一种思想就是分布表示:

  • 分布表示
    将字符变量映射到固定长度的向量中,向量空间中的点可以表示某个字符变量,且字符间的距离有意义。理想状况下,两个对象越相似其在空间中的距离就越近。

word2vec就是分布表示的一种,思想主要是想通过一个固定滑动窗口,单词为中心,左右两侧的单词都是与该单词高度相关,也就是该单词的上下文单词。这么做的依据是:学习词与词之间的映射关系。这个关系可以用来做为词与词之间的度量关系。比如:男孩和男人这两个词,虽然他们不可能出现在一个窗口内,但是可能会出现:

  • A:男孩喜欢汽车
  • B:男人喜欢汽车

这样“男孩”与“汽车”相关,“男人”也与“汽车”相关,这样就可以学习到共同的单词,自然“男孩”与 “男人”也就特别相似了,几乎可以互相替换。

word2vec的两种模型方式:CBOW与Skip-Gram

首先word2vec中有CBOW与Skip-Gram两种实现方式,这两种方式有什么区别呢?

  • CBOW:Continuous Bag-of-Words 缩写,思想是用文本某个词的上下文单词来预测这个词,输入:某个单词周边一定数量的单词,输出:预测这个单词,
    例子:“我想你看看这句话的前三个字”分词后:“我想你“、”看看“、”这句话“、”前三个字“,我们的目标是“这句话”这个单词,CBOW模型就会用上下文的“看看”“前三个字”来作为输入,输出就是预测这句话单词的概率,输出也是一个向量,这里上下文的单词个数可以自己定,不讨论向量形式。

  • Skip-Gram:思想是用单词来预测上下文的单词信息,用这句话作为输入,输出预测上下文的“看看”“前三个字”的概率。
    上述就CBOW与Skip-Gram的模型思想,不讨论具体的表示形式,详情大家可以关注参考博客,后续我也会讲解到。

模型表示

我们具体看一下CBOW与Skip-Gram模型图,先看下CBOW模型:

Skip-Gram模型:

虽然理念不一样,但是两个模型结构比较接近,所以归纳成一个模型就是:

这其实是一个神经网络的结构,也就是说早期其实是把单词的词向量作为一个任务,我们来了解网络结构:

  • 输入是一个单词向量
    每个单词是one-hot向量,表示为: d i = [ 1 , 0 , 0 , 0 ] d_i=[1,0,0,0] di=[1,0,0,0],这个向量长度是训练的文本中单词的个数,向量只有1个1,其他都是0,这样1所在的位置就表示具体的某个单词。我们假设向量是1*n维的向量。

  • 隐层
    这是一个全连接的隐层,输入到隐层、隐层到输出层都是全连接,采用的是 y = w x + b y=wx+b y=wx+b,这里没用激活函数,所以只有线性特性,隐层的节点数可以设定,我们假设有m个节点,则输入权重 w i n : n ∗ m w_{in}:n*m winnm维度的,输出权重是 w o u t : m ∗ n w_{out}:m*n woutmn,这里输出层的第二维度 n n n是因为输出层也是一个单词的向量形式,也就是1*n维的单词向量。

  • 输出层
    输出层加了soft Max 函数,每个单词的表示也是1*n维的,但输出的每一个维度都是概率值,跟one-hot表示的标签值是不一样的

  • 词向量
    那么这样训练之后,我们得到的词向量是什么呢?通过一次DNN前向传播算法得到概率大小排前8的softmax概率对应的神经元所对应的词即可。另一种是主流深度学习的方式是:通过词嵌入(embedding)的方式获得的,词向量是输入层和隐藏层之间的权重。



这样神经网络的结构就出来了,根据误差反向传递的方式来更新网络权重参数和偏移值。那么问题来了,样本怎么构建?

构建样本

构建样本的步骤前面其实有讲,其实就是滑动窗口的方式来得到输入和输出:

  • 使用较小的窗口大小(2-15)
    两个嵌入之间的高相似性得分表明这些单词是可互换的(注意,如果我们只查看附近距离很近的单词,反义词通常可以互换——例如,好的和坏的经常出现在类似的语境中)。
  • 使用较大的窗口大小(15-50,甚至更多)会得到相似性更能指示单词相关性的嵌入
    在实际操作中,你通常需要对嵌入过程提供指导以帮助读者得到相似的”语感“。

Gensim默认窗口大小为5(除了输入字本身以外还包括输入字之前与之后的两个字)。

用神经网络训练词向量缺点?

从上面的描述来看,存在的问题则是计算量大,体现在哪里呢?主要体现在最后一步的预测结果的误差计算上,我们来看下,每一个预测的结果都是1*n维度的,也就计算误差也要计算n词,每个单词计算误差都要经过这一步,几百万单词,输入的样本有几百万,样本的输入维度、输出维度也都是几百万长度,这个计算量确实大,同时影响着反向传播。
所以word2vec内部其实不是这么实现的,不是完全的神经网络,而是有如下改善:

  • 哈夫曼编码
  • 任务转换、引入负采样
基于Hierarchical Softmax的模型

Hierarchical Softmax的提出主要是解决隐层到输出端的softmax问题,是依据哈夫曼编码理论得来的,该模型其实已经不是基于神经网络模型,它主要改进了两个方面:

  • 输入端的数据不会有线性的隐层
    而是求平均的方式,基于CBOW的语言模型的Hierarchical Softmax模型对中心词的窗口词词向量进行维度求平均,这里不在需要神经网络。比如输入词向量是(1,2,3,4),(9,6,11,8),(5,10,7,12),那么我们word2vec映射后的词向量就是(5,6,7,8)。
  • 类比隐层到输出层,这里采用Hierarchical Softmax
    依据哈夫曼树的结构,从而实现词到词的路径,树的节点都会计算输入词向量的一个概率,依据概率值决定走节点的左子树还是右子树,其中概率公式是sigmod函数,

我们来看下模型大致流程:
哈夫曼树结构

1、构建哈夫曼树

依据词频来构建哈夫曼树,规定右子树是1,左子树是0,那么每个词都有一个路径,从而使得高频词靠近根节点,减少输入样本的计算、迭代更新路径,其中每个节点也都是计算节点,其中sigmod函数:
δ ( x w T θ ) = 1 1 + e x w T θ \delta(x_w^T\theta)=\frac{1}{1+e^{x_w^T\theta}} δ(xwTθ)=1+exwTθ1
其中 x w x_w xw是词向量, θ \theta θ是模型参数,也是需要迭代更新的参数,它与词向量关联。 δ ( x w T θ ) \delta(x_w^T\theta) δ(xwTθ)>0.5,就走右子树,反过来走左子树。这样经过每一个节点都要计算一次概率,从根节点到叶子节点经过一系列点。

2、更新迭代 θ \theta θ

既然我们的输入词向量的到叶子节点的路径是已经知道的,我们通过似然最大化来更新 θ \theta θ。什么是似然?极大似然是频率学派的参数估计方法,固定参数,不同的样本作为输入,这叫概率,固定样本,求解参数,这叫似然。本节就是已知路径,求解参数 θ \theta θ。首先我们看下似然函数:
∏ j = 2 l w P ( d j w ∣ x w , θ j − 1 w ) = ∏ j = 2 l w [ δ ( x w T θ j − 1 w ) ] 1 − d j w [ 1 − δ ( x w T θ j − 1 w ) ] d j w \prod_{j=2}^{l_w} P(d_j^w|x_w,\theta_{j-1}^w) = \prod_{j=2}^{l_w}[\delta(x_w^T\theta_{j-1}^w)]^{1-d_j^w}[1-\delta(x_w^T\theta_{j-1}^w)]^{d_j^w} j=2lwP(djwxw,θj1w)=j=2lw[δ(xwTθj1w)]1djw[1δ(xwTθj1w)]djw
解释一下参数:

  • w:当前的输入词
  • d j w d_j^w djw:表明理想情况下该节点的正确的走向,如果经过该节点后走左子树,则 d j w = 0 d_j^w=0 djw=0,走右子树则 d j w = 1 d_j^w=1 djw=1
  • x w x_w xw :w的词向量
  • l w l_w lw :当前词到输出词的哈夫曼树路径。
  • δ \delta δ:概率函数,一般是sigmoid函数,上文有公式。
  • θ j − 1 w \theta_{j-1}^w θj1w:求解概率的参数,表明跟词向量、节点有关。说明不同词作为输入,即使通过同一个节点,参数都不一样。

举个例子,单词“我”的哈夫曼路径是[1,0,1],则似然公式为:
p = ( 1 − 1 1 + e x w T θ 1 ) ( 1 1 + e x w T θ 2 ) ( 1 − 1 1 + e x w T θ 3 ) p=(1-\frac{1}{1+e^{x_w^T\theta_1}})(\frac{1}{1+e^{x_w^T\theta_2}})(1-\frac{1}{1+e^{x_w^T\theta_3}}) p=(11+exwTθ11)(1+exwTθ21)(11+exwTθ31)

3、训练

我们的目标是保证似然函数最大,这其实是一个求最大值的问题:
m a x ∏ j = 2 l w P ( d j w ∣ x w , θ j − 1 w ) max \prod_{j=2}^{l_w} P(d_j^w|x_w,\theta_{j-1}^w) maxj=2lwP(djwxw,θj1w)
这里采取梯度下降的方法,通过求解一阶导数来更新参数,这里就列一个结果。

一些疑问:

  • 词向量初始化值是怎么来的?
    随机初始化的,在后面会不断更新。随机初始化词向量是第一轮迭代的输入。在第一轮迭代完毕后,所有的词向量会更新。在第二轮迭代的时候,输入就已经不是随机初始化的词向量了,而是第一轮迭代后更新的词向量。

  • 哈夫曼树怎么来的?
    严格的说是用训练语料中的词和该词的上下文,训练出一颗哈夫曼树,同时可以得到训练样本中每个词的词向量。如果词汇表的某些词没有训练样本,那么就无法得到它的词向量。正常其实是根据词频,因为高词频的单词会经常被用到

  • 每次优化的都不是该单词的词向量?
    是的,目标词的词向量,只有在它作为其他样本的上下文词出现时,才会更新。

  • skip-gram 模型中,不需要中心词,每次都是窗口词,更新也是窗口词,中心词是不是就没用啦?
    中心词有用的,至少中心词提供了窗口词,可以提供哈夫曼树路径,这样才可以更新窗口词。

  • θ \theta θ值是固定不变的,还是跟中心词有关?
    哈夫曼树的内部节点参数𝜃只与中心词有关。不同的样本如果其中心词不同,则对应的参数𝜃不同。在迭代的过程中,𝜃会更新。比如某一个以𝑤为中心词的样本来了,我们会更新𝑤的所有参数𝜃,另一个以𝑤为中心词的样本来了,我们会在刚才更新后的基础上再更新𝑤的所有参数𝜃。另一个中心词,参数𝜃就不一样。所以说每个节点对于每个词w都有参数。不过,这样不就太多参数𝜃,一个百万级别的单词,可能也有百万级的𝜃,有点可怕?

  • skip-gram 与 dbow 那个更快?
    skip-gram 比cbow 的计算复杂度高了2C倍,因为两个模型的计算都集中在哈夫曼树上,每一次输入,无论是前馈还是反馈,skip-gram的概率计算都是2c次,cbow只有一次。但每一次都是更新,每一轮更新词向量个数来说,skip-gram更新的要多一些。

任务转换、引入负采样
  • 之前的任务
    计算输入、输出单词的关系,输入和输出都是维度的。
  • 修改任务
    输入是两个单词,目的是输出相邻性,相邻性就是一个概率值,这样一个样本误差只计算一次。输出一个表明它们是否是邻居的分数(0表示“不是邻居”,1表示“邻居”)。

图解看下:

总的结构就是:

简单的变换将我们需要的模型从神经网络改为逻辑回归模型或者分类问题——因此它变得更简单,计算速度更快,在几分钟内就能处理数百万个例子,是我们还需要解决一个问题:分类问题需要分类为1的样本,也需要分类为0的样本。

  • 如果所有的例子都是邻居(目标:1)
    样本的标签值都是1,那么训练的模型准确性是百分百了,还有什么意义呢?
  • 目标:0的例子哪里来呢?
    答案是负采样

负采样就是随机抽取不相邻的单词组合成标签值为0的样本,但好像不是完全随机,跟单词频率有关,那么负采样的个数呢?原始论文认为5-20个负样本是比较理想的数量。它还指出,当你拥有足够大的数据集时,2-5个似乎就已经足够了。Gensim默认为5个负样本。也就是一个标签值为1的样本,有5个标签值为0的样本。

但以上更多的偏向词嵌入的方法,word2vec并不是这么做的,它的算法实现比较特殊,它也是引入的负采样,它是基于似然函数最大为目标,依据梯度上升法来更新数据。似然函数为:
∏ i = 0 n e g P ( c o n t e n t ( w 0 ) , w i ) = δ ( x w 0 T θ w 0 ) ∏ i = 1 n e g [ 1 − δ ( x w 0 T θ w i ) ] \prod_{i=0}^{neg} P(content(w_0),w_i) =\delta(x_{w_0}^T\theta^{w_0}) \prod_{i=1}^{neg}[1-\delta(x_{w_0}^T\theta^{w_i})] i=0negP(content(w0),wi)=δ(xw0Tθw0)i=1neg[1δ(xw0Tθwi)]
其中:

  • θ w i \theta^{w_i} θwi:表示单词的权重因子,一个单词一个权重因子,所以上述公式:i=1,2…,neg
  • δ ( x w 0 T θ w 0 ) \delta(x_{w_0}^T\theta^{w_0}) δ(xw0Tθw0):表示中心词 w 0 w_0 w0的相似度,当然这个相似度越大越好
  • 1 − δ ( x w 0 T θ w i ) 1-\delta(x_{w_0}^T\theta^{w_i}) 1δ(xw0Tθwi):表示中心词 w 0 w_0 w0与负采样单词的不相似的概率,这个概率越大越好

通过似然函数函数最大我们可以依据梯度上升法来得到更新 θ w i \theta^{w_i} θwi w 0 w_0 w0,更新算法同哈夫曼树的更新一样。这里有几点大家可能比较关心:

  • 1、如何负采样?
    负采样其实是通过将一个长度为1的线段分成M份, M = 1 0 8 M=10^8 M=108,这是平均分成M份的,然后同样有一个长度为1的线段,分成词汇表的长度,假设词汇表长度是10,那么就分成10份,但这不是平均划分的,根据词频来划分长度,词频出现高的单词,划分的线段就长,这样映射到M份线上上占有的平均线段就多,负采样是通过随机再M个线程上找位置,根据位置对应的单词来负采样。其实就是一个M大小的数组,数组值是单词,对数组坐标来随机取坐标,根据坐标找单词。
  • 2、灰出现采样的多个数据是同一个单词?还有可能是正采样数据?
    这是完全有可能的,毫无疑问可能的。但如果数据足够多,随机性足够好,这其实是可以牺牲的,原算法中就没对此种情况做处理。
  • 3、不管是cbow模型还是skip模型,都是得到中心词关联的上下文词向量,更新的也都是这些词向量。
  • 4、训练时一个正例,neg个负例,样本会不平衡嘛?
    预测模型不存在这个问题,分类问题才会有这个问题。
  • 5、一般数据量少的时候Hierarchical Softmax 可能会好一些。如果数据量非常大,则一般使用Negative Sampling。

说一下优点:

  • 哈夫曼树模型中,对于低频词,查找长度太长了,用负采样就无这个问题
  • 哈夫曼树中每个单词在每个节点上都有一个参数 θ \theta θ,而采用负采样就一个参数,减少了很多计算。

安装gensim

安装的时候百度了网上的教程,看到有人发帖说安装的时候遇到与numpy版本冲突,但我在安装的时候没有出现这类问题,不清楚是安装方式的不同,还是因为gensim库更新了,安装命令:

pip install gensim

安装后会提示successful,然后在jupter notbook中引用gensim包,

import gensim
sentences = [['first', 'sentence'], ['second', 'sentence']]
# train word2vec on the two sentences
model = gensim.models.Word2Vec(sentences, min_count=1)

没有报错就说明引用成功,也表明安装没有问题。

实战

这里参考了一篇博客,在他博客的基础上做研究,这个语料分词后的文本链接是语料集

from gensim.models import word2vec
import os
import jieba
import jieba.analyse

#人民的名义的分词后的语料

fileToFenci="D:/Data/in_the_name_of_people/in_the_name_of_people-分词.txt"

# sg: 即我们的word2vec两个模型的选择了。如果是0, 则是CBOW模型,是1则是Skip-Gram模型,默认是0即CBOW模型。
# window:即词向量上下文最大距离

sentences = word2vec.LineSentence(fileToFenci) 

model = word2vec.Word2Vec(sentences, hs=1,min_count=1,window=3,size=100)

modelFilePath="D:/Data/in_the_name_of_people/model.model"
model.save(modelFilePath)

word2vec_model = Word2Vec.load(modelFilePath)

req_count = 5
for key in word2vec_model.wv.similar_by_word('沙瑞金', topn =100):
    if len(key[0])==3:
        req_count -= 1
        print(key[0], key[1])
        if req_count == 0:
            break;


res = word2vec_model.most_similar("反贪局")
print("反贪局")
print(res)


print(word2vec_model.similarity('高育良', '侯亮平'))      #计算两个单词的相似度
print('----------------------------------')
word2vec_model['高育良']      #获取单词的词向量

大家也可以参考本博客的参考博客,继续深入了解。

参考博客

word2vec原理(一) CBOW与Skip-Gram模型基础
[NLP] 秒懂词向量Word2vec的本质
Word2vec Tutorial 英文博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值