1. 从零开始:理解深度学习的“骨架”与“灵魂”

如果你刚接触深度学习,可能会被一堆缩写搞得晕头转向:MLP、CNN、RNN、Transformer……它们听起来像某种神秘代码。别担心,我刚开始也这样。其实,你可以把它们想象成不同功能的“工具”。就像木匠有锯子、锤子、刨子一样,处理不同类型的数据,我们也需要不同的“算法工具”。这篇文章,我就带你亲手把这些工具从工具箱里拿出来,看看它们长什么样,怎么用,以及什么时候该用哪一把。

深度学习本质上是一类通过多层神经网络自动从数据中学习特征的方法。它的“骨架”是各种网络结构,而“灵魂”则是让这些结构学会调整自身参数的优化算法(比如反向传播)。我们今天要讲的MLP、CNN、RNN和Transformer,就是四种最经典、也最核心的“骨架”。它们分别擅长处理不同类型的数据:MLP对付表格数据,CNN专攻图像,RNN处理像语言、语音这样的序列,而Transformer则是近年来在序列任务上大放异彩的“全能选手”。

理解它们的关键,不在于死记硬背公式,而在于抓住两个核心直觉:数据的内在结构计算的高效性。比如,图像数据在空间上是高度结构化的,相邻像素关系紧密,CNN的卷积操作就是为此量身定做的,它能极大地减少参数数量。而处理一句话时,单词的顺序和上下文至关重要,RNN和Transformer就是为了捕捉这种序列依赖关系而生的。接下来,我们就从最基础的MLP开始,一步步拆解,并用代码实战让你真正“上手”。

2. 基石算法:多层感知机(MLP)的构建与实战

2.1 MLP的核心思想:万能逼近器

MLP,也就是多层感知机,是深度学习世界里最基础的建筑模块。你可以把它想象成一个超级复杂、多层的信号处理器。它由三部分组成:一个输入层(接收数据)、一个或多个隐藏层(负责计算和特征变换)、一个输出层(给出最终结果)。每一层的神经元都与下一层的所有神经元相连,这就是所谓的“全连接”。

为什么说它是“万能逼近器”呢?理论上,只要隐藏层足够宽、层数足够多,MLP可以以任意精度逼近任何复杂的连续函数。这给了我们解决非线性问题的强大武器。比如,给你一个人的年龄、收入、职业等一堆特征(表格数据),MLP就能学习到一个非常复杂的函数,来判断这个人是否会购买某款产品。

它的工作原理很简单:数据从输入层进入,每一层都对数据做一次“加权求和 + 非线性变换”。这个非线性变换由激活函数(比如ReLU、Sigmoid)完成,它至关重要,没有它,多层网络就会退化成单层线性模型,能力大打折扣。整个网络通过反向传播算法,根据预测结果和真实标签的误差,从后往前一点点调整每一层连接的“权重”和“偏置”,直到模型预测得足够准。

2.2 用PyTorch手搓一个MLP:解决鸢尾花分类

光说不练假把式,我们直接用代码来实现一个MLP,解决经典的鸢尾花分类问题。这个数据集有150个样本,每个样本有4个特征(花萼和花瓣的长宽),需要分为3类。

import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. 准备数据
iris = load_iris()
X, y = iris.data, iris.target
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 标准化,这对神经网络训练很重要
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
# 转为PyTorch张量
X_train = torch.FloatTensor(X_train)
y_train = torch.LongTensor(y_train)
X_test = torch.FloatTensor(X_test)
y_test = torch.LongTensor(y_test)

# 2. 定义MLP模型
class IrisClassifier(nn.Module):
    def __init__(self, input_size=4, hidden_size=10, output_size=3):
        super(IrisClassifier, self).__init__()
        # 定义网络层
        self.fc1 = nn.Linear(input_size, hidden_size)  # 输入层 -> 隐藏层
        self.relu = nn.ReLU()                           # 激活函数
        self.fc2 = nn.Linear(hidden_size, output_size) # 隐藏层 -> 输出层
        # 我们没有用Softmax层,因为CrossEntropyLoss内部包含了它

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

# 初始化模型、损失函数和优化器
model = IrisClassifier()
criterion = nn.CrossEntropyLoss()  # 交叉熵损失,适用于多分类
optimizer = optim.Adam(model.parameters(), lr=0.01)  # Adam优化器,学习率0.01

