目标检测 = 分类+定位,与单纯图像分类区分开:

  • 分类:整张图只输出一个类别
  • 检测:找出图中所有目标(sigmoid),同时输出:框坐标(x,y,w/h)+类别+置信度。

基础概念:

  • 边界框(Bounding Box): 标注目标的矩形框,主流格式(x1,y1,x2,y2)或(cx,cy,w,h)
  • IOU: 两个框的交集/并集,用来判断框预测得准不准,区分正负样本
  • NMS 非极大值抑制:去掉同一个目标上重复得预测框,只保留最优框

一、分类

1.1 任务定义:一张图像只能属于其中一类(猫 / 狗 / 鸟三选一,互斥),最终输出 1 个类别。

1.2 网络结构:

输入图片-> 卷积/Transformer特征提取->全局池化->全连接(分类头)->输出【logits向量】
假设 3 分类,最后全连接层输出长度 = 3:
logits = [2.5, 0.8, -1.2]
 

1.3 网络解释

输入图片(原始数据),图片只是一堆像素RGB数值,像素本身没法直接区分猫狗,原始像素是底层信息,没有“语义”,网络的目标是:把像素->编程有区分能力的特征向量。

1.3.1 卷积CNN:
  • 浅层事变边缘、线条、色块;
  • 中层识别纹理、圆弧、局部形状
  • 深层组合局部特征,学习高级语义特征
     
1.3.2 Transformer

把图片切成一个个小块patch,通过自注意力机制,建立图像各个区域之间的关联

比如:看到“耳朵”,关联“脑袋”,推理出这是猫

1.3.3 全局池化

经过卷积后得到的是[通道数,高,宽] 特征图,我们想要的是固定长度的一维向量,将这组一维向量接入全连接层,中间可以使用池化。

  • 全局平均池化GAP:每个通道所有空间像素求平均值
  • 全局最大池化GMP:每个通道所有空间像素取最大值

作用:去掉空间位置信息,将二维特征图压缩成一维特征向量,例如[512,7,7]->GAP->长度512的特征向量,这个向量,就是整张图的全局语义表征,向量里每个数值,代表某一种图像模式的强弱。向量相当于图片的“数字指纹”。

1.3.4 全连接(分类头)

输入:全局池化得到的图像特征指纹(比如512维向量)

输出:logits向量,维度=类别总数

logits = W.feature + b

W:权重矩阵,b: 偏置,是网络训练学到的参数

网络学习一套权重,用来衡量:当前图片特征和各类别标准模式的匹配得分,匹配度越高-》logits数值越大

1.3.5 后续推理

logits------(softmax)---->各类别概率

概率------(argmax)----->最大概率下标---->唯一类别。

1.4 训练时发生了什么(为什么网络能自动学会)

  • 输入图片,算出预测类别
  • 和真实标签对比,用交叉熵计算误差(损失loss)
  • 反向传播,逐层调整卷积、全连接的权重
  • 反复迭代,不断减小预测误差。最终权重收敛,能够准确提取特征,区分不同物体。

交叉熵损失:Loss = -ln(p), p越大,-ln(p)越小,loss越小

假设:模型输出logits = [z0,z1,z2,p3], 经过softmax : softmax(p0) = (e^z0)/(e^z0+e^z1+e^z2+e^z3)

Loss = -ln(p0) 

z0越大---》p0越大----》ln(p0)越小  -----》Loss越小

训练闭环:反向传播如何修改权重实现这个目标

完整链条:

  1. 前向传播:输入图片 → 算出特征→ 算出 logits
  2. 计算 Loss(误差)
  3. 反向传播:求梯度 算出:增大 / 减小每一个权重 W,会让 Loss 变大还是变小
  4. 优化器 (SGD/Adam):沿着降低 Loss的方向更新所有参数(卷积权重 + 分类头 W/b)

落到分类头 W 上理解

矩阵W可以理解成储存着每一类的模板向量

  • W0:类别 0 模板
  • W1:类别 1 模板

Z0=W0.f+b0,本质是特征向量 f 和类别 0 模板的相似度得分

训练时:图片真实标签是类别 0 梯度信号会引导网络做两件事:

  1. 更新W0:让W0尽量和 “猫图片特征 f” 方向靠近 → 内积变大 →Z0变大
  2. 更新W1,W2,W3:让它们和 “猫特征 f” 方向远离 →Z1,Z2,Z3变小

