第 1 章 绪论
大规模语言模型(LLM)是一种具有数百亿以上参数的深度神经网络,通常通过自监督学习在大量无标注文本上进行训练。自2018年以来,多个公司和研究机构,如Google、OpenAI、Meta、百度、华为等,发布了多种模型(如BERT、GPT等),并在各种自然语言处理任务中表现出色。2019年以后,大型语言模型快速发展,尤其是2022年11月发布的ChatGPT,引起了全球广泛关注。大规模语言模型不仅可以进行问答、分类、摘要、翻译、聊天等任务,还展示了强大的世界知识和语言理解能力。
有需要这本
《大规模语言模型:从理论到实践》
书籍PDF文档,可以微信扫描下方优快云官方认证二维码
,免费领取【保证100%免费】
第 2 章 大语言模型基础
Transformer 模型
Transformer 模型是由谷歌在 2017 年提出并首先应用于机器翻译的神经网络模型结构,Transformer 结构完全通过注意力机制完成对源语言序列和目标语言序列全局依赖的建模。当前几乎全部大语言模型都是基于 Transformer 结构。
接下来详细讲解模型结构、实现代码和数据维度的变化,我觉得通过操作数据,知道数据输入输出的维度变化,就能比较清楚的理解原理了。
嵌入表示层
这已经是个标准流程,把词或token的独热编码转换成稠密的特征向量,表示更丰富的信息。
class Embedder(nn.Module):
def \_\_init\_\_(self, vocab\_size, d\_model):
super().\_\_init\_\_()
self.embed = nn.Embedding(vocab\_size, d\_model)
def forward(self, x):
return self.embed(x)
代码简单,每个词都会转换成一个d_model维度的向量,这个向量会在训练做梯度下降时进行调整。
首先我们统一后续用到的输入标准格式:
vocab_size #词表大小
batch_size # 批量大小
seq_len # 序列长度
d_model # 特征维度
h # 头的数量
d_k = d_model // h # 每个头的维度
那么 self.embed(x) 的维度变化:batch_size * seq_len -> batch_size * seq_len * d_model。除了训练批量和序列长度这个标准输入以外就是从1到d_model维度,这个1维是整数,从[0,vocab_size-1],每个整数代表1个词,解码最后一层需要映射回来。
位置编码
不同与RNN的循环方式,需要告诉词的位置信息。Transformer模型的位置编码使用是正余弦函数,如下pos 表示单词所在的位置,2i 和 2i+ 1 表示位置编码向量中的对应维度,d 则对应位置编码的总维度。:
class PositionalEncoder(nn.Module):
def \_\_init\_\_(self, d\_model, max\_seq\_len=80):
super().\_\_init\_\_()
self.d\_model = d\_model
\# 根据 pos 和 i 创建一个常量 PE 矩阵
pe = torch.zeros(max\_seq\_len, d\_model)
for pos in range(max\_seq\_len):
for i in range(0, d\_model, 2):
pe\[pos, i\] = math.sin(pos / (10000 \*\* ((2 \* i) / d\_model)))
pe\[pos, i + 1\] = math.cos(pos / (10000 \*\* ((2 \* (i + 1)) / d\_model)))
pe = pe.unsqueeze(0)
self.register\_buffer('pe', pe)
def forward(self, x):
\# 使得单词嵌入表示相对大一些 我们在加法之前增加 embedding 值的原因是为了使位置编码相对较小。这意味着当我们将它们相加时,嵌入向量中的原始含义不会丢失。
x = x \* math.sqrt(self.d\_model)
\# 增加位置常量到单词嵌入表示中
seq\_len = x.size(1)
x = x + Variable(self.pe\[:, :seq\_len\], requires\_grad\=False).cuda()
return x
__init__函数里初始化pe是max_seq_len * d_model,pe.unsqueeze(0)后是 1 * max_seq_len * d_model。forward中 x 维度batch_size * seq_len * d_model,pe的第2个维度取seq_len长度,所以x + pe 维度还是batch_size * seq_len * d_model。位置编码本身不改变数据维度。
计算注意力
这个图其实没标出的还有2个步骤:mask操作和dropout。
def attention(q, k, v, d\_k, mask=None, dropout=None):
scores = torch.matmul(q, k.transpose(-2, \-1)) / math.sqrt(d\_k)
\# 掩盖掉那些为了填补长度增加的单元,使其通过 softmax 计算后为 0 if mask is not None:
mask = mask.unsqueeze(1)
scores = scores.masked\_fill(mask == 0, \-1e9)
scores = F.softmax(scores, dim\=-1)
if dropout is not None:
scores = dropout(scores)
output = torch.matmul(scores, v)
return output
q、k、v维度:batch_size * seq_len * d_model,K.transpose(-2,-1)是把2,3维度互换,Q*K的转置得出的scores的维度:batch_size * seq_len * seq_len
其他操作维度不变,output = torch.matmul(scores, V),维度还是变回:batch_size * seq_len * d_model
自注意力层
class MultiHeadAttention(nn.Module):
def \_\_init\_\_(self, heads, d\_model, dropout=0.1):
super().\_\_init\_\_()
self.d\_model = d\_model
self.d\_k = d\_model // heads
self.h = heads
self.q\_linear = nn.Linear(d\_model, d\_model)
self.v\_linear = nn.Linear(d\_model, d\_model)
self.k\_linear = nn.Linear(d\_model, d\_model)
self.dropout = nn.Dropout(dropout)
self.out = nn.Linear(d\_model, d\_model)
def forward(self, q, k, v, mask=None):
bs = q.size(0)
\# 进行线性操作划分为成 h 个头
k = self.k\_linear(k).view(bs, \-1, self.h, self.d\_k)
q = self.q\_linear(q).view(bs, \-1, self.h, self.d\_k)
v = self.v\_linear(v).view(bs, \-1, self.h, self.d\_k)
\# 矩阵转置 bs \* h \* sl \* d\_model k = k.transpose(1, 2)
q = q.transpose(1, 2)
v = v.transpose(1, 2)
\# 计算 attention scores = attention(q, k, v, self.d\_k, mask, self.dropout)
\# 连接多个头并输入到最后的线性层
concat = scores.transpose(1, 2).contiguous().view(bs, \-1, self.d\_model)
output = self.out(concat)
return output
其中:(bat_size简写bs,seq_len简写sl)
拼接并线性变换【concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)】:
scores.transpose(1, 2) 将维度 [bs, h, sl, d_k] 转换为 [bs, sl, h, d_k]。
contiguous() 确保内存布局是连续的。
view(bs, -1, self.d_model) 将结果展平,将 sl 和 h * d_k 合并为一个维度。最终形状为 [bs, sl, d_model]
维度变化:
初始输入:
q, k, v 的维度:[bs, sl, d_model]
线性变换后:
k, q, v 的维度:[bs, sl, h, d_k]
转置后:
k, q, v 的维度:[bs, h, sl, d_k]
scores 的维度(attention 计算结果):[bs, h, sl, d_k]
拼接后:
concat 的维度:[bs, sl, d_model]
最终输出:
output 的维度:[bs, sl, d_model]
每一步维度的变化都与多头注意力机制中分配和重组信息的方式密切相关,确保每个头的计算独立并最终合并。
前馈层
**
class FeedForward(nn.Module):
def \_\_init\_\_(self, d\_model, d\_ff=2048, dropout = 0.1):
super().\_\_init\_\_()
\# d\_ff 默认设置为 2048 self.linear\_1 = nn.Linear(d\_model, d\_ff)
self.dropout = nn.Dropout(dropout)
self.linear\_2 = nn.Linear(d\_ff, d\_model)
def forward(self, x):
x = self.dropout(F.relu(self.linear\_1(x)))
x = self.linear\_2(x)
return x
维度变化:
初始化部分:
linear_1: 输入 [bs, sl, d_model] -> 输出 [bs, sl, d_ff]
dropout: 输入 [bs, sl, d_ff] -> 输出 [bs, sl, d_ff](形状不变)
linear_2: 输入 [bs, sl, d_ff] -> 输出 [bs, sl, d_model]
前向传播部分:
linear_1(x): 输入 [bs, sl, d_model] -> 输出 [bs, sl, d_ff]
F.relu(…): 输入 [bs, sl, d_ff] -> 输出 [bs, sl, d_ff](形状不变)
dropout(…): 输入 [bs, sl, d_ff] -> 输出 [bs, sl, d_ff](形状不变)
linear_2(x): 输入 [bs, sl, d_ff] -> 输出 [bs, sl, d_model]
最终,输出 x 的维度是 [bs, sl, d_model]
层归一化
**
class Norm(nn.Module):
def \_\_init\_\_(self, d\_model, eps=1e-6):
super().\_\_init\_\_()
self.size = d\_model
\# 层归一化包含两个可以学习的参数
self.alpha = nn.Parameter(torch.ones(self.size))
self.bias = nn.Parameter(torch.zeros(self.size))
self.eps = eps
def forward(self, x):
norm = self.alpha \* (x - x.mean(dim\=-1, keepdim\=True)) \\
/ (x.std(dim\=-1, keepdim\=True) + self.eps) + self.bias
return norm
维度变化:
输入维度:[bs, sl, d_model]
计算均值 (x.mean(dim=-1, keepdim=True)): 结果是 [bs, sl, 1]
中心化 (x - x.mean(dim=-1, keepdim=True)): 结果是 [bs, sl, d_model]
计算标准差 (x.std(dim=-1, keepdim=True)): 结果是 [bs, sl, 1]
标准化 ((x - x.mean(dim=-1, keepdim=True)) / (x.std(dim=-1, keepdim=True) + self.eps)): 结果是 [bs, sl, d_model]
缩放 (self.alpha * (x - x.mean(dim=-1, keepdim=True)) / (x.std(dim=-1, keepdim=True) + self.eps)): 结果是 [bs, sl, d_model]
加偏移 (+ self.bias): 结果是 [bs, sl, d_model]
最终返回的 norm 维度是 [bs, sl, d_model]
编码器和解码器
class EncoderLayer(nn.Module):
def \_\_init\_\_(self, d\_model, heads, dropout=0.1):
super().\_\_init\_\_()
self.norm\_1 = Norm(d\_model)
self.norm\_2 = Norm(d\_model)
self.attn = MultiHeadAttention(heads, d\_model)
self.ff = FeedForward(d\_model)
self.dropout\_1 = nn.Dropout(dropout)
self.dropout\_2 = nn.Dropout(dropout)
def forward(self, x, mask):
x2 = self.norm\_1(x)
x = x + self.dropout\_1(self.attn(x2, x2, x2, mask))
x2 = self.norm\_2(x)
x = x + self.dropout\_2(self.ff(x2))
return x
class DecoderLayer(nn.Module):
def \_\_init\_\_(self, d\_model, heads, dropout=0.1):
super().\_\_init\_\_()
self.norm\_1 = Norm(d\_model)
self.norm\_2 = Norm(d\_model)
self.norm\_3 = Norm(d\_model)
self.dropout\_1 = nn.Dropout(dropout)
self.dropout\_2 = nn.Dropout(dropout)
self.dropout\_3 = nn.Dropout(dropout)
self.attn\_1 = MultiHeadAttention(heads, d\_model)
self.attn\_2 = MultiHeadAttention(heads, d\_model)
self.ff = FeedForward(d\_model).cuda()
def forward(self, x, e\_outputs, src\_mask, trg\_mask):
x2 = self.norm\_1(x)
x = x + self.dropout\_1(self.attn\_1(x2, x2, x2, trg\_mask))
x2 = self.norm\_2(x)
x = x + self.dropout\_2(self.attn\_2(x2, e\_outputs, e\_outputs,
src\_mask))
x2 = self.norm\_3(x)
x = x + self.dropout\_3(self.ff(x2))
return x
1、EncoderLayer
输入维度:[batch_size, seq_len, d_model]
经过标准化、注意力和前馈网络后,输出维度:[batch_size, seq_len, d_model]
维度变化:
Norm 层:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
MultiHeadAttention 层:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Dropout 层:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
FeedForward 层:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
最终输出:[batch_size, seq_len, d_model]
2、DecoderLayer
输入维度:[batch_size, seq_len, d_model]
经过标准化、注意力机制(自注意力和源-目标注意力)以及前馈网络后,输出维度:[batch_size, seq_len, d_model]
维度变化:
Norm 层 1:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
MultiHeadAttention 层 1(自注意力):[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Dropout 层 1:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Norm 层 2:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
MultiHeadAttention 层 2(源-目标注意力):[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Dropout 层 2:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Norm 层 3:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
FeedForward 层:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
Dropout 层 3:[batch_size, seq_len, d_model] → [batch_size, seq_len, d_model]
最终输出:[batch_size, seq_len, d_model]
小结:
在这两个层的前向传播过程中,输入张量的维度始终保持为 [batch_size, seq_len, d_model]。无论是注意力层(自注意力或源-目标注意力),还是前馈网络,或者是标准化、Dropout 层,它们都不会改变输入张量的维度。它们的作用更多的是对数据进行变换或调节(如加权求和、标准化、丢弃部分特征等),但维度在整个过程中始终是固定的。
每个层的维度保持不变,这使得模型的每一层都能够在不同的层次上对输入进行处理和学习,同时保持输入的结构不变,便于在堆叠多个层次时进行有效的学习和信息传递。
def get\_clones(module, N):
return nn.ModuleList(\[copy.deepcopy(module) for i in range(N)\])
class Encoder(nn.Module):
def \_\_init\_\_(self, vocab\_size, d\_model, N, heads):
super().\_\_init\_\_()
self.N = N
self.embed = Embedder(vocab\_size, d\_model)
self.pe = PositionalEncoder(d\_model)
self.layers = get\_clones(EncoderLayer(d\_model, heads), N)
self.norm = Norm(d\_model)
def forward(self, src, mask):
x = self.embed(src)
x = self.pe(x)
for i in range(self.N):
x = self.layers\[i\](x, mask)
return self.norm(x)
class Decoder(nn.Module):
def \_\_init\_\_(self, vocab\_size, d\_model, N, heads):
super().\_\_init\_\_()
self.N = N
self.embed = Embedder(vocab\_size, d\_model)
self.pe = PositionalEncoder(d\_model)
self.layers = get\_clones(DecoderLayer(d\_model, heads), N)
self.norm = Norm(d\_model)
def forward(self, trg, e\_outputs, src\_mask, trg\_mask):
x = self.embed(trg)
x = self.pe(x)
for i in range(self.N):
x = self.layers\[i\](x, e\_outputs, src\_mask, trg\_mask)
return self.norm(x)
class Transformer(nn.Module):
def \_\_init\_\_(self, src\_vocab, trg\_vocab, d\_model, N, heads):
super().\_\_init\_\_()
self.encoder = Encoder(src\_vocab, d\_model, N, heads)
self.decoder = Decoder(trg\_vocab, d\_model, N, heads)
self.out = nn.Linear(d\_model, trg\_vocab)
def forward(self, src, trg, src\_mask, trg\_mask):
e\_outputs = self.encoder(src, src\_mask)
d\_output = self.decoder(trg, e\_outputs, src\_mask, trg\_mask)
output = self.out(d\_output)
return output
1、Encoder类维度变化:
Line 1 (self.embed(src)):
输入:src,形状为 [batch_size, seq_len]。
输出:嵌入后的张量,形状为 [batch_size, seq_len, d_model]。
Line 2 (self.pe(x)):
输入:x,形状为 [batch_size, seq_len, d_model]。
输出:加上位置编码后的张量,形状仍为 [batch_size, seq_len, d_model]。
Line 3-4 (self.layers[i](x, mask)):
每次调用 EncoderLayer 时,输入维度为 [batch_size, seq_len, d_model],经过该层的多头注意力和前馈网络等操作后,输出维度仍为 [batch_size, seq_len, d_model]。
Line 5 (self.norm(x)):
输入:x,形状为 [bach_size, seq_len, d_model]。
输出:经过归一化后的张量,形状仍为 [batch_size, seq_len, d_model]。
2、Decoder类维度变化:
Line 1 (self.embed(trg)):
输入:trg,形状为 [batch_size, seq_len]。
输出:目标嵌入后的张量,形状为 [batch_size, seq_len, d_model]。
Line 2 (self.pe(x)):
输入:x,形状为 [batch_size, seq_len, d_model]。
输出:添加位置编码后的张量,形状为 [batch_size, seq_len, d_model]。
Line 3-4 (self.layers[i](x, e_outputs, src_mask, trg_mask)):
每个 DecoderLayer 的输入维度为 [batch_size, seq_len, d_model](来自 x)。并且额外传入 e_outputs(编码器输出)和 src_mask、trg_mask,但这些掩码并不改变维度。
输出维度仍为 [batch_size, seq_len, d_model]。
Line 5 (self.norm(x)):
输入:x,形状为 [batch_size, seq_len, d_model]。
输出:经过归一化后的张量,形状仍为 [batch_size, seq_len, d_model]。
3、Transformer 类
维度变化
输入:
src:形状为 [batch_size, src_seq_len]。
trg:形状为 [batch_size, trg_seq_len]。
src_mask:形状为 [batch_size, src_seq_len]。
trg_mask:形状为 [batch_size, trg_seq_len]。
编码器输出:e_outputs 形状为 [batch_size, src_seq_len, d_model]。
解码器输出:d_output 形状为 [batch_size, trg_seq_len, d_model]。
最终输出:output 形状为 [batch_size, trg_seq_len, trg_vocab]。
这样,Transformer 模型的每一层都会处理不同的输入,最终输出目标序列在词汇表中的概率分布。
有需要这本
《大规模语言模型:从理论到实践》
书籍PDF文档,可以微信扫描下方优快云官方认证二维码
,免费领取【保证100%免费】