news 2026/9/26 18:35:02

ResNet50特征提取+逻辑回归:猫狗大战快速分类实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ResNet50特征提取+逻辑回归:猫狗大战快速分类实战

简介:这份源码案例面向深度学习入门者与计算机视觉初学者,围绕猫狗二分类任务,演示如何用预训练ResNet50提取图像特征,再交由逻辑回归完成分类。案例完整覆盖数据预处理、加载并微调ResNet50、批量提取特征向量、训练逻辑回归、评估准确率与F1等指标,以及超参数调整和未知图片预测,帮助读者理解深度特征与浅层分类器融合的完整流程。压缩包共43个文件,以25个py脚本和11个pyc缓存为主,另含png图示、txt说明、json配置、md文档与pickle特征文件,整体约907KB,目录按数据构建、模型训练、工具函数等模块划分,结构清晰便于按需查阅。目前已有219人学习。读者可据此掌握迁移学习与特征工程结合的基本套路,并直接复用脚本完成自己的图像分类实验。

1. 从一份 ResNet50 加逻辑回归的源码说起:为什么特征提取比端到端训练更值得先跑通

猫狗大战这个数据集,几乎每个做图像分类的人都碰过。但真正到了工程落地,你会发现一个反直觉的事实:用 ResNet50 做迁移学习,把最后的全连接层换成二分类头去微调,往往不如「ResNet50 只做特征提取 + 逻辑回归做分类」来得稳、来得快、来得省。原因不复杂——当你的标注样本只有几千张、GPU 只有一张消费级卡的时候,微调整个 25M 参数的骨干网络,过拟合风险和显存压力都会让你难受。而把 ResNet50 当成一个固定的特征编码器,每张图吐出一个 2048 维向量,再拿逻辑回归去拟合,训练时间从小时级压到分钟级,准确率却经常能咬住微调方案。

这份源码要解决的就是这条路径:用预训练的 ResNet50 把猫狗图片批量转成特征矩阵,再用 scikit-learn 的逻辑回归完成分类。它适合三类人:刚学完 python 基础语法、想找一个能跑通的完整机器学习项目练手的新手;手头有少量标注图片、想快速验证分类可行性的从业者;以及需要把深度学习特征和传统分类器组合起来做 baseline 的工程师。整条链路不依赖大规模算力,一台带普通显卡的机器甚至纯 CPU 都能跑完,这也是它比端到端微调更适合作为第一个落地项目的原因。

2. 特征提取这条路的原理与选型:ResNet50 为什么适合当固定编码器

2.1 迁移学习的两种用法,以及为什么这里选「冻结骨干」

迁移学习在图像领域有两条常见路线。第一条是微调:加载预训练权重后,解冻部分或全部卷积层,用新数据继续训练,让骨干网络适应你的任务分布。第二条是特征提取:把预训练网络当成一个黑匣子,只取它某一层的输出作为特征向量,骨干参数完全不动,后面接一个独立的分类器。

微调的上限更高,但代价也明显。ResNet50 在 ImageNet 上预训练后,浅层学到的是边缘、纹理这类通用特征,深层学到的是语义部件。猫狗大战的图片和 ImageNet 分布接近,所以深层特征本身就已经很有判别力,不需要再动骨干。冻结骨干带来的好处是:每次训练只需要前向传播一次拿到特征,之后所有实验都在 2048 维向量上做,迭代成本极低。你可以今天试逻辑回归,明天换 SVM,后天换 XGBoost,特征只提一次。

选 ResNet50 而不是更小的 ResNet18 或更大的 ResNet101,是一个精度和速度的折中。ResNet18 的全局池化输出是 512 维,特征容量偏小,猫狗这种类间差异不算极端的任务上,准确率通常比 ResNet50 低一到两个点。ResNet101 参数量翻倍,特征维度还是 2048,但提取速度慢不少,收益却不明显。ResNet50 卡在中间,是社区里被验证最多的选择。

2.2 取哪一层的输出:全局平均池化层是默认答案

ResNet50 的结构里,最后一个卷积块输出的是 7×7×2048 的特征图。如果直接展平,得到的是 100352 维向量,维度太高,逻辑回归在这种维度上容易过拟合,训练也慢。常见做法是取全局平均池化层(Global Average Pooling)之后的输出,也就是每个通道的 7×7 特征图求平均,压成一个 2048 维向量。

