神经网络基础-从零开始搭建一个神经网络

        

一、什么是神经网络

人工神经网络(Articial Neural Network,简写为ANN)也称为神经网络(NN),是一种模仿生物神经网络和功能的计算模型,人脑可以看做是一个生物神经网络,由众多的神经元连接而成,各个神经元传递复杂的电信号,树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。人工神经网络也是类似,通过模仿人脑的“学习机器”,通过不断“看”数据(比如图片,数字),自己总结规律,最终学会预测(比如分类图片)

1.1、如何构建人工神经网络中的神经元

神经元=输入X权重+偏置->激活函数

权重:调节每个输入的重要性(类似音量旋钮)

偏置:调整输出的基准线(比如天生爱吃甜食)

激活函数:决定是否触发输出(比如及格线60分)

权重和偏置怎么来的?

一开始随机初始化,训练时通过数据自动调整到最优值

1.1、神经网络

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

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动,神经元的三层结构:

输入层:

  • 像眼睛一样接受数据,比如输入一张3个数字的表格(比如身高,体重,年龄)
  • 代码层面对应的是:my_data = torch.randn(5,3)生成一个5行3列的随机数据(5个人,每人3个特征)

隐藏层

  • 像大脑一样处理数据,图中有两层“脑细胞”(Hidder Layer1和Hidden Layer2)

输出层:

  • 像“嘴巴”一样给出答案,比如输出两个结果,y1,y2

1.2、神经网络的特点:

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

深度学习与机器学习的关系

深度学习是机器学习的一个分支,深度学习用多层神经网络(尤其是深层的网络)来学习数据,自动从数据中提取复杂特征,不用人工设计规则。

二、激活函数

激活函数用于对每层的输出数据进行变换,进而为整个网络注入非线性因素,此时,神经网络就可以拟合各种曲线,没有引入非线性因素的网络等价于使用一个线性模型来拟合

通过给网格输出增加激活函数,实现引入非线性因素,使得网络模型可以逼近任意函数,提升网络对复杂问题的拟合能力

2.1、常见的激活函数

1、sigmoid激活函数
  • 可以将任意的输入映射到(0,1)之间,当输入的值大致在<-6或者>6时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分信息
  • 对于sigmoid函数而言,输入值在[-6,6]之间输出值才有明显差异,输入值在[3,-3]之间才会有比较好的效果
  • sigmoid网络在5层之内就会产生梯度消失现象,该激活函数并不是以0为中心的,在实践中这种激活函数使用的很少,一般只用于二分类的输出层

2、tanh激活函数

  • Tanh 函数将输入映射到 (-1, 1) 之间,图像以 0 为中心,在 0 点对称,当输入 大概<-3 或者>3 时将被映射为 -1 或者 1。其导数值范围 (0, 1),当输入的值大概 <-3 或者 > 3 时,其导数近似 0
  • 与 Sigmoid 相比,它是以 0 为中心的,且梯度相对于sigmoid大,使得其收敛速度要比Sigmoid 快,减少迭代次数。然而,从图中可以看出,Tanh 两侧的导数也为 0,同样会造成梯度消失
  • 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数
3、softMax激活函数

softMax用于多分类过程中,是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展示出来,将网络输出的logits通过softMax函数,映射成为(0,1)的值,这些值的累加和为1,选取概率最大的节点,作为我们的预测目标类别

4、ReLU激活函数

ReLU 激活函数将小于 0 的值映射为 0,而大于 0 的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。

当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”。

ReLU是目前最常用的激活函数。与sigmoid相比,RELU的优势是:

采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大,而采用

Relu激活函数,整个过程的计算量节省很多。 sigmoid函数反向传播时,很容易就会出现梯度

消失的情况,从而无法完成深层网络的训练。 Relu会使一部分神经元的输出为0,这样就造成了

网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生

2.2、激活函数的选择方法

对于隐藏层:

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

对于输出层:

  1. 二分类问题选择sigmoid激活函数
  2. 多分类问题选择softmax激活函数
  3. 回归问题选择identity激活函数
2.3、参数初始化
  • 均匀分布初始化

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

  • 正态分布式初始化

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

  • 全0初始化

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

  • 全1初始化

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

  • 固定值初始化

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

  • kaiming初始化,也叫做HE初始化

HE 初始化分为正态分布的 HE 初始化、均匀分布的 HE 初始化

正态化的he初始化

stddev = sqrt(2 / fan_in)

均匀分布的he初始化

它从 [-limit,limit] 中的均匀分布中抽取样本, limit是 sqrt(6 / fan_in)

fan_in输入神经元的个数

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():
    print(f"均匀分布随机初始化{'='*30}均匀分布随机初始化")
    linear = nn.Linear(5,3)
    nn.init.uniform_(linear.weight)
    print(linear.weight.data)

# 2、固定初始化
def test02():
    print(f"固定初始化{'='*30}固定初始化")
    linear = nn.Linear(5,3)
    nn.init.constant_(linear.weight,5)
    print(linear.weight.data)

#3、全0初始化
def test03():
    print(f"全0初始化{'='*30}全0初始化")
    linear = nn.Linear(5,3)
    nn.init.zeros_(linear.weight)
    print(linear.weight.data)

def test04():
    print(f"全1初始化{'='*30}全1初始化")
    linear = nn.Linear(5,3)
    nn.init.ones_(linear.weight)
    print(linear.weight.data)
