线性回归——Pytorch的入门D2L代码略读
前言:
关于Pytorch的种种,我这边就不废话了,以下内容参考《DIVE INTO DEEP LEARNING》(下面简称D2L),由于D2L中分为复杂实现和简单实现,为了效率也为了方便,这边不再对二者做区别,而是记录我认为在工程中会怎么写。
回归与预测,是机器学习中的两类问题,回归即可理解为分类,是一个在有限的集合中找答案的行为;预测从字面角度理解,就是根据现有的数据,让机器学习潜在的规律,随后预测近似条件下,需要预测的值。
在回归问题中,线性回归是基础中的基础,同时作为我第一个开始上手代码的问题,在理解理论上有非常好的效果。这里以D2L的例子为主。线性回归在本质上是所有输入的特征组合,对于一个输入样本X,假设有28*28个特征(这里的数值是采用了28*28的mniFashion数据集totensor后的数字,理解不了没有关系,只是一个数字)。随后会将这些特征输入到第一层节点中,去计算如下该式:
注意,这个b(Bias)可以认为是神经元本身的属性。关于神经网络结构这边粘上《一份简单的深度学习笔记》》中的一个图作为理解:
上图中,b可以看做是神经元(一个个点)本身的属性,因此对于输入到某个神经元的任意 \(x_i\)(不同的特征输入),我们都加上相同的b。
以上基本已经讲完了线性回归的数学模型,剩下的细节部分可以留意一下:
- 为什么这样简单的模型能做到回归
- 特征是什么
- ·······
对于问题一,其实是建立在机器学习的概念上的,所有复杂的模型都是建立在简单的模型基础上,通过层的叠加组合完成各种任务,那么对于简单的单层线性网络,也一定会有他能预测和回归的任务,只不过相对比较简单。对于特征比较明显,并且需要学习的内容比较少的样本,单层线性网络(就是上面说的那一层)往往有很好的效果(体现在参数简单,易训练,降低复杂性,有效防止过拟合,这个在滞后的博客会讲到)只能处理非常简单的任务,像是那种图像识别任务,需要更复杂的模型(添加激活函数,使模型表达能力更强),这个模型是做不到的。
我们在训练的时候,其实并不知道机器是如何学的,我们只是知道,对于这种参数可以改变的、有输入有输出的模型,我们可以通过改变参数来使模型输出,和我们想要的值尽量相近。
总之,神经网络的本质就是由一个输入得到一个输出,中间的过程我们通过训练这一个方法,来使这个过程趋于合理。
对于问题二,本文的特征可以粗浅的认为只是一个输入,这个特征在不同的任务中会有区别(词向量,图像量等),重点是理解特征和样本和batch和训练集的关系。在我学习的过程当中,我最难理解的就是特征跟样本,以图像为例,一张图是一个样本,图中提取出来的是特征(提取方式不同,特征会不同,是输入到神经网络的东西),很多张图是一个batch(批),训练就是一次拿出很多张图,把特征输进去,然后得到结果。
线性回归代码实现部分
这一部分是真正与框架Pytorch接轨的部分,同时也是理解整个过程的部分,我会结合我踩的坑尽量分析一下可能的弯路,强烈建立认真看(当然你不看我也米有办法(doge))。
要实现一个模型,必须有以下的步骤,这个步骤可以结合你的训练过程来记忆,自己写代码的时候就会有条理很多,我接下来也按照这个顺序讲:
- 构建数据集
- 构建数据迭代器
- 构建模型
- 构建优化器
- 构建损失函数
- 训练
- 测试与评估
- 模型比较和总结
1. 构建数据集(dataset)
数据集的具体概念我就不讲了,就讲讲他应该有的两个部分:(data,target),这个是Pytorch源码中的写法,在实际使用中可能也会有像是(data,label)的说法,无论叫法如何,data指的就是我们手中的拿来喂给模型当做输入的东西,而target就是我们希望模型得出的东西。注意,数据集的来源有很多:在D2L中,我们广泛的使用到Pytorch中已经有了的数据集,但是在日后的任务中,我们可能需要别的数据集,我们甚至可以自己做数据集。总之,只要具备上述两个要素,我认为就能够当做数据集使用。
关于数据集,我推荐大家看一下B站up小土堆的视频 ,了解一下关于网络上下载的数据集的使用方式,但是视频中讲的还不算很丰富,但是作为上手是合适的。
在D2L线性回归问题中,采用了自己构建的数据集,构建代码如下,先贴代码,再讲解
#首先,我们做一下库的导入,与D2L本体不同,我不想要调用D2L中的包来解释,毕竟想来在以后的工程中并不会允许使用,也更好理解本职
import random
import torch
import torch.utils import data
from torch import nn
def synthetic_data(w, b, num_examples):
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
如果有看上述代码感觉吃力的同学,可以补充一下关于torch中tensor的概念,线代中矩阵操作的知识还有python的核心编程。上述代码难点不多,唯一需要了解的就是两个API
- ' torch.normal(mean: _float, std: _float, size: _size)',参数当然不止这些,这里只是列举了上述对应的,意思是产生一个均值为mean,方差为std,size为_size的一个tensor(类似数组)
- torch.matmul()矩阵乘法,可以记为matrix multiply。在y=那里应用了广播(将一维的扩展成很多维,是比较重要的概念,可以看一下numpy或者tensor广播机制来理解)
用features还有labels调用完函数后,就可以得到一组对应的值。
features, labels = synthetic_data(true_w, true_b, 1000)
随后,由于我们使用的是Pytorch框架,在真正使用这个数据集的时候,我们需要把他融入到体系中,也就是将其转为Tensor Dataset类,因此调用函数TensorDataset(*data_arrays),但是与D2L中不同,为了严格按照流程,我认为将features和label组合成dataset类是数据集的部分,而不是数据迭代器的部分,因此我将代码拆开。
dataset=data.TensorDataset(features,labels)
这样我们就得到了一个真正意义上的Pytorch中可用的数据集。
2.构建 数据迭代器
batch_size=256
is_train=True
data_iter=data.DadaLoader(dataset,batch_size,shuffle=is_train)
我们可以看到,返回的是一个data_iter最后是一个DataLoader,这边先告诉大家,在以后的代码中,迭代器都是以
这样的形式来得到,DataLoader我在查资料的时候说是迭代数据对象,并不是迭代器,在我的观念中,只有迭代器是可以迭代的,这边还没有理解,但是经过总结,发现dataloader的返回值是可以迭代的,之后就可以使用for循环来遍历数据集了。
另外再解释一下关于DataLoader的参数的解释,dataset就是要处理的数据,batch_size就是批大小,一次取多少的意思,对于batch_size的选择,建议大家另外找资料学习,还是很有门道的,shuffle是布尔类型,表示要不要打乱顺序。
3. 定义模型并初始化
有了数据集之后我们就可以定义模型,在D2L中,复杂实现中,由于线性网络逻辑清晰,运算简单,实现也不麻烦,但是为了匹配后面的多层感知机章节,这里采用类定义。
class net(nn.Module):
def __init__(self):
super().__init__()
self.Linear=nn.Sequential(nn.Linear(2,1))
self.Linear[0].weight.data.normal_(0,0.01)
self.Linear[0].bias.data.fill_(0)
def forward(self,X):
return X=self.Linear(X)
对这里有困难或者疑惑的同学,可以先看下面的讲解,如果还是看不懂或者有困难,可以先移步到D2L中的深度学习计算章节。
上述代码主要有5个难点:
- 为什么要类定义
- 类定义中要做到什么
- 定义模型并初始化
- 编写forward方法
- 相关API的理解
- 如何进行神经网络的设计
对于问题一:在以后编写神经网络模型的时候,往往会有多个层和块的结合,采用类定义的方法,就可以组合多个层和块,更好地构建模型,以Lenet为例,中间包含了多个线性层和多个卷积层,这种情况下,将卷积层池化层还有激活函数放在一个块中更为合理,同时块也可以通过for loop循环加入Sequential来减少代码量。
对于问题二:
- 定义模型并初始化:
定义模型中,我们可以看大采用了Sequential的方法,这个可以理解为将各个层链接,并且一旦实例化,还可以通过像数组那样通过{0,1,2,3,4……}做下标来访问各层。同时Sequential还可以进行嵌套,为我们的块实现行了方便
nn.Linear(in_num,out_num)是线性层,也就是线性模型中的主要组成部分,只需要指定输入的特种数,输出的个数就可以确定线性层
self.Linear[0].weight.data.normal_(0,0.01)
self.Linear[0].bias.data.fill_(0)
除开直接使用sequential把所有的层都包括进去,我们还可以一点点定义块,定义单个层,这里我们介绍一下Sequential的add_module方法,先贴段代码,大家就可以看出这种方法的优势:
class net(nn.Module):
def __init__(self):
super().__init__()
self.Linear_block=nn.Sequential()
self.Linear_block.add_module("Flatten",nn.Flatten())
self.Linear_block.add_module('Linear1',nn.Linear(in_num,out_num))
self.Linear_block.add_module('ReLU',nn.ReLU())
从上面的代码中'Flatten''Linear''ReLU'都是该层的名字,也就是说通过这种办法,我们可以给层命名,另外,也可以较为进行层的管理,除此之外,也可以通过这个方法进行嵌套,总之极大的丰富了模型多样性。
上面这两个是初始化,由于这里的模型比较简单,因此直接索引出线性层直接改参数就可以了,关于参数的改动和初始化,有比较多的方法,在D2L中见得比较多的也比较使用的还有net.apply方法调用具体代码如下(也以线性层为例):
class net(nn.Module):
def __init__(self):
super().__init__()
self.Linear=nn.Sequential(nn.Linear(2,1))
def ini_weight(m):
if type(m)==nn.Linear: #注意:这里的nn.Linear不能写成nn.Linear(),否则类型不匹配
nn.init.normal(m.weight,std=0.01) #调用nn.init库
nn.init.zero_(m.bias)
self.Linear.apply(ini_weight) #这个是moudle类的一个方法,从代码理解看就是对该moudle每一个层调用括号里的函数
def forward(self,X):
return X=self.Linear(X)
- 编写forward方法
__init__方法负责模型的构建,可以看到经过该方法的调用,我们已经有了一个模型,但是这个模型获得输入后不会产生输出,因此我们重新编写forward方法,该方法可以在nn.Module源码中找到,在我们实例化,喂进数据之后,我们就能通过forward函数得到返回输出。所以也会有出现在forward函数中,重复将数据喂给同一层,块的行为,这在整个模型层面又会扯上参数共享,这边就不在做过多赘述了,在之后的层块章节会具体展开。
在forward编写中,如果碰到一个sequential包含很多层的,尽量避免如下写法:
return X=self.Linear(X)
可以看到,这里我们因为只有一层,所以可以这么用,但如果层数上去了,这个return很有可能直接超过最大递归深度,进而直接报错,最好写成x下方这中非递归式写法:
for layer in self.net:
X=net[layer](X)
return X
对于问题三:
啊嘞,相关API在上面已经讲了,呃呃,就好好看看上面吧
对于问题四:
这个问题我在第一次碰到这种类定义的时候还是蛮困惑的,不过在熟练线性网络的代码和阅读了卷积神经网络的实现后,我发现,只要抓住网络中每个层设计,剩下的就交给Sequential就好了。
4. 构建优化器:
先贴上代码:
updater=torch.optim.SGD(net.parameters(),lr=0.03)
定义优化器比较简单,可以看到只要指定元素和学习率,就可以完成定义,但是优化器实际上可以单独指定需要衰减的参数,这个在后续的权重衰减中可以看到,这边先提一嘴,权重衰减可以认为是L2正则化,这个以后再说,先贴上代码,知道有这么回事就可以了。
updater=torch.optim.SGD([
{"params":net[0].weight,'weight_decay':wd},
{'params':net[0].bias}],
lr=0.1)
5.构建损失函数
在D2L中使用了MSELoss(均方损失),可以看到只需要定义好损失函数就可以了,但损失函数中我们可以定义权重,需要忽略的点巴拉巴拉,具体我现在还不是很懂,以后留着更新(-_-||)
loss=nn.MSELoss()
6. 训练
神经网络的训练是一个比较重要的内容,主要可以分为两个部分,都是很有套路的部分:
- 迭代训练模型参数
- 训练可视化(可选,最好有,方便无效训练)
先贴上训练的模板代码:
num_epochs=10
for epoch in range(num_epoch):
for X,y in data_iter: #注意,这边的dataiter千万别打括号
l=loss(net(X),y)
updater.zero_grad()
l.backward()
trainer.step()
经过上述的代码,我们的net模型就被训练好了,在这个过程中可以才用像是D2L中的可视化过程那个画图的类,由于代码太长,并且对matplotlib不是很熟练,因此我在这边隆重介绍TensorBoard!!
关于tensorboard的操作有很多,这边先贴上链接,由于在这篇文章中我们只需要用到部分功能,就不把Tensorboard的详细内容放在这里了。有时间我会补的
在这项任务中,我们主要需要的是add_scalar,该函数简单来说就是要把你需要保存的数值点保存在指定路径下,然后之后把点描出,画图,在这边把我们需要写的语句列举一下:
#导入库,注意我们在这里不需要太多,并且Tensorboard原先是Tensorflow的一个部分,但后面直接在pytorch中实现了,因此直接从torch#中导,不能直接导tensorboard
from torch.utils.tensorboard import SummaryWriter
wirter=SummaryWriter("这里填你想放数据的文件夹")
writer.add_scalar("加入的这系列点的名字,例如train_loss",l,iter) #关于tensorboard的API用法还是看上面那个链接吧,这里的iter相当于是加入点应画的横坐标
writer.closs()#这句很重要,我之前没有加入这句,就算他没有加完,还在等你继续加
#以上是在代码中需要穿插的部分
#以下是应该放到控制台的部分:
tensorboard --logdir="这里写你放的点的路径,注意,不是单独的文件夹,而是放那些数据的文件夹,否则打不开tensorboard"
#然后点一下他提供的端口链接就可以了,我的是http://localhost:6006/
7. 测试与评估
在实验中,我们常常需要测试和评估模型,但是在目前我在这方面还不是很熟练,因此先略过了。
8. 模型的比较
同7,经验不足,后日谈

浙公网安备 33010602011771号