吴恩达深度学习课程二: 改善深层神经网络 第三周:超参数调整,批量标准化和编程框架 课后习题和代码实践

此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:

  1. 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
  2. github课程资料,含课件与笔记:吴恩达深度学习教学资料
  3. 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案

本篇为第二课第三周的课程习题和代码实践部分笔记。


1. 理论习题:独热编码

还是先上链接:
【中英】【吴恩达课后测验】Course 2 - 改善深层神经网络 - 第三周测验
因为本周内容大多是一些补充,因此习题也大多只是之前了解到的死知识,就不再多提了。
这部分补充一个之前出现的技术:独热编码

1.1 独热编码(One-Hot Encoding)

之前在多值预测与多分类这部分里,我们提到过,在多分类的情况下, 使用独热编码来表示各个分类,现在就来展开一下这个技术。
为了让解释更直观,我们全程使用同一个实例:

例子:识别动物类别,有三类:猫(Cat)、狗(Dog)、兔子(Rabbit)。
我们用这个例子贯穿整个独热编码的说明。

(1)用独热编码表示分类的直接形式

在多分类中,一个类别就是一个“离散的标签”,没有数值大小,也不存在“比谁大一点”的概念
但神经网络输出的是一组数字,为了让网络能理解“哪个类别是正确答案”,我们就需要把“猫/狗/兔子”变成神经网络能处理的格式。

独热编码就是最直接的方式:每一个类别对应一个位置,正确的那个位置为 1,其余为 0。
来看看具体怎么做:现在对我们的动物识别例子做独热编码处理,结果如下:

类别 独热编码
猫 Cat [1, 0, 0]
狗 Dog [0, 1, 0]
兔 Rabbit [0, 0, 1]

其中:

  • 三个神经元对应三个分类
  • “1”表示正确分类,“0”表示不是
  • 标签永远只有一个位置是 1

这就是“独热”:只有一个地方热
这是它对多分类的直接表现形式。

(2)为什么二分类不使用独热编码?

那你可能会问:
“既然多分类用独热,那二分类是不是也能写成 [1,0][0,1]?”
答案:理论上可以,实践中不会这么做
原因很简单:没必要
简单展开一下:
二分类的本质是:是否属于某一类(比如“是不是猫”)

只需要一个神经元 + sigmoid,就能表达“是的概率”。
这意味着 一个神经元就能表达整个二分类的状态

这种结构浪费计算,还会带来梯度重复问题。
什么叫“梯度重复”?,这是softmax在二分类应用中出现的问题。

假设某张图真实标签是:

猫 → [1, 0]

而模型预测是:

ŷ = [0.4, 0.6]

也就是模型认为“不是猫”的概率比“是猫”还高。
根据交叉熵,我们得到两个神经元的损失项:

L1 = -1 * log(0.4)
L2 = -0 * log(0.6)

乍看只第一个有影响。
但真正计算梯度时,Softmax 会让两个神经元一起参与:

  • 第 1 个神经元(猫)要把概率从 0.4 推到更高
  • 第 2 个神经元(不是猫)要把概率从 0.6 推到更低

于是反向传播时两个神经元都会更新:

  • 第一个神经元:“我应该更强一点”
  • 第二个神经元:“我应该更弱一点”
    这就产生了两个方向相反但意义重复的梯度

而这两个神经元本质上是一件事:

P(不是猫) = 1 − P(是猫)

所以,这种结构就是让网络:

  • 学一次“猫应该更强”
  • 再学一次“不是猫应该更弱”

这其实是同一条语义的两次更新

这就是二分类使用softmax带来的梯度重复现象:
它让模型参数增加,训练更慢,softmax还让两个输出互相牵连,一个升另一降,让本来很简单的二分类被人为增加了耦合难度。

(3) 多分类不使用独热编码的影响

那多分类为什么不能像二分类一样直接写成0,1,2呢?
就像这样:

类别 非独热写法
0
1
2

你可能已经发现了问题所在,我们在一开始就强调了:类别不存在“比谁大一点”的概念
使用上面这种分类方法带俩的严重问题就是:神经网络会错误地认为“兔 > 狗 > 猫”

再简单展开一下:
在这种分类方式下,模型会把 “误差”理解为数值距离

例如真实标签是“兔 = 2”,模型预测成“猫 = 0”。
模型认为误差 = |2 − 0| = 2
那预测成“狗 1”误差就会变小。
于是, 模型会错误地认为预测成“狗”比预测成“猫”更接近正确答案,带来梯度的混乱
但实际上,我们知道:“猫”和“狗”与“兔”之间没有“更近”的关系,它们应该是三种平行的、不可比较的类别
所以这种写法会导致训练逻辑错误,学习方向混乱,效果极差。

(4)独热编码对多分类的适配性

