拼拼凑凑的pytorch学习——神经网络训练

目录

  • 前置准备
  • 准备训练数据
  • 网络结构
    • 构建方式
    • 初始化网络层信息
    • 构建前向传播计算过程
  • 选择损失函数
  • 选择优化器
  • 开始训练


前置准备

有python环境,安装好pytorch

因为纯粹是为了演示训练过程,具体训练的内容并不是很重要,所以干脆来个简单点的,也好清楚地展示

下面将训练一个玩具神经网络,判断一个向量(x,y)位于第几象限,数据随机生成,网络结构只使用前向神经网络

准备训练数据

先写一个向量类

# 向量类
class Vector:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return "("+str(self.x)+","+str(self.y)+")"

	# 判断象限的方法
    @staticmethod
    def judge_quadrant(vec):
        if not isinstance(vec, Vector):
            raise Exception

        if vec.x > 0 and vec.y > 0:
            return 0
        elif vec.x < 0 and vec.y > 0:
            return 1
        elif vec.x < 0 and vec.y < 0:
            return 2
        elif vec.x > 0 and vec.y < 0:
            return 3
        else:
            return 0

构建数据集

# Toy dataset 准备数据集
import random

data_size = 12800
dataset = []
for i in range(data_size):
    x = random.randint(-1000, 1000)
    y = random.randint(-1000, 1000)
    vec = Vector(x, y)
    dataset.append(vec)
	
x_train = torch.Tensor([[vec.x, vec.y] for vec in dataset]) # 使用pytorch张量的格式
label_set = [Vector.judge_quadrant(vec) for vec in dataset]
y_train = []
for label in label_set:
    if label == 0:
        y_train.append([1, 0, 0, 0])
    elif label == 1:
        y_train.append([0, 1, 0, 0])
    elif label == 2:
        y_train.append([0, 0, 1, 0])
    elif label == 3:
        y_train.append([0, 0, 0, 1])

y_train = torch.Tensor(y_train)

输入数据x就是随机生成的二维向量,标签y由一个四维向量构成,类似于[1,0,0,0],[0,1,0,0]这样的标签数据,[1,0,0,0]代表对应的向量在第一象限

代码中有个将python列表转化为torch.Tensor的操作,便于下面训练时利用数据,torch.Tensor是pytorch这个模块的张量类,本质上和numpy的array差不多,使用起来也是大同小异,而且这俩可以互转。

通过上面的步骤就构建好了训练数据集

网络结构

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


class ToyNet(nn.Module):

	# 这里定义网络层的信息
    def __init__(self):
        super(ToyNet, self).__init__()

        self.fc1 = nn.Linear(2, 16)
        self.fc2 = nn.Linear(16, 64)
        self.fc3 = nn.Linear(64, 4)

	# 这里构建前向传播计算过程
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

构建方式

使用pytorch构建神经网络结构,只需自定义一个类,并且继承torch.nn.Module这个类即可

初始化网络层信息

在初始化函数__init__中定义网络层的信息,这里nn.Linear表示全连接层,并且这里定义了三个层,第一个fc1是输入层,输入为2个维度,因为接受(x,y)这个二维向量,最后的fc3是输出层,输出为4个维度,因为(x,y)的象限分类有四种。

如果做图像相关内容想要用卷积层什么的也是在这里定义

构建前向传播计算过程

在forward函数中,写上前向传播的计算过程,传入参数x代表的是输入,因为这里的神经网络结构非常的简单,所以计算过程也很简单,这里的代码就是把整个一个前向传播的计算过程写出来,x先经过fc1层计算后再使用激活函数添加上非线性的因素,fc2也是同理,最后的fc3是输出层所以不用激活函数。

非线性函数都在torch.nn.functional这个包里面,想要用sigmoid和Tanh也可以直接替换,顺带一提,pytorch的池化层操作也是写在这个地方。

选择损失函数

# 定义损失函数
criterion = nn.MSELoss()

nn包里面有很多的损失函数可以选择,这里的MSELoss是计算均方误差,比较常用的还有交叉熵损失nn.CrossEntropyLoss

选择优化器

和损失函数类似,torch.optim模块里面也有很多的优化器可以挑选,下面的SGD是随机梯度下降,还有Nesterov-SGD、Adam、RMSProp等等

