15 | 卷积神经网络上完成训练、使用GPU训练

2022-07-11 15:45:35 浏览数 (1)

不知不觉已经写到第16篇,最近有很多朋友默默地关注了,有什么错误疏漏的地方还请多多包涵~如果你喜欢的话就推荐给你的朋友吧,谢谢各位大佬。

上一节我们说到用卷积核提取图像的特征了,但是这里面还有一些问题,比如说我们使用的是3×3的卷积核,但是我们怎么能够确定图像上的特征会出现在3×3的区域内呢?如果说图像比较大,一个物体的边缘都要占到5个格子的像素,那我们的卷积核不是要失效了吗?

下采样(Down-sampling)

于是这里有一个功能叫做下采样,简单来说就是把图像缩小。当然有下采样就有上采样,那就是把图像变大。关于把图像变小的一个方法就是池化(pooling)。最常见的是平均池化和最大池化。所谓的池化,就是把一组相邻的像素变成一个像素,比如说我们对4个像素取一个平均值,然后这个平均值作为输出图像的一个像素,图像就缩小到原来的四分之一,最大池化思路也是一样的,只不过不是取平均值,而是去四个像素里的最大值。下图是最大池化的示意图。

除了使用池化方法,我们还可以用带有步长的卷积。我们之前计算的过程中,我们的示例图在滑动卷积核,从输入的左上角开始,每次往左滑动一列或者往下滑动一行逐一计算输出,我们将每次滑动的行数和列数称为Stride,在之前的图片中,Stride=1,但是如果我们把步长设大,比如Stride=2,那么就是跳过某些元素来计算,输出的图像也会相应的变小。

接下来调用一下池化方法,可以看到输出确实小了,由原来的32×32变成了16×16。

代码语言:javascript复制
pool = nn.MaxPool2d(2)
output = pool(img.unsqueeze(0))

img.unsqueeze(0).shape, output.shape
outs:
(torch.Size([1, 3, 32, 32]), torch.Size([1, 3, 16, 16]))

最后看这个图片,把卷积和池化联合起来,第一层先进行卷积,然后最大池化加卷积,再进行最大池化,最后转化为向量。在这个过程中,原始图形的大型十字图形被转化到最后的结果中。

这里面还有一个概念,叫做感受野(receptive field)。在卷积神经网络中,感受野的定义是卷积神经网络每一层输出的特征图上的像素点在原始图像上映射的区域大小。 对于上面这个图上的流程,可以说在给定的3×3 conv、2×2-maxpool、3×3 conv输出的神经元有一个8×8的感受野。

接下来把池化操作加入模型中。在这个模型中,首先用卷积把3个RGB通道的图像转换成16个卷积通道的图像,然后用tanh激活,再用最大池化将图像缩小为原来的四分之一;第二个卷积把16个卷积通道转化为8个通道,接着是激活,池化,最后用线性方法把数据转化成2分类。

代码语言:javascript复制
model = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, padding=1),
            nn.Tanh(),
            nn.MaxPool2d(2),
            nn.Conv2d(16, 8, kernel_size=3, padding=1),
            nn.Tanh(),
            nn.MaxPool2d(2),
            # ... 这里缺少一些东西
            nn.Linear(8 * 8 * 8, 32),
            nn.Tanh(),
            nn.Linear(32, 2))

先来看一下这个模型用到了多少个参数

代码语言:javascript复制
numel_list = [p.numel() for p in model.parameters()]
sum(numel_list), numel_list
outs:
(18090, [432, 16, 1152, 8, 16384, 32, 64, 2])

可以看到这个模型总共才用到1.8w个参数,比我们之前的全连接网络的370w个参数小了差不多200倍! 当然现在这个模型还不能运行,因为里面有一块还没有补全,就是写的缺少一些东西的地方,上面输出是一个8×8的图像,下面输入是8×8×8的,这两个尺寸不匹配,所以我们要继续改写这个模型。

构建nn.Module的子类

我们前面使用Sequential方法来构建模型,当然是很方便的,就只需要调用nn里面的各种模块就可以了,但是如果有时候里面有些模块不能满足我们搭建一个新网络的需求,我们怎么办呢?因此我们需要自己去定义前向传播函数来实现这个功能。为此,我们需要先在init方法里先定义好各个模块,然后在forward中定义模块的使用。

下面init中定义了2个卷积操作,3个激活函数,两个池化和两个线性操作。