# 3. 训练模型
epochs = 200
for epoch in range(epochs):
    # 前向传播
    outputs = model(X_train)
    loss = criterion(outputs, y_train)

    # 反向传播和优化
    optimizer.zero_grad()  # 清空上一轮的梯度
    loss.backward()        # 计算梯度
    optimizer.step()       # 更新参数

    if (epoch+1) % 20 == 0:
        print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')

# 4. 评估模型
with torch.no_grad():  # 评估时不计算梯度,节省内存和计算
    model.eval()       # 将模型设置为评估模式(影响Dropout、BatchNorm等层)
    predictions = model(X_test)
    _, predicted = torch.max(predictions, 1)  # 取概率最大的类别作为预测结果
    accuracy = (predicted == y_test).sum().item() / y_test.size(0)
    print(f'\n测试集准确率: {accuracy * 100:.2f}%')

运行这段代码,你通常能看到准确率很快达到95%以上。这个简单的例子展示了MLP的核心流程:定义网络结构、准备数据、前向计算损失、反向传播更新权重、评估性能。虽然MLP结构简单,但对于这类特征维度不高、样本量不大的结构化数据,它往往能取得不错的效果。然而,当面对图像(成千上万个像素)或长文本序列时,MLP的全连接结构会导致参数数量爆炸,且无法有效利用数据的空间或时序结构,这时就需要更专门的工具了。

3. 视觉王者:卷积神经网络(CNN)的原理与图像识别

3.1 卷积与池化:CNN的两大法宝

当数据从表格变成图像时,MLP就力不从心了。一张100x100的彩色图片,拉平后就是3万个输入特征,如果第一隐藏层有1000个神经元,光这一层就有3000万个参数!这不仅是计算灾难,也极易过拟合。CNN的聪明之处在于,它引入了两个关键思想:局部连接权重共享

卷积层是CNN的核心。它不再像MLP那样让每个神经元连接所有输入,而是使用一个小的卷积核(比如3x3或5x5的滤波器)在图像上滑动。这个卷积核只关注图像的一小块局部区域(感受野),并提取该区域的局部特征(如边缘、纹理)。更重要的是,同一个卷积核会滑过整张图像,这意味着检测同一种特征(比如垂直边缘)的权重在整个图像上是共享的。这极大地减少了参数量,并赋予了模型平移不变性——无论猫在图片的左上角还是右下角,都能被同样的“猫耳朵检测器”识别出来。

池化层通常跟在卷积层后面,它的作用是进行下采样,进一步减少数据量并增强特征的鲁棒性。最常用的是最大池化,它在一个小窗口(比如2x2)内只保留最大值。这样做的好处是:1. 降低计算复杂度;2. 使特征对小的平移、旋转更加不敏感;3. 扩大后续卷积层的感受野。经过多个“卷积-池化”块的堆叠,网络就能从低级特征(边缘、角点)逐步组合出高级特征(眼睛、轮子、整个物体)。

3.2 实战:用CNN构建一个手写数字识别器

让我们用最著名的MNIST手写数字数据集来实战。我们将构建一个简单的CNN,它包含两个卷积-池化块,最后接上全连接层进行分类。

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 1. 数据准备与加载
transform = transforms.Compose([
    transforms.ToTensor(),  # 将PIL图像或numpy数组转为Tensor,并归一化到[0,1]
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST数据集的均值和标准差
])

train_dataset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

# 2. 定义CNN模型
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 第一个卷积块: 1个输入通道(灰度图),输出32个特征图,3x3卷积核
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)  # 2x2最大池化,尺寸减半

        # 第二个卷积块
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(2, 2)

        # 全连接层
        # 经过两次池化,28x28的图像变成了7x7 (28/2/2=7),通道数为64
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.relu3 = nn.ReLU()
        self.fc2 = nn.Linear(128, 10)  # 输出10个类别(数字0-9)

    def forward(self, x):
        x = self.pool1(self.relu1(self.conv1(x)))
        x = self.pool2(self.relu2(self.conv2(x)))
        x = x.view(-1, 64 * 7 * 7)  # 将特征图展平成一维向量,送入全连接层
        x = self.relu3(self.fc1(x))
        x = self.fc2(x)
        return x

