深度学习基本原理和网络搭建
摘要:本文系统介绍了神经网络的基本原理和实现方法。首先阐述了人工神经网络的定义及其对生物神经网络的模拟机制,重点讲解了全连接网络的三层结构(输入层、隐藏层、输出层)。其次详细分析了常用激活函数(Sigmoid、Tanh、ReLU、Softmax)的特性、数学表达式及适用场景。接着探讨了多种参数初始化方法(均匀分布、正态分布、Kaiming、Xavier等)及其实现代码。最后通过PyTorch示例展
一. 神经网络概念
1 神经网络定义
人工神经网络(Artificial Neural Network,简称ANN或NN)是一种模拟生物神经网络结构与功能的计算模型。人脑本质上就是一个复杂的生物神经网络系统,由大量相互连接的神经元构成。每个神经元通过树突接收输入信号,经过内部处理后,再通过轴突输出电信号。

当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。
2 构建神经网络
那怎么构建人工神经网络中的神经元呢?

这一过程类似于将来自不同树突(每个树突具有不同权重)的信息进行加权计算,输入到细胞中进行总和运算,最后通过激活函数输出细胞值。
接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:
• 输入层: 即输入 x 的那一层
• 输出层: 即输出 y 的那一层
• 隐藏层: 输入层和输出层之间都是隐藏层
主要特点:
• 同一层的神经元之间没有连接。
• 第 N 层的每个神经元和第 N-1层 的所有神经元相连(这就是full connected的含义),这就是全连接神经网络。
• 第N-1层神经元的输出就是第N层神经元的输入。
• 每个连接都有一个权重值(w系数和b系数)。
二. 激活函数
激活函数通过对每层输出数据进行非线性变换,为神经网络引入关键的非线性特性。这种机制使网络能够拟合复杂的曲线关系。
• 没有引入非线性因素的网络等价于使用一个线性模型来拟合
• 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力.
如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:

1 sigmoid激活函数
激活函数公式
激活函数求导公式
激活函数图像如下:

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 的公式如下:
激活函数求导公式:
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 公式如下:
激活函数求导公式:
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. 缺点
• 黑箱,很难解释模型是怎么工作的
• 训练时间长,需要大量的计算资源
• 网络结构复杂,需要调整超参数
• 部分数据集上表现不佳,容易发生过拟合
更多推荐

所有评论(0)