transformer学习笔记-词嵌入embedding原理
.在学习transformer的时候,可以看到,输入通常需要对词token进行embedding处理,如果没有先了解embedding的原理可能会有疑问,这个embedding,到底怎么来的,怎么就把一个token 变成一个矩阵,这个矩阵到底暗藏了哪些信息。总不能随便给一个token配一个矩阵,self-attention机制就能处理吧?肯定不是的,接下来我们来尝试理解下embedding的奥秘。
一、词嵌入embedding基本原理
在机器学习领域,机器无法直接对自然语言、图片、语音等数据直接处理,往往需要对其进行编码,这里我们只讨论自然语言领域的词token编码。
1.1、one-hot编码
假设我们现在有一个词库(词汇表),里面有五个词[ I , love , programming , in, Python ],我们通过索引下标将其标识出来分别对应
[1, 2, 3, 4 , 5],但是这样的表示方法,基本很难帮助我们去发现他们之间的关系,比如相似性、多义性,所以我们引入向量空间,帮助我们更好表示不同词token,one-hot就是其中一种:
对于单个词token,比如:I,[ 1, 0, 0, 0, 0] 是个1* n的矩阵, n的值与词汇表中的词token个数相等。
可以看到,当m个token构成的m*n矩阵,起码有如下问题:
1、随着词汇量增多,矩阵的维度(n的个数),占用空间将会特别大;
2、矩阵过于稀疏,元素含0太多,无法有效进行矩阵计算(两个矩阵一算内积全是0),比如计算相似性(余弦相似性);
聪明的人类发明了词嵌入embedding解决以上问题。
1.2、embedding编码
对于一个n维(也就是n列)的one-hot编码(),通过一个n*v嵌入矩阵,将n维向量转成v维向量。
假设这个嵌入矩阵是5 * 3的矩阵,然后对love 和in两个词embedding编码:
可以发现:
1、嵌入矩阵的行,就是词汇库的词token个数,列就是词token embedding后的维度(列数).,也就是v维向量。
2、one-hot矩阵的中token向量 1所在的位置,就是对应嵌入矩阵的第几行,也就是通过one-hot的index从嵌入矩阵查表。
3、实际上,就是通过嵌入矩阵的变换,将one-hot五维空间,投影到embedding三维空间。
我们如何理解这个embedding矩阵?
盲人摸象的故事我们都听过,每个盲人摸到大象的不同位置,对大象的形容就不一样,但是把不同位置的的形容合并到一起,大致就能说明大象的整体特征:
大象 = 鼻子长 + 耳朵大 + 高大 + 牙齿长]
回到嵌入矩阵,假设不同的列代表不同的特征,同时他也是在三维空间里的坐标如:(1.1,0.2,0.5)
以苹果为例:在不同场景中可能不一样,可能是水果、可能是手机、也可能是容貌相关的苹果肌。
假设特征1:是水果、特征2:是手机、特征3:是面貌,嵌入矩阵第一行代表苹果,那么
苹果 = 1.1水果 +0.2手机 +0.5*面貌
也就是说,embedding矩阵的每个元素,代表每个特征的权重,
当然我们也可以将其投射到三维空间中,通过位置关系,表示不同词token之间的相似性和关联度,通常我们采用余弦相似性原理:
也就是两个向量的夹角越小越相似。
回到上面苹果的例子,苹果向量跟水果、手机、面貌的向量位置,决定了在当前预料环境,苹果更可能表示的是哪个特征,也就是词token在空间中的位置,依赖不同语义场景的上下文关系。
实际上,当某一维度的语料越多,词token在空间中的位置就越往该维度调整(夹角越小),位置越靠近。
上面只是对高维稀疏空间投影到低维空间,同样更低维的向量的特征可能被压缩到一起,也可以通过嵌入矩阵将其投射到高维空间,将压缩到一起的特征分离。
那么我们怎么得到这个嵌入矩阵。
二、词嵌入矩阵训练
词嵌入矩阵是通过特定的词嵌入算法训练得到,比如word2vec、FastText、GloVe等,本文我们通过word2vec,大致了解嵌入矩阵的训练过程。
word2vec包含两种算法,一种是C-bow,一种是Skip-Gram,C-bow通过上下文预测目标词,而Skip-Gram通过目标词预测上下文。
当然,此处我们也只了解C-bow的原理,体会下如何得到嵌入矩阵。
C-bow主要通过上下文去预测中间位置的token,比如当窗口大小为1,以 love和in 两个词预测中间的programming,如果窗口大小为2,则已I 、love和in、Python四个词预测中间的词。具体的预测流程也是通过一个神经网络来实现:
预测通过 I ,love,in,Python,得到目标值programming
输入层:
输入层的每个词token是以one-hot编码的向量,也就是1*n的矩阵,图中为省空间,竖排展示,实际计算式按[1,0,0,0,0]横向的方式计算。
以上图为例:
分别将I、love、in、Python四个词token的one-hot编码输入嵌入矩阵
嵌入矩阵:
嵌入矩阵的行数与输入向量的列数相等,n列的输入矩阵,对应的嵌入向量就是n行,嵌入矩阵的维度v,也就是嵌入矩阵的列数,因此嵌入矩阵是一个n*v的矩阵。
图中的四个嵌入矩阵其实是同一个,图中为了表示每个词token单独计算词向量,此处我们使用上面的例子,我们初始化一个 5*3 的嵌入矩阵:(假设下图中的元素值都是随机生成的)
通过嵌入矩阵的变换,得到四个嵌入向量:I [1.1, 0.2 ,0.5],love [-1.7, 0.8 ,0.1],in [0.1, 0.9 ,-0.3],Python[1.3, -0.4 ,0.8]
隐藏层:
隐藏层对上下文中的嵌入向量求和取平均值,
这里,我们对得到四个对应的嵌入向量 ,求和再平均:
得到矩阵[0.2 ,0.375, 0.275]
然后在通过一个v*n的矩阵(一般使用嵌入矩阵的转置)然后通过softmax函数计算词库中每个词的概率分布
输出层
得到词汇表中每个词的概率输出,概率最大的词即为预测的值,此处0.5最高,对应第三个词programming。
但是预测值与真实值相差较大,那么需要根据损失函数反向传播调整嵌入矩阵。可以参考:transformer学习笔记-神经网络原理第二章和第三章。
经过多轮的反向传播调整后,得到最终的嵌入矩阵。
注意: 通过word2vec生成词嵌入,是比较固定的,因为一旦嵌入矩阵训练完成,除非重新训练,否则就是不变的了,因此在不同语境的句子中,同一个词的词嵌入是一样的,即便这个词嵌入同时包含了不同语境下的信息。因此word2vec在处理多义词场景效果不是很好,基于transformer架构的bert模型更擅长此类场景,bert可以动态结合上下文生成词嵌入。
学习完transformer的机制后,有机会我们再深入学习bert模型原理。本文主要为了通过word2vec更好理解我们为什么需要embedding。
三、示例代码
gensim已经封装了c-bow和skip-gram两种算法的实现,以下是示例代码,感兴趣的小伙伴也可以尝试自己实现一个。
import numpy as np
from gensim.models import Word2Vec
from gensim.utils import simple_preprocess
# 准备数据
sentences = [
"I love programming in Python",
"Python is a powerful language",
"JavaScript is also popular",
]
# 预处理数据
processed_sentences = [simple_preprocess(sentence,min_len=1,max_len=16) for sentence in sentences]
# 训练Word2Vec模型 (c-bow)
model = Word2Vec(sentences=processed_sentences, vector_size=4, window=2, min_count=1, workers=4)
#(skip-gram)
#model = Word2Vec(sentences=processed_sentences, vector_size=4, window=2, min_count=1, workers=4, sg=1)
# 获取词向量
word_vector = model.wv['python']
print(word_vector)
word1 = 'python'
word2 = 'language'
# 使用most_similar方法找到与这两个词最相似的词
similar_words1 = model.wv.most_similar(word1, topn=5)
similar_words2 = model.wv.most_similar(word2, topn=5)
print(f"与 {word1} 最相似的词:")
for word, similarity in similar_words1:
print(f"{word}: {similarity}")
print(f"\n与 {word2} 最相似的词:")
for word, similarity in similar_words2:
print(f"{word}: {similarity}")