news 2026/9/26 7:41:59

ResNet50特征提取+逻辑回归:快速构建猫狗分类基线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ResNet50特征提取+逻辑回归:快速构建猫狗分类基线

简介:这是一份面向深度学习入门与计算机视觉实践者的完整案例源码,围绕ResNet50特征提取与逻辑回归分类展开,帮助读者理解如何将预训练卷积网络与传统机器学习方法结合,解决猫狗二分类这一经典问题。压缩包共43个文件,约907KB,以25个Python脚本为核心,辅以11个pyc缓存、3张png图示、1个pickle特征文件及txt、json、md等说明配置,涵盖数据预处理、模型构建、训练与评估等模块,目录结构清晰,便于按流程阅读与复现。已有219人学习下载,适合希望掌握迁移学习基本流程、了解图像特征表示与分类决策的读者。通过该案例,可系统走通从加载预训练ResNet50、提取特征向量到训练逻辑回归并评估准确率、精确率、召回率与F1分数的完整链路,同时体会深度特征与浅层分类器融合的工程思路,为后续迁移到其他图像任务提供可复用的参考模板。

1. ResNet50 提特征 + 逻辑回归分类:一条被低估的迁移学习基线

猫狗大战这个数据集,很多人第一次跑都是在 Keras 里搭个几层卷积从头训,结果要么过拟合到 99% 训练准确率、验证集却卡在 70% 上下,要么训一下午还在原地打转。其实有一条更省事、更稳的路子:拿一个在 ImageNet 上预训练好的 ResNet50 当固定特征提取器,把每张图压成一个 2048 维向量,再在这个向量上训一个逻辑回归。整套流程不需要 GPU 也能跑,CPU 上十几分钟出结果,验证集准确率通常能到 96% 以上。

这篇讲的就是这条基线怎么落地:ResNet50 的哪一层输出能当特征、图像预处理要注意什么、逻辑回归的正则强度怎么调、缓存特征怎么省时间,以及几个我实际踩过的坑。适合刚接触迁移学习、想快速拿到一个能用的猫狗分类器的人,也适合已经会训 CNN、但想找一个可解释、可复现的对照基线的人。源码是 Python,依赖 PyTorch 和 scikit-learn,环境配好之后照着敲就能跑通。

2. 为什么选 ResNet50 做特征提取器,而不是从头训 CNN

2.1 迁移学习的核心逻辑:特征复用

从头训一个 CNN 分类猫狗,本质是让网络自己从像素里学出「耳朵尖不尖」「毛色什么样」这些判别性特征。问题是猫狗大战训练集也就两万多张,这个量级对深层网络来说太少,卷积核很容易记住训练样本的噪声,泛化能力上不去。

ResNet50 在 ImageNet 上见过 120 万张图、1000 个类别,它的卷积层已经学到了一套通用的视觉特征:浅层是边缘、纹理,中层是局部形状,深层是语义部件。猫和狗在 ImageNet 里本来就有对应类别,所以这套特征对猫狗任务天然适配。把 ResNet50 的卷积部分冻结,只把它当特征映射函数用,等于免费借用了别人几百万张图训出来的视觉先验。

这里有个反直觉的点:特征提取器越深不一定越好。ResNet50 的 2048 维特征对猫狗这种二分类已经绰绰有余,换成 ResNet152 提升有限,但推理时间翻倍。我一般先用 ResNet50 打底,效果不够再考虑换 backbone。

2.2 逻辑回归为什么够用

2048 维特征 + 二分类,逻辑回归的参数量是 2049 个(含偏置),这个量级下模型几乎不可能过拟合,训练也快。更重要的是它可解释:每个特征的权重直接告诉你哪些维度对「是狗」贡献大。相比之下,在特征上再接一个 MLP,虽然可能涨零点几个百分点,但调参成本和不确定性都上去了。

逻辑回归的损失函数是交叉熵,配合 L2 正则控制权重幅度。scikit-learn 的LogisticRegression默认就带 L2,C参数是正则强度的倒数,C 越小正则越强。这个参数是整条流程里最值得调的,后面会细说。

2.3 环境准备与依赖安装

先把环境搭起来。Python 建议 3.8 以上,PyTorch 和 torchvision 版本要匹配。如果你用 conda,一条命令搞定;用 pip 的话注意 CUDA 版本对应关系。

# 创建虚拟环境(conda 方式) conda create -n catdog python=3.9 -y conda activate catdog # 安装 PyTorch(CPU 版本,有 GPU 的话去官网查对应 CUDA 命令) pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装其余依赖 pip install scikit-learn numpy pillow tqdm

如果你用 vscode 配 python 环境,记得在右下角选对解释器,否则跑起来会报ModuleNotFoundError。这一步看着简单,但我见过太多人卡在这里,以为是代码问题,其实是解释器选错了。