import torch.optim as optim
net = ToyNet()
optimizer = optim.SGD(net.parameters(), lr=0.001)

优化器需要传入神经网络的参数,并且还有些参数可以调节,lr代表学习率,可以选取一个合适的值。

开始训练

前面我们将一个神经网络训练需要的部分拼拼凑凑,挑挑拣拣地都构建完成了,下面最终到了训练的步骤了

epochs = 100000
for epoch in range(epochs):
    #  计算输出
    output = net(x_train)
    #  计算损失值
    loss = criterion(output, y_train)
    #  清零梯度缓存
    optimizer.zero_grad()
    # 计算梯度
    loss.backward()
    #  更新参数
    optimizer.step()

    print(epoch, 'times loss:', loss)

让我们一步一步看看吧

训练次数epoch由自己定义

第一步,根据当前的神经网络参数,计算出训练数据的输出值

第二步,将计算的输出值和真实标签传入损失函数进行计算,并且计算出损失值

第三步,将优化器的梯度信息清除

第四步,调用loss.backward(),计算损失函数的梯度

第五步,更新参数

最后等它训练完毕即可


上面基本上是使用pytorch训练神经网络的标准步骤了,有些地方解释下

loss的backward为什么能计算了梯度?

pytorch的张量类会自动存储操作的历史记录,如果调用backward方法,那么就会根据操作历史来计算梯度,并且在类中生成一个grad属性来记录这个梯度,神经网络构建的各个层fc1,fc2,fc3里面以张量的形式存储着weight和bias,而loss损失值的计算会用到神经网络的参数信息,所以loss.backward()调用后,会将神经网络的参数当成自变量计算相应的梯度。

在这里插入图片描述

为什么要用optimizer.zero_grad()清空梯度缓存?

同上,如果一个张量不清空梯度和历史记录,那么它的操作会继续累积下去,梯度的值也会根据这些操作而变得不同,而我们神经网络的训练只需要当前情况下的梯度信息,所以这个optimizer.zero_grad()是用来清空上一次训练时的操作和梯度。

我们知道优化器实例生成时传入了net.parameters(),也就是优化器有了神经网络的参数信息,虽然是优化器optimizer调用清空操作,但清空的还是神经网络各个层的weight和bias的梯度

这里的optimizer.step()是怎么更新参数的?
各个优化器的更新方式都不一样,不过pytorch的SGD应该算是按照批量梯度下降的方式在更新

SGD的step步骤源码是这样的

loss = None
        if closure is not None:
            with torch.enable_grad():
                loss = closure()

        for group in self.param_groups:
            weight_decay = group['weight_decay']
            momentum = group['momentum']
            dampening = group['dampening']
            nesterov = group['nesterov']

            for p in group['params']:
                if p.grad is None:
                    continue
                d_p = p.grad
                if weight_decay != 0:
                    d_p = d_p.add(p, alpha=weight_decay)
                if momentum != 0:
                    param_state = self.state[p]
                    if 'momentum_buffer' not in param_state:
                        buf = param_state['momentum_buffer'] = torch.clone(d_p).detach()
                    else:
                        buf = param_state['momentum_buffer']
                        buf.mul_(momentum).add_(d_p, alpha=1 - dampening)
                    if nesterov:
                        d_p = d_p.add(buf, alpha=momentum)
                    else:
                        d_p = buf

                p.add_(d_p, alpha=-group['lr'])

        return loss

跳过那些参数处理的部分来看到

for p in group['params']:
	if p.grad is None:
	    continue
	d_p = p.grad

params这里存储的就是各个层weight和bias的张量

因为我的例子里面只有三层神经网络,所以weight和bias一共用6个张量表示
在这里插入图片描述
如果调用过backward(),张量里面就会有grad这个属性,然后是对weight_decay和momentum这两参数的处理

拉到最后,看到

p.add_(d_p, alpha=-group['lr'])

这个完全就是最原始的梯度下降计算公式
张量p = 张量p - 梯度d_p * 学习率lr

所以说pytorch里的SGD计算过程是批量梯度下降,因为就是根据输入的全体数据来计算相对应的梯度值,如果想用随机梯度下降,或者mini-batch那么需要在传入训练数据那一部分进行相对应的变化。

你可能感兴趣的:(python,#,pytorch,pytorch,神经网络)