#适合从正态分布 N(mean,std^2) 采样权重。
#需手动调整方差,适合浅层网络或特定分布假设
def test05():
    print(f"正态分布随机初始化{'='*30}正态分布随机初始化")
    linear = nn.Linear(5,3)
    # mean:均值,默认0
    #std 标准差,默认1
    nn.init.normal_(linear.weight,mean=0,std=1)
    print(linear.weight.data)

#6、kaiming 初始化
#针对ReLU族激活函数设计,根据输入/输出维度调整方差
#适合深层网络(如CNN)、ReLU/Leaky ReLU激活函数
def test06():
    print(f"kaiming正态分布初始化{'='*30}kaiming正态分布初始化")
    #kaiming正态分布初始化
    linear = nn.Linear(5,3)
    nn.init.kaiming_normal_(linear.weight)
    print(linear.weight.data)

    print(f"kaiming均匀分布初始化{'='*30}kaiming均匀分布初始化")
    #kaiming均匀分布初始化
    linear = nn.Linear(5,3)
    #nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
    #mode:fan_in(前向传播,默认)或 fan_out(反向传播)
    #nonlinearity:激活函数类型(如 relu, leaky_relu)
    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)

#7xavier初始化
#正态分布:nn.init.xavier_normal_(tensor, gain=1.0)
#均匀分布:nn.init.xavier_uniform_(tensor, gain=1.0)
#保持输入输出方差一致,适用于对称激活函数(如tanh、sigmoid)
#gain:根据激活函数调整的缩放因子(如tanh的gain=5/3)
def test07():
    print(f"xavier正态分布初始化{'='*30}xavier正态分布初始化")
    #xavier正态分布初始化
    linear = nn.Linear(5,3)
    nn.init.xavier_uniform_(linear.weight)
    print(linear.weight.data)
    
    print(f"xavier均匀分布初始化{'='*30}xavier均匀分布初始化")
    liner = nn.Linear(5,3)
    nn.init.xavier_uniform_(linear.weight)
    print(linear.weight.data)

if __name__ =="__main__":
    test01()
    test02()
    test03()
    test04()
    test05()
    test06()
    test07()
    

输出结果:

均匀分布随机初始化==============================均匀分布随机初始化
tensor([[0.6716, 0.1197, 0.7008, 0.0801, 0.8953],
        [0.0483, 0.4593, 0.5607, 0.2824, 0.1307],
        [0.5942, 0.7758, 0.1852, 0.8597, 0.5166]])
固定初始化==============================固定初始化
tensor([[5., 5., 5., 5., 5.],
        [5., 5., 5., 5., 5.],
        [5., 5., 5., 5., 5.]])
全0初始化==============================全0初始化
tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])
全1初始化==============================全1初始化
tensor([[1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.],
        [1., 1., 1., 1., 1.]])
正态分布随机初始化==============================正态分布随机初始化
tensor([[-1.0910, -0.4724, -0.6468, -0.0121, -1.1449],
        [ 0.6663, -0.1809,  1.1174,  0.9543, -0.2671],
        [ 0.5979,  1.5574,  0.4182, -0.0591,  0.4090]])
kaiming正态分布初始化==============================kaiming正态分布初始化
tensor([[-0.5628,  0.2732,  0.3467, -0.7840,  0.7371],
        [ 0.0155,  0.6244,  0.1153,  0.1079,  0.4050],
        [-1.2527, -0.0167, -0.2999, -0.4666, -1.9518]])
kaiming均匀分布初始化==============================kaiming均匀分布初始化
tensor([[ 1.0959, -0.3050, -0.1851,  0.5269, -0.3553],
        [ 0.1472, -0.4767,  0.0920, -0.2831, -0.0370],
        [ 0.0023, -0.5695, -0.3103, -0.5530,  1.0353]])
tensor([[-0.4601, -0.1885,  0.3863, -0.9680, -0.8768],
        [-0.5476, -0.5110,  0.0127, -0.3590, -0.1244],
        [ 0.0694, -0.5519, -1.0414,  0.5382, -0.5391]])
xavier正态分布初始化==============================xavier正态分布初始化
tensor([[ 0.3084, -0.4327,  0.0733, -0.5733, -0.5567],
        [ 0.4315, -0.6243,  0.4632,  0.4075,  0.2278],
        [-0.6804, -0.6680,  0.6128,  0.6658, -0.4149]])
xavier均匀分布初始化==============================xavier均匀分布初始化
tensor([[-0.5401, -0.1223, -0.7067,  0.7691, -0.3456],
        [-0.4341,  0.1888,  0.0948,  0.5501,  0.4527],
        [ 0.4234, -0.3090, -0.6213, -0.7041, -0.5588]])

三、神经网络搭建和参数计算

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

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

3.1、神经网络模型

编码设计:

  • 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
  • 第2个隐藏层:权重初始化采用标准化的He初始化,激活函数采用relu
  • out输出层线性层,假若二分类,采用softmax做数据归一化
import torch  # 导入PyTorch主库
import torch.nn as nn  # 导入神经网络模块
from torchsummary import summary  # 导入模型汇总工具,用于显示模型结构

