自然语言处理(NLP)的核心挑战在于将人类语言转化为计算机可处理的数字形式。词嵌入(Word Embedding)和位置编码(Position Encoding)正是解决这一问题的两大关键技术。本文将通过实例和代码,深入浅出地讲解这两个概念的原理与应用。
一、词嵌入的必要性
计算机只能理解数字,当我们希望它分析"我喜欢吃苹果"这句话时,必须先将文字转换为数字表示。
最简单的方式是为每个词分配唯一编号:
- "我" → 1
- "喜欢" → 2
- "吃" → 3
- "苹果" → 4
这种方法称为独热编码(One-Hot Encoding),但存在明显缺陷:每个词都是孤立的,模型无法感知"苹果"与"香蕉"同属水果,"喜欢"与"热爱"语义相近。
词嵌入通过将每个词映射为高维向量来解决这个问题:
- "苹果" → [0.2, 0.5, 0.1, ..., 0.3](100维向量)
- "香蕉" → [0.3, 0.4, 0.2, ..., 0.1]
这些向量在多维空间中的距离(如余弦相似度)能够反映词义的相似程度,语义相近的词在向量空间中会彼此靠近。
二、PyTorch实现词嵌入
下面用PyTorch实现一个简单的词嵌入示例:
import torch
import torch.nn as nn
# 假设词典包含8个单词
vocab_size = 8 # 词典大小
embedding_dim = 4 # 每个词向量的维度
# 创建词嵌入层
embedding = nn.Embedding(vocab_size, embedding_dim)
# 用单词索引表示句子
sentence = torch.tensor([1, 3, 5, 2]) # 例如:[我, 喜欢, 吃, 苹果]
# 获取词嵌入向量
embedded_sentence = embedding(sentence)
print("原始句子索引:", sentence)
print("词嵌入后的结果形状:", embedded_sentence.shape) # 输出: [4, 4]
print("嵌入后的向量:\n", embedded_sentence)
代码解析:
nn.Embedding 创建了一个可训练的查找表
- 表的维度为
vocab_size × embedding_dim
- 输入词的索引,输出对应的向量表示
初始时这些向量是随机的,但随着训练的进行,模型会学习到有意义的表示,使得"苹果"和"香蕉"的向量逐渐接近。
三、序列建模中的位置信息问题
词嵌入解决了词义表示,但忽略了一个关键因素:词序。
在自然语言中,词的顺序至关重要:
- "狗咬人"与"人咬狗"意思截然不同
- "我今天吃了苹果"与"我吃了苹果今天"虽表达同一事件,但语序差异明显
标准词嵌入模型会将所有词视为无序集合,为了让模型感知词序,需要引入位置编码。
四、位置编码原理
位置编码的目标是为每个词添加位置信息。最直观的方法是简单编号:
但这种线性编号难以让模型学习相对位置关系(如"第3个词"与"第4个词"相邻)。
Transformer提出了更优雅的正弦余弦位置编码方案:
- 为每个位置生成一个向量
- 向量的各维度使用不同频率的正弦和余弦函数
- 相邻位置的编码向量相似,距离较远的位置编码差异较大
代码实现如下:
import torch
import math
max_position = 10 # 最大位置数
embedding_dim = 4 # 向量维度
# 创建位置编码矩阵
position_encoding = torch.zeros(max_position, embedding_dim)
# 对每个位置
for pos in range(max_position):
# 对每个维度
for i in range(0, embedding_dim, 2):
# 偶数维度使用正弦函数
position_encoding[pos, i] = math.sin(pos / (10000 ** (i / embedding_dim)))
# 奇数维度使用余弦函数
if i + 1 < embedding_dim:
position_encoding[pos, i + 1] = math.cos(pos / (10000 ** (i / embedding_dim)))
print("位置编码矩阵形状:", position_encoding.shape) # [10, 4]
print("前5个位置的编码:\n", position_encoding[:5])
这种位置编码具有以下特性:
- 不同位置的编码向量各不相同
- 相邻位置的编码相似(可通过向量点积验证)
- 能够表示相对位置关系
五、词嵌入与位置编码的结合
实际应用中,我们将词嵌入和位置编码相加作为模型输入。以下是两个完整示例:
示例1:基础组合
import torch
import torch.nn as nn
import torch.nn.functional as F
# 参数设置
vocab_size = 10
embedding_dim = 16
max_seq_len = 5
# 创建词嵌入层
word_embedding = nn.Embedding(vocab_size, embedding_dim)
# 创建位置编码
position_encoding = torch.zeros(max_seq_len, embedding_dim)
for pos in range(max_seq_len):
for i in range(0, embedding_dim, 2):
position_encoding[pos, i] = math.sin(pos / (10000 ** (i / embedding_dim)))
if i + 1 < embedding_dim:
position_encoding[pos, i + 1] = math.cos(pos / (10000 ** (i / embedding_dim)))
# 将位置编码转为Embedding层(不参与训练)
position_embedding = nn.Embedding.from_pretrained(position_encoding, freeze=True)
# 假设有一个句子
sentence_indices = torch.tensor([2, 4, 6, 0, 0]) # 0表示padding
sentence_length = 3 # 实际有效长度
# 获取词嵌入
word_vectors = word_embedding(sentence_indices)
# 创建位置索引
positions = torch.arange(sentence_length, dtype=torch.long)
positions = positions.unsqueeze(0).expand(1, sentence_length)
# 获取位置嵌入
pos_vectors = position_embedding(positions)
# 组合词嵌入和位置嵌入
final_vectors = word_vectors[:sentence_length] + pos_vectors
print("词嵌入形状:", word_vectors.shape) # [5, 16]
print("位置嵌入形状:", pos_vectors.shape) # [1, 3, 16]
print("最终向量形状:", final_vectors.shape) # [3, 16]
这个示例展示了:
- 创建词嵌入层和位置编码层
- 对句子(用索引表示)进行处理
- 获取词嵌入和位置嵌入
- 将两者相加得到最终输入表示
示例2:批次处理与序列对齐
import torch
import numpy
import torch.nn as nn
import torch.nn.functional as F
# 批次大小
batch_size = 2
# 单词表大小
max_num_src_words = 8
max_num_tgt_words = 8
model_dim = 16
# 序列大小
max_src_seq_len = 5
max_tgt_seq_len = 5
max_position_len = 5
# 序列长度
src_len = torch.Tensor([2, 4]).to(torch.int)
tgt_len = torch.Tensor([4, 3]).to(torch.int)
# 生成单词索引构成的句子
src_seq = torch.cat([torch.unsqueeze(F.pad(torch.randint(1, max_num_src_words, (L,)), (0, max_src_seq_len - L)), 0) for L in src_len])
tgt_seq = torch.stack([F.pad(torch.randint(1, max_num_tgt_words, (L,)), (0, max_tgt_seq_len - L)) for L in tgt_len], 0)
# 构造词嵌入
src_embedding_table = nn.Embedding(max_num_src_words+1, model_dim)
tgt_embedding_table = nn.Embedding(max_num_tgt_words+1, model_dim)
src_embedding = src_embedding_table(src_seq)
tgt_embedding = tgt_embedding_table(tgt_seq)
# 构造位置编码
pos_mat = torch.arange(max_position_len).reshape((-1, 1))
i_mat = torch.pow(10000, torch.arange(0, model_dim, 2).reshape((1, -1)) / model_dim)
pe_embedding_table = torch.zeros(max_position_len, model_dim)
pe_embedding_table[:, 0::2] = torch.sin(pos_mat / i_mat)
pe_embedding_table[:, 1::2] = torch.cos(pos_mat / i_mat)
pe_embedding = nn.Embedding(max_position_len, model_dim)
pe_embedding.weight = nn.Parameter(pe_embedding_table, requires_grad=False)
src_pos = torch.cat([torch.unsqueeze(torch.arange(max(src_len)), 0) for _ in src_len]).to(torch.int32)
tgt_pos = torch.cat([torch.unsqueeze(torch.arange(max(tgt_len)), 0) for _ in src_len]).to(torch.int32)
src_pe_embedding = pe_embedding(src_pos)
tgt_pe_embedding = pe_embedding(tgt_pos)
print(src_pos)
print(tgt_pos)
print(src_pe_embedding)
print(tgt_pe_embedding)
数据准备:序列与批次
代码首先定义了基本参数:
batch_size = 2 # 批次大小
max_num_src_words = 8 # 源语言词汇表大小
max_num_tgt_words = 8 # 目标语言词汇表大小
model_dim = 16 # 向量维度
max_src_seq_len = 5 # 源序列最大长度
max_tgt_seq_len = 5 # 目标序列最大长度
max_position_len = 5 # 最大位置数
src_len = torch.Tensor([2, 4]).to(torch.int) # 源序列实际长度
tgt_len = torch.Tensor([4, 3]).to(torch.int) # 目标序列实际长度
这里定义了:
- 批次大小:一次处理2个序列对
- 词汇表大小:源语言和目标语言各8个单词
- 模型维度:每个词向量16维
- 序列长度:源序列长度为2和4,目标序列长度为4和3
生成序列数据
src_seq = torch.cat([torch.unsqueeze(F.pad(torch.randint(1, max_num_src_words, (L,)), (0, max_src_seq_len - L)), 0) for L in src_len])
tgt_seq = torch.stack([F.pad(torch.randint(1, max_num_tgt_words, (L,)), (0, max_tgt_seq_len - L)) for L in tgt_len], 0)
这段代码完成三个步骤:
- 随机生成单词索引:
torch.randint 生成1到8之间的随机整数
- 填充序列:
F.pad 将短序列用0填充到固定长度
- 组合批次:
torch.cat 或 torch.stack 将多个序列组合
生成的源序列示例:
tensor([[6, 5, 0, 0, 0], # 第一个序列长度2,补3个0
[1, 4, 3, 4, 0]]) # 第二个序列长度4,补1个0
词嵌入:索引到向量的映射
src_embedding_table = nn.Embedding(max_num_src_words+1, model_dim)
tgt_embedding_table = nn.Embedding(max_num_tgt_words+1, model_dim)
src_embedding = src_embedding_table(src_seq)
tgt_embedding = tgt_embedding_table(tgt_seq)
关键点:
- 参数:
(词汇表大小, 向量维度)
- +1的原因:0用作填充符号,所以需要9个位置(0-8)
- 工作原理:可训练的查找表,将索引映射到向量
位置编码:添加位置信息
pos_mat = torch.arange(max_position_len).reshape((-1, 1))
i_mat = torch.pow(10000, torch.arange(0, model_dim, 2).reshape((1, -1)) / model_dim)
pe_embedding_table = torch.zeros(max_position_len, model_dim)
pe_embedding_table[:, 0::2] = torch.sin(pos_mat / i_mat)
pe_embedding_table[:, 1::2] = torch.cos(pos_mat / i_mat)
pe_embedding = nn.Embedding(max_position_len, model_dim)
pe_embedding.weight = nn.Parameter(pe_embedding_table, requires_grad=False)
实现了Transformer的正弦余弦位置编码:
- 位置矩阵:
[0, 1, 2, 3, 4] 表示词的位置
- 频率矩阵:控制正弦余弦函数的周期
- 编码表:偶数维用正弦,奇数维用余弦
- 固定参数:
requires_grad=False 训练时不更新
获取位置编码向量
src_pos = torch.cat([torch.unsqueeze(torch.arange(max(src_len)), 0) for _ in src_len]).to(torch.int32)
tgt_pos = torch.cat([torch.unsqueeze(torch.arange(max(tgt_len)), 0) for _ in src_len]).to(torch.int32)
src_pe_embedding = pe_embedding(src_pos)
tgt_pe_embedding = pe_embedding(tgt_pos)
这里:
src_pos 和 tgt_pos 是位置索引矩阵
- 对于源序列,有效长度最长为4,位置索引为
[0, 1, 2, 3]
- 每个位置索引对应位置编码表中的一行向量
最终得到:
src_embedding:词嵌入向量
src_pe_embedding:位置编码向量
- 将两者相加,得到同时包含词义和位置信息的输入表示
设计思想
代码完整实现了Transformer模型的两个基础组件:
- 词嵌入:将单词转换为连续向量空间表示
- 位置编码:通过正弦余弦函数为每个位置创建唯一编码
这种设计的优势:
- 捕获单词间的语义关系
- 保留序列中的位置信息
- 编码固定,不依赖训练数据,具有通用性
六、技术价值与应用
词嵌入和位置编码是深度学习处理文本的基石,带来了以下突破:
-
语义表示:词嵌入让计算机能够"理解"词义,捕捉词语间的语义关系
-
上下文感知:位置编码使模型能够区分"狗咬人"与"人咬狗"
-
泛化能力:通过学习到的词向量,模型可以对未见过的句子进行推理
这些技术是Transformer、BERT、GPT等先进模型的基础,推动了自然语言处理领域的快速发展。
转自 CSDN 作者 山野蓝莓酸奶昔