简介这是一份面向深度学习入门与计算机视觉实践者的完整案例源码围绕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 做特征提取器而不是从头训 CNN2.1 迁移学习的核心逻辑特征复用从头训一个 CNN 分类猫狗本质是让网络自己从像素里学出「耳朵尖不尖」「毛色什么样」这些判别性特征。问题是猫狗大战训练集也就两万多张这个量级对深层网络来说太少卷积核很容易记住训练样本的噪声泛化能力上不去。ResNet50 在 ImageNet 上见过 120 万张图、1000 个类别它的卷积层已经学到了一套通用的视觉特征浅层是边缘、纹理中层是局部形状深层是语义部件。猫和狗在 ImageNet 里本来就有对应类别所以这套特征对猫狗任务天然适配。把 ResNet50 的卷积部分冻结只把它当特征映射函数用等于免费借用了别人几百万张图训出来的视觉先验。这里有个反直觉的点特征提取器越深不一定越好。ResNet50 的 2048 维特征对猫狗这种二分类已经绰绰有余换成 ResNet152 提升有限但推理时间翻倍。我一般先用 ResNet50 打底效果不够再考虑换 backbone。2.2 逻辑回归为什么够用2048 维特征 二分类逻辑回归的参数量是 2049 个含偏置这个量级下模型几乎不可能过拟合训练也快。更重要的是它可解释每个特征的权重直接告诉你哪些维度对「是狗」贡献大。相比之下在特征上再接一个 MLP虽然可能涨零点几个百分点但调参成本和不确定性都上去了。逻辑回归的损失函数是交叉熵配合 L2 正则控制权重幅度。scikit-learn 的LogisticRegression默认就带 L2C参数是正则强度的倒数C 越小正则越强。这个参数是整条流程里最值得调的后面会细说。2.3 环境准备与依赖安装先把环境搭起来。Python 建议 3.8 以上PyTorch 和 torchvision 版本要匹配。如果你用 conda一条命令搞定用 pip 的话注意 CUDA 版本对应关系。# 创建虚拟环境conda 方式 conda create -n catdog python3.9 -y conda activate catdog # 安装 PyTorchCPU 版本有 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(weightsmodels.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 的统计量单张推理时结果会飘。参数上weightsResNet50_Weights.IMAGENET1K_V2是 torchvision 新版的写法老版本用pretrainedTrue效果一样。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_size64): 批量提取整个数据集的 ResNet50 特征 ds ImageFolderFlat(root, preprocess) loader DataLoader(ds, batch_sizebatch_size, shuffleFalse, num_workers4) feats, labels [], [] with torch.no_grad(): for imgs, lbls in tqdm(loader, descfextract {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)逻辑说明shuffleFalse保证特征和标签顺序对应虽然这里标签是一起存的但养成习惯。num_workers4加速图片读取Windows 上如果报错就改成 0。squeeze(-1).squeeze(-1)把(B,2048,1,1)压成(B,2048)比flatten(1)更直观。参数说明batch_size64在 CPU 上大概占 1GB 内存显存不够就降到 32 或 16。提取完的特征文件两万张图约 160MBfloat32加载比重新推理快几十倍。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_iterscikit-learn 的LogisticRegression默认用 L2 正则C是正则强度的倒数。C 越大正则越弱C 越小正则越强。猫狗这种线性可分度较高的任务C 在 0.01 到 10 之间都能用但最优值值得搜一下。# 基础训练 clf LogisticRegression( C1.0, # 正则强度倒数 max_iter1000, # 迭代上限特征维度高时默认 100 不够 solverlbfgs, # 拟牛顿法适合中小规模 multi_classauto, # 二分类自动处理 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_iter1000是必须调的2048 维特征下默认的 100 次迭代经常不收敛会弹ConvergenceWarning。solverlbfgs对 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(CC, max_iter1000, solverlbfgs, n_jobs-1) scores cross_val_score(clf, X_train_s, y_train, cv5, scoringaccuracy) print(fC{C:7}: cv acc {scores.mean():.4f} (/- {scores.std():.4f}))跑完你会看到一条曲线C 太小欠拟合C 太大过拟合中间有个平台。我实测猫狗任务上 C0.1 到 1.0 之间差异很小选 C1.0 就够。但如果你换数据集这个扫描步骤别省。参数说明cv5是 5 折数据量小可以调到 10。scoringaccuracy对均衡数据集合适类别不均衡要换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 是否真的应用了。实在不行换solversaga它对大规模数据更稳但慢一些。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(C0.1, max_iter5000) 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 逻辑回归基线拿到一个准确率数字再决定要不要上更复杂的模型。这条基线跑一次不到半小时但它给你的参照系能帮你判断后面所有优化到底有没有用。希望帮到你。本文还有配套的精品资源点击获取
