目录
Transform的由来
Seq2seq 模型
Transform 的内部结构
语言模型
BERT 介绍
BERT 模型的组成
分词器
位置编码
Softmax
残差结构
BERT 模型总结
Transform的由来
传统的语⾔模型,⽐如RNN(循环神经⽹络),就像⼀个“短视”的学⽣。它在读⼀篇⽂章时,只能逐字逐句地读,并且每次只能记住前⼀个词的信息。当句⼦很⻓时,它就会“忘记” 前⾯读过的内容,导致对整句话的理解不完整。
Transformer 的出现,就像给这个学⽣配上了“全局视野”。它最⼤的创新是引⼊了⾃注意⼒机制(Self-Attention Mechanism)。简单来说,⾃注意⼒机制让模型在处理⼀个词时,能够 同时关注到句⼦中的所有其他词,并根据它们之间的关系来确定每个词的重要性
RNN(或者LSTM、GRU等)的计算限制为是顺序的,也就是RNN相关算法只能从左向右或从右向左依次计算:
- 时间片t的计算依赖于t-1时刻的计算结果,限制了模型的并行能力;
- 尽管LSTM等门机制的结构缓解了长期依赖的问题,但顺序计算的过程中信息会丢失,
Transformer的提出解决了上面两个问题,它使用了 Attention机制,将序列中的任意两个位置之间的距离缩小为一个常量;其次他不是类似于RNN的顺序结构,因此具有更好的并行性,符合现有的GPU框架。
- 注意⼒机制(Attention Mechanism)
⼈类阅读⼀段⽂字时,⼤脑并不是平等地处理每⼀个字。相反,你会根据上下⽂,有意识或⽆意识地去关注那些对理解当前信息更重要的词语。
注意⼒机制的⼯作过程分解成三个简单的步骤:
- 打分(Scoring): 对于句⼦中的每⼀个词,模型都会计算它与当前正在处理的词之间的 “相关性分数”。分数越⾼,代表这两个词越相关。
- 归⼀化(Normalizing): 这些分数会被转换成权重,通常使⽤ Softmax 函数。这样做的 好处是,所有权重加起来等于1,⽽且越重要的词权重越⾼,不重要的词权重越低,这就像 给每个词分配了不同等级的关注度。
- 加权求和(Weighted Sum): 最后,模型会将所有词的原始信息(也叫“值”)与它们对 应的权重相乘,然后相加。这样得到的最终表⽰,就包含了所有词的信息,但重点突出了那 些权重⾼的、更重要的词。
注意⼒机制通过让模型能够“回头看”句⼦中的所有词,并给它们分配不同的权重,彻底解决了这个问题。它让模型在处理⻓句⼦时,不再“短视”,⽽是拥有了“全局视野”,能更好地理解整个句⼦的语境和含义
Seq2seq 模型
Seq2seq(Sequence-to-Sequence)将自然语言处理中的任务(如文本摘要、机器翻译、对话系统等)看作从一个输入序列到另外一个输出序列的映射,然后通过一个端到端的神经网络来直接学习序列的映射关系。
Seq2seq也是编码器-解码器结构的雏形。
- 仅使用encoder的预训练模型: BERT (理解文本)
- 仅使用decoder的预训练模型: GPT (生成文本)
Transform 的内部结构
Transformer是一种基于注意力机制的编码器-解码器结构,具有很好的并行性能,同时利用注意力机制很好地解决了长序列的长程依赖问题
多头注意力机制接受输入词嵌入与位置编码之和,并进行多头注意力的计算。注意力机制中的Q、K、V的来源不同。以机器翻译为例,Q来源于目标输出,而K和V来源于输入信息。与之相对,自注意力机制的Q、K、V均来源于同一个X。
多头注意力机制。多头注意力机制是多个自注意力机制的组合,目的是从多个不同角度提取交互信息,其中每个角度称为一个注意力头,多个注意力头可以独立并行计算。
Transformer编码器的输入是词嵌入与位置编码之和。将输入序列转化成词嵌入的方法是从一张査询表(Lookup Table)中获取每个词元(Token)对应的向量表示。但如果仅使用词嵌入作为Transformer的注意力机制的输入,则在计算词元之间的相关度时并未考虑它们的位置信息。原始的Tansformer采用了正余弦位置编码。
- 通过计算得出各个位置每个维度上的信息,而非通过训练学习到
- 输入长度不受最大长度的限制,可以计算到比训练数据更长的位置,具有一定的外推性;
- 每个分量都是正弦或者余弦函数,并且整体的位置编码具有远程衰减性质,具备位置信息;
残差连接和层归一化
在多头注意力机制和前馈神经网络中都会进行相关逻辑处理
- 残差连接的核心思想是在网络层的输出中直接添加该层的输入,形成"短路"连接
- 层归一化对每个样本的所有特征维度进行归一化处理,使数据的均值为0,方差为1
残差连接其作用在于
- 缓解梯度消失 :通过直接传递梯度,使得深层网络的梯度能够更有效地回传
- 学习恒等映射 :允许网络层更容易学习恒等变换(identity mapping),促进模型收敛
- 增强特征表达 :保留原始输入信息,同时引入新的特征表示
层归一化其作用在于
- 稳定训练 :减少内部协变量偏移(Internal Covariate Shift),使网络训练更稳定
- 加速收敛 :标准化的输入分布有助于优化器更快地找到最优解
- 降低学习率敏感性 :减少对学习率选择的依赖,提高模型鲁棒性
- 支持不同长度序列 :相比批归一化(Batch Normalization),层归一化不依赖于批次大小,更适合处理变长序列
在transformer 中,代码中的实现采用了"预归一化"(Pre-Normalization)设计,即层归一化应用在每个子层的输出之后,而不是输入之前。这种设计有以下优势:
- 使残差连接更有效,因为归一化后的梯度更稳定
- 简化了训练过程,不需要精心调整学习率和初始化
- 允许构建更深的网络架构
self.layer_norm = nn.LayerNorm(d_embedding) # 一行代码实现残差连接+归一化# 将输出与残差连接相加,并进行层归一化
# 残差连接的作用是将原始输入与计算结果相加 ---> 绕过注意力机制相关的线条
# output 的维度: [batch_size, len_q, d_embedding]
output = self.layer_norm(output + residual)
transformer 结构代码示例
import numpy as np # 导入 numpy 库,用于科学计算
import torch # 导入 torch 库,用于构建神经网络
import torch.nn as nn # 导入 torch.nn 库,包含了各种神经网络层# --- 全局参数 ---
d_k = 64 # Q 和 K 向量的维度
d_v = 64 # V 向量的维度
d_embedding = 128 # 词嵌入的维度
n_heads = 8 # 多头注意力机制中头的数量
n_layers = 6 # 编码器和解码器的层数
batch_size = 3 # 训练批次大小
epochs = 10 # 训练轮次# --- 注意力机制模块 ---class ScaledDotProductAttention(nn.Module):"""缩放点积注意力模块根据 Q、K、V 计算注意力分数和上下文向量包含掩码机制,通过 scores.masked_fill_(attn_mask.bool(), -1e9) 屏蔽填充位置或未来信息输入:Q: 查询向量,维度 [batch_size, n_heads, len_q, dim_q]K: 键向量,维度 [batch_size, n_heads, len_k, dim_k]V: 值向量,维度 [batch_size, n_heads, len_v, dim_v]attn_mask: 注意力掩码,维度 [batch_size, n_heads, len_q, len_k]输出:context: 上下文向量,维度 [batch_size, n_heads, len_q, dim_v]attn_weights: 注意力权重,维度 [batch_size, n_heads, len_q, len_k]输出:点积注意力的缩放操作解决了高维空间中点积值过大导致softmax梯度消失的问题,提高了训练稳定性缩放操作通过除以 sqrt(d_k) 来缩放点积,防止点积值过大导致 softmax 函数输出接近于 0 或 1,从而避免梯度消失问题"""def __init__(self):super(ScaledDotProductAttention, self).__init__()def forward(self, Q, K, V, attn_mask):# Q, K, V 的维度: [batch_size, n_heads, len_q/k/v, dim_q=k/v]# attn_mask 的维度: [batch_size, n_heads, len_q, len_k]# 1. 计算注意力分数# 将 Q 与 K 的转置相乘,并除以 sqrt(d_k) 进行缩放# scores 的维度: [batch_size, n_heads, len_q, len_k]scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)# 2. 应用注意力掩码# 将 attn_mask 中为 True 的位置的 scores 替换为一个极小值 (-1e9)# 这样在 softmax 之后,这些位置的权重将接近于0scores.masked_fill_(attn_mask, -1e9)# 3. 对分数进行 softmax 归一化# 沿着最后一个维度 (len_k) 进行 softmax# weights 的维度: [batch_size, n_heads, len_q, len_k]weights = nn.Softmax(dim=-1)(scores)# 4. 计算上下文向量# 将归一化后的权重与 V 相乘# context 的维度: [batch_size, n_heads, len_q, dim_v]context = torch.matmul(weights, V)# 返回上下文向量和注意力权重return context, weightsclass MultiHeadAttention(nn.Module):"""多头注意力机制模块将输入投影到多个子空间,并并行计算注意力,最后将结果拼接"""def __init__(self):super(MultiHeadAttention, self).__init__()# 线性投影层,用于生成 Q, K, Vself.W_Q = nn.Linear(d_embedding, d_k * n_heads)self.W_K = nn.Linear(d_embedding, d_k * n_heads)self.W_V = nn.Linear(d_embedding, d_v * n_heads)# 最后的线性层,将拼接后的多头输出投影回原始维度self.linear = nn.Linear(n_heads * d_v, d_embedding)# 层归一化(Layer Normalization),样本的特征进行归一下,使其均值为0 ,方差为1self.layer_norm = nn.LayerNorm(d_embedding)def forward(self, Q, K, V, attn_mask):# Q, K, V 的维度: [batch_size, len_q/k/v, d_embedding]residual, batch_size = Q, Q.size(0)# 1. 线性投影并重塑# 将输入 Q, K, V 投影到多头子空间,并调整维度以便并行计算# .view() 和 .transpose() 操作将维度变为: [batch_size, n_heads, len_q/k/v, d_q=k/v]q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1, 2)k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1, 2)v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1, 2)# 2. 复制注意力掩码# 将 attn_mask 复制 n_heads 次,以适应多头注意力# attn_mask 的维度: [batch_size, n_heads, len_q, len_k]# unsqueeze(1) :在第1维(索引从0开始)插入一个新的维度,将掩码形状变为 [batch_size, 1, len_q, len_k]# repeat(1, n_heads, 1, 1) :在新增的维度上复制 n_heads 次(其他维度保持不变),最终掩码形状变为 [batch_size, n_heads, len_q, len_k]attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)# 3. 计算缩放点积注意力context, weights = ScaledDotProductAttention()(q_s, k_s, v_s,attn_mask)# context 的维度: [batch_size, n_heads, len_q, dim_v]# weights 的维度: [batch_size, n_heads, len_q, len_k]# 4. 拼接多头结果# 调整维度并使用 .contiguous() 确保内存连续,然后用 .view() 将多头结果拼接# context 的维度: [batch_size, len_q, n_heads * dim_v]context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v)# 5. 最终线性投影和层归一化# 将拼接后的结果通过线性层投影回 d_embedding 维度output = self.linear(context)# 将输出与残差连接相加,并进行层归一化# output 的维度: [batch_size, len_q, d_embedding]output = self.layer_norm(output + residual)# 返回最终输出和注意力权重return output, weights# --- 前馈网络模块 ---class PoswiseFeedForwardNet(nn.Module):"""逐位置前馈网络模块对序列中的每个位置独立地应用一个全连接前馈网络"""def __init__(self, d_ff=2048):super(PoswiseFeedForwardNet, self).__init__()# 两个一维卷积层,相当于两个全连接层self.conv1 = nn.Conv1d(in_channels=d_embedding, out_channels=d_ff, kernel_size=1)self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_embedding, kernel_size=1)# 层归一化self.layer_norm = nn.LayerNorm(d_embedding)def forward(self, inputs):# inputs 的维度: [batch_size, len_q, d_embedding]residual = inputs# 1. 维度转换并应用第一个卷积层# inputs.transpose(1, 2) 将维度变为 [batch_size, d_embedding, len_q]# 卷积操作后,output 的维度: [batch_size, d_ff, len_q]output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))# 2. 应用第二个卷积层并转换维度# 卷积操作后,output 的维度: [batch_size, d_embedding, len_q]# .transpose(1, 2) 将维度恢复为 [batch_size, len_q, d_embedding]output = self.conv2(output).transpose(1, 2)# 3. 残差连接和层归一化output = self.layer_norm(output + residual)return output# --- 位置编码和掩码函数 ---def get_sin_enc_table(n_position, embedding_dim):"""生成正弦位置编码表用于在序列中引入词语的绝对位置信息"""# 初始化正弦编码表sinusoid_table = np.zeros((n_position, embedding_dim))# 计算不同位置和维度的角度for pos_i in range(n_position):for hid_j in range(embedding_dim):angle = pos_i / np.power(10000, 2 * (hid_j // 2) / embedding_dim)sinusoid_table[pos_i, hid_j] = angle# 将偶数维应用 sin 函数,奇数维应用 cos 函数sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])# 转换为 PyTorch 张量return torch.FloatTensor(sinusoid_table)def get_attn_pad_mask(seq_q, seq_k):"""生成填充注意力掩码用于在注意力计算中忽略填充 <pad> 词语"""# seq_q 的维度: [batch_size, len_q]# seq_k 的维度: [batch_size, len_k]batch_size, len_q = seq_q.size()batch_size, len_k = seq_k.size()# 找到 seq_k 中所有值为 0 (<pad>) 的位置# pad_attn_mask 的维度: [batch_size, 1, len_k]pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)# 将掩码扩展到与注意力分数相同的形状# pad_attn_mask 的维度: [batch_size, len_q, len_k]pad_attn_mask = pad_attn_mask.expand(batch_size, len_q, len_k)return pad_attn_maskdef get_attn_subsequent_mask(seq):"""生成后续注意力掩码 (仅用于解码器)用于在注意力计算中忽略当前位置之后的信息,防止信息泄露"""# seq 的维度: [batch_size, seq_len]attn_shape = [seq.size(0), seq.size(1), seq.size(1)]# 创建一个上三角矩阵,k=1 表示主对角线上方的元素为 1# subsequent_mask 的维度: [batch_size, seq_len, seq_len]subsequent_mask = np.triu(np.ones(attn_shape), k=1)# 转换为 PyTorch 字节张量 (布尔类型)subsequent_mask = torch.from_numpy(subsequent_mask).byte()return subsequent_mask# --- 编码器和解码器模块 ---class EncoderLayer(nn.Module):"""编码器的一层包含一个多头自注意力层和一个位置前馈网络"""def __init__(self):super(EncoderLayer, self).__init__()self.enc_self_attn = MultiHeadAttention()self.pos_ffn = PoswiseFeedForwardNet()def forward(self, enc_inputs, enc_self_attn_mask):# enc_inputs 的维度: [batch_size, seq_len, d_embedding]# enc_self_attn_mask 的维度: [batch_size, seq_len, seq_len]# 将相同的 Q, K, V 输入多头自注意力层enc_outputs, attn_weights = self.enc_self_attn(enc_inputs, enc_inputs,enc_inputs, enc_self_attn_mask)# 将自注意力输出输入位置前馈网络enc_outputs = self.pos_ffn(enc_outputs)# 返回最终输出和注意力权重return enc_outputs, attn_weightsclass Encoder(nn.Module):"""Transformer 编码器由词嵌入层、位置嵌入层和多个编码器层组成"""def __init__(self, corpus):super(Encoder, self).__init__()self.src_emb = nn.Embedding(len(corpus.src_vocab), d_embedding)# 从预计算的位置编码表初始化位置嵌入层,并冻结参数self.pos_emb = nn.Embedding.from_pretrained(get_sin_enc_table(corpus.src_len + 1, d_embedding), freeze=True)# 堆叠 n_layers 个编码器层self.layers = nn.ModuleList(EncoderLayer() for _ in range(n_layers))def forward(self, enc_inputs):# enc_inputs 的维度: [batch_size, source_len]# 生成位置索引序列# orch.arange(1, enc_inputs.size(1) + 1) 创建一个从1开始到输入序列长度的整数序列 # unsqueeze(0) 在第0维(索引从0开始)插入一个新的维度,将序列长度转换为 [1, source_len]# to(enc_inputs) 将位置索引移动到与输入相同的设备上(如 GPU)pos_indices = torch.arange(1, enc_inputs.size(1) + 1).unsqueeze(0).to(enc_inputs)# 词嵌入和位置嵌入相加enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(pos_indices)# 生成填充注意力掩码# get_attn_pad_mask 函数用于生成填充注意力掩码,忽略输入序列中的 <pad> 词语# enc_self_attn_mask 的维度: [batch_size, source_len, source_len]# True 和 False 分别表示填充位置和非填充位置,填充位置的注意力权重为 0,非填充位置的注意力权重为 1# True 表示该位置是填充标记,需要被忽略 # False 表示该位置不是填充标记,需要被考虑enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)# 存储所有层的注意力权重enc_self_attn_weights = []# 逐层通过编码器层for layer in self.layers:enc_outputs, enc_self_attn_weight = layer(enc_outputs, enc_self_attn_mask)enc_self_attn_weights.append(enc_self_attn_weight)# 返回最终输出和所有层的注意力权重return enc_outputs, enc_self_attn_weightsclass DecoderLayer(nn.Module):"""解码器的一层包含一个多头自注意力层、一个编码器-解码器注意力层和一个位置前馈网络"""def __init__(self):super(DecoderLayer, self).__init__()self.dec_self_attn = MultiHeadAttention()self.dec_enc_attn = MultiHeadAttention()self.pos_ffn = PoswiseFeedForwardNet()def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):# dec_inputs 的维度: [batch_size, target_len, d_embedding]# enc_outputs 的维度: [batch_size, source_len, d_embedding]# 1. 第一个注意力子层: 多头自注意力# Q, K, V 都来自解码器输入dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs,dec_inputs, dec_self_attn_mask)# 2. 第二个注意力子层: 编码器-解码器注意力# Q 来自解码器输出,K, V 来自编码器输出dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs,enc_outputs, dec_enc_attn_mask)# 3. 位置前馈网络dec_outputs = self.pos_ffn(dec_outputs)# 返回最终输出和两个注意力层的权重return dec_outputs, dec_self_attn, dec_enc_attnclass Decoder(nn.Module):"""Transformer 解码器由词嵌入层、位置嵌入层和多个解码器层组成"""def __init__(self, corpus):super(Decoder, self).__init__()self.tgt_emb = nn.Embedding(len(corpus.tgt_vocab), d_embedding)# 从预计算的位置编码表初始化位置嵌入层,并冻结参数# from_pretrained 方法用于从预计算的位置编码表中初始化位置嵌入层# get_sin_enc_table 函数用于生成正弦位置编码表# freeze=True 表示冻结位置嵌入层的参数,不进行训练self.pos_emb = nn.Embedding.from_pretrained(get_sin_enc_table(corpus.tgt_len + 1, d_embedding), freeze=True)# 堆叠 n_layers 个解码器层self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])def forward(self, dec_inputs, enc_inputs, enc_outputs):# dec_inputs 的维度: [batch_size, target_len]# enc_inputs 的维度: [batch_size, source_len]# enc_outputs 的维度: [batch_size, source_len, d_embedding]# 生成位置索引序列pos_indices = torch.arange(1, dec_inputs.size(1) + 1).unsqueeze(0).to(dec_inputs)# 词嵌入和位置嵌入相加dec_outputs = self.tgt_emb(dec_inputs) + self.pos_emb(pos_indices)# 1. 生成解码器自注意力掩码dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)# 将填充掩码和后续掩码相加,结果大于 0 的位置为 Truedec_self_attn_mask = torch.gt((dec_self_attn_pad_mask+ dec_self_attn_subsequent_mask), 0)# 2. 生成编码器-解码器注意力掩码 (仅考虑填充)dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)dec_self_attns, dec_enc_attns = [], []# 逐层通过解码器层for layer in self.layers:dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs,dec_self_attn_mask, dec_enc_attn_mask)dec_self_attns.append(dec_self_attn)dec_enc_attns.append(dec_enc_attn)# 返回最终输出和所有层的注意力权重return dec_outputs, dec_self_attns, dec_enc_attns# --- Transformer 模型 ---class Transformer(nn.Module):"""Transformer 模型的总框架由编码器、解码器和最后的线性投影层组成"""def __init__(self, corpus):super(Transformer, self).__init__()self.encoder = Encoder(corpus)self.decoder = Decoder(corpus)# 最后的线性层,将解码器输出映射到目标词汇表大小self.projection = nn.Linear(d_embedding, len(corpus.tgt_vocab), bias=False)def forward(self, enc_inputs, dec_inputs):# enc_inputs 的维度: [batch_size, source_seq_len]# dec_inputs 的维度: [batch_size, target_seq_len]# 1. 编码器前向传播enc_outputs, enc_self_attns = self.encoder(enc_inputs)# enc_outputs 的维度: [batch_size, source_len, d_embedding]# 2. 解码器前向传播dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)# dec_outputs 的维度: [batch_size, target_len, d_embedding]# 3. 线性投影dec_logits = self.projection(dec_outputs)# dec_logits 的维度: [batch_size, target_len, tgt_vocab_size]# 返回逻辑值和所有注意力权重return dec_logits, enc_self_attns, dec_self_attns, dec_enc_attns# --- 数据处理 ---from collections import Counterclass TranslationCorpus:"""数据处理类,用于管理语料库、词汇表和批次生成"""def __init__(self, sentences):self.sentences = sentencesself.src_len = max(len(sentence[0].split()) for sentence in sentences) + 1self.tgt_len = max(len(sentence[1].split()) for sentence in sentences) + 2self.src_vocab, self.tgt_vocab = self.create_vocabularies()self.src_idx2word = {v: k for k, v in self.src_vocab.items()}self.tgt_idx2word = {v: k for k, v in self.tgt_vocab.items()}def create_vocabularies(self):src_counter = Counter(word for sentence in self.sentences for word in sentence[0].split())tgt_counter = Counter(word for sentence in self.sentences for word in sentence[1].split())src_vocab = {'<pad>': 0, **{word: i + 1 for i, word in enumerate(src_counter)}}tgt_vocab = {'<pad>': 0, '<sos>': 1, '<eos>': 2,**{word: i + 3 for i, word in enumerate(tgt_counter)}}return src_vocab, tgt_vocabdef make_batch(self, batch_size, test_batch=False):input_batch, output_batch, target_batch = [], [], []# 随机选择句子索引# torch.randperm(len(self.sentences)) 生成一个长度为句子总数的随机排列整数序列# [:batch_size] 选择前 batch_size 个索引sentence_indices = torch.randperm(len(self.sentences))[:batch_size]for index in sentence_indices:src_sentence, tgt_sentence = self.sentences[index]# 将句子转换为索引序列src_seq = [self.src_vocab[word] for word in src_sentence.split()]tgt_seq = ([self.tgt_vocab['<sos>']] +[self.tgt_vocab[word] for word in tgt_sentence.split()] +[self.tgt_vocab['<eos>']])# 对序列进行填充src_seq += [self.src_vocab['<pad>']] * (self.src_len - len(src_seq))tgt_seq += [self.tgt_vocab['<pad>']] * (self.tgt_len - len(tgt_seq))input_batch.append(src_seq)# 在测试模式下,解码器输入只包含 <sos> 符号output_batch.append([self.tgt_vocab['<sos>']] +([self.tgt_vocab['<pad>']] * (self.tgt_len - 2)) if test_batch else tgt_seq[:-1])target_batch.append(tgt_seq[1:])return torch.LongTensor(input_batch), torch.LongTensor(output_batch), torch.LongTensor(target_batch)# --- 训练和推理 ---# 定义语料库
sentences = [['毛老师 喜欢 人工智能', 'TeacherMao likes AI'],['我 爱 学习 人工智能', 'I love studying AI'],['深度学习 改变 世界', ' DL changed the world'],['自然语言处理 很 强大', 'NLP is powerful'],['神经网络 非常 复杂', 'Neural-networks are complex']
]# 创建语料库实例
corpus = TranslationCorpus(sentences)# 实例化模型、损失函数和优化器
model = Transformer(corpus)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.0001)# 模型训练
print("--- 开始训练 ---")
for epoch in range(epochs):optimizer.zero_grad()enc_inputs, dec_inputs, target_batch = corpus.make_batch(batch_size)outputs, _, _, _ = model(enc_inputs, dec_inputs)# 调整输出和目标张量维度以适应损失函数# - outputs.view(-1, len(corpus.tgt_vocab)) 将输出调整为 [batch_size * target_len, tgt_vocab_size]# - target_batch.view(-1) 将目标调整为 [batch_size * target_len]loss = criterion(outputs.view(-1, len(corpus.tgt_vocab)), target_batch.view(-1))if (epoch + 1) % 1 == 0:print(f"Epoch: {epoch + 1:04d} cost = {loss:.6f}")loss.backward()optimizer.step()# 模型推理 (翻译)
print("\n--- 开始翻译 ---")
# 创建一个大小为 1 的测试批次
enc_inputs, dec_inputs, target_batch = corpus.make_batch(batch_size=1, test_batch=True)# 打印输入数据
print("编码器输入 :", enc_inputs)
print("解码器输入 :", dec_inputs)
print("目标数据 :", target_batch)# 进行翻译预测
predict, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)# 后处理预测结果
predict = predict.view(-1, len(corpus.tgt_vocab))
predict = torch.argmax(predict, dim=1, keepdim=True) # Greedy Encoding 贪婪解码,选取每个位置概率最大的词的索引# 将索引转换为单词
translated_sentence = [corpus.tgt_idx2word[idx.item()] for idx in predict.squeeze()]
input_sentence = ' '.join([corpus.src_idx2word[idx.item()] for idx in enc_inputs[0]])# 打印翻译结果
print(f"输入句子: '{input_sentence}'")
print(f"翻译结果: '{' '.join(translated_sentence)}'")
语言模型
现阶段所有的NLP模型都不能理解这个世界,只是依赖已有的数据集进行概率计算。而在目前的“猜概率”游戏环境下,基于大型语言模型(LLM, Large Language Model)演进出了最主流的两个方向:BERT 和GPT
BERT 是之前最流行的方向,统治了所有NLP领域的判别任务,并在自然语言理解类任务中发挥出色。而最初GPT则较为薄弱,在GPT3.0发布前,GPT方向一直是弱于BERT的。
BERT 介绍
掩码预训练语言模型BERT
2018年Devlin等人提出了掩码预训练语言型BERT(Bidirectional Encoder Representation from Transformers)。BERT利用掩码机制构造了基于上下文预测中间词的预训练任务,相较于传统的语言模型建模方法,BERT能进一步挖掘上下文所带来的丰富语义。
Bidirectional Encoder Representations fromTransformers,直译过来就是“来自Transformer的双向编码表示”。
- 双向(Bidirectional):这是 BERT最大的特点。
- 编码器(Encoder):表明它擅长理解文本。
- 来自Transformer (from Transformers):说明它的底层架构是Transformer。
BERT 模型的组成
ERT由多层Transformer编码器组成,这意味着在编码过程中每个位置都能获得所有位置的信息,而不仅仅是历史位置的信息。BERT同样由输入层,编码层和输出层三部分组成。编码层由多层Transformer编码器组成。
在预训练时,模型的最后有两个输出层MLM和NSP,分别对应了两个不同的预训练任务:掩码语言模型(Masked LanguageModeling,MLM)和下一句预测(Next Sentence Prediction,NSP)
掩码语言模型的训练对于输入形式没有要求,可以是一句话也可以一段文本,甚至可以是整个篇章,但是下一句预测则需要输入为两个句子,因此BERT在预训练阶段的输入形式统一为两段文字的拼接,这与其他预训练模型相比有较大区别。
BERT的创新之处在于,它采用了双向的训练方式。它在训练时,会随机遮盖(mask)掉句子中一部分词,然后让模型根据被遮盖词的前后所有词来预测它。除了“完形填空”(MaskedLanguage Model)这个主要任务外,BERT还有另一个辅助任务:下一句预测(NextSentence Prediction)。
BERT首先在海量的文本数据上进行“预训练”,学习通用的语言知识。之后,我们可以用少量的特定任务数据对它进行“微调”,让它胜任具体的任务,如情感分析、问答系统、命名实体识别等。
分词器
tokenizer将原始文本转换为模型能够处理的数字序列。像 BERT和 GPT 这样的 Transformer模型,都只能处理数字,模型根本不认识这些汉字。分词器的作用,就是把这个句子拆解并转 换成一串数字,比如[101,23,45,67,102]。然后,这些数字才能被输入到模型中进行 计算。
1. 分词(Tokenization)分词器会把一句话分解成一个个独立的“词元”(Token)。
- 按词分词:比如,句子“love cats”会被分成['I','love','cats']
- 按字分词:对于中文,句子“我爱中国”会被分成['我','爱’,'中','国']
2. 特殊标记(Special Tokens)为了让模型更好地理解句子的结构,分词器会添加一些特殊的标22.记:
- [CLS] :通常放在句子的开头,它代表了整个句子的信息,常用于文0本分类任务。
- [SEP] :通常放在句子的结尾,用于分隔不同的句子。如果想让模型同时处理两个句子(比如问答任务),会在它们之间放一个[SEP]。
- [PAD] :当批次中的句子长度不一时,分词器会用 [PAD]来填充短句子,让所有句子的长度都相同,方便模型并行处理。
3. 映射到 ID(Token to lD)分词器内部有一个巨大的“词汇表”(Vocabulary),它将每个词元映射到一个唯一的整数 ID。
4. 生成注意力掩码(Attention Mask)在上面的步骤2中,我们用 [PAD]填充了句子。 但模型需要知道哪些是真正的词,哪些是填充物。
位置编码
Transformer 的位置编码不是简单的数字。它是一种与词嵌入(word embedding)相加的编码方式。在将词语输入模型之前,会将它的词嵌入向量和位置编码向量相加。位置编码有多种实现方式,但Transformer论文中介绍的是一种基于正弦(sine)和余弦(cosine)函数的编码方法。
Softmax
Softmax 主要作用是吧一串普通的数字,转换成一串所有数字加起来等于1的概率
残差结构
在深度神经网络中,信息需要一层一层地向前传递。当网络层数很深时,就像信息要走一条非常长的路。在这个过程中,可能会出现梯度消失的问题,导致信息越来越弱,甚至丢失。这会让模型很难训练,效果也会变差。残差结构的核心思想就是为信息传递提供一条捷径(shortcut)
- 解决梯度消失问题:当梯度在反向传播时,可以通过这条捷径直接传递到前面的层,避免了信息在多层计算中逐渐衰减。这让训练更深的网络变得可能。
- 保留原始信息:它确保了每一层的输出都包含了原始输入的信息。这就像在学习新知识时,不仅要吸收新的内容,还要不断回顾旧的知识,这样才能学得更扎实。
BERT 模型总结
预训练阶段: BERT采用了无监督的预训练策略,通过在大规模语料库上进行预训练,学习通用的语言表示。模型在两个任务上进行预训练:
- Masked Language Model(MLM): 随机遮蔽输入文本中的一些词,然后预测这些被遮蔽的词。
- Next Sentence Prediction(NSP): 预测两个相邻句子是否是原文中相邻的句子。
微调阶段: 在具体任务上进行有监督的微调,例如文本分类、命名实体识别等。
- 优点:模型精度高,且泛化性较好
- 缺点:模型复杂度较高
GPT 介绍
生成式预训练语言模型GPT
OpenAI公司在2018年提出的GPT(Generative Pre-Training)模型是典型的生成式预训练语言模型之一。GPT-2由多层Transformer组成的单向语言模型,主要可以分为输入层,编码层和输出层三部分。
GPT 也预留了文本预测、文本分类的相关架构