# 初始化
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 3. 训练循环
def train(model, device, train_loader, optimizer, epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % 100 == 0:
            print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} '
                  f'({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')

# 4. 测试函数
def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += criterion(output, target).item()
            pred = output.argmax(dim=1, keepdim=True)  # 获取预测结果
            correct += pred.eq(target.view_as(pred)).sum().item()

    test_loss /= len(test_loader.dataset)
    accuracy = 100. * correct / len(test_loader.dataset)
    print(f'\n测试集: 平均损失: {test_loss:.4f}, 准确率: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)\n')
    return accuracy

# 5. 开始训练和测试
for epoch in range(1, 6):  # 训练5个epoch
    train(model, device, train_loader, optimizer, epoch)
    test(model, device, test_loader)

这个简单的CNN模型在MNIST上通常只需要几个epoch就能达到99%以上的测试准确率。你可以尝试调整卷积核数量、层数、学习率等超参数,观察它们对结果的影响。CNN的成功启发了后续无数变体,如AlexNet、VGG、ResNet等,但它们的基本思想一脉相承。CNN的强大在于它完美契合了图像的二维空间结构,但当我们处理像句子、语音、股票价格这样的序列数据时,就需要另一种结构了。

4. 序列建模专家:循环神经网络(RNN)与长短时记忆网络(LSTM)

4.1 RNN的困境与LSTM的救赎

RNN的设计初衷是为了处理序列数据。它的核心是一个“循环”结构,使得网络能够保留一个“记忆状态”,将之前时间步的信息传递到当前时间步。这就像你读小说时,理解当前句子需要记住前面句子的情节。RNN的数学形式很简单:h_t = f(W * x_t + U * h_{t-1} + b),其中h_t是当前时刻的隐藏状态,它由当前输入x_t和上一时刻状态h_{t-1}共同决定。

然而,经典RNN有个致命弱点:长期依赖问题。在训练时,梯度需要通过时间步反向传播。当序列很长时,梯度在连续相乘中会变得极小(梯度消失)或极大(梯度爆炸),导致网络无法学习到远距离的依赖关系。这就像让你复述一个很长故事的开头细节,你很可能已经忘记了。

为了解决这个问题,**长短时记忆网络(LSTM)**被提出。它通过引入精巧的“门控机制”来有选择地记住和忘记信息。LSTM单元内部有三个门:

  • 遗忘门:决定从细胞状态中丢弃哪些信息。
  • 输入门:决定哪些新信息要存入细胞状态。
  • 输出门:基于细胞状态,决定输出什么。

你可以把细胞状态想象成一条传送带,它贯穿整个时间线,只有少量的线性交互,信息在上面流传很容易保持不变。三个门就像看管这条传送带的守卫,决定什么信息可以上去、什么留下来、什么被扔掉。正是这种设计,让LSTM能够有效地捕捉长距离的依赖关系。**门控循环单元(GRU)**是LSTM的一个流行变体,它合并了遗忘门和输入门,结构更简单,计算效率更高,在许多任务上表现与LSTM相当。

4.2 实战:用LSTM进行文本情感分析

让我们用一个实战项目来感受LSTM处理序列数据的能力。我们将使用IMDb电影评论数据集,这是一个二分类任务(正面/负面评价)。

import torch
import torch.nn as nn
import torch.optim as optim
from torchtext.datasets import IMDB
from torchtext.data.utils import get_tokenizer
from torchtext.vocab import build_vocab_from_iterator
from torch.utils.data import DataLoader
from torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence

# 1. 数据预处理(关键步骤)
tokenizer = get_tokenizer('basic_english')  # 使用基础英文分词器

def yield_tokens(data_iter):
    for _, text in data_iter:
        yield tokenizer(text)

# 加载训练数据并构建词汇表
train_iter = IMDB(split='train')
vocab = build_vocab_from_iterator(yield_tokens(train_iter), specials=['<unk>', '<pad>', '<bos>', '<eos>'])
vocab.set_default_index(vocab['<unk>'])  # 设置默认索引为未知词

text_pipeline = lambda x: [vocab[token] for token in tokenizer(x)]
label_pipeline = lambda x: 1 if x == 'pos' else 0

