手把手教你用Pytorch搭建神经网络,然后用起来
- 前言
- 1 Pytoch搭建神经网络的方式
- 2 神经网络的设计原则
- 3 训练一个能够预测房价的神经网络
- 3.1 数据准备
- 3.2 神经网络设计
- 3.3 训练网络
- 3.4 模型的评估
- 3.5 模型的使用
- 总结
前言
教你如何使用Pytorch框架搭建自己的神经网络,然后将其用于数据预测。在这之前,请确保你已经装好了Pytorch环境。关于神经网络的基本原理,可以参照我之前写的博文:
小白也能看懂的最速神经网络入门(顺便手搓一个神经网络)
1 Pytoch搭建神经网络的方式
在Pytorch中,神经网络有两种主流的搭建方式。
- 通过继承torch.nn.Module类搭建神经网络(推荐方式)
torch.nn.Module类提供了神经网络的整体框架,包括一个可覆写的初始化函数和可覆写的前向传播函数forward,最重要的是,这个类提供了方向传播函数backward的实现,这样我们就不需要根据反向传播算法自己手动实现反向传播部分的代码了(自己写超级麻烦的,参考我之前的博客就知道)。具体步骤如下:
(1)编写一个神经网络类,该类继承torch.nn.Module类(也就是创建一个神经网络的框架);
(2)重写初始化函数,在初始化函数中创建自己的神经网络中所需要用到的层(相当于凑齐搭建这个网络所需的零件);
(3)重写forward函数,在forward函数中定义(2)中的层的计算顺序(相当于把(2)中的零件组装起来)。
我们用一个具体的例子试一试,假设我们要利用Pytorch构建如下图所示的神经网络。
上图的神经网络中,有5个输入,4个输出,中间有三个全连接层。我们按照上述步骤构建一下这个网络。
importtorchfromtorch.nnimportModule,Linear,ReLUclassMyNN(Module):def__init__(self):super().__init__()self.fc1=Linear(5,7)self.fc2=Linear(7,7)self.fc3=Linear(7,4)defforward(self,x):x=ReLU()(self.fc1(x))x=ReLU()(self.fc2(x))x=ReLU()(self.fc3(x))returnxif__name__=="__main__":#创建一个网络对象试试nn=MyNN()print(nn)#可以查看一下模型结构input=torch.rand(size=(1,5))out=nn(input)print(out)#输出看一下能否正确输出上面代码中,使用Linear类构造全连接层,其中有两个必要参数,第一个是输入数据数量,第二个是输出数据数量。由于整个网络输入数据是长度为5的向量,因此第一个全连接层由Linear(5,7)定义。另外,由于不确定用什么激活函数,因此“无脑”直接用ReLU。
可以看到,__init__方法中定义了这个网络所用的所有层。forward方法中,让输入数据依次经过fc1、fc2、fc3这三个全连接层。需要注意的是,一定要让前一层的输出结构与下一层的输入结构一致。例如,前一层输出的是长度为7的向量(例如fc1层),那么下一层的输入就必须是长度为7的向量(例如fc2层)。
如果你对上面代码的运行过程还是不清楚,这里再说明两件事情。
(1)self.fc1(x)是什么意思?
self.fc1是一个Linear类对象,在Python中,像函数一样调用一个对象(就是一个对象后面加括号这种写法)指的就是调用这个对象的call方法。也就是说,self.fc1(x)这种写法完全等价于self.fc1.call(x)。而在Linear类中,call方法中写的就是这个Linear类的forward方法。因此,self.fc1(x)意思就是让输入x经过self.fc1层的前向传播。
后面类似这样的写法完全和上述是一样的意思。
(2)为什么测试数据input的shape是(1,5)?
从图中或者代码实现可以看出,nn这个网络接收的输入就是一个长度为5的一维向量,其shape应该就是(5)。但是在Pytorch的前向传播中,规定了第一个维度为batch_size。意思就是一次可以同时送batch_size个数据给这个网络进行前向传播计算,这是为了提高数据计算的并行化,从而尽可能提升计算速度。而我们这里是送1个数据进网络,因此batch_size=1,从而input.shape=(1,5)。
上述方法是最灵活的网络构建方法,推荐以后构建自己的神经网络尽可能使用这种方法。
- 利用Squential对象构建网络
这种方法仅使用于简单的小网络。这种方法是“顺序结构”的,一层接一层,无法实现分支结构。上面的网络如果用这种方法构建,写法如下:
nn=torch.nn.Sequential(Linear(5,7),ReLU(),Linear(7,7),ReLU(),Linear(7,4),ReLU())可以看到,这种写法非常简单。直接使用torch.nn.Sequential类,然后将模型中的层对象直接按照顺序写在Sequential类的参数中即可。
网络构建好之后,我们直接输出这个对象来查看其结构,或者使用下面的方法来查看其中的权重、偏置参数。
fornpinnn.named_parameters():print(np)2 神经网络的设计原则
如果你看懂了这篇博文之前的内容,那么现在你通过看神经网络结构图就已经能够将对应的神经网络在Pytorch中实现了。但是,给你一个具体的任务,你要怎么样确定神经网络的结构呢?(也就是说,你要自己设计一个神经网络结构,然后将它画出来。)
实际上这个问题不是一下就能回答出来的。就如同一个经验老到的画家,你不可能刚刚学会线条、色彩搭配就能够做出完美的作品一样。神经网络的设计没有非常固定的范式,在不同的场景下,不同的应用下,不同的客户要求下,神经网络的结构可能都需要修改,这是一件是非常吃经验的事情。
这里,我只能告诉大家一些基本的原则。
首先,神经网络的输入和输出结构,是由数据确定的。
假设,我需要你设计一个神经网络,用于预测每天的降雨量。假设,现在你拿到的具体数据是之前360天的天气数据,包括每天的“日期,温度,风速,湿度,风向”以及每天的“降雨量”。这就是你能拿到的训练数据,其中输入x=[日期,温度,风速,湿度,风向],输出y=[降雨量]。
显然,这堆数据的输入是5个数,输出是1个数。那么,你构建的网络,输入就必须是5个数,输出必须是1个数。只有中间层是怎么样的,那就只能看客户要求和经验了。学者们提出的注意力机制、Dropout层、各种激活函数、分支网络、多尺度特征提取等等,都是为了更加优秀的预测性能。
假设现在将当天和前一天的降雨量加入到考虑范畴,并且要求你输出明天和后天的降雨量,那么此时:x=[日期,温度,风速,湿度,风向,当天降雨量,前一天降雨量],y=[明天的降雨量,后天的降雨量],此时网络就要设计成7个元素的向量作为输入,输出2个元素的向量。
再比如,现在要求你利用神经网络识别图像中的26个英文字母(区分大小写),假设你输入的图片是单通道32*32像素的灰度图,那么你的网络的输入就应该是长度为1024(32×32)的向量,或者是一个(32×32×1)的矩阵。输出,可以是一个数(0表示a,1表示b…,26表示A,27表示B…),也可以是一个长度为52的向量,向量中的每个元素表示属于某个字母的概率(在实际使用中,我们多采用这种方法)。
3 训练一个能够预测房价的神经网络
我们已经学会了如何根据任务要求搭建神经网络。下面,我们用一个具体的例子来试试神经网络的强大之处。
我们使用神经网络来预测波士顿房价。首先让我们来看一下波士顿房价数据是什么样的。
3.1 数据准备
波士顿房价数据集当中的每条数据,有以下14个属性:
CRIM-城镇人均犯罪率。
ZN-占地面积超过 25,000 平方英尺的住宅用地比例。
INDUS-每个城镇非零售商业用地的比例。
CHAS-查尔斯河虚拟变量(如果流域边界为河流,则为1;否则为0,就是是不是江景房的意思😄)
NOX-氮氧化物浓度(百万分之几)
RM-每套住宅的平均房间数
AGE-1940年之前建造的自用住宅的比例
DIS-波士顿五个就业中心的加权距离
RAD-径向公路可达性指数(就是里大路的距离)
TAX-每10000美元的全额房产税税率
PTRATIO-城镇的师生比例
B-黑人比例
LSTAT-低收入人群比例
MEDV-自有住房的中位数价格,单位是1000美元,这个是我们最终想要预测出来的量
上述有些指标我们看不懂也没关系,我只要知道,波士顿房价MEDV受上面的13个变量的影响。
波士顿房价数据集可以在这个地址免费下载到:波士顿房价预测数据集
下载之后,数据集是CSV格式,可以使用记事本或者表格工具打开,结果如下:
我们现在要使用Python,将数据集读取进来,然后转换成Pytorch张量的形式。代码如下:
importcsvimporttorch data=[]withopen('BostonHousing.csv','r')asf:reader=csv.reader(f)next(reader)# 跳过表头,如果没有表头就删掉这行forrowinreader:#将数据中的空字符串转换为'0',不然数值转换会出错row=['0'ifx==''elsexforxinrow]data.append([float(x)forxinrow])# string转floattensor=torch.tensor(data)print(tensor.shape)#划分训练集和测试集#划分训练集和测试集train_set=tensor[0:300,:]val_set=tensor[300:400,:]test_set=tensor[400:,:]上述代码输出torch.Size([505, 14]),说明我们一共得到了505条数据,每条数据是有14个元素的向量,其中最后一个元素是房价,前13个分别是之前提到的影响房价的因素。然后我们将数据集划分为了训练集、验证集和测试集。
3.2 神经网络设计
数据准备好了,现在可以开始来设计神经网络了。按照之前的设计思路,这个神经网络应该是几个输入几个输出呢?
没错,13个输入,1个输出。那神经网络内部呢?当前阶段我们可以随意设置,我们就随意设置三个全连接层吧,一个具有32个神经元,一个具有64个神经元,最后一个有1个神经元。注意,最后这个必须是1个神经元,因为输出是1个!!!
这个网络代码如下:
fromtorch.nnimportModule,Linear,ReLUclassBostonNN(Module):def__init__(self):super().__init__()self.fc1=Linear(13,32)self.fc2=Linear(32,64)self.fc3=Linear(64,1)defforward(self,x):out=ReLU()(self.fc1(x))out=ReLU()(self.fc2(out))out=ReLU()(self.fc3(out))returnout上述神经网络仍然使用ReLU作为激活函数。
接下来,我们只要将这个网络训练起来即可。
3.3 训练网络
网络的训练实际上就是利用已有数据进行前向传播,然后计算损失,然后使用反向传播更新权重w和偏置b。这个过程如果不清楚,可以回去看一下这篇博客:小白也能看懂的最速神经网络入门(顺便手搓一个神经网络)
现在,我们还需要确定一下损失函数。波士顿房价最终输出的房价是一个数,而且是连续的数,这种在机器学习中叫做回归任务。具体的可以不用太理会,你只需要知道,我们要使用平均绝对误差或者均方误差来衡量。在这个例子中,我们先使用均方误差(Mean Square Error, MSE)试一试。
在正式开始训练之前,我们要确定几件事情。我们使用Adam优化器,他是梯度下降优化器的一种改进方案,训练效果比较明显,学习率我们先设置为0.001,如果效果不好可以再调整。下面是整个训练代码。
#划分训练集和测试集train_set=tensor[0:300,:]val_set=tensor[300:400,:]test_set=tensor[400:,:]#模型实例化,然后设置基本参数nn=BostonNN()loss_obj=MSELoss()#实例化MSE损失函数对象optimizer=torch.optim.Adam(nn.parameters(),lr=0.001)#指定优化器和学习率epoch=50#训练的总次数foreinrange(epoch):nn.train()#设置为训练模式,此时所有权重和偏置都可以被修改#正向传播y_pred=nn(train_set[:,0:13])#计算损失loss=loss_obj(y_pred,train_set[:,13])#梯度下降optimizer.zero_grad()#清空上一轮的梯度loss.backward()#反向传播optimizer.step()#更新参数#到一定的epoch输出一下损失信息if(e+1)%2==0:print(f'Epoch [{epoch+1}/{e}], Loss:{loss.item():.4f}')上面的代码中,我们指定了使用MSE损失,设置了优化器为Adam,指定学习率为0.001,然后开始了训练循环。训练的过程和我之前手搓神经网络那一集是一样的,也就是:首先前向传播一次,然后计算损失,最后反向传播(包括梯度计算和权重、偏置更新两个过程)。
当使用不同的参数时,例如不同的优化器,学习率和损失函数,模型的训练结果、性能会不一样。
具体怎么样选用,这也是非常吃经验的事情。总之,默认情况就是:回归任务用均方误差,分类任务用交叉熵。如果要改进甚至发明自己的损失函数,那就是要写论文的事情了。
3.4 模型的评估
在训练过程中,我们也可以在某一定的epoch去看看我们模型的性能。例如,下面的代码利用验证集,每隔100个epoch输出验证集上的绝对平均误差(就是真实值减去预测值的绝对值)。
if(e+1)%2==0:nn.eval()withtorch.no_grad():test_pred=nn(val_set[:,0:13])test_error=torch.mean(torch.abs(test_pred-val_set[:,13]))print(f'Test MAE:{test_error:.4f}')3.5 模型的使用
模型训练好了,我们可以用测试集试一试预测得准不准。
#使用模型,看看测试集前面10个数据预测得准不准out=nn(test_set[0:10,0:13])print("真实房价:",test_set[0:10,13].tolist())print("预测房价:",out.tolist())print("预测的平均绝对误差:",torch.mean(torch.abs(test_set[0:10,13]-out)).item())整体代码如下:
importcsvimporttorchfromtorch.nnimportModule,Linear,ReLU,MSELossclassBostonNN(Module):def__init__(self):super().__init__()self.fc1=Linear(13,32)self.fc2=Linear(32,64)self.fc3=Linear(64,1)defforward(self,x):out=ReLU()(self.fc1(x))out=ReLU()(self.fc2(out))out=ReLU()(self.fc3(out))returnout data=[]withopen('BostonHousing.csv','r')asf:reader=csv.reader(f)next(reader)# 跳过表头,如果没有表头就删掉这行forrowinreader:row=['0'ifx==''elsexforxinrow]#将数据中的空字符串转换为'0',不然数值转换会出错data.append([float(x)forxinrow])# string转floattensor=torch.tensor(data)print(tensor.shape)#划分训练集和测试集train_set=tensor[0:300,:]val_set=tensor[300:400,:]test_set=tensor[400:,:]#模型实例化,然后设置基本参数nn=BostonNN()loss_obj=MSELoss()optimizer=torch.optim.Adam(nn.parameters(),lr=0.001)epoch=50#训练的总次数foreinrange(epoch):nn.train()#设置为训练模式,此时所有权重和偏置都可以被修改#正向传播y_pred=nn(train_set[:,0:13])#计算损失loss=loss_obj(y_pred,train_set[:,13])#梯度下降optimizer.zero_grad()loss.backward()optimizer.step()#到一定的epoch输出一下损失信息if(e+1)%2==0:print(f'Epoch [{epoch+1}/{e}], Loss:{loss.item():.4f}')#到一定的epoch验证模型# 5. 评估if(e+1)%2==0:nn.eval()withtorch.no_grad():test_pred=nn(val_set[:,0:13])test_error=torch.mean(torch.abs(test_pred-val_set[:,13]))print(f'Test MAE:{test_error:.4f}')#使用模型,看看测试集前面10个数据预测得准不准out=nn(test_set[0:10,0:13])print("真实房价:",test_set[0:10,13].tolist())print("预测房价:",out.tolist())print("预测的平均绝对误差:",torch.mean(torch.abs(test_set[0:10,13]-out)).item())总结
现在,你已经会搭建任意全连接神经网络并且将其训练起来了。
这样的神经网络可以处理任意的呈向量形式表达的数据。
但是,如果你需要处理图像这样的二维数据(输入是一个矩阵)甚至视频这样的三维数据(输入是一个三维张量),那就需要使用到卷积神经网络。
喜欢的可以点个关注或者收藏,下次还会更新卷积神经网络的使用。