第十一周周报:卷积神经网络、服务器

目录

摘要

Abstract

一、卷积神经网络

1.1 空洞卷积

1.2 Res2Net

二、服务器使用

2.1 PyCharm配置

2.2 测试

2.2.1 连通性测试

2.2.2 模型训练测试

总结


摘要

本周对卷积神经网络进行了更加深入的学习,尤其是ResNet和Res2Net在今后的学习和实践中会重点使用,本周也将其论文进行了深入的学习,了解了整个网络的流程,以及层与层之间参数的传递情况。本周的剩余时间,还学习了服务器的使用,通过SSH在PyCharm直接调用远程服务器资源进行训练,这有利于我后期的学习。

Abstract

This week, I delved deeper into the study of convolutional neural networks, especially ResNet and Res2Net, which will be emphasized in future learning and practice. I also studied my paper in depth this week, gaining an understanding of the entire network process and the transfer of parameters between layers. For the rest of this week, I also learned how to use the server and used SSH to directly call remote server resources in PyCharm for training, which will be beneficial for my later learning.

一、卷积神经网络

1.1 空洞卷积

在本周的学习中,遇到个新名词----空洞卷积(膨胀卷积)。虽然在近期的学习中不会使用到,但是我还是做了一个大致的了解。

下图是常规的卷积:

下图是空洞卷积:

由上面两张图可以看出,两个卷积核的大小是一样的,但是感受野的范围不一样。空洞卷积就是在元素之间增加一些缝隙(填充0),这些缝隙在空洞卷积中构成了膨胀因子。

空洞卷积的优点:

  • 增大感受野

无需增加参数和计算量:空洞卷积通过在卷积核的元素之间插入“空洞”(即零),使得卷积核在保持原有尺寸和参数数量的同时,能够覆盖更大的输入区域,即增大了感受野。这意味着每个卷积输出都包含了更大范围的信息。

提高上下文感知能力:增大的感受野使得模型能够捕获到更广泛的上下文信息,有助于提升模型对复杂任务的处理能力,如语义分割、图像超分辨率等。

  • 提高计算效率

减少参数数量:由于空洞卷积在卷积核中插入了零,实际上参与计算的参数数量并没有增加,反而可能通过减少计算量来提高计算效率。

降低内存占用:较少的参数数量也意味着较低的内存占用,这对于资源受限的环境尤为重要。

  • 灵活性

可调节的空洞率:空洞卷积引入了“空洞率”这一新参数,通过调整空洞率,可以灵活地控制卷积核的视野大小,以适应不同任务的需求。

适用于多种场景:空洞卷积不仅适用于图像分割任务,还可以应用于图像超分辨率、人脸识别等多个领域,展现出广泛的适用性。

  • 保持信息完整性

避免信息丢失:在图像分割等任务中,传统的下采样操作往往会降低图像分辨率并丢失部分信息。而空洞卷积通过增大感受野而不是下采样,能够在不丢失信息的情况下捕获更多的上下文信息。

即保持原输入特征图的高度和宽度,在实际使用中,一般都会对padding进行设置,这样就能保证输入特征图的高度和宽度不变。

  • 实际应用中的优势

提高分割精度:在语义分割任务中,空洞卷积能够帮助网络更好地理解对象的边界和上下文信息,从而提高分割精度。

改善图像质量:在图像超分辨率任务中,空洞卷积有助于提取更多细节信息,改善图像质量。

不能连续使用几个膨胀系数相同的空洞卷积,会造成gridding effect问题。如想深入了解,请移步这篇博客

1.2 Res2Net

论文链接:Res2Net:一种新的多尺度骨干架构 |IEEE期刊和杂志 |IEEE Xplore 

目前Res2Net在卷积神经网络中,经过实验证明效果算很好的。于是,本周重点学习了该网络的原理。视频讲解请移步b站大佬

Res2Net核心是将之前3×3滤波器改进为一组较小的过滤器组,每个过滤器组w通道。然后,这些较小的过滤器组以类似分层残差的样式连接,以增加输出要素可以表示的尺度数量。模型的拟合还是采用残差网络的思想。

将输入特征图分为几组。一组过滤器首先从一组输入特征图中提取特征。然后,前一组的输出特征与另一组输入特征图一起发送到下一组滤波器。此过程将重复多次,直到处理完所有输入特征图。最后,来自所有组的特征图被连接起来并发送到另一组1x1的卷积层来完全融合信息。如下(b)图所示:

我的理解是:

在Res2Net模块中,输入特征图首先被平均分割成s组,每组记作xi(i∈{1,2,...,s}),其中s是尺度尺寸的控制参数,表示分组的数量。每组特征图的通道数均为输入特征图通道数的1/s。

除了第一组x1外,每组特征图xi都会经过一个3×3的卷积操作,以提取该组的特征。第一组x1通常不直接进行卷积操作,而是作为后续组的输入之一,这可以视为一种特征重用的方式。从第二组x2开始,每组的特征图xi在送入卷积操作之前,都会与前一组Ki-1的输出特征图进行相加(特征融合)。这种累加操作使得每组的特征图都能够接收到来自前面所有组的特征信息,从而增加了特征图的感受野和多样性。

经过上述分组和特征提取过程后,所有组的输出特征图在通道维度上进行拼接,形成一个新的特征图。这个新的特征图包含了来自所有组的特征信息,具有更丰富的多尺度表示能力。

最后,这个拼接后的特征图会经过一个1×1的卷积操作,以进一步融合信息并调整特征图的通道数,使其符合后续网络层的要求。

通过分组和累加操作,Res2Net能够在更细粒度的级别上提取多尺度特征,从而增强网络对复杂场景的理解能力。如下图所示:

我认为从代码角度更好理解,下面是Res2Net模块PyTorch代码的呈现:

import torch
from torch import nn