这一步在 PyTorch 里对应的是把模型最后的fc层替换成nn.Identity(),或者直接取avgpool之后的张量。2048 维是一个比较舒服的维度:信息量足够,逻辑回归能收敛,存储和计算都不吃力。如果你想要更强的空间信息,也可以取layer4的输出自己做池化,但对猫狗大战这个任务,全局平均池化已经够用。

2.3 逻辑回归在这里的角色:一个线性分类器为什么够用

有人会问,ResNet50 提取的特征已经这么强了,为什么后面只接一个逻辑回归,而不是再接几层 MLP?答案在于特征的可分性。预训练骨干在 ImageNet 上学到的表示,已经把猫和狗映射到了一个线性可分程度很高的空间里。逻辑回归本质上是在这个空间里找一个超平面,把两类分开。如果特征本身够好,线性分类器就能拿到很高的准确率,加非线性层反而增加过拟合风险。

逻辑回归的另一个好处是可解释性和训练速度。它的损失函数是凸的,优化到全局最优没有悬念,不需要调学习率衰减、不需要担心陷入局部极小。对于「先跑通再优化」这个目标,它是最省心的选择。scikit-learn 的LogisticRegression默认用 L2 正则,配合liblinear或lbfgs求解器,在几千个样本、2048 维特征上几秒钟就能收敛。

3. 从图片到特征矩阵:ResNet50 批量提取的完整实现

3.1 环境准备与依赖安装

先把环境搭起来。这份源码依赖 PyTorch、torchvision、scikit-learn、Pillow 和 numpy。如果你用的是 conda,建议单独建一个环境,避免和系统里的包冲突。

conda create -n catdog python=3.9 -y conda activate catdog pip install torch torchvision scikit-learn pillow numpy tqdm

如果你只有 CPU,PyTorch 的 CPU 版本就够用,提取几千张图大概几分钟。有 NVIDIA 显卡的话装对应 CUDA 版本的 torch,速度会快一个数量级。装完之后用下面这行验证一下:

python -c "import torch, torchvision, sklearn; print(torch.__version__, torch.cuda.is_available())"

输出里True表示 GPU 可用。如果显示False但你确实有显卡,多半是 CUDA 版本和 torch 版本没对上,去 PyTorch 官网按你的 CUDA 版本重新装一次。

3.2 数据目录结构与预处理管道

猫狗大战的原始数据通常是train/cat.0.jpg、train/dog.0.jpg这种命名,或者按类别分文件夹。源码里一般会统一成按文件夹分:

dataset/ train/ cat/ cat.001.jpg ... dog/ dog.001.jpg ... val/ cat/ dog/

预处理管道要和 ResNet50 训练时的保持一致,否则特征分布会偏。标准做法是:缩放到 256×256,中心裁剪到 224×224,转成张量,再用 ImageNet 的均值和标准差归一化。