依赖说明:torchvision里自带 ResNet50 的预训练权重,第一次调用会下载约 100MB 的模型文件,需要联网。tqdm只用来显示进度条,不影响功能。scikit-learn提供逻辑回归和评估指标。

3. 用 ResNet50 批量提取 2048 维特征

3.1 关键决策:取哪一层的输出

ResNet50 的结构是:conv1 → bn1 → relu → maxpool → layer1 → layer2 → layer3 → layer4 → avgpool → fc。最后的fc层输出 1000 维 ImageNet 类别分数,这个不能要,因为它是针对 ImageNet 类别训的。我们要的是avgpool之后的输出,形状是(batch, 2048, 1, 1),展平就是 2048 维特征向量。

在 PyTorch 里,最干净的做法是把模型的前面部分单独拿出来:

import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image import numpy as np import os from tqdm import tqdm # 加载预训练 ResNet50 resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) # 去掉最后的全连接层,保留到 avgpool # children() 返回各模块,去掉最后一个 fc 即可 feature_extractor = nn.Sequential(*list(resnet.children())[:-1]) # 切到推理模式,关闭 dropout 和 batchnorm 的训练行为 feature_extractor.eval() # 有 GPU 就上 GPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") feature_extractor = feature_extractor.to(device)

逻辑说明:list(resnet.children())[:-1]把除fc外的所有层打包成一个 Sequential,输入一张图输出(1, 2048, 1, 1)。eval()必须调用,否则 BatchNorm 会用当前 batch 的统计量,单张推理时结果会飘。参数上,weights=ResNet50_Weights.IMAGENET1K_V2是 torchvision 新版的写法,老版本用pretrained=True,效果一样。

3.2 图像预处理:必须和预训练时一致

ResNet50 训练时的预处理是:缩放到 256、中心裁剪 224、转张量、按 ImageNet 均值方差归一化。推理时如果预处理不一致,特征分布会偏移,逻辑回归的效果会明显下降。这是最容易被忽略的坑之一。

# 预处理管道,必须和 ImageNet 训练时一致 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] ), ]) def extract_single(img_path): """提取单张图的 2048 维特征""" img = Image.open(img_path).convert("RGB") # 强制 RGB,防止灰度图报错 tensor = preprocess(img).unsqueeze(0).to(device) # 加 batch 维度 with torch.no_grad(): # 关闭梯度,省显存 feat = feature_extractor(tensor) # (1, 2048, 1, 1) return feat.squeeze().cpu().numpy() # 展平成 (2048,)

参数说明:Resize(256)是短边缩到 256,长边按比例;CenterCrop(224)从中心裁 224。如果你的猫狗图片主体不在中心,可以考虑改成Resize((224,224))直接拉伸,但那样会变形,一般还是中心裁剪稳。convert("RGB")处理灰度图和 RGBA 图,不加这行遇到 PNG 透明通道会直接崩。

3.3 批量提取与特征缓存

两万多张图一张张过太慢,用 DataLoader 批量跑。更重要的是把特征存成.npy文件,逻辑回归调参时直接读缓存,不用每次重新过 ResNet50。

from torch.utils.data import Dataset, DataLoader class ImageFolderFlat(Dataset): """读取 data/class_name/xxx.jpg 结构的图片""" def __init__(self, root, transform): self.samples = [] self.transform = transform 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)) 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_all(root, batch_size=64): """批量提取整个数据集的 ResNet50 特征""" ds = ImageFolderFlat(root, preprocess) 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 = feature_extractor(imgs) # (B, 2048, 1, 1) out = out.squeeze(-1).squeeze(-1) # (B, 2048) feats.append(out.cpu().numpy()) labels.append(lbls.numpy()) return np.concatenate(feats), np.concatenate(labels) # 假设数据放在 data/train 和 data/val,各自下面有 cat/ 和 dog/ X_train, y_train = extract_all("data/train") X_val, y_val = extract_all("data/val") # 缓存到磁盘,下次直接加载 np.save("feat_train.npy", X_train) np.save("label_train.npy", y_train) np.save("feat_val.npy", X_val) np.save("label_val.npy", y_val) print("train:", X_train.shape, "val:", X_val.shape)

逻辑说明:shuffle=False保证特征和标签顺序对应,虽然这里标签是一起存的,但养成习惯。num_workers=4加速图片读取,Windows 上如果报错就改成 0。squeeze(-1).squeeze(-1)把(B,2048,1,1)压成(B,2048),比flatten(1)更直观。

参数说明:batch_size=64在 CPU 上大概占 1GB 内存,显存不够就降到 32 或 16。提取完的特征文件,两万张图约 160MB(float32),加载比重新推理快几十倍。

4. 在 2048 维特征上训练逻辑回归

