一. 神经网络概念

1 神经网络定义

人工神经网络(Artificial Neural Network,简称ANN或NN)是一种模拟生物神经网络结构与功能的计算模型。人脑本质上就是一个复杂的生物神经网络系统,由大量相互连接的神经元构成。每个神经元通过树突接收输入信号,经过内部处理后,再通过轴突输出电信号。

当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。

2 构建神经网络

那怎么构建人工神经网络中的神经元呢?

这一过程类似于将来自不同树突(每个树突具有不同权重)的信息进行加权计算,输入到细胞中进行总和运算,最后通过激活函数输出细胞值。

接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:

• 输入层: 即输入 x 的那一层
• 输出层: 即输出 y 的那一层
• 隐藏层: 输入层和输出层之间都是隐藏层

主要特点:

• 同一层的神经元之间没有连接。
第 N 层的每个神经元和第 N-1层 的所有神经元相连(这就是full connected的含义),这就是全连接神经网络。
• 第N-1层神经元的输出就是第N层神经元的输入。
• 每个连接都有一个权重值(w系数和b系数)。

二. 激活函数

激活函数通过对每层输出数据进行非线性变换,为神经网络引入关键的非线性特性。这种机制使网络能够拟合复杂的曲线关系。

• 没有引入非线性因素的网络等价于使用一个线性模型来拟合
• 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力.

如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:

1 sigmoid激活函数

激活函数公式

                                        ​​​​​​​        ​​​​​​​        f(x) = \tfrac{1}{1 + e^{x}}

激活函数求导公式

                                                                ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​          f'(x) = (\frac{1}{1+e^{-x}})' = \frac{1}{1+e^{-x}}(1- \frac{1}{1+e^{-x}}) = f(x)(1-f(x))

激活函数图像如下:

sigmoid 函数能将任意输入值映射到 (0,1) 区间。但当输入值小于 -6 或大于 6 时,输出激活值会趋于稳定,导致信息损失。例如:输入 100 和 10000 经过 sigmoid 处理后,输出结果都接近 1,这使得两者之间 100 倍的差异信息完全丢失。

• 对于 sigmoid 函数而言,输入值在 [-6, 6] 之间输出值才会有明显差异,输入值在 [-3, 3] 之间才会有比较好的效果。
• 通过上述导数图像,我们发现导数数值范围是 (0, 0.25),当输入 <-6 或者 >6 时,sigmoid 激活函数图像的导数接近为 0,此时网络参数将更新极其缓慢,或者无法更新。
• 一般来说, sigmoid 网络在 5 层之内就会产生梯度消失现象。而且,该激活函数并不是以 0 为中心的,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层。

import torch
import matplotlib.pyplot as plt