现在再来看看独热编码的优势。
继续使用我们动物识别例子:
真实标签“兔子” → [0, 0, 1]
假设模型输出的是 Softmax 后的概率:

预测为:猫   0.1
预测为:狗   0.2
预测为:兔   0.7

Softmax 输出为:

ŷ = [0.1, 0.2, 0.7]

真实标签为:

y  = [0, 0, 1]

交叉熵损失就很自然:

Loss = - log(预测为兔的概率) = -log(0.7)

只有正确类别那一项会参与计算,其余项为 0,不影响损失。

但重点来了:虽然损失项只有一项,但梯度来自所有类别
上面的损失表达式容易让人误解:“只有一项有用,那是不是梯度也只来自那一项?”
其实不是。

我们继续看:
Softmax + CrossEntropy 的梯度公式非常简单:

\[\frac{\partial L}{\partial z_i} = \hat{y}_i - y_i \]

代入我们的例子:

  • 对“猫”神经元:\(0.1 - 0 = 0.1\)
  • 对“狗”神经元:\(0.2 - 0 = 0.2\)
  • 对“兔”神经元:\(0.7 - 1 = -0.3\)
    可以看到:
  • “兔” 的梯度是负的 → 相关参数会变大(让概率更接近 1)
  • “猫”和“狗”的梯度是正的 → 相关参数会变小(让概率更接近 0)

这恰好符合我们对多分类的直观理解: 正确类变得更确定,其他类一起被压下去。

这就是多分类中,独热编码,softmax,交叉熵形成的更新链条,我们在下面的实践部分就能感受到它的效果。

2. 代码实践

在课程要求里,这周的实践作业是Tensorflow的入门,主要以了解Tensorflow的基本原理和语法为主,还是把这位博主的链接放在前面,介绍了使用Tensorflow构建神经网络的过程。
【中文】【吴恩达课后编程作业】Course 2 - 改善深层神经网络 - 第三周作业

虽然依然使用Pytorch来进行演示,但随着引入Tensorflow框架,之后课程内容对此的介绍和使用也会增加。因此,之后我都会在最后附上一个Tensorflow版本的代码。

2.1 多分类数据集

为了演示本周的内容,我们暂时放下之前的猫狗二分类数据集。
这次,我们使用一个新的数据集:手写数字图像识别
你可能之前已经知道这个数据集了,它并不需要我们和之前一样在网上寻找数据集下载。
pytorch内置了这个经典数据集的下载链接,我们可以直接通过API下载它到项目目录:

from torchvision import datasets, transforms  
from torch.utils.data import DataLoader
# 载入训练数据集  
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)  
# 载入测试数据集  
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

运行后,你就会在你设置的root路径中发现这样的一个文件夹:
image.png
这是一个十分类数据集,包含七万张手写数字图像。可以以此对手写数字的图像进行分类,如果训练的模型较为成功,那么我们就可以得到一个可以识别手写数字的分类器。

2.2 网络结构

根据我们在本周所了解的内容,再结合数据集的情况,我们设计新的网络结构如下:

class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28*28, 512)  
        # 灰度图只有一个通道来表示亮暗程度,不用像彩色图像一样乘3。
        self.hidden2 = nn.Linear(512, 256)  
        self.hidden3 = nn.Linear(256, 128)  
        self.hidden4 = nn.Linear(128, 32)  
        self.relu = nn.ReLU()  
        # 输出层(使用Softmax进行多分类)  
        self.output = nn.Linear(32, 10)  # 输出10个类别(0-9)  
        self.softmax = nn.Softmax(dim=1) 
        # dim=1:对每一行(即每个样本的所有类别分数)进行计算,将每个类别的分数转化为概率。
        init.xavier_uniform_(self.output.weight)  
  
    def forward(self, x):  
        x = self.flatten(x)  
        x = self.relu(self.hidden1(x))  
        x = self.relu(self.hidden2(x))  
        x = self.relu(self.hidden3(x))  
        x = self.relu(self.hidden4(x))  
        x = self.output(x)  
        x = self.softmax(x)  # 使用Softmax输出类别概率  
        return x

2.3 损失函数和其他设置

# 迭代设置
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)  
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 图像较简单,因此增加批次大小到64

# 损失函数和优化器  
criterion = nn.CrossEntropyLoss()  # 多分类使用交叉熵损失  
optimizer = optim.Adam(model.parameters(), lr=0.001) # 优化器默认选择Adam
num_epochs = 10 # 训练十轮

这里要单独说明的是,我们上面了解到的对多分类的独热编码就被封装在CrossEntropyLoss损失函数的设置里,它内部会自动把标签整数转为独热的形式进行计算。

2.4 第一次结果分析: 多分类