4.1 标准化:逻辑回归的隐形前提

逻辑回归对特征尺度敏感。ResNet50 的 2048 维特征各维度方差差异很大,不标准化的话,梯度下降会震荡,正则也会不公平地惩罚大方差维度。标准做法是减均值除标准差。

from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, classification_report # 用训练集拟合 scaler,再应用到验证集 scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_val_s = scaler.transform(X_val) # 注意:用训练集的统计量,不能重新 fit

这里有个细节:验证集必须用训练集拟合出来的 scaler,不能自己 fit。否则等于把验证集的分布信息泄露进了预处理,评估结果会虚高。这个坑在特征工程里非常常见。

4.2 逻辑回归的关键参数:C 和 max_iter

scikit-learn 的LogisticRegression默认用 L2 正则,C是正则强度的倒数。C 越大正则越弱,C 越小正则越强。猫狗这种线性可分度较高的任务,C 在 0.01 到 10 之间都能用,但最优值值得搜一下。

# 基础训练 clf = LogisticRegression( C=1.0, # 正则强度倒数 max_iter=1000, # 迭代上限,特征维度高时默认 100 不够 solver="lbfgs", # 拟牛顿法,适合中小规模 multi_class="auto", # 二分类自动处理 n_jobs=-1 # 并行 ) 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=["cat", "dog"]))

参数说明:max_iter=1000是必须调的,2048 维特征下默认的 100 次迭代经常不收敛,会弹ConvergenceWarning。solver="lbfgs"对 L2 正则支持好,数据量再大可以换saga。n_jobs=-1用满所有 CPU 核。

4.3 用交叉验证选 C,而不是拍脑袋

C 的选择直接影响准确率,别凭感觉设。用训练集做 5 折交叉验证,扫一遍候选值。

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

跑完你会看到一条曲线:C 太小欠拟合,C 太大过拟合,中间有个平台。我实测猫狗任务上 C=0.1 到 1.0 之间差异很小,选 C=1.0 就够。但如果你换数据集,这个扫描步骤别省。

参数说明:cv=5是 5 折,数据量小可以调到 10。scoring="accuracy"对均衡数据集合适,类别不均衡要换f1。交叉验证在 2048 维、两万样本上大概跑几分钟,值得等。

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

5.1 现象:验证准确率只有 50% 上下,像随机猜

原因:预处理和预训练不一致。最常见的是忘了Normalize,或者用了Resize((224,224))直接拉伸导致形变。ResNet50 对输入分布很敏感,归一化漏掉的话特征基本是废的。

解决:打印一张图预处理后的均值和方差,确认在 0 附近、标准差在 1 附近。对照本文 3.2 的管道逐行检查,mean和std的顺序不能反。

5.2 现象:ConvergenceWarning: lbfgs failed to converge

原因:max_iter太小。2048 维特征下,lbfgs 需要更多迭代才能收敛,默认 100 次不够。

解决:把max_iter提到 1000 甚至 2000。如果还警告,说明特征尺度差异太大,检查 StandardScaler 是否真的应用了。实在不行换solver="saga",它对大规模数据更稳,但慢一些。

5.3 现象:验证集准确率比训练集高

原因:验证集的 scaler 是自己 fit 的,或者特征提取时验证集用了不同的预处理。前者是数据泄露,后者是分布不一致。

解决:确认X_val_s = scaler.transform(X_val)用的是训练集的 scaler。特征提取阶段,训练集和验证集必须走同一个preprocess对象,不能各写一份。

5.4 现象:提取特征时内存爆掉

原因:batch_size太大,或者一次性把所有图片读进内存。两万张 224x224 的图,float32 下约 12GB,直接 OOM。

解决:用 DataLoader 流式读取,batch_size降到 32。特征提取完立刻存.npy,不要在内存里堆着。如果图片特别大,先把数据集缩放到 256 存一份小图,再提取特征。

5.5 现象:单张推理时结果和批量不一致

原因:忘了feature_extractor.eval(),BatchNorm 在单张推理时用了自己的统计量,和训练时的滑动平均不一致。

解决:模型加载后立刻.eval(),推理时套torch.no_grad()。这两个是标配,别省。

6. 把这条基线用起来:从单张预测到特征复用

6.1 单张图片端到端预测

训练完的模型要能对新图做预测,把 scaler、逻辑回归、特征提取器串起来。

def predict(img_path): """端到端预测单张图片""" feat = extract_single(img_path) # (2048,) feat = scaler.transform(feat.reshape(1, -1)) # 标准化 prob = clf.predict_proba(feat)[0] # [cat_prob, dog_prob] label = "dog" if prob[1] > 0.5 else "cat" return label, prob label, prob = predict("test/mycat.jpg") print(f"预测: {label}, 置信度: {max(prob):.4f}")

