AI模型框架实战指南:从CNN到Transformer的演进
前言:架构演进背后的逻辑
神经网络架构的演进,本质上是对"什么重要"的理解在不断变化:
没有最好的架构,只有最适合场景的架构。本文梳理从 CNN 到 Transformer 的完整演进路径。
一、CNN:卷积的黄金时代
1.1 CNN 的核心思想
CNN 通过滑动窗口在局部提取特征,一层层堆叠后逐渐扩大感受野。
class TextCNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, num_classes):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.conv = nn.Conv1d(embedding_dim, 100, kernel_size=3)
self.fc = nn.Linear(100, num_classes)
def forward(self, x):
x = self.embedding(x)
x = x.transpose(1, 2)
x = torch.relu(self.conv(x))
x = torch.max_pool1d(x, x.size(2))
x = x.squeeze(2)
return self.fc(x)
CNN 的优势:
- 训练速度快
- 计算资源要求低
- 适合短文本/小图像
- 工具链成熟(TensorRT、ONNX 支持好)
CNN 的局限:
- 长文本/长距离依赖效果差
- 上下文理解能力有限
- 对全局关系建模弱
1.2 ResNet:解决深层网络退化
问题: 网络加深后,训练误差和测试误差反而一起上升(不是过拟合,是优化本身出问题)。
ResNet 的解决方案: 让层学残差 F(x) = H(x) - x,输出 y = F(x) + x。
为什么残差连接有效:
- 梯度传播更顺:shortcut 提供了一条"高速公路",梯度能少经过几层非线性
- 优化目标更简单:学增量比学整个映射容易
- 特征复用:浅层特征通过 shortcut 直接参与深层决策
1.3 ResNet 实战改造
以篆书 OCR 项目为例,基于 ResNet-18 做的改造:
from torchvision.models import resnet18
network = resnet18(weights=None)
# 1. 单通道输入(灰度图)
network.conv1 = nn.Conv2d(1, 64, kernel_size=5, stride=2, padding=2, bias=False)
# 2. 去掉 maxpool(小图保留更多分辨率)
network.maxpool = nn.Identity()
# 3. 去掉分类头,输出 embedding
features = network.fc.in_features
network.fc = nn.Identity()
self.backbone = network
self.embedding = nn.Sequential(
nn.Dropout(0.15),
nn.Linear(features, 256, bias=False),
nn.BatchNorm1d(256),
)
# 4. 余弦分类头(比 softmax 更适合跨字体泛化)
self.classifier = CosineClassifier(256, num_classes, scale=30.0)
几条实战经验:
- 先确认瓶颈是不是网络:精度上不去先查数据,再考虑换更大的模型
- 输入尺寸和 stem 要匹配任务:小图 OCR 经常需要改 conv1 核大小、去掉 maxpool
- 分类头可以换:线性层、余弦层、ArcFace 都行,看任务是分类还是检索
二、RNN 到 Attention 的演进
2.1 RNN 的困境
class SimpleRNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.rnn = nn.RNN(embedding_dim, hidden_dim, batch_first=True)
RNN 的三大问题:
- 梯度消失:处理长序列时梯度指数级衰减
- 计算效率低:无法并行,必须按时间步顺序处理
- 长距离依赖弱:处理 1000 字文档,跑到一半就忘了开头
2.2 Attention 的突破
2017 年《Attention Is All You Need》彻底改变游戏规则:处理每个 token 时,同时关注其他所有 token。
Attention 的数学表达:
$$ \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$
四个关键点:
- $QK^T$:计算 Query 和所有 Key 的相似度
- 除以 $\sqrt{d_k}$:缩放因子,防止 softmax 梯度消失
- softmax:相似度转概率分布
- 乘以 V:根据注意力权重加权求和
2.3 为什么需要缩放因子
如果不加 $\sqrt{d_k}$,当 $d_k$ 很大时,$QK^T$ 的点积值会很大,softmax 进入饱和区,梯度接近 0。
假设 $d_k = 512$,$Q$ 和 $K$ 的分量均值 0、标准差 1,那么 $QK^T$ 的标准差是 $\sqrt{512} \approx 22.6$。加上 $\sqrt{d_k}$ 后,点积标准差被缩放回 1 左右,训练就稳定了。
三、Transformer 架构详解
3.1 Multi-Head Attention
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.d_model = d_model
self.num_heads = num_heads
self.d_k = d_model // num_heads
self.W_q = nn.Linear(d_model, d_model)
self.W_k = nn.Linear(d_model, d_model)
self.W_v = nn.Linear(d_model, d_model)
self.W_o = nn.Linear(d_model, d_model)
def scaled_dot_product_attention(self, Q, K, V, mask=None):
attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
attn_probs = torch.softmax(attn_scores, dim=-1)
output = torch.matmul(attn_probs, V)
return output, attn_probs
def forward(self, x, mask=None):
batch_size = x.size(0)
Q = self.W_q(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
K = self.W_k(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
V = self.W_v(x).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2)
attn_output, attn_probs = self.scaled_dot_product_attention(Q, K, V, mask)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
return self.W_o(attn_output)
多头的好处: 单头只能在一个子空间计算相似度,多头让模型在多个子空间关注不同信息——有的头关注语法结构,有的关注语义相似度,有的关注位置关系。
3.2 Transformer Block
class TransformerBlock(nn.Module):
def __init__(self, embed_size, heads, dropout, forward_expansion):
super().__init__()
self.attention = MultiHeadAttention(embed_size, heads)
self.norm1 = nn.LayerNorm(embed_size)
self.norm2 = nn.LayerNorm(embed_size)
self.feed_forward = nn.Sequential(
nn.Linear(embed_size, forward_expansion * embed_size),
nn.ReLU(),
nn.Linear(forward_expansion * embed_size, embed_size)
)
self.dropout = nn.Dropout(dropout)
def forward(self, value, key, query, mask):
attention = self.attention(value, key, query, mask)
x = self.dropout(self.norm1(attention + query)) # Add & Norm
forward = self.feed_forward(x)
out = self.dropout(self.norm2(forward + x)) # Add & Norm
return out
两个关键设计:
- 残差连接(Add):帮助梯度流动,避免深层网络梯度消失
- Layer Normalization(Norm):每个样本内部归一化,比 BatchNorm 更适合序列数据
为什么用 LayerNorm 而不是 BatchNorm:
BatchNorm 依赖批次统计量,序列长度变化时归一化不稳定。LayerNorm 在每个样本内部归一化,训练和推理行为一致。
3.3 位置编码
Transformer 本身没有循环或卷积结构,需要位置编码提供位置信息:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super().__init__()
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(
torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)
)
pe = torch.zeros(max_len, d_model)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)
def forward(self, x):
return x + self.pe[:, :x.size(1)]
用正弦和余弦函数,不同维度对应不同频率,能外推到比训练时更长的序列。
3.4 掩码机制
Transformer 里常见的注意力掩码有三类,作用各不相同,别混在一起:
| 掩码类型 | 作用 | 形状 |
|---|---|---|
| Padding mask | 屏蔽 padding 位置,不让无意义的填充参与注意力 | [B, 1, 1, L] |
| Causal mask(三角掩码) | 防止 Decoder 看到未来 token | [1, 1, L, L] |
| Attention mask | padding + causal 的组合,逐元素相乘 | [B, 1, L, L] |
Padding mask 直接从输入 token id 生成:padding 位置为 0,真实 token 为 1。
def make_src_mask(src, pad_idx):
# src: [B, L],只要 != pad_idx 的位置
return (src != pad_idx).unsqueeze(1).unsqueeze(2) # [B, 1, 1, L]
Causal mask(三角掩码) 保证生成时只能看左边,Decoder 的 Self-Attention 必须加:
def create_trg_mask(trg):
N, trg_len = trg.shape
trg_mask = torch.tril(torch.ones((trg_len, trg_len))).expand(
N, 1, trg_len, trg_len
)
return trg_mask
注意用 torch.tril(下三角),不是 torch.triu。 写反了会导致模型看到未来信息。
这两种掩码最终都喂给 3.1 节里的 masked_fill(mask == 0, -1e9),把被屏蔽位置的注意力分数压成负无穷,softmax 后权重趋近 0。
还有一类是 BERT 预训练用的 MLM 掩码——它在数据层面把 token 换成
[MASK],和上面屏蔽注意力分数的机制不同,详见 6.6 节。
3.5 激活函数的选择
上面 TransformerBlock 的 FFN 用了 nn.ReLU(),但 ReLU 不总是最佳选择。激活函数本质是给网络注入非线性——没有它,多层线性变换堆再深仍等价于一层。
ReLU 的问题:
def relu(x):
return max(0, x)
ReLU 计算快、缓解梯度消失,但有三个坑:
- 负区间死亡:输入长期为负时梯度恒为 0,节点不再更新
- 输出无界:正区间可能数值爆炸
- 均值非零:输出分布不以 0 为中心
排查"死亡节点"可以看中间层输出的零值比例——如果超过 90% 的节点输出为 0,基本就是 ReLU 把梯度堵死了。这时降学习率、调初始化通常治标不治本,得直接换激活函数。
常见替代方案:
| 激活函数 | 公式 | 核心特点 | 适用场景 |
|---|---|---|---|
| ReLU | $\max(0, x)$ | 快、稀疏,但负区间死亡 | 默认 baseline,浅层网络 |
| LeakyReLU | $\max(0.01x, x)$ | 负区间保留小梯度 | ReLU 死亡严重时 |
| ELU | $x \ (x>0);\ \alpha(e^x-1) \ (x\le0)$ | 负区间平滑,输出均值接近 0 | 需要更稳定训练 |
| Swish/SiLU | $x \cdot \sigma(x)$ | 平滑非单调,保留小负值 | 深层网络,追求性能 |
| GELU | $x \cdot \Phi(x)$ | 考虑随机正则化的平滑函数 | BERT、GPT 等预训练模型 |
Swish 原理: $f(x) = x \cdot \sigma(\beta x)$,默认 $\beta=1$。它处处可导、负区间保留约 -0.278 的下界(不像 ReLU 直接清零),结构上自带"门控"意味,在深层网络里通常比 ReLU 表现更好。
import torch.nn.functional as F
# 方法 1:Swish 在 PyTorch 里叫 SiLU(beta=1 时等价)
output = F.silu(x)
# 方法 2:手动实现
def swish(x, beta=1.0):
return x * torch.sigmoid(beta * x)
参数与数值稳定性: Swish 的 $\beta$ 默认 1.0 效果最好,太大会接近硬分段、太小会让激活区域变窄;自定义时建议对输入 clip,避免 sigmoid 在大输入下溢出。
def safe_swish(x, beta=1.0):
x = torch.clamp(x, -50.0, 50.0)
return x * torch.sigmoid(beta * x)
实践建议:
- 默认先用 ReLU:简单任务、小数据集从 ReLU 开始
- 遇到问题再换:训练 loss 卡在随机猜测水平、中间层大量输出为 0 时,换 Swish/GELU
- Transformer / 预训练模型优先 GELU:BERT、GPT 默认就是 GELU
- 注意输出分布:训练中定期检查中间层零值比例,早发现早调整
Swish 不是万能药——小数据集上可能过拟合,推理延迟敏感场景要权衡它比 ReLU 高的计算成本。
四、训练 Transformer 的坑
4.1 梯度爆炸
# 训练循环中加梯度裁剪
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
4.2 学习率调度
Transformer 对学习率敏感,需要 Warmup 策略:
def get_linear_schedule_with_warmup(optimizer, num_warmup_steps, num_training_steps):
def lr_lambda(current_step):
if current_step < num_warmup_steps:
return float(current_step) / float(max(1, num_warmup_steps))
return max(0.0, float(num_training_steps - current_step) /
float(max(1, num_training_steps - num_warmup_steps)))
return LambdaLR(optimizer, lr_lambda)
逻辑: 先线性增加学习率,再按步数衰减。保证训练初期稳定性,后期精细收敛。
4.3 序列长度不一致
用动态 padding 和 collate_fn:
from torch.nn.utils.rnn import pad_sequence
def collate_fn(batch):
src_batch = [item['src'] for item in batch]
trg_batch = [item['trg'] for item in batch]
src_batch = pad_sequence(src_batch, batch_first=True, padding_idx=PAD_IDX)
trg_batch = pad_sequence(trg_batch, batch_first=True, padding_idx=PAD_IDX)
return {'src': src_batch, 'trg': trg_batch}
4.4 显存优化
混合精度训练:
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
for batch in train_loader:
optimizer.zero_grad()
with autocast():
output = model(src, trg[:-1, :])
loss = criterion(output.reshape(-1, output.shape[2]), trg[1:, :].reshape(-1))
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
梯度累积(模拟更大 batch size):
accumulation_steps = 4
for i, batch in enumerate(train_loader):
with autocast():
output = model(src)
loss = criterion(output, target) / accumulation_steps
scaler.scale(loss).backward()
if (i + 1) % accumulation_steps == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
五、ViT:视觉 Transformer
5.1 从 CNN 到 ViT 的核心差异
- CNN:通过滑动窗口在局部提取特征,逐层扩大感受野
- ViT:把图像切成 patch 序列,用自注意力让每个 patch 都能和其他 patch 交互
5.2 Patch Embedding
class ViTPatchEmbedding(nn.Module):
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
super().__init__()
self.n_patches = (img_size // patch_size) ** 2
# 本质上是一个 kernel=patch_size, stride=patch_size 的卷积
self.proj = nn.Conv2d(in_channels, embed_dim,
kernel_size=patch_size, stride=patch_size)
def forward(self, x):
x = self.proj(x) # [B, embed_dim, H/patch, W/patch]
x = x.flatten(2) # [B, embed_dim, n_patches]
x = x.transpose(1, 2) # [B, n_patches, embed_dim]
return x
关键洞察: ViT 的 patch embedding 本质就是一个大步长的卷积。
5.3 完整 ViT 实现
class VisionTransformer(nn.Module):
def __init__(self, img_size=224, patch_size=16, in_channels=3, num_classes=1000,
embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0, dropout=0.1):
super().__init__()
self.patch_embed = ViTPatchEmbedding(img_size, patch_size, in_channels, embed_dim)
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_embed = nn.Parameter(torch.zeros(1, 1 + self.patch_embed.n_patches, embed_dim))
self.pos_drop = nn.Dropout(dropout)
self.blocks = nn.ModuleList([
TransformerBlock(embed_dim, num_heads, dropout, mlp_ratio)
for _ in range(depth)
])
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, num_classes)
def forward(self, x):
x = self.patch_embed(x)
cls_token = self.cls_token.expand(x.shape[0], -1, -1)
x = torch.cat((cls_token, x), dim=1)
x = x + self.pos_embed
x = self.pos_drop(x)
for block in self.blocks:
x = block(x, x, x, None)
x = self.norm(x)
return self.head(x[:, 0]) # 用 CLS token 分类
5.4 ViT 的坑
坑一:位置编码插值
推理时图像尺寸和训练不同,位置编码需要插值:
def resize_pos_embed(posemb, new_grid_size):
cls_token_emb = posemb[:, :1]
grid_emb = posemb[:, 1:].reshape(1, int(math.sqrt(posemb.shape[1] - 1)),
int(math.sqrt(posemb.shape[1] - 1)), -1)
grid_emb = grid_emb.permute(0, 3, 1, 2)
grid_emb = torch.nn.functional.interpolate(
grid_emb, size=new_grid_size, mode='bicubic', align_corners=False)
grid_emb = grid_emb.permute(0, 2, 3, 1).reshape(1, -1, grid_emb.shape[1])
return torch.cat([cls_token_emb, grid_emb], dim=1)
坑二:计算资源需求高
ViT 显存占用比相同参数量的 CNN 高,主要是注意力矩阵。RTX 3090 上训练 ViT-Base,batch size 通常只能 64,而 ResNet-50 可以开到 256。
坑三:训练稳定性
# 推荐:AdamW + cosine schedule
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.05)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=300)
5.5 CNN vs ViT 的选择
| 维度 | CNN | ViT |
|---|---|---|
| 计算效率 | 高,适合移动端 | 低,显存占用大 |
| 小数据集 | 表现稳定 | 容易过拟合 |
| 大数据集 | 受限于局部感受野 | 全局建模能力强 |
| 迁移学习 | 一般 | 效果更好 |
| 工具链 | 成熟(TensorRT) | 需要 Flash Attention 等 |
实用建议: 先用 CNN 快速迭代验证想法,方案稳定后再用 ViT 做精度提升。
5.6 Swin Transformer:层次化注意力
Swin Transformer 把自注意力限制在局部窗口,通过 shifted window 实现跨窗口信息流动,显著降低计算复杂度。
六、BERT:预训练范式的确立
6.1 BERT 的核心思想
用 Transformer encoder 做双向上下文编码器,在大规模语料上做两个预训练任务:
- MLM(Masked Language Model):随机 mask 掉 15% 的 token,让模型预测
- NSP(Next Sentence Prediction):判断两个句子是否连续
6.2 BERT 架构
- BERT-base:12 层,768 维,12 头,110M 参数
- BERT-large:24 层,1024 维,16 头,340M 参数
三个关键设计:
- 双向注意力:能同时看到上下文(GPT 只能看左边)
- 可学习的位置嵌入:不是固定的 sin/cos 编码
- Segment embeddings:区分 token 属于哪个句子
6.3 BERT 的使用
from transformers import BertTokenizer, BertForSequenceClassification
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=3)
text = "这家餐厅的菜品很不错,但服务有点慢"
inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=128)
with torch.no_grad():
outputs = model(**inputs)
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
6.4 BERT 家族
| 模型 | 改进点 | 适用场景 |
|---|---|---|
| RoBERTa | 去掉 NSP,更多数据 | 通用 NLP |
| ALBERT | 参数共享,压缩规模 | 资源受限 |
| ELECTRA | 判别式预训练 | 高效训练 |
| DistilBERT | 蒸馏压缩 | 边缘部署 |
| XLM-RoBERTa | 多语言 | 跨语言场景 |
| Longformer | 长文本支持 | 长文档处理 |
6.5 BERT 微调实践
端到端微调的坑:
- 过拟合风险高(小数据集)
- 显存占用大
- 训练不稳定
推荐做法:参数高效微调
# 冻结前面层,只训练最后几层
for name, param in model.named_parameters():
if "encoder.layer.0" in name or "encoder.layer.1" in name:
param.requires_grad = False
# 或者用 LoRA / Adapter
optimizer = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=2e-5)
6.6 MLM 掩码策略
6.1 提到 BERT 用 MLM 预训练,但"随机 mask 15%“只是起点。掩码策略直接决定模型学到什么,值得单独说说。下面三种策略从简单到复杂,效果和开销也递增。
① BERT 标准掩码: 选 15% 的 token,其中 80% 换成 [MASK]、10% 随机替换成别的词、10% 保持不变。这样模型在不同条件下都得学会预测。
def bert_masking(tokens, mask_prob=0.15):
num_masks = int(len(tokens) * mask_prob)
mask_indices = random.sample(range(len(tokens)), num_masks)
masked_tokens = tokens.copy()
for i in mask_indices:
prob = random.random()
if prob < 0.8:
masked_tokens[i] = "[MASK]" # 80% 替换
elif prob < 0.9:
masked_tokens[i] = random.choice(all_tokens) # 10% 随机词
# 10% 保持原样
return masked_tokens, mask_indices
② 动态词性掩码: 给名词、动词等实词更高的掩码权重,助词(“的"“了”)少掩——否则模型总学去预测高频虚词,忽略上下文。
def dynamic_masking(tokens, pos_tags, mask_prob=0.15):
pos_weights = {'NOUN': 1.5, 'VERB': 1.5, 'ADJ': 1.2,
'ADV': 1.2, 'PRON': 0.8, 'PART': 0.5, 'default': 1.0}
mask_indices = [i for i, tag in enumerate(pos_tags)
if random.random() < mask_prob * pos_weights.get(tag, 1.0)]
masked_tokens = tokens.copy()
for i in mask_indices:
masked_tokens[i] = "[MASK]"
return masked_tokens, mask_indices
③ 句子级掩码: 整句掩掉,强迫模型学句子结构和长距离依赖,适合长文本理解任务。
三种策略对比:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| BERT 标准掩码 | 简单易实现,效果稳定 | 不考虑词义差异 | 通用预训练 |
| 动态词性掩码 | 针对性强,重要词学得更好 | 需词性标注器,增加开销 | 特定任务微调 |
| 句子级掩码 | 学句子结构,长距离依赖强 | 计算复杂,短文本不友好 | 长文本理解 |
选择逻辑: 通用预训练 / 快速验证 baseline 直接用 BERT 标准掩码;特定任务微调且能用词性信息时上动态词性掩码;长文本任务、计算资源充足再考虑句子级掩码。复杂策略不一定更好,简单稳定往往才是王道。
踩坑提醒:
- 掩码比例不是越高越好:调到 30% 会上下文信息不足,模型根本学不动,15% 是验证过的稳妥值
- 固定随机种子:否则每次结果不可复现,排查问题时容易被带偏
- 高频词下采样:“的"“是"这类词上下文太多,模型总猜它们,需要降采样
- DataLoader 多进程:
num_workers>0时在线动态掩码会让每个 worker 掩码不一致,最好在预处理阶段就掩好
# 错误:DataLoader 里动态掩码,多进程下模式不一致
dataloader = DataLoader(dataset, batch_size=32, num_workers=4,
collate_fn=lambda batch: dynamic_masking(batch))
# 正确:预处理阶段就掩码好
preprocessed_data = [dynamic_masking(t) for t in dataset]
dataloader = DataLoader(preprocessed_data, batch_size=32, num_workers=4)
七、GPT:生成式大模型
7.1 BERT vs GPT
| 维度 | BERT | GPT |
|---|---|---|
| 架构 | Encoder | Decoder |
| 注意力 | 双向 | 单向(只看左边) |
| 擅长 | 理解类任务 | 生成类任务 |
| 预训练 | MLM + NSP | 自回归 |
7.2 GPT 的演进
- GPT-1:验证预训练 + 微调范式
- GPT-2:规模扩大,Zero-shot 能力初现
- GPT-3:175B 参数,In-context learning
- GPT-4:多模态,推理能力大幅提升
7.3 从 GPT 到 ChatGPT
ChatGPT 的训练流程:
- 预训练:海量文本学语言能力
- SFT(监督微调):指令-响应对子学指令理解
- RLHF:人类反馈对齐偏好
八、架构选择的实战经验
8.1 按场景选择
| 场景 | 推荐架构 |
|---|---|
| 小数据集 + 简单分类 | CNN / RNN |
| 长文本理解 | Transformer Encoder (BERT) |
| 文本生成 | Transformer Decoder (GPT) |
| 图像分类 + 移动端 | ResNet / MobileNet |
| 图像分类 + 高精度 | ViT / ConvNeXt |
| 目标检测 | YOLO / DETR |
| 跨模态 | CLIP / BLIP |
8.2 选型要问的几个问题
- 数据量多大? 小数据优先简单架构
- 计算资源够不够? 有限的话优先轻量模型
- 对精度和速度的要求? 实时场景优先 CNN
- 团队熟悉程度? 不熟悉的架构踩坑多
8.3 模型性能对比(某文本分类项目)
| 模型 | 准确率 | 训练时间 | 推理延迟 | 显存占用 |
|---|---|---|---|---|
| RNN | 72.3% | 2.5h | 45ms | 2.1GB |
| LSTM | 75.8% | 3.2h | 52ms | 2.4GB |
| GRU | 76.2% | 2.8h | 48ms | 2.2GB |
| Transformer | 82.5% | 1.8h | 15ms | 3.8GB |
注意:Transformer 训练时显存高,但推理速度快(并行计算)。
九、关键工程经验
9.1 不要盲目追新
NLP/CV 领域更新太快,今天的新架构明天就被超越。很多老模型在生产环境跑得很好,没必要频繁换。
9.2 工程细节比模型选择更重要
学习率调度、数据增强、早停策略、正则化方法,这些在实际项目中往往比选 BERT 还是 RoBERTa 影响更大。
9.3 预训练不是银弹
如果下游任务和预训练数据分布差异很大,预训练模型可能还不如从头训练。
9.4 模型规模不是越大越好
数据量有限时,盲目扩大模型规模得不偿失。BERT-large 在小数据上可能不收敛,BERT-base 反而更快出结果。
十、写在最后
从 LeNet 到 Transformer,架构演进背后是对"什么重要"的理解变化:
- LeNet 时代:手工设计特征,网络只是分类器
- AlexNet/ResNet 时代:让网络自己学特征,深度是关键
- Transformer 时代:注意力机制让模型理解全局关系
- 预训练时代:大规模数据 + 大规模模型 = 涌现能力
但核心问题一直没变:如何让模型更好地理解数据、提取有效信息。
技术没有终点,只有下一段要修的路。没有最好的模型,只有最适合场景的模型。 多了解一种思路,在遇到问题时就多一个选择。
本文整合了 14 篇模型架构相关文章,涵盖 CNN、ResNet、Transformer、ViT、BERT、GPT 等核心架构的演进、实现与实践经验,并补充了激活函数选择与 MLM 掩码策略两大专题。
版权声明: 本文首发于 指尖魔法屋-AI模型框架实战指南:从CNN到Transformer的演进(https://blog.thinkmoon.cn/post/ai-model-framework-comprehensive-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。