1.什么是Word2vec,为什么需要?
- one-hot编码存在维度灾难和语义鸿沟的问题。
维度灾难是one-hot的向量维度和词库大小一致。one-hot表示的向量维度很大,使得数据样本稀疏,距离计算困难,造成维度灾难,样本的特征过于多,导致模型学习过程中容易发生过拟合。
语义鸿沟是因为one-hot生成的词向量都是彼此正交的,体现不出任何语义的联系。
-
Word Embedding就是将「不可计算」「非结构化」的词转化为「可计算」「结构化」的向量。
-
Word2vec 是 Word Embedding 的方法之一。Word2vec是谷歌团队2013年开源推出的一个专门用于获取词向量的工具包,其核心算法是对NNLM运算量最大的那部分进行效率上的改进。word2vec是使用网络训练的手段将单词编码成向量,此方法的编码效果要好于one-hot。
-
优点:
1.由于 Word2vec 会考虑上下文,跟之前的 Embedding 方法相比,效果要更好
2.比之前的 Embedding方法维度更少,所以速度更快
3.通用性很强,可以用在各种 NLP 任务中
2.Word2vec的过程
word2vec主要包含两个模型:跳字模型(skip-gram)和连续词袋模型(continuous bag of words,简称CBOW)。
- CBOW模型根据中心词W(t)周围的词来预测中心词,如下图所示,该模型的特点是输入已知上下文,输出对当前单词的预测。
其学习目标是最大化对数似然函数:
模型的训练过程:
(1)当前词的上下文词语的独热编码输入到输入层。
(2)这些词分别乘以同一个矩阵后分别得到各自的一维向量。
(3)将这些向量取平均为一个一维向量,作为隐含层的输出。
(4)将这个输出向量乘隐含层与输出层之间的矩阵,变成一个一维向量输出。
(5)将向量softmax归一化后输出取每个词的概率向量。
(6)将概率值最大的数对应的词作为预测词。
(7)将预测的结果向量和真实标签向量计算误差
(8)在每次前向传播之后反向传播误差更新权值和阈值矩阵。
-
Skip-gram模型:
skip-gram只是逆转了CBOW的因果关系而已,即已知当前词语,预测上下文。
使用的参数说明:
1.在数据库或文本中出现的特殊单词的汇总词典。这个词典就叫做词汇量,是系统的已知词。词汇量用字母“v”来表示。
2.“N”代表隐藏层中神经元的数量。
3.窗口大小就是预测单词的最大的上下文位置。“c” 代表窗口大小。比如,在给定的架构图中窗口大小为2,因此,我们会在 (t-2), (t-1), (t+1) 和 (t+2) 的上下文位置中预测单词。
4.上下文窗口是指会在给定词的范围内出现的、要预测的单词数量。对于2*c的并且由K表示的窗口大小来说,上下文窗口值是该窗口大小的两倍。给定图像的上下文窗口值是4。
5.输入向量的维度等于|V|。每个单词都要进行one-hot编码。
6.隐藏层的权重矩阵(W)的维度是[|V|, N]。“||” 是把数组值还原的模函数。
7.隐藏层的输出向量是H[N]。
8.隐藏层和输出层之间的权重矩阵 (W’) 维度是[N,|V|]。
9.W’和H之间的点积生成输出向量U[|v|]
具体过程:
- 利用one-hot编码将单词转换为向量,这些向量的维度是 [1,|v|]。
-
单词w(t)从|V|神经元被传递到隐藏层。
-
隐藏层执行权重向量W[|v|, N] 和输入向量w(t)之间的点积运算。这里,我们可以总结出:第(t)行的W[|v|, N] 会输出(H[1, N])。
-
谨记:隐藏层不使用激活函数,所以H[1,k] 会直接传递到输出层。
-
输出层会执行H[1, N]和W’[N, |v|]之间的点积运算,并给出向量U。
-
现在,要得出每个向量的概率,我们要使用softmax函数,因为每次迭代都得出输出向量U,这是一种one-hot编码模式。
-
概率最大的那个单词就是最终结果。如果在指定上下文位置中预测的单词是错误的,我们会使用反向传播算法来修正权重向量W和W’。
以上步骤对字典中的每个单词w(t) 都要执行。而且,每个单词w(t) 会被传递K次。所以我们可以得知,正向传播算法在每段时间内会执行 |v|*k次。
概率函数:
w(c, j) 是在第c个上下文位置上预测的第j个单词;w(O, c)是在第c个上下文位置上出现的实际单词;w(I)是唯一的输入词;u(c, j)是在第c个上下文位置上预测单词时,U向量的第j个值。
损失函数:
由于我们想在第c个上下文位置预测w(c, j) 时实现概率最大化,可以使用损失函数L。
3.word2vec里面技术优点
我们知道在word2vec模型中,训练集或语料库是十分庞大的时候,softmax函数是将每一次的预测都需要基于全部的数据集进行计算,这会是一个非常大的计算过程,这时word2vec有什么优化?
word2vec模型中有两种高效的训练方法:负采样(negative sampling)和层序softmax(hierarchical softmax)
- negative sampling:
负采样是采样负例来帮助训练的手段,用来提高模型的训练速度。比如我们有一个训练样本,中心词是w,它周围上下文共有2c个词,记为context(w)。由于这个中心词w,的确和context(w)相关存在,因此它是一个真实的正例。通过Negative Sampling采样,我们得到neg个和w不同的中心词wi,i=1,2,..neg,这样context(w)和w_i对应的模型参数$\theta_{i},和每个词的词向量。
采用的是sigmoid函数来进行计算。具体的计算过程不做详细说明:blog.csdn.net/sun_brother…
- hierarchical softmax:
由于我们把之前所有都要计算的从输出softmax层的概率计算变成了一颗二叉霍夫曼树,那么我们的softmax概率计算只需要沿着树形结构进行就可以了。如下图所示,我们可以沿着霍夫曼树从根节点一直走到我们的叶子节点的词w2。
和之前的神经网络语言模型相比,我们的霍夫曼树的所有内部节点就类似之前神经网络隐藏层的神经元,其中,根节点的词向量对应我们的投影后的词向量,而所有叶子节点就类似于之前神经网络softmax输出层的神经元,叶子节点的个数就是词汇表的大小。在霍夫曼树中,隐藏层到输出层的softmax映射不是一下子完成的,而是沿着霍夫曼树一步步完成的,因此这种softmax取名为"Hierarchical Softmax"。
如何“沿着霍夫曼树一步步完成”呢?在word2vec中,我们采用了二元逻辑回归的方法,即规定沿着左子树走,那么就是负类(霍夫曼树编码1),沿着右子树走,那么就是正类(霍夫曼树编码0)。判别正类和负类的方法是使用sigmoid函数,即:
其中xw是当前内部节点的词向量,而θ则是我们需要从训练样本求出的逻辑回归的模型参数。
使用霍夫曼树有什么好处呢?首先,由于是二叉树,之前计算量为V,现在变成了log2V。第二,由于使用霍夫曼树是高频的词靠近树根,这样高频词需要更少的时间会被找到,这符合我们的贪心优化思想。
容易理解,被划分为左子树而成为负类的概率为P(−)=1−P(+)。在某一个内部节点,要判断是沿左子树还是右子树走的标准就是看P(−),P(+)谁的概率值大。而控制P(−),P(+)谁的概率值大的因素一个是当前节点的词向量,另一个是当前节点的模型参数θ。
对于上图中的w2,如果它是一个训练样本的输出,那么我们期望对于里面的隐藏节点n(w2,1)的P(−)概率大,n(w2,2)的P(−)概率大,n(w2,3)的P(+)概率大。
- 两种方法对比分析:
虽然通过霍夫曼树的形式能够提升效率,但是如果训练样本的target word是一个很生僻的词,那么就得在霍夫曼树里往下走很久。也就是说,Hierachical softmax其实是一个不够稳定的算法。而Negative Sampling摒弃了霍夫曼树。
4.word2vec简单代码
- gensim
from gensim.models import word2vec
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
model = word2vec.Word2Vec(sens_list,min_count=1,iter=20)
model.save("word2vec.model")
- python
def generate_batch(batch_size, num_skips, skip_window):
global data_index #global关键字 使data_index 可在其他函数中修改其值
assert batch_size % num_skips == 0 #assert断言用于判断后者是否为true,如果返回值为假,处罚异常
assert num_skips <= 2 * skip_window
batch = np.ndarray(shape=(batch_size), dtype=np.int32) #ndarray对象用于存放多维数组
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
span = 2 * skip_window + 1 # [ skip_window target skip_window]
# 初始化最大长度为span的双端队列,超过最大长度后再添加数据,会从另一端删除容不下的数据
# buffer: 1, 21, 124, 438, 11
buffer = collections.deque(maxlen=span) #创建一个队列,模拟滑动窗口
for _ in range(span):
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
for i in range(batch_size // num_skips): # // 是整数除
# target : 2
target = skip_window # target label at the center of the buffer
# target_to_avoid : [2]
targets_to_avoid = [ skip_window ] # 需要忽略的词在当前的span位置
# 更新源单词为当前5个单词的中间单词
source_word = buffer[skip_window]
# 随机选择的5个span单词中除了源单词之外的4个单词中的两个
for j in range(num_skips):
while target in targets_to_avoid:
target = random.randint(0, span - 1)
targets_to_avoid.append(target) # 已经经过的target放入targets_to_avoid
#batch中添加源单词
batch[i * num_skips + j] = source_word
#labels添加目标单词,单词来自随机选择的5个span单词中除了源单词之外的4个单词中的两个
labels[i * num_skips + j, 0] = buffer[target]
# 往双端队列中添加下一个单词,双端队列会自动将容不下的数据从另一端删除
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
return batch, labels
```