逻辑说明:extract_single返回一维向量,reshape(1,-1)变成一行样本喂给 scaler。predict_proba返回两类概率,取大的那个。这套流程单张推理在 CPU 上约 50ms,够用。

6.2 特征复用:一次提取,多处使用

2048 维特征提取一次要十几分钟,但提取完就是通用的。同一份特征可以喂给 SVM、随机森林、XGBoost,横向对比哪个分类器最适合你的数据。我一般会顺手跑一个 LinearSVC 做对照:

from sklearn.svm import LinearSVC svm = LinearSVC(C=0.1, max_iter=5000) svm.fit(X_train_s, y_train) print("LinearSVC val acc:", accuracy_score(y_val, svm.predict(X_val_s)))

猫狗任务上 LinearSVC 和逻辑回归通常差不到 0.5 个百分点,但逻辑回归能输出概率、可解释性更好,所以我默认用逻辑回归。如果你的任务需要概率校准,逻辑回归是更稳的选择。

6.3 一个容易被忽略的技巧:特征拼接

如果手头有额外的元信息(比如图片尺寸、拍摄设备),可以把这些低维特征和 2048 维 ResNet 特征拼在一起再训逻辑回归。拼接前记得对元信息也做标准化,否则量纲差异会淹没视觉特征。这个技巧在工业场景里很实用,纯视觉特征搞不定的 case,加一两个业务特征往往能救回来。

6.4 验证方法:别只看准确率

准确率在类别均衡时够用,但猫狗大战的验证集如果猫狗比例是 6:4,准确率会虚高。养成看混淆矩阵和每类 F1 的习惯:

from sklearn.metrics import confusion_matrix import pandas as pd cm = confusion_matrix(y_val, pred) print(pd.DataFrame(cm, index=["true_cat","true_dog"], columns=["pred_cat","pred_dog"]))

如果某一类召回明显低,说明特征对那一类判别力不够,可以考虑换 backbone 或者做数据增强再提特征。我自己的习惯是:任何分类任务,先跑通这条 ResNet50 + 逻辑回归基线,拿到一个准确率数字,再决定要不要上更复杂的模型。这条基线跑一次不到半小时,但它给你的参照系,能帮你判断后面所有优化到底有没有用。希望帮到你。

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

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

数据库内存省一半?NVMatrix块存储EBS实战解析

内存价格这一轮涨得实在离谱,DDR4 从底部翻倍都不止,DDR5 更是让人不敢直视。做数据库运维的同学应该都体会过那种痛:业务说慢,开发说加内存,领导说看预算。一台 512G 内存的数据库服务器,光内存成本就能顶…

作者头像 李华
网站建设 2026/9/26 7:38:11

YOLO目标检测与云台伺服控制的工业级闭环实现

简介:本资源是一套基于YOLO的智能追踪云台完整实现方案,面向深度学习初学者、毕业设计与课程设计学生,解决实时目标检测与物理云台协同控制这一典型AI硬件落地问题。项目融合YOLOv8目标检测(含训练好的yolov8n.pt模型)…

作者头像 李华
网站建设 2026/9/26 7:38:07

别再盲目学Python了,这3个坑千万别踩

坑一:把语法当终点,从不写完整项目变量、循环、函数、类,这些语法半个月就能过一遍。很多人学完这些就觉得自己会Python了,然后开始刷面试题,背八股文。可一到实际项目,连一个文件读取加数据清洗都写不出来…

作者头像 李华
网站建设 2026/9/26 7:37:51

蓝牙GFSK调制原理与BT=0.5工程实践

1. 什么是GFSK?从蓝牙模块“连不上”说起你有没有遇到过这样的场景:手头一块HC-05蓝牙模块,接好串口、供电正常、AT指令也发得出去,可手机就是搜不到它;或者用ESP32做蓝牙串口透传,数据偶尔错乱、丢包率忽高…

作者头像 李华
网站建设 2026/9/26 7:36:34

CNN+Transformer运动想象脑电分类:本科毕设完整代码拆解与避坑指南

简介:这份本科毕业设计资源聚焦于基于Transformer的运动想象脑电信号分类,面向人工智能与生物医学工程交叉方向的本科生及脑机接口入门研究者。项目采用CNNTransformer混合框架,由CNN提取局部时空特征、Transformer捕捉全局依赖,覆…

作者头像 李华
网站建设 2026/9/26 7:36:16

WorkBuddy Skill加载实战:从SKILL.md编写到稳定复用

1. 为什么“加载一个真正用得上的 Skill”值得单独拿出来讲WorkBuddy 这类工具刚上手的时候,绝大多数人都会经历一个相同的阶段:装好、登录、随便丢几个问题进去,觉得“也就那样”。真正让体验发生质变的,往往不是模型本身换了多大…

作者头像 李华