代码语言:javascript复制
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.act1 = nn.Tanh()
        self.pool1 = nn.MaxPool2d(2)
        self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1)
        self.act2 = nn.Tanh()
        self.pool2 = nn.MaxPool2d(2)
        self.fc1 = nn.Linear(8 * 8 * 8, 32)
        self.act3 = nn.Tanh()
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x):
        out = self.pool1(self.act1(self.conv1(x)))
        out = self.pool2(self.act2(self.conv2(out)))
        out = out.view(-1, 8 * 8 * 8) # 这里加入了一个view视图操作,把我们的数据改变了形状以适配下一个层的输入
        out = self.act3(self.fc1(out))
        out = self.fc2(out)
        return out

上面代码实现的流程如下

函数式API

到目前为止,我们使用的都是子模块来搭建我们的模型,因此需要显式分配一个实例。当然,PyTorch也为每个nn模块提供了函数式API供我们调用。因此我们可以把代码写成

代码语言:javascript复制
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(8 * 8 * 8, 32)
        self.fc2 = nn.Linear(32, 2)
        
    def forward(self, x):
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * 8)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

这个代码比之前的简洁了很多,但是实现的功能是完全一样的。当然这里我们只是把激活函数和池化进行了API式调用,对于卷积和线性变换仍然进行了初始化定义,我想这主要是为了方便查看以及后期的运算和修改。

训练模型

接下来就真的进入到我们的模型训练环节了

代码语言:javascript复制
import datetime  # 加入了时间模块,方便我们记录模型训练耗时#定义训练环节def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs   1):  # 从1开始
        loss_train = 0.0 # 训练损失累加
        for imgs, labels in train_loader:  # 读取批数据
            
            outputs = model(imgs)  # 模型先预测结果
            
            loss = loss_fn(outputs, labels)  # 计算结果损失

            optimizer.zero_grad()  # 优化器梯度归零
            
            loss.backward()  # 反向传播
            
            optimizer.step()  # 更新参数

            loss_train  = loss.item()  # 把一代中的所有损失累加起来

        if epoch == 1 or epoch % 10 == 0:
            print('{} Epoch {}, Training loss {}'.format(
                datetime.datetime.now(), epoch,
                loss_train / len(train_loader)))  # 这里给出的是每一个批数据的平均损失train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=True)  # <1>#开启训练model = Net()  #  初始化网络,Net就是我们之前自己定义的模型optimizer = optim.SGD(model.parameters(), lr=1e-2)  #  随机梯度下降优化器loss_fn = nn.CrossEntropyLoss()  #  交叉熵损失training_loop(  #训练循环
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,)outs:2022-06-05 14:39:04.654081 Epoch 1, Training loss 0.58062619957954262022-06-05 14:39:27.132287 Epoch 10, Training loss 0.337326001304729732022-06-05 14:39:51.472333 Epoch 20, Training loss 0.294371871147186162022-06-05 14:40:16.650081 Epoch 30, Training loss 0.265221589120330342022-06-05 14:40:41.399406 Epoch 40, Training loss 0.24575546578427032022-06-05 14:41:05.886279 Epoch 50, Training loss 0.230724617981227342022-06-05 14:41:30.979836 Epoch 60, Training loss 0.214764873977679352022-06-05 14:41:55.383647 Epoch 70, Training loss 0.199675980741810642022-06-05 14:42:20.188359 Epoch 80, Training loss 0.186165753040154262022-06-05 14:42:44.789208 Epoch 90, Training loss 0.174995643128255362022-06-05 14:43:09.554941 Epoch 100, Training loss 0.16153321856526054

点了运行之后,我的电脑就开始嗡嗡响了,这个环节跑的很慢,总共100个循环,大概花了4分钟,真怕我电脑崩了。从输出结果可以看出,每10个epoch大概花费25秒左右,损失是稳步下降的。

接下来就是计算准确率了。搞了两天终于搞出来了,让我们拭目以待结果怎么样。

代码语言:javascript复制
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=False)val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64,
                                         shuffle=False)def validate(model, train_loader, val_loader):
    for name, loader in [("train", train_loader), ("val", val_loader)]:
        correct = 0
        total = 0

        with torch.no_grad():  # 不需要梯度
            for imgs, labels in loader:
                outputs = model(imgs)
                _, predicted = torch.max(outputs, dim=1) # 取预测结果的最大值索引作为输出,也就是说如果第一个位置概率高就输出0,第二个位置概率高就输出1
                total  = labels.shape[0]  # 统计样本数量
                correct  = int((predicted == labels).sum())  # 统计分类正确的样本数量

        print("Accuracy {}: {:.2f}".format(name , correct / total))validate(model, train_loader, val_loader)outs:Accuracy train: 0.93Accuracy val: 0.90

