02_神经网络基础

1 神经网络的构成

1
2
3
4
5
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme()

人工神经网络(Artificial Neural Network,ANN)简称神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型,由多个神经元构成。

每个神经元类似机器学习中的感知机,每个输入有不同的权重,加权求和经过激活函数得到输出。

使用多个神经元构成网络,相邻层之间相互连接,每层之间通过激活函数进行非线性映射,最终得到结果。同一层的多个神经元可以看作是并行计算处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。类似集成学习的思想,多个神经元组合在一起,可以处理更复杂的数据。

每一层神经网络(全连接层)本质都是一次线性变换加一次非线性映射,类似逻辑回归+激活函数。神经网络正是靠这种“线性变换 + 非线性映射”的重复堆叠,才能从简单的线性模型扩展成可以逼近任意函数的复杂结构。

  • 输入层(Input Layer): 每个输入特征对应一个神经元
  • 输出层(Output Layer): 输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果
  • 隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的“深度”通常由隐藏层的数量决定

相邻层的神经元相互连接,称为全连接(Fully Connected),全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递。每个连接都会有一个权重,神经元的信息逐层传递称为前向传播(forward),上一层的输出作为下一层的输入。

每个神经元在前向传播时保存内部状态值(加权求和值)激活值,在反向传播时计算并保存的激活值梯度内部状态值梯度

  • 前向传播
    • 内部状态值:𝑧 = 𝑤 ⋅ 𝑥 + 𝑏
    • 激活值:𝑎 = 𝑓(𝑧)
  • 反向传播
    • 激活值梯度:损失函数对激活值的偏导 $\frac{\partial L}{\partial a}$
    • 内部状态值梯度:激活值梯度 × 激活函数导数 $\frac{\partial L}{\partial z}=\frac{\partial L}{\partial a} · f^{\prime}(z)$

2 激活函数

如果没有激活函数,无论加入多少隐藏层,整个神经网络都等效于单层线性变换。激活函数为神经网络引入了非线性,使得神经网络能够学习和表示复杂的非线性关系。

可视化神经网络

2.1 阶跃函数

阶跃函数是在感知机中最简单的激活函数,可以为输入设置一个阈值;一旦超过这个阈值,就切换输出(0 或者 1)。阶跃函数在0点不可导,并且在其他点导数为0,因此无法进行梯度下降。