# 创建画布和坐标轴
_, axes = plt.subplots(1, 2)
# sigmoid函数图像
x = torch.linspace(-20, 20, 1000)
# 输入值x通过sigmoid函数转换成激活值y
y = torch.sigmoid(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Sigmoid 函数图像')

# sigmoid导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.sigmoid(x).sum().backward()
# x.detach():输入值x的数值
# x.grad:计算梯度,求导
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Sigmoid 导数图像')
plt.show()

2 tanh激活函数

Tanh 的公式如下:

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

激活函数求导公式:

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

Tanh 的函数图像、导数图像如下:

Tanh 函数将输入值映射到 (-1, 1) 区间,其图像关于原点对称。当输入绝对值超过 3 时,输出值趋近于 ±1。该函数的导数范围在 (0, 1) 之间,且在输入绝对值大于 3 时,导数值近似为 0。

• 与 Sigmoid 相比,它是以 0 为中心的,且梯度相对于sigmoid大,使得其收敛速度要比 Sigmoid 快,减少迭代次数。然而,从图中可以看出,Tanh 两侧的导数也为 0,同样会造成梯度消失。
• 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数。

import torch
import matplotlib.pyplot as plt

# 创建画布和坐标轴
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.tanh(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')

# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.tanh(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')
plt.show()

3 ReLU激活函数

ReLU 公式如下:

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        f(x)= max(0, x)

激活函数求导公式:

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        f'(x) = 0 or 1

ReLU 的函数图像如下:

ReLU 激活函数将负值输出置零,同时保留正值不变。这种设计突出正信号的作用,有效简化计算过程,显著提升模型训练效率。

• 当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”
• ReLU是目前最常用的激活函数。与sigmoid相比,RELU的优势是:
• 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。
• sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
• Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。

# 创建画布和坐标轴
import torch
from matplotlib import pyplot as plt

_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.relu(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('ReLU 函数图像')
# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.relu(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('ReLU 导数图像')
plt.show()

4 SoftMax激活函数

softmax用于多分类过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。计算方法如下图所示:

Softmax 函数将网络输出的 logits 值转换为 (0,1) 区间内的概率分布,这些概率的总和为 1。我们通常选择概率最大的类别作为最终的预测结果。

import torch

scores = torch.tensor([0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75])
# dim = 0,按行计算
probabilities = torch.softmax(scores, dim=0)
print(probabilities)

5 其他激活函数

6 激活函数的选择方法

对于隐藏层:

• 优先选择ReLU激活函数
• 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
• 如果你使用了ReLU, 需要注意一下Dead ReLU问题, 避免出现大的梯度从而导致过多的神经元死亡。
• 少用使用sigmoid激活函数,可以尝试使用tanh激活函数

对于输出层:

• 二分类问题选择sigmoid激活函数
• 多分类问题选择softmax激活函数
• 回归问题选择identity激活函数

三、参数初始化方法

1 均匀分布初始化

权重参数初始化从区间均匀随机取值。即在(-1/√d,1/√d)均匀分布中生成当前神经元的权重,其中d为每个神经元的输入数量

2 正态分布初始化

随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化

3 全0初始化

​ 将神经网络中的所有权重参数初始化为 0

4 全1初始化

将神经网络中的所有权重参数初始化为 1.

5 固定值初始化

将神经网络中的所有权重参数初始化为某个固定值.

6 Kaiming 初始化

也叫做 HE 初始化,HE 初始化分为正态分布的 HE 初始化、均匀分布的 HE 初始化.

• 正态化的he初始化
• stddev = sqrt(2 / fan_in)
• 均匀分布的he初始化
• 它从 [-limit,limit] 中的均匀分布中抽取样本, limit是 sqrt(6 / fan_in)
• fan_in 输入神经元的个数

7 Xavier 初始化

也叫做 Glorot初始化,该方法也有两种,一种是正态分布的 xavier 初始化、一种是均匀分布的 xavier 初始化.

• 正态化的Xavier初始化
• stddev = sqrt(2 / (fan_in + fan_out))
• 均匀分布的Xavier初始化
• [-limit,limit] 中的均匀分布中抽取样本, limit 是 sqrt(6 / (fan_in + fan_out))
• fan_in 是输入神经元的个数, fan_out 是输出的神经元个数

代码:

import torch
import torch.nn.functional as F
import torch.nn as nn

# 1. 均匀分布随机初始化
def test01():
    linear = nn.Linear(5, 3)
    # 从0-1均匀分布产生参数
    nn.init.uniform_(linear.weight)
    print(linear.weight.data)

# 2.固定初始化
def test02():
    linear = nn.Linear(5, 3)
    nn.init.constant_(linear.weight, 5)
    print(linear.weight.data)

# 3. 全0初始化
def test03():
    linear = nn.Linear(5, 3)
    nn.init.zeros_(linear.weight)
    print(linear.weight.data)

# 4. 全1初始化
def test04():
    linear = nn.Linear(5, 3)
    nn.init.ones_(linear.weight)
    print(linear.weight.data)

# 5. 正态分布随机初始化
def test05():
    linear = nn.Linear(5, 3)
    nn.init.normal_(linear.weight, mean=0, std=1)
    print(linear.weight.data)

# 6. kaiming 初始化
def test06():
    # kaiming 正态分布初始化
    linear = nn.Linear(5, 3)
    nn.init.kaiming_normal_(linear.weight)
    print(linear.weight.data)

    # kaiming 均匀分布初始化
    linear = nn.Linear(5, 3)
    nn.init.kaiming_uniform_(linear.weight)
    print(linear.weight.data)

# 7. xavier 初始化
def test07():
    # xavier 正态分布初始化
    linear = nn.Linear(5, 3)
    nn.init.xavier_normal_(linear.weight)
    print(linear.weight.data)

    # xavier 均匀分布初始化
    linear = nn.Linear(5, 3)
    nn.init.xavier_uniform_(linear.weight)
    print(linear.weight.data)

一般我们在使用 PyTorch 构建网络模型时,每个网络层的参数都有默认的初始化方法,优先选择kaming的初始化,xavier初始化方式。

四. 神经网络搭建及参数计算

在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:

• __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
• forward方法,在实例化模型的时候,底层会自动调用该函数。该函数中可以定义学习率,为初始化定义的layer传入数据等。

编码设计如下

1. 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
2. 第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu
3. out输出层线性层 假若二分类,采用softmax做数据归一化

import torch
import torch.nn as nn
from torchsummary import summary  # 计算模型参数,查看模型结构, pip install torchsummary

# 创建神经网络模型类
class Model(nn.Module):
    # 初始化属性值
    def __init__(self):
        super(Model, self).__init__() # 调用父类的初始化属性值
        self.linear1 = nn.Linear(3, 3) # 创建第一个隐藏层模型, 3个输入特征,3个输出特征
        nn.init.xavier_normal_(self.linear1.weight) # 初始化权
        # 创建第二个隐藏层模型, 3个输入特征(上一层的输出特征),2个输出特征
        self.linear2 = nn.Linear(3, 2)
        # 初始化权重
        nn.init.kaiming_normal_(self.linear2.weight)
        # 创建输出层模型
        self.out = nn.Linear(2, 2)

    # 创建前向传播方法,自动执行forward()方法
    def forward(self, x):
        # 数据经过第一个线性层
        x = self.linear1(x)
        # 使用sigmoid激活函数
        x = torch.sigmoid(x)
        # 数据经过第二个线性层
        x = self.linear2(x)
        # 使用relu激活函数
        x = torch.relu(x)
        # 数据经过输出层
        x = self.out(x)
        # 使用softmax激活函数
        # dim=-1:每一维度行数据相加为1
        x = torch.softmax(x, dim=-1)
        return x

if __name__ == "__main__":
    # 实例化model对象
    my_model = Model()
    # 随机产生数据
    my_data = torch.randn(5, 3)
    print("mydata shape", my_data.shape)
    # 数据经过神经网络模型训练
    output = my_model(my_data)
    print("output shape-->", output.shape)
    # 计算模型参数
    # 计算每层每个神经元的w和b个数总和
    summary(my_model, input_size=(3,), batch_size=5) 
    # 查看模型参数
    print("======查看模型参数w和b======")
    for name, parameter in my_model.named_parameters():
        print(name, parameter)

神经网络的输入数据是为[batch_size, in_features]的张量经过网络处理后获取了[batch_size, out_features]的输出张量。

• 在上述例子中,batchsize=5, infeatures=3,out_features=2, 结果如下所示:

​ mydata.shape---> torch.Size([5, 3])
​ output.shape---> torch.Size([5, 2])

模型参数的计算

1.以第一个隐层为例:该隐层有3个神经元,每个神经元的参数为:4个(w1,w2,w3,b1),所以一共用3x4=12个参数。
2.输入数据和网络权重是两个不同的事儿!对于初学者理解这一点十分重要,要分得清。

总结

1. 神经网络的搭建方法

• 定义继承自nn.Module的模型类
• 在__init__方法中定义网络中的层结构
• 在forward方法中定义数据传输方式

2. 网络参数量的统计方法

• 统计每一层中的权重w和偏置b的数量

五、优缺点

1. 优点

• 精度高,性能优于其他的机器学习算法,甚至在某些领域超过了人类
• 可以近似任意的非线性函数随之计算机硬件的发展,
• 近年来在学界和业界受到了热捧,有大量的框架和库可供调。

2. 缺点

• 黑箱,很难解释模型是怎么工作的
• 训练时间长,需要大量的计算资源
• 网络结构复杂,需要调整超参数
• 部分数据集上表现不佳,容易发生过拟合

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