«

Python深度学习入门-day03-DNN

ZealSinger 发布于 阅读:29


前言

在学习PyTorch的时候,我们首先需要将其与NumPy相联系,还是之前NumPy中提到过的,PyTorch中很多概念直接由NumPy中迁移对比理解就行

然后我们需要了解GPU存储张量/数据,如下,我们按照之前的方式创建一个阶张量并且打印

import torch  
ts1 = torch.randn(3,4)  
print(ts1)

输出
tensor([[ 1.3641, -0.3565, -1.1902,  0.4284],
        [ 0.6414,  0.0928,  0.7420,  0.0064],
        [ 1.3765, -0.8911, -0.5673, -1.7459]])

一切运行都没问题,但是默认上所有的输出都是在CPU上进行的,我们的深度学习要利用GPU,所以我们必须让GPU存储张量,这个时候我们需要用到data.to("cuda:0"),使用这个就可以让data存储到对应的0号GPU上(GPU有几号取决于你的设备,通过nvidia-smi查看有几个,有多个的可以cuda:1)

import torch  
ts1 = torch.randn(3,4).to("cuda")  
print(ts1)

同样的,我们在后续的学习中也需要把神经网络的模型也要搬到GPU上才可以正常运行,所以需要使用如下代码

class DNN(torch.nn.Module):
    ...略...

model = DNN().to("cuda:0")

所以这个to("cuda:0")很常用,大家需要记住和学会使用

DNN原理篇

DNN就是深度神经网络,其核心就是:通过学习大量样本的输入和输出特征之间的关系,以拟合出输入和输出之间的方程,学习完毕后,能达到你给输出特征,他给出输出特征。神经网络可以分为如下几个步骤:划分数据集,训练网络,测试网络,使用网络

划分数据集

首先,数据集是一个包含多个样本的集合,每个样本都必须要有输入和输出,将数据集按照一定比例划分为训练集和测试集,分别用于训练网络和测试网络(简单理解:数据集=训练集+测试集)
如下数据集,1000个样本,三个输入,三个输出,按照8:2的比例划分
image.png

神经节点是根据数据集来的,因为输入和输出都是三个,那么肯定输入和输出都会需要三个神经元,即输入神经元必须和每个样本的输入特征数量一致,输出层必须和每个样本的输出特征数量一致。至于中间的隐藏层可以自行设计
如下图,是可以适用于上面数据集的神经网络结构,因为在编程语言中索引都是从0开始的,所以我们神经网络结构图也从输入层开始称之为第0层,依次往后就是第1层,第2层...
image.png

训练网络

神经网络的训练过程就是经过很多次前向传播和反向传播的轮回,最终不断调整其内部参数(权重w和偏置b)以拟合任意复杂函数的过程,内部参数一开始是随机的(例如Xavier初始值,He初始值),但是最终会被不断优化到最佳,神经网络的最终目的就是为了优化内部参数,仅此一个目标而已

当然,还有一些训练网络在开始之前需要设置好一些外部参数,例如网络的层数,每个隐藏层的节点数,每个节点的激活函数类型,学习率,轮回次数,每次轮回的样本数等等,业界习惯把内部参数称之为参数,外部参数称之为超参数

前向传播

所谓前向传播就是:将输入特征送入到神经网络输出层后,神经网络逐层计算最终到输出层,简单理解就是从输入到输出的一个过程就称之为前向传播

计算过程中所使用的参数就是内部参数,所有的隐藏层与输出层的神经元都有内部参数,输入层是没有参数的
假设现在我们要将输入进入到第一层的第一个神经元上进行计算,该神经元节点的计算过程为y=w1*x1+w2*x2+w3*x3+b。你可以理解为,每一根线就是一个权重 ω,每一个神经元节点也都有它自己的偏置 b。
image.png

因为按照上面的过程得到每个神经元节点的函数都是线性的方程,所以必须在计算完成后套一个非线性的函数,也称之为激活函数
image.png

为什么需要一个非线性的激活函数呢?这个很好理解,训练网络就是来回训练,你全部都是线性计算那么其实根本不需要来回训练,训练一次其实就拟合出来了。如下,一个比较简单的四层神经网络,反向来看,y=w5 z5+w6 z6 +d,然后依次把z5和z6逐层拆分,那么在最后肯定是得到一个关于X的线性方程即ax+b这样的形式,如果有多个输入,也无非就是多加了几个元,即y=a1x1+a2x2+b,所以激活函数是必备的
image.png

反向传播

经过前向传播,网络会根据当前的内部参数计算出一个输出特征的预测值,但是这个预测值和真实值之间肯定会存在很大的差距。首先我们会需要一个损失函数来计算这个差距(例如求预测值和真实值之间的差的绝对值,这个就是一个典型的损失函数)

损失函数计算完之后,逐层退回求梯度(这个过程涉及到了比较复杂的数学理论,目前先可以不需要掌握。这个过程的逻辑大概就是看看每个内部参数变大还是变小,能减少损失函数值,也就是调整内部参数以减少损失函数值),从而达到优化内部参数的目的