从输出的结果可以看出,我们的模型效果超出了之前的全连接网络一大截,在训练集上的准确率是93%,在验证集上的准确率也达到了90%,说明它的泛化性能非常好!如果我在工作中每个模型都能达到这一的准确率就好了。

保存模型

既然我们对模型很满意,那我们就得把模型保存下来,留着以后用,不然总不能每次用的时候都重新训练一遍吧。这个地方只需要一行代码就够了,使用save方法,设定一个路径,就可以把模型存下来了。

代码语言:javascript复制
torch.save(model.state_dict(), data_path   'birds_vs_airplanes.pt')

下次再用的时候,我们把模型掏出来,这里需要注意的是,我们保存的实际上是模型的参数,我们构建的模型结构还是在代码中的,所以在加载的时候也是先把我们定义的模型实例化,然后用这个实例化的模型去load对应的参数权重。

代码语言:javascript复制
loaded_model = Net() loaded_model.load_state_dict(torch.load(data_path                                          'birds_vs_airplanes.pt'))outs:<All keys matched successfully>

如果加载成功,就会返回上面的结果,如果加载不成功,比如你用一个其他的模型去load这个参数,就会返回错误。

用GPU训练

大家都知道GPU这两年贵的离谱,拿来算浮点运算很方便,都被买去挖矿了,当然神经网络的发展也起到了推波助澜的作用。我们前面大概介绍过使用Tensor.To方法能够把tensor移到GPU上,下面就看一下如何用GPU进行模型训练。使用PyTorch很简单,只需要定义一下我们的模型训练使用的设备device就可以了。

代码语言:javascript复制
device = (torch.device('cuda') if torch.cuda.is_available()
          else torch.device('cpu'))print(f"Training on device {device}.")

上面我们定义了device,当存在cuda的时候使用cuda,如果不存在的时候使用cpu

代码语言:javascript复制
import datetimedef training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs   1):
        loss_train = 0.0
        for imgs, labels in train_loader:
            imgs = imgs.to(device=device)  # 数据移动到gpu上面
            labels = labels.to(device=device)
            outputs = model(imgs)
            loss = loss_fn(outputs, labels)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            loss_train  = loss.item()

        if epoch == 1 or epoch % 10 == 0:
            print('{} Epoch {}, Training loss {}'.format(
                datetime.datetime.now(), epoch,
                loss_train / len(train_loader)))train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64,
                                           shuffle=True)model = Net().to(device=device)  # 把模型也移到GPU上面optimizer = optim.SGD(model.parameters(), lr=1e-2)loss_fn = nn.CrossEntropyLoss()training_loop(
    n_epochs = 100,
    optimizer = optimizer,
    model = model,
    loss_fn = loss_fn,
    train_loader = train_loader,)outs:2022-06-05 15:12:33.016390 Epoch 1, Training loss 0.54564654371541022022-06-05 15:12:37.215517 Epoch 10, Training loss 0.33088296253210422022-06-05 15:12:41.897076 Epoch 20, Training loss 0.29666221644848022022-06-05 15:12:46.600443 Epoch 30, Training loss 0.27768537126908642022-06-05 15:12:51.550271 Epoch 40, Training loss 0.25605587167724682022-06-05 15:12:56.415659 Epoch 50, Training loss 0.235854996522520762022-06-05 15:13:01.177421 Epoch 60, Training loss 0.22036242318950642022-06-05 15:13:06.003335 Epoch 70, Training loss 0.204593056990842152022-06-05 15:13:10.775602 Epoch 80, Training loss 0.190941299483844422022-06-05 15:13:15.484186 Epoch 90, Training loss 0.17214741605292462022-06-05 15:13:20.176941 Epoch 100, Training loss 0.16055620855586544

从输出结果可以看到,我们的训练耗时大大降低了,10个epoch耗时只有4-5秒,基本上是在CPU上的五分之一。我们这只是一个小网络,如果是更大的网络,GPU运算的优势会更加明显。

使用GPU训练的模型,在保存和加载的时候需要注意,保存的时候如果仍然是使用GPU的状态,那么在加载模型的时候它也会试图恢复到GPU上面,因此这里建议是在训练完模型之后统一把模型移回CPU,以后加载有需要的话手动移到GPU上去,否则如果我们在没有GPU的环境中加载模型就会遇到问题。

我们终于把第一个卷积网络跑通了,今天就先写到这里。

0 人点赞