from torchvision import transforms preprocess = transforms.Compose([ transforms.Resize(256), # 短边缩到 256 transforms.CenterCrop(224), # 中心裁到 224x224 transforms.ToTensor(), # 转张量,像素值归到 [0,1] transforms.Normalize( # ImageNet 统计量 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ])

这里的mean和std是 ImageNet 训练集的统计值,不是随便填的。如果你换成自己的数据集统计量,特征分布会和预训练时不一致,准确率会掉。Resize(256)加CenterCrop(224)是 torchvision 官方示例里的标准组合,短边缩到 256 再裁中心,能保留主体又统一尺寸。

3.3 加载 ResNet50 并剥离分类头

接下来加载预训练模型,把最后的全连接层去掉,只保留到全局平均池化。

import torch import torch.nn as nn from torchvision import models device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 加载 ImageNet 预训练权重 model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) # 把最后的 fc 层替换成 Identity,输出就是 2048 维池化特征 model.fc = nn.Identity() model = model.to(device) model.eval() # 关键:切到推理模式,冻结 BN 和 Dropout

model.eval()这行不能省。ResNet50 里有 BatchNorm 层,训练模式下它会用当前 batch 的统计量做归一化,推理模式下才用训练时累积的 running mean 和 running var。如果你忘了切 eval,同一张图在不同 batch 里提取出的特征会不一样,后面逻辑回归的训练就会莫名其妙地不稳定。这是血泪经验里排前三的坑。

weights=models.ResNet50_Weights.IMAGENET1K_V2是 torchvision 新版的写法,老版本用pretrained=True。两者加载的权重略有差异,V2 版本在 ImageNet 上精度更高,建议用新的。

3.4 批量提取特征并保存为 npy

有了模型和预处理,就可以遍历数据集提取特征了。核心是用torch.no_grad()关掉梯度计算,省显存也提速。

import os import numpy as np from PIL import Image from torch.utils.data import DataLoader, Dataset from tqdm import tqdm class ImageFolderFlat(Dataset): def __init__(self, root, transform): self.samples = [] self.transform = transform # 约定:root 下每个子文件夹是一个类别 for label, cls in enumerate(sorted(os.listdir(root))): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((".jpg", ".jpeg", ".png")): self.samples.append((os.path.join(cls_dir, fname), label)) self.classes = sorted(os.listdir(root)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert("RGB") return self.transform(img), label def extract_features(root, model, transform, batch_size=32): ds = ImageFolderFlat(root, transform) loader = DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=4) feats, labels = [], [] with torch.no_grad(): for imgs, lbls in tqdm(loader, desc=f"extract {root}"): imgs = imgs.to(device) out = model(imgs) # 形状 [B, 2048] feats.append(out.cpu().numpy()) labels.append(lbls.numpy()) return np.concatenate(feats), np.concatenate(labels), ds.classes X_train, y_train, classes = extract_features("dataset/train", model, preprocess) X_val, y_val, _ = extract_features("dataset/val", model, preprocess) np.save("X_train.npy", X_train) np.save("y_train.npy", y_train) np.save("X_val.npy", X_val) np.save("y_val.npy", y_val) print("train:", X_train.shape, "val:", X_val.shape, "classes:", classes)

几个参数值得说清楚。batch_size=32是显存和速度的平衡点,2048 维输出不大,32 或 64 都行,显存紧张就降到 16。num_workers=4控制数据加载的并行进程数,Windows 上如果报错就改成 0。shuffle=False是必须的,因为我们要保证特征和标签的顺序严格对应,打乱会导致标签错位。提取完成后把特征存成.npy,后面调分类器时直接加载,不用每次重新跑骨干网络。

ImageFolderFlat里用sorted(os.listdir(root))保证类别顺序稳定,cat 在前 dog 在后,标签就是 0 和 1。如果你直接用os.listdir不排序,不同机器上文件系统返回的顺序可能不同,标签含义就乱了。这个细节在单机上跑没事,一旦换机器复现就对不上。

4. 逻辑回归训练与评估:把 2048 维特征变成分类结果

4.1 特征标准化与逻辑回归的关键参数

特征提取完之后,先做一步标准化。ResNet50 的输出经过 ReLU,值域是 [0, +∞),不同维度的尺度差异可能很大。逻辑回归对特征尺度敏感,标准化能显著加快收敛。

from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_val_s = scaler.transform(X_val) # 注意:用训练集的统计量 clf = LogisticRegression( C=1.0, # 正则强度倒数,越大越弱 penalty="l2", # L2 正则,防止过拟合 solver="lbfgs", # 拟牛顿法,适合中小规模 max_iter=1000, # 迭代上限,不够会警告 n_jobs=-1, # 用满 CPU ) clf.fit(X_train_s, y_train) pred = clf.predict(X_val_s) print("val acc:", accuracy_score(y_val, pred)) print(classification_report(y_val, pred, target_names=classes))

C是最需要调的参数。C 越大正则越弱,模型越倾向拟合训练集;C 越小正则越强,偏向简单模型。猫狗大战这种特征质量高的任务,C 在 0.1 到 10 之间试几个值就能找到不错的点。solver="lbfgs"是默认选择,2048 维、几千样本的规模它处理得很好。如果特征维度再高一个量级,可以换saga,它支持 L1 正则且对稀疏特征更友好。

StandardScaler的fit_transform只在训练集上调用,验证集必须用transform,复用训练集的均值和方差。如果验证集也fit_transform,等于把验证集的分布信息泄漏进了训练过程,评估结果会虚高。这是新手最容易犯的错之一。

4.2 用交叉验证找 C 的合理区间

单次划分的验证集准确率有波动,想稳一点就用交叉验证扫一遍 C。

from sklearn.model_selection import cross_val_score import numpy as np for c in [0.01, 0.1, 1.0, 10.0, 100.0]: clf = LogisticRegression(C=c, max_iter=1000, n_jobs=-1) scores = cross_val_score(clf, X_train_s, y_train, cv=5, scoring="accuracy") print(f"C={c:>6} mean={scores.mean():.4f} std={scores.std():.4f}")

5 折交叉验证在几千样本上跑起来很快。看mean找最高的 C,同时看std,如果某个 C 的 std 明显大,说明模型对数据划分敏感,换更小的 C 增加正则通常能稳住。实践中 C=1 附近往往就是不错的起点,不需要扫太细。

4.3 混淆矩阵与错误样本分析

准确率只是一个数,想知道模型错在哪,看混淆矩阵和具体错分样本。

from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_val, pred) print(cm) # 找出预测错误的验证集样本索引 wrong = np.where(pred != y_val)[0] print("错分数量:", len(wrong))