# SE模块
class SEModule(nn.Module):
    def __init__(self, channels, reduction=16):
        super(SEModule, self).__init__()
        self.avg_pool = nn.AdaptiveAvgPool2d(1)
        self.fc1 = nn.Conv2d(channels, channels // reduction, kernel_size=1, padding=0)
        self.relu = nn.ReLU(inplace=True)
        self.fc2 = nn.Conv2d(channels // reduction, channels, kernel_size=1, padding=0)
        self.sigmoid = nn.Sigmoid()

    def forward(self, input):
        x = self.avg_pool(input)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        x = self.sigmoid(x)
        return input * x

class Res2NetBottleneck(nn.Module):
    expansion = 4  # 残差块的输出通道数=输入通道数*expansion
    def __init__(self, inplanes, planes, downsample=None, stride=1, scales=4, groups=1, se=True,  norm_layer=True):
        # scales为残差块中使用分层的特征组数,groups表示其中3*3卷积层数量,SE模块和BN层
        super(Res2NetBottleneck, self).__init__()

        if planes % scales != 0: #输出通道数为4的倍数
            raise ValueError('Planes must be divisible by scales')
        if norm_layer:  # BN层
            norm_layer = nn.BatchNorm2d

        bottleneck_planes = groups * planes
        self.scales = scales
        self.stride = stride
        self.downsample = downsample
        # 1*1的卷积层,在第二个layer时缩小图片尺寸
        self.conv1 = nn.Conv2d(inplanes, bottleneck_planes, kernel_size=1, stride=stride)  #1x1的卷积将输入通道数翻scales倍
        self.bn1 = norm_layer(bottleneck_planes)
        # 3*3的卷积层,一共有3个卷积层和3个BN层
        self.conv2 = nn.ModuleList([nn.Conv2d(bottleneck_planes // scales, bottleneck_planes // scales, kernel_size=3, stride=1, padding=1, groups=groups) for _ in range(scales-1)])
        self.bn2 = nn.ModuleList([norm_layer(bottleneck_planes // scales) for _ in range(scales-1)])
        # 1*1的卷积层,经过这个卷积层之后输出的通道数变成
        self.conv3 = nn.Conv2d(bottleneck_planes, planes * self.expansion, kernel_size=1, stride=1)
        self.bn3 = norm_layer(planes * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        # SE模块
        self.se = SEModule(planes * self.expansion) if se else None

    def forward(self, x):
        identity = x

        # 1*1的卷积层
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        # scales个(3x3)的残差分层架构
        xs = torch.chunk(out, self.scales, 1) #将x分割成scales块
        ys = []
        for s in range(self.scales):
            if s == 0:
                ys.append(xs[s])
            elif s == 1:
                ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s]))))
            else:
                ys.append(self.relu(self.bn2[s-1](self.conv2[s-1](xs[s] + ys[-1]))))
        out = torch.cat(ys, 1)

        # 1*1的卷积层
        out = self.conv3(out)
        out = self.bn3(out)

        # 加入SE模块
        if self.se is not None:
            out = self.se(out)
        # 下采样
        if self.downsample:
            identity = self.downsample(identity)

        out += identity
        out = self.relu(out)

        return out

class Res2Net(nn.Module):
    def __init__(self, layers, num_classes, width=16, scales=4, groups=1, zero_init_residual=True, se=True, norm_layer=True):
        super(Res2Net, self).__init__()
        if norm_layer:  # BN层
            norm_layer = nn.BatchNorm2d
        # 通道数分别为64,128,256,512
        planes = [int(width * scales * 2 ** i) for i in range(4)]
        self.inplanes = planes[0]

        # 7*7的卷积层,3*3的最大池化层
        self.conv1 = nn.Conv2d(3, planes[0], kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = norm_layer(planes[0])
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        # 四个残差块
        self.layer1 = self._make_layer(Res2NetBottleneck, planes[0], layers[0], stride=1, scales=scales, groups=groups, se=se, norm_layer=norm_layer)
        self.layer2 = self._make_layer(Res2NetBottleneck, planes[1], layers[1], stride=2, scales=scales, groups=groups, se=se, norm_layer=norm_layer)
        self.layer3 = self._make_layer(Res2NetBottleneck, planes[2], layers[2], stride=2, scales=scales, groups=groups, se=se, norm_layer=norm_layer)
        self.layer4 = self._make_layer(Res2NetBottleneck, planes[3], layers[3], stride=2, scales=scales, groups=groups, se=se, norm_layer=norm_layer)
        # 自适应平均池化,全连接层
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(planes[3] * Res2NetBottleneck.expansion, num_classes)

        # 初始化
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
        # 零初始化每个剩余分支中的最后一个BN,以便剩余分支从零开始,并且每个剩余块的行为类似于一个恒等式
        if zero_init_residual:
            for m in self.modules():
                if isinstance(m, Res2NetBottleneck):
                    nn.init.constant_(m.bn3.weight, 0)

    def _make_layer(self, block, planes, blocks, stride=1, scales=4, groups=1, se=True, norm_layer=True):
        if norm_layer:
            norm_layer = nn.BatchNorm2d

        downsample = None  # 下采样,可缩小图片尺寸
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride),
                norm_layer(planes * block.expansion),
            )

        layers = []
        layers.append(block(self.inplanes, planes, downsample, stride=stride, scales=scales, groups=groups, se=se, norm_layer=norm_layer))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes, scales=scales, groups=groups, se=se, norm_layer=norm_layer))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        logits = self.fc(x)
        probas = nn.functional.softmax(logits, dim=1)

        return probas

二、服务器使用

因为受电脑配置限制,在学习过程中会有不少阻碍。于是,本周花了一点时间将服务器调通,下面会有详细步骤,以及测试情况展示。

我使用PyCharm配置SSH连接远程服务器,利用服务器资源进行训练,这种方法也便于后期代码的调试。

服务器短期租用此链接 。

2.1 PyCharm配置

需要下载PyCharm专业版才能使用SSH。

  • 首先新建一个python项目,如下图所示:

  • 在新建项目中,设置-->Python解释器-->添加新解释器-->选择SSH,如下图所示:

  • 在新目标SSH中,填入租用信息(登录指令、密码),如下图所示:

  • 需要开启服务器才能链接成功,如下图所示:

  • 选择conda环境,以及在服务器端创建的环境,如下图所示:

  • 创建,等待进度条加载完毕即可
  • 可通过服务器端JupyterLab进行相应环境配置

2.2 测试

2.2.1 连通性测试

在Python项目下创建Python文件,代码如下:

import numpy as np

x=np.random.randn(10,10)

y=np.linalg.det(x)

print(x)
print(y)

利用服务器资源远程训练,结果如下:

到这步已经可以成功使用服务器了,也便于我后期的学习需求。 

2.2.2 模型训练测试

使用租用的服务器进行了基础CNN对MNIST数据集手写数字识别的训练。

import numpy as np
import torch
import torchvision
from torch import nn
from torchvision import datasets, transforms, utils
from PIL import Image
import matplotlib.pyplot as plt
import torch.optim as optim

print(torch.__version__)

# 定义超参数
batch_size = 128  # 每个批次(batch)的样本数

# 对输入的数据进行标准化处理
# transforms.ToTensor() 将图像数据转换为 PyTorch 中的张量(tensor)格式,并将像素值缩放到 0-1 的范围内。
# 这是因为神经网络需要的输入数据必须是张量格式,并且需要进行归一化处理,以提高模型的训练效果。
# transforms.Normalize(mean=[0.5],std=[0.5]) 将图像像素值进行标准化处理,使其均值为 0,标准差为 1。
# 输入数据进行标准化处理可以提高模型的鲁棒性和稳定性,减少模型训练过程中的梯度爆炸和消失问题。
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5])])

