深度学习概述
什么是深度学习
人工智能、机器学习和深度学习之间的关系:
机器学习是实现人工智能的一种途径,深度学习是机器学习的子集,区别如下:
传统机器学习算法依赖人工设计特征、提取特征,而深度学习依赖算法自动提取特征。深度学习模仿人类大脑的运行方式,从大量数据中学习特征,这也是深度学习被看做黑盒子、可解释性差的原因。
随着算力的提升,深度学习可以处理图像,文本,音频,视频等各种内容,主要应用领域有:
- 图像处理:分类、目标检测、图像分割(语义分割)
- 自然语言处理:LLM、NLP、Transformer
- 语音识别:对话机器人、智能客服(语音+NLP)
- 自动驾驶:语义分割(行人、车辆、实线等)
- LLM:大Large语言Language模型Model
- 机器人:非常火的行业
有了大模型的加持,AI+各行各业。
深度学习发展历史
可百度了解
深度学习的优势
神经网络
我们要学习的深度学习(Deep Learning)是神经网络的一个子领域,主要关注更深层次的神经网络结构,也就是深层神经网络(Deep Neural Networks,DNNs)。
感知神经网络
神经网络(Neural Networks)是一种模拟人脑神经元网络结构的计算模型,用于处理复杂的模式识别、分类和预测等任务。生物神经元如下图:
生物学:
1.人脑可以看做是一个生物神经网络,由众多的神经元连接而成
- 树突:从其他神经元接收信息的分支
- 细胞核:处理从树突接收到的信息
- 轴突:被神经元用来传递信息的生物电缆
- 突触:轴突和其他神经元树突之间的连接
2.人脑神经元处理信息的过程:
- 多个信号到达树突,然后整合到细胞体的细胞核中
- 当积累的信号超过某个阈值,细胞就会被激活
- 产生一个输出信号,由轴突传递。
神经网络由多个互相连接的节点(即人工神经元)组成。
人工神经元
人工神经元(Artificial Neuron)是神经网络的基本构建单元,模仿了生物神经元的工作原理。其核心功能是接收输入信号,经过加权求和和非线性激活函数处理后,输出结果。
构建人工神经元
人工神经元接受多个输入信息,对它们进行加权求和,再经过激活函数处理,最后将这个结果输出。
组成部分
- 输入(Inputs): 代表输入数据,通常用向量表示,每个输入值对应一个权重。
- 权重(Weights): 每个输入数据都有一个权重,表示该输入对最终结果的重要性。
- 偏置(Bias): 一个额外的可调参数,作用类似于线性方程中的截距,帮助调整模型的输出。
- 加权求和: 神经元将输入乘以对应的权重后求和,再加上偏置。
- 激活函数(Activation Function): 用于将加权求和后的结果转换为输出结果,引入非线性特性,使神经网络能够处理复杂的任务。常见的激活函数有Sigmoid、ReLU(Rectified Linear Unit)、Tanh等。
数学表示
如果有 n 个输入 ,权重分别为 ,偏置为 ,则神经元的输出 表示为:
其中, 是激活函数。
对比生物神经元
人工神经元和生物神经元对比如下表:
生物神经元 | 人工神经元 |
---|---|
细胞核 | 节点 (加权求和 + 激活函数) |
树突 | 输入 |
轴突 | 带权重的连接 |
突触 | 输出 |
深入神经网络
- 神经网络是由大量人工神经元按层次结构连接而成的计算模型。每一层神经元的输出作为下一层的输入,最终得到网络的输出。
基本结构
神经网络有下面三个基础层(Layer)构建而成:
- 输入层(Input): 神经网络的第一层,负责接收外部数据,不进行计算。
- 隐藏层(Hidden): 位于输入层和输出层之间,进行特征提取和转换。隐藏层一般有多层,每一层有多个神经元。
- 输出层(Output): 网络的最后一层,产生最终的预测结果或分类结果
网络构建
我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个权重,经典如下:
注意:同一层的各个神经元之间是没有连接的。
全连接神经网络
全连接(Fully Connected,FC)神经网络是前馈神经网络的一种,每一层的神经元与上一层的所有神经元全连接,常用于图像分类、文本分类等任务。
特点
- 全连接层: 层与层之间的每个神经元都与前一层的所有神经元相连。
- 权重数量: 由于全连接的特点,权重数量较大,容易导致计算量大、模型复杂度高。
- 学习能力: 能够学习输入数据的全局特征,但对于高维数据却不擅长捕捉局部特征(如图像就需要CNN)。
计算步骤
- 数据传递: 输入数据经过每一层的计算,逐层传递到输出层。
- 激活函数: 每一层的输出通过激活函数处理。
- 损失计算: 在输出层计算预测值与真实值之间的差距,即损失函数值。
- 反向传播(Back Propagation): 通过反向传播算法计算损失函数对每个权重的梯度,并更新权重以最小化损失。
数据处理
数据和加载器
- Ecxel表格的数据;
- 图片是数据;
- 文本信息是数据;
- 官方数据集是数据;
- make_regression模拟的也是数据;
参数初始化
- 神经网络的参数初始化是训练深度学习模型的关键步骤之一。初始化参数(通常是权重和偏置)会对模型的训练速度、收敛性以及最终的性能产生重要影响。下面是关于神经网络参数初始化的一些常见方法及其相关知识点。
- 官方文档参考:torch.nn.init — PyTorch 2.5 documentation
固定值初始化
- 固定值初始化是指在神经网络训练开始时,将所有权重或偏置初始化为一个特定的常数值。这种初始化方法虽然简单,但在实际深度学习应用中通常并不推荐。
全零初始化
- 将神经网络中的所有权重参数初始化为0。
- 方法:将所有权重初始化为零。
- 缺点:导致对称性破坏,每个神经元在每一层中都会执行相同的计算,模型无法学习。
- 应用场景:通常不用来初始化权重,但可以用来初始化偏置。
import torch
def test01():
model = torch.nn.Linear(5, 1)
# 全零初始化
# 将权重初始化为0,model.weight被初始化为0的参数
torch.nn.init.zeros_(model.weight)
print(model.weight)
model.weight.data.fill_(1)
print(model.weight)
if __name__ == '__main__':
test01()
"""
Parameter containing:
tensor([[0., 0., 0., 0., 0.]], requires_grad=True)
Parameter containing:
tensor([[1., 1., 1., 1., 1.]], requires_grad=True)
"""
全1初始化
- 全1初始化会导致网络中每个神经元接收到相同的输入信号,进而输出相同的值,这就无法进行学习和收敛。所以全1初始化只是一个理论上的初始化方法,但在实际神经网络的训练中并不适用。
import torch.nn as nn
def test02():
# 全1参数初始化
# 4不是输入的数据有4条,而是输入的数据的权重有4个,每条特征值有4个
model = torch.nn.Linear(4, 1)
torch.nn.init.ones_(model.weight)
print(model.weight)
if __name__ == '__main__':
test02()
"""
Parameter containing:
tensor([[1., 1., 1., 1.]], requires_grad=True)
"""
任意常数初始化
- 将所有参数初始化为某个非零的常数(如 0.1,-1 等)。虽然不同于全0和全1,但这种方法依然不能避免对称性破坏的问题。
import torch.nn as nn
def test03():
# 指定参数初始化
# 4不是输入的数据有4条,而是输入的数据的权重有4个,每条特征值有4个
model = torch.nn.Linear(4, 1)
torch.nn.init.constant_(model.weight, 5)
print(model.weight)
if __name__ == '__main__':
test03()
"""
Parameter containing:
tensor([[5., 5., 5., 5.]], requires_grad=True)
"""
随机初始化
- 方法:将权重初始化为随机的小值,通常从正态分布或均匀分布中采样。
- 应用场景:这是最基本的初始化方法,通过随机初始化避免对称性破坏。
- 随机分布之均匀初始化
import torch.nn as nn
def test04():
# 随机参数初始化
# 4不是输入的数据有4条,而是输入的数据的权重有4个,每条特征值有4个
# 三套权重,每套权重有4个输入值,每条特征值有4个
model = torch.nn.Linear(4, 3)
torch.nn.init.uniform_(model.weight)
print(model.weight)
if __name__ == '__main__':
test04()
"""
Parameter containing:
tensor([[0.3642, 0.9121, 0.5020, 0.5839],
[0.7654, 0.6401, 0.8633, 0.0704],
[0.4772, 0.1756, 0.9870, 0.0416]], requires_grad=True)
"""
- 正态分布初始化
import torch
import torch.nn as nn
# 正态分布初始化
def test05():
model = torch.nn.Linear(6, 8)
torch.nn.init.normal_(model.weight, mean=1, std=2)
print(model.weight)
if __name__ == '__main__':
test05()
"""
Parameter containing:
tensor([[ 2.5076, 5.2577, 3.3463, -0.3659, -2.8118, 1.3285],
[-0.1769, -1.6597, 1.4845, 0.7917, 1.2998, -0.3249],
[-2.7227, 3.4356, 1.6258, 2.7487, 4.9062, 1.2346],
[ 3.6401, -0.6951, 0.9113, 2.2389, 0.7237, 1.1446],
[ 1.1042, 1.3029, 3.6920, 0.5030, -1.0741, 3.0714],
[ 2.4586, -0.9933, 4.2211, 0.5617, -0.2933, -2.3886],
[ 1.1387, -2.0781, 3.4274, 3.7742, 4.0110, 0.9324],
[-0.1291, -1.2657, 2.2761, -2.6135, 1.3359, -0.4781]],
requires_grad=True)
"""
Xavier 初始化
也叫做Glorot初始化。
方法:根据输入和输出神经元的数量来选择权重的初始值。权重从以下分布中采样:
或者
其中 是当前层的输入神经元数量,是输出神经元数量。
优点:平衡了输入和输出的方差,适合 和 激活函数。
应用场景:常用于浅层网络或使用 、 激活函数的网络。
import torch
import torch.nn as nn
# Xavier 初始化
def test06():
model = nn.Linear(6, 2)
torch.nn.init.xavier_uniform_(model.weight)
print(model.weight)
torch.nn.init.xavier_normal_(model.weight)
print(model.weight)
if __name__ == '__main__':
test06()
"""
Parameter containing:
tensor([[ 0.3246, 0.5916, -0.5449, -0.0188, -0.1047, 0.5020],
[-0.4950, -0.6065, -0.6630, 0.4130, 0.0719, 0.3317]],
requires_grad=True)
Parameter containing:
tensor([[-0.2404, 0.4160, 0.7544, 0.7362, 0.6896, 0.1267],
[ 0.3274, -0.3232, -0.1033, 0.1438, -0.6681, -0.2525]],
requires_grad=True)
"""
He初始化
也叫kaiming 初始化。
方法:专门为 ReLU 激活函数设计。权重从以下分布中采样:
其中 是当前层的输入神经元数量。
优点:适用于 和 激活函数。
应用场景:深度网络,尤其是使用 ReLU 激活函数时。
import torch
import torch.nn as nn
# He初始化
def test07():
model = torch.nn.Linear(6, 8)
torch.nn.init.kaiming_uniform_(model.weight)
print(model.weight)
model1 = torch.nn.Linear(6, 8)
torch.nn.init.kaiming_normal_(model1.weight)
print(model1.weight)
if __name__ == '__main__':
test07()
"""
Parameter containing:
tensor([[-0.6672, -0.2011, -0.2701, 0.8939, 0.4433, -0.3782],
[-0.2380, 0.7126, 0.2886, 0.5757, -0.1072, -0.3268],
[-0.2221, 0.0136, 0.0984, 0.6404, 0.2004, -0.2737],
[-0.1816, -0.2019, -0.7821, -0.8135, 0.9801, -0.1041],
[ 0.9661, 0.5574, 0.4388, -0.0619, 0.1639, -0.2632],
[-0.5419, 0.4985, 0.8074, -0.7523, 0.9440, -0.9783],
[-0.8417, 0.6788, 0.3290, 0.5034, 0.7955, -0.7616],
[ 0.7639, 0.4722, 0.8272, 0.7239, 0.7598, -0.7180]],
requires_grad=True)
Parameter containing:
tensor([[ 0.4509, 0.7777, 0.2351, 0.0130, -0.9703, -0.7420],
[-0.1100, -0.4148, -0.0657, 0.1857, -0.5336, -0.4752],
[ 0.5175, -0.6691, 0.8017, 1.4748, -0.2622, -0.7321],
[-0.8864, 0.2444, 0.0927, 0.1975, -0.1230, -0.1230],
[ 0.4144, 0.5364, 0.1676, 0.6524, 0.3700, -0.3855],
[ 0.0819, -0.8229, -0.6674, 0.6379, 0.4000, -0.3821],
[-0.0486, -0.0296, -0.0282, 0.5270, 0.1232, -0.2186],
[ 0.1837, -0.9335, 0.2759, 0.0452, -0.0542, -0.9096]],
requires_grad=True)
"""
总结
- 在使用Torch构建网络模型时,每个网络层的参数都有默认的初始化方法,同时还可以通过以上方法来对网络参数进行初始化。
激活函数
- 激活函数的作用是在隐藏层引入非线性,使得神经网络能够学习和表示复杂的函数关系,使网络具备非线性能力,增强其表达能力。
基础概念
- 通过认识线性和非线性的基础概念,深刻理解激活函数存在的价值。
非线性理解
如果在隐藏层不使用激活函数,那么整个神经网络会表现为一个线性模型。
假设:
- 神经网络有L层,每层的输出为
- 每层的权重矩阵为,偏置向量为。
- 输入数据为,输出为。
一层网络的情况
- 对于单层网络(输入层到输出层),如果没有激活函数,输出 可以表示为:
两层网络的情况
假设我们有两层网络,且每层都没有激活函数,则:
- 第一层的输出:
- 第二层的输出:
将代入到中,可以得到:
我们可以看到,输出是输入的线性变换,因为:其中,。
多层网络的情况
如果有层,每层都没有激活函数,则第层的输出为:
通过递归代入,可以得到:
表达式可简化为:
其中, 是所有权重矩阵的乘积,是所有偏置项的线性组合。
如此可以看得出来,无论网络多少层,意味着:
- 整个网络就是线性模型,无法捕捉数据中的非线性关系。
- 激活函数是引入非线性特性、使神经网络能够处理复杂问题的关键。
非线性可视化
我们可以通过可视化的方式去理解非线性的拟合能力:A Neural Network Playground
常见激活函数
激活函数通过引入非线性来增强神经网络的表达能力,对于解决线性模型的局限性至关重要。由于反向传播算法(BP)用于更新网络参数,因此激活函数必须是可微(可导)。
sigmoid
Sigmoid激活函数是一种常见的非线性激活函数,特别是在早期神经网络中应用广泛。它将输入映射到0到1之间的值,因此非常适合处理概率问题。
公式
Sigmoid函数的数学表达式为:
其中, 是自然常数(约等于2.718), 是输入。
特征
- 将任意实数输入映射到 (0, 1)之间,因此非常适合处理概率场景。
- sigmoid函数一般只用于二分类的输出层。
- 微分性质: 导数计算比较方便,可以用自身表达式来表示:
- σ′(x)=σ(x)⋅(1−σ(x))
缺点
1.梯度消失:
- 在输入非常大或非常小时,Sigmoid函数的梯度会变得非常小,接近于0。这导致在反向传播过程中,梯度逐渐衰减。
- 最终使得早期层的权重更新非常缓慢,进而导致训练速度变慢甚至停滞。
2.信息丢失:输入100和输入10000经过sigmoid的激活值几乎都是等于 1 的,但是输入的数据却相差 100 倍。
3.计算成本高: 由于涉及指数运算,Sigmoid的计算比ReLU等函数更复杂,尽管差异并不显著。
函数绘制
通过代码实现函数和导函数绘制:
import torch
import matplotlib.pyplot as plt
def test01():
x = torch.linspace(-10, 10, 100, requires_grad=True)
y = torch.sigmoid(x)
_, ax = plt.subplots(1,2)
ax[0].plot(x.detach().numpy(), y.detach().numpy())
ax[0].set_title('sigmoid')
# 求导
y.sum().backward()
ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
ax[1].set_title('gradient')
plt.show()
if __name__ == '__main__':
test01()
运行结果:
tanh
- tanh(双曲正切)是一种常见的非线性激活函数,常用于神经网络的隐藏层。tanh 函数也是一种S形曲线,输出范围为(−1,1)。
公式
tanh数学表达式为:
特征
- 输出范围: 将输入映射到(-1, 1)之间,因此输出是零中心的。相比于Sigmoid函数,这种零中心化的输出有助于加速收敛。
- 对称性: Tanh函数关于原点对称,因此在输入为0时,输出也为0。这种对称性有助于在训练神经网络时使数据更平衡。
- 平滑性: Tanh函数在整个输入范围内都是连续且可微的,这使其非常适合于使用梯度下降法进行优化。
缺点
- 梯度消失: 虽然一定程度上改善了梯度消失问题,但在输入值非常大或非常小时导数还是非常小,这在深层网络中仍然是个问题。
- 计算成本: 由于涉及指数运算,Tanh的计算成本还是略高,尽管差异不大。
函数绘制
- 图像
ReLU
- ReLU(Rectified Linear Unit)是深度学习中最常用的激活函数之一,它的全称是修正线性单元。ReLU 激活函数的定义非常简单,但在实践中效果非常好。
公式
ReLU 函数定义如下:
即对输入进行非线性变换:
当时当时∙当 x>0 时,ReLU(x)=x∙当 x≤0 时,ReLU(x)=0
特征
- 计算简单:ReLU 的计算非常简单,只需要对输入进行一次比较运算,这在实际应用中大大加速了神经网络的训练。
- ReLU 函数的导数是分段函数:
- 缓解梯度消失问题:相比于 Sigmoid 和 Tanh 激活函数,ReLU 在正半区的导数恒为 1,这使得深度神经网络在训练过程中可以更好地传播梯度,不存在饱和问题。
- 稀疏激活:ReLU在输入小于等于 0 时输出为 0,这使得 ReLU 可以在神经网络中引入稀疏性(即一些神经元不被激活),这种稀疏性可以提升网络的泛化能力。
缺点
- 神经元死亡:由于在x≤0时输出为0,如果某个神经元输入值是负,那么该神经元将永远不再激活,成为“死亡”神经元。随着训练的进行,网络中可能会出现大量死亡神经元,从而会降低模型的表达能力。
函数绘图
- 图像
LeakyReLU
- Leaky ReLU是一种对 ReLU 函数的改进,旨在解决 ReLU 的一些缺点,特别是Dying ReLU 问题。Leaky ReLU 通过在输入为负时引入一个小的负斜率来改善这一问题。
公式
Leaky ReLU 函数的定义如下:
其中, 是一个非常小的常数(如 0.01),它控制负半轴的斜率。这个常数 是一个超参数(认为规定的数),可以在训练过程中可自行进行调整。
特征
- 避免神经元死亡:通过在 区域引入一个小的负斜率,这样即使输入值小于等于零,Leaky ReLU仍然会有梯度,允许神经元继续更新权重,避免神经元在训练过程中完全“死亡”的问题。
- 计算简单:Leaky ReLU 的计算与 ReLU 相似,只需简单的比较和线性运算,计算开销低。
缺点
- 参数选择: 是一个需要调整的超参数,选择合适的 值可能需要实验和调优。
- 出现负激活:如果 设定得不当,仍然可能导致激活值过低。
函数绘制
- 图像
softmax
- Softmax激活函数通常用于分类问题的输出层,它能够将网络的输出转换为概率分布,使得输出的各个类别的概率之和为 1。Softmax 特别适合用于多分类问题。
公式
- 假设神经网络的输出层有n个节点,每个节点的输出为,则 Softmax 函数的定义如下:
特征
- 将输出转化为概率:通过,可以将网络的原始输出转化为各个类别的概率,从而可以根据这些概率进行分类决策。
- 概率分布:的输出是一个概率分布,即每个输出值都是一个介于0和1之间的数,并且所有输出值的和为 1:
- 突出差异:会放大差异,使得概率最大的类别的输出值更接近,而其他类别更接近。
- 在实际应用中,常与交叉熵损失函数结合使用,用于多分类问题。在反向传播中,的导数计算是必需的。
缺点
- 数值不稳定性:在计算过程中,如果的数值过大,可能会导致数值溢出。因此在实际应用中,经常会对进行调整,如减去最大值以确保数值稳定。
解释:
- 是一个非正数,那么的值就位于0到1之间,有效避免了数值溢出。
- 这中调整不会改变的概率分布结果,因为从数学的角度讲相当于分子、分母都除以了。
- 难以处理大量类别:在处理类别数非常多的情况下(如大模型中的词汇表)计算开销会较大。
代码实现
- 图像
如何选择
更多激活函数可以查看官方文档:torch.nn — PyTorch 2.5 documentation
隐藏层
- 优先选ReLU;
- 如果ReLU效果不咋地,那么尝试其他激活,如Leaky ReLU等;
- 使用ReLU时注意神经元死亡问题, 避免出现过多神经元死亡;
- 不使用sigmoid,尝试使用tanh;
输出层
- 二分类问题选择sigmoid激活函数;
- 多分类问题选择softmax激活函数;
- 回归问题选择identity激活函数;
损失函数
线性回归损失函数
MAE损失
MAE(Mean Absolute Error,平均绝对误差)通常也被称为 L1-Loss,通过对预测值和真实值之间的绝对差取平均值来衡量他们之间的差异。。
MAE的公式如下:
其中:
-
是样本的总数。
-
是第 个样本的真实值。
-
是第 个样本的预测值。
-
是真实值和预测值之间的绝对误差。
特点:
- 鲁棒性:与均方误差(MSE)相比,MAE对异常值(outliers)更为鲁棒,因为它不会像MSE那样对较大误差平方敏感。
- 物理意义直观:MAE以与原始数据相同的单位度量误差,使其易于解释。
应用场景: MAE通常用于需要对误差进行线性度量的情况,尤其是当数据中可能存在异常值时,MAE可以避免对异常值的过度惩罚。
使用torch.nn.L1Loss
即可计算MAE:
import torch
def test01():
l1_loss_fn = torch.nn.L1Loss()
y_true = torch.tensor([1, 2, 3, 4, 5, 6], dtype=torch.float32)
y_pred = torch.tensor([10, 20, 30, 40, 50, 60], dtype=torch.float32)
loss = l1_loss_fn(y_pred, y_true)
print(loss)
if __name__ == '__main__':
test01()
MSE损失
均方差损失,也叫L2Loss。
MSE(Mean Squared Error,均方误差)通过对预测值和真实值之间的误差平方取平均值,来衡量预测值与真实值之间的差异。
MSE的公式如下:
其中:
- 是样本的总数。
- 是第 个样本的真实值。
- 是第 个样本的预测值。
- 是真实值和预测值之间的误差平方。
特点:
- 平方惩罚:因为误差平方,MSE 对较大误差施加更大惩罚,所以 MSE 对异常值更为敏感。
- 凸性:MSE 是一个凸函数,这意味着它具有一个唯一的全局最小值,有助于优化问题的求解。
应用场景:
- MSE被广泛应用在神经网络中。
使用 torch.nn.MSELoss
可以实现:
import torch
def test01():
l2_loss_fn = torch.nn.MSELoss()
y_true = torch.tensor([1, 2, 3, 4, 5, 6], dtype=torch.float32)
y_pred = torch.tensor([2, 3, 4, 5, 6, 8], dtype=torch.float32)
loss = l2_loss_fn(y_pred, y_true)
print(loss)
if __name__ == '__main__':
test01()
SmoothL1Loss
SmoothL1Loss可以做到在损失较小时表现为 L2 损失,而在损失较大时表现为 L1 损失。
SmoothL1Loss 的公式如下:
其中,x 表示预测值和真实值之间的误差,即。
所有样本的平均损失为:
特点:
- 平滑过渡:当误差较小时,损失函数表现为 L2 Loss(平方惩罚);当误差较大时,损失函数逐渐向 L1 Loss过渡。这种平滑过渡既能对大误差有所控制,又不会对异常值过度敏感。
- 稳健性:对于异常值更加稳健,同时在小误差范围内提供了较好的优化效果。
应用场景:
- SmoothL1Loss常用于需要对大误差进行一定控制但又不希望完全忽略小误差的回归任务。特别适用于目标检测任务中的边界框回归,如 Faster R-CNN 等算法中。
- 示例:
import torch
def test01():
# loss_fn = torch.nn.SmoothL1Loss()
loss_fn = torch.nn.functional.smooth_l1_loss
y_true = torch.tensor([1.0, 2.0, 3.0, 4.0])
y_pred = torch.tensor([3.0, 2.5, 3.5, 4.5])
loss = loss_fn(y_pred, y_true)
print(loss)
if __name__ == '__main__':
test01()
CrossEntropyLoss
交叉熵损失函数,使用在输出层使用softmax激活函数进行多分类时,一般都采用交叉熵损失函数。
对于多分类问题,CrossEntropyLoss 公式如下:
其中:
-
C是类别的总数。
-
y 是真实标签的one-hot编码向量,表示真实类别。
-
是模型的输出(经过 softmax 后的概率分布)。
-
是真实类别的第 i个元素(0 或 1)。
-
是预测的类别概率分布中对应类别 i的概率。
特点:
- Softmax 直白来说就是将网络输出的 logits 通过 softmax 函数,就映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。
import torch
def test01():
l1_loss_fn = torch.nn.CrossEntropyLoss()
# l1_loss_fn = torch.nn.functional.cross_entropy
one_hot = torch.tensor([[1.5, 2.0, 0.5], [0.5, 1.0, 1.5]])
y_pred = torch.tensor([1,2])
loss = l1_loss_fn(one_hot, y_pred)
print(loss)
if __name__ == '__main__':
test01()
BCELoss
- 二分类交叉熵损失函数,使用在输出层使用sigmoid激活函数进行二分类时。
- 对于二分类问题,CrossEntropyLoss 的简化版本称为二元交叉熵(Binary Cross-Entropy Loss),公式为:
- log的底数一般默认为e,y是真实类别目标,根据公式可知L是一个分段函数 :
- 激活值当激活值当L=−log(sigmoid激活值),当y=1L=−log(1−sigmoid激活值),当y=0
- 以上损失函数是一个样本的损失值,总样本的损失值是求损失均值即可。
import torch
def test01():
# 样本
x = torch.tensor([[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[10, 11, 12]])
# 初始化权重
w = torch.tensor([[0.1, 0.2, 0.5],
[0, 0.1, 0],
[0.3, 0.5, 0.3],
[0.2, 0.2, 0.3]])
# 偏置
b = 0.1
# 预测
y = w*x + b
# 激活后的预测结果
y_pred = torch.nn.functional.sigmoid(y)
print(y_pred)
y_true = torch.tensor([[1,0,0], [1,0,0], [0,0,1], [0,0,1]], dtype=torch.float32)
loss_fn = torch.nn.BCELoss()
# 计算损失,主要是通过激活后的预测值作为输入,和y_true做BCELoss计算算式函数
# 使y_true的值为0或者1,如果y_true为1,y_pred越大损失越小,反之亦然
loss = loss_fn(y_pred, y_true)
print(loss)
if __name__ == '__main__':
test01()
"""
tensor([[0.5498, 0.6225, 0.8320],
[0.5250, 0.6457, 0.5250],
[0.9002, 0.9837, 0.9427],
[0.8909, 0.9089, 0.9759]])
tensor(1.4082)
"""
总结
- 当输出层使用softmax多分类时,使用交叉熵损失函数;
- 当输出层使用sigmoid二分类时,使用二分类交叉熵损失函数, 比如在逻辑回归中使用;
- 当功能为线性回归时,使用smooth L1损失函数或均方差损失-L2 loss;
BP算法
1.多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练,值得指出的是 BP 算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说 BP 网络时,一般是指用 BP 算法训练的多层前馈神经网络。
2.误差反向传播算法(BP)的基本步骤:
- 前向传播:正向计算得到预测值。
- 计算损失:通过损失函数 计算预测值和真实值的差距。
- 梯度计算:反向传播的核心是计算损失函数 L 对每个权重和偏置的梯度。
- 更新参数:一旦得到每层梯度,就可以使用梯度下降算法来更新每层的权重和偏置,使得损失逐渐减小。
- 迭代训练:将前向传播、梯度计算、参数更新的步骤重复多次,直到损失函数收敛或达到预定的停止条件。
前向传播
1.前向传播(Forward Propagation)把输入数据经过各层神经元的运算并逐层向前传输,一直到输出层为止。
下面是一个简单的三层神经网络(输入层、隐藏层、输出层)前向传播的基本步骤分析。
2.输入层到隐藏层
- 给定输入 x和权重矩阵 及偏置向量 ,隐藏层的输出(激活值)计算如下:
- 将 通过激活函数 进行激活:
3.隐藏层到输出层
- 隐藏层的输出 通过输出层的权重矩阵 和偏置 生成最终的输出:
- 输出层的激活值 是最终的预测结果:
4.前向传播的主要作用是:
- 计算神经网络的输出结果,用于预测或计算损失。
- 在反向传播中使用,通过计算损失函数相对于每个参数的梯度来优化网络。
反向传播
- 反向传播(Back Propagation,简称BP)通过计算损失函数相对于每个参数的梯度来调整权重,使模型在训练数据上的表现逐渐优化。反向传播结合了链式求导法则和梯度下降算法,是神经网络模型训练过程中更新参数的关键步骤。
原理
- 利用链式求导法则对每一层进行求导,直到求出输入层x的导数,然后利用导数值进行梯度新
链式法则
- 链式求导法则(Chain Rule)是微积分中的一个重要法则,用于求复合函数的导数。在深度学习中,链式法则是反向传播算法的基础,这样就可以通过分层的计算求得损失函数相对于每个参数的梯度。
- 其中 x 是输入数据,w 是权重,b是偏置。
函数分解
- 可以将该复合函数分解为:
函数 | 导数 | 我们假设 w=0, b=0, x=1 |
---|---|---|
- 从外到内一层一层分别求导在相乘
链式求导
- 复合函数 𝑓(𝑥; 𝑤, 𝑏) 关于参数 𝑤 和 𝑏 的导数可以通过 𝑓(𝑥; 𝑤, 𝑏) 与参数 𝑤 和 𝑏 之间所有导数连乘得到,数学过程如下:
- 以w为例,当 𝑥 = 1, 𝑤 = 0, 𝑏 = 0 时,可以得到:
代码实现
import torch
def test01():
# 链式求导
x = torch.tensor(1.)
w = torch.tensor(0., requires_grad=True)
b = torch.tensor(0., requires_grad=True)
fx = 1/(1+ torch.exp(-(w*x+b)))
fx.backward()# fx对w求导,fx对b求导
print(w.grad)
print(b.grad)
if __name__ == '__main__':
test01()
BP算法代码实现
- 手动实现大致思路
import torch
i1 = 0.05
i2 = 0.10
b1 = 0.35
def h1():
w1 = 0.15
w2 = 0.20
linear1 = i1 * w1 + i2 * w2 + b1
h1 = (1+torch.e**(-linear1))**-1
return h1
print("h1神经元的输出结果:",h1())
def h2():
w1 = 0.25
w2 = 0.30
linear2 = i1 * w1 + i2 * w2 + b1
h2 = (1+torch.e**(-linear2))**-1
return h2
print("h2神经元的输出结果:",h2())
b2 = 0.60
def o1():
w1 = 0.40
w2 = 0.45
linear3 = h1() * w1 + h2() * w2 + b2
o1 = (1+torch.e**(-linear3))**-1
return o1
print("o1神经元的输出结果:",o1())
def o2():
w1 = 0.50
w2 = 0.55
linear4 = h1() * w1 + h2() * w2 + b2
o2 = (1+torch.e**(-linear4))**-1
return o2
print("o2神经元的输出结果:",o2())
# 手动反向传播
# 对于o层神经元
# 目标是求mes对o1和o2的w1和w2的导数
# mes对o1_pred求导,o1_pred对l3求导,l3对w1求导
def mes():
o_target = 0.01
o2_target = 0.99
mes = 0.5*((o_target - o1())**2 + (o2_target - o2())**2)
return mes
loss = mes()# loss是一个具体的值
# 对于o层神经元,补充:sigmoid函数求导为:sigmoid(x)*(1-sigmoid(x))
# 目标是求loss对w1的导数:求w1的梯度(主要先分别求偏导)
# loss对于o1求导
# o1对于linear3求导
# linear3对于h1求导
# h1对w1求导
# loss对w2求导同理
- 函数简单实现
import torch
def train():
# 前向传播
i = torch.tensor([[0.05, 0.1]])
model1 = torch.nn.Linear(2, 2)
model1.weight.data = torch.tensor([[0.15, 0.20], [0.25, 0.30]])
model1.bias.data = torch.tensor([0.35, 0.35])
l1_l2 = model1(i)
h1_h2 = torch.sigmoid(l1_l2)
model2 = torch.nn.Linear(2, 2)
model2.weight.data = torch.tensor([[0.40, 0.45], [0.50, 0.55]])
model2.bias.data = torch.tensor([0.60, 0.60])
l3_l4 = model2(h1_h2)
o1_o2 = torch.sigmoid(l3_l4)
# 反向传播
o1_o2_true = torch.tensor([[0.01, 0.99]])
mse = torch.nn.MSELoss()
loss = mse(o1_o2, o1_o2_true)
loss.backward()
print(model1.weight.grad)
print(model2.weight.grad)
train()
"""
tensor([[0.0004, 0.0009],
[0.0005, 0.0010]])
tensor([[ 0.0822, 0.0827],
[-0.0226, -0.0227]])
"""
- 官方标准BP算法写法
import torch
# 官方反向传播标准写法
class mynet(torch.nn.Module):
def __init__(self) -> None:
super(mynet,self).__init__()
# 定义网络结构
self.linear1 = torch.nn.Linear(2,2)
self.linear2 = torch.nn.Linear(2,2)
self.activation = torch.sigmoid
# 初始化参数
self.linear1.weight.data = torch.tensor([[0.15, 0.20], [0.25, 0.30]])
self.linear1.bias.data = torch.tensor([0.35, 0.35])
self.linear2.weight.data = torch.tensor([[0.40, 0.45], [0.50, 0.55]])
self.linear2.bias.data = torch.tensor([0.60, 0.60])
# 定义前向传播函数
def forward(self,x):
x = self.linear1(x)
x = self.activation(x)
x = self.linear2(x)
x = self.activation(x)
return x
if __name__ == '__main__':
model = mynet()
optimizer = torch.optim.SGD(model.parameters(),lr=0.1)
input = torch.tensor([[0.05, 0.10]])
target = torch.tensor([[0.01, 0.99]])
pred = model(input)# torch.nn.Module父类已经实现了调用forward前向传播函数,所有forword函数名不可更改名字
mes = torch.nn.MSELoss()
loss = mes(pred,target)
optimizer.zero_grad()
loss.backward()
print(model.linear1.weight.grad)
print(model.linear2.weight.grad)
# model.linear1.weight.data-=0.01*model.linear1.weight.grad
# model.linear2.weight.data-=0.01*model.linear2.weight.grad
optimizer.step()
"""
tensor([[0.0004, 0.0009],
[0.0005, 0.0010]])
tensor([[ 0.0822, 0.0827],
[-0.0226, -0.0227]])
"""
import torch
# 反向传标准写法2
import torch
# 官方反向传播标准写法
class mynet(torch.nn.Module):
def __init__(self) -> None:
super(mynet,self).__init__()
# 定义网络结构
self.hide1 = torch.nn.Sequential(torch.nn.Linear(2,2),torch.nn.Sigmoid())
self.out = torch.nn.Sequential(torch.nn.Linear(2,2),torch.nn.Sigmoid())
# 初始化参数
self.hide1[0].weight.data = torch.tensor([[0.15, 0.20], [0.25, 0.30]])
self.hide1[0].bias.data = torch.tensor([0.35, 0.35])
self.out[0].weight.data = torch.tensor([[0.40, 0.45], [0.50, 0.55]])
self.out[0].bias.data = torch.tensor([0.60, 0.60])
# 定义前向传播函数
def forward(self,x):
x = self.hide1(x)
x = self.out(x)
return x
if __name__ == '__main__':
model = mynet()
optimizer = torch.optim.SGD(model.parameters(),lr=0.1)
input = torch.tensor([[0.05, 0.10]])
target = torch.tensor([[0.01, 0.99]])
pred = model(input)# torch.nn.Module父类已经实现了调用forward前向传播函数,所有forword函数名不可更改名字
mes = torch.nn.MSELoss()
loss = mes(pred,target)
optimizer.zero_grad()
loss.backward()
print(model.hide1[0].weight.grad)
print(model.out[0].weight.grad)
# model.linear1.weight.data-=0.01*model.linear1.weight.grad
# model.linear2.weight.data-=0.01*model.linear2.weight.grad
optimizer.step()
"""
tensor([[0.0004, 0.0009],
[0.0005, 0.0010]])
tensor([[ 0.0822, 0.0827],
[-0.0226, -0.0227]])
"""
重要性
- 反向传播算法极大地提高了多层神经网络训练的效率,使得训练深度模型成为可能。通过链式法则逐层计算梯度,反向传播可以有效地处理复杂的网络结构,确保每一层的参数都能得到合理的调整。
BP之梯度下降
- 梯度下降算法的目标是找到使损失函数 最小的参数 ,其核心是沿着损失函数梯度的负方向更新参数,以逐步逼近局部或全局最优解,从而使模型更好地拟合训练数据。
数学描述
数学公式
其中,是学习率:
- 学习率太小,每次训练之后的效果太小,增加时间和算力成本。
- 学习率太大,大概率会跳过最优解,进入无限的训练和震荡中。
- 解决的方法就是,学习率也需要随着训练的进行而变化。
过程阐述
- 初始化参数:随机初始化模型的参数 ,如权重 W和偏置 b。
- 计算梯度:损失函数 对参数 的梯度 ,表示损失函数在参数空间的变化率。
- 更新参数:按照梯度下降公式更新参数:,其中, 是学习率,用于控制更新步长。
- 迭代更新:重复【计算梯度和更新参数】步骤,直到某个终止条件(如梯度接近0、不再收敛、完成迭代次数等)。
传统下降方式
批量梯度下降
Batch Gradient Descent BGD
1.特点:每次更新参数时,使用整个训练集来计算梯度。
2.优点:
- 收敛稳定,能准确地沿着损失函数的真实梯度方向下降。
- 适用于小型数据集。
3.缺点:
- 对于大型数据集,计算量巨大,更新速度慢。
- 需要大量内存来存储整个数据集。
4.公式:
- 其中,m是训练集样本总数,是第 i个样本及其标签。
随机梯度下降
Stochastic Gradient Descent, SGD
1.特点:每次更新参数时,仅使用一个样本来计算梯度。
2.优点:
- 更新频率高,计算快,适合大规模数据集。
- 能够跳出局部最小值,有助于找到全局最优解。
3.缺点:
- 收敛不稳定,容易震荡,因为每个样本的梯度可能都不完全代表整体方向。
- 需要较小的学习率来缓解震荡。
4.公式:
- 其中, 是当前随机抽取的样本及其标签。
小批量梯度下降
Mini-batch Gradient Descent MGBD
1.特点:每次更新参数时,使用一小部分训练集(小批量)来计算梯度。
2.优点:
- 在计算效率和收敛稳定性之间取得平衡。
- 能够利用向量化加速计算,适合现代硬件(如GPU)。
3.缺点:
- 选择适当的批量大小比较困难;批量太小则接近SGD,批量太大则接近批量梯度下降。
- 通常会根据硬件算力设置为32\64\128\256等2的次方。
4.公式:
- 其中,b是小批量的样本数量,也就是 。
存在的问题
- 收敛速度慢:BGD和MBGD使用固定学习率,太大会导致震荡,太小又收敛缓慢。
- 局部最小值和鞍点问题:SGD在遇到局部最小值或鞍点时容易停滞,导致模型难以达到全局最优。
- 训练不稳定:SGD中的噪声容易导致训练过程中不稳定,使得训练陷入震荡或不收敛。
优化梯度下降方式
- 为优化局部最优解和梯度不在更新问题
- 降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam 等.
指数加权平均
1.算数平均:指的是将所有数加起来除以数的个数,每个数的权重是相同的。
2.加权平均:指的是给每个数赋予不同的权重求得平均数。
3.移动平均数: 指的是计算最近邻的 N 个数来获得平均数。
4.指数移动加权平均(Exponential Moving Average简称EMA): 是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。
- 比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。
- 计算公式可以用下面的式子来表示:
-
St 表示指数加权平均值(EMA);
-
Yt 表示 t 时刻的值;
-
是平滑系数,取值范围为 。 越接近 1,表示对历史数据依赖性越高;越接近 0则越依赖当前数据。该值越大平均数越平缓
import numpy as np
import matplotlib.pyplot as plt
def test01():
np.random.seed(666)
y = np.random.randint(5, 40, 30)
print(y)
x = np.arange(30)
plt.plot(x, y, c = 'b')
plt.scatter(x, y, c = 'r')
plt.show()
def test02(beta = 0.9):
np.random.seed(666)
y = np.random.randint(5, 40, 30)
print(y)
y_e = []
for i in range(30):
if i == 0:
y_e.append(y[i])
else:
st = beta * y_e[-1] + (1 - beta) * y[i]
y_e.append(st)
# 会把y_e中的第一个数据给覆盖掉
# y_e=[(beta * y_e[-1] + (1 - beta) * i) for i in y[1:]] x = np.arange(30)
print(y_e)
plt.plot(x, y_e, c = 'b')
plt.scatter(x, y_e, c = 'r')
plt.show()
if __name__ == '__main__':
test01()
test02()
Momentum
1.特点
动量(Momentum)是对梯度下降的优化方法,可以更好地应对梯度变化和梯度消失问题,从而提高训练模型的效率和稳定性。
- 惯性效应: 该方法加入前面梯度的累积,这种惯性使得算法沿着当前的方向继续更新。如遇到鞍点,也不会因梯度逼近零而停滞。
- 减少震荡: 该方法平滑了梯度更新,减少在鞍点附近的震荡,帮助优化过程稳定向前推进。
- 加速收敛: 该方法在优化过程中持续沿着某个方向前进,能够更快地穿越鞍点区域,避免在鞍点附近长时间停留。
2.梯度计算公式
(1)梯度计算公式:
- St-1 表示历史梯度移动加权平均值
- wt 表示当前时刻的梯度值
- β 为权重系数
(2)假设:权重 β 为 0.9,例如:
- 第一次梯度值:s1 = d1 = w1
- 第二次梯度值:s2 = 0.9 * s1 + d2 * 0.1
- 第三次梯度值:s3 = 0.9 * s2 + d3 * 0.1
- 第四次梯度值:s4 = 0.9 * s3 + d4 * 0.1
- - w 表示初始梯度
- - d 表示当前轮数计算出的梯度值
- - s 表示历史梯度值
3.原理
- 每次的梯度都累计了前面不同权重的梯度值,确保了梯度可以继续向下更新
4.API
optimizer = optim.SGD(model.parameters(), lr=0.6, momentum=0.9) # 学习率和动量值可以根据实际情况调整,momentum 参数指定了动量系数,默认为0。动量系数通常设置为 0 到0.5 之间的一个值,但也可以根据具体的应用场景调整
5.总结:
- 动量项更新:利用当前梯度和历史动量来计算新的动量项。
- 权重参数更新:利用更新后的动量项来调整权重参数。
- 梯度计算:在每个时间步计算当前的梯度,用于更新动量项和权重参数。
Momentum 算法是对梯度值的平滑调整,但是并没有对梯度下降中的学习率进行优化。
AdaGrad
1.AdaGrad(Adaptive Gradient Algorithm)为每个参数引入独立的学习率,它根据历史梯度的平方和来调整这些学习率,这样就使得参数具有较大的历史梯度的学习率减小,而参数具有较小的历史梯度的学习率保持较大,从而实现更有效的学习。AdaGrad避免了统一学习率的不足,更多用于处理稀疏数据和梯度变化较大的问题。
2.AdaGrad流程:
- 初始化学习率 α、初始化参数 θ、小常数 σ = 1e-6
- 初始化梯度累积变量 s = 0
- 从训练集中采样 m 个样本的小批量,计算梯度 g
- 累积平方梯度 s = s + g ⊙ g,⊙ 表示各个分量相乘
3.学习率 α 的计算公式
4.参数更新公式如下:
- 是全局的初始学习率。
- 是一个非常小的常数,用于避免除零操作(通常取)。
- 是自适应调整后的学习率。
5.优点:
- 自适应学习率:由于每个参数的学习率是基于其梯度的累积平方和 来动态调整的,这意味着学习率会随着时间步的增加而减少,对梯度较大且变化频繁的方向非常有用,防止了梯度过大导致的震荡。
- 适合稀疏数据:AdaGrad 在处理稀疏数据时表现很好,因为它能够自适应地为那些较少更新的参数保持较大的学习率。
6.缺点:
- 学习率过度衰减:随着时间的推移,累积的时间步梯度平方值越来越大,导致学习率逐渐接近零,模型会停止学习。
- 不适合非稀疏数据:在非稀疏数据的情况下,学习率过快衰减可能导致优化过程早期停滞。
AdaGrad是一种有效的自适应学习率算法,然而由于学习率衰减问题,我们会使用改 RMSProp 或 Adam 来替代。
7.API
optimizer = optim.Adagrad(model.parameters(), lr=0.9) # 设置学习率
RMSProp
1.RMSProp(Root Mean Square Propagation)在时间步中,不是简单地累积所有梯度平方和,而是使用指数加权平均来逐步衰减过时的梯度信息。这种方法专门用于解决AdaGrad在训练过程中学习率过度衰减的问题。
2.RMSProp过程
- 初始化学习率 α、初始化参数 θ、小常数 σ = ( 用于防止除零操作(通常取 ))。
- 初始化参数 θ
- 初始化梯度累计变量 s=0
- 从训练集中采样 m 个样本的小批量,计算梯度 g
3.使用指数移动平均累积历史梯度
4.学习率 α 的计算公式
5.参数更新公式
6.优点
- 适应性强:RMSProp自适应调整每个参数的学习率,对于梯度变化较大的情况非常有效,使得优化过程更加平稳。
- 适合非稀疏数据:相比于AdaGrad,RMSProp更加适合处理非稀疏数据,因为它不会让学习率减小到几乎为零。
- 解决过度衰减问题:通过引入指数加权平均,RMSProp避免了AdaGrad中学习率过快衰减的问题,保持了学习率的稳定性
7.缺点
- 依赖于超参数的选择:RMSProp的效果对衰减率 和学习率 的选择比较敏感,需要一些调参工作。
需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些
8.API
optimizer = optim.RMSprop(model.parameters(), lr=0.7, momentum=0.9) # 设置学习率和动量
Adam
1.Adam(Adaptive Moment Estimation)算法将动量法和RMSProp的优点结合在一起:
- 动量法:通过一阶动量(即梯度的指数加权平均)来加速收敛,尤其是在有噪声或梯度稀疏的情况下。
- RMSProp:通过二阶动量(即梯度平方的指数加权平均)来调整学习率,使得每个参数的学习率适应其梯度的变化。
- Momentum 使用指数加权平均计算当前的梯度值、AdaGrad、RMSProp 使用自适应的学习率,Adam 结合了 Momentum、RMSProp 的优点,使用:移动加权平均的梯度和移动加权平均的学习率。使得能够自适应学习率的同时,也能够使用 Momentum 的优点。
2.优点
- 高效稳健:Adam结合了动量法和RMSProp的优势,在处理非静态、稀疏梯度和噪声数据时表现出色,能够快速稳定地收敛。
- 自适应学习率:Adam通过一阶和二阶动量的估计,自适应调整每个参数的学习率,避免了全局学习率设定不合适的问题。
- 适用大多数问题:Adam几乎可以在不调整超参数的情况下应用于各种深度学习模型,表现良好。
3.缺点
- 超参数敏感:尽管Adam通常能很好地工作,但它对初始超参数(如 、 和 )仍然较为敏感,有时需要仔细调参。
- 过拟合风险:由于Adam会在初始阶段快速收敛,可能导致模型陷入局部最优甚至过拟合。因此,有时会结合其他优化算法(如SGD)使用。
4.API
optimizer = optim.Adam(model.parameters(), lr=0.05) # 设置学习率
总结
- 梯度下降算法通过不断更新参数来最小化损失函数,是反向传播算法中计算权重调整的基础。在实际应用中,根据数据的规模和计算资源的情况,选择合适的梯度下降方式(批量、随机、小批量)及其变种(如动量法、Adam等)可以显著提高模型训练的效率和效果。
- Adam是目前最为流行的优化算法之一,因其稳定性和高效性,广泛应用于各种深度学习模型的训练中。Adam结合了动量法和RMSProp的优点,能够在不同情况下自适应调整学习率,并提供快速且稳定的收敛表现。
过拟合与欠拟合
- 在训练深层神经网络时,由于模型参数较多,在数据量不足时很容易过拟合。而正则化技术主要就是用于防止过拟合,提升模型的泛化能力(对新数据表现良好)和鲁棒性(对异常数据表现良好)。
概念认知
过拟合
1.过拟合是指模型对训练数据拟合能力很强并表现很好,但在测试数据上表现较差。
2.过拟合常见原因有:
- 数据量不足:当训练数据较少时,模型可能会过度学习数据中的噪声和细节。
- 模型太复杂:如果模型很复杂,也会过度学习训练数据中的细节和噪声。
- 正则化强度不足:如果正则化强度不足,可能会导致模型过度学习训练数据中的细节和噪声。
欠拟合
- 欠拟合是由于模型学习能力不足,无法充分捕捉数据中的复杂关系。
如何判断
1.过拟合
- 训练误差低,但验证时误差高。模型在训练数据上表现很好,但在验证数据上表现不佳,说明模型可能过度拟合了训练数据中的噪声或特定模式。
2.欠拟合
- 训练误差和测试误差都高。模型在训练数据和测试数据上的表现都不好,说明模型可能太简单,无法捕捉到数据中的复杂模式。
解决欠拟合
1.欠拟合的解决思路比较直接:
- 增加模型复杂度:引入更多的参数、增加神经网络的层数或节点数量,使模型能够捕捉到数据中的复杂模式。
- 增加特征:通过特征工程添加更多有意义的特征,使模型能够更好地理解数据。
- 减少正则化强度:适当减小 L1、L2 正则化强度,允许模型有更多自由度来拟合数据。
- 训练更长时间:如果是因为训练不足导致的欠拟合,可以增加训练的轮数或时间.
解决过拟合
- 避免模型参数过大是防止过拟合的关键步骤之一。。
L2正则化
1.L2 正则化通过在损失函数中添加权重参数的平方和来实现,目标是惩罚过大的参数值。
2.数学表示
- 设损失函数为 ,其中 表示权重参数,加入L2正则化后的损失函数表示为:
- 是原始损失函数(比如均方误差、交叉熵等)。
- 是正则化强度,控制正则化的力度。
- 是模型的第 i个权重参数。
- 是所有权重参数的平方和,称为 L2 正则化项。
L2 正则化会惩罚权重参数过大的情况,通过参数平方值对损失函数进行约束。
3.梯度更新
在 L2 正则化下,梯度更新时,不仅要考虑原始损失函数的梯度,还要考虑正则化项的影响。更新规则为:
- 是学习率。
- 是损失函数关于参数 的梯度。
- 是 L2 正则化项的梯度,对应的是参数值本身的衰减。
很明显,参数越大惩罚力度就越大,从而让参数逐渐趋向于较小值,避免出现过大的参数。
4.作用
- 防止过拟合:当模型过于复杂、参数较多时,模型会倾向于记住训练数据中的噪声,导致过拟合。L2 正则化通过抑制参数的过大值,使得模型更加平滑,降低模型对训练数据噪声的敏感性。
- 限制模型复杂度:L2 正则化项强制权重参数尽量接近 0,避免模型中某些参数过大,从而限制模型的复杂度。通过引入平方和项,L2 正则化鼓励模型的权重均匀分布,避免单个权重的值过大。
- 提高模型的泛化能力:正则化项的存在使得模型在测试集上的表现更加稳健,避免在训练集上取得极高精度但在测试集上表现不佳。
- 平滑权重分布:L2 正则化不会将权重直接变为 0,而是将权重值缩小。这样模型就更加平滑的拟合数据,同时保留足够的表达能力。
import torch.optim as optim
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.001) # L2 正则化,weight_decay就是L2正则化前面的参数λ
L1正则化
1.L1 正则化通过在损失函数中添加权重参数的绝对值之和来约束模型的复杂度。
2.损失函数
3.梯度更新函数
- 是学习率。
- 是损失函数关于参数 的梯度。
- 是参数 的符号函数,表示当 为正时取值为 1,为负时取值为 -1,等于 0 时为 0。
4.作用
- 稀疏性:L1 正则化的一个显著特性是它会促使许多权重参数变为 零。这是因为 L1 正则化倾向于将权重绝对值缩小到零,使得模型只保留对结果最重要的特征,而将其他不相关的特征权重设为零,从而实现 特征选择 的功能。
- 防止过拟合:通过限制权重的绝对值,L1 正则化减少了模型的复杂度,使其不容易过拟合训练数据。相比于 L2 正则化,L1 正则化更倾向于将某些权重完全移除,而不是减小它们的值。
- 简化模型:由于 L1 正则化会将一些权重变为零,因此模型最终会变得更加简单,仅依赖于少数重要特征。这对于高维度数据特别有用,尤其是在特征数量远多于样本数量的情况下。
- 特征选择:因为 L1 正则化会将部分权重置零,因此它天然具有特征选择的能力,有助于自动筛选出对模型预测最重要的特征。
5.与L2对比
- L1 正则化 更适合用于产生稀疏模型,会让部分权重完全为零,适合做特征选择。
- L2 正则化 更适合平滑模型的参数,避免过大参数,但不会使权重变为零,适合处理高维特征较为密集的场景。
import torch
def train():
# 模型构建
model = torch.nn.Linear(5, 2)
# 损失函数
loss_fn = torch.nn.MSELoss()
# 优化器
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 输入数据
x =torch.tensor([[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]], dtype=torch.float32)
# 预测
y_pred = model(x)
target = torch.tensor([[0.1, 1.2], [1.2, 0.6]], dtype=torch.float32)
# 计算损失:L1 正则化项并将其加入到总损失中
l1_lambda = 0.001
# arr = []
# print(model.parameters())
# for p in model.parameters():
# arr.append(p.abs().sum())
# print(p)
# print(sum(arr))
l1_norm = sum([p.abs().sum() for p in model.parameters()])
loss = loss_fn(y_pred, target)+l1_lambda*l1_norm
# print(loss)
# 梯度清零
if model.weight.grad is not None:
model.zero_grad()
# 反向传播
loss.backward()
# 进行权重参数更新
optimizer.step()
# 打印更新之后的权重参数
print(model.weight)
# 保存模型权重参数
train()
Dropout
- Dropout 是一种在训练过程中随机丢弃部分神经元的技术。它通过减少神经元之间的依赖来防止模型过于复杂,从而避免过拟合。
基本实现
import torch
def test01():
x = torch.tensor([1,2,3,42,21,3,2,1,1,3,4,2,4,2,2,3],dtype=torch.float32)
# 丢弃的是输出神经元,不是weights或者是x
drop = torch.nn.Dropout(p=0.6)# 每个数据有0.6的概率被置为0
print(x.shape)
x = drop(x)
print(x)
print(x.shape)
print(x!=0)
print(sum(x!=0))
print(x.shape[0])
print(sum(x!=0)/x.shape[0])
test01()
"""
torch.Size([16])
tensor([2.5000, 5.0000, 7.5000, 0.0000, 0.0000, 7.5000, 0.0000, 0.0000, 0.0000,
7.5000, 0.0000, 0.0000, 0.0000, 0.0000, 5.0000, 0.0000])
torch.Size([16])
tensor([ True, True, True, False, False, True, False, False, False, True,
False, False, False, False, True, False])
tensor(6)
16
tensor(0.3750)
"""
Dropout过程:
- 按照指定的概率把部分神经元的值设置为0;
- 为了规避该操作带来的影响,需对非 0 的元素使用缩放因子进行强化
权重影响
import torch
def test02():
x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
w = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32,requires_grad=True)
drop = torch.nn.Dropout(p=0.5)
x = drop(x)
out = x*w
print(out)
out.sum().backward()
print(w.grad)
if __name__ == '__main__':
test02()
"""
tensor([[ 0., 0., 18.],
[ 0., 50., 0.]], grad_fn=<MulBackward0>)
tensor([[ 0., 0., 6.],
[ 0., 10., 0.]])
"""
简化模型
- 减少网络层数和参数: 通过减少网络的层数、每层的神经元数量或减少卷积层的滤波器数量,可以降低模型的复杂度,减少过拟合的风险。
- 使用更简单的模型: 对于复杂问题,使用更简单的模型或较小的网络架构可以减少参数数量,从而降低过拟合的可能性。
数据增强
- 通过对训练数据进行各种变换(如旋转、裁剪、翻转、缩放等),可以增加数据的多样性,提高模型的泛化能力。
from torchvision import transforms
transform = transforms.Compose([
transforms.RandomHorizontalFlip(),
transforms.RandomVerticalFlip(),
transforms.RandomRotation(10),
transforms.ToTensor()
])
早停
- 早停是一种在训练过程中监控模型在验证集上的表现,并在验证误差不再改善时停止训练的技术。这样可避免训练过度,防止模型过拟合。
模型集成
- 通过将多个不同模型的预测结果进行集成,可以减少单个模型过拟合的风险。常见的集成方法包括投票法、平均法和堆叠法。
交叉验证
- 使用交叉验证技术可以帮助评估模型的泛化能力,并调整模型超参数,以防止模型在训练数据上过拟合。
- 这些方法可以单独使用,也可以结合使用,以有效地防止参数过大和过拟合。根据具体问题和数据集的特点,选择合适的策略来优化模型的性能。
批量标准化
- 在神经网络的搭建过程中,Batch Normalization (批量归一化)是经常使用一个网络层,其主要的作用是控制数据的分布,加快网络的收敛。
- 我们知道,神经网络的学习其实在学习数据的分布,随着网络的深度增加、网络复杂度增加,一般流经网络的数据都是一个 mini batch,每个 mini batch 之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性,使得模型难以收敛。
- 如果我们对每一个 mini batch 的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。
实现过程
- 批量标准化的基本思路是在每一层的输入上执行标准化操作,并学习两个可训练的参数:缩放因子 偏移量 。
计算均值和方差
对于给定的神经网络层,假设输入数据为 ,其中 m是批次大小。我们首先计算该批次数据的均值和方差。
- 均值(Mean)
- 方差
标准化
使用计算得到的均值和方差对数据进行标准化,使得每个特征的均值为0,方差为1。
标准化后的值
- 其中, 是一个很小的常数,防止除以零的情况。
缩放和平移
标准化后的数据通常会通过可训练的参数进行缩放和平移,以恢复模型的表达能力。
- 缩放:
-
平移:
标准化公式
其中, 和 是在训练过程中学习到的参数。
- λ 和 β 是可学习的参数,它相当于对标准化后的值做了一个线性变换,λ 为系数,β 为偏置;
- 通常指为 ,避免分母为 0;
- 表示变量的均值;
- 表示变量的方差;
训练和推理阶段
- 训练阶段: 在训练过程中,均值和方差是基于当前批次的数据计算得到的。
- 推理阶段: 在推理阶段,批量标准化使用的是训练过程中计算得到的全局均值和方差(来自于训练),而不是当前批次的数据。这些全局均值和方差通常会被保存在模型中,用于推理时的标准化过程。
作用
- 批量标准化(Batch Normalization, BN)通过以下几个方面来提高神经网络的训练稳定性、加速训练过程并减少过拟合
缓解梯度问题
- 标准化处理可以防止激活值过大或过小,避免了激活函数(如 Sigmoid 或 Tanh)饱和的问题,从而缓解梯度消失或爆炸的问题。
加速训练
- 由于 BN 使得每层的输入数据分布更为稳定,因此模型可以使用更高的学习率进行训练。这可以加快收敛速度,并减少训练所需的时间。
减少过拟合
- 类似于正则化:虽然 BN 不是一种传统的正则化方法,但它通过对每个批次的数据进行标准化,可以起到一定的正则化作用。它通过在训练过程中引入了噪声(由于批量均值和方差的估计不完全准确),这有助于提高模型的泛化能力。
- 避免对单一数据点的过度拟合:BN 强制模型在每个批次上进行标准化处理,减少了模型对单个训练样本的依赖。这有助于模型更好地学习到数据的整体特征,而不是对特定样本的噪声进行过度拟合。
BatchNorm
- 数据在经过 BN 层之后,无论数据以前的分布是什么,都会被归一化成均值为 β,标准差为 γ 的分布。
注意:BN 层不会改变输入数据的维度,只改变输入数据的的分布. 在实际使用过程中,BN 常常和卷积神经网络结合使用,卷积层的输出结果后接 BN 层。
API
torch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True)
- 由于每次使用的 mini batch 的数据集,所以 BN 使用移动加权平均来近似计算均值和方差,而 momentum 参数则调节移动加权平均值的计算;
- affine = False 表示 =1,β=0,反之,则表示 γ 和 β 要进行学习;
- BatchNorm2d 适用于输入的数据为 4D,输入数据的形状 [N,C,H,W]
- N 表示批次,C 代表通道数,H 代表高度,W 代表宽度
- 由于每次输入到网络中的时小批量的样本,我们使用指数加权平均来近似表示整体的样本的均值和方差,其更新公式如下:
running_mean = momentum * running_mean + (1.0 – momentum) * batch_mean running_var = momentum * running_var + (1.0 – momentum) * batch_var
- batch_mean 和 batch_var 表示当前批次的均值和方差。而 running_mean 和 running_var 是近似的整体的均值和方差的表示。当我们进行评估时,可以使用该均值和方差对输入数据进行归一化。
代码实现
import torch
import torch.nn as nn
def test01():
img = torch.randint(0,255,(16, 3, 512, 512), dtype=torch.float32)# CHW
# print(img[0])
norm2d = nn.BatchNorm2d(num_features=3)
# 具有返回值
img=norm2d(img)
print(img)
test01()
项目实战
1.项目介绍
- 鲍勃开了自己的手机公司。他想与苹果、三星等大公司展开硬仗。 他不知道如何估算自己公司生产的手机的价格。在这个竞争激烈的手机市场,你不能简单地假设事情。为了解决这个问题,他收集了各个公司的手机销售数据。 鲍勃想找出手机的特性(例如:RAM、内存等)和售价之间的关系。但他不太擅长机器学习。所以他需要你帮他解决这个问题。 在这个问题中,你不需要预测实际价格,而是要预测一个价格区间,表明价格多高。
- 需要注意的是: 在这个问题中,我们不需要预测实际价格,而是一个价格范围,它的范围使用 0、1、2、3 来表示,所以该问题也是一个分类问题。
- 数据说明:手机价格分类_数据集-阿里云天池 Mobile Price Classification
- 推荐专业的数据集平台:Mobile Price Classification | Kaggle
字段 | 说明 |
---|---|
battery_power | 电池容量(mAh) |
blue | 是否支持蓝牙 |
clock_speed | 微处理器执行指令的速度 |
dual_sim | 是否支持双卡 |
fc | 前置摄像头分辨率(百万像素) |
four_g | 是否支持4G |
int_memory | 存储内存(GB) |
m_dep | 手机厚度(厘米) |
mobile_wt | 重量 |
n_cores | 核心数 |
pc | 主摄像头分辨率(百万像素) |
px_height | 屏幕分辨率高度(像素) |
px_width | 屏幕分辨率宽度(像素) |
ram | 运行内存(MB) |
sc_h | 屏幕长度(厘米) |
sc_w | 屏幕宽度(厘米) |
talk_time | 单次充电最长通话时间 |
three_g | 是否支持3G |
touch_screen | 是否是触摸屏 |
wifi | 是否支持WIFI |
price_range | 价格区间 |
构建数据集
1.数据简介
- 数据共有 2000 条, 其中 1600 条数据作为训练集, 400 条数据用作测试集。 我们使用 sklearn 的数据集划分工作来完成。并使用 PyTorch 的 TensorDataset 来将数据集构建为 Dataset 对象,方便构造数据集加载对象。
2.调整策略
- 对输入数据进行标准化
- 调整优化方法
- 调整学习率
- 增加批量归一化层
- 增加网络层数、神经元个数
- 增加训练轮数
3.本题主要调整
- 优化方法由 SGD 调整为 Adam
- 学习率由 1e-3 调整为 1e-4
- 对数据数据进行标准化
- 增加网络深度, 即: 增加网络参数量
3.项目实战代码
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
# import numpy as np
from sklearn.preprocessing import StandardScaler
import os
# 1.数据
def phone_data_set(path):
data = pd.read_csv(path)
# print(data)
# 抽离特征和目标数据
x = data.iloc[:, :-1] # 手机价格区间,目标值
y = data.iloc[:, -1] # 特征值
# 因为读出来的数据是dataframe数据,需要先转为numpy,需要转成tensor
print(type(x.values), type(y.values))
# print(x.values.shape, y.values.shape)
# print(x.values[0], y.values[0])
scaler = StandardScaler()
x = scaler.fit_transform(x.values)
x = torch.tensor(x, dtype=torch.float32)
y = torch.tensor(y.values, dtype=torch.long)
# 数据划分
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=666, stratify=y) # 随机种子
# 标准化
# scaler = StandardScaler()
# x_train = scaler.fit_transform(x_train)
return x_train, x_test, y_train, y_test
# 需要写在一个类中
# phone_data_set("./手机价格预测/data/手机价格预测.csv")
class my_phone_data_loader(Dataset):
def __init__(self, x, y):
self.x = x
self.y = y
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
return self.x[idx], self.y[idx]
def data_loder(x_train, y_train,batch_size = 16):
# data = my_phone_data_loader(x_train, y_train)
data = TensorDataset(x_train, y_train)# 返回出一个包含特征值和目标值的元组
data_loader_ = DataLoader(data, batch_size=batch_size, shuffle=True) # 划分测试样本
return data_loader_
# 2.模型(构建和初始化数据),神经网络
class Net(torch.nn.Module):
def __init__(self, input_feature, output_feature):
super(Net, self).__init__()
self.linear1 = torch.nn.Linear(input_feature, 128)
self.activation1 = torch.nn.LeakyReLU()
self.linear2 = torch.nn.Linear(128, 256)
self.activation2 = torch.nn.LeakyReLU()
self.linear3 = torch.nn.Linear(256, 128)
self.activation3 = torch.nn.LeakyReLU()
self.linear4 = torch.nn.Linear(128, 64)
self.activation4 = torch.nn.LeakyReLU()
self.out = torch.nn.Linear(64, output_feature)
self.activation5 = torch.nn.Softmax()
# 初始化模型参数
# 默认是初始化过的
torch.nn.init.kaiming_normal_(self.linear1.weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.linear2.weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.linear3.weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.linear4.weight, nonlinearity='leaky_relu')
def forward(self, input_data):
x = self.linear1(input_data)
x = self.activation1(x)
x = self.linear2(x)
x = self.activation2(x)
x = self.linear3(x)
x = self.activation3(x)
x = self.linear4(x)
x = self.activation4(x)
x = self.out(x)
y_pred = self.activation5(x)
return y_pred
x_train, x_test, y_train, y_test = phone_data_set('.\data\手机价格预测.csv')
# 训练
def train():
# 加载数据
data_loader_ = data_loder(x_train, y_train,batch_size=32)
# 模型生成
x_features = x_train.shape[1]
y_features = torch.unique(y_train).shape[0]# 输d出的特征(类别的数据)
model = Net(x_features, y_features)
# 损失函数
# 虽然分类结果可以用均方差损失函数,但是分类问题,对应的概率,一般使用交叉熵损失函数(因为计算出来的梯度更大)
loss_fn = torch.nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
# 定义训练参数
epochs = 1000
for epoch in range(epochs):
e =0
count =0
start_time = time.time()
for x, y in data_loader_:
count +=1
# 生成预测值
y_pred = model(x)# 执行model对象的forword方法
# 损失计算
loss = loss_fn(y_pred, y)
e +=loss
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
end_time = time.time()
print(f"epoch:{epoch},loss:{e/count}, time:{end_time-start_time}")
torch.save(model.state_dict(), ".\model\model.pth")# 保存的路径不能有中文
# 评估
def test():
# 加载数据
data_loader_ = data_loder(x_test, y_test,batch_size=32)
# data_loader_ = DataLoader(data, batch_size=32, shuffle=True)
# 加载模型
# 模型生成
x_test_feature = x_test.shape[1]
y_test_feature = torch.unique(y_test).shape[0]
model = Net(x_test_feature, y_test_feature)
state_dict = torch.load(".\model\model.pth", map_location="cpu")
model.load_state_dict(state_dict)
total = 0
for x,y in data_loader_:
y_pred = model(x)
y_pred = torch.argmax(y_pred, dim=1)
# print("y_pred",y_pred)
# print("y",y)
total += (y_pred == y).sum()
print("正确率:", total/len(x_test))
if __name__ == '__main__':
train()
test()
5.项目整合
- dataset数据加载和分割
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
# import numpy as np
from sklearn.preprocessing import StandardScaler
import os
# 1.数据
def phone_data_set(path):
data = pd.read_csv(path)
# print(data)
# 抽离特征和目标数据
x = data.iloc[:, :-1] # 手机价格区间,目标值
y = data.iloc[:, -1] # 特征值
# 因为读出来的数据是dataframe数据,需要先转为numpy,需要转成tensor
# print(type(x.values), type(y.values))
# print(x.values.shape, y.values.shape)
# print(x.values[0], y.values[0])
scaler = StandardScaler()
x = scaler.fit_transform(x.values)
x = torch.tensor(x, dtype=torch.float32)
y = torch.tensor(y.values, dtype=torch.long)
torch.save(scaler, "./model/phone_scaler.pth")
# 数据划分
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=666, stratify=y) # 随机种子
# 标准化
# scaler = StandardScaler()
# x_train = scaler.fit_transform(x_train)
return x_train, x_test, y_train, y_test
# 需要写在一个类中
# phone_data_set("./手机价格预测/data/手机价格预测.csv")
class my_phone_data_loader(Dataset):
def __init__(self, x, y):
self.x = x
self.y = y
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
return self.x[idx], self.y[idx]
def data_loder(x_train, y_train,batch_size = 16):
# data = my_phone_data_loader(x_train, y_train)
data = TensorDataset(x_train, y_train)# 返回出一个包含特征值和目标值的元组
data_loader_ = DataLoader(data, batch_size=batch_size, shuffle=True) # 划分测试样本
return data_loader_
# x_train,x_test,y_trian,y_test=phone_data_set("./data/手机价格预测.csv")
# def train_data():
# # 加载数据
# return x_train,y_trian
# def test_data():
# # 加载数据
# return x_test,y_test
def train_data(path,batch_size):
x_train, x_test, y_train, y_test = phone_data_set(path)
data_loader_ = data_loder(x_train, y_train, batch_size)
return x_train, y_train, data_loader_
def test_data(path,batch_size):
x_train, x_test, y_train, y_test = phone_data_set(path)
data_loader_ = data_loder(x_test, y_test, batch_size)
return x_test, y_test, data_loader_
- 构建网络模型
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
from sklearn.preprocessing import StandardScaler
import os
# 模型(构建和初始化数据),神经网络
class Net(torch.nn.Module):
def __init__(self, input_feature, output_feature):
super(Net, self).__init__()
self.hide1 = torch.nn.Sequential(torch.nn.Linear(input_feature, 128), torch.nn.LeakyReLU())
self.hide2 = torch.nn.Sequential(torch.nn.Linear(128, 256), torch.nn.LeakyReLU())
self.hide3 = torch.nn.Sequential(torch.nn.Linear(256, 512), torch.nn.LeakyReLU())
self.hide4 = torch.nn.Sequential(torch.nn.Linear(512, 128), torch.nn.LeakyReLU())
self.out = torch.nn.Sequential(torch.nn.Linear(128, output_feature), torch.nn.Softmax())
# 初始化模型参数
# 默认是初始化过的
torch.nn.init.kaiming_normal_(self.hide1[0].weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.hide2[0].weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.hide3[0].weight, nonlinearity='leaky_relu')
torch.nn.init.kaiming_normal_(self.hide4[0].weight, nonlinearity='leaky_relu')
def forward(self, input_data):
x = self.hide1(input_data)
x = self.hide2(x)
x = self.hide3(x)
x = self.hide4(x)
y_pred = self.out(x)
return y_pred
- 训练集
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
# import numpy as np
from sklearn.preprocessing import StandardScaler
import os
import data.dataset as dataset
from net.net import Net
# 训练
def train():
# 加载数据
# x_train,y_train=dataset.train_data()
# data_loader_=dataset.data_loader(x_train,y_train)
# 传入的路径以当前的文件夹为准
x_train, y_train, data_loader_ = dataset.train_data('./data/手机价格预测.csv', batch_size=32)
# 模型生成
x_features = x_train.shape[1]
y_features = torch.unique(y_train).shape[0]# 输d出的特征(类别的数据)
model = Net(x_features, y_features)
# 损失函数
# 虽然分类结果可以用均方差损失函数,但是分类问题,对应的概率,一般使用交叉熵损失函数(因为计算出来的梯度更大)
loss_fn = torch.nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
# 定义训练参数
epochs = 100
for epoch in range(epochs):
e =0
count =0
start_time = time.time()
for x, y in data_loader_:
count +=1
# 生成预测值
y_pred = model(x)# 执行model对象的forword方法
# 损失计算
loss = loss_fn(y_pred, y)
e +=loss
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
end_time = time.time()
print(f"epoch:{epoch},loss:{e/count}, time:{end_time-start_time}")
torch.save(model.state_dict(), ".\model\model.pth")# 保存的路径不能有中文
if __name__ == "__main__":
train()
- 评估集
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
# import numpy as np
from sklearn.preprocessing import StandardScaler
import os
from net.net import Net
import data.dataset as dataset
# 评估
def test():
# 加载数据
x_test, y_test, data_loader_ = dataset.test_data(".\data\手机价格预测.csv",32)
# data_loader_ = data_loder(x_test, y_test,batch_size=32)
# data_loader_ = DataLoader(data, batch_size=32, shuffle=True)
# 加载模型
# 模型生成
x_test_feature = x_test.shape[1]
y_test_feature = torch.unique(y_test).shape[0]
model = Net(x_test_feature, y_test_feature)
state_dict = torch.load(".\model\model.pth", map_location="cpu")
model.load_state_dict(state_dict)
total = 0
for x,y in data_loader_:
y_pred = model(x)
y_pred = torch.argmax(y_pred, dim=1)
# print("y_pred",y_pred)
# print("y",y)
total += (y_pred == y).sum()
print("正确率:", total/len(x_test))
if __name__ == '__main__':
test()
- 测试集
import time
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader,TensorDataset
# import numpy as np
from sklearn.preprocessing import StandardScaler
import os
from net.net import Net
def detect(input_data):
path = './model/model.pth'
state_dict = torch.load(path, map_location="cpu")
model = Net(20,4)# 保存的时候把这20,4保存起来,现在就取出来用
model.load_state_dict(state_dict)
y_pred = model(input_data)
y_pred = torch.argmax(y_pred, dim=1)
print("预测结果为:", y_pred)
return y_pred
if __name__ == '__main__':
path = './data/手机价格预测test.csv'
data = pd.read_csv(path).iloc[:,1:]
transfer = torch.load('./model/phone_scaler.pth')
x=transfer.transform(data.values)
x = torch.tensor(x,dtype=torch.float32)
y_pred = detect(x)
print(y_pred)