Transformer-1: 词嵌入与位置编码

自然语言处理 (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 来实现一个简单的词嵌入例子。别担心,代码很简单!

import torch
import torch.nn as nn

# 假设我们有一个包含8个单词的小词典
vocab_size = 8  # 词典大小
embedding_dim = 4  # 每个词向量的维度(可以理解为每个词用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],即4个词,每个词4维向量
print("嵌入后的向量:\n", embedded_sentence)

这个例子中:

  1. nn.Embedding 创建了一个可训练的 "查找表"
  2. 表的大小是 vocab_size × embedding_dim
  3. 输入一个词的索引,就能得到对应的向量

刚开始,这些向量的值是随机的,但在训练过程中,模型会学习到有意义的表示。比如,"苹果" 和 "香蕉" 的向量会变得相似。

三、序列处理的挑战:位置信息

词嵌入解决了词义表示的问题,但还有一个关键问题:词序

在自然语言中,词的顺序非常重要。比如:

  • "狗咬人" 和 "人咬狗" 意思完全不同
  • "我今天吃了苹果" 和 "我吃了苹果今天" 表达的是同一件事,但语序不同

标准的词嵌入模型会忽略词序,就像把所有词扔进一个袋子里。为了解决这个问题,我们需要引入位置编码

四、位置编码:给词加上 "位置标签"

位置编码的目标是让模型知道每个词在句子中的位置。最直接的方法可能是给每个位置一个编号:

  • 第 1 个词 → 1
  • 第 2 个词 → 2
  • ...

但这种简单编号有局限性,模型可能难以学习到相对位置关系(比如 "第 3 个词" 和 "第 4 个词" 相邻)。

Transformer 模型提出了一种更聪明的方法:正弦余弦位置编码。它的核心思想是:

  1. 为每个位置生成一个向量
  2. 向量的每个维度使用不同频率的正弦和余弦函数
  3. 这样,相邻位置的编码向量会相似,相距较远的位置会不同

让我们看看代码实现:

import torch
import math

max_position = 10  # 最大位置数(假设句子最长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. 不同位置的编码向量不同
  2. 相邻位置的编码相似(可以通过向量点积验证)
  3. 可以表示相对位置关系(比如,位置 k 的编码可以表示为位置 k+n 的编码的线性组合)

五、结合词嵌入和位置编码

在实际应用中,我们通常会把词嵌入和位置编码结合起来使用。让我们看几个完整的例子:

1、例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)))

# 将位置编码转为可训练的参数(不参与训练)
position_embedding = nn.Embedding.from_pretrained(position_encoding, freeze=True)

# 假设我们有一个句子
sentence_indices = torch.tensor([2, 4, 6, 0, 0])  # 0表示padding(无实际单词)
sentence_length = 3  # 实际句子长度(前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]

在这个例子中:

  1. 我们创建了词嵌入层和位置编码层
  2. 对一个句子(用索引表示)进行处理
  3. 获取词嵌入和位置嵌入
  4. 将它们相加得到最终的输入表示

这种组合方式让模型既能理解词义,又能感知词序,是现代 NLP 模型的基础。

2、例2:

import torch
import numpy
import torch.nn as nn
import torch.nn.functional as F


# 关于word embedding, 以离散的序列建模为例子

# 考虑source sentence 和 target sentence

# 构建序列, 序列的字符以其在词表中的索引的形式表示

# 批次大小
batch_size = 2

# 单词表大小
max_num_src_words = 8
max_num_tgt_words = 8
max_num_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])   #  生成以单词索引序列构成的源句子,padding补齐每一个序列长度
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)  #  生成以单词索引序列构成的目标句子,padding补齐每一个序列长
#  src_seq: [tensor([6, 5, 0, 0, 0]), tensor([1, 4, 3, 4, 0])]---> [tensor([[6, 5, 0, 0, 0]]), tensor[([1, 4, 3, 4, 0]])]--->
# tensor([[5, 5, 0, 0, 0],
#         [3, 7, 6, 6, 0]])  每一个句子最多5个单词, 用单词索引表示单词, 0来补齐
# tgt_seq: [tensor([3, 1, 4, 5, 0]), tensor([4, 7, 3, 0, 0])] ---->
# tensor([[1, 2, 1, 2, 0],
#         [3, 2, 2, 0, 0]])
# 所以,通过torch.unsqueeze将序列增加一维, 再通过torch.cat拼接 === torch.stack

# 构造word embedding
src_embedding_table = nn.Embedding(max_num_src_words+1, model_dim)  # 第0行是padding的0 Embedding(9, 16)
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)