在这个过程中,有一个外部参数称之为学习率,学习率越大,内部参数优化越快,但是不能过大,过大的学习率会导致损失函数越过最低点在谷底反复横跳,因此在训练开始前选择一个合适的学习率很重要(可以这么理解,想象一个先下降后上升的函数,在这个过程中我们的损失函数是越来越低的,但是会存在一个最低点,学习率越大,下降的越快,但是可能下降的过多导致超过了这个最低点,最低点后又会变大了,变大后就得逆向调整,但是因为每次变化都比较大,就是达到不到那个最低点,从而导致反复横跳)

batch_size

前向传播和反向传播一次时,有如下三种情况

epochs

一个epochs就是指全部数据完成了一次前向传播和后向传播
为了帮助理解,可以看一下如下的说明
假设样本一共有10240个,batch_size=1024,epochs=5,这个情况说明

  1. 1个epochs需要10次(10240/1024)前向传播和后向传播
  2. 全部样本都将进行5次前向传播和后向传播
  3. 整个过程经过了(10* 5)次前向传播和后向传播

测试网络

测试网络的目的主要是为了防止训练过拟合,所谓的过拟合就是,训练过头,网络优化后的内部参数只针对于训练样本有效,对于别的样本无效。(可以理解为原本要训练成一个普遍适用的函数,结果训练过头成为了训练样本的特例函数)所以网络训练好后,需要拿测试集作为输入,进行一次前向传播后,将结果与测试集的真实输出进行对比查看准确率
image.png

使用网络

真正使用网络进行预测时,样本只知输入,不知输出。直接将样本的输入进行 1 次前向传播,即可得到预测的输出。

DNN的实现

在Python的第三方库中,torch.nn提供了搭建网络所需要的所有组件,所以我们可以映入库且为了方便别名nn

import torch.nn as nn

按照之前原理篇内容,我们知道DNN的四个步骤,其中第一个步骤就是划分数据集

那么我们首先来准备数据集,在这里我们生成10000个样本,每个样本设定三个输入特征(X1,X2,X3)和三个输出特征(Y1,Y2,Y3),我们规定

import torch.nn as nn  
import torch  

# 生成三个输入特征,rand生成1000行1列的范围在[0,1]的张量  
X1 = torch.rand(1000,1)  
X2 = torch.rand(1000,1)  
X3 = torch.rand(1000,1)  

# 生成输出特征, .float()将布尔张量转化为浮点型张量  
Y1 = ( (X1+X2+X3)<1 ).float() # 输出特征 1
Y2 = ( (1<(X1+X2+X3)) & ((X1+X2+X3)<2) ).float() # 输出特征 2
Y3 = ( (X1+X2+X3)>2 ).float() # 输出特征 3  
data = torch.cat([X1,X2,X3,Y1,Y2,Y3],axis=1) # 整合数据集  
data = data.to('cuda:0')  # 迁入到GPU
print(data.shape) # torch.Size([1000, 6])

数据集准备完毕后,我们需要划分数据集为测试集和样本集

# 划分数据集大小 我们采用7:3  
train_size = int(len(data)*0.7)  
test_size = len(data)-train_size  
# 打乱数据  data.size(0)获取第0维度即第一列的长度 
# arr.randperm(n)打乱arr上0~n-1索引内的范围
data = data[torch.randperm(data.size(0)),:]  
train_data = data[0:train_size,:] # 训练集  
test_data = data[train_size:len(data)] # 测试集  
print(train_data.shape, test_data.shape,sep="##")  # 打印查看是否正确划分

这部分基本属于通用代码,以后如果需要都可以直接拿着用


数据都准备好了,接下就是需要构建网络,一般在搭建自己的神经网络的时候,可以以nn.Module为父类,该对象中包含了网络各个层的定义

在定义的网络子类中,通常包含__init__构造方法forward方法,其中init方法用于构造自己的神经网络结构,forward方法用于将输入数据进行前向传播。由于张量可以自动计算梯度,所以不需要出现反向传播方法。

class MyDNN(nn.Module):  
    def __init__(self):  
        ''' 搭建神经网络各个层 '''        
        super(MyDNN,self).__init__() 
        #Sequential() 可以理解为顺序队列,在里面按顺序定义各个隐藏层,不要把输入层定义在里面 
        self.net = nn.Sequential( 
        # 隐藏层也称之为线形层所以我们这方法也就是.Linear(preNodeNumber,selfNodeNumber)  
        # 两个参数是 上一层节点数和本层节点数  
        # 因为这是第一层隐藏层,所以上一层就是输入层,我们是三个输入特征,所以第一个参数为3  
            nn.Linear(3,5),  
            # 激活函数 激活函数一般有ReLU tanh sigmoid,一般选ReLU  
            nn.ReLU(),  
            # 再来一个隐藏层 只不过这个隐藏层的输入是上一层的输出,所以第一个参数为5  
            nn.Linear(5,5),  
            nn.ReLU(),  
            nn.Linear(5,5),  
            nn.ReLU(),  
            nn.Linear(5,3)  
        )  

    def forward(self,x):  
        '''  前向传播 '''        # net就是上面定义的顺序队列,上面叫啥这里就调用啥  
        y = self.net(x)  
        return y # y即输出数据

# 创建MyDNN实例对象且放入GPU,打印查看一下
dnn = MyDNN().to('cuda:0')
print(dnn)