# 2. 准备数据加载器(处理变长序列)
def collate_batch(batch):
    label_list, text_list, lengths = [], [], []
    for (_label, _text) in batch:
        label_list.append(label_pipeline(_label))
        processed_text = torch.tensor(text_pipeline(_text), dtype=torch.int64)
        text_list.append(processed_text)
        lengths.append(len(processed_text))
    # 填充文本序列,使它们长度一致
    text_list = pad_sequence(text_list, padding_value=vocab['<pad>'], batch_first=True)
    label_list = torch.tensor(label_list, dtype=torch.float32)
    lengths = torch.tensor(lengths, dtype=torch.int64)
    # 按长度降序排列,便于pack_padded_sequence
    lengths, sorted_idx = lengths.sort(descending=True)
    text_list = text_list[sorted_idx]
    label_list = label_list[sorted_idx]
    return label_list.to(device), text_list.to(device), lengths.to(device)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_iter = IMDB(split='train')
train_loader = DataLoader(list(train_iter), batch_size=32, shuffle=True, collate_fn=collate_batch)

# 3. 定义LSTM模型
class SentimentLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim, n_layers, dropout):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=vocab['<pad>'])
        self.lstm = nn.LSTM(embed_dim, hidden_dim, num_layers=n_layers, 
                            dropout=dropout, batch_first=True, bidirectional=False)
        self.fc = nn.Linear(hidden_dim, output_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, text, text_lengths):
        # text shape: [batch_size, seq_len]
        embedded = self.dropout(self.embedding(text))  # [batch_size, seq_len, embed_dim]

        # 打包序列,避免对填充部分进行计算
        packed_embedded = pack_padded_sequence(embedded, text_lengths.cpu(), batch_first=True, enforce_sorted=True)
        packed_output, (hidden, cell) = self.lstm(packed_embedded)
        # 解包(此处不需要输出序列,我们只取最后一个隐藏状态)
        # output, output_lengths = pad_packed_sequence(packed_output, batch_first=True)

        # 取最后一个时间步的隐藏状态
        hidden = self.dropout(hidden[-1, :, :])  # [batch_size, hidden_dim]
        return self.fc(hidden)

# 超参数
VOCAB_SIZE = len(vocab)
EMBED_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1  # 二分类输出一个标量,用Sigmoid激活
N_LAYERS = 2
DROPOUT = 0.5

model = SentimentLSTM(VOCAB_SIZE, EMBED_DIM, HIDDEN_DIM, OUTPUT_DIM, N_LAYERS, DROPOUT).to(device)
criterion = nn.BCEWithLogitsLoss()  # 二分类交叉熵损失(内部含Sigmoid)
optimizer = optim.Adam(model.parameters())

# 4. 训练(简化版,仅展示一个epoch)
model.train()
for labels, texts, lengths in train_loader:
    optimizer.zero_grad()
    predictions = model(texts, lengths).squeeze(1)
    loss = criterion(predictions, labels)
    loss.backward()
    optimizer.step()
    print(f'Batch Loss: {loss.item():.4f}')
    break  # 仅演示一个batch

print("模型结构定义和单批次训练完成。在实际项目中,你需要完整的训练循环和验证集。")

这个例子展示了处理NLP任务的标准流程:分词、构建词表、词嵌入、用LSTM处理序列、最后用全连接层分类。注意我们使用了pack_padded_sequence,这是处理变长序列、提升RNN/LSTM计算效率的关键技巧。LSTM在序列建模上取得了巨大成功,但其顺序计算的特性(必须等t-1时刻算完才能算t时刻)限制了训练速度。此外,尽管LSTM缓解了长期依赖问题,但对于非常长的序列(如数百上千个时间步),信息传递依然会衰减。这催生了下一代的序列模型——Transformer。

5. 新时代的霸主:Transformer与自注意力机制

5.1 抛弃循环:自注意力如何工作

Transformer模型在2017年由论文《Attention Is All You Need》提出,它完全摒弃了CNN和RNN的结构,仅依赖注意力机制来建立输入序列中任意两个位置之间的依赖关系。它的出现彻底改变了NLP领域,催生了BERT、GPT等划时代的模型。