同时,前面卷积 / Transformer 特征提取层也会同步更新: 网络改造特征提取能力,猫图片提取出来的特征 f,越来越靠近猫模板W0

举一个极简数字例子

3 分类:猫 (0)、狗 (1)、鸟 (2);图片是猫。

训练初期(权重随机)

logits =[1.0,2.2,0.5], argmax 判定成狗(预测错误),Loss 很大。

反向传播更新全部权重: 提升类别 0 得分,压制类别 1 得分。

更新一轮后

logits =[3.1,1.4,0.6]现在Z0最大,预测正确,Loss 明显下降。

多次迭代持续重复这个修正逻辑: 真实类别的 logits 持续拉大优势,错误类别持续被压低。

目标分类代码示例

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

# ===================== 1. 数据预处理 =====================
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])

# 加载CIFAR10数据集
train_dataset = datasets.CIFAR10(
    root="./data", train=True, download=True, transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)

# 类别名称
class_names = [
    "飞机", "汽车", "鸟", "猫", "鹿",
    "狗", "青蛙", "马", "船", "卡车"
]

# ===================== 2. 搭建我们讨论的网络 =====================
class SimpleCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # 【特征提取:卷积层】
        self.feature_extractor = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1),
            nn.ReLU()
        )
        # 【全局平均池化 GAP】把特征图 [B,128,H,W] -> [B,128]
        self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
        # 【分类头:全连接,输出logits】
        self.classifier = nn.Linear(128, num_classes)

    def forward(self, x):
        # x: [batch, 3, 32, 32] 图片
        feat_map = self.feature_extractor(x)    # 卷积提取特征图
        feat = self.global_pool(feat_map)       # 全局池化
        feat = torch.flatten(feat, 1)           # 摊平成一维特征向量
        logits = self.classifier(feat)          # 输出logits!没有softmax
        return logits

# ===================== 3. 初始化模型、损失、优化器 =====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)

# 单标签分类:交叉熵损失!输入必须是logits,内部自带softmax
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# ===================== 4. 训练循环(完整迭代闭环) =====================
print("开始训练...")
model.train()
for epoch in range(2):  # 先跑2轮感受过程
    total_loss = 0.0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)

        # -------- 前向传播 --------
        logits = model(images)   # 模型输出原始logits
        loss = criterion(logits, labels)  # 计算误差

        # -------- 反向传播 + 更新权重 --------
        optimizer.zero_grad()    # 清空梯度
        loss.backward()          # 反向传播求梯度
        optimizer.step()          # 更新所有卷积、全连接权重

        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)
    print(f"第{epoch+1}轮, 平均Loss:{avg_loss:.4f}")

# ===================== 5. 推理预测(真实预测流程) =====================
print("\n===== 推理测试 =====")
model.eval()
with torch.no_grad():  # 推理不需要计算梯度,加速
    # 取一张图片
    img, label = next(iter(train_loader))
    img = img[0:1].to(device)  # 只拿第1张
    true_label = label[0].item()

    logits = model(img)
    # logits -> softmax概率
    probs = torch.softmax(logits, dim=1)
    max_prob, pred_idx = torch.max(probs, dim=1)

    print(f"真实类别:{class_names[true_label]}")
    print(f"预测类别:{class_names[pred_idx.item()]}")
    print(f"预测置信度:{max_prob.item():.3f}")
    arr = logits.detach().cpu().numpy()[0]
    print(f"输出Logits向量:\n{arr.round(2)}")

二、检测

目标检测网络需要同时预测3样东西:

  • 物体坐标: x, y, w,h (回归任务)
  • 目标置信度:有无物体(obj_conf)
  • 类别logits: 框内物体分类

为了代码不至于过于庞大,我们先实现简化版单尺度检测器(类 YOLO 思想)

假设:把图片划分为 \(S\times S\) 网格,每个网格预测 1 个物体。

输出头通道数计算董事:

\mathit{output\_dim}= \textrm{4}(\textrm{zuobiao}) + 1(zhixindu) + num\_classes

举例:S=7 网格,20 分类 最终输出 shape:[batch, 7,7, 4+1+20]

2.1 手写极简目标检测器框架