现在,我们根据上面的设置,来看看训练结果:
image.png
如果你看过之前的几次代码实践,可能会有一些疑惑,为什么几乎相同的配置下,猫狗二分类的准确率最高才刚刚到70%,现在都扩展到十分类并简化了网络结构的情况下,准确率却在90%以上?
很明显,二者最大的区别就是数据集不同。
我们来解释一下为什么手写数字图像识别的训练效果这么好:

  • 猫狗数据集:图像复杂、背景多变、光照、姿势都可能不同,样本间差异大,网络需要学习的特征复杂,因此训练难度高,准确率提升较慢。
  • 手写数字 MNIST 数据集:图像统一大小、灰度处理,数字相对居中,背景干净,样本间差异小,网络很容易学习到区分特征,因此即使网络结构相对简单,也能快速达到高准确率。

简单来说,就是手写数字的数据好,图像简单,而数据的可分性和特征明确程度直接决定了训练效果。
因此,MNIST也常常作为图神经网络的入门教程,即使我们使用的是全连接网络,也能达到较高的准确率,甚至你使用sigmoid和二分类交叉熵也能达到较好的拟合效果。

究其根本,数据好,就像品质极佳的原材料,就是水煮一下,也十分美味。
20251124114508842

2.5 加入批量标准化

我们本周了解了batch归一化,知道了它能起到加速训练,同时有轻微正则化的作用。
现在,我们就再把BN加入数字图像识别模型。
在Pytorch中,BN也被封装在网络结构模块里,完善后如下:

class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28 * 28, 512)  
        self.bn1 = nn.BatchNorm1d(512)  # 第一层的BN
        self.hidden2 = nn.Linear(512, 256)  
        self.bn2 = nn.BatchNorm1d(256)  # 第二层的BN
        self.hidden3 = nn.Linear(256, 128)  
        self.bn3 = nn.BatchNorm1d(128)  # 第三层的BN
        self.hidden4 = nn.Linear(128, 32)  
        self.bn4 = nn.BatchNorm1d(32)  # 第四层的BN
        self.relu = nn.ReLU()  
        self.output = nn.Linear(32, 10)  
        self.softmax = nn.Softmax(dim=1)  
        init.xavier_uniform_(self.output.weight)  
    # 把BN加入传播过程
    def forward(self, x):  
        x = self.flatten(x)  
        x = self.hidden1(x)  
        x = self.bn1(x)  # 这里 
        x = self.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x)  # 这里
        x = self.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x)  # 这里
        x = self.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x)  # 这里
        x = self.relu(x)  
        x = self.output(x)  
        x = self.softmax(x)  
        return x

于此同时,我们记得BN在训练和测试中对参数的使用有差别,测试中会使用训练中的全局均值和全局方差。
而这个逻辑是通过训练模式和评估模式的转换完成的:

model.train()  # 训练中维护全局 BN 参数
····训练代码
model.eval()   # 测试中使用固定全局 BN 参数

现在我们再来看看结果。

2.6 第二次结果分析:加入BN

来看看加入BN前后的对比:
20251124124510355
经过多次测试,可以较明显的发现,BN起到了加速训练的作用,在相同的其他配置下,使用BN的模型准确率也高于不使用BN。

3.附录

3.1 PyTorch版:数字图像识别模型代码

import torch  
import torch.nn as nn  
import torch.optim as optim  
from torchvision import datasets, transforms  
from torch.utils.data import DataLoader  
from torch.nn import init  
import matplotlib.pyplot as plt  
  
transform = transforms.Compose([  
    transforms.ToTensor(),  
    transforms.Normalize((0.5,), (0.5,))  
])  
# 载入训练数据集  
train_dataset = datasets.MNIST(  
    root='./data',  
    train=True,  
    download=True,  
    transform=transform  
)  
test_dataset = datasets.MNIST(  
    root='./data',  
    train=False,  
    download=True,  
    transform=transform  
)  
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)  
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)  
  
class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28 * 28, 512)  
        self.bn1 = nn.BatchNorm1d(512)  
        self.hidden2 = nn.Linear(512, 256)  
        self.bn2 = nn.BatchNorm1d(256)  
        self.hidden3 = nn.Linear(256, 128)  
        self.bn3 = nn.BatchNorm1d(128)  
        self.hidden4 = nn.Linear(128, 32)  
        self.bn4 = nn.BatchNorm1d(32)  
        self.relu = nn.ReLU()  
        self.output = nn.Linear(32, 10)  
        self.softmax = nn.Softmax(dim=1)  
        init.xavier_uniform_(self.output.weight)  
  
    def forward(self, x):  
        x = self.flatten(x)  
        x = self.hidden1(x)  
        x = self.bn1(x)  
        x = self.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x)  
        x = self.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x)  
        x = self.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x)  
        x = self.relu(x)  
        x = self.output(x)  
        x = self.softmax(x)  
        return x  
  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
