国科大深度学习期末计算题实战:用Python复现卷积、损失函数与模型设计

备考深度学习课程时,单纯记忆理论概念往往事倍功半。本文将以2020-2023年国科大深度学习期末考题中的计算题为核心,通过Python代码完整复现解题过程,帮助读者在实战中掌握卷积运算、损失函数计算等核心技能。我们将使用TensorFlow和PyTorch双框架实现,并附赠调试技巧和可视化方法。

1. 卷积运算的三重境界:Full/Same/Valid实战

卷积神经网络(CNN)的核心操作在不同填充方式下表现迥异。让我们以2021年考题为例,实现三种卷积模式:

import tensorflow as tf
import numpy as np

# 输入矩阵和卷积核(与2021年考题一致)
input_matrix = np.array([
    [5, 6, 0, 1, 8, 2],
    [0, 9, 8, 4, 6, 5],
    [2, 6, 5, 3, 8, 4],
    [6, 3, 4, 9, 1, 0],
    [7, 5, 9, 1, 6, 7],
    [2, 5, 9, 2, 3, 7]
], dtype=np.float32)

kernel = np.array([
    [0, -1, 1],
    [1, 0, 0],
    [0, -1, 1]
], dtype=np.float32)

# 转换为TensorFlow需要的4D格式 (batch, height, width, channels)
input_tensor = tf.reshape(input_matrix, [1, 6, 6, 1])
kernel_tensor = tf.reshape(kernel, [3, 3, 1, 1])

# Valid卷积实现
valid_conv = tf.nn.conv2d(input_tensor, kernel_tensor, 
                         strides=1, padding='VALID')
valid_relu = tf.nn.relu(valid_conv)

print("Valid卷积结果:\n", valid_conv.numpy().squeeze())
print("ReLU激活后:\n", valid_relu.numpy().squeeze())

# Same卷积实现
same_conv = tf.nn.conv2d(input_tensor, kernel_tensor,
                        strides=1, padding='SAME')
same_relu = tf.nn.relu(same_conv)

print("\nSame卷积结果:\n", same_conv.numpy().squeeze())
print("ReLU激活后:\n", same_relu.numpy().squeeze())

# Full卷积模拟(通过手动补零)
padded_input = tf.pad(input_tensor, [[0,0], [2,2], [2,2], [0,0]])
full_conv = tf.nn.conv2d(padded_input, kernel_tensor,
                        strides=1, padding='VALID')
full_relu = tf.nn.relu(full_conv)

print("\nFull卷积结果:\n", full_conv.numpy().squeeze())
print("ReLU激活后:\n", full_relu.numpy().squeeze())

调试提示:当卷积结果与预期不符时,检查输入数据的维度顺序是否正确。TensorFlow使用NHWC格式,而PyTorch默认使用NCHW格式。

三种卷积方式的差异对比如下:

卷积类型输出尺寸边界处理适用场景
Valid(n-f+1)×(n-f+1)不填充希望输出尺寸缩减时
Samen×n自动填充保持尺寸需要保持空间分辨率
Full(n+f-1)×(n+f-1)充分填充需要完整覆盖所有可能重叠

2. 损失函数计算:从交叉熵到Focal Loss

深度学习模型的优化核心在于损失函数计算。我们复现2020年和2021年考题中的损失函数计算场景。

2.1 多分类交叉熵实战

import torch
import torch.nn as nn
import numpy as np

# 2020年考题数据
def softmax(x):
    e_x = np.exp(x - np.max(x))
    return e_x / e_x.sum()

# 模型A输出
logits_A = np.array([np.log(20), np.log(40), np.log(60), np.log(80)])
probs_A = softmax(logits_A)
target = np.array([0, 0, 0, 1])  # one-hot编码

# 手动计算交叉熵
def cross_entropy(probs, target):
    return -np.sum(target * np.log(probs))

loss_A = cross_entropy(probs_A, target)
print(f"模型A交叉熵: {loss_A:.4f}")

# 模型B输出
logits_B = np.array([np.log(10), np.log(30), np.log(50), np.log(90)])
probs_B = softmax(logits_B)
loss_B = cross_entropy(probs_B, target)
print(f"模型B交叉熵: {loss_B:.4f}")

# 使用PyTorch验证
criterion = nn.CrossEntropyLoss()
loss_A_torch = criterion(torch.tensor([logits_A]), torch.tensor([3]))  # 类别索引3
loss_B_torch = criterion(torch.tensor([logits_B]), torch.tensor([3]))
print(f"PyTorch验证 - 模型A: {loss_A_torch.item():.4f}, 模型B: {loss_B_torch.item():.4f}")