输出
MyDNN(
  (net): Sequential(
    (0): Linear(in_features=3, out_features=5, bias=True)
    (1): ReLU()
    (2): Linear(in_features=5, out_features=5, bias=True)
    (3): ReLU()
    (4): Linear(in_features=5, out_features=5, bias=True)
    (5): ReLU()
    (6): Linear(in_features=5, out_features=3, bias=True)
  )
)

我们构建了网络,但是有没有发现好像并没有设计到内部参数即权重和偏置,其内部参数其实在神经网络训练前会被赋予随机数,且随着训练会逐渐迭代至最佳值
我们可以通过DNN对象.named_parameters()+for循环进行查看

for name,param in dnn.named_paramaeters():
    print(f"参数:{name}\n 形状:{param.shape}\n 数值:{param}\n")

可以看到遍历了所有的神经元节点,展示了每个节点的权重w和偏置b,可以看到w是[5,3]那么也就是一共15根线,每一根线是不同的权重,这个和我们上面的那个图是一致的;b是[5],而我们的第一层隐藏层也是五个节点,也就是每个节点都有自己的偏置b,初始值都是随机的
而且可以看到,有权重和偏置的是0,2,4,6这几个节点,这是因为1,3,5都是激活函数;所有的节点后面都是device="cuda:0"说明都在GPU上 ; 所有的requires_grad=True是说明是否需要计算梯度,这里都是True说明需要进行反向传播的内部参数都打开了张量自带的梯度计算功能
image.png


我们再来看一下外部参数,外部参数是调参师/炼丹师!关注的重点,包括但不限于网络层数;各隐藏层节点数;各节点激活函数;内部参数的初始值和训练网络有关的超参数有损失函数,学习率,优化算法,batch_size,epochs

对于激活函数,我们可以访问访问对应版本的文档torch.nn — PyTorch 2.1 documentation查看PyTorch内置的激活函数,有很多而且点对应的函数可以看到其数学公式和函数图像,我们这里使用的就是ReLu()

对于损失函数,可以访问对应版本的torch.nn — PyTorch 2.1 documentation,也有很多

loss_fn = nn.MSELoss()

对于学习率和优化算法可以访问torch.optim — PyTorch 2.10 documentation,这里面比较出名的就是SGD,Adam,可以按照他的实例进行使用,lr参数就是学习率,需要注意的是,PyTorch实现时只支持BGD和MBGD,不支持单个样本的输入方式,我们下面案例代码中torch.optim.SGD只代表梯度下降

learning_rate = 0.01 #学习率
optimizer = torch.optim.SGD(dnn.parameters(),lr=learing_rate)# 优化算法

image.png


都准备好了,接下来就可以训练网络了。
训练网络就是一个来回进行前向传播和反向传播的过程,所以最外层我们就是一个循环
然后使用dnn(输入特征)可以得到预期值Pred
然后有了预期值就需要和真实值计算误差函数loss = loss_fn(预期值Pred,真实输出特征Y) 这里的loss_fn是前面定义的损失函数名
每一轮梯度都会不一样,为了不受前面的影响,需要记得清除上一轮滞留的梯度optimizer.zero_grad(),其中optimizer是优化算法对象
之后进行反向传播loss.backward()loss是上面误差函数调用后的返回值
最后利用优化算法对象进行一次内部参数优化optimizer.step()

# 训练网络  
epochs = 1000 # 所有样本都会轮回1000次  
losses = [] # 用于记录损失函数变化的列表  

# 给训练集划分输入和输出(在我们当前案例中前三列为输入,后三列为输出)  
X = train_data[:,:3]  
Y = train_data[:,-3:]  
# 开始循环进行训练  
for epoch in range(epochs):  
    Pred = dnn(X) # 一次前向传播BGD  
    loss = loss_fn(Pred, Y) # 计算损失函数  
    # 保存记录损失值  
    # loss.item()方法可以将损失函数降级为一个元素  
    # 因为loss是一个张量,张量不能直接存储在list中,所以这里要先降级  
    losses.append(loss.item())  
    optimizer.zero_grad() # 每一轮梯度都不一样 消除上一轮滞留的梯度  
    loss.backward() # 一次反向传播  
    optimizer.step() # 优化内部参数  

Fig = plt.figure()  
plt.plot(range(epochs),losses)  
plt.xlabel('epochs')  
plt.ylabel('loss')  
plt.show()

可以看到训练后损失函数越来越少
image.png


最后是测试网络
测试时只需要让测试集进行一次向前传播即可,不需要计算梯度,所以可以局部关闭梯度,该操作使用with torch.no_grad()命令实现
考虑到输出特征时独热编码,而预测的数据一般都接近0/1的小数,为了让预测数据和真实数据之间进行比较,需要对预测数据进行规整。除此之外还要计算预测值正确总数从而计算占比

