前置准备
有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那么需要在传入训练数据那一部分进行相对应的变化。