什么是深度学习呢?深度学习是一种以深层计算堆栈为特征的机器学习方法。正是这种计算的深度使得深度学习模型能够分解在最具挑战性的真实世界数据集中发现的复杂和层次化的模式。
凭借其强大和可扩展性,神经网络已成为深度学习的标志性模型。神经网络由神经元组成,每个神经元仅执行简单的计算。神经网络的力量来自于这些神经元能够形成的连接的复杂性。
线性单元
那么让我们从神经网络的根本组件开始:单个神经元。作为一个图示,一个具有一个输入的神经元(或单元)看起来像:
线性单元:y = wx + b
输入是 x 。它与神经元的连接有一个权重,这个权重是 w 。每当一个值通过一个连接流动时,你将这个值乘以连接的权重。对于输入 x ,到达神经元的是 w * x 。神经网络通过修改其权重来“学习”。
b 是一种特殊的权重,我们称之为偏差。偏差没有与任何输入数据相关联;相反,我们在图中放入一个 1 ,以便到达神经元的值只是 b (因为 1 * b = b )。偏差使神经元能够独立于其输入来修改输出。
y 是神经元最终输出的值。为了得到输出,神经元会将其通过连接接收到的所有值加总。这个神经元的激活值是 y = w * x + b ,或者用公式表示为 y = wx + b,一个经典的斜截式方程。
尽管单个神经元通常只作为更大网络的一部分工作,但通常从单个神经元模型开始作为基准是有用的。单个神经元模型是线性模型。
我们可以为每个附加特征向神经元添加更多的输入连接。为了找到输出,我们将每个输入与其连接权重相乘,然后将它们全部加在一起。
具有两个输入的线性单元将拟合一个平面,而具有更多输入的单元将拟合一个超平面。
Kera中的线性单元
在 Keras 中创建模型最简单的方法是通过 keras.Sequential ,它将神经网络创建为层的堆栈。我们可以使用密集层来创建上述模型。
我们可以定义一个接受三个输入特征( 'sugars' 、 'fiber' 和 'protein' )并产生一个输出( 'calories' )的线性模型,如下所示:
from tensorflow import keras
from tensorflow.keras import layers
# 创建只有一个单元的网络
model = keras.Sequential([
layers.Dense(units=1, input_shape=[3])
])使用第一个参数 units ,我们定义我们想要的输出数量。在这种情况下,我们只是预测 'calories' ,所以我们将使用 units=1 。
使用第二个参数 input_shape ,我们告诉 Keras 输入的维度。设置 input_shape=[3] 确保模型将接受三个特征作为输入( 'sugars' 、 'fiber' 和 'protein' )。
这个模型现在可以拟合训练数据了!
为什么 input_shape 是一个 Python 列表?
我们在这门课程中将要使用的数据是表格数据,就像 Pandas DataFrame 中的数据一样。我们将为数据集中的每个特征提供一个输入。特征按列排列,所以我们总是会有
input_shape=[num_columns]。Keras 在这里使用列表的原因是允许使用更复杂的数据集。例如,图像数据可能需要三个维度:[height, width, channels]。
深度神经网络
Layers
神经网络通常将它们的神经元组织成层。当我们把具有相同输入集的线性单元聚集在一起时,我们就得到了一个密集层(Dense Layer)。全连接"(Fully Connected)指的是神经网络中相邻两层之间的每一个神经元都与另一层的所有神经元相互连接。这种连接方式是最经典和基础的神经网络层结构之一,也称为密集层(Dense Layer)
你可以将神经网络中的每一层视为执行某种相对简单的转换。通过层层堆叠,神经网络可以以越来越复杂的方式转换其输入。在一个训练良好的神经网络中,每一层都是一个转换,让我们更接近解决方案。
许多种类的层
Keras 中的"层"是一个非常通用的概念。层本质上可以是任何类型的数据转换。许多层,如卷积层和循环层,通过使用神经元来转换数据,它们的主要区别在于形成的连接模式。然而,其他层则用于特征工程或简单的算术运算。有整个世界层的等待探索——去发现它们吧!
激活函数
然而,事实证明,两个密集层之间没有任何东西,与单个密集层本身相比并没有更好。单独的密集层永远无法带我们走出线条和平面的世界。我们需要的是非线性的东西。我们需要的是激活函数。
激活函数简单来说就是我们对每一层的输出(它的激活值)应用的一些函数。最常见的是修正线性函数: \max (0,x)
整流函数的图像是一条线,其中负的部分被“整流”为零。将此函数应用于神经元的输出会在数据中产生一个弯曲,使我们远离简单的直线。
当我们将整流器连接到一个线性单元时,我们得到一个整流线性单元或 ReLU。(因此,通常将整流函数称为"ReLU 函数"。)对一个线性单元应用 ReLU 激活意味着输出变为 max(0, w * x + b) ,我们可以在图中这样绘制:
堆叠密集层
现在我们有了非线性,让我们看看如何堆叠层来得到复杂的数据转换。
输出层之前的层有时被称为隐藏层,因为我们直接看不到它们的输出。
现在,请注意最终(输出)层是一个线性单元(也就是说,没有激活函数)。这使得该网络适合回归任务,即我们试图预测某个任意的数值。其他任务(如分类)可能需要在输出层使用激活函数。
构建序列模型
我们一直在使用的 Sequential 模型将按顺序连接一系列层:第一层接收输入,最后一层产生输出。这创建了上图中的模型:
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
# 隐藏的ReLU层
layers.Dense(units=4, activation='relu', input_shape=[2]),
layers.Dense(units=3, activation='relu'),
# 线性输出
layers.Dense(units=1),
])随机梯度下降
和所有机器学习任务一样,我们从一个训练数据集开始。训练数据集中的每个示例都包含一些特征(输入)以及一个期望的目标(输出)。训练网络意味着调整其权重,使其能够将特征转换为目标。例如,在 80 Cereals 数据集中,我们希望构建一个网络,能够接收每种谷物的 'sugar' 、 'fiber' 和 'protein' 含量,并预测其 'calories' 。如果我们成功地训练一个网络来做到这一点,那么它的权重必须在某种程度上代表训练数据中这些特征与目标之间的关系。
除了训练数据,我们还需要两个更多东西:
- 一个"损失函数",用于衡量网络的预测有多好。
- 一个能够告诉网络如何改变其权重的"优化器"。
损失函数
我们已经看到了如何设计一个网络的架构,但我们还没有看到如何告诉网络要解决什么问题。这就是损失函数的工作。
损失函数测量目标真实值与模型预测值之间的差异。
不同的问题需要不同的损失函数。我们一直在研究回归问题,这类问题的任务是为某些数值进行预测——例如 80 种谷物中的卡路里含量,或红酒质量中的评分。其他回归任务可能包括预测房屋价格或汽车的燃油效率。
回归问题中常见的损失函数是平均绝对误差(MAE)。对于每个预测值 y_pred ,MAE 通过绝对差值 abs(y_true - y_pred) 来衡量与真实目标值 y_true 之间的差异。
数据集上的总 MAE 损失是所有这些绝对差值的平均值。

