Transformer 架构
及其衍生模型 BERT、GPT-2 的对应关系

Transformer 的编码器 - 解码器(Encoder-Decoder)结构,这是现代自然语言处理(NLP)的核心基础架构,由 "注意力机制 + 前馈网络 + 残差连接 / 层归一化" 构成重复模块(标记为 "N×")。
编码器与 BERT 的对应:
- 图左侧的编码器模块对应BERT(340M 参数),BERT 基于 Transformer 的编码器构建,采用双向的 Masked Multi-Head Attention,因此擅长文本理解类任务(如语义分类、实体识别)。
解码器与 GPT-2 的对应:
- 图右侧的解码器模块对应GPT-2(1542M 参数),GPT-2 基于 Transformer 的解码器构建,采用自回归的 Masked Multi-Head Attention(仅关注前文 token),因此擅长文本生成类任务(如续写、创作)。
核心模块:
- 编码器 / 解码器的重复单元均包含 "Multi-Head Attention(多头注意力,捕捉序列关联)""Feed Forward(前馈网络,特征变换)""Add & Norm(残差连接 + 层归一化,稳定训练)",是 Transformer 实现语义建模的关键组件。
这些模型是 NLP 领域的里程碑:BERT 开启了双向预训练的范式,GPT-2 则推动了大模型文本生成能力的普及。
Bert
"文本→模型输入" 的预处理流程

完成 "原始文本→分词→数值化输入" 的转换,是 BERT 等预训练语言模型的标准前置操作。
- 步骤分解:
- 分词:将连续文本 "商务大床房" 拆分为基础单元(此处为单个汉字 "商、务、大、床、房")。
- 数值化编码:分词结果被转换为模型可识别的格式:
- Input ids:每个分词单元映射为数字编码(示例中101 是起始标记 [CLS] 、102 是结束标记 [SEP],其余为汉字对应的编码);
- Mask:标记有效文本位置(1 表示有效,0 表示无效填充);
- Seq_ids:区分不同输入序列(单文本任务中通常统一为 0)。
该流程是 NLP 任务(如文本分类、语义检索)的基础,通过将自然语言转换为数值格式,使模型能够进行语义建模。

-
Cls\]:序列开头的专用标记,用于后续任务(如文本分类)的全局语义聚合;
-
pad\]:序列长度不足时的填充标记,保证所有输入序列维度统一。
-
Input ids 是标记的数字映射
-
Mask 区分有效内容与填充内容
-
Seq_ids 用于单 / 双文本任务的序列区分
该流程通过标准化输入格式,使 BERT 能统一处理不同长度、内容的文本,为后续语义理解、分类等任务提供合规的输入数据。
Embedding结构
BERT 基于 Transformer 架构实现语义建模的核心输入层设计