# 创建神经网络模型类 
class Model(nn.Module):
    # 初始化属性值
    def __init__(self):
        super(Model, self).__init__()  # 调用父类(nn.Module)的初始化方法
        self.linear1 = nn.Linear(3, 3)  # 创建第一个隐藏层,3个输入特征,3个输出特征
        nn.init.xavier_normal_(self.linear1.weight)  # 使用Xavier正态分布初始化权重
        
        # 创建第二个隐藏层,3个输入特征(上一层的输出特征),2个输出特征
        self.linear2 = nn.Linear(3, 2)
        nn.init.kaiming_normal_(self.linear2.weight)  # 使用Kaiming(He)正态分布初始化权重
        
        # 创建输出层,2个输入特征,2个输出特征
        self.out = nn.Linear(2, 2)
        # 注意:输出层没有特别初始化权重,使用了PyTorch默认初始化方法
    
    # 定义前向传播方法,PyTorch会自动调用forward()方法
    def forward(self, x):
        x = self.linear1(x)  # 输入数据经过第一个线性层
        x = torch.sigmoid(x)  # 第一层激活函数为sigmoid
        
        x = self.linear2(x)  # 数据经过第二个线性层
        x = torch.relu(x)  # 第二层激活函数为ReLU
        
        x = self.out(x)  # 数据经过输出层
        x = torch.softmax(x, dim=-1)  # 输出层使用softmax激活函数,在最后一个维度上计算
        return x

if __name__ == "__main__":
    my_model = Model()  # 实例化模型
    my_data = torch.randn(5, 3)  # 创建随机输入数据,5个样本,每个样本3个特征
    print("mydata shape", my_data.shape)  # 打印输入数据形状
    
    # 数据经过神经网络模型前向传播
    output = my_model(my_data)
    print("output shape-->", output.shape)  # 打印输出数据形状
    
    # 使用summary计算每层神经元的权重和偏置总数
    summary(my_model, input_size=(3,), batch_size=5)
    
    # 打印模型参数
    print("===========查看模型参数w和b=========")
    for name, parameter in my_model.named_parameters():
        print(name, parameter)  # 打印每一层的权重和偏置参数

输出结果:

3.2、神经网络的优缺点

优点
  • 精度高,性能优于其他的机器学习算法,甚至在某些领域超过了人类
  • 可以近似任意的非线性函数随之计算机硬件的发展
  • 近年来在学界和业界受到了热捧,有大量的框架和库可供调
缺点
  • 黑箱,很难解释模型是怎么工作的
  • 训练时间长,需要大量的计算资源
  • 网络结构复杂,需要调整超参数
  • 部分数据集上表现不佳,容易发生过拟合

四、损失函数

4.1、什么是损失函数

在深度学习中,损失函数是用来衡量模型参数的质量的函数,衡量的方式是比较网络输出和真实输出的差异

损失函数的不同命名方式

4.2、多分类损失函数

4.2.1、什么是多分类损失函数

专门用于处理多类别问题的损失函数,在这类问题中,模型需要从3个或更多个类别中预测出正确的类别,比如:

  • 识别手写数字(0-9共10个类别)
  • 图像分类(猫、狗、鸟、马等多个类别)
  • 新闻文章分类(体育、政治、科技、娱乐等)
  • 语言识别(英语、中文、法语等)
4.2.2、多分类损失函数的计算方法
1、分类交叉熵损失(Catagorical Cross-Entropy)

这是最常用的多分类损失函数,计算步骤:

  • 模型输出每个类别的得分或概率(通常经过softmax函数处理)
  • 将真实标签转换为"独热编码"(one-hot encoding)形式
  • 计算预测概率和真实标签之间的交叉熵

计算公式:L = -Σ(y_true * log(y_pred))

  • y_true是真实标签的独热编码(只有正确类别为1,其他为0)
  • y_pred是模型预测的各类别概率
  • log是自然对数
  • Σ表示对所有类别求和

符号“-”符号的作用是将结果变成负值,因为对数函数对于小于1的概率值会返回负数,通过符号可以将最终结果变为整数

  • 如果模型对正确类别给出高概率(接近1),损失会很小
  • 如果模型对正确类别给出低概率(接近0),损失会很大
  • 模型被鼓励对正确类别"充满信心"
4.2.3、二分类任务损失函数

在处理二分类任务时,我们不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数

其中:

1. y是样本x属于某一个类别的真实概率

2. 而y^是样本属于某一类别的预测概率

3. L用来衡量真实值y与预测值y^之间差异性的损失结果

4.2.4、回归任务损失函数-MAE损失函数

Mean absolute loss(MAE)也被称为L1 Loss,是以绝对误差作为距离。损失函数公式:

特点:

  • 由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束
  • L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值

4.2.4、回归任务损失函数-MSE损失函数

Mean Squared Loss/Quadratic Loss(MSE loss)被称为L2 loss,或欧式距离,以误差的平方和的均值作为距离损失函数:

特点:

L2 loss也常常作为正则项。

当预测值与目标值相差很大时, 梯度容易爆炸

4.2.5、回归任务损失函数-smooth L1损失函数

smooth L1说的是光滑之后的L1,损失函数公式

其中:𝑥=f(x)−y 为真实值和预测值的差值。

从下图中可以看出,该函数实际上就是一个分段函数

1. 在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题

2. 在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题

损失函数代码实现:

