神经网络(Aritificial Neural Network,ANN/NN):是一种模仿生物神经网络结构和功能的计算模型。人脑可以看做是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号,树突收到输入信号,然后对信号进行处理,通过轴突输出信号。
神经网络每层都有大量节点,每个节点相当于一个神经元,并排排列在一起,同层互不传递信号,轴突末梢将信号传递到下一层。
如何构建神经网络
神经网络是由多个神经元组成,构建神经网络就是在构建神经元。以下是神经网络中神经元的构建说明:
激活函数先把线性算出来的数重塑(做非线性变换)、过滤,弱信号压低、强信号保留,然后把处理好的值传给下一层。
接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图:
神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:
- 输入层(Input Layer) :即输入 x 的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。
- 输出层(Output Layer) :即输出 y 的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
- 隐藏层(Hidden Layers) :输入层和输出层之间都是隐藏层,神经网络的 “深度” 通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。
特点是:
- 同一层的神经元之间没有连接
- 第 N 层的每个神经元和第 N-1 层 的所有神经元相连(这就是 full connected 的含义),这就是全连接神经网络
- 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
- 第 N-1 层神经元的输出就是第 N 层神经元的输入
- 每个连接都有一个权重值(w 系数和 b 系数)
注意: 输入层只负责传数据,不做任何计算,隐藏层 + 输出层:才是真正做线性 + 激活计算的神经元。
神经网络内部状态值和激活值:
每一个神经元工作时,前向传播会产生两个值,内部状态值(加权求和值)和激活值;反向传播时会产生激活值梯度和内部状态值梯度。
内部状态值(线性加权得到的结果)
神经元或隐藏单元的内部存储值,它反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。z=W⋅x+b
- W:权重矩阵
- x:输入值
- b:偏置
激活值(对线性加权结果进行非线性转换得到的结果)
通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果。激活值决定了当前神经元的输出。a=f(z)
- f:激活函数
- z:内部状态值
激活函数
激活函数用于对每层的输出数据进行变换,进而为整个网络注入了非线性因素。此时,神经网络就可以拟合各种曲线。
- 没有引入非线性因素的网络等价于使用一个线性模型来拟合
- 通过给网络输出增加激活函数,实现引入非线性因素,使得网络模型可以逼近任意函数,提升网络对复杂问题的拟合能力.
常见的激活函数-sigmoid激活函数(一般只用于二分类的输出层,如果要使用在隐藏层,不要超过5层)
Sigmoid 是什么?
它的公式是:f(x)=1+e−x1
- 作用:把任意一个数,压缩到 0~1 之间(比如 - 10→0,10→1,0→0.5)。
- 最经典的用法:二分类问题的输出层,用来表示 “概率”(比如这个样本是猫的概率是 0.8)。 用法: torch.sigmoid(x)
它的导数公式(图里的 f'(x))
f′(x)=f(x)×(1−f(x))
- 从公式能看出来,导数的最大值是
0.5 × 0.5 = 0.25,永远不会超过 0.25。 - 对应右边的导数图像:只有在输入接近 0 的时候,导数才接近 0.25;输入离 0 越远,导数直接趋近于 0。
什么是「链式法则」(反向传播的本质->链式求导)?
举个最简单的例子,你就懂了:假设你有一个函数链:a → b → c也就是:c = f(b),而 b = g(a)
如果想算 c 对 a 的导数(变化率),链式法则说:dc/da = dc/db × db/da意思就是:最终结果的变化率,等于中间每一步变化率的乘积。
放到神经网络里:
- 输出层的误差
Loss→ 第 5 层的输出 → 第 4 层的输出 → … → 第 1 层的输出 - 所以
Loss对第 1 层参数的导数(梯度),就是误差从后往前,每一步的导数连乘。
为什么要「连乘激活函数的导数」?
在神经网络里,每一层的计算是这样的:
- 线性计算:
z = wx + b(加权求和) - 激活函数:
a = σ(z)(Sigmoid 变换)
反向传播时,误差要从后往前传,必须先经过激活函数这一步。所以,第 1 层参数的梯度,会被连续乘上每一层激活函数的导数:∂Loss/∂w1 = 误差 × σ'(z5) × σ'(z4) × σ'(z3) × σ'(z2) × σ'(z1)
训练神经网络,本质就是两件事来回循环:
-
前向传播:把数据从输入层算到输出层,算出最终的损失
loss。 -
⭐反向传播 + 梯度下降:
- 算出
loss对每一层每个参数的梯度(导数) 。 - 用这个梯度告诉参数:往哪个方向走、走多远,才能让 loss 变小。
- 然后按
参数 = 参数 - 学习率 × 梯度更新参数。
- 算出
所以,没有梯度,就不知道该往哪调、调多少,模型根本没法训练。
为什么 Sigmoid 会导致「梯度消失」?
关键就在 Sigmoid 导数的特性上:
- 它的导数
σ'(z)最大值只有 0.25,大部分时候都远小于 1。 - 连续乘多个小于 1 的数,结果会指数级变小!
举个具体的数字例子:假设每一层的导数都取最大值 0.25,网络有 5 层:0.25×0.25×0.25×0.25×0.25=0.255=0.0009765625这个结果几乎等于 0
误差传到最前面的第 1 层时,已经被乘得几乎消失了,参数几乎得不到任何更新信号,这就是梯度消失。
常见的激活函数 -tanh激活函数
用法: torch.tanh(x)
常见的激活函数-ReLU激活函数
- Sigmoid:全局必然梯度消失
- ReLU:局部个别神经元死掉,整体网络不会全员梯度消失
“ReLU 会使一部分神经元的输出为 0” ,就是说:部分神经元因为输入长期为负,被函数截断输出为 0,既不贡献信号,也不更新参数,最终导致网络变稀疏,部分神经元 “死亡”。
-
好处:减少了参数之间的相互依赖,让模型不会过度拟合训练数据,起到了缓解过拟合的效果。
-
代价:如果死掉的神经元太多,网络的有效容量就会下降,学习能力会变弱。
过拟合:模型在训练集上表现极好(loss 很低,准确率很高),但在测试集上表现很差。它过度学习了训练数据里的随机噪声、异常值,把它们当成了通用规律
用法: torch.relu(x)
常见的激活函数-SofMax 激活函数:
激活函数的选择方法:
对于隐藏层:
- 优先选择 ReLU 激活函数
- 如果 ReLu 效果不好,那么尝试其他激活,如 Leaky ReLu 等。
- 如果你使用了 ReLU, 需要注意一下 Dead ReLU 问题, 避免出现 0 梯度从而导致过多的神经元死亡。
- 少用使用 sigmoid 激活函数,可以尝试使用 tanh 激活函数
对于输出层:
- 二分类问题选择 sigmoid 激活函数
- 多分类问题选择 softmax 激活函数
- 回归问题选择 identity 激活函数
参数初始化:
均匀分布初始化
- 权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置为在(-1/√d,1/√d) 均匀分布中生成当前神经元的权重,其中 d 为神经元的输入数量(输入到神经元的连接数量(也叫“扇入”),为什么要除以√d,这是为了控制输出值的方差,如果没有这个缩放,当d很大(输入连接很多时),加权求和的结果可能就非常大,导致激活函数进入饱和区,梯度变小,学习变慢,缩放后,无论d多大,网络出奇的输出值能保持在一个合理的范围内,利于训练)
正态分布初始化
- 随机初始化从均值为 0,标准差是 1 的高斯分布中取样,使用一些很小的值对参数 W 进行初始化
全 0 初始化
- 将神经网络中的所有权重参数初始化为 0
全 1 初始化
- 将神经网络中的所有权重参数初始化为 1
固定值初始化
- 将神经网络中的所有权重参数初始化为某个固定值
为什么不能随意初始化?
- 初始状态:神经元 A 和 B 的所有输入连接权重都设成了 0(或者同一个固定值)。
- 前向传播:A 和 B 接收的输入完全一样,权重也完全一样,所以它们的输出也完全一样。
- 反向传播:A 和 B 的输出一样,计算出来的梯度也完全一样。
- 参数更新:梯度一样、学习率一样,所以权重更新的幅度和方向也完全一样。
- 最终结果:A 和 B 永远保持一模一样,就像一个神经元复制了两遍,网络的容量被白白浪费了 —— 本来两个神经元可以学两种不同的特征,现在只能学一种。(破坏了对称性,使得神经元无法差异化)。
假设我们有一个超简单的线性模型:y=w⋅x+b
- x 是输入
- w 是权重,b 是偏置
- y 是模型的输出
我们定义损失函数为:L=(y−t)2其中 t 是真实标签(目标值)。
现在我们要算损失对权重 w 的梯度,也就是:∂L/∂w根据链式法则:∂L/∂w=∂L/∂y⋅∂y/∂w
- 第一部分 ∂L/∂y:损失对输出 y 的导数,它告诉我们 “输出变一点点,损失会变多少”。
- 第二部分 ∂y/∂w:输出对权重 w 的导数,它告诉我们 “权重变一点点,输出会变多少”。
把两部分乘起来,就得到了最终的梯度。
回到之前的神经元例子
两个神经元 A 和 B:
- 初始权重完全一样,输入也一样 → 它们的输出 yA 和 yB 也完全一样。
- 因为输出一样,它们对最终损失的贡献也完全一样 → 损失对输出的导数∂L/ ∂yA 和∂L/ ∂yB 也完全一样。
- 再加上它们的输出对权重的导数 ∂yA/∂wA 和 ∂yB/∂wB 也一样 → 最终的梯度 ∂L/∂wA 和 ∂L/∂wB 也完全一样。
所以:输出完全相同 → 梯度完全相同 → 权重更新完全相同 → 两个神经元永远一模一样。
各初始化方法比较表:
import torch
import torch.nn as nn # 神经网络工具箱,里面全是搭网络、初始化、激活函数的工具
# 给某一层里的权重参数w做初始化
d = 5 # 输入维度(输入连接数/上一层神经元的个数)
w = torch.empty(3,d)
# 创建一个3行5列的空矩阵,用来存3个神经元的权重(本层神经元个数是3,矩阵里存的是上一层5个神经元到这一层每个神经元连接的权重)
#均匀分布初始化
nn.init.uniform_(w,a=-1/torch.sqrt(torch.tensor(d)),b=1/torch.sqrt(torch.tensor(d)))
#nn.uniform:初始化工具。 uniform_: 均匀分布随机赋值,所有值在[a,b]之间完全随机,概率一样
#w:要初始的权重矩阵。 a:最小值。 b:最大值。
print(w)
#Xavier初始化(最常用/适合Sigmoid/Tanh)
nn.init.xavier_uniform_(w)
print(w)
#He初始化(最常用,适合ReLU/GELU)
nn.init.kaiming_uniform_(w)
print(w)
神经网络搭建和参数计算:
在pytorch中定义神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:
- __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
- forward方法,在实例化模型的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等。