骨架手写!骨干 CNN 沿用你 CIFAR10 那套卷积,自己写检测头,没有封装好的 YOLO 库。

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

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# ====================== 1. 手写网络:CNN骨干 + 自制检测头 ======================
class SimpleDetector(nn.Module):
    def __init__(self, S=7, num_classes=10):
        super().__init__()
        self.S = S
        self.num_classes = num_classes
        # 4(xywh) + 1(obj置信度) + num_classes
        self.out_channel = 4 + 1 + num_classes

        # 骨干特征提取(复用你CIFAR10的卷积结构!)
        self.backbone = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(32, 64, 3, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(64, 128, 3, stride=2, padding=1),
            nn.ReLU(),
        )

        # 检测头:输出 S×S 网格预测
        self.head = nn.Sequential(
            nn.Conv2d(128, 256, 3, padding=1),
            nn.ReLU(),
            nn.Conv2d(256, self.out_channel, 3, padding=1)
        )

    def forward(self, x):
        # x: [B,3,H,W]
        feat = self.backbone(x)
        pred = self.head(feat)  # [B, out_channel, Hf, Wf]

        # 变换维度:B,C,H,W → B,H,W,C (YOLO习惯格式)
        pred = pred.permute(0, 2, 3, 1)
        return pred

# ====================== 2. 模拟前向传播,看懂输出shape ======================
if __name__ == "__main__":
    model = SimpleDetector(S=7, num_classes=10).to(device)
    # 模拟一批图片:batch=2,3通道,32×32(CIFAR尺寸)
    dummy_img = torch.randn(2, 3, 32, 32).to(device)

    out = model(dummy_img)
    print("网络输出shape:", out.shape)
    # 输出:[2, 8, 8, 15]
    # 解析:batch=2, 特征图8×8网格,每个网格预测 4+1+10=15个值

    # 拆分每个网格预测内容(核心!对应检测三大任务)
    # out[b, gy, gx, 0:4]  → xywh 边框
    # out[b, gy, gx, 4]   → 目标置信度(是否存在物体)
    # out[b, gy, gx, 5:]  → 类别logits(和图像分类一模一样)

1)对比图像分类网络

  • 分类:卷积 → GAP → 一维向量 → FC 输出类别 logits
  • 简易检测器:卷积 → 不全局池化!保留空间网格 → 卷积检测头输出每个网格预测

⚠️最重要区别:目标检测不能随便全局池化!全局池化会抹除位置信息,而检测需要预测坐标!

2)各个分支激活函数怎么选(极易踩坑)
  1. xywh 坐标:连续回归值 → 不加 softmax,因为softmax是用来做概率的,比如z0/(z0+z1+..+zn), 可以选择性添加sigmoid,通常 sigmoid 约束到 0~1(相对网格坐标)
  2. obj 置信度:0~1 有无物体 → 使用 Sigmoid
  3. 类别 Logits
    • 原版 YOLO:每个网格内单物体,Softmax 分类;
    • 现代 YOLO:Sigmoid 多类别
3)损失不再单一交叉熵!手写检测需要组合 3 种 loss
总Loss = 坐标回归Loss(L1/MSE) + 置信度Loss(BCE) + 类别分类Loss(交叉熵)
4)手写目标检测会遇到的 4 大难点
  1. 标签编码 原图真实框 → 映射到对应网格、转换成相对网格的 xywh,手写很繁琐。
  2. 正负样本匹配 哪些网格负责预测真实物体?大量网格是空背景(YOLO 系列不断迭代标签分配策略)
  3. NMS 非极大值抑制(后处理) 网络会输出大量重叠框,推理阶段需要自己写 NMS 过滤冗余框。
  4. 尺度问题 单层网格只能适配中等大小物体;想要检测大小物体,必须手写多尺度特征融合(类似 YOLO Neck)
5)循序渐进学习路线
  1. 先运行上面 Demo,看懂张量维度、如何拆分【框 / 置信度 / 类别】
  2. 手写损失函数,完成完整前向 + 反向传播闭环
  3. 手写简易 NMS 后处理,实现推理可视化
  4. 理解短板:单尺度、无锚框缺陷
  5. 再去看 ultralytics YOLO 源码,你就能看懂框架内部在干什么,不再黑盒
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