import torch
from torch import nn
def test1():
    # 1 设置真实值
    y_true = torch.tensor([1,2],dtype=torch.int64)
    #设置预测值,sigmoid输出的结果
    y_pred = torch.tensor([[0.2,0.6,0.2],[0.1,0.8,0.1]],dtype=torch.float32)

    #实例化交叉熵损失
    loss = nn.CrossEntropyLoss()
    #计算损失结果
    my_loss = loss(y_pred,y_true).numpy()
    print('多分类任务损失loss:',my_loss)
#二分任务
def test2():
    y_pred = torch.tensor([0.6901, 0.5459, 0.2469],requires_grad=True)
    y_true = torch.tensor([0,1,0],dtype=torch.float32)
    # 实例化二分类交叉熵损失
    criterion = nn.BCELoss()
    #计算损失
    my_loss = criterion(y_pred,y_true).detach().numpy()
    print('二分类任务损失loss:',my_loss)
# 计算算inputs与target之差的绝对值
def test3():
    #设置真实值和预测值
    y_pred= torch.tensor([1.0,1.0,1.9],requires_grad=True)
    y_true = torch.tensor([2.0,2.0,2.0],dtype=torch.float32)

    #实例MAE损失对象
    loss = nn.L1Loss()
    #计算损失
    my_loss = loss(y_pred,y_true).detach().numpy()
    print("MAE损失loss:",my_loss)

def test4():
     #设置真实值和预测值
    y_pred= torch.tensor([1.0,1.0,1.9],requires_grad=True)
    y_true = torch.tensor([2.0,2.0,2.0],dtype=torch.float32)

    #实例MSE损失对象
    loss = nn.MSELoss()
    #计算损失
    my_loss = loss(y_pred,y_true).detach().numpy()
    print('MSE损失loss:',my_loss)

def test5():
     #设置真实值和预测值
    y_pred= torch.tensor([0.6,0.4],requires_grad=True)
    y_true = torch.tensor([0,3])

    #实例MAE损失对象
    loss = nn.SmoothL1Loss()
    #计算损失
    my_loss = loss(y_pred,y_true).detach().numpy()
    print('smooth L1损失loss:',my_loss)

if __name__ == '__main__':
    test1()
    test2()
    test3()
    test4()
    test5()

输出结果:

五、网络优化方法

5.1、梯度下降算法

梯度下降发事一种寻找损失函数最小化的方法,从数据上的角度来看,梯度的方向就是函数增长速度最快的方向,梯度的反方向就是梯度减少最快的方向

η是学习率,r如果学习率太小,每次训练之后得到的效果都太小,增大训练的事件成本,如果学习率太大,就有可能直接跳过最优解,进入无限训练中,解决方法就是学习率也需要随着训练的进行而变化

基础概念:

  1. Epoch: 使用全部数据对模型进行以此完整训练,训练轮次
  2. Batch_size: 使用训练集中的小部分样本对模型权重进行以此反向传播的参数更新,每次训练每批次样本数量
  3. Iteration: 使用一个 Batch 数据对模型进行一次参数更新的过程

在深度学习中,梯度下降的几种方式的区别是Batch Size不同

注:上表中 Mini-Batch 的 Batch 个数为 N / B + 1 是针对未整除的情况。整除则是 N / B

5.2、反向传播(BP算法)

前向传播:指的是数据输入的神经网络中,逐层向前传输,一直运算到输出层为止

反向传播(Back Propagation): 利用损失函数ERROR,从后往前,结合梯度下降算法,依次求各个参数偏导,并进行参数更新

import torch 
from torch import nn
from torch import optim

#创建神经网络类
class Model(nn.Module):
    #初始化参数
    def __init__(self):
        #调用父类方法
        super(Model,self).__init__()
        #创建网络层
        self.linear1 = nn.Linear(2,2)
        self.linear2 = nn.Linear(2,2)
        #初始化神经网络参数
        self.linear1.weight.data = torch.tensor([[0.15,0.20],[0.25,0.30]])
        self.linear2.weight.data = torch.tensor([[0.40,0.45],[0.50,0.55]])
        self.linear1.bias.data = torch.tensor([0.35,0.35])
        self.linear2.bias.data = torch.tensor([0.60,0.60])
    #前向传播方法
    def forward(self,x):
        #数据经过第一层隐藏层
        x = self.linear1(x)
        # 计算第一层激活值
        x = torch.sigmoid(x)
        # 数据经过第二层隐藏层
        x = self.linear2(x)
        #计算按第二层激活值
        x = torch.sigmoid(x)
        return x
if __name__ == '__main__':
    #定义网络输入值和目标值
    inputs = torch.tensor([[0.05,0.10]])
    target = torch.tensor([[0.01,0.99]])

    #实例化神经网络对接
    model = Model()
    output = model(inputs)
    print("output-->",output)
    #计算误差
    loss = torch.sum((output-target)**2)/2
    print("loss->",loss)
    #优化方法和反向传播算法
    optimizer = optim.SGD(model.parameters(),lr=0.5)
    optimizer.zero_grad()
    loss.backward()
    print("w1,w2,w4->",model.linear1.weight.grad.data)
    print("w1,w6,w7,w8",model.linear2.weight.grad.data)
    optimizer.step()

    #打印神经网络参数
    print(model.state_dict())

输出结果:

梯度下降优化算法,会遇到的情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到 “鞍点” ,梯度为 0,参数无法优化
  • 碰到局部最小值,参数不是最优

