此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:
- 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
- github课程资料,含课件与笔记:吴恩达深度学习教学资料
- 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案
本篇为第二课第三周的课程习题和代码实践部分笔记。
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()