model = NeuralNetwork().to(device)  
  
criterion = nn.CrossEntropyLoss()  
optimizer = optim.Adam(model.parameters(), lr=0.001)  
  
  
num_epochs = 10  
train_losses, train_accuracies, test_accuracies = [], [], []  
# 训练  
for epoch in range(num_epochs):  
    model.train()  
    running_loss = 0.0  
    correct_train = 0  
    total_train = 0  
    for images, labels in train_loader:  
        images, labels = images.to(device), labels.to(device)  
        optimizer.zero_grad()  
        outputs = model(images)  
        loss = criterion(outputs, labels)  
        loss.backward()  
        optimizer.step()  
        running_loss += loss.item() * images.size(0)  
        _, predicted = torch.max(outputs, 1)  
        total_train += labels.size(0)  
        correct_train += (predicted == labels).sum().item()  
  
    epoch_loss = running_loss / len(train_loader.dataset)  
    train_accuracy = correct_train / total_train  
    train_losses.append(epoch_loss)  
    train_accuracies.append(train_accuracy)  
  
    # 测试  
    model.eval()  
    correct_test = 0  
    total_test = 0  
    with torch.no_grad():  
        for images, labels in test_loader:  
            images, labels = images.to(device), labels.to(device)  
            outputs = model(images)  
            _, predicted = torch.max(outputs, 1)  
            total_test += labels.size(0)  
            correct_test += (predicted == labels).sum().item()  
    test_accuracy = correct_test / total_test  
    test_accuracies.append(test_accuracy)  
  
    print(f"Epoch {epoch + 1}/{num_epochs} | Loss: {epoch_loss:.4f} | "          f"Train Acc: {train_accuracy:.4f} | Test Acc: {test_accuracy:.4f}")  
  
# 可视化  
plt.figure(figsize=(10, 5))  
plt.plot(train_losses, label='Train Loss', marker='o')  
plt.plot(train_accuracies, label='Train Accuracy', marker='x')  
plt.plot(test_accuracies, label='Test Accuracy', marker='s')  
plt.title('Training Loss & Accuracy')  
plt.xlabel('Epoch')  
plt.ylabel('Value')  
plt.ylim(0, max(max(train_losses), 1.0) + 0.1)  
plt.grid(True)  
plt.legend()  
plt.show()

3.2 Tensorflow版:数字图像识别模型代码

import tensorflow as tf  
from tensorflow.keras import layers, optimizers, losses  
import matplotlib.pyplot as plt  
  
# 载入 MNIST 数据  
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()  
# 定义模型类  
class NeuralNetwork(tf.keras.Model):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = layers.Flatten()  
        self.rescale = layers.Rescaling(1. / 127.5, offset=-1)  # [-1,1] 归一化  
        self.hidden1 = layers.Dense(512)  
        self.bn1 = layers.BatchNormalization()  
        self.hidden2 = layers.Dense(256)  
        self.bn2 = layers.BatchNormalization()  
        self.hidden3 = layers.Dense(128)  
        self.bn3 = layers.BatchNormalization()  
        self.hidden4 = layers.Dense(32)  
        self.bn4 = layers.BatchNormalization()  
        self.output_layer = layers.Dense(10, activation='softmax')  
  
    def call(self, x, training=False):  
        x = self.flatten(x)  
        x = self.rescale(x)  
        x = self.hidden1(x)  
        x = self.bn1(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.output_layer(x)  
        return x  
  
  
# 实例化模型  
model = NeuralNetwork()  
  
# 编译模型:加设置  
model.compile(optimizer=optimizers.Adam(learning_rate=0.001),  
              loss=losses.SparseCategoricalCrossentropy(),  
              metrics=['accuracy'])  
  
# 训练模型  
num_epochs = 10  
batch_size = 64  
history = model.fit(x_train, y_train,  
                    validation_data=(x_test, y_test),  
                    epochs=num_epochs,  
                    batch_size=batch_size)  
  
# 可视化训练曲线  
plt.figure(figsize=(10, 5))  
plt.plot(history.history['loss'], label='Train Loss', marker='o')  
plt.plot(history.history['accuracy'], label='Train Accuracy', marker='x')  
plt.plot(history.history['val_accuracy'], label='Test Accuracy', marker='s')  
plt.title('Training Loss & Accuracy')  
plt.xlabel('Epoch')  
plt.ylabel('Value')  
plt.ylim(0, max(max(history.history['loss']), 1.0) + 0.1)  
plt.grid(True)  
plt.legend()  
plt.show()
posted @ 2025-11-24 13:33  哥布林学者  阅读(100)  评论(0)    收藏  举报