# 测试网络  
# 将测试集的输入特征和输出特征分开  
X = train_data[:,:3]  
Y = train_data[:,-3:]  
# 进行一次前向传播  
# 局部关闭梯度计算功能 这个部分的代码块不会进行梯度计算  
with torch.no_grad():  
    Pred = dnn(X) # 前向传播一次  
    # 输出特征是独热编码,预测数据一般都是0~1内的小数,需要进行格式规整  
    # 预测结果Pred中,每行样本都是三个输出,最大的那个也就是最有可能的,将其设置为1,其余的设置为0  
    # 这样子Pred就和真实结果集格式一致  
    Pred[:,torch.argmax(Pred,axis=1)]=1  
    Pred[Pred!=1]=0  
    # 计算测试集的准确度  
    # Pred==Y 让预测结果和真实结果比较 得到3000行3列布尔类型的张量  
    # .all()方法会扫描只有全部为True的时候才会返回True (1)代表按行扫描 得到一个3000行1列的张量  
    # torch.sum就是将3000行相加,True为1False为0,所以最终结果就是预测值==真实值的正确样本总数  
    correct = torch.sum((Pred==Y).all(1)) # 预测结果正确的样本  

    # 正常数组shape(行,列,...) 类比到张量就是 第0维就是对应行,第1维对应列  
    # 可以理解为shape(0维,1维,2维)  
    # size(维度)是张量特有的,numpy中没有  
    total = Y.size(0) # 第一个维度上的值也就是3000  
    print(f'测试集精准度:{100*correct/total}%')

归纳:到目前为止,我们已经初步完成了一个DNN的入门代码案例,整个代码内容和运行结果如下

import torch.nn as nn  
import torch  
from matplotlib import pyplot as plt  

class MyDNN(nn.Module):  
    def __init__(self):  
        ''' 搭建神经网络各个层 '''  
        #Sequential() 可以理解为顺序队列,在里面按顺序定义各个隐藏层,不要把输入层定义在里面 
        super(MyDNN,self).__init__()  
        self.net = nn.Sequential( 
        # 隐藏层也称之为线形层 所以我们这方法也就是.Linear(preNodeNumber,selfNodeNumber) 
        # 两个参数是 上一层节点数和本层节点数  
        # 因为这是第一层隐藏层,所以上一层就是输入层,我们是三个输入特征,所以第一个参数为3  
            nn.Linear(3,5),  
        # 激活函数 激活函数一般有ReLU tanh sigmoid,一般选ReLU  
            nn.ReLU(),  
        # 再来一个隐藏层 只不过这个隐藏层的输入是上一层的输出,所以第一个参数为5  
            nn.Linear(5,5),  
            nn.ReLU(),  
            nn.Linear(5,5),  
            nn.ReLU(),  
            nn.Linear(5,3)  
        )  

    def forward(self,x):  
        '''  前向传播 '''        
        # net就是上面定义的顺序队列,上面叫啥这里就调用啥  
        y = self.net(x)  
        return y # y即输出数据  

dnn = MyDNN().to('cuda:0') # 放入GPU  
loss_fn = nn.MSELoss() # 损失函数  
learing_rate = 0.01 # 学习率  
optimizer = torch.optim.SGD(dnn.parameters(),lr=learing_rate) # 优化算法  

# 生成三个输入特征,rand生成1000行1列的范围在[0,1]的张量  
X1 = torch.rand(1000,1)  
X2 = torch.rand(1000,1)  
X3 = torch.rand(1000,1)  

# 生成输出特征, .float()将布尔张量转化为浮点型张量  
Y1 = ( (X1+X2+X3)<1 ).float() # 输出特征 1
Y2 = ( (1<(X1+X2+X3)) & ((X1+X2+X3)<2) ).float() # 输出特征 2
Y3 = ( (X1+X2+X3)>2 ).float() # 输出特征 3  
data = torch.cat([X1,X2,X3,Y1,Y2,Y3],axis=1) # 整合数据集  
data = data.to('cuda:0')  
print(data.shape) # torch.Size([1000, 6])  

# 划分数据集大小 我们采用7:3  
train_size = int(len(data)*0.7)  
test_size = len(data)-train_size  
# 打乱数据  
data = data[torch.randperm(data.size(0)),:]  
train_data = data[0:train_size,:] # 训练集  
test_data = data[train_size:len(data)] # 测试集  
print(train_data.shape, test_data.shape,sep="##")  # 打印查看是否正确划分  

# 训练网络  
epochs = 1000 # 所有样本都会轮回1000次  
losses = [] # 用于记录损失函数变化的列表  

# 给训练集划分输入和输出(在我们当前案例中前三列为输入,后三列为输出)  
X = train_data[:,:3]  
Y = train_data[:,-3:]  
# 开始循环进行训练  
for epoch in range(epochs):  
    Pred = dnn(X) # 一次前向传播BGD  
    loss = loss_fn(Pred, Y) # 计算损失函数  
    # 保存记录损失值  
    # loss.item()方法可以将损失函数降级为一个元素  
    # 因为loss是一个张量,张量不能直接存储在list中,所以这里要先降级  
    losses.append(loss.item())  
    optimizer.zero_grad() # 每一轮梯度都不一样 消除上一轮滞留的梯度  
    loss.backward() # 一次反向传播  
    optimizer.step() # 优化内部参数  

Fig = plt.figure()  
plt.plot(range(epochs),losses)  
plt.xlabel('epochs')  
plt.ylabel('loss')  
plt.show()  

# 测试网络  
# 将测试集的输入特征和输出特征分开  
X = train_data[:,:3]  
Y = train_data[:,-3:]  
# 进行一次前向传播  
# 局部关闭梯度计算功能 这个部分的代码块不会进行梯度计算  
with torch.no_grad():  
    Pred = dnn(X) # 前向传播一次  
