1. 图像识别到底是怎么工作的:CNN的底层逻辑
1.1 传统图像识别卡在哪
先说一个扎心的现实:如果你现在去翻阅五年前的图像识别项目,会发现大量时间不是花在写模型上,而是花在"设计特征"上。所谓特征,就是告诉程序"你看,这个物体边缘有梯度,纹理是竖向的,颜色偏暗……"这种手工设计特征的方式,在特定场景下能跑通,但换一个光照环境、换一个拍摄角度,准确率立刻崩盘。我做过一个零件表面缺陷检测的项目,同一套特征在不同产线设备上采集的图像上,F1值从0.9直接跌到0.6,后来彻底放弃手工特征,改用CNN,才算是把这事儿真正解决。
CNN(卷积神经网络)的颠覆之处在于,它把"特征设计"这件事也交给了数据。网络通过训练自动学习到从边缘、纹理到部件、语义的层次化特征,不需要人再去定义"缺陷长什么样"。这是一个本质区别:传统方法的瓶颈在人的经验能否覆盖所有变化,而CNN的瓶颈主要在于数据量和算力。理解了这一点,你就明白为什么现在聊图像识别几乎必然绕不开CNN,以及它衍生出的ResNet、YOLO、UNet这些变体。
1.2 卷积、池化、全连接:三个关键部件的拆解
CNN的核心结构可以压缩成三句话:用卷积层提取特征,用池化层压缩特征,用全连接层决定"这堆特征属于哪一类"。
卷积层干的事,说起来很简单:拿一个小的滤波器(比如3×3)在图像上滑动,每次都把覆盖区域的像素值和滤波器权重做点积运算。这个过程有一个特别重要的物理意义——它关注的不是某个像素,而是"局部像素之间的关系"。我常用一个类比:你把图像理解成一张地形图,卷积核就像一把放大镜在图上挪动,每挪一次就记录下"这一片区域的地形走势",最后得到一张新的地形图,也就是特征图。
这时候你会遇到第一个影响深远的参数:卷积核的尺寸和数量。3×3是最常用的,因为它能在保持感受野的同时减少参数开销。卷积核数量决定了这一层能学到多少种不同特征,比如10个卷积核就对应10种特征模式。但数量不是越多越好,我见过不少新手一上来就堆256个卷积核,结果参数量暴涨、训练变慢、还容易过拟合。合理的做法是:浅层放少量卷积核,比如32、64,深层慢慢增加,比如128、256,因为深层需要表达更复杂的语义。
池化层则是降维操作。最常见的是最大池化,取一个2×2窗口里的最大值。它的作用不只是让特征图变小,更重要的是带来一定的平移不变性:哪怕物体在图像里偏移了几个像素,池化后特征仍然能保持大致一致。代价是会损失部分空间细节,所以现在很多新网络更倾向于保留位置信息,用步长为2的卷积替代池化。但在入门项目里,池化仍然是最直观、最好用的压缩手段。
全连接层就比较好理解了,它把前面学到的所有特征拉平成一维向量,再接一个softmax输出每个类别的概率。需要留意的是,全连接层占据了CNN里最多的参数量,所以很多轻量网络会用全局平均池化替代它。你的项目如果是部署在手机或者ESP32这类单片机上,这一点尤其值得关注。
1.3 CNN到底在学什么:特征图可视化的视角
很多初学者会有个疑问:"网络内部到底发生了什么?"最常见的误解是把CNN当成黑盒子,其实不是。你完全可以把训练好的模型里某一层的特征图打印出来,肉眼看到它学到了什么。
以一个简单的猫狗二分类网络为例,训练完把第一层卷积输出的特征图可视化,你会发现里面有的是边缘检测器(明亮的轮廓线)、有的是颜色过滤器(特定颜色的区域被激活)、有的是纹理敏感器(毛发的方向)。但到了更深的层,可视化出来的特征图就越来越抽象,有的对应"耳朵的形状",有的对应"眼睛的位置",已经很难用人话描述,但网络确实靠这些抽象特征做出了正确的分类判断。
我在实际项目中把特征图可视化成了一面墙的缩略图,贴出来给同事看。那一刻大家才真正明白,CNN不是"魔法",而是层级化地提取信息。遇到调试困难时,先看特征图,往往比盯着训练loss瞎猜高效得多。
2. 环境搭建与图像数据准备:最容易被低估的环节
2.1 Python环境与依赖安装要点
我不止一次看到有人在项目已经跑到一半才发现torch和CUDA版本对不上,重装环境折腾两天。这里分享一个我自己验证过很多次的稳定组合,每次新开机器都用这一套,基本没翻过车。
基础环境是Python 3.9或3.10(3.8也能用,但部分新版库已经放弃支持了)。用conda创建环境是最省心的方式,命令如下:
conda create -n cnn_project python=3.9 conda activate cnn_project随后装PyTorch,这里最容易踩坑。不要直接pip install torch,那样装的是CPU版,跑起来慢到怀疑人生。正确做法是先去PyTorch官网,选择对应的CUDA版本,把生成的命令复制下来执行。以CUDA 11.8为例,命令长这样:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118装完之后务必验证一下GPU是否真的可用:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出True和你的显卡型号,说明环境正常。如果输出False,别急着重装,先检查CUDA驱动版本:在终端跑nvidia-smi,看右上角的CUDA Version是否≥11.8。驱动版本不够就直接更新驱动,不要折腾重装PyTorch,这两者的匹配逻辑和pip、conda无关。
其他依赖就按需安装了:numpy、matplotlib、pandas处理数据;opencv-python读图像;pillow做格式转换。我习惯写在一个requirements.txt里,方便换机器复现。
pip install numpy matplotlib pandas opencv-python pillow2.2 数据集的获取、预处理与增强
图像识别项目里有一句老话:数据决定上限,模型决定下限。你喂给网络什么样的数据,它就学到什么样的模式。最经典的任务是CIFAR-10,10类、6万张32×32彩色图,条目数量小到普通电脑也能跑,是入门CNN时信息密度最高的数据集之一。
torchvision提供了直接下载接口:
from torchvision import datasets, transforms transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_set = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) test_set = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)这里有个关键点:Normalize的三个mean和std不是随便填的,它对应ImageNet数据集的统计值。如果你用的是在ImageNet上预训练的模型(比如ResNet18),就必须用这套值,因为模型在预训练时已经适应了这个分布。如果你从零训练自己的网络,最好按你自己的数据集重新计算mean和std,否则输入分布差异会拖慢收敛。
数据增强是另一个常被新手忽视的点。图像识别最怕的就是模型把训练集的噪声背下来而不是学通用模式。常见增强手段有随机翻转、随机裁剪、色彩抖动、旋转等。torchvision里用几行就能搭好:
train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])注意:数据增强只放在训练集,测试集和验证集只做ToTensor和Normalize,否则会污染评估结果。我在真实项目里见过有人测试集也加了RandomCrop,导致验证集和训练集分布一致,指标好看得不行,一上线立刻现原形。
2.3 数据加载器构建:训练效率的起点
PyTorch的DataLoader是数据管线的核心。设定合理的batch_size、shuffle、num_workers和pin_memory,对训练速度有实打实的影响。
from torch.utils.data import DataLoader train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=4, pin_memory=True) test_loader = DataLoader(test_set, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)几个参数的经验值:batch_size看显存,常见8G显存跑224×224图像用32或64;num_workers通常设成CPU核数减一,设太大会因为进程切换反而变慢;pin_memory=True能减少数据从CPU到GPU的传输时间,前提是机器内存足够。
如果你做的是自己的数据集,建议用一个自定义Dataset子类,在__getitem__里完成图像读取和transform,比一次性把所有图片加载进内存更省资源,也更符合大数据集的真实场景。
3. 手写一个CNN分类器:从网络设计到训练调优
3.1 网络结构设计:参数怎么定,层数怎么选
这里我给出一个经过验证的浅层CNN结构,适合CIFAR-10或类似规模的小型图片分类任务:
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2, 2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 4, 256), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x参数怎么来的?这里的图像输入是32×32(CIFAR-10),经过三次2×2最大池化后,空间尺寸变成32÷2÷2÷2=4,所以最后的特征图是128×4×4。Linear层的输入维度照这个算,新手最容易在这里报维度不对的错误;解决办法也很简单,在定义之前先打印一下feature map的shape。
卷积层用padding=1,保持特征图尺寸不变,让下采样只发生在池化层,这会让感受野的增长更可控。激活函数选ReLU,理由很简单:计算量小、收敛快、实现成熟。最后的输出层不接softmax,因为CrossEntropyLoss内部自带softmax,你再接一层反而可能出现数值不稳定和梯度异常。
Dropout放在全连接层之间,设0.5,是防止过拟合最直接的手段。对CIFAR-10这种小数据集,不用Dropout的话训练集准确率很容易逼近100%而验证集卡在75%左右,加上Dropout后通常能提升几个点。
层数的选择原则是:由浅入深,先用一个小网络把流程跑通,确认数据管线、训练循环都没有问题,再慢慢加深。很多新手一上来就想复现ResNet的152层,结果连"loss为什么不下降"都排查不清楚。
3.2 训练管线:损失函数、优化器与学习率调度
训练代码是CNN项目的骨架,这里给出一个结构清晰的模板,我自己的项目基本都是在这个框架上改出来的:
import torch import torch.optim as optim from torch.optim.lr_scheduler import StepLR device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleCNN(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=5e-4) scheduler = StepLR(optimizer, step_size=10, gamma=0.1) epochs = 30 for epoch in range(epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_loss = running_loss / total train_acc = correct / total # 验证 model.eval() val_loss = 0.0 val_correct = 0 val_total = 0 with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) val_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_loss = val_loss / val_total val_acc = val_correct / val_total print(f'Epoch {epoch+1}/{epochs} | train_acc: {train_acc:.4f} | val_acc: {val_acc:.4f} | lr: {scheduler.get_last_lr()[0]:.1e}') scheduler.step()损失函数选CrossEntropyLoss,这是多分类问题的标准答案。优化器我优先推荐Adam,它的学习率自适应性很强,入门跑起来省心;但到了后期调优阶段,换回带动量的SGD往往能把验证集准确率再往上抬一些。原因在于Adam的滑动平均机制在训练后期可能导致收敛不充分,而SGD配合合适的学习率策略能更细致地逼近局部最优。
学习率调度这一步很多人会漏。没有调度意味着你用同一个步伐走完下山的全程,容易在前半段震荡过大、后半段在谷底附近也走不出精确的落脚点。StepLR每10个epoch把学习率乘以0.1,简单有效。你还可以用ReduceLROnPlateau,它在验证集loss不再下降时自动降低学习率,更省心。
训练过程中有两件事必须养成习惯:一是每轮epoch都输出训练集和验证集的准确率,二是定期保存checkpoint。只盯训练集准确率没有意义,训练集再高也只说明记住了,真正有价值的是验证集的表现。
3.3 迁移学习实战:ResNet18微调
自己从零训练一个完整网络确实能带来成就感,但如果你追求的是"拿到一个任务,快速跑出一个能用的模型",迁移学习才是性价比最高的路线。原因很直观:ResNet18这些网络已经在大规模数据集上学会了通用视觉特征,你可能只需要微调最后几层,就能把它适配到自己的数据集上。
实现起来非常简单,torchvision里直接提供了带预训练权重的模型:
from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, num_classes)关键是处理好预训练权重的冻结策略。如果你的数据集很小,比如只有几千张图,建议把除了fc之外的所有层冻结,只训练最后的分类层:
for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True如果数据集有几万张,可以全部放开微调,但初始学习率要比从零训练低一些,比如1e-4甚至1e-5,避免在预训练权重的基础上步子迈得太大把学到的信息破坏掉。
我在真实项目里试过用ResNet18做工业产品分类,数据只有5千张左右,冻结特征提取层只训练全连接层,准确率就能达到94%;如果从零训练一个类似深度的网络,同数据量下准确率只有82%左右。这就是预训练权重的威力。
3.4 评估与可视化:准确率、损失曲线、混淆矩阵
训练完不能只看准确率一个数字,我强烈建议画三张图:训练/验证损失曲线、训练/验证准确率曲线、混淆矩阵。
损失曲线的判断逻辑很简单:训练损失不断下降、验证损失也同步下降,正常;训练损失下降但验证损失先降后升,这就是过拟合的经典信号,训练超过某个拐点后开始背诵数据了。准确率曲线同理。注意,验证集准确率有正常波动,不要看到单次波动就惊慌,要看整体趋势。
混淆矩阵能告诉你模型具体错在哪里。比如二分类猫狗识别中,所有错误都集中在"把猫认成狗",那说明训练集里猫的样本姿态多样性不足。下面是快速画混淆矩阵的代码:
import matplotlib.pyplot as plt import numpy as np from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay all_preds = [] all_labels = [] model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=train_set.classes) disp.plot(cmap='Blues') plt.show()我见过一个非常典型的情况:一个人用同一个网络调了半个月,准确率始终上不去,结果画出混淆矩阵才发现,模型把"天空"和"湖水"两个类别互相误判,因为训练集里很多图片的背景就是水天一色。这个信息是靠单一准确率永远看不出来的。
4. 训练翻车现场:常见问题与排查技巧
4.1 过拟合与欠拟合:从曲线定位问题
先说欠拟合,症状是训练集准确率也不高。这种情况通常是模型容量不足,或者学习率设得太低导致几乎不更新。解决方案是换更深更大的网络、调高学习率、增加训练轮数。如果你的训练集准确率一直在50%、60%徘徊,先把优化器学习率调到1e-3、1e-2试试,确认loss确实在下降,再排查其他因素。
过拟合则恰恰相反,训练集准确率高达98%,验证集只有70%。这表示模型背住了训练集中的细节,但对新数据的泛化能力差。最有效的手段依次是:增加数据广度(做更强的数据增强)、增加Dropout比率(从0.5调到0.7)、降低模型复杂度、引入早停。早停的实现思路是:监控验证集loss,连续N个epoch不下降就停止训练并恢复最佳权重。这个东西写起来不复杂,但带来的收益非常直接,省去大量无谓的训练时间。
best_val_loss = float('inf') patience = 5 bad_epochs = 0 for epoch in range(epochs): # ... 训练和验证 ... if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'best_model.pth') bad_epochs = 0 else: bad_epochs += 1 if bad_epochs >= patience: print(f'Early stopping at epoch {epoch+1}') break4.2 显存溢出与训练提速的实用手段
显存溢出的报错基本是CUDA out of memory。最常见的解法是减小batch_size。但如果你已经减到很小仍然溢出,可以从这几方面排查:确认没有多余张量被遗留在显存里,比如在训练循环里累积了不必要的中间变量;确认PyTorch没有在梯度清零前累积了多步反向传播;确认模型和输入数据都被正确to(device),避免CPU和GPU之间反复搬运导致的隐性显存占用。
加快训练速度的手段按性价比排序:一是调大batch_size,充分利用GPU并行能力,在显存允许的前提下batch越大吞吐越高;二是用torch.cuda.amp做混合精度训练,float16和float32混合计算,速度提升明显,显存占用还更低。AMP的代码只比普通训练多几行:
scaler = torch.cuda.amp.GradScaler() for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()如果你用的是RTX 30系或者之后的支持硬件,混合精度几乎是免费的性能提升。我在1660Super这种老卡上也测过,能提速约20%到30%。
4.3 图像预处理不当导致的识别精度问题
这个问题很隐蔽,我一开始也没意识到。图像预处理的方式直接影响网络看到的数据分布,如果你预处理和网络预训练时的处理不一致,哪怕差异很小,精度都会有可感知的下降。
最典型的坑有两个。一个是尺寸不一致:你的训练集可能来源于手机拍的照片,有竖屏有横屏,如果直接Resize((224, 224))强制拉伸,会产生严重的几何变形。更合理的方案是用Resize到短边为224,再CenterCrop一个224×224正方形区域,或者用一个固定大小的RandomResizedCrop,既保留物体关系又保持尺寸。
另一个坑是颜色通道顺序。OpenCV读出来是BGR,而PyTorch里预训练模型期望的是RGB。如果你用OpenCV读图喂给ResNet,并且没做通道转换,模型的分类结果会异常,但你又看不出代码哪里有问题,这种错误排查起来非常费劲。所以要么始终用PIL读图(PIL默认RGB),要么在OpenCV读图后手动转换:img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。
4.4 训练集太小怎么办:数据增强与预训练权重
真实项目中拿到几万甚至几十万张已标注图像是奢侈的,大多数场景是只有两三千张甚至几百张。这里我想先强调一个观念:与其执着于更大的网络,不如在数据和训练策略上多下功夫。
数据增强是最容易上手的方式,但要注意有些任务不适合激进的增强。比如医学图像中,左右翻转可能是合理的,但颜色抖动可能会改变病灶的视觉特征;再比如车牌识别,大幅度的随机旋转会让字符变得难以辨认。增强策略必须结合任务语义谨慎使用,这是书本上不会写、但实践中特别重要的经验。
另一个立竿见影的手段是使用ImageNet预训练模型继续微调。预训练模型已经学到很丰富的通用特征,你的小数据集只需要在此基础上做轻量适配。前面提到的冻结层和降低学习率,在这种场景下尤为重要。我曾经在只有1500张图的钢卷表面缺陷数据集上用ResNet34微调,最终F1值达到0.88,而同样数据下从零训练的网络F1只有0.67。
5. 走出实验室:模型保存、部署与后续扩展
5.1 模型保存与加载:别踩这个最常见的坑
训练结束后,保存模型是最简单也最容易出错的一步。很多新手直接保存整个模型:
torch.save(model, 'model.pth')这样确实能加载,但换一台机器、换一个Python版本,很可能报错。我更推荐只保存state_dict,也就是权重字典,配合模型定义代码来加载:
# 保存 torch.save(model.state_dict(), 'model_weights.pth') # 加载 model = SimpleCNN(num_classes=10) model.load_state_dict(torch.load('model_weights.pth')) model.to(device) model.eval()注意两点:加载前必须先实例化模型的类,严格保持结构和训练时一致;推理前调用model.eval(),关闭Dropout和BN的随机行为,否则同一张图片每次预测结果都不一样。
5.2 把模型跑在摄像头上:实时推理的体验
部署的方向很多,可以做成Web服务,也可以用OpenCV接入摄像头做实时识别。这里给出一个最简的摄像头实时识别流程,适合拿来做演示或验证自己的训练成果:
import cv2 import torch from torchvision import transforms device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleCNN(num_classes=10).to(device) model.load_state_dict(torch.load('model_weights.pth')) model.eval() transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) _, pred = torch.max(outputs, 1) label = train_set.classes[pred.item()] cv2.putText(frame, f'Prediction: {label}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow('CNN Demo', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()实时推理性能上要注意几点:输入尺寸不变,推理时用torch.no_grad()减少内存开销,单帧耗时取决于网络大小和推理后端,ResNet18大约在10~30毫秒量级,足够用。如果要做更高效的部署,把模型转换为ONNX格式,然后用TensorRT或ONNX Runtime跑,速度会再上一个台阶,这是边缘部署时更实用的路线。
5.3 边缘设备部署与轻量化设计的启发
看完这一整套流程,你大概能感受到,从零写一个CNN分类器并不神秘,但离"真正可用"还有一段距离。如果你的目标是跑在ESP32-CAM这类设备上,就要考虑模型剪枝、量化、知识蒸馏这些路线。量化就是把模型权重从float32降到int8,效果立竿见影:模型体积缩小4倍,推理速度显著提升,精度损失通常控制在1到2个点以内,在工业场景中这是很有操作空间的取舍。
我个人做过的经验是:先在上位机上完成训练和验证,保住核心指标;再转ONNX做推理验证;最后量化到int8部署到目标板。整个链路中,数据预处理的一致性、模型接口的稳定性、以及测试集与真实场景分布的差异,是决定部署后效果能否达到预期的关键。很多人训练时指标很好,部署后效果崩掉,十有八九是数据处理和模型输入不匹配,而不是模型本身的问题。
关于后续扩展的方向,我建议优先尝试目标检测而不是一味加深分类网络。分类任务只能回答"图中是什么",目标检测才能回答"图里有哪些物体、各自在哪里",这是大量真实业务需求的核心诉求。掌握好CNN的基础原理之后,再去看YOLO、Faster R-CNN这类检测模型,会发现它们的内核仍然是卷积特征提取那套东西,只是在特征图上多做了候选框回归和类别判别。基础打得牢,往上走会顺很多。
最后分享一点体会:图像识别项目的推进节奏,最好遵循"小数据集试通流程、简网络验证逻辑、深网络追求效果、轻量化达成部署"这条路线。不要一上来就追求大模型大算力,先把闭环跑通,再考虑优化的上限在哪里。我踩过的坑里,有八成不是模型结构不够新,而是数据、环境和部署环节出了偏差。你在这个项目里投入的所有调试时间,最终都会变成对图像识别这件事更扎实的理解。