除了 MAE 之外,回归问题中你可能会看到的其他损失函数包括均方误差(MSE)或 Huber 损失(两者在 Keras 中都有提供)。
在训练过程中,模型将使用损失函数作为指导,以找到其权重的正确值(损失越低越好)。换句话说,损失函数告诉网络其目标。
优化器(Only 随机梯度下降)
我们已经描述了网络需要解决的问题,但现在我们需要说明如何解决它。这就是优化器的任务。优化器是一种调整权重以最小化损失的算法。
深度学习中几乎所有的优化算法都属于随机梯度下降这一家族。它们是逐步训练网络的迭代算法。训练的一个步骤如下:
- 采样一些训练数据,并将其输入网络以做出预测。
- 测量预测值与真实值之间的损失。
- 最后,在使损失减小的方向上调整权重。
然后就这样反复操作,直到损失变得尽可能小(或者直到它不再减少。)每次迭代的训练数据样本称为小批量(或简称"批量"),而完整的一轮训练数据称为一个 epoch。你训练的 epoch 数决定了网络会看到每个训练样本的次数。
学习率(Learning Rate)
注意,这条线在每个批量方向上只做了小幅度的移动(而不是完全移动)。这些移动的大小由学习率决定。较小的学习率意味着网络需要查看更多的子批量,其权重才能收敛到最佳值。
学习率和子批量的大小是影响 SGD 训练过程的两项参数。它们之间的相互作用通常很微妙,而这两项参数的正确选择并不总是显而易见的。(我们将在练习中探讨这些影响。)
幸运的是,对于大多数工作,无需进行广泛的超参数搜索即可获得令人满意的结果。Adam 是一种具有自适应学习率的 SGD 算法,这使得它适用于大多数问题而无需任何参数调整(在某种意义上,它是"自我调整"的)。Adam 是一个通用的优秀优化器。
添加损失函数和优化器
在定义模型后,您可以使用模型的 compile 方法添加损失函数和优化器:
model.compile(
optimizer="adam",
loss="mae",
)请注意,我们只需使用字符串即可指定损失函数和优化器。你也可以通过 Keras API 直接访问这些设置——例如,如果你想调整参数的话——但对我们来说,默认设置已经足够好了。
名字里有什么意义?
梯度 gradient 是一个向量,它告诉我们权重需要朝哪个方向变化。更精确地说,它告诉我们如何改变权重以使损失变化最快。我们称这个过程为梯度下降,因为它使用梯度来沿着损失曲线下降 descent 到最小值。随机 Stochastic 意味着“由机会决定”。我们的训练是随机的,因为小批量是从数据集中随机采样的。这就是它被称为 SGD 的原因!
过拟合和欠拟合
之前 Machine Learning 也记录过,但是并没有对学习曲线了解太多。
我们可以将训练数据中的信息分为两类:信号和噪声。信号是泛化部分,是能够帮助模型从新数据中进行预测的部分。噪声是仅适用于训练数据的部分;噪声是来自现实世界数据的随机波动,或者是所有偶然的、无信息的模式,这些模式实际上并不能帮助模型进行预测。噪声是看起来有用但实际上并非如此的部分。
我们通过选择能够使训练集上的损失最小化的权重或参数来训练模型。然而,你可能知道,为了准确评估模型的性能,我们需要在新的数据集,即验证数据集上对其进行评估
在训练模型时,我们一直按每个 epoch 绘制训练集上的损失。接下来,我们也将添加一个验证数据的绘图。这些图我们称为学习曲线。为了有效地训练深度学习模型,我们需要能够解读它们。
现在,训练损失下降要么是因为模型学习了信号,要么是因为它学习了噪声。但验证损失只会下降,当模型学习了信号时。 (模型从训练集中学习的任何噪声都不会泛化到新数据中。)所以,当模型学习信号时,两条曲线都会下降,但当它学习噪声时,曲线之间会出现一个间隙。间隙的大小告诉你模型学习了多少噪声。
理想情况下,我们希望创建的模型能学习所有信号而不学习任何噪声。这在实践中几乎不可能发生。相反,我们进行权衡。我们可以让模型以学习更多噪声为代价来学习更多信号。只要权衡对我们有利,验证损失就会继续下降。然而,在某个点上,权衡可能会对我们不利,成本超过了收益,验证损失开始上升。
这种权衡表明,在训练模型时可能会出现两个问题:信号不足或噪声过多。欠拟合训练集是指由于模型没有学习到足够的信号,导致损失没有达到可能的最小值。过拟合训练集是指由于模型学习了过多的噪声,导致损失没有达到可能的最小值。训练深度学习模型的诀窍在于在这两者之间找到最佳平衡。
我们将探讨几种从训练数据中获取更多信号同时减少噪声的方法。
模型容量
一个模型的容量指的是它能学习的模式的规模和复杂性。对于神经网络来说,这主要取决于它有多少个神经元以及它们是如何连接在一起的。如果您的网络看起来拟合数据不足,您应该尝试增加其容量。
您可以通过使网络变宽(给现有层增加更多单元)或使其变深(增加更多层)来增加网络的容量。较宽的网络更容易学习更线性的关系,而较深的网络则更倾向于非线性关系。哪种更好取决于数据集。
model = keras.Sequential([
layers.Dense(16, activation='relu'),
layers.Dense(1),
])
wider = keras.Sequential([
layers.Dense(32, activation='relu'),
layers.Dense(1),
])
deeper = keras.Sequential([
layers.Dense(16, activation='relu'),
layers.Dense(16, activation='relu'),
layers.Dense(1),
])Early Stopping
我们提到,当模型过于急切地学习噪声时,验证损失在训练过程中可能会开始增加。为了防止这种情况,我们只需在验证损失似乎不再减少时随时停止训练。以这种方式中断训练称为提前停止。
一旦我们检测到验证损失开始再次上升,我们就可以将权重重置为最小值出现的位置。这确保了模型不会继续学习噪声并过拟合数据。
使用早停机制也意味着我们更不容易在神经网络完成学习信号之前就停止训练,从而避免了过早停止训练的风险。因此,除了防止因训练时间过长导致过拟合,早停机制也能防止因训练时间不够导致欠拟合。只需将训练轮数设置为某个较大的数值(比你实际需要的多),早停机制会处理好剩下的部分。
在 Keras 中,我们通过回调函数在训练中包含早停机制。回调函数只是你希望在神经网络训练过程中定期运行的函数。早停回调函数会在每个轮次后运行。(Keras 预定义了多种有用的回调函数,你也可以定义自己的。)
from tensorflow.keras.callbacks import EarlyStopping
early_stopping = EarlyStopping(
min_delta=0.001, # 一轮最少要提升多少
patience=20, # 要跑多少轮epoch
restore_best_weights=True,
)这些参数表示:"如果在之前的 20 个 epoch 中,验证损失没有至少改善 0.001,那么停止训练并保留你找到的最佳模型。" 有时很难判断验证损失上升是由于过拟合还是仅仅由于随机批次变化。这些参数允许我们在何时停止训练时设置一些容许范围。
Dropout 和 批量归一化
深度学习的世界并不仅仅局限于密集层。你可以向模型中添加数十种不同类型的层。(可以尝试浏览一下 Keras 文档来获取一些示例!)有些层类似于密集层,定义神经元之间的连接,而另一些层可以进行预处理或执行其他类型的转换。
我们将学习两种特殊的层,它们本身不包含任何神经元,但可以添加一些功能,有时以各种方式使模型受益。这两种层在现代架构中都很常用。
Dropout
其中第一种是“dropout 层”,它可以帮助纠正过拟合。
在之前的内容中我们讨论了过拟合是如何由网络在训练数据中学习虚假模式引起的。为了识别这些虚假模式,网络通常会依赖非常特定的权重组合,一种“共谋”的权重。由于如此特定,它们往往很脆弱:移除一个,共谋就会瓦解。
这就是 dropout 的想法。为了打破这些共谋,我们在训练的每一步随机地丢弃一部分层的输入单元,这使得网络学习训练数据中那些虚假模式变得非常困难。相反,它必须寻找更广泛、更一般的模式,这些模式的权重模式往往更稳健。
你也可以将 dropout 理解为创建一种网络集成。预测将不再由一个大型网络做出,而是由一组小型网络共同完成。委员会中的个体往往会犯不同类型的错误,但在同一时间又是正确的,这使得整个委员会比任何个体都更优秀。(如果你熟悉随机森林作为决策树的集成,其原理是相同的。)
在 Keras 中,dropout rate 参数 rate 定义了要关闭的输入单元的百分比。将 Dropout 层放在要应用 dropout 的层之前:
keras.Sequential([
# ...
layers.Dropout(rate=0.3), # 在下一层添加 30% 的Dropout
layers.Dense(16),
# ...
])批量归一化
接下来我们将要研究的特殊层执行"批量归一化"(或"batchnorm"),这有助于纠正训练缓慢或不稳定的情况。
对于神经网络来说,通常是一个好主意将所有数据放在一个共同的尺度上,也许可以使用 scikit-learn 的 StandardScaler 或 MinMaxScaler 之类的工具。原因在于 SGD 会根据数据产生的激活大小来调整网络权重。那些倾向于产生非常不同大小激活的特征可能会导致训练行为不稳定。
现在,如果数据在进入网络之前进行归一化是好的,那么在网络内部进行归一化可能也会更好!实际上,我们有一种特殊的层可以做到这一点,那就是批量归一化层。批量归一化层会查看进入的每个批次,首先使用该批次自己的均值和标准差来归一化批次,然后还使用两个可训练的缩放参数将数据映射到新的尺度上。实际上,批量归一化会对它的输入进行一种协调的缩放。
通常情况下,批量归一化被添加到优化过程中作为辅助手段(尽管有时它也能帮助提升预测性能)。带有批量归一化的模型往往需要更少的训练周期即可完成训练。此外,批量归一化还可以解决导致训练"停滞"的各种问题。建议在模型中加入批量归一化,尤其是在训练过程中遇到困难时。
似乎批量归一化可以在网络的几乎任何位置使用。你可以把它放在一层之后或者放在一层和它的激活函数之间:
layers.Dense(16, activation='relu'),
layers.BatchNormalization(),layers.Dense(16),
layers.BatchNormalization(),
layers.Activation('relu'),如果你将它作为网络的第一层添加,它可以充当一种自适应预处理器,替代类似 Sci-Kit Learn 的 StandardScaler 。
二元分类
将数据分类到两个类别中是一个常见的机器学习问题。你可能想要预测客户是否会购买,信用卡交易是否欺诈,深空信号是否显示出新行星的证据,或者医学测试是否显示出疾病的证据。这些都是二元分类问题。
在你的原始数据中,类别可能由字符串如 "Yes" 和 "No" ,或者 "Dog" 和 "Cat" 表示。在使用这些数据之前,我们将分配一个类别标签:一个类别将是 0 ,另一个将是 1 。分配数字标签将数据转换为神经网络可以使用的格式。
准确率和交叉熵
准确率是用于衡量分类问题成功与否的众多指标之一。准确率是指正确预测数与总预测数的比率: accuracy = number_correct / total 。一个总是预测正确的模型将拥有 1.0 的准确率。在其他条件相同的情况下,只要数据集中的各类别出现频率大致相同,准确率就是一个合理的指标。
准确率(以及大多数其他分类指标)的问题在于它不能被用作损失函数。SGD 需要一个变化平滑的损失函数,但准确率作为计数比,是跳跃式变化的。因此,我们必须选择一个替代品来充当损失函数。这个替代品就是交叉熵函数。
现在,回想一下损失函数定义了网络在训练过程中的目标。在回归问题中,我们的目标是使预期结果与预测结果之间的距离最小化。我们选择了 MAE 来衡量这种距离。
在分类中,我们想要的其实是在概率之间的距离,而交叉熵正是提供了这种距离。交叉熵是一种衡量一个概率分布与另一个概率分布之间距离的度量。
我们的想法是希望我们的网络能够以概率 1.0 预测正确的类别。预测概率与 1.0 的距离越远,交叉熵损失就越大。
我们使用交叉熵的技术原因有些微妙,但要记住的主要一点是:使用交叉熵作为分类损失;其他你可能关心的指标(如准确率)往往会随着它一起提高。
Sigmoid 函数生成概率
交叉熵和准确率函数都需要概率作为输入,也就是说,需要 0 到 1 之间的数值。为了将密集层产生的实数值输出转换为概率,我们附加了一种新的激活函数——Sigmoid 激活函数。
要得到最终的类别预测,我们定义一个阈值概率。通常这个阈值是 0.5,这样四舍五入就能得到正确的类别:低于 0.5 表示标签为 0 的类别,0.5 或以上表示标签为 1 的类别。Keras 在默认的准确率指标中使用 0.5 的阈值。












文章评论