混淆矩阵能告诉你模型是偏向把猫认成狗,还是反过来。如果两类错分数量严重不对称,可能是训练集里两类样本数不均衡,或者某一类的图片质量整体偏差。把错分样本的路径打出来,肉眼看看是不是有标注错误、模糊图、或者主体不明显的图。猫狗大战数据集里确实存在少量标错的样本,这部分噪声会拉低上限,清洗掉能涨点。

4.4 保存模型与推理脚本

训练完把 scaler 和分类器一起存下来,推理时要成对使用。

import joblib joblib.dump({"scaler": scaler, "clf": clf, "classes": classes}, "catdog_lr.pkl") # 推理示例 bundle = joblib.load("catdog_lr.pkl") def predict_image(path): img = Image.open(path).convert("RGB") x = preprocess(img).unsqueeze(0).to(device) with torch.no_grad(): feat = model(x).cpu().numpy() feat = bundle["scaler"].transform(feat) idx = bundle["clf"].predict(feat)[0] return bundle["classes"][idx]

注意推理时特征提取用的model必须和训练时是同一个,preprocess也必须一致。如果换了预处理参数,特征分布变了,scaler 和分类器都会失效。把模型、scaler、类别列表打包成一个文件,能避免推理时漏掉某一步。

5. 避坑与排查:这条链路上最容易翻车的五个地方

5.1 忘了 model.eval() 导致特征每次都不一样

现象:同一张图跑两次提取,得到的 2048 维向量数值不同,逻辑回归训练 loss 震荡,验证准确率忽高忽低。

原因:ResNet50 里的 BatchNorm 在训练模式下用当前 batch 的统计量做归一化,batch 组成一变,输出就变。Dropout 层也会随机置零。

解决:加载模型后立刻调用model.eval(),并且整个特征提取过程包在torch.no_grad()里。这两步是配套的,缺一不可。

5.2 验证集用了 fit_transform 造成数据泄漏

现象:验证集准确率异常高,接近 99%,但拿新图片测试时表现差很多。

原因:对验证集调用scaler.fit_transform,等于用验证集的均值和方差去标准化验证集,把验证集分布信息泄漏了。

解决:训练集fit_transform,验证集和测试集一律transform。把 scaler 存下来,推理时复用同一个。

5.3 图片通道数不对导致预处理报错

现象:RuntimeError: output with shape [1, 224, 224] doesn't match the broadcast shape [3, 224, 224]。

原因:数据集里有灰度图或带 alpha 通道的 PNG,Image.open读出来是单通道或四通道,和 ResNet50 期望的三通道不匹配。

解决:在__getitem__里统一Image.open(path).convert("RGB")。这一行能挡掉绝大多数通道问题,代价只是多一次转换。

5.4 标签顺序不稳定导致类别对不上

现象:训练时 cat 是 0、dog 是 1,换台机器重新提特征后,cat 变成了 1,之前存的模型预测全反了。

原因:os.listdir返回的顺序依赖文件系统,不同环境可能不同。

解决:用sorted(os.listdir(root))固定类别顺序,并把classes列表和模型一起保存。推理时按保存的classes映射回类别名,不要重新扫目录。

5.5 特征没标准化直接喂逻辑回归

现象:逻辑回归训练很慢,max_iter调到几千还在警告不收敛,准确率也不理想。

原因:ResNet50 输出经过 ReLU,各维度尺度差异大,逻辑回归的梯度下降在这种条件下收敛慢。

解决:训练前用StandardScaler标准化。这一步几乎零成本,但对收敛速度和最终精度都有明显帮助。标准化后再跑,max_iter=1000通常就够了。

6. 把准确率再往上推一点:特征拼接与分类器替换的实操技巧

