第T5周:运动鞋品牌识别 - 学习笔记
## 🔗 声明
- **本文为「365 天深度学习训练营」内部学习记录。**
- **本文参考 K 同学啊课程内容完成,仅用于个人学习与交流。**
- **运动鞋品牌识别数据集仅用于学习交流,请勿对外分享数据集。**
- **本篇为个人在 T5 关卡上的实践记录。**
---
## 第 T5 周:运动鞋品牌识别
本文将采用 CNN 对运动鞋图片进行二分类,识别 **adidas** 与 **nike** 两个品牌。在 T4 猴痘病识别的基础上,本周新增 **指数衰减动态学习率(ExponentialDecay)**、**早停(EarlyStopping)** 与 **最佳模型保存**,并要求找出课程代码中被修改的参数并调到最佳。
### 📌 本周要求
- 学习 `ExponentialDecay` 动态学习率调度策略
- 使用 `EarlyStopping` 与 `ModelCheckpoint` 保存最佳模型
- **找出课程中被修改的参数并调整到最佳**(可在群内讨论)
- 加载最佳模型参数识别本地的一张图片
### 🚀 我的环境
- 语言环境:Python 3.6.5
- 编译器:Jupyter Notebook
- 深度学习框架:TensorFlow 2.4.1
- 显卡(GPU):NVIDIA GeForce RTX 3080
- 数据:百度网盘 / 和鲸链接(请不要对外分享数据集)
- 数据目录:`46-data/`(`train/` 与 `test/` 已划分,内含 `adidas` / `nike` 两类,共 **578** 张图片)
---
## 一、前期工作
### 1. 设置 GPU
如果使用的是 CPU 可以忽略这步。
```python
from tensorflow import keras
from tensorflow.keras import layers, models
import os, PIL, pathlib
import matplotlib.pyplot as plt
import tensorflow as tf
gpus = tf.config.list_physical_devices("GPU")
if gpus:
gpu0 = gpus[0] # 如果有多个 GPU,仅使用第 0 个 GPU
tf.config.experimental.set_memory_growth(gpu0, True) # 设置 GPU 显存用量按需使用
tf.config.set_visible_devices([gpu0], "GPU")
gpus
```
**预期输出(RTX 3080):**
```text
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
```
### 2. 导入数据
```python
data_dir = "./46-data/"
data_dir = pathlib.Path(data_dir)
```
将数据集解压到 `46-data` 文件夹,结构如下:
```text
46-data/
train/
adidas/
nike/
test/
adidas/
nike/
```
### 3. 查看数据
```python
image_count = len(list(data_dir.glob("*/*/*.jpg")))
print("图片总数为:", image_count)
roses = list(data_dir.glob("train/nike/*.jpg"))
PIL.Image.open(str(roses[0]))
```
**预期输出:**
```text
图片总数为: 578
```
---
## 二、数据预处理
### 1. 加载数据
使用 `image_dataset_from_directory` 方法将磁盘中的数据加载到 `tf.data.Dataset` 中。
**测试集与验证集的关系:**
- 验证集并没有参与训练过程梯度下降过程的,狭义上来讲是没有参与模型的参数训练更新的。
- 但是广义上来讲,验证集存在的意义确实参与了一个「人工调参」的过程,我们根据每一个 epoch 训练之后模型在 valid data 上的表现来决定是否需要训练进行 early stop,或者根据这个过程模型的性能变化来调整模型的超参数,如学习率、`batch_size` 等等。
- 因此,我们也可以认为,验证集也参与了训练,但是并没有使得模型去 overfit 验证集。
```python
batch_size = 32
img_height = 224
img_width = 224
```
```python
"""
关于 image_dataset_from_directory() 的详细介绍可以参考文章:
https://mtyjkh.blog.csdn.net/article/details/117018789
"""
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
"./46-data/train/",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size)
```
**预期输出:**
```text
Found 502 files belonging to 2 classes.
```
```python
"""
关于 image_dataset_from_directory() 的详细介绍可以参考文章:
https://mtyjkh.blog.csdn.net/article/details/117018789
"""
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
"./46-data/test/",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size)
```
**预期输出:**
```text
Found 76 files belonging to 2 classes.
```
我们可以通过 `class_names` 输出数据集的标签。标签将按字母顺序对应于目录名称。
```python
class_names = train_ds.class_names
print(class_names)
```
**预期输出:**
```text
['adidas', 'nike']
```
### 2. 可视化数据
```python
plt.figure(figsize=(20, 10))
for images, labels in train_ds.take(1):
for i in range(20):
ax = plt.subplot(5, 10, i + 1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[labels[i]])
plt.axis("off")
```
### 3. 再次检查数据
```python
for image_batch, labels_batch in train_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
```
**预期输出:**
```text
(32, 224, 224, 3)
(32,)
```
- **Image_batch** 是形状为 `(32, 224, 224, 3)` 的张量。这是一批 32 张 `224×224×3` 的图片(最后一维指的是彩色通道 RGB)。
- **Label_batch** 是形状为 `(32,)` 的张量,这些标签对应 32 张图片。
### 4. 配置数据集
```python
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
```
- **shuffle()**:打乱数据,关于此函数的详细介绍可以参考:[知乎文章](https://zhuanlan.zhihu.com/p/42417456)
- **prefetch()**:预取数据,加速运行
`prefetch()` 功能详细介绍:CPU 正在准备数据时,加速器处于空闲状态。相反,当加速器正在训练模型时,CPU 处于空闲状态。因此,训练所用的时间是 CPU 预处理时间和加速器训练时间的总和。`prefetch()` 将训练步骤的预处理和模型执行过程重叠到一起。当加速器正在执行第 N 个训练步时,CPU 正在准备第 N+1 步的数据。这样做不仅可以最大限度地缩短训练的单步用时(而不是总用时),而且可以缩短提取和转换数据所需的时间。如果不使用 `prefetch()`,CPU 和 GPU/TPU 在大部分时间都处于空闲状态;使用 `prefetch()` 可显著减少空闲时间。
- **cache()**:将数据集缓存到内存当中,加速运行
---
## 三、构建 CNN 网络
卷积神经网络(CNN)的输入是张量 (Tensor) 形式的 `(image_height, image_width, color_channels)`,包含了图像高度、宽度及颜色信息。不需要输入 batch size。`color_channels` 为 `(R,G,B)` 分别对应 RGB 的三个颜色通道(color channel)。在此示例中,我们的 CNN 输入的形状是 `(224, 224, 3)` 即彩色图像。我们需要在声明第一层时将形状赋值给参数 `input_shape`。
网络结构图(可单击放大查看):运行 `model.summary()` 可查看完整结构。
```python
"""
关于卷积核的计算不懂的可以参考文章:https://blog.csdn.net/qq_38251616/article/details/114278995
layers.Dropout(0.4) 作用是防止过拟合,提高模型的泛化能力。
关于 Dropout 层的更多介绍可以参考文章:https://mtyjkh.blog.csdn.net/article/details/115826689
"""
model = models.Sequential([
layers.experimental.preprocessing.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
layers.Conv2D(16, (3, 3), activation='relu', input_shape=(img_height, img_width, 3)), # 卷积层1,卷积核3*3
layers.AveragePooling2D((2, 2)), # 池化层1,2*2采样
layers.Conv2D(32, (3, 3), activation='relu'), # 卷积层2,卷积核3*3
layers.AveragePooling2D((2, 2)), # 池化层2,2*2采样
layers.Dropout(0.3), # 让神经元以一定的概率停止工作,防止过拟合
layers.Conv2D(64, (3, 3), activation='relu'), # 卷积层3,卷积核3*3
layers.Dropout(0.3),
layers.Flatten(), # Flatten层,连接卷积层与全连接层
layers.Dense(128, activation='relu'), # 全连接层,特征进一步提取
layers.Dense(len(class_names)) # 输出层,输出预期结果
])
model.summary() # 打印网络结构
```
**代码说明(与课程原文一致,结构正确):**
- `Rescaling(1./255)` 已放在网络第一层,训练时**无需**再手动 `/255.0`。
- 第一层 `Conv2D` 上的 `input_shape` 与 `Rescaling` 重复,属课程原文写法,可保留。
- 注释写 `Dropout(0.4)`,实际使用 `Dropout(0.3)`,以代码为准。
---
## 四、训练模型
在准备对模型进行训练之前,还需要再对其进行一些设置。以下内容是在模型的编译步骤中添加的:
- **损失函数(loss)**:用于衡量模型在训练期间的准确率。
- **优化器(optimizer)**:决定模型如何根据其看到的数据和自身的损失函数进行更新。
- **指标(metrics)**:用于监控训练和测试步骤。以下示例使用了准确率,即被正确分类的图像的比率。
### 1. 设置动态学习率
📮 **ExponentialDecay 函数**:
`tf.keras.optimizers.schedules.ExponentialDecay` 是 TensorFlow 中的一个学习率衰减策略,用于在训练神经网络时动态地降低学习率。
🔎 **主要参数**:
- `initial_learning_rate`(初始学习率):初始学习率大小。
- `decay_steps`(衰减步数):学习率衰减的步数。在经过 `decay_steps` 步后,学习率将按照指数函数衰减。**敲黑板:这里是指 steps,不是 epochs。**
- `decay_rate`(衰减率):学习率的衰减率,通常取值在 0 到 1 之间。
- `staircase`(阶梯式衰减):`True` 为阶梯下降,`False` 为连续衰减。
注:这里设置的动态学习率为指数衰减型(ExponentialDecay)。计算公式如下:
```text
learning_rate = initial_learning_rate * decay_rate ^ (step / decay_steps)
```
> **勘误**:课程原文写「每一个 epoch 开始前,学习率都会重置为初始学习率」——这与 `ExponentialDecay` 的实际行为不符。学习率按**全局训练 step** 累积衰减,**不会**在每个 epoch 开始时重置。
**学习率大与学习率小的优缺点分析:**
| | 优点 | 缺点 |
|---|---|---|
| 学习率大 | 加快学习速率;有助于跳出局部最优 | 易导致训练不收敛;模型精度下降 |
| 学习率小 | 有助于模型收敛与细化;提高精度 | 难跳出局部最优;收敛缓慢 |
📌 **本周被修改的参数(重点)**:
课程代码将 `initial_learning_rate` 设为 **`0.1`**。对比前四周经验:
- T2 / T3 / T4 中 Adam 优化器均使用 **`learning_rate=0.001`**
- `0.1` 比 `0.001` 大 **100 倍**,是本周需要找出的异常参数
**课程原文(含被修改的参数,勿直接照抄训练):**
```python
# 设置初始学习率
initial_learning_rate = 0.1
lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate,
decay_steps=10, # 敲黑板!!!这里是指 steps,不是指 epochs
decay_rate=0.92, # lr 经过一次衰减就会变成 decay_rate * lr
staircase=True)
# 将指数衰减学习率送入优化器
optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
model.compile(optimizer=optimizer,
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
```
**调整方案**:将 `initial_learning_rate` 改为与 T4 一致的 **`0.001`**(本地实测验证集最佳约 **84%**);社群常见写法为 **`2e-4`**(约 **68%–75%**)。二者均远优于课程默认的 `0.1`。
```python
# 设置初始学习率(课程默认 0.1 过大,按 T4 经验调整为 0.001)
initial_learning_rate = 0.001
lr_schedule = tf.keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate,
decay_steps=10, # 敲黑板!!!这里是指 steps,不是指 epochs
decay_rate=0.92,
staircase=True)
optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule)
model.compile(optimizer=optimizer,
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
```
### 2. 早停与保存最佳模型参数
**EarlyStopping() 参数说明:**
- `monitor`:被监测的数据。
- `min_delta`:在被监测的数据中被认为是提升的最小变化。
- `patience`:没有进步的训练轮数,在这之后训练就会被停止。
- `verbose`:详细信息模式。
- `mode`:`{auto, min, max}` 其中之一。
- `restore_best_weights`:是否从具有监测数量的最佳值的时期恢复模型权重。
```python
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
epochs = 50
# 保存最佳模型参数
checkpointer = ModelCheckpoint('best_model.h5',
monitor='val_accuracy',
verbose=1,
save_best_only=True,
save_weights_only=True)
# 设置早停
earlystopper = EarlyStopping(monitor='val_accuracy', min_delta=0.001,
patience=20, verbose=1)
```
### 3. 模型训练
```python
history = model.fit(train_ds,
validation_data=val_ds,
epochs=epochs,
callbacks=[checkpointer, earlystopper])
```
**训练日志节选(课程环境 TensorFlow 2.4.1 + `initial_learning_rate=0.1`):**
```text
Epoch 1/50
16/16 [==============================] - 4s 31ms/step - loss: 3.5439 - accuracy: 0.4721 - val_loss: 0.6931 - val_accuracy: 0.5789
Epoch 00001: val_accuracy improved from -inf to 0.57895, saving model to best_model.h5
......
Epoch 00040: val_accuracy did not improve from 0.89474
Epoch 41/50
16/16 [==============================] - 0s 12ms/step - loss: 0.0931 - accuracy: 0.9841 - val_loss: 0.3837 - val_accuracy: 0.8816
Epoch 00041: val_accuracy did not improve from 0.89474
Epoch 42/50
16/16 [==============================] - 0s 12ms/step - loss: 0.0871 - accuracy: 0.9801 - val_loss: 0.3834 - val_accuracy: 0.8816
Epoch 00042: val_accuracy did not improve from 0.89474
Epoch 00042: early stopping
```
第 40 轮左右验证集最佳准确率约 **89.47%**。若将 `initial_learning_rate` 调整为 **`0.001`**,在较新 TensorFlow 版本上同样可稳定训练到 **80%+**。
---
## 五、模型评估
### 1. Loss 与 Accuracy 图
```python
from datetime import datetime
import numpy as np
current_time = datetime.now() # 获取当前时间
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(len(loss))
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time) # 打卡请带上时间戳,否则代码截图无效
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
```
### 2. 指定图片进行预测
```python
# 加载效果最好的模型权重
model.load_weights('best_model.h5')
```
```python
from PIL import Image
import numpy as np
# img = Image.open("./45-data/Monkeypox/M06_01_04.jpg") # 这里选择你需要预测的图片
img = Image.open("./46-data/test/nike/1.jpg") # 这里选择你需要预测的图片
image = tf.image.resize(img, [img_height, img_width])
img_array = tf.expand_dims(image, 0) # 不需要再 /255.0,模型首层 Rescaling 会自动归一化
predictions = model.predict(img_array) # 这里选用你已经训练好的模型
print("预测结果为:", class_names[np.argmax(predictions)])
```
**预期输出:**
```text
预测结果为: nike
```
> **预测注意**:必须先 `model.load_weights('best_model.h5')` 加载最佳权重;图片路径 `./46-data/test/nike/1.jpg` 在数据集中存在。课程原文中 `#/255.0` 被注释掉是**正确**的——因为归一化已由模型内的 `Rescaling` 层完成。
---
## 答疑
### 如何找出本周被修改的参数?
沿着「前四周 → 本周新增」的线索排查:
1. T2/T3/T4 均使用固定 `Adam(learning_rate=0.001)`,训练稳定;
2. T5 新增 `ExponentialDecay`,课程将 `initial_learning_rate` 设为 `0.1`;
3. 用 `0.1` 训练时首轮 loss 异常偏高、验证集准确率长期低于 60%;
4. 将 `initial_learning_rate` 改为 **`0.001`**(与 T4 一致)或 **`2e-4`**(社群常见写法)后,训练更稳定,验证集准确率可达 **80%+**。
### `decay_steps=10` 是 10 个 epoch 还是 10 个 batch?
是 **10 个训练 step(batch)**,不是 10 个 epoch。若希望每 N 个 epoch 衰减一次,应设为 `decay_steps = steps_per_epoch * N`,其中 `steps_per_epoch = 样本数 / batch_size`。
### T4 与 T5 的数据划分有何不同?
- **T4**:单一目录 `45-data/`,用 `validation_split=0.2` 随机划分训练/验证。
- **T5**:数据集已按 `train/` 与 `test/` 分好,分别加载为训练集与验证集。
---
## 总结
本周完成了运动鞋品牌二分类的完整流程:本地数据加载 → CNN 训练 → **ExponentialDecay 动态学习率** → **EarlyStopping + ModelCheckpoint** → 指定本地图片预测。
核心收获是学会**对比历史实验找异常超参**:课程将 `initial_learning_rate` 设为 `0.1`,而 T2–T4 均使用 `0.001` 量级,据此即可定位并修正。跑通 `t5.ipynb` 后,可将实际训练日志与曲线截图补充到本文。
更多推荐


所有评论(0)