Transformer的核心是自注意力机制。想象一下你在读一段话时,为了理解某个词的含义,你会自动地给予句中其他词不同的关注度。自注意力机制做的就是这件事,但是以并行的、量化的方式。对于序列中的每个词,自注意力层会计算它与序列中所有词(包括它自己)的“关联分数”,然后根据这些分数对所有词的表示进行加权求和,得到该词新的、包含了全局上下文信息的表示。

这个过程有三个核心步骤:

  1. 查询(Query)、键(Key)、值(Value):每个输入词向量通过三个不同的线性变换,生成Q、K、V三个向量。
  2. 计算注意力分数:用当前词的Q去乘所有词的K,得到一个分数,表示当前词与每个词的关联程度。
  3. 加权求和:将分数通过Softmax归一化为权重,然后用这些权重对V向量进行加权求和,得到输出。

多头注意力则进一步增强了模型的能力。它并行地进行多次上述的自注意力计算(每个“头”关注不同的方面),然后将结果拼接起来。这就像让多个专家从不同角度分析同一段文本,最后综合他们的意见。

5.2 实战:用Transformer编码器进行文本分类

虽然完整的Transformer包含编码器和解码器,但很多任务(如文本分类)只需要编码器部分。下面我们用PyTorch自带的nn.TransformerEncoder来构建一个简单的分类模型。

import torch
import torch.nn as nn
import torch.optim as optim
import math

class TransformerClassifier(nn.Module):
    def __init__(self, vocab_size, embed_dim, num_heads, hidden_dim, num_layers, num_classes, max_len=512, dropout=0.1):
        super(TransformerClassifier, self).__init__()
        self.embed_dim = embed_dim
        
        # 1. 词嵌入层
        self.token_embedding = nn.Embedding(vocab_size, embed_dim)
        # 2. 位置编码:给模型注入序列顺序信息(因为自注意力本身没有位置概念)
        self.position_embedding = nn.Embedding(max_len, embed_dim)
        
        # 3. Transformer编码器层
        encoder_layer = nn.TransformerEncoderLayer(d_model=embed_dim, nhead=num_heads, 
                                                   dim_feedforward=hidden_dim, dropout=dropout, 
                                                   batch_first=True) # 使用batch_first更直观
        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        
        # 4. 分类头
        self.fc_out = nn.Linear(embed_dim, num_classes)
        self.dropout = nn.Dropout(dropout)
        
        # 5. 初始化
        self.init_weights()
        
    def init_weights(self):
        initrange = 0.1
        self.token_embedding.weight.data.uniform_(-initrange, initrange)
        self.fc_out.bias.data.zero_()
        self.fc_out.weight.data.uniform_(-initrange, initrange)
        
    def forward(self, src, src_key_padding_mask=None):
        """
        src: 输入序列,形状 [batch_size, seq_len]
        src_key_padding_mask: 用于屏蔽填充位置,形状 [batch_size, seq_len]
        """
        batch_size, seq_len = src.size()
        
        # 创建位置索引 [1, seq_len] -> [batch_size, seq_len]
        positions = torch.arange(0, seq_len).unsqueeze(0).expand(batch_size, seq_len).to(src.device)
        
        # 词嵌入 + 位置编码
        src_emb = self.token_embedding(src) * math.sqrt(self.embed_dim)  # 缩放嵌入
        src_emb = src_emb + self.position_embedding(positions)
        src_emb = self.dropout(src_emb)
        
        # 通过Transformer编码器
        # Transformer期望输入形状: [seq_len, batch_size, embed_dim] 当 batch_first=False 时
        # 但我们设置了batch_first=True,所以输入输出都是 [batch_size, seq_len, embed_dim]
        memory = self.transformer_encoder(src_emb, src_key_padding_mask=src_key_padding_mask)
        
        # 取序列第一个位置的输出(通常对应[CLS] token)用于分类,或做全局平均池化
        # 这里我们使用平均池化
        pooled = memory.mean(dim=1)  # [batch_size, embed_dim]
        
        # 分类
        output = self.fc_out(pooled)  # [batch_size, num_classes]
        return output

# 模拟数据,演示模型流程
VOCAB_SIZE = 10000
EMBED_DIM = 128
NUM_HEADS = 8
HIDDEN_DIM = 512
NUM_LAYERS = 3
NUM_CLASSES = 2
MAX_LEN = 128
BATCH_SIZE = 16