# 输出特征是独热编码,预测数据一般都是0~1内的小数,需要进行格式规整  
# 预测结果Pred中,每行样本都是三个输出,最大的那个也就是最有可能的,将其设置为1,其余的设置为0  
# 这样子Pred就和真实结果集格式一致  
    Pred[:,torch.argmax(Pred,axis=1)]=1  
    Pred[Pred!=1]=0  
# 计算测试集的准确度  
# Pred==Y 让预测结果和真实结果比较 得到3000行3列布尔类型的张量  
# .all()方法会扫描只有全部为True的时候才会返回True (1)代表按行扫描 得到一个3000行1列的张量  
# torch.sum就是将3000行相加,True为1False为0,所以最终结果就是预测值==真实值的正确样本总数  
    correct = torch.sum((Pred==Y).all(1)) # 预测结果正确的样本  

# 正常数组shape(行,列,...) 类比到张量就是 第0维就是对应行,第1维对应列  
# 可以理解为shape(0维,1维,2维)  
# size(维度)是张量特有的,numpy中没有  
    total = Y.size(0) # 第一个维度上的值也就是3000  
    print(f'测试集精准度:{100*correct/total}%')

image.png

网络保存与导入

有时候训练一个大网络需要几天,那么必须要把整个网络连同里面的优化好的内部参数给保存下来下次使用或者别的设备使用,这里就需要用到网络的保存与导入
网络的保存使用torch.save(dnn,文件名.pth);网络的导入使用torch.load(文件名)(load方法有个weights_only 参数决定安全策略,低版本中默认为False直接使用没问题,但是高版本中默认值从 False 改为 True,所以高版本中要使用需要指定一下torch.load(文件名,weights_only=False)
我们继续拿上面的案例尝试一下保存和导入,然后进行测试,测试准确率一致

torch.save(dnn,'MyFirstDNN.pth')
newDnn = torch.load('MyFirstDNN.pth',weights_only=False)  
# 用导入的newDnn进行测试  
with torch.no_grad():  
    Pred = newDnn(X)  
    Pred[:,torch.argmax(Pred,axis=1)]=1  
    Pred[Pred!=1]=0  
    correct = torch.sum((Pred==Y).all(1))  
    total = Y.size(0)  
    print(f'newDnn测试集精准度:{100*correct/total}%')

image.png

批量梯度下降

上面的DNN的相关实现的案例其实就是一种批量梯度下降,我们暂且先不用管批量梯度下降的具体含义和定义。接下来,我们使用一个已有的数据进行实战
我们这里有一份Data.xlsx的关于糖尿病相关的数据文件
image.png
对此我们进行依次批量梯度下降的深度神经网络学习的代码编写

import numpy as np  
import pandas as pd  
import torch.nn as nn  
import torch  
from matplotlib import pyplot as plt  

class MyDnn(nn.Module):  
    def __init__(self):  
        ''' 搭建自己的网络 '''        super(MyDnn, self).__init__()  
        self.net = nn.Sequential(  
            nn.Linear(8,32),  
            nn.Sigmoid(),  
            nn.Linear(32,8),  
            nn.Sigmoid(),  
            nn.Linear(8,4),  
            nn.Sigmoid(),  
            nn.Linear(4,1),  
            nn.Sigmoid()  
        )  

    # 核心修正:forword → forward  
    def forward(self,x):  
        ''' 前向传播 '''        y = self.net(x)  
        return y  

sourceData = pd.read_csv(r"D:\ZhuoMian\Data.csv", index_col=0)  
# 验证读取结果(打印前5行 + 数据基本信息)  
print(sourceData.head())  # 打印前5行  
# 转化为NumPy数据类型并且转化为float类型  
arr = sourceData.values.astype(np.float32)  
# 转化为张量且放入GPU上  
ts = torch.tensor(arr)  
ts = ts.to('cuda:0')  
print(ts)  
print("======================")  

# 依旧按照3:7划分训练集和测试集  
train_size = int(len(ts)*0.7)  
test_size = len(ts)-train_size  
# 打乱数据  
random_ts = ts[torch.randperm(ts.size(0)),:]  
train_data = random_ts[:train_size]  
test_data = random_ts[train_size:]  
print(train_data)  
print(test_data)  

model = MyDnn().to('cuda:0')  
print(model)  

# 创建损失函数 和 优化算法  
loss_fun = nn.BCELoss(reduction='mean')  
learing_rate = 0.005  # 注:learning 拼写错误,建议改为 learning_rateoptimizer = torch.optim.Adam(model.parameters(), lr=learing_rate)  

# 开始训练  
epochs = 5000  
losses = []  

X = train_data[:,:-1]  
Y = train_data[:,-1].reshape(-1,1) # reshape将一阶张量变为二阶  
for epoch in range(epochs):  
    pred = model(X)  # 对应变量名修正  
    loss = loss_fun(pred,Y)  
    losses.append(loss.item())  
    optimizer.zero_grad()  
    loss.backward()  
    optimizer.step()  

# 画图展示  
fig = plt.figure()  
plt.plot(range(epochs),losses)  
plt.xlabel("epoch")  
plt.ylabel("loss")  
plt.show()  

# 测试  
X = test_data[:,:-1]  
Y = test_data[:,-1].reshape(-1,1)  
with torch.no_grad():  
    pred = model(X)   
    pred[pred >=0.5] = 1  
    pred[pred <0.5] = 0  
    currentSize = torch.sum((pred==Y).all(1))  
    # 精度打印修正:转浮点型,避免张量显示问题  
    accuracy = 100 * currentSize / Y.size(0)  
    print(f"测试准确度 {accuracy.item():.2f}%")

小批量梯度下降

我们依旧使用上面的数据和神经网络结构,但是本次采用的方式为小批量梯度下降,使用小批量梯度下降的时候需要使用Pytorch的三个内置的实用工具

from torch.utils.data import Dataset, DataLoader,random_split

整体步骤是差不多的,首先依旧是制作数据集
在小批量梯度下降制作数据集的时候,我们需要用制作一个类继承上面说的DataSet且重写其__init__方法;__getitem__;__len__三个方法,三个方法的作用分别是加载数据集,获取数据索引,获取数据总量
小批量梯度下降和批量梯度下降在代码上,主要是数据集的封装划分;训练网络测试网络上有一些不同,其余的定义神经网络模型,损失函数定义和使用,优化算法的定义和使用都是一样的

这里不详细逐步介绍了,自行看代码查看不同和理解小批量梯度下降的处理逻辑

import numpy as np  
import pandas as pd  
import torch.nn as nn  
import torch  
from torch.utils.data import Dataset, DataLoader,random_split  
from matplotlib import pyplot as plt  

# 小批量梯度下降  
# 封装数据集  
class MyData(Dataset):  
    def __init__(self, filePath):  
        ''' 加载数据集 '''        # 我们的数据集还是之前那些数据,所以把之前通过Pandas获取数据的逻辑写到这里来即可  
        sourceData = pd.read_csv(filePath, index_col=0)  
        # 验证读取结果(打印前5行 + 数据基本信息)  
        print(sourceData.head())  # 打印前5行  
        # 转化为NumPy数据类型并且转化为float类型  
        arr = sourceData.values.astype(np.float32)  
        # 转化为张量且放入GPU上  
        ts = torch.tensor(arr)  
        ts = ts.to('cuda:0')  
        self.X = ts[:, :-1]  # 前八列为输入特征  
        self.Y = ts[:, -1].reshape(-1, 1)  # 最后一列为输出特征  
        self.len = ts.size(0)  

    # 获取数据索引  
    def __getitem__(self, index):  
        return self.X[index], self.Y[index]  

    # 获取数据集的长度  
    def __len__(self):  
        return self.len  

Data = MyData("D:/ZhuoMian/Data.csv")  
train_size = int(len(Data) * 0.7)  # 训练集大小  
test_size = len(Data) - train_size  # 测试集大小  
train_data, test_data = random_split(Data, [train_size, test_size])  # 直接利用工具函数 让其帮助我们进行测试集和训练集的划分  
# 批次加载器 第一个为数据集 第二个数据为批次数据 第三个参数为是否每一个epoch之内都要进行小批次划分 true为是的 false为否  
# 我们可以从批次加载器中获取训练数据  
# 一般我的训练集需要shuffler为True 保证每次不同从而训练效果好 测试集因为只要一次 所以没必要每次都划分  
train_loader = DataLoader(train_data, batch_size=128, shuffle=True)  
test_loader = DataLoader(test_data, batch_size=64, shuffle=False)  

# 搭建神经网络  
class DNN(nn.Module):  
    def __init__(self):  
        super(DNN, self).__init__()  
        self.net = nn.Sequential(  
            nn.Linear(8, 32), nn.Sigmoid(),  
            nn.Linear(32, 8), nn.Sigmoid(),  
            nn.Linear(8, 4), nn.Sigmoid(),  
            nn.Linear(4, 1), nn.Sigmoid(),  
        )  

    def forward(self, x):  
        y = self.net(x)  
        return y  

# 构建网络对象且放入GPU  
module = DNN().to('cuda:0')  
# 设置学习率,损失函数,优化算法  
loss_fun = nn.BCELoss(reduction='mean')  
learing_rate = 0.005  
optimizer = torch.optim.Adam(module.parameters(), lr=learing_rate)  
# 训练网络  
epochs = 500  
losses = []  
for epoch in range(epochs):  
    for (x, y) in train_loader:  
        Pred = module(x)  
        loss = loss_fun(Pred, y)  
        losses.append(loss.item())  
        optimizer.zero_grad()  
        loss.backward()  
        optimizer.step()  

Fig = plt.figure()  
plt.plot(range(len(losses)), losses)  
plt.xlabel("epoch")  
plt.ylabel("loss")  
plt.show()  

# 测试网络  
correct = 0  
total = 0  
for (x, y) in test_loader:  
    Pred = module(x)  
    Pred[Pred>0.5] = 1  
    Pred[Pred<0.5] = 0  
    correct += torch.sum((Pred==y).all(1))  
    total += y.size(0)  
    print(f"测试集准确率为{100*correct/total:.2f}%") // 这里我们是每批次都相加展示了一次

// 实际上应该在这里统一打印一次最后的
// print(f"测试集准确率为{100*correct/total:.2f}%")
// 不过问题不大,这里的打印结果肯定和上面for里面最后一次循环的打印是一致的

运行后可以看到,图片展示的loss损失值的变化和批量梯度下降完全不一样,并且测试网络给出的数据也是多个(因为分批次了,每个批次都有一个准确率的计算)
可以看到最终结果其实准确率要比批量梯度下降要好一些,虽然从图上来看损失函数不是递减的
image.png

手写数字识别

手写数字数据集(MNIST)是机器学习领域的标准数据集,被称之为机器学习领域的HelloWorld
,因为任何的AI算法都可以用词标准数据集进行校验,MNIST内的每个样本都是一副二维的灰度图像
image.png
在MNIST中,模型的输入就是一副图像,模型的输出就是与图像中对应的数字(0-9,非独热编码)

我们不用手动将输出转化为独热编码,因为PyTorch会在整个过程中自动将数据集的输出转化为独热编码,我们只需要在测试网络的时候,将预测输出和真正结果对比的时候进行转化对比即可

每一图/样本,其实都是一个28X28的二维数组(二维就是灰度图;三维对应的就是彩图)
image.png
和之前不同的是,在这种多分类问题中,神经网络的输出层需要一个softmax激活函数(之前的只要在隐藏层后面跟上激活函数即我们的.ReLU(),在这里最后的输出层那里也需要添加一个激活函数),它可以把输出层的数据归一化到0-1上且相加为1,从而模拟出概率的意思

如何理解这句话呢,就是我们这个数字识别,其输出层肯定是10个即0-9,每一组输入一张图都会有十个输出,这是个输出的值都会被压缩到0~1内且相加为1,就相当于0~9九个可能,每个可能的数值就是其概率,最大的那个也就是概率最大的

那我们现在开始制作数据集,这次的数据集需要在torchvision库中分别下载训练集和测试集,因此需要从torchvision库中导入datasets,下载前还需要借助torchvision中的ransforms进行图像转换,将数据集变为张量且调整数据集的统计分布
首先导入需要的包

import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision import datasets
import matplotlib.pyplot as plt

制作数据集

在下载数据集之前,我们需要设定转换参数transform,利用函数transforms.Compose([..]),该参数解决如下两个问题

# 设定下载集参数 这段代码基本固定 可以重复使用  
transform = transforms.Compose([  
    transforms.ToTensor(),  
    transforms.Normalize((0.1307,), (0.3081,))  
])

设置好下载参数之后,就可以下载训练集和测试集,下载数据集需要利用datasets.MNIST(root=...;train=...;transform=...;download=...)
下载会比较耗时间,需要等待

# 下载数据集  
# 四个参数含义:  
# 下载后保存的位置;是否为训练集(false则代表为测试集);  
# download代表检测第一个参数root下是否有,如果root存在则不需要下载  
train_Data = datasets.MNIST(  
    root='D:/ZhuoMian/bigThree/PythonLearn/BasePy/data/mnist',  
    train=True,  
    transform=transform,  
    download=True  
)  

test_Data = datasets.MNIST(  
    root='D:/ZhuoMian/bigThree/PythonLearn/BasePy/data/mnist',  
    train=False,  
    transform=transform,  
    download=True  
)

image.png

批次加载器

在这里我们也会需要用到批次加载器,和上面小批量梯度下降一致的使用

# 批次加载器  
train_Loader = DataLoader(train_Data,batch_size=64,shuffle=True)  
test_Loader = DataLoader(test_Data,batch_size=64,shuffle=False)

构建网络

整体还是一样的,继承nn.Module然后重写__init__和forward函数,不一样的是,因为输入是图片,我们需要将图片铺成一维的,所以在输入层之前需要借助nn.Flatten()实现

# 搭建神经网络  
class MnistDNN(nn.Module):  
    def __init__(self):  
        # 每个输入都是28*28的二维数组 所以输入得为28*28  
        # 输出为0-9所以需要10个节点  
        super(MnistDNN,self).__init__()  
        self.net = nn.Sequential(  
            nn.Flatten(),   # 把图片铺成一维  
            nn.Linear(28*28,512),  
            nn.ReLU(),  
            nn.Linear(512,256),  
            nn.ReLU(),  
            nn.Linear(256,128),  
            nn.ReLU(),  
            nn.Linear(128,64),  
            nn.ReLU(),  
            nn.Linear(64,10)  
        )  

    def forward(self, x):  
        return self.net(x)

# 创建模型对象且放入GPU  
model = MnistDNN().to("cuda:0")

损失函数和优化算法

损失函数和优化算法也还是一样的,这次我们使用的是CrossEntropyLoss这个损失函数,优化算法依旧选择SGD
可以看到,我们上面说了,这种多分类问题需要在输出层后面再加一个softMax激活函数,可以明显地看到我们在构建网络的链路最后并没有加,这是因为CrossEntropyLoss自带softMax激活函数,而损失函数刚好是计算输出层的数值的,所以这里就可以直接作用上

# 损失函数和优化算法  
loss_fn = nn.CrossEntropyLoss() # 自带softMax激活函数  
learning_rate = 0.01  
# 这里比平常多了一个参数momentum动量的设置,它使得梯度下降算法有了力和惯性  
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate,momentum=0.5)

