Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

隐藏层

我们成功训练了第一个神经网络模型,它可以根据天气预报来预测冰激凌的销量。

但冰激凌的销量真的是直接由天气情况决定吗?

事实上,天气并不是直接影响冰激凌销量的因素。天气首先影响的是人们的行为和偏好,例如是否愿意出门,以及是否想吃冰激凌。

  • 天气凉爽时:人们更愿意出门,但未必想吃冰激凌;

  • 天气酷热时:人们更想吃冰激凌,但可能不愿意出门;

  • 天气寒冷时:人们既不愿意出门,也不想吃冰激凌。

从这个角度看,冰激凌的销量并不是由天气直接决定的,而是由人们出门的愿望和吃冰激凌的愿望共同决定的。


那么,我们是否可以这样做:

  • 先用一个模型根据天气预测这些中间因素;

  • 再用另一个模型根据这些中间因素预测冰激凌销量?

更进一步,我们是否可以把前一个模型的输出,直接作为后一个模型的输入,并将这两个模型连接成一个整体?

如果可以,那么在训练时,我们是不是可以把冰激凌销量的预测误差,通过反向传播传递回前面的模型,使两个模型能够同时被训练?

这种在网络模型内部自动学习中间因素的能力,正是深度神经网络的独特之处。

层

现代深度神经网络模型由很多的层(Layer)组成,每层都可以看作是一个独立的小网络模型。具体到我们的例子,我们将尝试建立一个两层的网络模型。

多层的网络模型将同样具有三个核心流程:前向传播、反向传播和参数更新,并贯穿每一层网络:

  1. 前向传播:

两层网络的前向传播:特征 x 经过隐藏层 hidden.forward()(参数 w₁、b₁)得到中间值 h,再经过输出层 output.forward()(参数 w₂、b₂)得到预测值 p
图例:前向传播(模型推理)
  1. 反向传播:

两层网络的反向传播:loss.backward() 根据预测值 p 和标签 y 得到误差项 d,再经过 output.backward()(使用 w₂)得到传给隐藏层的梯度 g
图例:反向传播(梯度计算)
  1. 参数更新:

两层网络的参数更新:optimizer.step() 分别用误差项 d 和中间值 h 更新 w₂、b₂,用梯度 g 和特征 x 更新 w₁、b₁
图例:参数更新

💡 提示:函数 np.random.seed() 用于初始化随机数,确保每次运行都产生一样的随机数,结果可复现。

张量

数据集

模型

从现在开始,我们将构建第一个由多个子网络模型组成的复杂神经网络模型,称为多层(Multi-Layer)网络模型。

这些层按照统一规定的接口(Interface)相互连通,就构成了一个复杂的大型网络模型。

抽象层

因此我们需要先定义一个所有层都遵守的规范,称为抽象层(Abstract Layer)。它规定了每个层必须实现的两件事:

  • forward(前向函数):接受输入向量,计算出输出向量。这是每一层完成前向传播的地方;

  • parameters(参数列表):返回本层需要训练、更新的参数。默认为空列表。

💡 提示:ABC 是 Python 的抽象基类(Abstract Base Class)标记,它的作用是:标记这个类不能被实例化、只能被继承。@abstractmethod 则定义了其中的抽象函数,即子类必须实现的函数。

线性层

有了抽象层以后,我们把现有的线性回归模型改写成线性层(Linear Layer)。同时扩展两个重要的功能:

  1. 随机初始化权重

之前我们一直把权重初始化为平均值:np.ones((out_size, in_size)) / in_size。简单模型这样没问题,但是到了多层模型,完全相同的初始化权重,会计算出相同的输出值,进而相同的梯度,所有输出永远保持一模一样。等于所有多出来的神经元都白费了,这种现象被称为模型训练的对称性(Symmetry)。

解决办法很简单:给每个神经元不同的初始权重,使每个神经元的输出和梯度朝着不同的方向发展。我们用 np.random.randn 来生成(不同的)随机数,再使用一个缩放系数 np.sqrt(2 / in_size) 来调整范围。这个系数可以把权重的初始值控制在合理的范围内,避免梯度在多层间传播时越来越大、或者越来越小。这种初始化方法被称为 He 初始化(He Initialization)。

  1. 把梯度传递给父节点

之前的简单模型只需要计算自己权重的梯度。

有了多层结构以后,还要考虑需要把梯度传递给前面的层(父节点)。也就是说输入值可能不是特征,而是前一层传来的中间值,所以:

  • 在梯度函数中需要计算输入值的梯度;

  • 把输入值加入父节点列表中,因为输入值不再是叶节点了,而是中间节点。

💡 提示:这两行代码非常关键,因为这样就把多层网络模型连接了起来,使得反向传播可以穿透层与层的隔断,实现遍历整个复杂网络模型的计算图。

顺序层

顺序层(Sequential Layer)是我们的第一个复合层(Composite Layer)。

我们可以把复合层理解成多个子层的容器。顺序层是最简单的一种:顺序执行每一个子层。

  • forward(前向函数):把输入值依次输送给每一层,前一层的输出作为后一层的输入,最后一层的输出就是预测值;

  • parameters(参数列表):把所有层的参数列表整合成一个大的列表,一次性提供给优化器,从而更新所有参数。

💡 提示:顺序层本身也是从抽象层继承而来,因此它也可以成为另一个顺序层(或者其他复合层)的子层,从而“搭积木”构成更大型的网络模型。

损失函数(均方误差)

优化器(随机梯度下降)

训练器

超参数

学习率

批大小

轮数

建模

现在,我们用顺序层把两个线性层串联成一个两层网络模型:

  • 第一层 Linear(2, 4):输入是 2 个特征 [温度, 湿度],输出是 4 个中间值。这 4 个中间值,就对应我们开头设想的出门的愿望和吃冰激凌的愿望,只不过我们给了模型更大的自由度,让模型自己在训练中摸索出真实的数据关联性;

  • 第二层 Linear(4, 1):输入是第一层输出的 4 个中间值,输出是 1 个预测值(冰激凌销量)。

第一层的 4 个神经元不直接产生输出值(预测值),也被称为隐藏层(Hidden Layer)。这里也是网络模型体现智能的地方,可以自主捕捉数据的深层联系。

第二层,也即最后一层,被称为输出层(Output Layer)。

训练

评估

prediction:	Tensor([[166.52357679]])
loss:	Tensor(2.321286244423445)

实践证明,两层网络模型是可行的。但是和单层网络模型相比,也没有明显的区别,损失值甚至还更差一些。

这并不奇怪。因为到目前为止,我们还只是堆叠了两层线性回归。在数学上,两层线性回归依旧是线性的。要让多层网络模型真正发挥作用,我们还缺最后一块拼图:引入非线性。

课后练习

  1. 修改隐藏层的神经元个数(比如:2 个、8 个),观察模型训练效果有什么变化?

  2. 可以增加隐藏层的数量(比如:2 层、3 层)吗?试一试能不能实现?