本节课我们介绍MLP和CNN的代码实现,我们从Pytorch的安装开始说起。
Pytorch的安装
Pytorch的安装比较简单,我们可以看到下图的选择栏目,根据你的需要,下面会自动生成合适的命令,把命令copy到command line里,就可以完成安装。
可以在python shell里检查一下
1
2
3
>>> import torch
>>> torch.__version__
'1.9.0+cpu'
深度学习流程
我先简单的介绍一下深度学习的整个代码框架,我做了下面的一个流程图。大致分为这些模块和步骤。
原始的输入数据部分这个比较容易理解,我们一般输入的结构都是特征和标签两部分,当然这个也不一定,依据你们自己设计的模型和框架进行调整。
网络模型部分呢,其实本质上是一个类,从torch.nn.Module继承而来。然后我们从torch里面选择合适的函数,来搭建我们自己的网络。所以我们每次使用的时候,就是初始化这个类的一个实例。
然后我们进入到这个代码的主要部分,训练部分,首先我们需要对数据进行处理,例如随机切分训练集、验证集,将数据分成batch。然后,我们进行一些准备工作,例如选择一个合适的优化器,初始化模型参数,定义损失函数等等。
下面就来到了关键部分,我们需要对网络训练若干次,每一个epoch都要经历两个过程,训练过程、验证过程。对于训练过程,就是正向传播,计算损失函数,然后反向传播。验证过程,只需要正向传播,计算损失,不需要反向传播,但是可以根据自己的需要加入一些评价的过程,比如计算准确率,召回率等等。
数据处理
Tensor
在数学上,一维的数组是向量,二维的数组是矩阵,更高维度的数组就是张量。在深度学习里面我们的数据往往维度很高,举个例子,比如一组图片的数据,在网络学习的时候往往有四个维度${ (B,C,L,W) }$,${ B }$是batch的维度,${ C }$是channel的维度,${ (L,W) }$是图片的长宽。
在pytorch里面,各种计算和优化都是基于Tensor这个数据类型的。但是Tensor的各种操作特别繁多,也不复杂,更重要的是与Python常用的库Numpy的数组操作基本类似,所以这里只介绍简单的一些操作。遇到特殊的情况,可以去torch的官网,寻找你需要的计算函数。还有一个需要注意的地方,在Pytorch的0.4版本之前,Tensor不能直接计算梯度,需要先用Variable类处理一下,才能扔进网络里,但是后面Tensor和Variable合并了,所以这个操作做不做都一样,但是有很多代码还保留了这样的写法,或者仍然习惯这么去写。
1.from List
从Python的list生成,注意如果需要求梯度,数据类型一定需要浮点数才可以计算。
1
2
3
4
>>> torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
tensor([[ 0.1000, 1.2000],
[ 2.2000, 3.1000],
[ 4.9000, 5.2000]])
反过来和Numpy一样,我们也可以将tensor转化回list
1
2
a = torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
a = a.tolist()
2.from Numpy
从Numpy的数组生成tensor
1
2
3
>>> a = numpy.array([1.,2.,2.])
>>> torch.from_numpy(a)
tensor([1., 2., 2.], dtype=torch.float64)
同样,我们也可以将tensor转成numpy
1
2
3
4
5
>>> a = torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
>>> a.numpy()
array([[0.1, 1.2],
[2.2, 3.1],
[4.9, 5.2]], dtype=float32)
3.reshaping
因为很多Pytorch的函数输入的尺寸要求不同,所以我们往往需要给tensor数据整形,比如你的数据是${ (B,L,W) }$,如果想要输入卷积层,就需要添加一个channel的维度,变成${ (B,1,L,W) }$,或者有时候我们想把矩阵转化成一个向量,扔进全连接层都需要整形。我们下面介绍几个整形的函数
view()/reshape()
这两个函数差别不大,把你想整形成的维度输入放在括号里面就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> X = torch.tensor([[1,2,3,4],[5,6,7,8],[1,2,5,6]])
>>> X.view(1,12)
tensor([[1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 5, 6]])
>>> X = torch.tensor([[1,2,3,4],[5,6,7,8],[1,2,5,6]])
>>> X.view(3,1,4)
tensor([[[1, 2, 3, 4]],
[[5, 6, 7, 8]],
[[1, 2, 5, 6]]])
>>> X.reshape(1,3,4)
tensor([[[1, 2, 3, 4],
[5, 6, 7, 8],
[1, 2, 5, 6]]])
>>> X.reshape(2,6)
tensor([[1, 2, 3, 4, 5, 6],
[7, 8, 1, 2, 5, 6]])
squeeze()
比如我们在处理数据的时候,很有可能会出现有些维度是${ 1 }$,比如图片数据的Channel维度,我们想把这些空维度给压缩掉,就可以简单的使用squeeze()
1
2
3
4
5
6
7
8
9
10
11
12
>>> a = torch.tensor([[[1, 2, 3, 4]],
[[5, 6, 7, 8]],
[[1, 2, 5, 6]]])
>>> a.shape
torch.Size([3, 1, 4])
>>> a.squeeze()
tensor([[1, 2, 3, 4],
[5, 6, 7, 8],
[1, 2, 5, 6]])
>>> a = a.squeeze()
>>> a.shape
torch.Size([3, 4])
flatten()
flatten()比较容易理解,就是将张量直接拉平。
1
2
3
4
5
>>> a = torch.tensor([[[1, 2, 3, 4],
[5, 6, 7, 8],
[1, 2, 5, 6]]])
>>> a.flatten()
tensor([1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 5, 6])
数据切分、加载
构建自己的数据集,我们训练的时候,需要将feature和label组合在一起,一起划分、一起分割batch,我们可以用到下面这个函数torch.utils.data.TensorDataset,当然有时候我们的特征不只有一个部分,比如我们可以有一个矩阵的特征还有一个一维的特征,它们也许被分别处理之后,再拼接到一起,我们仍然可以将它们打包到一个数据集。
1
2
3
import torch.utils.data as data
train_feature_label = torch.utils.data.TensorDataset(train_feature_1, train_feature_2, train_label)
然后,我们可以进行数据加载,使用torch.utils.data.DataLoader将数据集加载,同时将数据集分成batch,大家可以用for循环打印观察一下train_loader,每个元素就是一个batch的数据,如果不太能理解,我们一会儿有实例,可以展示一下。(shuffle表示是否打乱顺序)
1
train_loader = torch.utils.data.DataLoader(dataset=train_feature_label, batch_size=32, shuffle=True)
当然了,我们这里面还有一个参数可以选择sampler=?? ,也就是我们可以按照我们的需要选择样本加载。比如,我们可以根据行号随机切分训练集、验证集。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from torch.utils.data.sampler import SubsetRandomSampler
samples_num = len(feature_label)
split_num = int(0.9 * samples_num)
data_index = np.arange(samples_num)
np.random.shuffle(data_index)
train_index = data_index[:split_num]
valid_index = data_index[split_num:]
train_sampler = SubsetRandomSampler(train_index) # 初始化一个类,输入一个索引,后面DataLoader按照这个索引采样
valid_sampler = SubsetRandomSampler(valid_index)
train_loader = torch.utils.data.DataLoader(dataset=feature_label, batch_size=BATCH, sampler=train_sampler)
valid_loader = torch.utils.data.DataLoader(dataset=feature_label, batch_size=BATCH, sampler=valid_sampler)
网络搭建
然后我们现在进入,比较关键的地方,搭建我们自己的网络。所谓的网络就是继承自nn.Module的一个类,我觉得网络主要分为两个部分,一个是初始化部分,一个是forward(),即前向传播部分。所谓的初始化部分,我觉得就是先配齐一个工具箱,你所需要的全连接层,卷积层,池化层,激活函数这些部件都实现准备好。所谓的forward()前向传播部分,就是将这些零件一个一个的组装起来。
1
2
3
4
5
6
7
8
9
10
11
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
############
#配置各种零件#
############
def forward(self, x):
############
#零件依次摆放#
############
return out
MLP网络
下面我们随便搭建一个MLP网络,尝试一下。我们先在初始化部分,构建好各个层,比如我们准备设计三个层,输入维度是128,然后进入64维度的隐藏层,然后输出层是尺寸是1,使用的函数就是nn.Linear()。最后来个Sigmoid函数,把取值限制在[0,1]。然后激活函数选ReLU(),dropout设置成0.5。
前向传播forward()部分就是把定义好的零部件挨个串起来就完成了。看起来是不是也挺简单的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.DROPOUT = 0.5
self.fc1 = nn.Linear(in_features=128, out_features=64, bias=True)
self.fc2 = nn.Linear(in_features=64, out_features=32)
self.fc3 = nn.Linear(in_features=32, out_features=1)
self.dropout = nn.Dropout(self.DROPOUT)
self.ac = nn.ReLU()
self.sigmoid = nn.Sigmoid()
def forward(self, x):
out = self.fc1(x)
out = self.dropout(out)
out = self.ac(out)
out = self.fc2(out)
out = self.dropout(out)
out = self.ac(out)
out = self.fc3(out)
out = self.sigmoid(out)
return out
当然了,这种写法非常的麻烦,才三层的MLP就写了这么多行。我们可以用nn.Sequential()在初始化部分就把部分零件串联起来。这样我们的forward()部分就简化很多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.DROPOUT = 0.5
self.fc1 = nn.Sequential(nn.Linear(in_features=128, out_features=64),nn.Dropout(self.DROPOUT),nn.ReLU())
self.fc2 = nn.Sequential()
self.fc2.add_module('fc',nn.Linear(in_features=64, out_features=32))
self.fc2.add_module('Drop', nn.Dropout(self.DROPOUT))
self.fc2.add_module('ReLU', nn.ReLU)
self.fc3 = nn.Sequential(nn.Linear(in_features=32, out_features=1), nn.Dropout(self.DROPOUT))
self.fc3.add_module('Sigmoid',nn.Sigmoid())
def forward(self, x):
out = self.fc1(x)
out = self.fc2(out)
out = self.fc3(out)
return out
还有些同学觉得,这个也很麻烦,我如果有多个隐藏层,要写很多,改个隐藏层个数的超参数也很麻烦,那么可以用下面这个函数nn.ModuleList(),可以像List一样将网络层接在一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MLP(nn.Module):
def __init__(self):
super(MLP, self).__init__()
self.DROPOUT = 0.5
self.LAYERS = 8
self.fc1 = nn.Sequential(nn.Linear(in_features=128, out_features=64),nn.Dropout(self.DROPOUT),nn.ReLU())
self.fc2 = nn.ModuleList()
for _ in range(self.LAYERS):
fc = nn.Sequential(nn.Dropout(self.DROPOUT), nn.Linear(64, 64), nn.ReLU())
self.hidden.append(fc)
self.fc3 = nn.Sequential(nn.Linear(in_features=32, out_features=1), nn.Dropout(self.DROPOUT))
self.fc3.add_module('Sigmoid',nn.Sigmoid())
def forward(self, x):
out = self.fc1(x)
for i in range(LAYERS):
out = self.fc2[i](out)
out = self.fc3(out)
return out
CNN网络
那么如果前面的MLP已经了解的话,搭建一个卷积的网路,也非常简单,这里随便给个例子,nn.Conv2d()是二维的卷积,我们前两个参数分别表示channel是输入输出个数,后面表示卷积核的size,padding表示补零列数(行数)的情况。nn.MaxPool2d()是最大池化(2,3)是池化核的形状。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.kernel_size = 3
self.conv = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=2,
kernel_size=(self.kernel_size,self.kernel_size),
padding=self.kernel_size//2),
nn.MaxPool2d((2,3),padding=self.kernel_size//2),
nn.ReLU())
self.fc = nn.Sequential(nn.Linear(64, 1), nn.Sigmoid())
def forward(self, x):
out = self.conv(x)
out = out.flatten()
out = self.fc(out)
return out
网络训练
下面我们来到了另一个重要的模块,就是网络训练我们分成几个部分来介绍。首先,可以用前面数据处理的几个函数,先完成数据的准备工作。然后,大概写个框架,我们需要那些函数核变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
import torch.nn as nn
import torch.utils.data
from torch.utils.data.sampler import SubsetRandomSampler
import torch.optim as optim
import torch.nn.functional as F
model = Net()
def train_epoch():
return
def valid_epoch():
return
def train(model,feature_label):
samples_num = len(feature_label)
split_num = int(0.9 * samples_num)
data_index = np.arange(samples_num)
np.random.shuffle(data_index)
train_index = data_index[:split_num]
valid_index = data_index[split_num:]
train_sampler = SubsetRandomSampler(train_index)
valid_sampler = SubsetRandomSampler(valid_index)
train_loader = torch.utils.data.DataLoader(dataset=feature_label,
batch_size=BATCH, sampler=train_sampler)
valid_loader = torch.utils.data.DataLoader(dataset=feature_label,
batch_size=BATCH, sampler=valid_sampler)
初始准备
我们初始化的准备,就是三项:1.模型参数初始化;2.优化器的设定;3.损失函数设置; 其实很简单的两行代码就可以解决,关于参数初始化这里写的就是最简单是随机初始化,其实还有很多初始化的方式,比如均匀分布、正态分布等等,这里不再赘述,可以去官网寻找,采用合适的方式即可。优化器此处用的是Adam,损失函数是二项交叉熵,也都可以替换,采用合适的即可。
1
2
3
4
5
import torch.optim as optim
import torch.nn.functional as F
optimizer = optim.Adam(model.parameters(), lr=0.001)
loss_fun = F.binary_cross_entropy
然后我们开始进入每个epoch的训练,先写个大致的框架
1
2
3
4
5
EPOCH = 30
for epoch in range(EPOCH):
loss_t = train_epoch(model, train_loader, optimizer, loss_fun)
loss_v = valid_epoch(model, valid_loader, loss_fun)
train epoch
下面我们具体写一下训练一个epoch的代码,model.train()是含义是将模型调整成训练状态,另一个状态是model.eval(),在训练状态下,比如dropout这类随机的情况是随机发生的,但是eval模式先,这种随机状态就被固定。
首先将变量变成可求梯度的状态,然后我们将梯度归零,进行前向传播,进一步计算损失函数,最后计算梯度,优化器优化参数。
最后很简单,我们给验证集每个样本的损失计算一个平均值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def train_epoch(model,train_loader, optimizer, loss_fun):
model.train()
loss = 0
num = 0
for step, (feature, label) in enumerate(train_loader):
feature = torch.autograd.Variable(feature.float())
label = torch.autograd.Variable(label.float())
optimizer.zero_grad()
output = model(feature)
train_loss = loss_fun(output, label).to(DEVICE)
train_loss.backward()
optimizer.step()
loss = loss + train_loss.item()
num = num + len(label)
epoch_loss = loss / num
return epoch_loss
valid epoch
训练epoch已经说明了,验证epoch就很简单了。with torch.no_grad()的作用是下面的代码都不会进行梯度计算,所以可以节省一些GPU资源,加速代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def valid_epoch(model, valid_loader, loss_fun):
model.eval()
loss = 0
num = 0
with torch.no_grad():
for step, (feature, label) in enumerate(valid_loader):
feature = torch.autograd.Variable(feature.float())
label = torch.autograd.Variable(label.float())
pred = model(feature)
valid_loss = loss_fun(pred, label)
loss = loss + valid_loss.item()
num = num + len(label)
epoch_loss = loss / num
return epoch_loss
模型保存与加载
当我们需要最终确定进行保存的时候,我们可以用下面两种方式保存模型和加载模型。
1.保存整个模型
1
2
torch.save(model,"./model/Net.pkl")
model = torch.load("./model/Net.pkl")
2.只保存模型的参数
1
2
3
torch.save(model.state_dict(),"./model/Net.pkl") # 存成.dat文件也可以
model = Net() # 创建一个实例
model.load_state_dict(torch.load('./model/Net.pkl')) # 模型的参数被赋值
run on GPU
GPU上运行,需要使用cuda,为了防止没有cuda而不能运行程序,这里事先检测一下cuda的存在性.
1
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # cuda:0是为了防止有多个GPU,因为张量运算需要在同一个GPU才能运行.
然后,将操作放在GPU上有两种方法
1
2
XXX.cuda() # No.1
XXX.to(DEVICE) #前面定义的DEVICE
举例如下,注意进行GPU运算的所有张量都要事先传入GPU,必须在同一设备下,一定切记!
1
2
3
4
model = MyConvNet().to(DEVICE)
for epoch in range(30):
for step, (feature, label) in enumerate(train_loader):
feature, label = feature.to(device), label.to(device)
最后,需要注意的就是,计算的结果如果再进一步计算的话,需要注意,可能需要再切换回CPU,使用XXX.cpu()即可。