笔者个人的毕业设计课题如下:
简介:使用预训练的Diffusion Model图像生成模型生成图像,将这些生成的图像作为扩充训练集加入到2D目标检测器、2D图像分类器的训练过程。深度学习是数据驱动的,随着数据量的扩充,能够提高检测器、分类器的鲁棒性、准确性。
建议的baseline:
分类:ResNet
检测:YOLO
可以看到,给的题目难度还是比较轻松的;本次毕设的全过程会以周为单位采用博客的形式记录下来。
本来本周的计划是搭建和运行跑通扩散模型的,但是由于个人原因,这周大部分的时间并不在学校;没法用实验室的服务器跑,只用我这张本子的2060跑想来还是有些吃力的,所以不得已临时修改了计划,改成先完成ResNet的部分,当然,由于ResNet比较简单,所以也会多一些原理上的说明。
如果谈及什么是ResNet,其实其核心就在于上面这张图里提到的残差模块的设计和应用。ResNet是为了解决“网络退化”问题,所谓的“网络退化”是指随着网络层次的加深,到达一定深度后,网络模型的性能不升反降,这被称为“网络退化”。随着网络层次的加深,网络变得难以训练,不易收敛,原因在于随着网络层次的加深,深层梯度难以反向传播到浅层,即使传播到浅层,浅层的梯度值也小的可怜。
谈到这里,我们不得不进行一些简单的数学推导;因为残差本身实际上也是在和损失函数作博弈
先将上面提到的残差模块简单的示意如上,我们将A和B的卷积层简单的看成函数A和B。那么我们也可以简单的推出:Y=x+B(A(X))
这里再考虑反向传播过程中,损失函数对X的偏导数如上图
可以看到如果没有残差结构,3个偏导数的乘积随着网络层次的加深会非常小,梯度到达饱和状态,网络不容易收敛。
反之,加入残差连接后,即使3个偏导数的乘积再小,但+1的操作使梯度值大大增加,非线性激活函数也到达非饱和区,这样做的好处在于:即使网络做的非常深,网络也是容易收敛、容易训练的。
在本来的模型算法中,我们只试图迭代训练左边的基础部分
但事实证明,如果只是这样的话,当网络的深度不断加深时,整个网络的性能反而不断的下降
这是由于实际上并非每个单元/节点都对不同的输入产生所需要的敏感性,事实上大部分的节点实际上是【收效甚微】的
而在过去,我们加深网络时;单纯的提高深度,会让这些【无效】的节点所占比例急剧上升,这时整体的性能反而下降了
所以我们现在加上右边的部分
我们在上面提到过,这时输出已经变成了F(x)+x,残差结构的伟大之处是在于
当网络的性能已经趋向于最好时,对于那些无用的节点,网络会尽可能的降低F(x)部分的权重,乃至于降低到0,只保留X,使得网络在保有性能的同时不受影响。
注意,接下来的环节建立在你已经搭建了win系统的Anconda的前提下进行
创建虚拟环境,非必要(图里单词拼写错误,这里勘误一下,是create)
确认安装
这一步因为网络原因提升失败或者提示找不到包,可以去搜索换源方法,因为各个镜像网站时不时存在扑街现象,一个源用不了时可以试试其他的,一般总有能用的源。
激活虚拟环境并安装pytorch
注意,不注明版本号默认安装最新版本
同样,包的下载过慢时,也可以尝试换源解决。
验证pytorch安装完毕
注:按ctrl+z退出python
划分数据集
开始训练
测试分类
import torch.nn as nn
import torch
# 定义ResNet18/34的残差结构,为2个3x3的卷积
class BasicBlock(nn.Module):
# 判断残差结构中,主分支的卷积核个数是否发生变化,不变则为1
expansion = 1
# init():进行初始化,申明模型中各层的定义
# downsample=None对应实线残差结构,否则为虚线残差结构
def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
# 使用批量归一化
self.bn1 = nn.BatchNorm2d(out_channel)
# 使用ReLU作为激活函数
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = downsample
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
# 残差块保留原始输入
identity = x
# 如果是虚线残差结构,则进行下采样
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# -----------------------------------------
out = self.conv2(out)
out = self.bn2(out)
# 主分支与shortcut分支数据相加
out += identity
out = self.relu(out)
return out
# 定义ResNet50/101/152的残差结构,为1x1+3x3+1x1的卷积
class Bottleneck(nn.Module):
# expansion是指在每个小残差块内,减小尺度增加维度的倍数,如64*4=256
# Bottleneck层输出通道是输入的4倍
expansion = 4
# init():进行初始化,申明模型中各层的定义
# downsample=None对应实线残差结构,否则为虚线残差结构,专门用来改变x的通道数
def __init__(self, in_channel, out_channel, stride=1, downsample=None,
groups=1, width_per_group=64):
super(Bottleneck, self).__init__()
width = int(out_channel * (width_per_group / 64.)) * groups
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
kernel_size=1, stride=1, bias=False)
# 使用批量归一化
self.bn1 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel * self.expansion,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
# 使用ReLU作为激活函数
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
# 残差块保留原始输入
identity = x
# 如果是虚线残差结构,则进行下采样
if self.downsample is not None:
identity = self.downsample(x)
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
# 主分支与shortcut分支数据相加
out += identity
out = self.relu(out)
return out
# 定义ResNet类
class ResNet(nn.Module):
# 初始化函数
def __init__(self,
block,
blocks_num,
num_classes=1000,
include_top=True,
groups=1,
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top
# maxpool的输出通道数为64,残差结构输入通道数为64
self.in_channel = 64
self.groups = groups
self.width_per_group = width_per_group
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 浅层的stride=1,深层的stride=2
# block:定义的两种残差模块
# block_num:模块中残差块的个数
self.layer1 = self._make_layer(block, 64, blocks_num[0])
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
# 自适应平均池化,指定输出(H,W),通道数不变
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
# 全连接层
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 遍历网络中的每一层
# 继承nn.Module类中的一个方法:self.modules(), 他会返回该网络中的所有modules
for m in self.modules():
# isinstance(object, type):如果指定对象是指定类型,则isinstance()函数返回True
# 如果是卷积层
if isinstance(m, nn.Conv2d):
# kaiming正态分布初始化,使得Conv2d卷积层反向传播的输出的方差都为1
# fan_in:权重是通过线性层(卷积或全连接)隐性确定
# fan_out:通过创建随机矩阵显式创建权重
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
# 定义残差模块,由若干个残差块组成
# block:定义的两种残差模块,channel:该模块中所有卷积层的基准通道数。block_num:模块中残差块的个数
def _make_layer(self, block, channel, block_num, stride=1):
downsample = None
# 如果满足条件,则是虚线残差结构
if stride != 1 or self.in_channel != channel * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = []
layers.append(block(self.in_channel,
channel,
downsample=downsample,
stride=stride,
groups=self.groups,
width_per_group=self.width_per_group))
self.in_channel = channel * block.expansion
for _ in range(1, block_num):
layers.append(block(self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
# Sequential:自定义顺序连接成模型,生成网络结构
return nn.Sequential(*layers)
# forward():定义前向传播过程,描述了各层之间的连接关系
def forward(self, x):
# 无论哪种ResNet,都需要的静态层
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)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
# ResNet()中block参数对应的位置是BasicBlock或Bottleneck
# ResNet()中blocks_num[0-3]对应[3, 4, 6, 3],表示残差模块中的残差数
# 34层的resnet
def resnet34(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet34-333f7ec4.pth
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
# 50层的resnet
def resnet50(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet50-19c8e357.pth
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
# 101层的resnet
def resnet101(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnet101-5d3b4d8f.pth
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
本周完成了ResNet相关部分的理论解释和实验内容运行,下周预计会把diffusion Model部分的实验内容完成配置并尝试跑通;希望环境不要是太奇怪的那种,不然配置和查报错查缺漏就得搞个一天。