# 构造position embedding
pos_mat = torch.arange(max_position_len).reshape((-1, 1))  # pos按行 写成一列 , 反映行变化
i_mat =torch.pow(10000, torch.arange(0, model_dim, 2).reshape((1, -1)) / model_dim)   # i 表示第几个特征,写成一行, 反映列变化
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)  # 获取位置编码, 这里的词汇表大小实际上是每个句子中有几个单词, 即表征的是单词的位置,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(pos_mat)
# print(i_mat)
# print(pe_embedding_table)
# print((pe_embedding.weight))
print(src_pos)
print(tgt_pos)
print(src_pe_embedding)
print(tgt_pe_embedding)

(1)、数据准备:理解序列与批次

首先创建了模拟数据:

# 批次大小
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)  # 源序列长度:[2, 4]
tgt_len = torch.Tensor([4, 3]).to(torch.int)  # 目标序列长度:[4, 3]

这段代码定义了:

  • 批次大小:一次处理 2 个序列
  • 词汇表大小:源语言和目标语言各有 8 个单词
  • 模型维度:每个词向量用 16 维表示
  • 序列长度:源序列分别为 2 个词和 4 个词,目标序列为 4 个词和 3 个词

(2)、生成序列数据(单词索引)

下一步是生成实际的序列数据: 

# 生成源序列和目标序列
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)

这部分代码做了三件事:

  1. 随机生成单词索引:使用torch.randint生成 1 到 8 之间的随机整数,表示单词在词汇表中的位置
  2. 填充序列:使用F.pad将短序列用 0 填充到固定长度(5)
  3. 组合批次:使用torch.cattorch.stack将多个序列组合成批次

例如,生成的源序列可能是:

tensor([[6, 5, 0, 0, 0],  # 第一个序列实际长度2,后面补3个0
        [1, 4, 3, 4, 0]]) # 第二个序列实际长度4,后面补1个0

(3)、词嵌入:将索引转换为向量

现在我们有了单词索引,下一步是将它们转换为向量:

# 构造词嵌入层
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)

这里的关键是nn.Embedding

  • 参数(词汇表大小, 向量维度)
  • 为什么 + 1:因为我们用 0 作为填充符号,所以词汇表实际需要 9 个位置(0-8)
  • 工作原理:本质是一个可训练的查找表,将单词索引映射到对应的向量

例如,当我们输入索引6时,src_embedding_table会返回表中第 6 行的 16 维向量。

(4)、位置编码:为序列添加位置信息

词嵌入忽略了词序信息,位置编码就是来解决这个问题的:

# 构造位置编码
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 模型中使用的正弦余弦位置编码:

  1. 位置矩阵[0, 1, 2, 3] 表示词的位置,有效的最大长度是4
  2. 频率矩阵:控制正弦余弦函数的周期,不同维度使用不同频率
  3. 编码表:偶数维度用正弦函数,奇数维度用余弦函数
  4. 固定参数requires_grad=False 表示训练时不更新位置编码

(5)、获取位置编码向量

最后,我们为每个词获取对应的位置编码:

# 获取位置索引
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 是位置索引矩阵
  • 对于源序列,尽管设置的max_len= 5, 但有效的长度最长为 4,则位置索引为 [0, 1, 2, 3]
  • 每个位置索引对应位置编码表中的一行向量

最终,我们得到了:

  • src_embedding:词嵌入向量
  • src_pe_embedding:位置编码向量
  • 将它们相加,就得到了同时包含词义和位置信息的输入表示

(6)、为什么这样设计?

代码完整实现了 Transformer 模型的两个基础组件:

  1. 词嵌入:将单词转换为连续向量空间的表示
  2. 位置编码:通过正弦余弦函数为每个位置创建唯一编码

这种设计的好处是:

  • 获了单词之间的语义关系
  • 保留了序列中的位置信息
  • 编码是固定的,不依赖于训练数据,具有通用性

六、为什么这很重要?

词嵌入和位置编码是深度学习处理文本的基石,它们带来了以下几个重要突破:

  1. 语义表示:词嵌入让计算机能够 "理解" 词义,捕捉词语之间的语义关系。

  2. 上下文感知:位置编码让模型能够区分 "狗咬人" 和 "人咬狗" 这样的句子。

  3. 泛化能力:通过学习到的词向量,模型可以对未见过的句子进行推理。

这些技术是 Transformer、BERT、GPT 等强大模型的基础,推动了自然语言处理领域的进步。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值