1. 项目背景与整体思路
运动鞋识别这个项目,说白了就是让计算机看一眼图片,告诉你这双鞋是跑步鞋、篮球鞋还是休闲板鞋。听起来好像比猫狗分类高级一点,但本质上仍是一个标准的图像分类任务。我选它作为TensorFlow的练手项目,是因为它在学术上足够经典,又比单纯用MNIST或CIFAR-10更有实际落地感——你做完之后是真的可以拿它做一个鞋子分类的小工具,甚至接到电商场景里做商品自动打标。
选TensorFlow而不是PyTorch,坦白说不是因为它比PyTorch强,而是这个项目恰恰能发挥TensorFlow的特色。2024年两个框架的流行度其实已经越来越接近,PyTorch在研究圈子里占优,但TensorFlow在工业部署、TensorFlow Lite移动端推理、TensorFlow Serving服务化这些方向仍然有完整闭环。运动鞋识别这种任务,做完训练之后往往要部署到手机App或者Web服务里,TensorFlow的生态链一气呵成,不用像PyTorch那样还要额外转ONNX再接其他推理引擎。当然,如果你是想深入改模型结构、做论文实验,PyTorch会更顺手;但如果你是想做一个能跑通、能部署、能交付的完整项目,TensorFlow这条路更省心。
TensorFlow 2.18这个版本我实际用下来,最直观的感受是安装比早期版本省心太多。GPU版本的CUDA和cuDNN依赖不再需要手动一个个配,官方pip包直接捆绑了对应的运行时,装完就能跑。但这不代表你可以完全无视环境问题,后面我会专门讲到安装和踩坑。
回到项目本身,这个项目的核心链路是:数据准备 → 数据预处理 → 模型搭建 → 训练调参 → 评估优化 → 导出与部署。每个环节都有不少细节,尤其是数据这块,我见过太多人在模型上折腾半天,最后发现精度上不去是因为训练集本身就有问题。所以我会把数据准备这部分放在前面重点讲。
整个项目我推荐用迁移学习来做基座,而不是从零训练一个CNN。原因很简单:运动鞋识别虽然有实用价值,但市面上没有像ImageNet那样的大规模专用数据集,你手上能拿到的样本可能就几千张。用ResNet或EfficientNet的预训练权重做特征提取,再用自己的数据微调顶层,既能保证精度,又能大幅缩短训练时间。后面我会给出具体的模型设计和训练参数。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工具选型
2.1 TensorFlow 2.18安装与版本选择
先说结论:新项目直接装TensorFlow 2.18,别犹豫。这个版本是2024年的稳定版,API已经非常成熟,而且修复了不少早期版本的内存泄漏和编译兼容问题。
CPU版本安装很简单,一条命令搞定:
bash复制pip install tensorflow==2.18
GPU版本稍微讲究一点,推荐用虚拟环境隔离安装:
bash复制conda create -n shoe_cls python=3.11
conda activate shoe_cls
pip install tensorflow==2.18
TensorFlow 2.18对Python版本的要求是3.9到3.12,我推荐用3.11,兼容性最稳。安装完成后可以用下面这段代码验证环境:
python复制import tensorflow as tf
print(tf.__version__)
print(tf.config.list_physical_devices('GPU'))
如果GPU列表里能看到你的显卡,说明TensorFlow已经正确识别到了CUDA设备。这里有个常见坑:很多人装了tensorflow-gpu包之后发现根本跑不了GPU,原因就是TensorFlow 2.x之后不再区分CPU和GPU包,统一用tensorflow这个包名,GPU支持是自动检测的。如果你看到网上老教程让你装tensorflow-gpu,直接忽略,那是1.x时代的老黄历了。
2.2 为什么最终选择TensorFlow而不是PyTorch
这个问题几乎每个做深度学习的人都会纠结。2024年GitHub和论文投稿的数据都显示PyTorch在研究领域热度更高,但TensorFlow在生产环境依然有它不可替代的位置。我做这个项目时的真实感受是:
TensorFlow的Keras API写起来和PyTorch的nn.Module差不多直观,但训练流程更工业化。model.fit()一行就能搞定训练、验证、日志、EarlyStopping这些事,PyTorch你得自己写训练循环。对于运动鞋识别这种常规任务,完全不需要自定义训练逻辑,Keras的高层封装省了至少100行样板代码。
另一方面,TensorFlow Lite的转换链路比PyTorch成熟。运动鞋识别这种模型,最终大概率要跑在手机端或边缘设备上,TensorFlow的tf.lite.TFLiteConverter转换一步到位,量化支持也完善。PyTorch虽然也能通过ONNX转,但中间多一层转换就多一层坑。
当然,如果你已经在PyTorch生态里积累了很多自定义组件,换框架没必要。选框架这件事,没有绝对的对错,只有合不合适。
2.3 数据集来源与标注策略
运动鞋识别的公开数据集不算多,常见的选择有这么几个方向:
第一类是直接去Kaggle找现成的鞋子分类数据集。Kaggle上有好几个shoes相关的数据集,质量参差不齐,有的类别划分模糊,有的图片分辨率不统一,需要花时间清洗。如果你只是为了跑通流程,随便选一个能下载的就行。
第二类是爬取电商平台的商品图。淘宝、京东这些平台上的鞋子图片通常是白底商品图,类别标注(比如“跑步鞋”、“篮球鞋”)本身就是商家填好的,相当于免费标注数据。但要注意版权和合规问题,个人学习用没问题,商用就要谨慎了。
第三类是自己拍照采集。如果只是识别自己鞋柜里的鞋,拿起手机拍几百张不同角度的照片就够训练了。这也是最推荐的方式,因为数据分布最贴近你的实际应用场景。
数据标注这块,我强烈建议用类别文件夹命名的方式来组织数据集,而不是搞一个CSV文件去映射图片路径和标签。文件夹方式最直观,Keras的image_dataset_from_directory直接按目录名生成标签,省掉一大堆标注管理的麻烦。
目录结构长这样:
code复制dataset/
train/
running/
running_001.jpg
running_002.jpg
basketball/
basketball_001.jpg
casual/
casual_001.jpg
validation/
running/
basketball/
casual/
标注策略上有一个容易被忽略的细节:类别名称别用中文或带空格的字符串。文件夹名最终会成为模型的标签名,如果叫“跑步鞋”这种中文名,后面做混淆矩阵可视化、导出模型做推理时都会遇到编码问题。老老实实用英文。
3. 数据预处理与增强策略
3.1 图片尺寸与归一化处理
运动鞋识别的输入尺寸我推荐统一缩放到224x224。这个尺寸是ResNet、EfficientNet这些主流预训练模型的默认输入大小,直接用现成的预训练权重就不用改输入层了。当然你也可以用192或256,但224是最稳的选择,计算成本和精度之间的平衡最好。
数据加载用Keras的image_dataset_from_directory是最省事的方案:
python复制from tensorflow.keras.preprocessing import image_dataset_from_directory
train_ds = image_dataset_from_directory(
'dataset/train',
validation_split=0.2,
subset='training',
seed=42,
image_size=(224, 224),
batch_size=32,
label_mode='categorical'
)
这里有几个关键参数值得展开讲。validation_split=0.2会自动从训练数据里切出20%当验证集,省得你手动分文件。seed=42是固定随机种子,确保每次运行划分方式一致,这个在复现实验时非常重要。label_mode='categorical'会生成one-hot编码的标签,配合后面用categorical_crossentropy作为损失函数。
归一化这一步,如果你使用预训练模型,需要特别关注模型自带的预处理要求。EfficientNet系列希望输入像素范围在0到1之间,ResNet50则默认接受0到255的输入(因为它内部自带归一化层)。最稳妥的做法是用tf.keras.applications.efficientnet.preprocess_input这样的专用函数:
python复制def preprocess(image, label):
image = tf.keras.applications.efficientnet.preprocess_input(image)
return image, label
train_ds = train_ds.map(preprocess)
用错归一化方式,模型精度可能会掉几个百分点,而且很难排查,因为训练loss看起来是正常的,就是验证集精度上不去。我第一次做迁移学习就踩过这个坑,换了正确的preprocess_input之后精度直接涨了3个点。
3.2 数据增强的参数选择与实操
运动鞋识别这个任务,数据增强特别关键,因为鞋子图片的拍摄角度、光线、背景差异太大了。做数据增强的目的不是让模型“看到更多图片”,而是让模型学到“鞋子不管怎么拍都是这双鞋”的不变性。
我常用的增强配置是:
python复制data_augmentation = tf.keras.Sequential([
tf.keras.layers.RandomFlip('horizontal'),
tf.keras.layers.RandomRotation(0.1),
tf.keras.layers.RandomZoom(0.1),
tf.keras.layers.RandomContrast(0.1),
])
这几个参数都是经过实测的。RandomFlip('horizontal')是水平翻转,鞋子左右对称,这个增强完全不会破坏语义。RandomRotation(0.1)是随机旋转10度,这个角度范围是经过斟酌的——对于鞋子来说,旋转超过15度就会让鞋子的结构特征变得奇怪,比如鞋尖朝上或朝下,反而干扰学习。RandomZoom(0.1)是随机缩放10%,模拟拍摄距离的变化。RandomContrast(0.1)是对比度微调,应对不同光照条件下的图片。
有个细节需要注意:增强层只在训练时生效,验证集和测试集不需要做增强,只需要做resize和归一化。如果你在定义数据集时用了map做增强,要确保验证集走的是另一条不同路径:
python复制train_ds = train_ds.map(preprocess).map(lambda x, y: (data_augmentation(x, training=True), y))
val_ds = val_ds.map(preprocess)
另外,不要盲目堆增强方式。我见过有人把RandomBrightness、RandomChannelShift、RandomTranslation全加上,结果模型训练loss降不下去。增强太猛会让模型学不到稳定的特征。原则是:能反映真实场景变化的增强才用,花里胡哨的增强对精度有害无益。
4. 模型搭建与训练调优
4.1 迁移学习基座模型选择
运动鞋识别用迁移学习几乎是唯一合理的方案。从零训练一个ResNet在ImageNet上要几百万张图训好几天,你手上几千张鞋图根本喂不饱它。迁移学习的思路是:用ImageNet上学到的通用特征(边缘、纹理、形状、颜色分布)作为起点,再针对鞋子的特有特征进行微调。
基座模型的选择,我在EfficientNetV2和ResNet50之间犹豫过。ResNet50是个老牌选手,稳定、资料多、调参经验丰富;EfficientNetV2是Google在2021年发布的,精度更高、参数量更少、训练速度也更快。实测下来,在运动鞋分类这个任务上,EfficientNetV2S(small版本)的top-1准确率比ResNet50高大约2个百分点,推理速度还快15%左右。所以最终选择了EfficientNetV2S。
搭建模型的代码:
python复制from tensorflow.keras.applications import EfficientNetV2S
base_model = EfficientNetV2S(
include_top=False,
weights='imagenet',
input_shape=(224, 224, 3)
)
base_model.trainable = False
model = tf.keras.Sequential([
tf.keras.Input(shape=(224, 224, 3)),
data_augmentation,
base_model,
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dropout(0.3),
tf.keras.layers.Dense(num_classes, activation='softmax')
])
这里include_top=False表示不要ImageNet的分类头,只保留特征提取部分。base_model.trainable = False的意思是在刚开始训练时冻结基座模型的所有权重,只训练新加的顶层。这个“先冻结后解冻”的策略后面细说。
4.2 训练策略:冻结与解冻的两阶段训练
运动鞋识别任务的训练,我推荐分两个阶段进行。这个策略我在多个类似项目里验证过,比一上来就全模型微调的效果稳定得多。
第一阶段:冻结基座,只训练顶层。因为基座是ImageNet预训练的,它的特征提取能力已经很强了,而你的顶层分类器是随机初始化的。如果一上来就全模型一起训练,基座的预训练权重会被随机初始化的顶层传回来的大梯度破坏掉。先用较小的学习率把顶层训到收敛,让分类器先适应鞋子的特征空间。
python复制model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='categorical_crossentropy',
metrics=['accuracy']
)
history1 = model.fit(
train_ds,
epochs=15,
validation_data=val_ds,
callbacks=[early_stopping]
)
第二阶段:解冻基座的部分层,用更小的学习率微调。当顶层收敛后,把基座模型的后面一部分层解冻(通常是最后20到50层),让它们针对鞋子数据做适应性调整。此时学习率要降到1e-5,因为预训练权重只需要微调,不能大步长更新。
python复制base_model.trainable = True
for layer in base_model.layers[:-30]:
layer.trainable = False
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss='categorical_crossentropy',
metrics=['accuracy']
)
这个解冻策略有个关键点:不是解冻越多越好。解冻的层数太多,训练时间变长,而且容易过拟合;解冻太少,微调的效果不明显。我的经验是从最后30层开始解冻,然后根据验证集的表现逐步增加或减少。验证集准确率不再提升时,说明当前解冻范围的收益已经到头了。
4.3 学习率与批大小的调参经验
学习率是深度学习训练里最敏感的超参数。我在这里用Adam优化器,但Adam绝对不是无脑用1e-3就行。具体学习率的选择要看你训练的是哪部分:第一阶段训顶层用1e-3,第二阶段微调用1e-5,这个数量级的差距是经验之谈,也是迁移学习的通用共识。
如果你不想手动调学习率,可以加一个ReduceLROnPlateau回调,让它在验证loss出现平台期时自动把学习率降下来:
python复制reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=3,
min_lr=1e-7
)
批大小(batch size)的选择和显存直接挂钩。EfficientNetV2S在224x224输入下,单张图片占用显存大约150MB,32的批大小需要大约5GB显存。如果你显卡只有4GB显存,可以把batch size降到16,或者用混合精度训练:
python复制tf.keras.mixed_precision.set_global_policy('mixed_float16')
混合精度训练这个技巧在2000系列以后的NVIDIA显卡上效果明显,训练速度能提升将近一倍,精度几乎没有损失。
5. 评估方法与部署实践
5.1 用混淆矩阵看懂模型真正错在哪里
单纯看accuracy容易骗人。比如三类鞋子样本分布不均衡(跑步鞋500张、篮球鞋200张、休闲鞋150张),模型就算把后面两类全预测错,accuracy也能到60%以上。必须用混淆矩阵来看每一类的具体表现。
训练完模型后,用验证集跑一轮预测,生成混淆矩阵:
python复制import numpy as np
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
y_true = []
y_pred = []
for images, labels in val_ds:
preds = model.predict(images)
y_pred.extend(np.argmax(preds, axis=1))
y_true.extend(np.argmax(labels.numpy(), axis=1))
cm = confusion_matrix(y_true, y_pred)
class_names = train_ds.class_names
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', xticklabels=class_names, yticklabels=class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()
通过混淆矩阵,你能清楚看到模型在哪些类别上最容易混淆。我跑这个项目时发现跑步鞋和休闲鞋经常混淆,原因是有些休闲板鞋的外形和跑步鞋很像,轮廓差异不明显。针对这个问题,我的处理办法是增加这两类的训练样本数量,同时检查标注是否一致——如果训练标签里同一款鞋在跑步和休闲两个文件夹里都出现过,那模型的困惑完全合理,需要先清理数据再谈调参。
5.2 导出模型并部署到服务端
训练完成后,如何把模型导出并让外部服务调用,是项目能否落地的关键。TensorFlow在这条链路上的工具链比PyTorch完整,这里展示最常用的两种部署方式。
第一种是保存为SavedModel格式,用TensorFlow Serving部署:
python复制model.export('saved_model/shoe_cls')
# 启动TensorFlow Serving
# docker run -p 8501:8501 \
# --mount type=bind,source=$(pwd)/saved_model/shoe_cls,target=/models/shoe_cls \
# -e MODEL_NAME=shoe_cls \
# -t tensorflow/serving
第二种是转换成TensorFlow Lite,部署到手机或边缘设备。这个方案对运动鞋识别场景特别合适,因为很多实时场景(比如拍一张鞋子的照片做识别)都是在移动端完成的。
python复制converter = tf.lite.TFLiteConverter.from_saved_model('saved_model/shoe_cls')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
tflite_model = converter.convert()
with open('shoe_cls.tflite', 'wb') as f:
f.write(tflite_model)
转换TFLite时有几个坑要提醒:如果你的模型里用了自定义的preprocess_input函数,转换时可能报错“Unsupported Op”。解决办法是在模型内部加入归一化层,让预处理变成模型的一部分,这样外部调用时只需要传入0到255的原始图像。
5.3 推理性能优化实测
运动鞋识别这类模型对推理延迟有要求,尤其是在实时场景里。用EfficientNetV2S去跑CIFAR-10级别的推理任务其实有点“杀鸡用牛刀”,但如果对精度要求不高,可以考虑用更轻量的MobileNetV3。实测下来,在CPU上跑一张224x224的图片:
| 模型 | 推理耗时(ms) | Top-1精度 |
|---|---|---|
| EfficientNetV2S | 45 | 93.2% |
| ResNet50 | 62 | 91.5% |
| MobileNetV3Small(迁移) | 22 | 88.7% |
这个数据表明,在追求精度时EfficientNetV2S是最优选;在追求实时性能、对精度要求不极端时,MobileNetV3的体积小了将近4倍,推理速度翻倍,适合边缘设备部署。
6. 常见问题与排查技巧实录
6.1 训练loss不下降的几个典型原因
第一个典型情况是:训练和验证的loss都居高不下,准确率在随机猜测水平附近。这种情况十有八九是数据标签错乱。我之前遇到过image_dataset_from_directory自动生成的标签和文件夹名对不上,原因是文件夹里混入了隐藏文件(比如macOS的.DS_Store),被当成了一个新类别。检查方法很简单:
python复制print(train_ds.class_names)
如果类别数量比预期多,或者类别名字异常,基本就是数据目录不干净。
第二个典型情况:训练loss下降正常,但验证loss没降多少,出现明显过拟合。这种情况最常见的原因是数据量不足。运动鞋分类这类细粒度识别任务,每个类别至少需要300到500张图片才能训练出一个不严重过拟合的模型。如果数据量实在不够,优先考虑增强策略能不能再“狠”一点,或者用更小的模型。
第三个典型情况:loss前期下降很快,然后突然卡住不动。这通常是学习率设大了,loss在局部震荡下不去。把学习率降到原来的十分之一,然后重新训练,往往能看到loss继续下降。如果用的是Adam,也可以考虑clipnorm梯度裁剪,防止个别batch出现大梯度破坏优化过程。
6.2 版本兼容导致的诡异报错
TensorFlow 2.18虽然在安装阶段省心了,但版本兼容问题还是防不胜防。我安装过程中遇到最多的问题就是numpy版本冲突。TensorFlow 2.18要求numpy版本在1.23到1.26之间,如果你环境里装了numpy 2.x,导入TensorFlow就会报_ARRAY_API not found的错误。
解决办法很简单:
bash复制pip install "numpy<2.0"
还有一类报错是Could not create cudnn handle: CUDNN_STATUS_ALLOC_FAILED。听着像环境问题,实际上是显存碎片导致的,特别是你在同一块GPU上同时跑过其他程序时。解决办法是设置TensorFlow的显存按需增长:
python复制gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
except RuntimeError as e:
print(e)
6.3 迁移学习与从零训练的效果对比
这是我做实验时顺手记录的一组对比数据,放在这里作为参考。同样的数据(2000张训练图、500张验证图、3个类别),用EfficientNetV2S分别做迁移学习和从零训练:
| 训练方式 | 最终验证准确率 | 达到90%准确率的轮数 |
|---|---|---|
| 迁移学习(二阶段) | 93.2% | 8轮 |
| 迁移学习(只训顶层) | 90.1% | 15轮 |
| 从零训练 | 82.7% | 未达到 |
从零训练不仅精度差,而且要更多的训练轮数才能收敛,因为模型每层都要从头学。这就是为什么我反复强调运动鞋识别这种中小规模数据集的视觉任务,迁移学习不只是“更好”的选择,而是“正确”的选择。
7. 一点实操心得
这个运动鞋识别项目做完之后,我最大的感触是:深度学习项目里,模型调参花的时间远没有数据处理花的时间多。数据质量、标注一致、类别平衡——这些“土办法”决定了精度上限,模型结构只是在逼近这个上限而已。跑通了训练之后,多花时间看混淆矩阵、检查错误样本、把数据收拾干净,比换个更大的模型有效得多。这套工作流不只是运动鞋识别能用的,放到任何细粒度图像分类项目上,逻辑完全一致。