2.2 Focal Loss实现(2021年考题)

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.4, gamma=2, reduction='mean'):
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma
        self.reduction = reduction

    def forward(self, inputs, targets):
        BCE_loss = nn.functional.binary_cross_entropy_with_logits(
            inputs, targets, reduction='none')
        
        pt = torch.exp(-BCE_loss)  # 计算p_t
        focal_loss = self.alpha * (1-pt)**self.gamma * BCE_loss
        
        if self.reduction == 'mean':
            return focal_loss.mean()
        elif self.reduction == 'sum':
            return focal_loss.sum()
        return focal_loss

# 2021年考题数据
targets = torch.tensor([
    [1, 0], [0, 1], [1, 0], [0, 1], [1, 0]
], dtype=torch.float32)

predictions = torch.tensor([
    [0.2, 0.8], [0.3, 0.7], [0.4, 0.6], [0.1, 0.9], [0.5, 0.5]
], dtype=torch.float32)

# 计算常规交叉熵
criterion = nn.BCELoss()
ce_loss = criterion(predictions, targets)
print(f"常规交叉熵损失: {ce_loss.item():.4f}")

# 计算Focal Loss
focal_loss = FocalLoss(alpha=0.4, gamma=2)
fl_loss = focal_loss(predictions, targets)
print(f"Focal Loss: {fl_loss.item():.4f}")

注意:Focal Loss中的alpha参数需要根据类别不平衡比例调整,gamma参数控制难易样本的权重。

3. 模型参数量计算与感受野分析

2022年考题要求计算CNN各层的参数量和感受野。我们通过代码实现自动化计算:

def calculate_parameters(input_shape, kernel_size, filters, stride=1):
    """计算卷积层参数量"""
    # 公式: (kernel_height * kernel_width * input_channels + 1) * filters
    # 假设不考虑偏置时去掉+1
    return kernel_size[0] * kernel_size[1] * input_shape[-1] * filters