5.3、梯度下降的优化方法

5.3.1、指数加权平均

指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)

• St 表示指数加权平均值;

• Yt 表示 t 时刻的值;

• β 调节权重系数,该值越大平均数越平缓

随机产生30天的平均气温数据:

1、实际平均温度

import torch
import matplotlib.pyplot as plt
Element_NUMBER = 30
#指数加权平均
#1 实际平均温度
def test01():
    #固定随机数种子
    torch.manual_seed(0)
    #产生30天的随机温度
    temperature = torch.randn(size=[Element_NUMBER,])*10
    print(temperature)
    #绘制平均温度
    days = torch.arange(1,Element_NUMBER+1,1)
    plt.plot(days,temperature,color='r')
    plt.scatter(days,temperature)
    plt.show()

输出结果:

指数加权平均温度

def test02(beta=0.9):
    torch.manual_seed(0) #固定随机数种子
    temperature = torch.randn(size=[Element_NUMBER,])*10 #产生30天的随机温度
    exp_weight_avg = []
    for idx,temp in enumerate(temperature,1):
        #第一个元素的EWA值等于自身
        if idx == 1:
            exp_weight_avg.append(temp)
            continue
        #第二各元素的EWA值等于等于上一个EWA乘以
        new_temp = exp_weight_avg[idx-2]*beta+(1-beta)*temp
        exp_weight_avg.append(new_temp)
    days = torch.arange(1,Element_NUMBER+1,1)
    plt.plot(days,exp_weight_avg,color='r')
    plt.scatter(days,temperature)
    plt.show()

β为0.9输出结果

β为0.5的结果:

由结果可知

指数加权平均绘制出的气温变化曲线更加平缓

β的值越大,则绘制出的折线越加平缓,波动越小

5.3.2、梯度下降的优化方法-动量算法Momentum

梯度计算公式:Dt = β * St-1 + (1- β) * Wt.

  • St-1 表示历史梯度移动加权平均值
  • Wt 表示当前时刻的梯度值
  • Dt 为当前时刻的指数加权平均梯度值
  • β 为权重系数

假设:权重 β 为 0.9,例如:

第一次梯度值:s1 = d1 = w1

第二次梯度值:d2=s2 = 0.9 * s1 + w2 * 0.1

第三次梯度值:d3=s3 = 0.9 * s2 + w3 * 0.1

第四次梯度值:d4=s4 = 0.9 * s3 + w4 * 0.1

梯度下降公式中梯度的计算,不再是当前时刻的t的梯度值,而是历史梯度值的指数移动加权平均值

W_t+1 = W_t - a * Dt

5.4、学习率衰减

5.4.1、等间隔学习率衰减

5.4.2、等间隔学习衰减率
import torch 
import matplotlib.pyplot as plt
from torch import optim

def test_StepLR():
    #0参数初始化,设置学习率初始化值
    LR = 0.1
    iteration = 10
    max_epoch = 200
    #1. 初始化参数
    y_true = torch.tensor([0])
    x = torch.tensor([1.0])
    w = torch.tensor([1.0],requires_grad=True)

    #优化器
    optimizer = optim.SGD([w],lr=LR,momentum=0.9)
    #设置学习率下降策略
    scheduler_lr = optim.lr_scheduler.StepLR(optimizer,step_size=50,gamma=0.5)
    #获取学习率的值和当前的epoch
    lr_list,epoch_list = list(),list()
    for epoch in range(max_epoch):
        lr_list.append(scheduler_lr.get_last_lr())#获取当前lr
        epoch_list.append(epoch)#获取当前的epoch
        for i in range(iteration):
            loss = ((w*x-y_true)**2)/2.0 #目标函数
            optimizer.zero_grad()
            #反向传播
            loss.backward()
            optimizer.step()

        #更新下一个epoch的学习率
        scheduler_lr.step()
    #绘制学习率变化的曲线
    plt.plot(epoch_list,lr_list,label="Step LR Scheduler")
    plt.xlabel("Epoch")
    plt.ylabel("Learning rate")
    plt.legend()
    plt.show()

if __name__ == '__main__':
    test_StepLR()

5.4.2、指定间隔学习衰减率

def test_multiStepLr():
    torch.manual_seed(1)
    LR = 0.1
    iteration = 10
    max_epoch = 200
    weights = torch.randn((1),requires_grad=True)
    target = torch.zeros((1))
    print ('weights-->',weights,'targee-->',target)
    optimizer = optim.SGD([weights],lr=LR,momentum=0.9)
    #设定调整时刻数
    milestones = [50,125,160]
    #设置学习率下降策略
    scheduler_lr =optim.lr_scheduler.MultiStepLR(optimizer,milestones=milestones,gamma=0.5)
    lr_list,epoch_list=list(),list()
    for epoch in range(max_epoch):
        lr_list.append(scheduler_lr.get_last_lr())
        epoch_list.append(epoch)
        for i in range(iteration):
            loss = torch.pow(weights-target,2)
            optimizer.zero_grad()
            #反向传播
            loss.backward()
            #参数更新
            optimizer.step()

        #更新下一个epoch学习率
        scheduler_lr.step()
    plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\nmilestones:{}".format(milestones))
    plt.xlabel("Epoch")
    plt.ylabel("Learning rate")
    plt.legend()
    plt.show()

输出结果:

六、正则化

在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化

目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊网络层优化等

6.1、Dropout正则化

在训练过程中,Droupout的实现是让神经元以超参数p的概率停止工作或者激活被置为0的进行缩放,缩放比例为1/(1-p)

import torch
import torch.nn as nn

def test():
    #初始化随机失活层
    droput = nn.Dropout(p=0.4)
    #初始化输入数据:表示某一层的weight信息
    inputs = torch.randint(0,10,size=[1,4]).float()
    layer = nn.Linear(4,5)
    y = layer(inputs)
    print("未失活FC层的输出结果:\n",y)
    y = droput(y)
    print("失活后FC层的输出结果:\n",y)

if __name__=='__main__':
    test()

输出结果:

6.2、批量归一化(BN层)

先对数据标准化,再对数据重构(缩放+平移),批量归一化层在计算机视觉领域使用较多

1. λ 和 β 是可学习的参数,它相当于对标准化后的值做了一个线性变换,λ 为系数,β 为偏置;
2. eps 通常指为 1e-5,避免分母为 0;
3. E(x) 表示变量的均值;
4. Var(x) 表示变量的方差;

七、案例-价格分类案例

7.1、需求分析

小明创办了一家手机公司,他不知道如何估算手机产品的价格。为了解决这个问题,他收集了多家公司的手机销售数据

。该数据为二手手机的各个性能的数据,最后根据这些性能得到4个价格区间,作为这些二手手机售出的价格区间。主要包括:

7.2、需求分析

我们需要帮助小明找出手机的功能(例如:RAM等)与其售价之间的某种关系。我们可以使用机器学习的方法来解决这个问题,也可以构建一个全连接的网络。

需要注意的是: 在这个问题中,我们不需要预测实际价格,而是一个价格范围,它的范围使用 0、1、2、3 来表示,所以该问题也是一个分类问题。接下来我们还是按照四个步骤来完成这个任务:

⚫ 准备训练集数据

⚫ 构建要使用的模型

⚫ 模型训练

⚫ 模型预测评估

7.3、构建数据集

数据共有 2000 条, 其中 1600 条数据作为训练集, 400 条数据用作测试集。 我们使用 sklearn 的数据集划分工作来完成。并使用 PyTorch 的 TensorDataset 来将数据集构建为 Dataset 对象,方便构造数据集加载对象。

def create_dataset():
    #使用pandas读取数据
    data = pd.read_csv('/Volumes/My Passport/学习资料/黑马大模型第三期/3期AI大模型配套资料/01阶段:配套资料/03/03-代码/02-神经网络/dataset/手机价格预测.csv',encoding="utf-8")
    #特征值和目标值
    x,y = data.iloc[:,:-1],data.iloc[:,-1]
    #类型转换,特征值,目标值
    x = x.astype(np.float32)
    y = y.astype(np.int64)
    #数据集划分
    x_train,x_valid,y_train,y_valid = train_test_split(x,y,train_size=0.8,random_state=88)
    # 构建数据集,转换pytorch的形式
    train_dataset = TensorDataset(torch.from_numpy(x_train.values),torch.tensor(y_train.values))
    valid_dataset = TensorDataset(torch.from_numpy(x_valid.values),torch.tensor(y_valid.values))

    #返回结果
    return train_dataset,valid_dataset,x_train.shape[1],len(np.unique(y))

获取数据结果:

   #获取数据
    train_dataset,valid_dataset,input_dim,class_num = create_dataset()
    print("输入特征数:",input_dim)
    print("分类个数:",class_num)

7.4、构建分类网络模型

由三个线性层来构建,使用relu激活函数:

1.第一层: 输入为维度为 20, 输出维度为: 128

2.第二层: 输入为维度为 128, 输出维度为: 256

3.第三层: 输入为维度为 256, 输出维度为: 4

#构建网络模型
class PhonePriceModel(nn.Module):
    def __init__(self,input_dim,output_dim):
        super(PhonePriceModel,self).__init__()
        #第一层,输入维度:20,输出维度128
        self.linear1 = nn.Linear(input_dim,128)
        #第二层:输入维度128,输出维度:256
        self.linear2 = nn.Linear(128,256)
        #第三层:输入维度256,输出维度4
        self.linear3 = nn.Linear(256,output_dim)
    
    def forward(self,x):
        #前向传播过程
        x = torch.relu(self.linear1(x))
        x = torch.relu(self.linear2(x))
        output = self.linear3(x)
        return output

模型实例化

  #模型实例化
    model = PhonePriceModel(input_dim,class_num)
    summary(model,input_size=(input_dim,),batch_size=16)

7.5、模型训练

指的是输入数据读取,送入网络,计算损失,更新参数的流程,该流程较为固定,使用的是多分类交叉生损失函数,使用SGD优化方法,最终将训练好的模型持久化到磁盘中

