目标检测到底在做什么
目标检测 = 分类+定位,与单纯图像分类区分开:
- 分类:整张图只输出一个类别
- 检测:找出图中所有目标(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越小
训练闭环:反向传播如何修改权重实现这个目标
完整链条:
- 前向传播:输入图片 → 算出特征→ 算出 logits
- 计算 Loss(误差)
- 反向传播:求梯度 算出:增大 / 减小每一个权重 W,会让 Loss 变大还是变小
- 优化器 (SGD/Adam):沿着降低 Loss的方向更新所有参数(卷积权重 + 分类头 W/b)
落到分类头 W 上理解
矩阵W可以理解成储存着每一类的模板向量。
- W0:类别 0 模板
- W1:类别 1 模板
Z0=W0.f+b0,本质是特征向量 f 和类别 0 模板的相似度得分
训练时:图片真实标签是类别 0 梯度信号会引导网络做两件事:
- 更新W0:让W0尽量和 “猫图片特征 f” 方向靠近 → 内积变大 →Z0变大
- 更新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 个物体。
输出头通道数计算董事:
举例: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)各个分支激活函数怎么选(极易踩坑)
- xywh 坐标:连续回归值 → 不加 softmax,因为softmax是用来做概率的,比如z0/(z0+z1+..+zn), 可以选择性添加sigmoid,通常 sigmoid 约束到 0~1(相对网格坐标)
- obj 置信度:0~1 有无物体 → 使用 Sigmoid
- 类别 Logits:
- 原版 YOLO:每个网格内单物体,Softmax 分类;
- 现代 YOLO:Sigmoid 多类别
3)损失不再单一交叉熵!手写检测需要组合 3 种 loss
总Loss = 坐标回归Loss(L1/MSE) + 置信度Loss(BCE) + 类别分类Loss(交叉熵)
4)手写目标检测会遇到的 4 大难点
- 标签编码 原图真实框 → 映射到对应网格、转换成相对网格的 xywh,手写很繁琐。
- 正负样本匹配 哪些网格负责预测真实物体?大量网格是空背景(YOLO 系列不断迭代标签分配策略)
- NMS 非极大值抑制(后处理) 网络会输出大量重叠框,推理阶段需要自己写 NMS 过滤冗余框。
- 尺度问题 单层网格只能适配中等大小物体;想要检测大小物体,必须手写多尺度特征融合(类似 YOLO Neck)
5)循序渐进学习路线
- 先运行上面 Demo,看懂张量维度、如何拆分【框 / 置信度 / 类别】
- 手写损失函数,完成完整前向 + 反向传播闭环
- 手写简易 NMS 后处理,实现推理可视化
- 理解短板:单尺度、无锚框缺陷
- 再去看 ultralytics YOLO 源码,你就能看懂框架内部在干什么,不再黑盒
更多推荐




所有评论(0)