AI模型框架实战指南:从CNN到Transformer的演进

前言:架构演进背后的逻辑

神经网络架构的演进,本质上是对"什么重要"的理解在不断变化:

timeline title 神经网络架构演进 section 1990s LeNet : 卷积核、池化层 section 2010s AlexNet : 深度网络 + GPU VGG : 小卷积核 ResNet : 残差连接 section 2017+ Transformer : 注意力机制 BERT/GPT : 预训练范式 ViT : 视觉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

flowchart LR x["输入 x"] --> conv1["Conv 3×3 + BN + ReLU"] conv1 --> conv2["Conv 3×3 + BN"] x --> add["逐元素相加"] conv2 --> add add --> relu["ReLU"] relu --> out["输出 y = F(x) + x"]

为什么残差连接有效:

  1. 梯度传播更顺:shortcut 提供了一条"高速公路",梯度能少经过几层非线性
  2. 优化目标更简单:学增量比学整个映射容易
  3. 特征复用:浅层特征通过 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 的三大问题:

  1. 梯度消失:处理长序列时梯度指数级衰减
  2. 计算效率低:无法并行,必须按时间步顺序处理
  3. 长距离依赖弱:处理 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 $$

四个关键点:

  1. $QK^T$:计算 Query 和所有 Key 的相似度
  2. 除以 $\sqrt{d_k}$:缩放因子,防止 softmax 梯度消失
  3. softmax:相似度转概率分布
  4. 乘以 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

两个关键设计:

  1. 残差连接(Add):帮助梯度流动,避免深层网络梯度消失
  2. 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 maskpadding + 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 计算快、缓解梯度消失,但有三个坑:

  1. 负区间死亡:输入长期为负时梯度恒为 0,节点不再更新
  2. 输出无界:正区间可能数值爆炸
  3. 均值非零:输出分布不以 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)

实践建议:

  1. 默认先用 ReLU:简单任务、小数据集从 ReLU 开始
  2. 遇到问题再换:训练 loss 卡在随机猜测水平、中间层大量输出为 0 时,换 Swish/GELU
  3. Transformer / 预训练模型优先 GELU:BERT、GPT 默认就是 GELU
  4. 注意输出分布:训练中定期检查中间层零值比例,早发现早调整

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 的选择

维度CNNViT
计算效率高,适合移动端低,显存占用大
小数据集表现稳定容易过拟合
大数据集受限于局部感受野全局建模能力强
迁移学习一般效果更好
工具链成熟(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 参数

三个关键设计:

  1. 双向注意力:能同时看到上下文(GPT 只能看左边)
  2. 可学习的位置嵌入:不是固定的 sin/cos 编码
  3. 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 微调实践

端到端微调的坑:

  1. 过拟合风险高(小数据集)
  2. 显存占用大
  3. 训练不稳定

推荐做法:参数高效微调

# 冻结前面层,只训练最后几层
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 标准掩码;特定任务微调且能用词性信息时上动态词性掩码;长文本任务、计算资源充足再考虑句子级掩码。复杂策略不一定更好,简单稳定往往才是王道。

踩坑提醒:

  1. 掩码比例不是越高越好:调到 30% 会上下文信息不足,模型根本学不动,15% 是验证过的稳妥值
  2. 固定随机种子:否则每次结果不可复现,排查问题时容易被带偏
  3. 高频词下采样:“的"“是"这类词上下文太多,模型总猜它们,需要降采样
  4. 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

维度BERTGPT
架构EncoderDecoder
注意力双向单向(只看左边)
擅长理解类任务生成类任务
预训练MLM + NSP自回归

7.2 GPT 的演进

  • GPT-1:验证预训练 + 微调范式
  • GPT-2:规模扩大,Zero-shot 能力初现
  • GPT-3:175B 参数,In-context learning
  • GPT-4:多模态,推理能力大幅提升

7.3 从 GPT 到 ChatGPT

ChatGPT 的训练流程:

  1. 预训练:海量文本学语言能力
  2. SFT(监督微调):指令-响应对子学指令理解
  3. RLHF:人类反馈对齐偏好

八、架构选择的实战经验

8.1 按场景选择

场景推荐架构
小数据集 + 简单分类CNN / RNN
长文本理解Transformer Encoder (BERT)
文本生成Transformer Decoder (GPT)
图像分类 + 移动端ResNet / MobileNet
图像分类 + 高精度ViT / ConvNeXt
目标检测YOLO / DETR
跨模态CLIP / BLIP

8.2 选型要问的几个问题

  1. 数据量多大? 小数据优先简单架构
  2. 计算资源够不够? 有限的话优先轻量模型
  3. 对精度和速度的要求? 实时场景优先 CNN
  4. 团队熟悉程度? 不熟悉的架构踩坑多

8.3 模型性能对比(某文本分类项目)

模型准确率训练时间推理延迟显存占用
RNN72.3%2.5h45ms2.1GB
LSTM75.8%3.2h52ms2.4GB
GRU76.2%2.8h48ms2.2GB
Transformer82.5%1.8h15ms3.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/) 转载或引用必须申明原指尖魔法屋来源及源地址!