吴恩达深度学习课程二: 改善深层神经网络 第三周:超参数调整,批量标准化和编程框架 课后习题和代码实践
<p>此分类用于记录吴恩达深度学习课程的学习笔记,目前已完结,点击进入全集目录<br>课程相关信息链接如下:</p>
<ol>
<li>原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai</li>
<li>github课程资料,含课件与笔记:吴恩达深度学习教学资料</li>
<li>课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案</li>
</ol>
<p>本篇为第二课第三周的课程习题和代码实践部分笔记。</p>
<hr>
<h1 id="1-理论习题独热编码">1. 理论习题:独热编码</h1>
<p>还是先上链接:<br>
【中英】【吴恩达课后测验】Course 2 - 改善深层神经网络 - 第三周测验<br>
因为本周内容大多是一些补充,因此习题也大多只是之前了解到的死知识,就不再多提了。<br>
这部分补充一个之前出现的技术:<strong>独热编码</strong></p>
<h2 id="11-独热编码one-hot-encoding">1.1 独热编码(One-Hot Encoding)</h2>
<p>之前在多值预测与多分类这部分里,我们提到过,在多分类的情况下, 使用独热编码来表示各个分类,现在就来展开一下这个技术。<br>
为了让解释更直观,我们全程使用同一个实例:</p>
<blockquote>
<p><strong>例子:识别动物类别,有三类:猫(Cat)、狗(Dog)、兔子(Rabbit)。</strong><br>
我们用这个例子贯穿整个独热编码的说明。</p>
</blockquote>
<h4 id="1用独热编码表示分类的直接形式">(1)用独热编码表示分类的直接形式</h4>
<p>在多分类中,一个类别就是一个“<strong>离散的标签</strong>”,没有数值大小,也<strong>不存在“比谁大一点”的概念</strong>。<br>
但神经网络输出的是<strong>一组数字</strong>,为了让网络能理解“哪个类别是正确答案”,我们就需要把“猫/狗/兔子”变成神经网络能处理的格式。</p>
<p><strong>独热编码就是最直接的方式:每一个类别对应一个位置,正确的那个位置为 1,其余为 0。</strong><br>
来看看具体怎么做:现在对我们的动物识别例子做独热编码处理,结果如下:</p>
<table>
<thead>
<tr>
<th>类别</th>
<th>独热编码</th>
</tr>
</thead>
<tbody>
<tr>
<td>猫 Cat</td>
<td><code></code></td>
</tr>
<tr>
<td>狗 Dog</td>
<td><code></code></td>
</tr>
<tr>
<td>兔 Rabbit</td>
<td><code></code></td>
</tr>
</tbody>
</table>
<p>其中:</p>
<ul>
<li>三个神经元对应三个分类</li>
<li>“1”表示正确分类,“0”表示不是</li>
<li><strong>标签永远只有一个位置是 1</strong></li>
</ul>
<p>这就是“独热”:只有一个地方热<br>
这是它对多分类的直接表现形式。</p>
<h4 id="2为什么二分类不使用独热编码">(2)为什么二分类不使用独热编码?</h4>
<p>那你可能会问:<br>
<strong>“既然多分类用独热,那二分类是不是也能写成 <code></code> 和 <code></code>?”</strong><br>
答案:理论上可以,实践中<strong>不会这么做</strong>。<br>
原因很简单:<strong>没必要</strong><br>
简单展开一下:<br>
二分类的本质是:<strong>是否属于某一类</strong>(比如“是不是猫”)</p>
<p>只需要一个神经元 + sigmoid,就能表达“是的概率”。<br>
这意味着 <strong>一个神经元就能表达整个二分类的状态</strong>。</p>
<p>这种结构浪费计算,还会带来梯度重复问题。<br>
什么叫“<strong>梯度重复</strong>”?,这是softmax在二分类应用中出现的问题。</p>
<p>假设某张图真实标签是:</p>
<pre><code>猫 →
</code></pre>
<p>而模型预测是:</p>
<pre><code>ŷ =
</code></pre>
<p>也就是模型认为“不是猫”的概率比“是猫”还高。<br>
根据交叉熵,我们得到两个神经元的损失项:</p>
<pre><code>L1 = -1 * log(0.4)
L2 = -0 * log(0.6)
</code></pre>
<p>乍看只第一个有影响。<br>
但真正计算梯度时,Softmax 会让两个神经元一起参与:</p>
<ul>
<li>第 1 个神经元(猫)要把概率从 0.4 推到更高</li>
<li>第 2 个神经元(不是猫)要把概率从 0.6 推到更低</li>
</ul>
<p>于是反向传播时两个神经元都会更新:</p>
<ul>
<li><strong>第一个神经元:“我应该更强一点”</strong></li>
<li><strong>第二个神经元:“我应该更弱一点”</strong><br>
这就产生了<strong>两个方向相反但意义重复的梯度</strong>。</li>
</ul>
<p>而这两个神经元本质上是一件事:</p>
<pre><code>P(不是猫) = 1 − P(是猫)
</code></pre>
<p>所以,这种结构就是让网络:</p>
<ul>
<li>学一次“猫应该更强”</li>
<li>再学一次“不是猫应该更弱”</li>
</ul>
<p>这其实是<strong>同一条语义的两次更新</strong>。</p>
<p>这就是二分类使用softmax带来的梯度重复现象:<br>
<strong>它让模型参数增加,训练更慢,softmax还让两个输出互相牵连,一个升另一降,让本来很简单的二分类被人为增加了耦合难度。</strong></p>
<h4 id="3-多分类不使用独热编码的影响">(3) 多分类不使用独热编码的影响</h4>
<p>那多分类为什么不能像二分类一样直接写成0,1,2呢?<br>
就像这样:</p>
<table>
<thead>
<tr>
<th>类别</th>
<th>非独热写法</th>
</tr>
</thead>
<tbody>
<tr>
<td>猫</td>
<td><code>0</code></td>
</tr>
<tr>
<td>狗</td>
<td><code>1</code></td>
</tr>
<tr>
<td>兔</td>
<td><code>2</code></td>
</tr>
</tbody>
</table>
<p>你可能已经发现了问题所在,我们在一开始就强调了:<strong>类别不存在“比谁大一点”的概念</strong><br>
使用上面这种分类方法带俩的严重问题就是:<strong>神经网络会错误地认为“兔 > 狗 > 猫”</strong></p>
<p>再简单展开一下:<br>
在这种分类方式下,模型会把 <strong>“误差”理解为数值距离</strong>。</p>
<p>例如真实标签是“兔 = 2”,模型预测成“猫 = 0”。<br>
模型认为误差 = |2 − 0| = <strong>2</strong><br>
那预测成“狗 1”误差就会变小。<br>
于是, <strong>模型会错误地认为预测成“狗”比预测成“猫”更接近正确答案,带来梯度的混乱</strong>。<br>
但实际上,我们知道:“猫”和“狗”与“兔”之间没有“更近”的关系,它们应该是三种<strong>平行的、不可比较的类别</strong>。<br>
所以这种写法会导致训练逻辑错误,学习方向混乱,效果极差。</p>
<h4 id="4独热编码对多分类的适配性">(4)独热编码对多分类的适配性</h4>
<p>现在再来看看独热编码的优势。<br>
继续使用我们动物识别例子:<br>
真实标签“兔子” → <code></code><br>
假设模型输出的是 Softmax 后的概率:</p>
<pre><code>预测为:猫 0.1
预测为:狗 0.2
预测为:兔 0.7
</code></pre>
<p>Softmax 输出为:</p>
<pre><code>ŷ =
</code></pre>
<p>真实标签为:</p>
<pre><code>y=
</code></pre>
<p>交叉熵损失就很自然:</p>
<pre><code>Loss = - log(预测为兔的概率) = -log(0.7)
</code></pre>
<p><strong>只有正确类别那一项会参与计算,其余项为 0,不影响损失。</strong></p>
<p>但重点来了:<strong>虽然损失项只有一项,但梯度来自所有类别</strong><br>
上面的损失表达式容易让人误解:“只有一项有用,那是不是梯度也只来自那一项?”<br>
其实不是。</p>
<p>我们继续看:<br>
Softmax + CrossEntropy 的梯度公式非常简单:</p>
<p></p><div class="math display">\[\frac{\partial L}{\partial z_i} = \hat{y}_i - y_i
\]</div><p></p><p>代入我们的例子:</p>
<ul>
<li>对“猫”神经元:<span class="math inline">\(0.1 - 0 = 0.1\)</span></li>
<li>对“狗”神经元:<span class="math inline">\(0.2 - 0 = 0.2\)</span></li>
<li>对“兔”神经元:<span class="math inline">\(0.7 - 1 = -0.3\)</span><br>
可以看到:</li>
<li>“兔” 的梯度是负的 → 相关参数会变大(让概率更接近 1)</li>
<li>“猫”和“狗”的梯度是正的 → 相关参数会变小(让概率更接近 0)</li>
</ul>
<p>这恰好符合我们对多分类的直观理解: <strong>正确类变得更确定,其他类一起被压下去。</strong></p>
<p>这就是多分类中,独热编码,softmax,交叉熵形成的更新链条,<strong>我们在下面的实践部分就能感受到它的效果。</strong></p>
<h1 id="2-代码实践">2. 代码实践</h1>
<p>在课程要求里,这周的实践作业是Tensorflow的入门,主要以了解Tensorflow的基本原理和语法为主,还是把这位博主的链接放在前面,介绍了使用Tensorflow构建神经网络的过程。<br>
【中文】【吴恩达课后编程作业】Course 2 - 改善深层神经网络 - 第三周作业</p>
<p>虽然依然使用Pytorch来进行演示,但随着引入Tensorflow框架,之后课程内容对此的介绍和使用也会增加。因此,之后我都会在最后附上一个Tensorflow版本的代码。</p>
<h2 id="21-多分类数据集">2.1 多分类数据集</h2>
<p>为了演示本周的内容,我们暂时放下之前的猫狗二分类数据集。<br>
这次,我们使用一个新的数据集:<strong>手写数字图像识别</strong>。<br>
你可能之前已经知道这个数据集了,它并不需要我们和之前一样在网上寻找数据集下载。<br>
pytorch<strong>内置</strong>了这个经典数据集的下载链接,我们可以直接通过API下载它到项目目录:</p>
<pre><code class="language-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)
</code></pre>
<p>运行后,你就会在你设置的root路径中发现这样的一个文件夹:<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202511/3708248-20251124131948737-1082796156.png" alt="image.png" loading="lazy"><br>
这是一个十分类数据集,包含<strong>七万张手写数字图像</strong>。可以以此对手写数字的图像进行分类,如果训练的模型较为成功,那么我们就可以得到一个可以识别手写数字的分类器。</p>
<h2 id="22-网络结构">2.2 网络结构</h2>
<p>根据我们在本周所了解的内容,再结合数据集的情况,我们设计新的网络结构如下:</p>
<pre><code class="language-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
</code></pre>
<h2 id="23-损失函数和其他设置">2.3 损失函数和其他设置</h2>
<pre><code class="language-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 # 训练十轮
</code></pre>
<p><strong>这里要单独说明的是,我们上面了解到的对多分类的独热编码就被封装在<code>CrossEntropyLoss</code>损失函数的设置里,它内部会自动把标签整数转为独热的形式进行计算。</strong></p>
<h2 id="24-第一次结果分析-多分类">2.4 第一次结果分析: 多分类</h2>
<p>现在,我们根据上面的设置,来看看训练结果:<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202511/3708248-20251124131949598-1683686688.png" alt="image.png" loading="lazy"><br>
如果你看过之前的几次代码实践,可能会有一些疑惑,<strong>为什么几乎相同的配置下,猫狗二分类的准确率最高才刚刚到70%,现在都扩展到十分类并简化了网络结构的情况下,准确率却在90%以上?</strong><br>
很明显,二者最大的区别就是数据集不同。<br>
我们来解释一下为什么手写数字图像识别的训练效果这么好:</p>
<ul>
<li><strong>猫狗数据集</strong>:图像复杂、背景多变、光照、姿势都可能不同,样本间差异大,网络需要学习的特征复杂,因此训练难度高,准确率提升较慢。</li>
<li><strong>手写数字 MNIST 数据集</strong>:图像统一大小、灰度处理,数字相对居中,背景干净,样本间差异小,网络很容易学习到区分特征,因此即使网络结构相对简单,也能快速达到高准确率。</li>
</ul>
<p>简单来说,就是<strong>手写数字的数据好,图像简单</strong>,而<strong>数据的可分性和特征明确程度</strong>直接决定了训练效果。<br>
因此,MNIST也常常作为图神经网络的入门教程,即使我们使用的是全连接网络,也能达到较高的准确率,甚至你使用sigmoid和二分类交叉熵也能达到较好的拟合效果。</p>
<p>究其根本,<strong>数据好</strong>,就像品质极佳的原材料,就是水煮一下,也十分美味。<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202511/3708248-20251124133252641-679973494.png" alt="20251124114508842" loading="lazy"></p>
<h2 id="25-加入批量标准化">2.5 加入批量标准化</h2>
<p>我们本周了解了batch归一化,知道了它能起到加速训练,同时有轻微正则化的作用。<br>
现在,我们就再把BN加入数字图像识别模型。<br>
<strong>在Pytorch中,BN也被封装在网络结构模块里</strong>,完善后如下:</p>
<pre><code class="language-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
</code></pre>
<p>于此同时,我们记得BN在训练和测试中对参数的使用有差别,<strong>测试中会使用训练中的全局均值和全局方差。</strong><br>
而这个逻辑是通过训练模式和评估模式的转换完成的:</p>
<pre><code class="language-python">model.train()# 训练中维护全局 BN 参数
····训练代码
model.eval() # 测试中使用固定全局 BN 参数
</code></pre>
<p>现在我们再来看看结果。</p>
<h2 id="26-第二次结果分析加入bn">2.6 第二次结果分析:加入BN</h2>
<p>来看看加入BN前后的对比:<br>
<img src="https://img2024.cnblogs.com/blog/3708248/202511/3708248-20251124133052394-1871567278.png" alt="20251124124510355" loading="lazy"><br>
经过多次测试,可以较明显的发现,BN起到了加速训练的作用,在相同的其他配置下,使用BN的模型准确率也高于不使用BN。</p>
<h1 id="3附录">3.附录</h1>
<h2 id="31-pytorch版数字图像识别模型代码">3.1 PyTorch版:数字图像识别模型代码</h2>
<pre><code class="language-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()
</code></pre>
<h2 id="32-tensorflow版数字图像识别模型代码">3.2 Tensorflow版:数字图像识别模型代码</h2>
<pre><code class="language-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()
</code></pre><br><br>
来源:https://www.cnblogs.com/Goblinscholar/p/19263775
頁:
[1]