跑通基础版本之后,如果想把验证集准确率从九十个点再往上推一推,有几个成本不高但有效的做法。

第一个是特征拼接。ResNet50 的全局平均池化输出是 2048 维,你可以额外取layer4输出的全局最大池化,再拼一个 2048 维,凑成 4096 维。平均池化捕捉的是整体响应强度,最大池化捕捉的是最显著的区域响应,两者互补。拼接后维度翻倍,逻辑回归训练时间增加不多,但准确率往往能涨零点五到一个点。代码上就是在模型 forward 里同时返回两个池化结果,或者用 hook 抓中间层输出。

第二个是换分类器做对比。逻辑回归是线性 baseline,你可以用同样的特征跑一遍 SVM(RBF 核)或 LightGBM,看哪个更高。实践中在 2048 维特征上,线性 SVM 和逻辑回归通常打平,RBF 核 SVM 可能略高但训练慢很多,LightGBM 在样本量几千时表现也不错。选哪个取决于你对推理延迟和可解释性的要求。逻辑回归的系数可以直接看哪些维度对分类贡献大,树模型和核方法就没这么直观。

第三个是数据增强后的特征平均。对训练集里每张图做几次随机裁剪或水平翻转,分别提取特征后取平均,相当于用增强样本的 feature 做了一次集成。这个做法能让特征更鲁棒,尤其是对拍摄角度变化大的图片。代价是提取时间乘以增强倍数,但特征只提一次,后面实验都受益。

方案特征维度验证集准确率参考训练耗时适用场景
ResNet50 + 逻辑回归2048基线秒级快速验证、教学
平均+最大池化拼接4096基线 +0.5~1秒级追求精度、算力有限
拼接 + LightGBM4096基线 +1~2分钟级有调参经验
拼接 + 增强特征平均4096基线 +1~2提取时间×N图片差异大

我自己的习惯是先把基础版本跑通、把准确率记下来,再逐项加技巧,每加一项单独评估,确认有正向收益才保留。曾经有一次把三个技巧一起加上,结果准确率反而掉了,排查半天才发现是增强倍数太高,平均后的特征把判别性抹平了。从那以后我坚持一次只动一个变量,这条习惯帮我省了很多后悔药。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:33:58

AI原生开发实战:Anthropic SDLC手册核心原则与落地指南

1. 这份手册到底在讲什么Anthropic 把内部用了很久的一套 AI 原生软件开发方法公开了,名字叫The AI-Native SDLC Playbook。SDLC 就是软件开发生命周期,从需求到设计、编码、测试、部署、运维这一整条链路。这份手册的核心主张很直接:把 AI 当…

作者头像 李华
网站建设 2026/9/26 18:32:41

AI变现三步法:隐形外包、数字积木与人机协同飞轮

1. 这不是“暴富故事”,而是一份可拆解、可复现的AI变现实操手记“我去年靠 AI 赚了100万,其实就用了这3个方法”——这句话在社交平台刷屏时,我正蹲在客户公司机房里调试一套OCR识别流水线。没点开任何一条短视频,但连续三天&…

作者头像 李华
网站建设 2026/9/26 18:31:53

NumPy傅里叶变换API深度解析:从参数原理到图像信号处理实战与避坑

NumPy的傅里叶变换API,说真的,是很多人学了又好像没学的状态。会用np.fft.fft算个频谱,但一碰到真实项目——图像滤波、信号去噪、卷积加速、频率成分分析——就卡壳。问题不在于傅里叶变换本身有多难,而在于numpy.fft这套API的设…

作者头像 李华
网站建设 2026/9/26 18:30:54

从Prompt到Skill:可复用AI能力包的工程化实践指南

1. 从零理解 Skill:它到底是什么,为什么值得折腾第一次接触 Skill 这个概念,很多人会把它和 Prompt 混为一谈。我刚开始也是这么想的——不就是一段提示词嘛,写长一点、写细一点不就完了?但真正用起来才发现&#xff0…

作者头像 李华
网站建设 2026/9/26 18:30:25

基于Transformer的运动想象脑电信号分类:本科毕设全流程实战指南

简介:这份本科毕业设计资源聚焦基于Transformer的运动想象脑电信号分类,面向人工智能与生物医学工程交叉方向的本科生及脑机接口入门研究者,帮助解决EEG信号深层模式挖掘与多类别运动想象识别问题。压缩包共31个文件,约18.45MB&am…

作者头像 李华