在上一章中,我们详细了解了整个深度学习生命周期,并理解了如何从头到尾使一个深度学习项目成功。凭借这些知识,我们现在可以更深入地探讨深度学习模型的技术细节。在本章中,我们将深入探讨在行业中常用的深度学习架构,并理解每种架构设计背后的原因。对于中级和高级读者,这将是一个简短的回顾,以确保在术语定义上的一致性。对于初学者,架构将以易于理解的方式呈现,以便你能迅速了解深度学习领域中的有用神经网络架构。
掌握多种架构背后的方法论,可以让你为自己的用例创新定制架构,并且最重要的是,学会根据数据输入或问题类型选择合适的基础架构。
本章的重点将放在多层感知器(MLP)网络架构上。全面覆盖MLP的内容,以及与神经网络实现相关的一些关键概念,如梯度、激活函数和正则化方法,为后续章节探索其他更复杂架构类型奠定基础。本章将具体涵盖以下主题:
- 探索使用MLP的神经网络基础
- 理解神经网络梯度
- 理解梯度下降
- 从零开始实现MLP
- 使用深度学习框架实现MLP
- 设计MLP
技术要求
本章包括一些Python编程语言的实际实现。为完成本章,你需要在计算机上安装以下库:
- pandas
- Matplotlib
- Seaborn
- Scikit-learn
- NumPy
- Keras
- PyTorch
代码文件可以在GitHub上找到:github.com/PacktPublis…。
使用MLP探索神经网络的基础
当至少使用三个感知器层(不包括输入层)时,就创建了一个深度学习架构。感知器是由神经元单元组成的单层网络。神经元单元包含一个偏置变量,并作为连接顶点的节点。这些神经元将与另一个层中的神经元相互作用,并在神经元之间的连接/顶点上应用权重。感知器也被称为全连接层或稠密层,而MLP(多层感知器)也被称为前馈神经网络或全连接神经网络。
让我们回顾一下上一章中的MLP图,以便更好地理解这个概念。
该图展示了三个数据列输入如何传递到输入层,随后被传播到隐藏层,并最终通过输出层。虽然图中未展示,但在隐藏层和输出层的输出上都会应用一个额外的激活函数。隐藏层的激活函数为模型添加了非线性,使得神经网络能够捕捉输入数据和输出数据之间的非线性关系。输出层使用的激活函数取决于问题的类型,关于这一点将在第8章《探索监督式深度学习》中进行更详细的讨论。
在我们深入探讨相关的隐藏激活方法之前,我们首先需要了解梯度消失问题。梯度消失问题是当损失函数相对于模型参数的梯度在反向传播过程中变得非常小时出现的挑战。这会导致学习变慢和收敛效果差,因为权重更新非常小甚至完全不更新。当使用将输入值压缩到狭窄范围的激活函数时,梯度消失问题尤为明显。为了解决这个问题,修正线性单元(ReLU)激活函数被广泛采用,因为它在一定程度上能够缓解梯度消失问题。ReLU将负值映射为零,并保持正值,正如图2.2所示。
除了ReLU外,还有其他一些有用的隐藏层激活函数,可以帮助缓解梯度消失问题,同时提供不同的优点。以下是其中一些:
- Leaky ReLU:Leaky ReLU是ReLU函数的一种变体,它允许负输入值有一个小的、非零的梯度。这有助于缓解“死亡ReLU”问题,即当神经元的输入值持续为负时,神经元会变得不活跃并停止学习。Leaky ReLU为负输入引入了一个小的斜率,确保梯度不会完全消失。
- Parametric ReLU (PReLU) :PReLU是ReLU函数的另一个变体,其中负斜率在训练过程中学习,这使得模型能够自适应地调整其行为。这种灵活性可能会带来更好的性能,但也会增加复杂性,并带来过拟合的风险。
此外,随着我们深入研究本书中的不同主流架构,我们还将探讨更多的隐藏层激活函数。每种激活函数都有其优点和缺点,选择激活函数取决于要解决的具体问题和使用的架构。理解、实验并评估这些激活函数对于选择最适合给定任务的隐藏层激活函数至关重要。此外,评估任何与模型构建相关实验的推荐方法将在第8章《探索监督式深度学习》中进行探讨。
接下来,从一个层到另一个层传播值的过程称为前向传播或前向传递,其公式一般可以定义如下:
其中,a表示神经网络层的输出(称为激活),g表示非线性激活函数,w表示神经元连接之间的权重,x表示输入数据或激活,b表示神经元的偏置。不同类型的神经网络层以不同的方式消耗和输出数据,但通常仍然以此公式为基础。
理解神经网络的梯度
对于MLP,机器学习的目标是找到能够有效地将输入映射到期望输出的权重和偏置。权重和偏置通常是随机初始化的。在训练过程中,给定一个数据集,它们会通过批量迭代的方式被更新,以最小化损失函数,损失函数使用一种叫做反向传播(也称为反向传播算法)的方法来计算梯度。一个批次是用于训练或评估的数据集的子集,它允许神经网络在较小的组中处理数据,而不是一次性处理整个数据集。损失函数也称为误差函数或代价函数。
反向传播是一种通过使用损失函数相对于权重和偏置的偏导数,来确定每个神经元的权重和偏置变化对整体损失的敏感度的技术。微积分中的偏导数是衡量一个函数相对于一个变量变化速率的度量,它使用的技术叫做微分,并在神经网络中得到有效应用。一个方便的方法叫做链式法则,允许你通过分别计算每个函数的偏导数(在神经网络中是前向传播)来获得神经网络的导数。需要明确的是,导数可以被称为变化的敏感度、梯度和变化速率。其思想是,当我们知道哪个模型参数对误差的影响最大时,我们可以根据其幅度和方向相应地更新其权重。
让我们通过一个简单的例子,假设有一个包含两层的MLP,每层有一个神经元,来理解这一点,如图2.3所示。
为了清晰起见,w 表示神经元连接的权重,b 表示神经元的偏置,L 表示层数。不同的问题类型需要不同的损失函数,但为了便于解释,假设这是一个用于回归问题的多层感知机(MLP),在其中我们将使用均方误差作为损失函数,来计算来自最后一层激活值和数值目标值的损失部分。损失函数可以定义为如下:
这里,n 是神经元的总数。为了获得损失函数相对于输出层权重 w2 的变化率,即 δL / δw2,让我们基于链式法则定义公式。考虑以下:
所以,如果 a2 = g(z2),其中 g 是 ReLU 函数,那么相对于输出层权重 w2 的梯度将定义如下:
损失函数相对于 w2 的变化率可以通过将三个独立的变化部分相乘来计算:即损失函数相对于第二层输出的变化,激活输出相对于未激活的 z2(即前向传播中的 z2)的变化,以及未激活的 z2 相对于 w2 的变化。接下来我们将定义这些部分。现在,考虑以下内容:
基于链式法则,第一个变化部分将定义如下:
将其放入简化的组件表示中,将得到以下方程:
对于第二个变化部分,它可以通过以下公式定义:
在这种情况下,输出层没有应用激活函数。
对于第三个也是最后一个变化部分,它可以通过以下公式定义:
最后,将三个部分的简化表示代入公式中,以获得输出层权重 w2 的梯度,将得到以下方程:
现在你需要做的就是将实际值代入,得到第二层权重的梯度。相同的公式结构可以类似地应用于隐藏层的权重 w1,如下所示:
在使用链式法则展开 δL / δa1 后,公式将如下所示:
其他单独的组件可以定义如下:
这里的 a0 是输入数据。现在,让我们定义隐藏层权重 w1 的梯度,使用可以代入实际值进行计算的表示,如下所示:
同样的过程可以重复应用于偏置项,以获得其梯度。只需要将 z 相对于权重的偏导数替换为 z 相对于偏置的偏导数,如下所示:
现在,让我们定义第一个偏置项的梯度,使用可以代入实际值进行计算的表示,如下所示:
之前定义的公式是针对每层只有一个神经元的示例神经网络的。实际上,这些层通常每层包含多个神经元。为了计算具有多个神经元的层以及多个数据样本的损失和导数,你只需要对所有值求平均即可。
一旦获得梯度或导数,就可以使用不同的策略来更新权重。用于优化神经网络权重和偏置的算法称为优化器。如今有许多优化器选项,每种都有其优缺点。由于梯度用于优化权重和偏置,因此这个优化过程称为梯度下降。
理解梯度下降
可以将深度学习模型的损失视为存在于一个三维的损失景观中,该景观有许多不同的山丘和山谷,其中山谷更加优化,如图 2.4 所示。
然而,在实际中,我们只能近似这些损失景观,因为神经网络的参数值可以以无数种方式存在。实践中,监控每个训练和验证周期中损失行为的最常见方法是简单地绘制一个二维线性图,其中 x 轴是已执行的周期数,y 轴是损失值。一个周期(epoch)是在神经网络训练过程中对整个数据集的一次迭代。图 2.4 中的损失景观是神经网络三维损失景观的近似。为了可视化图 2.4 中的三维损失景观,我们可以使用两个随机初始化的参数和一个完全训练的参数,所有这些参数来自神经网络中的相同神经元位置。损失可以通过对这三个参数进行加权求和来计算。完全训练的参数的权重保持不变,而两个随机初始化的参数的权重会进行调整。这个过程允许我们近似图 2.4 中显示的三维损失景观。在该图中,x 轴和 y 轴分别是同一神经网络中两个随机初始化参数的权重,z 轴是损失值。梯度下降的目标是尝试找到全局最深的山谷,而不是陷入局部山谷或局部最小值。计算出的梯度提供了建议的方向,用于逐步调整和更新权重和偏置。需要注意的一点是,梯度提供了以最快的方式增加损失函数的方向,因此在梯度下降时,参数是从梯度中减去的。接下来我们将讨论一种简单的梯度下降形式,控制权重和偏置应该如何更新:
这里,α 表示学习率,它控制着你希望深度学习模型的学习速度有多快。学习率是一个超参数,控制神经网络在优化过程中学习和更新其权重和偏置的速度。学习率越高,深度学习模型在损失景观中走的步伐就越大。通过迭代应用这个参数更新步骤,神经网络将慢慢向下坡移动,使得学习到的参数集能够使网络有效地将输入映射到期望的目标值。对于所有的数据样本,计算出的梯度会被平均在一起,从而得到权重和偏置更新的单一更新方向。
有时,数据集可能太大,导致基础梯度下降的学习过程变慢,因为需要在更新神经网络参数之前从每个样本计算梯度。随机梯度下降(SGD)是为了解决这个问题而创建的。其思想非常简单,就是分批学习数据集,并通过不同的数据批次划分迭代学习整个数据集,而不是等到从整个数据集中获得梯度后才更新网络的参数。通过这种方式,即使是大规模数据集,学习过程也能保持高效,并能快速看到初步结果。
梯度下降算法有很多种变体,它们提供不同的优点,并适用于特定的情况。在这里,我们将列出一些在各种数据集上平均表现良好并且相关的梯度下降算法:
- 动量法(Momentum) :动量法是 SGD 的一种变体,包含一个“动量”项,帮助优化器更有效地在损失景观中导航。这个动量项是梯度的移动平均,有助于优化器克服局部最小值并更快收敛。动量项还为优化器增加了一些惯性,使其在梯度一致的方向上迈出更大的步伐,从而加速收敛。
- 均方根传播(RMSProp) :RMSProp 是一种自适应学习率优化算法,它为每个参数单独调整学习率。通过将学习率除以梯度平方的指数衰减平均,RMSProp 有助于防止 SGD 收敛过程中出现的震荡。这使得收敛过程更稳定,并能更快速地接近最优解。
- 自适应矩估计(Adam) :Adam 是另一种流行的优化算法,结合了动量法和 RMSProp 的优点。它为每个参数维护独立的自适应学习率,并结合了动量项。这种组合使得 Adam 能够快速收敛并找到更准确的解,因此它在许多深度学习任务中都非常流行。
尽管有许多梯度下降算法可供选择,选择合适的算法取决于特定的问题和数据集。通常,Adam 因其自适应特性以及结合了动量法和 RMSProp 特性,常被推荐作为一个好的起点。为了确定最适合你特定深度学习任务的算法,实验不同的算法及其超参数并验证它们的性能是至关重要的。接下来,我们将使用 Python 编写一个多层感知机(MLP)的代码。
从头实现一个多层感知机(MLP)
今天,创建神经网络及其层以及反向传播过程的过程已经被封装在深度学习框架中。微分过程已经被自动化,因此实际上不需要手动定义导数公式。去除深度学习库提供的抽象层将有助于加深你对神经网络内部结构的理解。因此,让我们手动创建这个神经网络,并明确地实现前向传播和反向传播的逻辑,而不是使用深度学习库:
我们将首先导入 numpy 和来自 scikit-learn 库的函数,用于加载示例数据集并执行数据划分:
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
接下来,我们定义 ReLU 方法,使 MLP 非线性:
def ReLU(x):
return np.maximum(x, 0)
现在,让我们部分定义一个类,初始化一个带有单隐藏层的 MLP 模型,并使其可以执行前向传播。层通过权重表示,其中 w1 是隐藏层的权重,w2 是输出层的权重。此外,b1 是输入层与隐藏层之间连接的偏置,b2 是隐藏层与输出层之间连接的偏置:
class MLP(object):
def __init__(self, input_layer_size, hidden_layer_size, output_layer_size, seed=1234):
rng = np.random.RandomState(seed)
self.w1 = rng.normal(size=(input_layer_size, hidden_layer_size))
self.b1 = np.zeros(hidden_layer_size)
self.w2 = rng.normal(size=(hidden_layer_size, output_layer_size))
self.b2 = np.zeros(output_layer_size)
self.output_layer_size = output_layer_size
self.hidden_layer_size = hidden_layer_size
def forward_pass(self, x):
z1 = np.dot(x, self.w1) + self.b1
a1 = ReLU(z1)
z2 = np.dot(a1, self.w2) + self.b2
a2 = z2
return z1, a1, z2, a2
为了让 MLP 学习,我们将实现反向传播方法,生成偏置和权重的平均梯度:
def ReLU_gradient(x):
return np.where(x > 0, 1, 0)
def backward_pass(self, a0, z1, a1, z2, a2, y):
number_of_samples = len(a2)
average_gradient_w2 = (np.dot(a1.T, (a2 - y)) * (2 / (number_of_samples * self.output_layer_size)))
average_gradient_b2 = (np.mean((a2 - y), axis=0) * (2 / self.output_layer_size))
average_gradient_w1 = np.dot(a0.T, np.dot((a2 - y), self.w2.T) * ReLU_gradient(z1)) * 2 / (number_of_samples * self.output_layer_size)
average_gradient_b1 = np.mean(np.dot((a2 - y), self.w2.T) * ReLU_gradient(z1), axis=0) * 2 / self.output_layer_size
return average_gradient_w2, average_gradient_b2, average_gradient_w1, average_gradient_b1
注意,ReLU 函数的导数为 f′(x) = 1 当 x > 0,f′(x) = 0 当 x <= 0。
对于最后一个类方法,我们将实现梯度下降步骤,利用反向传播中得到的平均梯度,这是一个允许更新偏置和权重的过程:
def gradient_descent_step(self, learning_rate, average_gradient_w2, average_gradient_b2, average_gradient_w1, average_gradient_b1):
self.w2 = self.w2 - learning_rate * average_gradient_w2
self.b2 = self.b2 - learning_rate * average_gradient_b2
self.w1 = self.w1 - learning_rate * average_gradient_w1
self.b1 = self.b1 - learning_rate * average_gradient_b1
现在我们已经手动创建了 MLP 类,接下来设置数据集并尝试从中学习。由于 MLP 只适用于结构化的表格数据集类型,这些数据集是单维度且数值型的,因此我们将使用一个名为 diabetes 的数据集,它包含 10 个数值特征,例如年龄、性别、体重指数、平均血压和 6 个血清测量值,作为输入,以及糖尿病病程进展的定量指标作为目标数据。数据方便地保存在 scikit-learn 库中,因此我们首先加载输入的 DataFrame:
diabetes_data = datasets.load_diabetes(as_frame=True)
diabetes_df = diabetes_data['data']
现在,我们将把 DataFrame 转换为 NumPy 数组值,以便神经网络使用:
X = diabetes_df.values
加载数据的最后一步是从 diabetes 数据中加载目标数据,并确保它有一个额外的外维度,因为 PyTorch 模型以这种方式输出其预测:
target = np.expand_dims(diabetes_data['target'], 1)
接下来,将数据集划分为 80% 用于训练,20% 用于验证:
X_train, X_val, y_train, y_val = train_test_split(X, target, test_size=0.20, random_state=42)
数据准备好了,接下来初始化一个 MLP 模型,我们定义的类中有一个包含 20 个神经元的隐藏层和一个 1 个神经元的输出层:
mlp_model = MLP(
input_layer_size=len(diabetes_df.columns),
hidden_layer_size=20,
output_layer_size=target.shape[1]
)
数据和模型都准备好了,现在是时候训练我们从头构建的模型了。由于数据集足够小,只有 442 个样本,因此使用梯度下降不会遇到运行时问题,因此我们将在此使用完整的梯度下降进行 100 次迭代。一轮训练表示通过整个训练数据集进行一次完整的训练:
iterations = 100
training_error_per_epoch = []
validation_error_per_epoch = []
for i in range(iterations):
z1, a1, z2, a2 = mlp_model.forward_pass(X_train)
average_gradient_w2, average_gradient_b2, average_gradient_w1, average_gradient_b1 = mlp_model.backward_pass(X_train, z1, a1, z2, a2, y_train)
mlp_model.gradient_descent_step(
learning_rate=0.1,
average_gradient_w2=average_gradient_w2,
average_gradient_b2=average_gradient_b2,
average_gradient_w1=average_gradient_w1,
average_gradient_b1=average_gradient_b1,
)
_, _, _, a2_val = mlp_model.forward_pass(X_val)
training_error_per_epoch.append(mean_squared_error(y_train, a2))
validation_error_per_epoch.append(mean_squared_error(y_val, a2_val))
最后,使用 matplotlib 绘制收集到的训练和验证数据集的均方误差:
plt.figure(figsize=(10, 6))
plt.plot(training_error_per_epoch)
plt.plot(validation_error_per_epoch, linestyle='dotted')
plt.show()
这将生成以下图表:
到此为止,你已经从头实现了一个 MLP,并进行了训练,而不依赖于深度学习框架!但我们的实现是否正确且可靠呢?让我们在下一个主题中验证这一点。
使用深度学习框架实现 MLP
深度学习框架旨在简化和加速深度学习模型的开发。它们提供了大量常用的神经网络层、优化器和一般用于构建神经网络模型的工具,同时还提供了非常容易扩展的接口来实现新的方法。反向传播本身对框架的用户是抽象的,因为在需要时,梯度会自动在后台计算。最重要的是,它们允许使用 GPU 进行高效的模型训练和预测。
在本节中,我们将使用名为 PyTorch 的深度学习框架,构建与上一节相同的 MLP 模型,并验证这两种实现是否产生相同的结果:
我们将首先导入必要的库:
import torch
import torch.nn as nn
import torch.nn.functional as F
接下来,让我们定义 MLP 类,包含两个全连接层,以及前向传播方法,带有允许我们设置输入层大小、隐藏层大小和输出层大小的参数:
class MLPPytorch(nn.Module):
def __init__(self, input_layer_size, hidden_layer_size, output_layer_size):
super(MLPPytorch, self).__init__()
self.fc1 = nn.Linear(input_layer_size, hidden_layer_size)
self.fc2 = nn.Linear(hidden_layer_size, output_layer_size)
def forward(self, x):
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
你会注意到没有实现反向传播函数,这减少了定义神经网络模型所需的工作量。当你从 PyTorch 模块类继承时,反向传播功能会随你定义的 PyTorch 层一起提供。最后,让我们使用一个隐藏层大小为 10,以及根据糖尿病数据集设置输入和输出大小来初始化 MLP:
net = MLPPytorch(
input_layer_size=len(diabetes_df.columns),
hidden_layer_size=10,
output_layer_size=y_train.shape[1],
)
现在,让我们检查前向和反向传播功能,并与我们的 numpy 变体进行对比。首先,我们初始化 PyTorch MLP,并从基于 numpy 的 MLP 模型中复制权重:
with torch.no_grad():
net.fc1.weight.copy_(
torch.from_numpy(mlp_model.w1.T)
)
net.fc1.bias.copy_(
torch.from_numpy(mlp_model.b1)
)
net.fc2.weight.copy_(
torch.from_numpy(mlp_model.w2.T)
)
net.fc2.bias.copy_(
torch.from_numpy(mlp_model.b2)
)
接下来,我们将数据集准备为适合 PyTorch 模型使用的 Tensor 对象:
torch_input = torch.from_numpy(X_train)
torch_target = torch.from_numpy(y_train)
为了获得相同的梯度,我们必须使用相同的均方误差(MSE)损失,并应用反向传播:
criterion = nn.MSELoss()
output = net(torch_input.float())
loss = criterion(output, torch_target.float())
loss.backward()
现在,让我们验证两个实现的梯度:
np.testing.assert_almost_equal(output.detach().numpy(), a2, decimal=3)
np.testing.assert_almost_equal(net.fc2.weight.grad.numpy(), average_gradient_w2.T, decimal=3)
np.testing.assert_almost_equal(net.fc2.bias.grad.numpy(), average_gradient_b2, decimal=3)
np.testing.assert_almost_equal(net.fc1.weight.grad.numpy(), average_gradient_w1.T, decimal=3)
np.testing.assert_almost_equal(net.fc1.bias.grad.numpy(), average_gradient_b1, decimal=3)
这巩固了你对神经网络基础知识的理解,以及对 MLP 架构的了解,并为你准备了更高级的深度学习概念。在我们进入下一节,探讨正则化这一主题之前,我们将首先看一下如何设计一个具有实际应用场景的 MLP。
正则化
深度学习中的正则化已经发展成一种广泛的技术,现如今它指的是任何增加或修改神经网络、数据或训练过程的方式,这些方式用于提高模型对外部数据的泛化能力。如今,所有高性能的神经网络都嵌入了某种形式的正则化方法。某些正则化方法带来了一些额外的有益副作用,如加速训练过程或提高训练数据集上的性能。但最终,正则化的主要目标是提高泛化能力,换句话说,就是提高外部数据上的性能指标并减少误差。为了快速回顾,以下列出了常见的几种正则化方法:
- Dropout 层:在训练过程中,根据指定的概率水平随机从所有神经节点中移除信息,通过将神经节点的输出替换为零,有效地使信息失效。这减少了对任何单一节点/信息的过度依赖,并增加了泛化的概率。
- L1/L2 正则化:这些方法向损失函数中添加了惩罚项,阻止模型对特征赋予过高的权重。L1 正则化,也称为 Lasso,使用权重的绝对值,而 L2 正则化,也称为 Ridge,使用权重的平方值。通过控制权重的大小,这些方法有助于防止过拟合并提高泛化能力。通常,这些方法应用于输入特征。
- 批量归一化层(Batch Normalization) :该方法通过标准化数据,在训练和推理时对外部数据进行归一化处理,使数据的均值为零,标准差为一。这是通过移除计算得出的均值并除以计算得出的标准差来实现的。均值和标准差通过小批量(根据模型确定的训练批量大小)在训练过程中进行计算并迭代更新。在推理时,应用在训练过程中计算的最终学习到的均值和标准差。这具有提高训练时间、训练稳定性和泛化能力的副作用。请注意,每个元素都有自己的均值和标准差。研究表明,批量归一化可以平滑损失景观,使其更容易达到最优值。
- 组归一化层(Group Normalization) :与批量归一化不同,组归一化通过每个样本的组来标准化数据,每组有一个均值和一个标准差。组的数量可以进行配置。当由于硬件限制批次样本数较少时,批量归一化的性能会下降。此层在每批数据量很小时优于批量归一化,因为均值和标准化更新不依赖于批次。但在大批量时,批量归一化仍然是更优选择。
- 权重标准化(Weight Standardization) :对神经网络的权重应用相同的标准化过程。神经网络的权重在训练后可能会增长到非常大的数值,这会导致输出值过大。其想法是,如果我们已经使用了批量归一化层,输出值会自动进行标准化,那么为什么不在输出值成为最终结果之前,先对权重进行标准化呢?一些简单的基准测试表明,当与组归一化层结合使用时,它在小批量时表现良好,且比批量归一化在大批量设置下的性能更好。
- 随机深度(Stochastic Depth) :与通过 Dropout 在训练阶段使神经网络变得更窄不同,随机深度在训练过程中减少了网络的深度。这种正则化方法利用了 ResNets 中跳跃连接的概念(将在后面介绍),即从早期层输出的结果会额外传递到后续层。在训练过程中,跳跃连接之间的层会被完全绕过,随机模拟一个更浅的网络。此正则化器具有加快训练时间和提高泛化性能的效果。
- 标签平滑(Label Smoothing) :用于分类问题,包括二分类、多分类或多标签分类问题。它通过为实际的独热编码标签引入松弛来进行学习。它将独热编码向量中实际标签 1 修改为 1 − ε,其中 ε 是一个合理的小值。此外,0 会被替换为 ε / (k - 1),其中 k 可以通过类别数来设置。一个输入输出示例为:[0, 0, 0, 1] 和 [0.0001, 0.0001, 0.0001, 0.9999]。其目的是避免让模型对结果过于自信,这会表明它对训练数据过拟合,无法泛化到外部数据。此方法鼓励相同类别的样本的输出更接近,并且鼓励不同类别样本的输出彼此保持相等的距离。此外,它有助于减轻对标签不准确的样本的过度自信。
- 数据增强(Data Augmentation) :当原始数据未能充分代表任何标签的所有变体时,数据增强有助于通过计算方式向数据中添加变体,用于训练。这样可以有效增加泛化能力,因为模型能够从数据的更多变体中学习。该方法适用于任何数据模态,并将在第8章《探索监督学习》中进行更详细的介绍。
正则化是任何神经网络架构中的一个重要组成部分,将出现在本章将介绍的所有架构中。在为特定问题选择正则化技术时,你应该首先考虑数据集的性质和你要解决的问题。例如,如果你有一个较小的批量大小,组归一化或权重标准化可能比批量归一化更适合。如果你的数据集变异性有限,可以使用数据增强来提高泛化能力。在选择这些技术时,先从简单的正则化方法,如 Dropout 或 L1/L2 正则化开始,并评估其性能。然后,你可以尝试其他技术,单独或组合使用,并比较它们对模型性能的影响。必须监控训练和验证指标,以确保所选的正则化方法不会导致过拟合或欠拟合。最终,正则化技术的选择依赖于实验和验证、领域知识以及对特定问题和数据集的理解。
接下来,让我们深入探讨 MLP 的设计。
设计一个 MLP
表格数据并不是神经网络最擅长的领域,而且往往情况下,提升型决策树在指标性能上优于 MLP。然而,在某些数据集上,神经网络有时可以超越提升树。在处理表格数据时,确保将 MLP 与其他非神经网络模型进行基准对比。
MLP 是神经网络中最简单的形式,可以在两个维度上进行高层次的修改,类似于所有神经网络,即网络的宽度和深度。构建标准 MLP 架构时的一个常见策略是从较小的模型开始,采用浅层和窄宽度,先获得一个小的基准后,再逐步增加这两个维度。通常,对于表格数据上的 MLP,增加神经网络深度的性能提升在大约第四层时会停滞。ReLU 是一种标准的激活层,已被证明可以稳定梯度并优化任务的学习。然而,如果你有时间实现实际价值,可以考虑将激活层替换为更先进的激活层。此时,激活层研究领域过于细化,结果通常没有标准化,对于不同的数据集反应混合,因此使用任何先进的激活层并不保证会提高性能。
MLP 的一种适配方式是使用一种叫做去噪自编码器(denoising autoencoders)的神经网络,生成去噪的特征,这些特征可以作为 MLP 的输入。这个进展将在第 5 章《理解自编码器》中进行更详细的描述。训练方法与架构密切相关,当尝试实现良好的性能时,这些方法大多是通用的,并不依赖于任何特定的架构,因此将在第 8 章《探索监督式深度学习》和第 9 章《探索无监督式深度学习》中分别进行讲解。
接下来,让我们总结一下本章所学内容。
总结
MLP 是深度学习中基础的架构组成部分,不仅仅用于处理表格数据,它也不是一个已经被取代的老架构。如今,MLP 被广泛作为许多先进神经网络架构的子组件,既可以提供更自动化的特征工程,又可以减少大特征的维度,或将特征塑形为目标预测所需的形状。在接下来几章中要介绍的架构中,留意 MLP 或更重要的是全连接层!
深度学习框架提供的自动梯度计算简化了反向传播的实现,让我们能够专注于设计新的神经网络。确保这些网络中使用的数学函数是可微的,尽管采用成功的研究成果时通常会自动处理这一点。这正是开源研究与强大深度学习框架结合的魅力所在!
正则化是神经网络设计中至关重要的一部分,虽然我们在本章中已详细讨论了它,接下来的章节将展示它在不同架构中的应用,而不再进行进一步的解释。
在下一章中,我们将深入探讨另一种神经网络——卷积神经网络(CNN),它特别适用于图像相关任务,并具有广泛的应用。