训练网络

因为使用了批次加载器,那么我们依旧是从批次中获取数据
需要注意的是,因为我们的数据集是下载在文件下的,但是数据集内部我们无法进入,所以我们不能直接将整个数据集放入到cuda上,所以在遍历获取的时候,需要先将x,y放入到cuda上

epochs = 5  
losses = []  
for epoch in range(epochs):  
    for(x,y) in train_Loader:  
        # 因为无法将整个数据集放入到cuda上 所以这里在计算的时候再放入到cuda上  
        x,y = x.to("cuda:0"),y.to("cuda:0")  
        Pred = model(x)  
        loss = loss_fn(Pred, y)  
        losses.append(loss.item())  
        optimizer.zero_grad()  
        loss.backward()  
        optimizer.step()  

Fig = plt.figure()  
plt.plot(range(len(losses)),losses)  
plt.xlabel("Epochs")  
plt.ylabel("Loss")  
plt.show()

测试网络

同小批次梯度下降的测试网络过程,需要从测试集的批量加载器中获取数据,然后求出预估值之后,利用a,b = torch,max(Pred.data,dim=1)获取每一行的最大值和最大值的位置,最大位置其实就是我们的预测值(假设你一行输入,最后得到了10个数值索引为0-9,分别代表了0-9每个数字的占比/权重/概率,最大的那个就是我们的预测结果,所以这里获取最大值的位置其实就是对应的数字了)
因为predicted和y都是一阶张量(类比于一维数组),所以不能使用.all()方法