def calculate_output_shape(input_shape, kernel_size, stride, padding='valid'):
    """计算输出特征图尺寸"""
    h, w = input_shape[1], input_shape[2]
    kh, kw = kernel_size
    
    if padding.lower() == 'same':
        return (h // stride, w // stride)
    elif padding.lower() == 'valid':
        return ((h - kh) // stride + 1, (w - kw) // stride + 1)
    else:
        raise ValueError("Unsupported padding type")

# 2022年考题网络结构
input_shape = (1, 32, 32, 3)  # 假设输入为32x32 RGB图像

# C1层: 3x3卷积,深度5,stride=1
c1_params = calculate_parameters(input_shape, (3,3), 5)
c1_output = calculate_output_shape(input_shape, (3,3), 1, 'valid')
print(f"C1层 - 参数量: {c1_params}, 输出尺寸: {c1_output[0]}x{c1_output[1]}x5")

# P1层: 2x2均值池化,stride=2
p1_output = (c1_output[0]//2, c1_output[1]//2)
print(f"P1层 - 输出尺寸: {p1_output[0]}x{p1_output[1]}x5")

# C2层: 5x5卷积,深度2,stride=1
c2_input_shape = (1, p1_output[0], p1_output[1], 5)
c2_params = calculate_parameters(c2_input_shape, (5,5), 2)
c2_output = calculate_output_shape(c2_input_shape, (5,5), 1, 'valid')
print(f"C2层 - 参数量: {c2_params}, 输出尺寸: {c2_output[0]}x{c2_output[1]}x2")

# 全连接层参数量计算
fc_input = c2_output[0] * c2_output[1] * 2  # 展平后的维度
fc_output = 10  # 输出10维
fc_params = fc_input * fc_output
print(f"全连接层参数量: {fc_params}")

# 感受野计算
def calculate_receptive_field(layers):
    rf = 1
    stride_product = 1
    for layer in layers:
        if layer['type'] == 'conv':
            rf += (layer['kernel_size'] - 1) * stride_product
            stride_product *= layer['stride']
        elif layer['type'] == 'pool':
            stride_product *= layer['stride']
    return rf

# 定义网络结构
layers = [
    {'type': 'conv', 'kernel_size': 3, 'stride': 1},
    {'type': 'pool', 'stride': 2},
    {'type': 'conv', 'kernel_size': 5, 'stride': 1}
]

rf = calculate_receptive_field(layers)
print(f"F3层的感受野大小: {rf}x{rf}")

4. 设计题代码化:从思路到实现

深度学习考试中的设计题往往考察创新思维和实现能力。我们以2023年图像分割题为例,展示如何将设计方案转化为代码。

4.1 U-Net改进方案实现

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

class DoubleConv(nn.Module):
    """(卷积 => [BN] => ReLU) * 2"""
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.double_conv = nn.Sequential(
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True)
        )

    def forward(self, x):
        return self.double_conv(x)

class AttentionBlock(nn.Module):
    """注意力机制模块"""
    def __init__(self, F_g, F_l, F_int):
        super(AttentionBlock, self).__init__()
        self.W_g = nn.Sequential(
            nn.Conv2d(F_g, F_int, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(F_int)
        )
        
        self.W_x = nn.Sequential(
            nn.Conv2d(F_l, F_int, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(F_int)
        )
        
        self.psi = nn.Sequential(
            nn.Conv2d(F_int, 1, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(1),
            nn.Sigmoid()
        )
        
        self.relu = nn.ReLU(inplace=True)
        
    def forward(self, g, x):
        g1 = self.W_g(g)
        x1 = self.W_x(x)
        psi = self.relu(g1 + x1)
        psi = self.psi(psi)
        return x * psi

class ImprovedUNet(nn.Module):
    def __init__(self, n_channels=3, n_classes=1):
        super(ImprovedUNet, self).__init__()
        
        # 下采样路径
        self.inc = DoubleConv(n_channels, 64)
        self.down1 = nn.MaxPool2d(2)
        self.conv1 = DoubleConv(64, 128)
        self.down2 = nn.MaxPool2d(2)
        self.conv2 = DoubleConv(128, 256)
        self.down3 = nn.MaxPool2d(2)
        self.conv3 = DoubleConv(256, 512)
        self.down4 = nn.MaxPool2d(2)
        self.conv4 = DoubleConv(512, 1024)
        
        # 上采样路径 + 注意力机制
        self.up1 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2)
        self.att1 = AttentionBlock(512, 512, 256)
        self.conv5 = DoubleConv(1024, 512)
        
        self.up2 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2)
        self.att2 = AttentionBlock(256, 256, 128)
        self.conv6 = DoubleConv(512, 256)
        
        self.up3 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2)
        self.att3 = AttentionBlock(128, 128, 64)
        self.conv7 = DoubleConv(256, 128)
        
        self.up4 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2)
        self.att4 = AttentionBlock(64, 64, 32)
        self.conv8 = DoubleConv(128, 64)
        
        self.outc = nn.Conv2d(64, n_classes, kernel_size=1)
        
    def forward(self, x):
        # 下采样
        x1 = self.inc(x)
        x2 = self.conv1(self.down1(x1))
        x3 = self.conv2(self.down2(x2))
        x4 = self.conv3(self.down3(x3))
        x5 = self.conv4(self.down4(x4))
        
        # 上采样+注意力
        u1 = self.up1(x5)
        a1 = self.att1(u1, x4)
        u1 = torch.cat([a1, u1], dim=1)
        u1 = self.conv5(u1)
        
        u2 = self.up2(u1)
        a2 = self.att2(u2, x3)
        u2 = torch.cat([a2, u2], dim=1)
        u2 = self.conv6(u2)
        
        u3 = self.up3(u2)
        a3 = self.att3(u3, x2)
        u3 = torch.cat([a3, u3], dim=1)
        u3 = self.conv7(u3)
        
        u4 = self.up4(u3)
        a4 = self.att4(u4, x1)
        u4 = torch.cat([a4, u4], dim=1)
        u4 = self.conv8(u4)
        
        logits = self.outc(u4)
        return torch.sigmoid(logits)

# 模型测试
model = ImprovedUNet()
input_tensor = torch.randn(1, 3, 256, 256)
output = model(input_tensor)
print(f"输入尺寸: {input_tensor.shape}")
print(f"输出尺寸: {output.shape}")

这个改进版U-Net主要创新点:

  1. 引入注意力机制模块,让网络更关注重要区域
  2. 使用双卷积块保持特征提取能力
  3. 增加网络深度提升特征抽象能力

4.2 模型可视化与调试技巧

import torchviz
from torchsummary import summary

# 生成模型结构图
dot = torchviz.make_dot(model(input_tensor), params=dict(model.named_parameters()))
dot.render("improved_unet", format="png")

# 打印模型摘要
summary(model, input_size=(3, 256, 256))

# 梯度检查工具
def check_gradients(model):
    for name, param in model.named_parameters():
        if param.grad is not None:
            print(f"{name} - 梯度均值: {param.grad.mean().item():.6f}")
        else:
            print(f"{name} - 无梯度")

# 训练过程中可以定期调用检查
# check_gradients(model)

在实际项目中,我发现注意力机制能显著提升小目标分割效果,但会增加约15%的计算量。对于256x256的输入图像,建议batch size设置为8-16以适应大多数GPU内存。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