李宏毅深度学习课程实战:从零搭建PyTorch神经网络(附作业解析)

深度学习作为人工智能领域的核心技术,正在重塑各行各业的智能化进程。对于已经掌握Python编程基础但尚未深入实践深度学习框架的学习者来说,如何将理论知识转化为实际代码能力是一个关键挑战。本文将以李宏毅教授的深度学习课程为理论基础,结合PyTorch框架,手把手带你完成从数据预处理到模型训练的全流程实战。

1. 深度学习开发环境搭建与PyTorch入门

在开始构建神经网络之前,我们需要先配置好开发环境。PyTorch作为当前最受欢迎的深度学习框架之一,以其动态计算图和Pythonic的编程风格深受研究者喜爱。

1.1 PyTorch安装与基础操作

首先确保你的Python版本在3.7以上,然后通过pip安装PyTorch:

pip install torch torchvision torchaudio

验证安装是否成功:

import torch
print(torch.__version__)  # 应输出如1.12.1的版本号
print(torch.cuda.is_available())  # 检查GPU是否可用

PyTorch的核心数据结构是Tensor,它与Numpy数组类似但支持GPU加速:

# 创建Tensor的多种方式
x = torch.empty(5, 3)  # 未初始化矩阵
y = torch.rand(5, 3)   # 随机初始化矩阵
z = torch.zeros(5, 3, dtype=torch.long)  # 全零矩阵,指定类型

# Tensor运算
result = torch.add(y, z)  # 加法
result = y + z           # 等价写法

1.2 数据加载与预处理

PyTorch提供了torch.utils.data.DatasetDataLoader来高效处理数据。以宝可梦分类任务为例:

from torchvision import transforms

# 定义图像预处理流程
transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                         std=[0.229, 0.224, 0.225])
])

# 自定义Dataset类
class PokemonDataset(torch.utils.data.Dataset):
    def __init__(self, root_dir, transform=None):
        self.root_dir = root_dir
        self.transform = transform
        # 这里添加读取图片和标签的逻辑
    
    def __len__(self):
        return len(self.image_paths)
    
    def __getitem__(self, idx):
        image = Image.open(self.image_paths[idx])
        label = self.labels[idx]
        if self.transform:
            image = self.transform(image)
        return image, label

2. 神经网络模型构建实战

理解了PyTorch基础后,我们可以开始构建完整的神经网络模型。我们将从简单的全连接网络开始,逐步过渡到更复杂的结构。

2.1 全连接神经网络实现

以宝可梦属性预测为例,实现一个三层的全连接网络:

import torch.nn as nn
import torch.nn.functional as F

class PokemonNet(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(PokemonNet, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, output_size)
        self.dropout = nn.Dropout(0.2)  # 防止过拟合
        
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

2.2 卷积神经网络实现

对于图像分类任务,卷积神经网络(CNN)通常表现更好。以下是基于李宏毅课程中CNN理论的PyTorch实现:

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)  # 输入通道3,输出32
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(64 * 56 * 56, 512)
        self.fc2 = nn.Linear(512, 10)  # 假设有10类宝可梦
        self.dropout = nn.Dropout(0.25)
        
    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 64 * 56 * 56)  # 展平
        x = self.dropout(x)
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = self.fc2(x)
        return x

3. 模型训练与优化技巧

构建好模型后,训练过程同样至关重要。我们将深入探讨损失函数选择、优化器配置等关键环节。

3.1 损失函数与优化器选择

根据任务类型选择合适的损失函数:

任务类型推荐损失函数适用场景
分类任务CrossEntropyLoss多类别分类
二分类任务BCELoss二分类(需配合sigmoid)
回归任务MSELoss数值预测
多标签分类BCEWithLogitsLoss每个样本可能属于多个类别

优化器配置示例:

model = PokemonNet(input_size=100, hidden_size=128, output_size=10)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 学习率调度器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)

3.2 训练循环实现

完整的训练循环包含前向传播、损失计算、反向传播和参数更新:

