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

此分类用于记录吴恩达深度学习课程的学习笔记。

课程相关信息链接如下:

  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下载它到项目目录:

python 复制代码
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路径中发现这样的一个文件夹:

这是一个十分类数据集,包含七万张手写数字图像。可以以此对手写数字的图像进行分类,如果训练的模型较为成功,那么我们就可以得到一个可以识别手写数字的分类器。

2.2 网络结构

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

python 复制代码
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 损失函数和其他设置

python 复制代码
# 迭代设置
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 第一次结果分析: 多分类

现在,我们根据上面的设置,来看看训练结果:

如果你看过之前的几次代码实践,可能会有一些疑惑,为什么几乎相同的配置下,猫狗二分类的准确率最高才刚刚到70%,现在都扩展到十分类并简化了网络结构的情况下,准确率却在90%以上?

很明显,二者最大的区别就是数据集不同。

我们来解释一下为什么手写数字图像识别的训练效果这么好:

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

简单来说,就是手写数字的数据好,图像简单 ,而数据的可分性和特征明确程度 直接决定了训练效果。

因此,MNIST也常常作为图神经网络的入门教程,即使我们使用的是全连接网络,也能达到较高的准确率,甚至你使用sigmoid和二分类交叉熵也能达到较好的拟合效果。

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

2.5 加入批量标准化

我们本周了解了batch归一化,知道了它能起到加速训练,同时有轻微正则化的作用。

现在,我们就再把BN加入数字图像识别模型。
在Pytorch中,BN也被封装在网络结构模块里,完善后如下:

python 复制代码
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在训练和测试中对参数的使用有差别,测试中会使用训练中的全局均值和全局方差。

而这个逻辑是通过训练模式和评估模式的转换完成的:

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

现在我们再来看看结果。

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

来看看加入BN前后的对比:

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

3.附录

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

python 复制代码
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版:数字图像识别模型代码

python 复制代码
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()
相关推荐
WWZZ20252 小时前
快速上手大模型:深度学习11(数据增强、微调、目标检测)
人工智能·深度学习·算法·目标检测·计算机视觉·大模型·具身智能
安如衫2 小时前
【机器学习基础】Attention in Transformers:注意力机制
笔记·深度学习·学习·机器学习·注意力机制
CoovallyAIHub5 小时前
存储风暴下的边缘智能韧性:瑞芯微RK3588如何将供应链挑战转化为市场机遇
深度学习·算法·计算机视觉
iFlow_AI5 小时前
iFlow CLI快速搭建Flutter应用记录
开发语言·前端·人工智能·flutter·ai·iflow·iflow cli
guangzan6 小时前
AI SDK:重新定义 AI 应用开发
ai
孤狼warrior6 小时前
我想拥有作家的思想 循环神经网络及变型
人工智能·rnn·深度学习·神经网络·lstm