#模型训练
def train(train_dataset,input_dim,class_num,):
    #固定随机种子
    torch.manual_seed(0)
    #初始化模型
    model = PhonePriceModel(input_dim,class_num)
    #损失函数
    criterion = nn.CrossEntropyLoss()
    #优化方法
    optimizer = optim.SGD(model.parameters(),lr=1e-3)
    #训练轮数
    num_epoch = 50
    #遍历每个轮次的数据
    for epoch_idx in range(num_epoch):
        #初始化数据加载器
        dataloader = DataLoader(train_dataset,shuffle=True,batch_size=8)
        #训练时间
        start = time.time()
        #计算损失
        total_loss = 0.0
        total_num = 1
        for x,y in dataloader:
            #将数据送入网络中进行预测
            output = model(x)
            #计算损失
            loss = criterion(output,y)
            #梯度归零
            optimizer.zero_grad()
            #反向传播
            loss.backward()
            #参数更新
            optimizer.step()
            #损失计算
            total_num += 1
            total_loss += loss.item()
        #打印损失变换结果
        print('epoch: %4s loss: %.2f,time: %.2fs' % (epoch_idx+1,total_loss/total_loss/total_num,time.time()-start))

    torch.save(model.state_dict(),'/Users/zsz/workspace-py/pyTorch/model/phone.pth')

训练结果如下:

7.6、编写评估函数

使用训练好的模型,对未知的样本进行预测的过程

def test(valid_dataset,input_dim,class_num):
    #加载
    model = PhonePriceModel(input_dim,class_num)
    model.load_state_dict(torch.load('/Users/zsz/workspace-py/pyTorch/model/phone.pth'))
    #构建加速器
    dataloader = DataLoader(valid_dataset,batch_size=8,shuffle=False)
    #评估测试集
    correct = 0
    for x,y in dataloader:
        #将其送入网络中
        output = model(x)
        #获取类别结果
        y_pred = torch.argmax(output,dim=1)
        #获取预测正确的个数
        correct  += (y_pred == y).sum()
        #求预测正确的个数
    print('Acc: %.5f' % (correct.item()/len(valid_dataset)))

八、优化方案

8.1、数据预处理优化

添加了数据标准化处理(StandardScaler),使模型训练更稳定

8.2、模型结构优化
  • 使用Sequential模块重构了网络结构,使代码更简洁
  • 增加了网络深度,从3层扩展到5层(输入层→128→256→512→256→输出层)
  • 添加了BatchNormalization层,加速训练并提高稳定性
  • 增加了Dropout层,防止过拟合
  • 丰富了每个网络层之间的连接,提高了模型的表达能力
8.3、训练过程优化
  • 将优化器从SGD更改为Adam,提高训练效率
  • 学习率从1e-3降低到1e-4,使训练更稳定
  • 添加了权重衰减(weight_decay)参数,减少过拟合
  • 增加了学习率调度器(ReduceLROnPlateau),根据验证损失动态调整学习率
  • 训练轮数从50增加到100,给模型更多学习时间
  • 添加了训练损失曲线可视化
  • 每10轮在验证集上评估一次模型性能
8.4、评估方法优化
  • 新增单独的验证函数(validate),用于定期评估模型性能
  • 强了测试函数(test),记录所有预测结果,为后续详细分析做准备
  • 更准确地计算批次损失,考虑了批次大小
8.5、代码实现
import torch
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.optim as optim
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import time
from torchsummary import summary
#构建数据集
def create_dataset():
    #使用pandas读取数据
    data = pd.read_csv('/Volumes/My Passport/学习资料/黑马大模型第三期/3期AI大模型配套资料/01阶段:配套资料/03/03-代码/02-神经网络/dataset/手机价格预测.csv',encoding="utf-8")
    #特征值和目标值,x特征值取所有行,到倒数第2列,y目标值所有行的最后一列
    x,y = data.iloc[:,:-1],data.iloc[:,-1]
    #类型转换,特征值,目标值
    x = x.astype(np.float32)
    y = y.astype(np.int64)
    #数据集划分,80%训练数据,20%的验证数据,固定随机种子为88
    x_train,x_valid,y_train,y_valid = train_test_split(x,y,train_size=0.8,random_state=88)

    #数据标准化处理
    scaler = StandardScaler()
    x_train_scaled = scaler.fit_transform(x_train)
    x_valid_scaled = scaler.transform(x_valid)

    #

    # 构建数据集,转换pytorch的形式
    train_dataset = TensorDataset(torch.from_numpy(x_train_scaled),torch.tensor(y_train.values))
    valid_dataset = TensorDataset(torch.from_numpy(x_valid_scaled),torch.tensor(y_valid.values))

    #返回结果
    return train_dataset,valid_dataset,x_train.shape[1],len(np.unique(y))

#构建网络模型
class PhonePriceModel(nn.Module):
    def __init__(self,input_dim,output_dim):
       super(PhonePriceModel,self).__init__()
       #构建更深的网络结构
       self.model = nn.Sequential(
           
             #第一层,输入维度,input_dim,输出维度:128
           nn.Linear(input_dim,128),
           nn.ReLU(),
           nn.BatchNorm1d(128),
           nn.Dropout(0.2),

           #第二层,输入维度:128,输出维度:256
           nn.Linear(128,256),
           nn.ReLU(),
           nn.BatchNorm1d(256),
           nn.Dropout(0.3),

           #第三层,输入维度:256,输出维度512
           nn.Linear(256,512),
           nn.ReLU(),
           nn.BatchNorm1d(512),
           nn.Dropout(0.3),

           #第四层,输入维度512,输出维度256
           nn.Linear(512,256),
           nn.ReLU(),
           nn.BatchNorm1d(256),
           nn.Dropout(0.2),

           #输出层,输入维度:256,输出维度:output_dim(类别数)
           nn.Linear(256,output_dim)
       )
           
    
    def forward(self,x):
        """
        前向传播过程
        
        Args:
            x: 输入特征张量
        
        Returns:
            输出预测结果
        """
    
        return self.model(x)