$$f(x)=\left\{ \begin{array} {c}0,x<0 \\ 1, x\geq0 \end{array}\right. ,\quad f^{\prime}(x)=0$$

1
2
3
4
5
def step(x):
return np.array(x > 0, dtype=np.int32)

x = np.arange(-10, 10, 0.1)
plt.plot(x, step(x))
[<matplotlib.lines.Line2D at 0x176df26e0>]
png

2.2 Sigmoid函数

Sigmoid(也叫 Logistic 函数)是平滑可微的,能将任意输入映射到区间(0,1)。因其涉及指数运算,计算量相对较高。

$$f(x)=\frac{1}{1+e^{-x} } ,\quad f^{\prime}(x)=\frac{1}{1+e^{-x} }\left(1-\frac{1}{1+e^{-x} }\right)=f(x)\left(1-f(x)\right)$$

Sigmoid 的导数范围是(0, 0.25],梯度较小,并且输入在 [-6, 6] 之外的输出值变化很小,此时网络参数更新极其缓慢,甚至无法更新。

Sigmoid 一般在 5 层之内就会出现梯度消失的情况,并且由于该函数不是以0为中心的,激活值总是正数,在梯度更新时只对某些特征产生相同方向的影响。一般只用在二分类的输出层或者层数较少的神经网络。

Sigmoid 在二分类的输出层,输出为 1 个节点,直接表示样本属于类别 1 的概率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def my_sigmoid(x):
return 1 / (1 + np.exp(-x))

def sigmoid_prime(x):
return my_sigmoid(x) * (1 - my_sigmoid(x))

_, axs = plt.subplots(1, 2, figsize=(12, 5))
x = np.arange(-10, 10, 0.1)

axs[0].plot(x, my_sigmoid(x))
axs[0].set_title("Sigmoid(x)")
axs[1].plot(x, sigmoid_prime(x))
axs[1].set_title("Sigmoid'(x)")

axs[0].axhline(c='gray', ls='--') # x 轴
axs[0].axvline(c='gray', ls='--') # y 轴
axs[1].axhline(c='gray', ls='--')
axs[1].axvline(c='gray', ls='--')
<matplotlib.lines.Line2D at 0x177a2ccd0>
png

2.3 Tanh函数

上面说到 Sigmoid 不以 0 值为中心,并且导数值比较小,Tanh 就是对 Sigmoid 的改进,进行缩放和偏移,使其以 0 值为中心,并且导数值接近 1。

$$f(x)=\frac{2}{1+e^{-2x} }-1=\frac{2}{1+e^{-2x} }+\frac{-1-e^{-2x} }{1+e^{-2x} }=\frac{1-e^{-2x} }{1+e^{-2x} }$$

$$f^{\prime}(x)=1-\left(\frac{1-e^{-2x} }{1+e^{-2x} }\right)^{2}=1-f^{2}(x)$$

Tanh 在 [-3, 3] 之外的输出值变化也很小,与 Sigmoid 相比,Tanh 以 0 为中心,并且梯度值更大,所以收敛速度更快,但同样存在梯度消失的问题。可以在隐藏层中使用 Tanh,在输出层中使用 Sigmoid

$\frac{2}{1+e^{-2x} }-1$ 是 Sigmoid 的变形,2 倍的 Sigmoid 将 y 轴 放大 1 倍, 2x 将 x 轴缩放 1倍,再 -1 将 y 轴平移到 (-1, 1) 区间内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def my_tanh(x):
return np.tanh(x)

def tanh_prime(x):
return 1 - np.tanh(x) ** 2

_, axs = plt.subplots(1, 2, figsize=(12, 5))
x = np.arange(-10, 10, 0.1)

axs[0].plot(x, my_tanh(x))
axs[0].set_title("Tanh(x)")
axs[1].plot(x, tanh_prime(x))
axs[1].set_title("Tanh'(x)")

axs[0].axhline(c='gray', ls='--')
axs[0].axvline(c='gray', ls='--')
axs[1].axhline(c='gray', ls='--')
axs[1].axvline(c='gray', ls='--')
<matplotlib.lines.Line2D at 0x177a99150>
png

2.4 ReLU函数

ReLU(Rectified Linear Unit,修正线性单元)会将小于等于 0 的输入转换为 0,大于 0 的输入则保持不变。ReLu 更加重视正信号,而忽略负信号。

$$ f ( x )=\left\{ {\begin{array} {l} {0, x \leq0} \\ {x, x > 0} \end{array} } ,\quad \right. f^{\prime} ( x )=\left\{ {\begin{array} {l} {0, x \leq0} \\ {1, x > 0} \end{array} } \right. $$

ReLU 在大于 0 时,导数一直为 1,不存在梯度消失的问题。但在输入小于 0 时,输出为 0,ReLu 只会激活部分神经元,这种稀疏性有助于减少计算量提高效率,并且可以缓解过拟合的发生。但如果部分输入一直小于0,输出一直为0,那么就会导致另一个问题,即神经元死亡,如果大量的神经元死亡,会影响模型的学习能力。

ReLu 计算简单,不存在梯度消失的问题,并且可以使网络稀疏,常用于隐藏层,适用于深层神经网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def my_relu(x):
return np.maximum(0, x)

def relu_prime(x):
return 1.0 * (x > 0)

_, axs = plt.subplots(1, 2, figsize=(12, 5))
x = np.arange(-10, 10, 0.1)

axs[0].plot(x, my_relu(x))
axs[0].set_title("ReLU(x)")
axs[1].plot(x, relu_prime(x))
axs[1].set_title("ReLU'(x)")

axs[0].axhline(c='gray', ls='--')
axs[0].axvline(c='gray', ls='--')
axs[1].axhline(c='gray', ls='--')
axs[1].axvline(c='gray', ls='--')
<matplotlib.lines.Line2D at 0x177c517b0>
png

2.5 Leaky ReLU函数

为了解决 ReLu 的神经元死亡问题,引入了 Leaky ReLu 激活函数,在输入为负的时候引入一个很小的斜率,从而避免神经元死亡。

$$ f ( x )=\left\{ {\begin{array} {l} {\alpha x, x \leq0} \\ {x, x > 0} \end{array} },\quad \right. f^{\prime} ( x )=\left\{ {\begin{array} {l} {\alpha, x \leq0} \\ {1, x > 0} \end{array} } \right. $$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def my_leaky_relu(x):
return np.maximum(0.05 * x, x)

def leaky_relu_prime(x):
return np.where(x > 0, 1, 0.05)

_, axs = plt.subplots(1, 2, figsize=(12, 5))
x = np.arange(-10, 10, 0.1)

axs[0].plot(x, my_leaky_relu(x))
axs[0].set_title("Leaky ReLU(x)")
axs[1].plot(x, leaky_relu_prime(x))
axs[1].set_title("Leaky ReLU'(x)")

axs[0].axhline(c='gray', ls='--')
axs[0].axvline(c='gray', ls='--')
axs[1].axhline(c='gray', ls='--')
axs[1].axvline(c='gray', ls='--')
<matplotlib.lines.Line2D at 0x177d371c0>
png

2.6 Softmax函数

Softmax 将一个任意的实数向量转换为一个概率分布,确保输出值的总和为 1,是二分类激活函数 Sigmoid 在多分类上的推广。常用于多分类问题的输出层,用来表示类别的输出概率。

$$ y_{k}={\frac{e^{x_{k} } } {\sum_{i=1}^{n} e^{x_{i} } } }, \quad k=1 {\sim} n \qquad\qquad \frac{\partial y_{k} } {\partial x_{i} }=\left\{\begin{matrix} {y_{k} ( 1-y_{i} ), k=i} \\ {-y_{k} y_{i}, k \neq i} \\ \end{matrix} \right. $$

Softmax 会放大输入中较大的值,使得最大输入值对应的输出概率较大,其他较小的值会被压缩。

1
2
3
4
5
6
7
8
9
10
11
def my_softmax(x):
return np.exp(x) / np.sum(np.exp(x), axis=1).reshape(-1, 1)

x = np.array([[1, 2, 3, 4, 5], [1, 6, 1, 2, 3]])

_, axs = plt.subplots(1, 2, figsize=(10, 8))
axs[0].pie(my_softmax(x)[0], labels=x[0])
axs[1].pie(my_softmax(x)[1], labels=x[0])

# 每个分类的概率
print('\t1\t2\t3\t4\t5\n',np.char.add(np.round(my_softmax(x) * 100, 2).astype(str), '%'))
    1   2   3   4   5
 [['1.17%' '3.17%' '8.61%' '23.41%' '63.64%']
 ['0.62%' '92.46%' '0.62%' '1.69%' '4.6%']]
png

2.7 其他激活函数

2.7.1 Identity恒等函数

Identity 函数非常简单,它返回输入值本身,适用于回归任务的输出。

f(x) = x,  f(x) = 1

1
2
3
4
5
6
x = np.array([-10, 0, 10])
plt.plot(x, x)

plt.axhline(c='gray', ls='--')
plt.axvline(c='gray', ls='--')
plt.title('Identity')
Text(0.5, 1.0, 'Identity')
png

2.7.2 PReLU函数

PReLU(Parametric Rectified Linear Unit)中的 α 是一个可训练的参数,而不是固定常数。

$$ f ( x )=\left\{ {\begin{array} {l} {\alpha x, x \leq0} \\ {x, x > 0} \end{array} },\quad \right. f^{\prime} ( x )=\left\{ {\begin{array} {l} {\alpha, x \leq0} \\ {1, x > 0} \end{array} } \right. $$

2.7.3 RReLU函数

RReLU(Randomized Leaky ReLU)中的 α 是从均匀分布中随机选择的一个值,范围是 [0, 1]。

$$ f ( x )=\left\{ {\begin{array} {l} {\alpha x, x \leq0} \\ {x, x > 0} \end{array} },\quad \right. f^{\prime} ( x )=\left\{ {\begin{array} {l} {\alpha, x \leq0} \\ {1, x > 0} \end{array} } \right. $$

1
2
3
4
5
6
7
8
9
10
def rrelu(x, alpha=0.01):
return np.where(x > 0, x, alpha * x)

x = np.arange(-10, 10, 0.1)
plt.plot(x, rrelu(x))
plt.fill_between(x, rrelu(x, alpha=0.1), rrelu(x, alpha=0.5), where=(x < 0), color='navy', alpha=0.5)

plt.axhline(c='gray', ls='--')
plt.axvline(c='gray', ls='--')
plt.title('RReLU')
Text(0.5, 1.0, 'RReLU')
png

2.7.4 ELU函数

ELU(Exponential Linear Unit)是在负轴上更加平滑的ReLU函数。

$$ f ( x )=\left\{\begin{array} {c} { {\alpha( e^{x}-1 ), x \leq0} } \\ { {x, x > 0} } \end{array} \right.,\quad \ f^{\prime} ( x )=\left\{\begin{array} {c} { {\alpha e^{x}, x \leq0} } \\ { {1, x > 0} } \end{array} \right. $$

1
2
3
4
5
6
7
8
9
def elu(x, alpha=0.01):
return np.where(x > 0, x, alpha * (np.exp(x) - 1))

x = np.arange(-10, 10, 0.1)
plt.plot(x, elu(x, alpha=1))

plt.axhline(c='gray', ls='--')
plt.axvline(c='gray', ls='--')
plt.title('ELU')
Text(0.5, 1.0, 'ELU')
png

2.7.5 Swish函数

Swish,也称Sigmoid Linear Unit,SiLU函数。

$$ f ( x )=\frac{x} {1+e^{-x} } ,\quad f^{\prime( x )}=\frac{1+e^{-x}+x e^{-x} } {( 1+e^{-x} )^{2} } $$

1
2
3
4
5
6
7
8
9
def swish(x):
return x * my_sigmoid(x)

x = np.arange(-10, 10, 0.1)
plt.plot(x, swish(x))

plt.axhline(c='gray', ls='--')
plt.axvline(c='gray', ls='--')
plt.title('Swish')
Text(0.5, 1.0, 'Swish')
png

2.7.6 Softplus函数

$$ f ( x )=\ln( 1+e^{x} ) ,\quad f^{\prime( x )}=\frac{1} {1+e^{-x} } $$

1
2
3
4
5
6
7
8
9
def softplus(x):
return np.log(1 + np.exp(x))

x = np.arange(-10, 10, 0.1)
plt.plot(x, softplus(x))

plt.axhline(c='gray', ls='--')
plt.axvline(c='gray', ls='--')
plt.title('Softplus')
Text(0.5, 1.0, 'Softplus')
png

2.8 激活函数的选择

  • 隐藏层
    • 首选 ReLU 函数,如果效果不好可尝试 Leaky ReLU 等函数
    • Tanh 的输出均值为 0,对中心化数据更友好,但可能出现梯度消失,仅适用于浅层网络
    • Sigmoid 在隐藏层容易导致梯度消失,仅适用于浅层网络
  • 输出层
    • 二分类选择 Sigmoid
    • 多分类选择 Softmax
    • 回归选择 Identity

2.9 API 实现

  • Sigmoid
1
2
3
4
5
6
7
8
9
10
11
12
import torch
import torch.nn as nn

torch.manual_seed(66)
x = torch.randn(2, 3)

# 函数式写法,方便直接计算
z = torch.sigmoid(x)

# 模块写法,需要先创建对象,方便管理模型
sigmoid = nn.Sigmoid()
z = sigmoid(x)
  • Tanh
1
2
tanh = nn.Tanh()
tanh(x)
tensor([[ 0.9497, -0.2163,  0.3297],
        [-0.9473,  0.1161,  0.8130]])
  • ReLU
1
2
relu = nn.ReLU()
relu(x)
tensor([[1.8289, 0.0000, 0.3424],
        [0.0000, 0.1166, 1.1357]])
  • Leaky ReLU
1
2
leaky_relu = nn.LeakyReLU(negative_slope=0.1)
leaky_relu(x)
tensor([[ 1.8289, -0.0220,  0.3424],
        [-0.1805,  0.1166,  1.1357]])
  • Softmax
1
2
softmax = nn.Softmax(dim=-1)  # dim 为 -1 ,表示最后一维进行归一化,这里也就是列
softmax(x)
tensor([[0.7380, 0.0951, 0.1669],
        [0.0374, 0.2553, 0.7073]])

其他激活函数在 nn 中也有对应模块,不再一一演示。

3 神经网络简单实现

深度神经网络由多个层(layer)组成,通常将其称之为模型(Model)。整个模型接收原始输入(特征),生成输出(预测),并包含一些参数。下图是一个三层神经网络,权重和神经元的上标 (1) 表示网络层号,下标 21 分别代表输入神经元编号和输出神经元编号。

输入层->第1层,使用矩阵乘法表示:

A(1) = XW(1) + B(1)

其中

$$ A^{( 1 )}=( a_{1}^{( 1 )} \; a_{2}^{( 1 )} \; a_{3}^{( 1 )}), \quad X=( x_{1} \, \, x_{2} ), \quad B^{( 1 )}=( b_{1}^{( 1 )} \, \, \, b_{2}^{( 1 )} \, \, \, b_{3}^{( 1 )}), \quad W^{( 1 )}=\left( \begin{matrix} { {w_{1 1}^{( 1 )} } } & { {w_{1 2}^{( 1 )} } } & { {w_{1 3}^{( 1 )} } } \\ { {w_{2 1}^{( 1 )} } } & { {w_{2 2}^{( 1 )} } } & { {w_{2 3}^{( 1 )} } } \end{matrix} \right) $$

计算得到的 A(1) 再经过激活函数 h() 得到 Z(1),继续作为第1层->第2层的输入,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import numpy as np


# 初始化权重和偏置
def init_network():
network = {}
network['W1'] = np.random.randn(2, 3)
network['b1'] = np.random.randn(3)
network['W2'] = np.random.randn(3, 2)
network['b2'] = np.random.randn(2)
network['W3'] = np.random.randn(2, 2)
network['b3'] = np.random.randn(2)

return network


# 前向传播
def forward(network, x):
w1, w2, w3 = network['W1'], network['W2'], network['W3']
b1, b2, b3 = network['b1'], network['b2'], network['b3']

a1 = x @ w1 + b1 # 第 1 层加权求和值
z1 = my_tanh(a1) # 第 1 层经过激活函数
a2 = z1 @ w2 + b2 # 第 2 层加权求和值
z2 = my_relu(a2) # 第 2 层经过激活函数
a3 = z2 @ w3 + b3 # 第 3 层加权求和值
y = my_sigmoid(a3) # 第 3 层经过激活函数输出

return y


net = init_network()
X = np.array([4, 2])
y = forward(net, X)
print(y)
[0.88363004 0.34900234]

4 参数初始化

神经网络中的参数是需要初始化,参数初始化的作用:

  • 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
  • 提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
  • 保持对称性破除:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。(对称性指权重的值相同,神经元的功能没有差异)

4.1 常数初始化

无法打破对称性,反向传播时全部都会进行相同的更新,使得网络失去了不同神经元的意义,无法有效训练。通常只会使用全 0 初始化偏置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch.nn as nn

linear = nn.Linear(5, 2)

# 所有参数初始化为 0
nn.init.zeros_(linear.weight)
print(linear.weight)

# 所有参数初始化为 1
nn.init.ones_(linear.weight)
print(linear.weight)

# 所有参数初始化为固定常数
nn.init.constant_(linear.weight, 66)
print(linear.weight)
Parameter containing:
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]], requires_grad=True)
Parameter containing:
tensor([[1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.]], requires_grad=True)
Parameter containing:
tensor([[66., 66., 66., 66., 66.],
        [66., 66., 66., 66., 66.]], requires_grad=True)

4.2 秩初始化

权重参数初始化为单位矩阵。

1
2
nn.init.eye_(linear.weight)
print(linear.weight)
Parameter containing:
tensor([[1., 0., 0., 0., 0.],
        [0., 1., 0., 0., 0.]], requires_grad=True)

4.3 随机初始化

均匀分布初始化:从区间中均匀随机取值,默认区间为(0,1),可以设为 $(-,) $,其中 d 为神经元的输入数量。

1
2
nn.init.uniform_(linear.weight)
print(linear.weight)
Parameter containing:
tensor([[0.4520, 0.7268, 0.0245, 0.7780, 0.5695],
        [0.9082, 0.2932, 0.1449, 0.6995, 0.1598]], requires_grad=True)

正态分布初始化:从均值为 0,标准差为 1 的高斯分布中取样,使用一些很小的值进行初始化。

1
2
nn.init.normal_(linear.weight)
print(linear.weight)
Parameter containing:
tensor([[ 1.0646,  0.6571, -1.7206, -0.5381, -1.1052],
        [-0.4689,  1.1999, -1.7586,  0.9557, -0.8140]], requires_grad=True)

4.4 Xavier初始化

Xavier 初始化,也叫 Glorot 初始化,设计思想是让每层的输入方差和输出方差一致,保持信号在每一层传播时不衰减、不放大。常用于 Sigmoid 和 Tanh 激活函数,不推荐用于 ReLU 激活函数。

  • Xavier 均匀分布初始化:$\left[-{\sqrt{\frac{6} {n_{i n}+n_{o u t} } } }, {\sqrt{\frac{6} {n_{i n}+n_{o u t} } } } \right]$

  • Xavier 正态分布初始化:均值为 0,均值为 ${\sqrt{\frac{2} {n_{i n}+n_{o u t} } } }$

其中 nin 表示输入神经元的数量,也就是上一层神经元的数量,nout 是输出神经元的数量,也就是下一层神经元的数量。

1
2
3
4
5
6
7
# Xavier 均匀分布初始化
nn.init.xavier_uniform_(linear.weight)
print(linear.weight)

# Xavier 正态分布初始化
nn.init.xavier_normal_(linear.weight)
print(linear.weight)
Parameter containing:
tensor([[ 0.2802,  0.8988, -0.1018,  0.4321, -0.4568],
        [-0.2845,  0.3657, -0.7230, -0.2094,  0.9017]], requires_grad=True)
Parameter containing:
tensor([[ 0.2160, -0.9007, -0.3396, -0.4396,  0.2919],
        [-0.0365, -0.2839, -0.6361, -0.3293,  0.3296]], requires_grad=True)

4.5 Kaiming初始化

Kaiming 初始化,也叫 He 初始化,考虑到 ReLU 会把一半的神经元输出设为 0,因此需要更大的方差来维持信号幅度。常用于 ReLu 和 Leaky ReLU,不推荐用于 Sigmoid。

  • Kaiming 均匀分布初始化:$\left[-{\sqrt{\frac{6} {n_{i n} } } }, {\sqrt{\frac{6} {n_{i n} } } } \right]$

  • Kaiming 正态分布初始化:均值为 0,均值为 ${\sqrt{\frac{2} {n_{i n} } } }$

其中 nin 表示输入神经元的数量,也就是上一层神经元的数量。

1
2
3
4
5
6
7
# Kaiming 均匀分布初始化
nn.init.kaiming_uniform_(linear.weight)
print(linear.weight)

# Kaiming 正态分布初始化
nn.init.kaiming_normal_(linear.weight)
print(linear.weight)
Parameter containing:
tensor([[ 0.7357, -0.4194,  0.6922, -0.8139, -1.0905],
        [-0.2551, -0.3574,  0.3759,  0.9959,  0.1350]], requires_grad=True)
Parameter containing:
tensor([[ 0.3897,  0.4593,  1.3781, -0.0915,  0.3848],
        [ 0.3878, -0.3613,  0.7376, -0.3496, -0.1828]], requires_grad=True)

5 搭建神经网络

5.1 自定义网络

在神经网络框架中,由多个层组成的组件称之为模块(Module),在 PyTorch 中 Module 是所有神经网络的基类。定义 Module 时需要继承nn.Module,并主要重写两个方法:

  • __init__:定义网络中的层结构,并视情况进行初始化。
  • forward:根据输入进行前向传播,并返回输出。在调用神经网络模型对象的时候,底层会自动调用该函数。

下面神经网络设计如下:

  1. 输入层:输入层有4个节点,对应输入数据的4个特征
  2. 第1隐藏层:有3个节点,使用 Xavier 正态分布初始化权重,激活函数使用 Tanh
  3. 第2隐藏层:有2个节点,使用 Kaiming 正态分布初始化权重,激活函数使用 ReLU
  4. 输出层:有 3 个节点,按默认方式(PyTorch默认均匀随机分布)进行初始化,激活函数使用 Softmax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import torch 
import torch.nn as nn

class Model(nn.Module):
def __init__(self):
super().__init__() # 调用父类初始化

self.fc1 = nn.Linear(4, 3) # 第1层初始化,4个输入,3个输出
self.fc2 = nn.Linear(3, 2) # 第2层初始化,3个输入,2个输出
self.out = nn.Linear(2, 3) # 输出层,2个输入,3个输出

# 激活函数
self.relu = nn.ReLU()
self.tanh = nn.Tanh()

# 初始化权重
nn.init.xavier_normal_(self.fc1.weight)
nn.init.kaiming_normal_(self.fc2.weight)

def forward(self, x):
x = self.fc1(x) # 经过第 1 个隐藏层
x = self.tanh(x) # 第 1 层激活函数
x = self.fc2(x) # 经过第 2 个隐藏层
x = torch.relu(x) # 第 2 层激活函数
x = self.out(x) # 经过输出层
# softmax 的 dim 指定归一化的维度,此处可以使用 1 表示对每一行做归一化,也可以使用 -1 更加通用地表示最后一维
x = torch.softmax(x, dim=-1) # 激活函数

return x

model = Model()
X = torch.randn(6, 4)
y_pred = model(X)
print('预测值:\n', y_pred)
print('预测标签:\n', torch.argmax(y_pred, dim=1).detach().numpy())
预测值:
 tensor([[0.2632, 0.3978, 0.3391],
        [0.2272, 0.3991, 0.3737],
        [0.2632, 0.3978, 0.3391],
        [0.2632, 0.3978, 0.3391],
        [0.2632, 0.3978, 0.3391],
        [0.2342, 0.3991, 0.3667]], grad_fn=<SoftmaxBackward0>)
预测标签:
 [1 1 1 1 1 1]

5.2 查看模型参数

使用 named_parameters() 查看各层参数,通常迭代和访问所有可训练参数(requires_grad=True)供梯度更新使用。

1
2
for name, param in model.named_parameters():
print(name, param, '\n')
fc1.weight Parameter containing:
tensor([[-1.0289, -0.0880,  0.8888, -0.6779],
        [-0.3527, -0.1030, -0.1152, -0.0459],
        [-0.2099, -0.1153,  0.2160, -0.9007]], requires_grad=True) 

fc1.bias Parameter containing:
tensor([-0.0480,  0.2268, -0.4755], requires_grad=True) 

fc2.weight Parameter containing:
tensor([[-0.5187, -0.6714,  0.4459],
        [-0.0557, -0.4336, -0.9717]], requires_grad=True) 

fc2.bias Parameter containing:
tensor([-0.3928, -0.5646], requires_grad=True) 

out.weight Parameter containing:
tensor([[-0.1984, -0.6112],
        [-0.5136, -0.2566],
        [-0.0660, -0.0348]], requires_grad=True) 

out.bias Parameter containing:
tensor([0.0401, 0.4532, 0.2935], requires_grad=True) 

使用 state_dict 查看各层参数,返回一个字典,常用于存储和加载模型的所有状态,保存检查点和部署。

1
model.state_dict()
OrderedDict([('fc1.weight',
              tensor([[-1.0289, -0.0880,  0.8888, -0.6779],
                      [-0.3527, -0.1030, -0.1152, -0.0459],
                      [-0.2099, -0.1153,  0.2160, -0.9007]])),
             ('fc1.bias', tensor([-0.0480,  0.2268, -0.4755])),
             ('fc2.weight',
              tensor([[-0.5187, -0.6714,  0.4459],
                      [-0.0557, -0.4336, -0.9717]])),
             ('fc2.bias', tensor([-0.3928, -0.5646])),
             ('out.weight',
              tensor([[-0.1984, -0.6112],
                      [-0.5136, -0.2566],
                      [-0.0660, -0.0348]])),
             ('out.bias', tensor([0.0401, 0.4532, 0.2935]))])
1
2
3
4
5
# 保存模型参数
torch.save(model.state_dict(), 'model/model.pth')

# 加载模型参数
model.load_state_dict(torch.load('model/model.pth'))
<All keys matched successfully>

5.3 模型参数可视化

summary 是一个模型结构可视化工具,它可以清晰地模型每一层的:层名称(Layer Type)、输出张量形状(Output Shape)、参数数量(Params)、以及总参数量统计。需要使用 pip install torchsummary 安装。

summary 会自动构造一个虚拟输入张量,传入模型做一次 forward,然后记录每一层的输入/输出形状、参数量、占用内存等。

1
2
3
4
5
6
7
8
from torchsummary import summary

summary(
model = model, # 模型
input_size=(4, ), # 表示一个输入样本的维度
batch_size=16, # 批次大小
device='cpu' # 默认使用 GPU,PyTorch 不允许CPU和GPU混合运算,或者可以将模型和数据 to('cuda')
)
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Linear-1                    [16, 3]              15
              Tanh-2                    [16, 3]               0
            Linear-3                    [16, 2]               8
            Linear-4                    [16, 3]               9
================================================================
Total params: 32
Trainable params: 32
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
----------------------------------------------------------------

另外也可以使用torchinfo.summary(),只需要modelinput_size作为参数传入即可,更加方便。使用pip install torchinfo安装。

1
2
3
4
5
6
7
8
9
10
11
from torchinfo import summary

inputs = torch.randn(16, 4)

summary(
model=model,
input_size=(16, 4), # 输入数据的大小
# input_data=inputs, # 也可以使用输入数据
# batch_dim=0, # 指定批次的维度索引
verbose=2, # 显示详细信息
)
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
Model                                    [16, 3]                   --
├─Linear: 1-1                            [16, 3]                   15
│    └─weight                                                      ├─12
│    └─bias                                                        └─3
├─Tanh: 1-2                              [16, 3]                   --
├─Linear: 1-3                            [16, 2]                   8
│    └─weight                                                      ├─6
│    └─bias                                                        └─2
├─Linear: 1-4                            [16, 3]                   9
│    └─weight                                                      ├─6
│    └─bias                                                        └─3
==========================================================================================
Total params: 32
Trainable params: 32
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
==========================================================================================





==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
Model                                    [16, 3]                   --
├─Linear: 1-1                            [16, 3]                   15
│    └─weight                                                      ├─12
│    └─bias                                                        └─3
├─Tanh: 1-2                              [16, 3]                   --
├─Linear: 1-3                            [16, 2]                   8
│    └─weight                                                      ├─6
│    └─bias                                                        └─2
├─Linear: 1-4                            [16, 3]                   9
│    └─weight                                                      ├─6
│    └─bias                                                        └─3
==========================================================================================
Total params: 32
Trainable params: 32
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.00
Params size (MB): 0.00
Estimated Total Size (MB): 0.00
==========================================================================================

5.4 顺序网络

使用 nn.Sequential 构建的网络,各层按照添加的顺序逐层连接,其中每一层的输出直接作为下一层的输入。构建非常简单,但限制了网络的复杂性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
model = nn.Sequential(
nn.Linear(3, 4),
nn.Tanh(),
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 3),
nn.Softmax(dim=-1)
)

# 如果需要初始化参数,请使用以下代码
def init_params(m):
if type(m) == nn.Linear:
nn.init.kaiming_normal_(m.weight)
nn.init.zeros_(m.bias)

# apply 会遍历所有子模块依次调用此函数
model.apply(init_params)

model(torch.rand(2, 3))
tensor([[0.5799, 0.2505, 0.1696],
        [0.4363, 0.3862, 0.1775]], grad_fn=<SoftmaxBackward0>)

6 神经网络的本质

在神经网络中,每一层都可以看作是对输入特征的抽象与变换

假设有 10 个样本,每个样本有 5 个特征,即输入矩阵的形状是 (10, 5)。

  1. 第一隐藏层有 10 个神经元,对应权重矩阵的形状是 (5, 10)。

输入与权重相乘后得到输出矩阵 (10, 10),也就是模型将原来的 5 维特征抽象成了新的 10 维特征表示。

  1. 第二隐藏层有 4 个神经元,权重矩阵为 (10, 4)。

上一层输出 (10, 10) 乘以 (10, 4),得到新的表示 (10, 4),相当于模型进一步提炼出了 4 维抽象特征。

  1. 输出层有 3 个神经元,对应权重矩阵 (4, 3)。

(10, 4) @ (4, 3) = (10, 3),表示模型为每个样本生成了 3 个输出值,对应 3 个类别的未归一化得分(logits)。

  1. 最后通过 softmax 激活函数,得到 10 行 10 个样本,每一行的 3 个得分转化为概率分布,即每个样本属于 3 个类别的概率,形状仍是 (10, 3)。

可以看出,全连接层中的矩阵乘法会消掉连接的维度,保留输入层的批次维度或样本大小,以及输出层的神经元节点数,最终的输出形状为:

最终输出形状 = 批次大小 or 样本大小 × 输出节点数


02_神经网络基础
https://zhubaoduo.com/2024/08/05/大模型开发/05 深度学习/02_神经网络基础/
作者
baoduozhu
发布于
2024年8月5日
许可协议