BERT 的输入嵌入是三种嵌入的叠加 ,最终形成 Transformer 可处理的输入表示,这一设计让模型同时捕捉文本的语义、分段、位置信息。
- 各嵌入的功能
- Token Embeddings(词嵌入) :将输入的 token(包括普通词汇、特殊标记、子词如
##ing)映射为语义向量,是文本语义信息的基础载体; - Segment Embeddings(段嵌入) :用于区分双文本任务中的不同文本段(如图中
E_A对应第一段、E_B对应第二段),常见于问答、文本匹配等任务; - Position Embeddings(位置嵌入):由于 Transformer 本身无时序感知能力,通过位置嵌入为每个 token 添加位置信息,使模型捕捉文本的顺序关系。
- Token Embeddings(词嵌入) :将输入的 token(包括普通词汇、特殊标记、子词如
- 特殊 Token 的作用
- 输入序列中的
[CLS]是全局语义聚合标记(用于文本分类等任务),[SEP]是文本段的分隔 / 结束标记,##开头的子词是 BPE 分词的结果(解决未登录词问题)。
- 输入序列中的
这一嵌入结构是 BERT 实现双向语义理解的基础,确保模型能全面捕捉文本的多维度信息。
Bert结构

BERT 的结构从下到上分为四个核心部分:
- 嵌入层(embedding):将预处理后的文本转换为初始向量表示,是模型的输入层;
- BERT 层(bert layers):重复 N 次的 Transformer 编码器模块(图中 "N×" 表示重复),是 BERT 语义建模的核心;
- 池化输出(pooler output):对 BERT 层的输出进行聚合(通常取 [CLS] 标记的向量),为下游任务提供全局语义表示;
- 分类器(classifier):基于池化输出的下游任务分类层,用于文本分类等任务。
参数计算
BERT-base 模型的总参数数量:
维度 dim=768、词汇表大小 vocab=21128 、最大序列长度max_seq_len= 512 、忽略bias
嵌入层总参数:
- Token Embedding 层的参数是
vocab × dim- Position Embedding 层的参数是 max_seq_len×dim
- Segment Embedding 层的参数是 2×dim(支持 2 个文本段)
单个 BERT 层
- 注意力参数:Multi-Head Attention 层的参数是 4×dim×dim(
- Q/K/V 投影
- 输出投影层:把拼接后的多头结果映射为最终输出(维度
dim×dim,参数dim²);- )
- 前馈网络参数:Feed Forward 层的参数是 2×dim×dff(输入 / 输出线性层)
所有 BERT 层
- 12 层 BERT 层总参数
池化层(Pooler)
- dim2(对 [CLS] 标记做向量投影)
dff(Feed Forward Dimension)是 BERT/Transformer 前馈网络中间隐藏层的维度,是架构设计中的核心超参数,它的取值直接决定了前馈网络的特征表达能力和参数规模。
《Attention Is All You Need》论文中明确将前馈网络的中间层维度dff设定为模型隐藏层维度dim的4 倍 ,即 dff = 4 × dim;
前馈网络的作用是对注意力层输出的特征做非线性变换和维度扩张 - 压缩 ------ 先把dim维的输入 "升维" 到dff维(扩大特征空间),通过激活函数增强非线性表达,再 "降维" 回dim维(保证和输入维度一致,满足残差连接)。4 倍的比例是作者通过大量实验验证的最优选择
BERT-base 配置中 dim=768,因此:dff = 768 × 4 = 3072,这也是 BERT-base/ Transformer-base 的标准dff值。
Bert的迁移学习流程

1. 预训练阶段(图上半部分)
- 逻辑:由开发者 / 机构(图中 "大佬")在大规模无标注文本上完成预训练任务(如 Masked Language Model、Next Sentence Prediction),让 BERT 学习通用的语言语义规律;
- 作用:预训练后的 BERT 是一个 "现成的特征提取器",具备理解文本语义的基础能力,可被不同下游任务直接复用,避免每个任务从头训练模型。
2. 微调阶段(图红色框内流程)
- 输入:下游具体任务的文本(如图中示例 "我今天很高兴");
- 流程:将文本输入预训练好的 BERT,由 BERT 提取其语义特征(图中虚线框),再在特征后添加任务专用的分类层;
- 输出:完成特定任务的预测(如图中 "好 / 坏" 对应的情感分类任务);
- 作用:通过少量下游任务的标注数据,调整模型参数(或仅调整分类层),让通用语义特征适配具体任务的需求。
这种 "预训练 - 微调" 模式是 BERT 普及的核心原因:既降低了下游任务的训练成本(数据、算力),也大幅提升了任务效果。
bert分类实战------酒店数据集
获取Bert模型
网址:
https://huggingface.co/bert-base-chinese/tree/main

点那个下载箭头就可以下 下载圈中的 放在bert-base-chinese文件夹。

放到我们的文件夹(bert-base-chinese)中

数据集(jiudian.txt):

读取文件
def read_txt(path):
label = []
data = []
with open(path, "r", encoding="utf-8") as f:
for i, line in tqdm(enumerate(f)):
if i == 0: # 第一行数据是label,review
continue
if i > 200 and i < 7500: # 取前面200行好评,和后200行差评
continue
line = line.strip('\n') # 去掉换行符
line = line.split(",", 1) # 只分割第一个逗号
label.append(line[0]) # 标签部分
data.append(line[1]) # 评论内容部分
return data, label
数据集接口
class JdDataset(Dataset):
def __init__(self, data, lable):
self.X = data # 特征数据(文本/评论)
y = [int(i) for i in lable] # 将标签从字符串转为整数
self.Y = torch.LongTensor(lable) # Y必须是 PyTorch 的 LongTensor
def __getitem__(self, item):
return self.X[item], self.Y[item] # 返回值:一个元组 (特征, 标签)
def __len__(self):
return len(self.Y)
数据加载器
# batchsize:批量大小,默认为1(逐个样本处理)
# valSize:验证集比例,默认为0.2(20%作为验证集)
def get_dataloader(path, batchsize=1, valSize=0.2):
x, y = read_txt(path)
# 数据分割:用于将数据集分割成训练集和测试集
train_x, val_x, train_y, val_y = train_test_split(x, y,test_size=valSize,shuffle=True, stratify=y)
train_set = JdDataset(train_x, train_y)
val_set = JdDataset(val_x, val_y)
train_loader = DataLoader(train_set, batch_size=batchsize)
val_loader = DataLoader(val_set, batch_size=batchsize)
return train_loader, val_loader
train_test_split 函数
train_test_split 是机器学习中最常用的数据分割函数,用于将数据集划分为训练集和测试集。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, # 特征数据
y, # 标签数据
test_size=0.25, # 测试集比例
random_state=42, # 随机种子
shuffle=True, # 是否洗牌
stratify=None # 是否分层采样
)
stratify=y 的重要性:
假设原始数据标签分布为:
好评(1):800 个
差评(0):200 个
总计:1000 个
没有
stratify的情况:
随机分割可能导致训练集和验证集标签比例不一致
例如:训练集可能有 750好评/150差评,验证集 50好评/50差评
有
stratify=y的情况:
保证两个集合的标签比例与原始数据一致
训练集:640好评/160差评(80%)
验证集:160好评/40差评(20%)
保持了 80:20 的好评差评比例
Bert模型
class myBertModel(nn.Module):
def __init__(self, bert_path, num_class, device):
super(myBertModel, self).__init__()
self.device = device
self.num_class = num_class
# 加载BERT模型(有预训练权重)
self.bert = BertModel.from_pretrained(bert_path)
# 加载分词器
self.tokenizer = BertTokenizer.from_pretrained(bert_path)
# 创建顺序容器
self.out = nn.Sequential(
nn.Linear(768,num_class) # 线性层:输入维度768(BERT输出维度),输出到分类数
)
# 将文本转换为BERT输入格式
def get_bert_input(self, text):
# 调用分词器处理文本
Input = self.tokenizer(text,
return_tensors='pt', # 返回PyTorch张量格式
padding='max_length', # 填充到最大长度
truncation=True, # 截断超过最大长度的文本
max_length=128) # 设置最大长度为128个token
input_ids = Input["input_ids"].to(self.device) # token IDs,移动到指定设备
attention_mask = Input["attention_mask"].to(self.device) # 注意力掩码(区分真实token和填充)
token_type_ids = Input["token_type_ids"].to(self.device) # 句子类型ID(用于句子对任务)
return input_ids, attention_mask, token_type_ids # 返回三个张量
# 前向传播
def forward(self, text):
# 1. 构建BERT输入:原始文本 → BERT要求的三大核心张量
input_ids, attention_mask, token_type_ids = self.get_bert_input(text)
# 通过 BERT 模型:self.bert(...)
# 2. 通过BERT模型:输入张量 → 语义特征(序列特征+全局特征)
sequence_out, pooled_output = self.bert(
input_ids=input_ids, # 文本的数字编码张量 [batch_size, seq_len]
attention_mask=attention_mask, # 掩码张量(区分有效token/填充)[batch_size, seq_len]
token_type_ids=token_type_ids, # 段标识张量(单文本全0)[batch_size, seq_len]
return_dict=False # 返回元组(而非字典),方便解包
)
# sequence_out:每个 token 的语义向量(768 维),适合 token 级任务(如实体识别)
# pooled_output:[CLS] 标记经 Pooler 层投影后的全局语义向量(Pooler 层输出),适合文本级任务(如分类)
# 分类头:self.out(pooled_output)
# 3. 分类头:全局语义特征 → 分类预测结果
pred= self.out(pooled_output) # pooled_output: [batch_size, 768] → 映射到类别空间
return pred # 最终输出形状: [batch_size, num_class]
训练
def train_val(para):
model = para['model']
train_loader =para['train_loader']
val_loader = para['val_loader']
scheduler = para['scheduler']
optimizer = para['optimizer']
loss = para['loss']
epoch = para['epoch']
device = para['device']
save_path = para['save_path']
max_acc = para['max_acc']
val_epoch = para['val_epoch']
plt_train_loss = []
plt_train_acc = []
plt_val_loss = []
plt_val_acc = []
val_rel = []
for i in range(epoch):
start_time = time.time()
model.train()
train_loss = 0.0
train_acc = 0.0
val_acc = 0.0
val_loss = 0.0
for batch in tqdm(train_loader):
model.zero_grad()
text, labels = batch[0], batch[1].to(device)
pred = model(text)
bat_loss = loss(pred, labels)
bat_loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
train_loss += bat_loss.item() #.detach 琛ㄧず鍘绘帀姊害
train_acc += np.sum(np.argmax(pred.cpu().data.numpy(),axis=1)== labels.cpu().numpy())
plt_train_loss . append(train_loss/train_loader.dataset.__len__())
plt_train_acc.append(train_acc/train_loader.dataset.__len__())
if i % val_epoch == 0:
model.eval()
with torch.no_grad():
for batch in tqdm(val_loader):
val_text, val_labels = batch[0], batch[1].to(device)
val_pred = model(val_text)
val_bat_loss = loss(val_pred, val_labels)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == val_labels.cpu().numpy())
val_rel.append(val_pred)
if val_acc > max_acc:
torch.save(model, save_path+str(epoch)+"ckpt")
max_acc = val_acc
plt_val_loss.append(val_loss/val_loader.dataset.__len__())
plt_val_acc.append(val_acc/val_loader.dataset.__len__())
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f | valAcc: %3.6f valLoss: %3.6f ' % \
(i, epoch, time.time()-start_time, plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1])
)
if i % 50 == 0:
torch.save(model, save_path+'-epoch:'+str(i)+ '-%.2f'%plt_val_acc[-1])
else:
plt_val_loss.append(plt_val_loss[-1])
plt_val_acc.append(plt_val_acc[-1])
print('[%03d/%03d] %2.2f sec(s) TrainAcc : %3.6f TrainLoss : %3.6f ' % \
(i, epoch, time.time()-start_time, plt_train_acc[-1], plt_train_loss[-1])
)
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('loss')
plt.legend(['train', 'val'])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title('Accuracy')
plt.legend(['train', 'val'])
plt.savefig('acc.png')
plt.show()
1. 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm= 1.0)
限制梯度范数不超过1.0,防止梯度爆炸
**第一个参数:model.parameters():**获取模型所有需要梯度更新的参数
第二个参数:max_norm=5.0 : 这是最大范数阈值,是梯度裁剪的核心参数
什么时候需要梯度裁剪?
1. 训练RNN/LSTM/GRU等循环神经网络
RNN容易出现梯度爆炸问题(梯度随时间步指数增长)
特别是处理长序列时,梯度裁剪几乎是必需的
RNN训练中的典型使用
for batch in dataloader:
optimizer.zero_grad()
loss = train_rnn(batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) # 必须
optimizer.step()
- 模型层数很深时
深层网络(如Transformer、BERT、深层CNN)
梯度在反向传播中可能累积变大
深层Transformer模型
BERT, GPT等预训练模型通常需要梯度裁剪
3. 学习率设置较大时
高学习率 + 大梯度 = 参数更新过大 → 训练不稳定
梯度裁剪可以稳定训练过程
2、优化器optimizer
optimizer = torch.optim.AdamW(
model.parameters(), # 要优化的参数
lr=learning_rate, # 学习率
weight_decay=0.0001 # 权重衰减系数
)
-
model.parameters():模型所有可训练参数 -
lr=learning_rate:初始学习率(如 1e-4, 2e-5) -
weight_decay=0.0001:-
作用:L2正则化,防止过拟合
-
值:0.0001 = 1e-4(常用值)
-
效果 :权重会以
w = w - lr * weight_decay * w方式衰减
-
3、学习率调度器 scheduler
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer, # 要调度的优化器
T_0=20, # 第一个周期的长度(epoch数)
eta_min=1e-9 # 最小学习率
)
算法原理:
1. 余弦退火(Cosine Annealing):
- 学习率按余弦函数从初始值衰减到最小值
2. 热重启(Warm Restarts):
-
在每个周期结束时,"重启"学习率
-
但每个新周期的最大学习率可能不同
参数详解:
T_0=20:
第一个周期的长度
表示:每20个epoch完成一个余弦周期
然后重启开始新周期
# 周期长度可能变化:
T_0 = 20 # 第一个周期:20个epoch
T_1 = T_0 * 2 = 40 # 第二个周期:40个epoch
T_2 = T_1 * 2 = 80 # 第三个周期:80个epoch
# 以此类推(默认倍增)
eta_min=1e-9:
学习率的最小值
# 学习率变化范围:
# 开始时:lr = learning_rate(如1e-4)
# 周期结束时:lr = eta_min = 1e-9
# 重启后:lr = 学习率 * 衰减因子(可能)
不用bert参数的备选方案:
from transformers import BertConfig, BertModel
# 方案1:从配置文件加载配置
bert_config = BertConfig.from_pretrained(bert_path) # 加载BERT配置
self.bert = BertModel(bert_config) # 用配置初始化BERT(随机权重)
-
BertConfig.from_pretrained(bert_path):只加载配置文件(如config.json),不加载预训练权重 -
BertModel(bert_config):根据配置创建新的BERT模型,所有权重随机初始化
缺点:
-
需要更多数据:通常需要大量数据才能训练好
-
训练时间更长:需要更多epoch才能收敛
-
可能效果较差:特别是小数据集上
-
需要调参:学习率等超参数需要重新调整
-
资源消耗:从头训练计算成本高