深度学习基础回顾:张量操作、自动求导机制、卷积神经网络(CNN)核心概念、感受野与特征图、PyTorch构建简单CNN

各位同学,欢迎来到第二章。说实话,每次开新课讲到这部分,我都有点感慨。很多同学一上来就急着调YOLO,结果连张量是啥都搞不清楚,跑通了也不知道为什么。嗯,今天咱们就把地基打牢。

我个人习惯是,不管多忙,每接触一个新框架,先花半小时把张量操作和自动求导摸一遍。这半小时,能帮你省下后面几十个小时的debug时间。你想想看,是不是这个道理?

2.1 张量操作:深度学习的基本单元

张量,说白了就是多维数组。0维是标量,1维是向量,2维是矩阵,3维以上统称张量。在PyTorch里,一切数据都是张量。

我在项目中遇到过最典型的坑:图像数据读进来是H×W×C(高度、宽度、通道),但PyTorch要求C×H×W。很多人直接送进网络,报错半天找不到原因。后来我养成了一个习惯——拿到数据先打印shape。

核心操作速查:

  • torch.tensor():创建张量
  • .shape.size():查看形状
  • .reshape().view():改变形状
  • .permute():维度重排(比如HWC→CHW)
  • torch.cat():拼接张量
import torch

# 创建一个3x4的随机张量
x = torch.randn(3, 4)
print(x.shape)  # torch.Size([3, 4])

# 改变形状为2x6
y = x.reshape(2, 6)
print(y.shape)  # torch.Size([2, 6])

# 维度重排:假设是图像数据 HWC -> CHW
img_hwc = torch.randn(224, 224, 3)
img_chw = img_hwc.permute(2, 0, 1)
print(img_chw.shape)  # torch.Size([3, 224, 224])

我的小技巧:调试时多用 .shape 打印形状。我曾经因为一个维度没对齐,整整查了两小时。从那以后,每个关键操作后面我都加一句 print(x.shape)

2.2 自动求导机制:PyTorch的灵魂

为什么PyTorch这么火?自动求导功不可没。你只需要定义好前向计算,反向传播的梯度它会自动帮你算。

核心就两个东西:requires_gradbackward()。你设置某个张量需要梯度,PyTorch就会记录所有对它的操作,形成一个计算图。然后你调用 .backward(),它就从输出往输入反向传播梯度。

# 一个简单的例子:y = x^2,求dy/dx
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2
y.backward()
print(x.grad)  # tensor(6.0) 因为 dy/dx = 2x = 6

你想想看,如果让你手动推导一个50层网络的梯度,是不是头皮发麻?自动求导就是帮你干这个苦力活的。

注意:每次反向传播后,梯度会累积。如果你在循环里训练,记得调用 optimizer.zero_grad() 清空梯度。我曾经忘了这步,结果loss越跑越大,还以为模型坏了。

2.3 卷积神经网络(CNN)核心概念

CNN的核心就三件事:卷积、池化、激活。说白了,卷积负责提取特征,池化负责压缩信息,激活负责引入非线性。

卷积层:用一个小窗口(卷积核)在图像上滑动,每个位置做点积。不同的卷积核提取不同的特征——有的检测边缘,有的检测纹理,有的检测形状。

池化层:最常见的是最大池化,取窗口内的最大值。它的作用是降维,减少计算量,同时让模型对微小位移不那么敏感。

激活函数:ReLU(线性整流单元)是标配。公式很简单:f(x) = max(0, x)。它解决了梯度消失问题,让深层网络也能训练。

一个典型的CNN结构:

输入图像 → 卷积 → ReLU → 池化 → 卷积 → ReLU → 池化 → 全连接层 → 输出

我在项目中遇到过一个问题:网络层数越深,参数量越大,但效果反而变差。后来发现是梯度消失了。解决办法就是加残差连接(ResNet的思路),这个后面YOLO部分会详细讲。

2.4 感受野与特征图

感受野,简单说就是特征图上的一个点,对应到原始图像上的区域有多大。你想想看,浅层网络的感受野小,只能看到局部细节;深层网络的感受野大,能看到全局语义。

举个例子:一个3×3的卷积核,感受野就是3×3。如果再叠一个3×3的卷积,感受野就变成了5×5。所以网络越深,感受野越大。

网络层数 卷积核大小 感受野大小
第1层 3×3 3×3
第2层 3×3 5×5
第3层 3×3 7×7

特征图就是卷积后的输出。它的尺寸由输入尺寸、卷积核大小、步长、填充共同决定。公式很简单:

输出尺寸 = (输入尺寸 - 卷积核大小 + 2 * 填充) / 步长 + 1

比如输入224×224,卷积核3×3,步长1,填充1,输出还是224×224。这就是「same卷积」,保持尺寸不变。

避坑指南:我曾经设计一个网络,特征图尺寸越卷越小,到后面只剩1×1了,根本没法做检测。后来我学会了用填充来保持尺寸,或者用步长2来有控制地降采样。

2.5 PyTorch构建简单CNN

好了,理论讲完了,咱们动手写一个。用PyTorch的 nn.Module 来定义网络,这是最标准的方式。

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

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 卷积层:输入通道3(RGB),输出通道16,卷积核3x3
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        # 池化层:2x2最大池化
        self.pool = nn.MaxPool2d(2, 2)
        # 第二个卷积层:输入16,输出32
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        # 全连接层:输入32*56*56,输出10个类别
        self.fc = nn.Linear(32 * 56 * 56, 10)

    def forward(self, x):
        # 卷积 -> 激活 -> 池化
        x = self.pool(F.relu(self.conv1(x)))
        # 再次卷积 -> 激活 -> 池化
        x = self.pool(F.relu(self.conv2(x)))
        # 展平
        x = x.view(-1, 32 * 56 * 56)
        # 全连接层
        x = self.fc(x)
        return x

# 创建模型
model = SimpleCNN()
print(model)

# 测试前向传播
dummy_input = torch.randn(1, 3, 224, 224)
output = model(dummy_input)
print(output.shape)  # torch.Size([1, 10])

这段代码我建议你亲手敲一遍。注意 forward 函数里的数据流:从224×224的RGB图像,经过两次卷积+池化,变成56×56×32的特征图,然后展平成一维向量,最后通过全连接层输出10个类别的分数。

关键点总结:

  • nn.Conv2d:定义卷积层,参数是输入通道、输出通道、卷积核大小
  • nn.MaxPool2d:定义池化层,参数是窗口大小和步长
  • F.relu:激活函数,在forward里调用
  • .view():展平操作,把多维特征图变成一维向量

嗯,到这里,深度学习的基础就回顾完了。你可能觉得这些内容很简单,但相信我——后面YOLO的源码里,张量操作和卷积计算会反复出现。把今天的内容吃透,后面你会轻松很多。

下一章,咱们开始正式进入YOLO的世界,从数据标注讲起。到时候你会发现,今天学的这些,全都能用上。