correct = 0  
total = 0  
with torch.no_grad():  
    for (x,y) in test_Loader:  
        x,y = x.to("cuda:0"),y.to("cuda:0")  
        Pred = model(x)  
        _,predicted = torch.max(Pred.data,dim=1)  
        correct += torch.sum((predicted==y))  
        total += y.size(0)  
print(f"测试集精准度:{100*correct/total}%")

该案例的最终代码如下

import torch  
import torch.nn as nn  
from torch.utils.data import DataLoader  
from torchvision import transforms  
from torchvision import datasets  
import matplotlib.pyplot as plt  

# 设定下载集参数 这段代码基本固定 可以重复使用  
transform = transforms.Compose([  
    transforms.ToTensor(),  
    transforms.Normalize((0.1307,), (0.3081,))  
])  

# 下载数据集  
# 四个参数含义:  
# 下载后保存的位置;是否为训练集(false则代表为测试集);  
# download代表检测第一个参数root下是否有,如果root存在则不需要下载  
train_Data = datasets.MNIST(  
    root='D:/ZhuoMian/bigThree/PythonLearn/BasePy/data/mnist',  
    train=True,  
    transform=transform,  
    download=True  
)  

test_Data = datasets.MNIST(  
    root='D:/ZhuoMian/bigThree/PythonLearn/BasePy/data/mnist',  
    train=False,  
    transform=transform,  
    download=True  
)  