# 加载MNIST数据集
train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)

# 创建数据加载器(用于将数据分次放进模型进行训练),打包训练集和数据集
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size,
                                           shuffle=True,  # 装载过程中随机乱序
                                           num_workers=2)  # 表示2个子进程加载数据
test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False, num_workers=2)

# 打印前5个数据
for i in range(0,5):
    oneimg,label = train_dataset[i]
    grid = utils.make_grid(oneimg)  # 将多张图片拼接成一张网格图像
    grid = grid.numpy().transpose(1,2,0)
    std = [0.5]
    mean = [0.5]
    grid = grid * std + mean
    # 可视化图像
    plt.subplot(1, 5, i+1)
    plt.imshow(grid)
    plt.axis('off')
plt.show()

# 搭建网络模型
class CNN(nn.Module):
    # 定义网络结构
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5, stride=1, padding=2)  # kernel_size卷积核大小,有16个滤波器5x5x1
        self.relu1 = nn.ReLU()
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv2 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5, stride=1, padding=2)  # 2个滤波器5x5x16
        self.relu2 = nn.ReLU()
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
        self.fc1 = nn.Linear(in_features=7 * 7 * 32, out_features=256)
        self.relu3 = nn.ReLU()
        self.fc2 = nn.Linear(in_features=256, out_features=10)

    # 定义前向传播过程的计算函数
    def forward(self, x):
        # 第一层卷积、激活函数和池化
        x = self.conv1(x)
        x = self.relu1(x)
        x = self.pool1(x)
        # 第二层卷积、激活函数和池化
        x = self.conv2(x)
        x = self.relu2(x)
        x = self.pool2(x)
        # 将数据平展成一维
        x = x.view(-1, 7 * 7 * 32)
        # 第一层全连接层
        x = self.fc1(x)
        x = self.relu3(x)
        # 第二层全连接层
        x = self.fc2(x)
        return x

learning_rate = 0.001  # 学习率

# 定义损失函数,计算模型的输出与目标标签之间的交叉熵损失
criterion = nn.CrossEntropyLoss()

model = CNN()  # 实例化CNN模型
num_epochs = 10  # 定义迭代次数
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)  # momentum 表示动量因子,可以加速优化过程并提高模型的泛化性能。
# 如果可用的话使用 GPU 进行训练,否则使用 CPU 进行训练。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(device)
# 将神经网络模型 net 移动到指定的设备上。
model = model.to(device)
total_step = len(train_loader)
for epoch in range(num_epochs):
    for i, (images,labels) in enumerate(train_loader):
        images=images.to(device)
        labels=labels.to(device)
        optimizer.zero_grad() # 清空上一个batch的梯度信息
        # 将输入数据 inputs 喂入神经网络模型 net 中进行前向计算,得到模型的输出结果 outputs。
        outputs=model(images)
        # 使用交叉熵损失函数 criterion 计算模型输出 outputs 与标签数据 labels 之间的损失值 loss。
        loss=criterion(outputs,labels)
        # 使用反向传播算法计算模型参数的梯度信息,并使用优化器 optimizer 对模型参数进行更新。
        loss.backward()
         # 更新梯度
        optimizer.step()
        # 输出训练结果
        if (i+1) % 100 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch+1, num_epochs, i+1, total_step, loss.item()))

print('Finished Training')


# 测试模型
with torch.no_grad():  # 进行评测的时候网络不更新梯度
    correct = 0
    total = 0
    for images, labels in test_loader:
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))



# 可视化一个批次的预测结果
dataiter = iter(test_loader)  # 将 test_loader 转换为一个可迭代对象 dataiter
# 使用 next(dataiter) 获取 test_loader 中的下一个 batch 的图像数据和标签数据
images, labels = next(dataiter)

test_img = utils.make_grid(images)
test_img = test_img.numpy().transpose(1,2,0)
std = [0.5]
mean =  [0.5]
test_img = test_img*std+0.5
plt.imshow(test_img)
plt.show()
plt.savefig('./data/mnist_net.png')
print('GroundTruth: ', ' '.join('%d' % labels[j] for j in range(128)))

代码运行结果如下:

  • 输出前5张图片

  • 训练过程及结果 

总结

本周的学习到此结束,下周将继续卷积神经网络的学习,以及Transformer更加深入的学习。

如有错误,请各位大佬指出,谢谢! 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值