#模型训练
def train(train_dataset,input_dim,class_num,):
    #固定随机种子
    torch.manual_seed(0)
    #初始化模型
    model = PhonePriceModel(input_dim,class_num)
    #使用交叉熵损失函数
    criterion = nn.CrossEntropyLoss()
    
    #优化方法从SGD改为Adam,学习率从le-3调整为le-4
    optimizer = optim.Adam(model.parameters(),lr=1e-4,weight_decay=1e-5)

    #增加学习率调度器
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer,'min',patience=5,factor=0.5,verbose=True)
    #训练轮数从50增加到100
    num_epoch = 100

    #记录训练过程中的损失
    train_losses = []
    #遍历每个轮次的数据
    for epoch_idx in range(num_epoch):
        #初始化数据加载器
        dataloader = DataLoader(train_dataset,shuffle=True,batch_size=8)
        #训练时间
        start = time.time()
        #设置模型为训练模式
        model.train()

        #计算损失
        total_loss = 0.0
        total_samples = 0
        for x,y in dataloader:
            #将数据送入网络中进行预测
            output = model(x)
            #计算损失
            loss = criterion(output,y)
            #梯度归零
            optimizer.zero_grad()
            #反向传播
            loss.backward()
            #参数更新
            optimizer.step()
            #累加损失
            batch_size = x.size(0)
            total_samples += batch_size
            total_loss += loss.item()*batch_size

        #计算平均损失
        epoch_loss = total_loss / total_samples
        train_losses.append(epoch_loss)
        #打印训练信息
        print(f'Epoch: {epoch_idx+1:4d} | Loss: {epoch_loss:.4f} | Time: {time.time()-start:.2f}s | LR: {optimizer.param_groups[0]["lr"]:.6f}')

        #每10轮进行一次验证
        if(epoch_idx+1)%10==0:
            validate(model,valid_dataset,criterion)
    torch.save(model.state_dict(),'/Users/zsz/workspace-py/pyTorch/model/phone2.pth')

    #绘制损失曲线
    plt.figure(figsize=(10,5))
    plt.plot(train_losses)
    plt.title('Training Loss Over Epochs')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.grid(True)
    plt.savefig('/Users/zsz/workspace-py/pyTorch/model/training_loss.png')
    plt.show()

#验证函数
def validate(model,valid_dataset,criterion):
    """
    在验证集上评估模型性能
    Args:
        model: 训练的模型
        valid_dataset:验证数据集
        criterion:损失函数
    """
    #设置为评估模式
    model.eval()
    #构建数据加载器
    dataloader = DataLoader(valid_dataset,batch_size=8,shuffle=False)

    #初始化评估指标
    correct = 0
    total_samples = 0
    valid_loss = 0.0

    #禁用梯度计算
    with torch.no_grad():
        for x,y in dataloader:
            #向前传播
            outputs = model(x)
            #计算损失
            loss = criterion(outputs,y)
            #获取预测结果
            _,predicted = torch.max(outputs,1)
            #统计样本和正确预测数
            batch_size = y.size(0)
            total_samples += batch_size
            correct += (predicted==y).sum().item()
            valid_loss += loss.item() * batch_size
    #计算准确率和平均损失
    accuracy = correct / total_samples
    avg_loss = valid_loss / total_samples

    print(f'Validation - Loss: {avg_loss:.4f} | Accuracy: {accuracy:.4f}')

#测试函数    
def test(valid_dataset,input_dim,class_num):
    """
    测试模型在验证集上的性能
    
    Args:
        valid_dataset: 验证数据集
        input_dim: 输入特征的维度
        class_num: 分类的类别数
    """
    #加载
    model = PhonePriceModel(input_dim,class_num)
    model.load_state_dict(torch.load('/Users/zsz/workspace-py/pyTorch/model/phone2.pth'))
    #设置模型
    model.eval()

    #构建加速器
    dataloader = DataLoader(valid_dataset,batch_size=8,shuffle=False)
    #评估测试集
    correct = 0
    total = 0
    all_predictions = []
    all_targets = []

    #禁用梯度计算
    with torch.no_grad():
        for x,y in dataloader:
            #前向转播
            outputs = model(x)

            #获取预测类别
            _,predicted = torch.max(outputs,1)

            #记录预测结果
            all_predictions.extend(predicted.numpy())
            all_targets.extend(y.numpy())

            #统计准确率
            total += y.size(0)
            correct += y.size(0)
            correct += (predicted==y).sum().item()

    accuracy = correct/total
    print(f'Test Accuracy:{accuracy:.5f}')

if __name__ == '__main__':
    #获取数据
    train_dataset,valid_dataset,input_dim,class_num = create_dataset()
    print("输入特征数:",input_dim)
    print("分类个数:",class_num)
    
    #模型实例化
    model = PhonePriceModel(input_dim,class_num)
    #打印模型结构
    summary(model,input_size=(input_dim,),batch_size=16)

    #模型训练
    train(train_dataset,input_dim,class_num)

    #模型预测
    test(valid_dataset,input_dim,class_num)

输出结果:

右图可知,优化后,精度由0.59750提升到0.825,优化效果明显

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值