# 批次加载器  
train_Loader = DataLoader(train_Data,batch_size=64,shuffle=True)  
test_Loader = DataLoader(test_Data,batch_size=64,shuffle=False)  

# 搭建神经网络  
class MnistDNN(nn.Module):  
    def __init__(self):  
        # 每个输入都是28*28的二维数组 所以输入得为28*28  
        # 输出为0-9所以需要10个节点  
        super(MnistDNN,self).__init__()  
        self.net = nn.Sequential(  
            nn.Flatten(),   # 把图片铺成一维  
            nn.Linear(28*28,512),  
            nn.ReLU(),  
            nn.Linear(512,256),  
            nn.ReLU(),  
            nn.Linear(256,128),  
            nn.ReLU(),  
            nn.Linear(128,64),  
            nn.ReLU(),  
            nn.Linear(64,10)  
        )  

    def forward(self, x):  
        return self.net(x)  

# 创建模型对象且放入GPU  
model = MnistDNN().to("cuda:0")  

# 损失函数和优化算法  
loss_fn = nn.CrossEntropyLoss() # 自带softMax激活函数  
learning_rate = 0.01  
# 这里比平常多了一个参数momentum动量的设置,它使得梯度下降算法有了力和惯性  
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate,momentum=0.5)  

# 训练网络  
epochs = 5  
losses = []  
for epoch in range(epochs):  
    for(x,y) in train_Loader:  
        # 因为无法将整个数据集放入到cuda上 所以这里在计算的时候再放入到cuda上  
        x,y = x.to("cuda:0"),y.to("cuda:0")  
        Pred = model(x)  
        loss = loss_fn(Pred, y)  
        losses.append(loss.item())  
        optimizer.zero_grad()  
        loss.backward()  
        optimizer.step()  

Fig = plt.figure()  
plt.plot(range(len(losses)),losses)  
plt.xlabel("Epochs")  
plt.ylabel("Loss")  
plt.show()  

# 测试网络  
correct = 0  
total = 0  
with torch.no_grad():  
    for (x,y) in test_Loader:  
        x,y = x.to("cuda:0"),y.to("cuda:0")  
        Pred = model(x)  
        _,predicted = torch.max(Pred.data,dim=1)  
        correct += torch.sum((predicted==y))  
        total += y.size(0)  
print(f"测试集精准度:{100*correct/total}%")

最后的运行结果为
image.png

DNN Python 深度学习 编程 项目