model = TransformerClassifier(VOCAB_SIZE, EMBED_DIM, NUM_HEADS, HIDDEN_DIM, NUM_LAYERS, NUM_CLASSES, MAX_LEN)
print(model)

# 创建模拟输入
dummy_src = torch.randint(0, VOCAB_SIZE, (BATCH_SIZE, 50))  # 假设序列长度为50
# 创建模拟的padding mask(假设后10个位置是填充的)
dummy_mask = torch.zeros(BATCH_SIZE, 50, dtype=torch.bool)
dummy_mask[:, 40:] = True  # 后10个位置为True,表示需要被mask

output = model(dummy_src, src_key_padding_mask=dummy_mask)
print(f"输入形状: {dummy_src.shape}")
print(f"输出形状: {output.shape}")  # 应为 [16, 2]

# 定义损失和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.0001)

# 模拟训练一步
dummy_labels = torch.randint(0, NUM_CLASSES, (BATCH_SIZE,))
optimizer.zero_grad()
loss = criterion(output, dummy_labels)
loss.backward()
optimizer.step()
print(f"模拟训练一步,损失: {loss.item():.4f}")

这个示例展示了Transformer编码器的基本用法。在实际应用中,比如BERT,就是在多层Transformer编码器之上添加了特定的预训练任务(如掩码语言模型)。Transformer的最大优势在于并行计算强大的长距离依赖建模能力。自注意力机制让序列中任意两个位置都能直接交互,不受距离限制,这比RNN的逐步传递高效得多。当然,它的计算复杂度与序列长度的平方成正比,对于超长序列是个挑战,后续也出现了如Longformer、BigBird等改进模型。

6. 算法选择指南:从问题出发,而非模型炫技

学完了这四大核心算法,你可能会问:我到底该用哪个?答案是:从你的数据和任务出发,而不是追求最时髦的模型。下面这张对比表可以帮你快速决策:

算法核心结构擅长数据类型典型应用场景优点缺点/挑战
MLP全连接层结构化数据(表格、向量)风险评估、房价预测、简单分类/回归结构简单,易于实现和理解参数多,无法处理空间/序列结构,易过拟合
CNN卷积层、池化层网格状数据(图像、视频、音频频谱图)图像分类、目标检测、人脸识别、医学影像分析参数共享,平移不变性,能有效提取局部特征对序列的长期依赖建模能力弱
RNN/LSTM循环单元,带门控序列数据(文本、语音、时间序列)机器翻译、文本生成、语音识别、股票预测天然适合序列,能处理变长输入训练慢(无法并行),长序列梯度问题(LSTM缓解)
Transformer自注意力层、前馈网络序列数据(尤其长文本)、也开始用于图像(ViT)机器翻译、文本摘要、BERT/GPT等预训练模型、图像分类(ViT)并行计算效率高,长距离依赖建模能力强计算复杂度O(n²),对短序列可能过参数化,需要大量数据

我的个人经验是,对于新手项目,可以遵循这个路径:

  1. 从MLP开始:如果你的数据是规整的表格数据,先用MLP或更简单的模型(如XGBoost)建立基线。这能帮你快速理解问题。
  2. 图像任务必选CNN:只要是处理图像,CNN永远是第一选择。可以从ResNet、EfficientNet等预训练模型开始微调,这是最快的成功路径。
  3. 序列任务先试LSTM/GRU:对于文本分类、情感分析、时间序列预测,LSTM仍然是非常可靠且易于理解的选择。它比Transformer需要的数据量少,更容易在小数据集上训练。
  4. 追求SOTA或处理长文本时用Transformer:当你有充足的数据和计算资源,并且任务需要极强的上下文理解能力(如问答、复杂翻译)时,再考虑基于Transformer的模型(如BERT、T5)。对于超长序列,可以关注其改进模型。

最后,别忘了实践出真知。多跑代码,多观察模型在验证集上的表现,学会使用TensorBoard或Weights & Biases这样的工具可视化训练过程。理解损失曲线的变化、关注过拟合和欠拟合的迹象,比死记硬背模型结构更重要。每一个成功的模型背后,都离不开对数据的深刻理解、反复的调优尝试以及大量的耐心。

更多推荐