## 🔗 声明

- **本文为「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` 后,可将实际训练日志与曲线截图补充到本文。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