def train(model, train_loader, criterion, optimizer, epochs=10):
    model.train()
    for epoch in range(epochs):
        running_loss = 0.0
        for i, (inputs, labels) in enumerate(train_loader):
            optimizer.zero_grad()
            
            outputs = model(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
            
            running_loss += loss.item()
            if i % 100 == 99:
                print(f'Epoch {epoch+1}, Batch {i+1}: loss {running_loss/100:.3f}')
                running_loss = 0.0
        scheduler.step()

4. 模型评估与调优策略

训练完成后,我们需要评估模型性能并针对性地进行优化。

4.1 评估指标与实现

常用的评估指标包括准确率、精确率、召回率等:

def evaluate(model, test_loader):
    model.eval()
    correct = 0
    total = 0
    with torch.no_grad():
        for inputs, labels in test_loader:
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    
    accuracy = 100 * correct / total
    print(f'Test Accuracy: {accuracy:.2f}%')
    return accuracy

4.2 常见问题与解决方案

在实际训练中常遇到的问题及对策:

  • 过拟合

    • 增加Dropout层
    • 使用L2正则化(weight decay)
    • 数据增强
    • 早停(Early Stopping)
  • 欠拟合

    • 增加模型复杂度
    • 减少正则化
    • 延长训练时间
  • 梯度消失/爆炸

    • 使用Batch Normalization
    • 合适的权重初始化
    • 梯度裁剪
# Batch Normalization示例
class BNNet(nn.Module):
    def __init__(self):
        super(BNNet, self).__init__()
        self.fc1 = nn.Linear(100, 256)
        self.bn1 = nn.BatchNorm1d(256)
        self.fc2 = nn.Linear(256, 10)
        
    def forward(self, x):
        x = F.relu(self.bn1(self.fc1(x)))
        x = self.fc2(x)
        return x

5. 作业解析与实战技巧

结合李宏毅课程作业,分享几个关键实战技巧和常见错误。

5.1 宝可梦分类作业解析

作业中常见的几个技术要点:

  1. 数据不平衡处理

    # 使用加权交叉熵损失
    class_weights = torch.tensor([1.0, 2.0, 1.5])  # 根据类别分布设置
    criterion = nn.CrossEntropyLoss(weight=class_weights)
    
  2. 学习率寻找技巧

    # 学习率范围测试
    lr_finder = LRFinder(model, optimizer, criterion)
    lr_finder.range_test(train_loader, end_lr=10, num_iter=100)
    lr_finder.plot()  # 找到损失下降最快的区间
    
  3. 模型集成提升性能

    # 多个模型的预测结果平均
    outputs = (model1(inputs) + model2(inputs)) / 2
    

5.2 视频点击量预测作业解析

回归任务中的关键点:

  1. 特征工程

    # 添加多项式特征
    from sklearn.preprocessing import PolynomialFeatures
    poly = PolynomialFeatures(degree=2, include_bias=False)
    X_poly = poly.fit_transform(X)
    
  2. 自定义损失函数

    def weighted_mse_loss(input, target):
        weight = torch.abs(target - input) + 1  # 给大误差更大权重
        return (weight * (input - target) ** 2).mean()
    
  3. 超参数搜索

    # 使用Optuna进行超参数优化
    import optuna
    
    def objective(trial):
        lr = trial.suggest_float('lr', 1e-5, 1e-2, log=True)
        hidden_size = trial.suggest_categorical('hidden_size', [64, 128, 256])
        
        model = Net(input_size, hidden_size, output_size)
        optimizer = torch.optim.Adam(model.parameters(), lr=lr)
        
        # 训练和验证代码
        return validation_accuracy
    

6. 进阶技巧与最新进展

掌握了基础后,我们可以探索更高级的技术和最新研究进展。

6.1 迁移学习实践

使用预训练模型可以大幅提升小数据集上的表现:

from torchvision import models

# 加载预训练ResNet
model = models.resnet18(pretrained=True)

# 替换最后一层
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, len(class_names))

# 只训练最后一层
for param in model.parameters():
    param.requires_grad = False
for param in model.fc.parameters():
    param.requires_grad = True

6.2 自注意力机制实现

自注意力机制是Transformer的核心组件,也可以用于CV任务:

class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size
        self.heads = heads
        self.head_dim = embed_size // heads
        
        self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)
        
    def forward(self, values, keys, query, mask):
        N = query.shape[0]
        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]
        
        # 拆分多头
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        queries = query.reshape(N, query_len, self.heads, self.head_dim)
        
        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))
            
        attention = torch.softmax(energy / (self.embed_size ** (1/2)), dim=3)
        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )
        
        out = self.fc_out(out)
        return out

6.3 模型解释性技术

理解模型决策过程同样重要,常用的可视化技术:

# Grad-CAM可视化
from torchcam.methods import GradCAM

cam_extractor = GradCAM(model, 'layer4')
with torch.no_grad():
    out = model(input_tensor)
    
# 生成类别激活图
activation_map = cam_extractor(out.squeeze(0).argmax().item(), out)

深度学习实践是一个不断迭代和积累经验的过程。从理论到代码的转化能力是AI工程师的核心竞争力,建议在学习过程中多动手实践,尝试复现论文结果,参与Kaggle比赛等实战项目。遇到问题时,PyTorch官方论坛和GitHub issue区是很好的求助资源。

更多推荐