手把手教你基于pytorch实现ResNet(长文)
前言
最近在看经典的卷积网络架构,打算自己尝试复现一下,在此系列文章中,会参考很多文章,有些已经忘记了出处,所以就不贴链接了,希望大家理解。
完整的代码在最后。
本系列必须的基础
python基础知识、CNN原理知识、pytorch基础知识
本系列的目的
一是帮助自己巩固知识点;
二是自己实现一次,可以发现很多之前的不足;
三是希望可以给大家一个参考。
目录结构
文章目录
1. 前言与参考资料:
本来是没打算再更新图像分类算法的实现的,因为前面alexnet、vgg已经很好的给出了例子,大家只需要按照流程来基本都可以实现。
不过,之所以出这篇文章,是因为在看pytorch实现resnet那篇文章的时候,发现很多地方都写错了,所以这里来纠正一下,顺带再次复习一下图像分类算法实现流程,并且尽力做到全面(主要是调整学习率训练和采取预训练模型试试)。
数据参考资料
B站的一个up主的GitHub仓库,链接为:
https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
这个up不仅提供了数据集,也提供了相关代码和讲解视频,大家可以自己去学习,但是建议大家自己先实现一次,体验下难点在哪里。
2. 数据集介绍与下载:
数据集下载
**方法一:**从GitHub中下载,然后还需要自己处理一下。
**方法二:**从下面的百度云下载:
链接:https://pan.baidu.com/s/18xFTO8Ps_jPRi3SGWmuVlQ
提取码:6666
数据集介绍
这个数据集也是来自于网上公开的数据集的子集,是一个花分类的数据集,总共有5个类别,分别为daisy(雏菊)、dandelion(蒲公英)、rose(玫瑰)、sunflower(向日葵)、tulip(郁金香)
。
从百度网盘获取的数据集,分为两个文件夹,一个为train、一个为test,train中每个类别都有200张图片,共1000张图片;test中每个类别100张图片,共500张图片。
3. ResNet构建与完善:
之前已经写过了怎么构建resnet,当时的完整代码为:
# 创建block块
class My_Res_Block(nn.Module):
def __init__(self,in_planes,out_planes,stride=1,downsample=None):
'''
:param in_planes: 输入通道数
:param out_planes: 输出通道数
:param stride: 步长,默认为1
:param downsample: 是否下采样,主要是为了res+x中两者大小一样,可以正常相加
'''
super(My_Res_Block, self).__init__()
self.model = nn.Sequential(
# 第一层是1*1卷积层:只改变通道数,不改变大小
nn.Conv2d(in_planes,out_planes,kernel_size=1,stride=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第二层为3*3卷积层,根据上图的介绍,可以看出输入和输出通道数是相同的
nn.Conv2d(out_planes,out_planes,kernel_size=3,stride=stride,padding=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第三层1*1卷积层,输出通道数扩大四倍(上图中由64->256)
nn.Conv2d(out_planes,out_planes*4,kernel_size=1,stride=1),
nn.BatchNorm2d(out_planes*4),
nn.ReLU(),
)
self.relu = nn.ReLU()
self.downsample = downsample
def forward(self,x):
res = x
result = self.model(x)
# 是否需要下采样来保证res与result可以正常相加
if self.downsample is not None:
res = self.downsample(x)
# 残差相加
result += res
# 最后还有一步relu
result = self.relu(result)
return result
# 创建ResNet模型
class My_ResNet(nn.Module):
def __init__(self,layers=50,num_classes=1000,in_planes=64):
'''
:param layers: 我们ResNet的层数,比如常见的50、101等
:param num_classes: 最后输出的类别数,就是softmax层的输出数目
:param in_planes: 我们的block第一个卷积层使用的通道个数
'''
super(My_ResNet, self).__init__()
# 定义一个字典,来存储不同resnet对应的block的个数
# 在官方实现中,使用另外一个参数来接收,这里参考博客,采用一个字典来接收,都类似
self.layers_dict = {
50: [3,4,6,3],
101: [3,4,23,3],
}
self.in_planes = in_planes
# 最开始的一层,还没有进入block
# 输入彩色,通道为3;输出为指定的
self.conv1 = nn.Conv2d(3,self.in_planes,kernel_size=7,stride=2,padding=3)
self.bn1 = nn.BatchNorm2d(self.in_planes)
self.relu = nn.ReLU()
# 根据网络结构要求,大小变为一半
self.maxPool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
# 进入block层
self.block1 = self.make_layers(self.layers_dic[layers][0], stride=1, planes=64)
self.block2 = self.make_layers(self.layers_dic[layers][1], stride=2, planes=128)
self.block3 = self.make_layers(self.layers_dic[layers][2], stride=2, planes=256)
self.block4 = self.make_layers(self.layers_dic[layers][1], stride=2, planes=512)
# 要经历一个平均池化层
self.avgpool = nn.AvgPool2d(7, stride=1)
# 最后接上一个全连接输出层
self.fc = nn.Linear(512 * 4, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_x', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self,layers,stride,planes):
'''
:param planes: 最开始卷积核使用的通道数
:param stride: 步长
:param layers:该层bloack有多少个重复的
:return:
'''
downsample = None
# 判断是否需要下采样
if stride != 1 or self.inplanes != planes*4 :
downsample = nn.Sequential(
nn.Conv2d(self.in_planes, planes * 4, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * 4),
)
layers = []
# 创建第一个block,第一个参数为输入的通道数,第二个参数为第一个卷积核的通道数
layers.append(My_Res_Block(self.in_planes, planes, stride, downsample))
# 输出扩大4倍
self.in_planes = self.in_planes * 4
# 对于18,34层的网络,经过第一个残差块后,输出的特征矩阵通道数与第一层的卷积层个数一样
# 对于50,101,152层的网络,经过第一个残差块后,输出的特征矩阵通道数时第一个卷积层的4倍,因此要将后续残差块的输入特征矩阵通道数调整过来
for i in range(1, layers):
# 输入*4,输出变为最初的
layers.append(My_Res_Block(self.in_planes, planes))
return nn.Sequential(*layers) # 将列表解码
def forward(self,x):
# conv1
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# conv2_x
x = self.maxPool(x)
x = self.block1(x)
# conv3_x
x = self.block2(x)
# conv4_x
x = self.block3(x)
# conv5_x
x = self.block4(x)
# average pool and fc
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
不过,后来在实际运行的时候,才发现很多地方写错了。不过改错也是一种基本能力,毕竟你拿来别人的代码,一般很少可以一次性跑成功,总是需要修改的。(如果不看改错的部分,直接跳到后面的完整代码即可,这里写出来,主要是为了纠正前面文章的错误)
首先,来纠错:
- 有四行关于make_layers的,本来应该为_make_layer写为了make_layers
# 进入block层
# 写错了,改为_make_layer
self.block1 = self.make_layers(self.layers_dic[layers][0], stride=1, planes=64)
self.block2 = self.make_layers(self.layers_dic[layers][1], stride=2, planes=128)
self.block3 = self.make_layers(self.layers_dic[layers][2], stride=2, planes=256)
self.block4 = self.make_layers(self.layers_dic[layers][1], stride=2, planes=512)
- 也是上面这四行代码,字典self.layers_dic居然少些了个t
layers_dic ---》 layers_dict
- 还是上面的问题,最后传入的字典,把3写为了1
self.layers_dic[layers][1] ---> self.layers_dic[layers][3]
- 有两个地方in_planes与planes写岔了:
self.in_planes = self.in_planes * 4
# 改为
self.in_planes = planes * 4
if stride != 1 or self.inplanes != planes*4 :
# 改为
if stride != 1 or self.in_planes != planes*4 :
- 另外,layers变量重复了,需要添加一个temp_layers变量
layers = []
# 创建第一个block,第一个参数为输入的通道数,第二个参数为第一个卷积核的通道数
layers.append(My_Res_Block(self.in_planes, planes, stride, downsample))
# 输出扩大4倍
self.in_planes = self.in_planes * 4
# 对于18,34层的网络,经过第一个残差块后,输出的特征矩阵通道数与第一层的卷积层个数一样
# 对于50,101,152层的网络,经过第一个残差块后,输出的特征矩阵通道数时第一个卷积层的4倍,因此要将后续残差块的输入特征矩阵通道数调整过来
for i in range(1, layers):
# 输入*4,输出变为最初的
layers.append(My_Res_Block(self.in_planes, planes))
return nn.Sequential(*layers) # 将列表解码
# 改为
temp_layers = []
# 创建第一个block,第一个参数为输入的通道数,第二个参数为第一个卷积核的通道数
temp_layers.append(My_Res_Block(self.in_planes, planes, stride, downsample))
# 输出扩大4倍
self.in_planes = planes * 4
# 对于18,34层的网络,经过第一个残差块后,输出的特征矩阵通道数与第一层的卷积层个数一样
# 对于50,101,152层的网络,经过第一个残差块后,输出的特征矩阵通道数时第一个卷积层的4倍,因此要将后续残差块的输入特征矩阵通道数调整过来
for i in range(1, layers):
# 输入*4,输出变为最初的
temp_layers.append(My_Res_Block(self.in_planes, planes))
return nn.Sequential(*temp_layers) # 将列表解码
- 参数初始化部分,kaiming初始化中的参数写错
nn.init.kaiming_normal_(m.weight, mode='fan_x', nonlinearity='relu')
# 改为
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
完成上面修改后,新的resnet代码为:
# ResNet
# 创建block块
class My_Res_Block(nn.Module):
def __init__(self, in_planes, out_planes, stride=1, downsample=None):
'''
:param in_planes: 输入通道数
:param out_planes: 输出通道数
:param stride: 步长,默认为1
:param downsample: 是否下采样,主要是为了res+x中两者大小一样,可以正常相加
'''
super(My_Res_Block, self).__init__()
self.model = nn.Sequential(
# 第一层是1*1卷积层:只改变通道数,不改变大小
nn.Conv2d(in_planes, out_planes, kernel_size=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第二层为3*3卷积层,根据上图的介绍,可以看出输入和输出通道数是相同的
nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=stride, padding=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第三层1*1卷积层,输出通道数扩大四倍(上图中由64->256)
nn.Conv2d(out_planes, out_planes * 4, kernel_size=1),
nn.BatchNorm2d(out_planes * 4),
# nn.ReLU(),
)
self.relu = nn.ReLU()
self.downsample = downsample
def forward(self, x):
res = x
result = self.model(x)
# 是否需要下采样来保证res与result可以正常相加
if self.downsample is not None:
res = self.downsample(x)
# 残差相加
result += res
# 最后还有一步relu
result = self.relu(result)
return result
# 创建ResNet模型
class My_ResNet(nn.Module):
def __init__(self, layers=50, num_classes=5, in_planes=64):
'''
:param layers: 我们ResNet的层数,比如常见的50、101等
:param num_classes: 最后输出的类别数,就是softmax层的输出数目
:param in_planes: 我们的block第一个卷积层使用的通道个数
'''
super(My_ResNet, self).__init__()
# 定义一个字典,来存储不同resnet对应的block的个数
# 在官方实现中,使用另外一个参数来接收,这里参考博客,采用一个字典来接收,都类似
self.layers_dict = {
50: [3, 4, 6, 3],
101: [3, 4, 23, 3],
}
self.in_planes = in_planes
# 最开始的一层,还没有进入block
# 输入彩色,通道为3;输出为指定的
self.conv1 = nn.Conv2d(3, self.in_planes, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(self.in_planes)
self.relu = nn.ReLU()
# 根据网络结构要求,大小变为一半
self.maxPool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 进入block层
self.block1 = self._make_layer(self.layers_dict[layers][0], stride=1, planes=64)
self.block2 = self._make_layer(self.layers_dict[layers][1], stride=2, planes=128)
self.block3 = self._make_layer(self.layers_dict[layers][2], stride=2, planes=256)
self.block4 = self._make_layer(self.layers_dict[layers][3], stride=2, planes=512)
# 要经历一个平均池化层
self.avgpool = nn.AvgPool2d(7, stride=1)
# 最后接上一个全连接输出层
self.fc = nn.Linear(512 * 4, 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.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, layers, stride, planes):
'''
:param planes: 最开始卷积核使用的通道数
:param stride: 步长
:param layers:该层bloack有多少个重复的
:return:
'''
downsample = None
# 判断是否需要下采样
if stride != 1 or self.in_planes != planes * 4:
downsample = nn.Sequential(
nn.Conv2d(self.in_planes, planes * 4, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * 4),
)
temp_layers = []
# 创建第一个block,第一个参数为输入的通道数,第二个参数为第一个卷积核的通道数
temp_layers.append(My_Res_Block(self.in_planes, planes, stride, downsample))
# 输出扩大4倍
self.in_planes = planes * 4
# 对于18,34层的网络,经过第一个残差块后,输出的特征矩阵通道数与第一层的卷积层个数一样
# 对于50,101,152层的网络,经过第一个残差块后,输出的特征矩阵通道数时第一个卷积层的4倍,因此要将后续残差块的输入特征矩阵通道数调整过来
for i in range(1, layers):
# 输入*4,输出变为最初的
temp_layers.append(My_Res_Block(self.in_planes, planes))
return nn.Sequential(*temp_layers) # 将列表解码
def forward(self, x):
# conv1
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# conv2_x
x = self.maxPool(x)
x = self.block1(x)
# conv3_x
x = self.block2(x)
# conv4_x
x = self.block3(x)
# conv5_x
x = self.block4(x)
# average pool and fc
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
4. 数据加载器构建:
这一部分我直接把前面复制过来了,本来想跳过的,但是方便读者就复制过来了
这里,我们需要自己实现Dataset类(用来获取数据和标签,配合Dataloader使用)。
目录结构
在介绍如何写代码前,先说明一下我的目录结构:
data # 文件夹
net_train_images # 下载后解压的数据文件夹
net_test_images # 下载后解压的数据文件夹
图像分类 # 文件夹
resnet.py # 代码文件
基本框架
首先,根据pytorch基础知识,写出Dataset类的基本框架:
class My_Dataset(Dataset):
def __init__(self):
pass
def __len__(self):
pass
def __getitem__(self,idx):
pass
_init_()填写
我们需要定义两个基本的参数filename,transform
:
def __init__(self,filename,transform=None):
self.filename = filename # 文件路径
self.transform = transform # 是否对图片进行变化
而在init方法中,我们需要获取到我们的**图像路径和相应标签,**因此我们定义一个函数来实现该想法:
def __init__(self,filename,transform=None):
self.filename = filename # 文件路径
self.transform = transform # 是否对图片进行变化
# 变化之处
self.image_name,self.label_image = self.operate_file()
operate_file方法实现
由于我们的图片存在于多个文件夹中,因此**决定了我们的filename参数应该是一个文件夹路径,**在我的目录结构中应该为:
'../data/net_train_images'
因此,可以这么写代码(看注释)
def operate_file(self):
# 获取所有的文件夹路径 '../data/net_train_images'下的文件夹
dir_list = os.listdir(self.filename)
# 拼凑出图片完整路径 '../data/net_train_images' + '/' + 'xxx.jpg'
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 由于一个文件夹的所有标签都是同一个值,而字符值必须转为数字值,因此我们使用数字0-4代替标签值
# 将标签每个复制200个
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
for j in range(5):
for i in range(200):
label_list.append(temp_list[j])
return name_list,label_list
这里,我必须解释一下:**为什么np那里需要声明为int64类型?**因为你训练的时候,使用损失函数计算loss(pred,ture_label)
那里,必须要求int类型为int64。
__len__方法填写
这个简单,直接按照固定套路写即可:
def __len__(self):
return len(self.image_name)
__getitem__方法填写
实现的思路:打开图片、对图片下采样为224*224、获取标签、是否需要处理、转为tensor对象、返回值。
具体代码为:(看注释)
def __getitem__(self,idx):
# 由路径打开图片
image = Image.open(self.image_name[idx])
# 下采样: 因为图片大小不同,需要下采样为224*224
trans = transforms.RandomResizedCrop(224)
image = trans(image)
# 获取标签值
label = self.label_image[idx]
# 是否需要处理
if self.transform:
image = self.transform(image)
# 转为tensor对象
label = torch.from_numpy(np.array(label))
return image,label
完整代码
class My_Dataset(Dataset):
def __init__(self,filename,transform=None):
self.filename = filename # 文件路径
self.transform = transform # 是否对图片进行变化
self.image_name,self.label_image = self.operate_file()
def __len__(self):
return len(self.image_name)
def __getitem__(self,idx):
# 由路径打开图片
image = Image.open(self.image_name[idx])
# 下采样: 因为图片大小不同,需要下采样为224*224
trans = transforms.RandomResizedCrop(224)
image = trans(image)
# 获取标签值
label = self.label_image[idx]
# 是否需要处理
if self.transform:
image = self.transform(image)
# 转为tensor对象
label = torch.from_numpy(np.array(label))
return image,label
def operate_file(self):
# 获取所有的文件夹路径 '../data/net_train_images'的文件夹
dir_list = os.listdir(self.filename)
# 拼凑出图片完整路径 '../data/net_train_images' + '/' + 'xxx.jpg'
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 由于一个文件夹的所有标签都是同一个值,而字符值必须转为数字值,因此我们使用数字0-4代替标签值
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
# 将标签每个复制200个
for j in range(5):
for i in range(200):
label_list.append(temp_list[j])
return name_list,label_list
5. 训练代码:
在完成了模型创建、Dataset类构建,就可以开始着手实现训练过程了。
这里,我将训练过程放入了一个名为train
的函数中进行。
def train():
pass
前期准备
首先,创建我们的模型,并将模型放入GPU中:
def train():
model = My_VGG16() # 创建模型
# 将模型放入GPU中
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
声明,为了简便,后面不会写重复的部分,只会写新多出来的部分。
接着,我们定义损失函数,这里采取分类任务常用的交叉熵损失函数:
# 定义损失函数
loss_func = nn.CrossEntropyLoss()
然后,定义优化器,这里采取Adam优化器:
# 定义优化器
optimizer = optim.Adam(params=model.parameters(),lr=0.0002)
下一步,定义每批训练的数据个数并加载数据:
batch_size = 32 # 批量训练大小
# 加载数据
train_set = My_Dataset('../data/net_train_images',transform=transforms.ToTensor())
train_loader = DataLoader(train_set, batch_size, shuffle=True)
训练中
假设训练20次,并定义一个临时变量loss_temp
来存储损失值:
# 训练20次
for i in range(20):
loss_temp = 0 # 临时变量
接着,批量批次接收数据:
for i in range(20):
loss_temp = 0 # 临时变量
for j,(batch_data,batch_label) in enumerate(train_loader):
# 之后的代码都在这个循环中
首先,把数据放入GPU中:
# 数据放入GPU中
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
接着,便是丝滑小连招:
# 梯度清零
optimizer.zero_grad()
# 模型训练
prediction = model(batch_data)
# 损失值
loss = loss_func(prediction,batch_label)
loss_temp += loss.item()
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
当内层结束循环时,打印一下这次的平均损失值:
# 这里新增的
print('[%d] loss: %.3f' % (i+1,loss_temp/len(train_loader)))
最后,我们添加一个训练完成后模型保存的函数即可完成整个训练部分的代码:
# 保存模型
torch.save(model,'ResNet.pkl')
完整代码:
# 训练过程
def train():
batch_size = 10 # 批量训练大小
model = My_ResNet() # 创建模型
# 将模型放入GPU中
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
# 定义损失函数
loss_func = nn.CrossEntropyLoss()
# 定义优化器
optimizer = optim.SGD(params=model.parameters(),lr=0.0002)
# 加载数据
train_set = My_Dataset('../data/net_train_images',transform=transforms.ToTensor())
train_loader = DataLoader(train_set, batch_size, shuffle=True,drop_last=True)
# 训练20次
for i in range(20):
loss_temp = 0 # 临时变量
for j,(batch_data,batch_label) in enumerate(train_loader):
# 数据放入GPU中
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 梯度清零
optimizer.zero_grad()
# 模型训练
prediction = model(batch_data)
# 损失值
loss = loss_func(prediction,batch_label)
loss_temp += loss.item()
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
# 训练完一次打印平均损失值
print('[%d] loss: %.3f' % (i+1,loss_temp/len(train_loader)))
# 保存模型
torch.save(model,'ResNet.pkl')
6. 训练与调整:
第一次尝试:GPU报错
我首先尝试使用batch_size=32,SGD优化器,学习率为0.02进行训练。结果果然不出我所料,报错了,还是vgg那篇的原因,GPU逊爆了,不过这方面我也不懂,所以我决定用我懂的方法解决它,减小batch_size大小。
第二次尝试:SGD、学习率0.02,batch_size=10
于是,我将batch_size改为了10,继续尝试:
[1] loss: 4.396
[2] loss: 2.204
[3] loss: 1.978
[4] loss: 1.642
[5] loss: 1.516
[6] loss: 1.437
[7] loss: 1.417
[8] loss: 1.308
[9] loss: 1.292
[10] loss: 1.254
[11] loss: 1.284
[12] loss: 1.280
[13] loss: 1.257
[14] loss: 1.219
[15] loss: 1.155
[16] loss: 1.129
[17] loss: 1.151
[18] loss: 1.111
[19] loss: 1.177
[20] loss: 1.159
第三次尝试:在上面模型的基础上,改小学习率,变为0.002
从上面的训练应该可以猜测,模型还没有收敛,因此可以继续训练。此时已经保存了模型参数,于是我修改了训练部分的代码:
# model = My_ResNet() # 创建模型
# 将原来创建模型的部分注释掉,改为加载模型
model = torch.load('ResNet.pkl')
并且将学习率改为0.002,结果为:
[1] loss: 1.057
[2] loss: 0.968
[3] loss: 0.983
[4] loss: 0.925
[5] loss: 0.940
[6] loss: 0.942
[7] loss: 0.960
[8] loss: 0.976
[9] loss: 0.925
[10] loss: 0.927
[11] loss: 0.935
[12] loss: 0.897
[13] loss: 0.913
[14] loss: 0.913
[15] loss: 0.927
[16] loss: 0.917
[17] loss: 0.926
[18] loss: 0.903
[19] loss: 0.929
[20] loss: 0.856
效果似乎还不错,毕竟损失值在逐渐变小。好在我们把模型保存了,后面把测试代码写完了可以用来验证一下效果如何。
7. 加载预训练模型:
我们在网上找到resnet50的预训练模型,下载到本地保存,我保存的路径为:
pretrained_model # 文件夹
resnet50-0676ba61.pth # 预训练模型
resnet.py # 程序文件
如果你想下载,可以直接在网上搜resnet预训练模型下载,这个网上很多,我就不贴链接了。
由于自己写的模型总会与官方有出入,因此加载预训练模型,建议采用官方提供的resnet50模型:
# 导入
from torchvision.models import resnet50
然后,需要把一些代码注释掉,然后加入预训练的代码:
# model = My_ResNet() # 创建模型
# 预训练模型
model = resnet50(num_classes=5) # 指定输出个数
model.load_state_dict(torch.load(r'.\pretrained_model\resnet50-0676ba61.pth'))
因为我们需要将我们的模型和预训练模型对比,所以在运行之前,记得改一下模型保存部分的路径,这样避免文件名重叠被覆盖了。
运行结果如下:
[1] loss: 2.527
[2] loss: 0.664
[3] loss: 0.504
[4] loss: 0.407
[5] loss: 0.360
[6] loss: 0.327
[7] loss: 0.291
[8] loss: 0.266
[9] loss: 0.250
[10] loss: 0.232
[11] loss: 0.239
[12] loss: 0.210
[13] loss: 0.155
[14] loss: 0.194
[15] loss: 0.151
[16] loss: 0.197
[17] loss: 0.194
[18] loss: 0.199
[19] loss: 0.156
[20] loss: 0.195
可以看到,结果比我们自己训练的好多了,不过,这也是我们本身没有训练好有关。
8. 测试代码:
测试部分的代码,需要写两个部分,一是测试集数据的加载,二是测试过程代码。
数据集加载的代码,可以仿照训练集加载来写 ,我直接把代码放在这里,大家可以自行参考:
# 继承自训练数据加载器,只修改一点点的地方
class My_Dataset_test(My_Dataset):
def operate_file(self):
# 获取所有的文件夹路径
dir_list = os.listdir(self.filename)
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 将标签每个复制一百个
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
for j in range(5):
for i in range(100): # 只修改了这里
label_list.append(temp_list[j])
return name_list,label_list
另外,就是测试过程的代码,也很简单,可以看注释:
def test(model):
# 批量数目
batch_size = 10
# 预测正确个数
correct = 0
# 加载数据
test_set = My_Dataset_test('../data/net_test_images', transform=transforms.ToTensor())
test_loader = DataLoader(test_set, batch_size, shuffle=False)
# 开始
for batch_data,batch_label in test_loader:
# 放入GPU中
batch_data, batch_label = batch_data.cuda(), batch_label.cuda()
# 预测
prediction = model(batch_data)
# 将预测值中最大的索引取出,其对应了不同类别值
predicted = torch.max(prediction.data, 1)[1]
# 获取准确个数
correct += (predicted == batch_label).sum()
print('准确率: %.2f %%' % (100 * correct / 500)) # 因为总共500个测试数据
最后,我们定义一个运行测试函数的代码,主要作用是加载我们已经训练好的模型:
# 测试代码运行
def run_test():
# 加载模型
model = torch.load('ResNet.pkl') # 自己的模型
# model = torch.load('ResNet1.pkl') # 预训练模型训练的模型
test(model)
9. 测试:
我们来测试一下自己训练的和预训练模型的结果差别。
# 自己训练的
准确率: 22.60 %
# 预训练的
准确率: 25.60 %
woc,怎么回事?这个准确率怎么这么低?
我表示很纳闷,因为之前VGG16训练的时候损失值降低到0.8左右也有58%左右的准确率呀,而预训练的ResNet-50好歹也降低到0.2左右,怎么准确率这么低呢?
我看了下代码,发现一个问题,就是我预训练模型,没有修改输出个数,大家都知道ImageNet比赛的输出为1000个,因此我怀疑是这个的问题,但是VGG16当时也忘记修改了也没有低呀。
本着试试的态度,我修改了代码:
# 预训练模型
model = resnet50()
model.load_state_dict(torch.load(r'.\pretrained_model\resnet50-0676ba61.pth'))
# 提取fc层中固定的参数
fc_features = model.fc.in_features
# 修改类别为5
model.fc = nn.Linear(fc_features, 5)
再次运行:
[1] loss: 1.286
[2] loss: 0.809
[3] loss: 0.617
[4] loss: 0.545
[5] loss: 0.463
[6] loss: 0.447
[7] loss: 0.380
[8] loss: 0.352
[9] loss: 0.330
[10] loss: 0.272
[11] loss: 0.276
[12] loss: 0.265
[13] loss: 0.254
[14] loss: 0.271
[15] loss: 0.265
[16] loss: 0.221
[17] loss: 0.209
[18] loss: 0.196
[19] loss: 0.209
[20] loss: 0.153
准确率:
准确率: 23.80 %
??这下我有点蒙蔽了,我想了很久没有想到为什么?难道是过拟合了?
这个问题我暂时不清楚,后面如果有机会再来解决吧,我的想法是如果过拟合了,那么可以使用数据增强试试,看看能不能提高精度。
10. 总结:
这篇文章,主要目的是纠正之前那篇文章的笔误,其次就是尝试调整学习率训练,不过说是调整,其实就是训练一次,改变一次学习率。我觉得应该可以随时暂停,然后调整学习率,再训练。不过,怎么实现我不清楚。
另外,这次文章还留下一个问题,就是准确率为什么这么低?希望后面可以解决。
完整代码
# author: baiCai
import os
import numpy as np
from torch.utils.data import DataLoader,Dataset
from torchvision import transforms
from torch import optim
import torch
from torch import nn
from torch.nn import functional as F
from PIL import Image
from torchvision.models import resnet50,ResNet
# ResNet
# 创建block块
class My_Res_Block(nn.Module):
def __init__(self, in_planes, out_planes, stride=1, downsample=None):
'''
:param in_planes: 输入通道数
:param out_planes: 输出通道数
:param stride: 步长,默认为1
:param downsample: 是否下采样,主要是为了res+x中两者大小一样,可以正常相加
'''
super(My_Res_Block, self).__init__()
self.model = nn.Sequential(
# 第一层是1*1卷积层:只改变通道数,不改变大小
nn.Conv2d(in_planes, out_planes, kernel_size=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第二层为3*3卷积层,根据上图的介绍,可以看出输入和输出通道数是相同的
nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=stride, padding=1),
nn.BatchNorm2d(out_planes),
nn.ReLU(),
# 第三层1*1卷积层,输出通道数扩大四倍(上图中由64->256)
nn.Conv2d(out_planes, out_planes * 4, kernel_size=1),
nn.BatchNorm2d(out_planes * 4),
# nn.ReLU(),
)
self.relu = nn.ReLU()
self.downsample = downsample
def forward(self, x):
res = x
result = self.model(x)
# 是否需要下采样来保证res与result可以正常相加
if self.downsample is not None:
res = self.downsample(x)
# 残差相加
result += res
# 最后还有一步relu
result = self.relu(result)
return result
# 创建ResNet模型
class My_ResNet(nn.Module):
def __init__(self, layers=50, num_classes=5, in_planes=64):
'''
:param layers: 我们ResNet的层数,比如常见的50、101等
:param num_classes: 最后输出的类别数,就是softmax层的输出数目
:param in_planes: 我们的block第一个卷积层使用的通道个数
'''
super(My_ResNet, self).__init__()
# 定义一个字典,来存储不同resnet对应的block的个数
# 在官方实现中,使用另外一个参数来接收,这里参考博客,采用一个字典来接收,都类似
self.layers_dict = {
50: [3, 4, 6, 3],
101: [3, 4, 23, 3],
}
self.in_planes = in_planes
# 最开始的一层,还没有进入block
# 输入彩色,通道为3;输出为指定的
self.conv1 = nn.Conv2d(3, self.in_planes, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(self.in_planes)
self.relu = nn.ReLU()
# 根据网络结构要求,大小变为一半
self.maxPool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 进入block层
self.block1 = self._make_layer(self.layers_dict[layers][0], stride=1, planes=64)
self.block2 = self._make_layer(self.layers_dict[layers][1], stride=2, planes=128)
self.block3 = self._make_layer(self.layers_dict[layers][2], stride=2, planes=256)
self.block4 = self._make_layer(self.layers_dict[layers][3], stride=2, planes=512)
# 要经历一个平均池化层
self.avgpool = nn.AvgPool2d(7, stride=1)
# 最后接上一个全连接输出层
self.fc = nn.Linear(512 * 4, 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.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def _make_layer(self, layers, stride, planes):
'''
:param planes: 最开始卷积核使用的通道数
:param stride: 步长
:param layers:该层bloack有多少个重复的
:return:
'''
downsample = None
# 判断是否需要下采样
if stride != 1 or self.in_planes != planes * 4:
downsample = nn.Sequential(
nn.Conv2d(self.in_planes, planes * 4, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * 4),
)
temp_layers = []
# 创建第一个block,第一个参数为输入的通道数,第二个参数为第一个卷积核的通道数
temp_layers.append(My_Res_Block(self.in_planes, planes, stride, downsample))
# 输出扩大4倍
self.in_planes = planes * 4
# 对于18,34层的网络,经过第一个残差块后,输出的特征矩阵通道数与第一层的卷积层个数一样
# 对于50,101,152层的网络,经过第一个残差块后,输出的特征矩阵通道数时第一个卷积层的4倍,因此要将后续残差块的输入特征矩阵通道数调整过来
for i in range(1, layers):
# 输入*4,输出变为最初的
temp_layers.append(My_Res_Block(self.in_planes, planes))
return nn.Sequential(*temp_layers) # 将列表解码
def forward(self, x):
# conv1
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
# conv2_x
x = self.maxPool(x)
x = self.block1(x)
# conv3_x
x = self.block2(x)
# conv4_x
x = self.block3(x)
# conv5_x
x = self.block4(x)
# average pool and fc
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
# 模型输入:224*224*3
class My_Dataset(Dataset):
def __init__(self,filename,transform=None):
self.filename = filename # 文件路径
self.transform = transform # 是否对图片进行变化
self.image_name,self.label_image = self.operate_file()
def __len__(self):
return len(self.image_name)
def __getitem__(self,idx):
# 由路径打开图片
image = Image.open(self.image_name[idx])
# 下采样: 因为图片大小不同,需要下采样为224*224
trans = transforms.RandomResizedCrop(224)
image = trans(image)
# 获取标签值
label = self.label_image[idx]
# 是否需要处理
if self.transform:
image = self.transform(image)
# image = image.reshape(1,image.size(0),image.size(1),image.size(2))
# print('变换前',image.size())
# image = interpolate(image, size=(227, 227))
# image = image.reshape(image.size(1),image.size(2),image.size(3))
# print('变换后', image.size())
# 转为tensor对象
label = torch.from_numpy(np.array(label))
return image,label
def operate_file(self):
# 获取所有的文件夹路径 '../data/net_train_images'的文件夹
dir_list = os.listdir(self.filename)
# 拼凑出图片完整路径 '../data/net_train_images' + '/' + 'xxx.jpg'
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 由于一个文件夹的所有标签都是同一个值,而字符值必须转为数字值,因此我们使用数字0-4代替标签值
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
# 将标签每个复制200个
for j in range(5):
for i in range(200):
label_list.append(temp_list[j])
return name_list,label_list
class My_Dataset_test(My_Dataset):
def operate_file(self):
# 获取所有的文件夹路径
dir_list = os.listdir(self.filename)
full_path = [self.filename+'/'+name for name in dir_list]
# 获取里面的图片名字
name_list = []
for i,v in enumerate(full_path):
temp = os.listdir(v)
temp_list = [v+'/'+j for j in temp]
name_list.extend(temp_list)
# 将标签每个复制一百个
label_list = []
temp_list = np.array([0,1,2,3,4],dtype=np.int64) # 用数字代表不同类别
for j in range(5):
for i in range(100): # 只修改了这里
label_list.append(temp_list[j])
return name_list,label_list
# 训练过程
def train():
batch_size = 10 # 批量训练大小
# model = My_ResNet() # 创建模型
# 预训练模型
model = resnet50()
model.load_state_dict(torch.load(r'.\pretrained_model\resnet50-0676ba61.pth'))
# 提取fc层中固定的参数
fc_features = model.fc.in_features
# 修改类别为5
model.fc = nn.Linear(fc_features, 5)
# model = torch.load('ResNet.pkl')
# 将模型放入GPU中
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
# 定义损失函数
loss_func = nn.CrossEntropyLoss()
# 定义优化器
optimizer = optim.SGD(params=model.parameters(),lr=0.002)
# 加载数据
train_set = My_Dataset('../data/net_train_images',transform=transforms.ToTensor())
train_loader = DataLoader(train_set, batch_size, shuffle=True,drop_last=True)
# 训练20次
for i in range(20):
loss_temp = 0 # 临时变量
for j,(batch_data,batch_label) in enumerate(train_loader):
# 数据放入GPU中
batch_data,batch_label = batch_data.cuda(),batch_label.cuda()
# 梯度清零
optimizer.zero_grad()
# 模型训练
prediction = model(batch_data)
# 损失值
loss = loss_func(prediction,batch_label)
loss_temp += loss.item()
# 反向传播
loss.backward()
# 梯度更新
optimizer.step()
# 训练完一次打印平均损失值
print('[%d] loss: %.3f' % (i+1,loss_temp/len(train_loader)))
# 保存模型
torch.save(model,'ResNet1.pkl')
test(model) # 这样可以不用加载模型,直接传给测试代码
def test(model):
# 批量数目
batch_size = 5
# 预测正确个数
correct = 0
# 加载数据
test_set = My_Dataset_test('../data/net_test_images', transform=transforms.ToTensor())
test_loader = DataLoader(test_set, batch_size, shuffle=False)
# 开始
for batch_data,batch_label in test_loader:
# 放入GPU中
batch_data, batch_label = batch_data.cuda(), batch_label.cuda()
# 预测
prediction = model(batch_data)
# 将预测值中最大的索引取出,其对应了不同类别值
predicted = torch.max(prediction.data, 1)[1]
# 获取准确个数
correct += (predicted == batch_label).sum()
print('准确率: %.2f %%' % (100 * correct / 500)) # 因为总共500个测试数据
# 测试代码运行
def run_test():
# 加载模型
# model = torch.load('ResNet.pkl') # 自己的模型
model = torch.load('ResNet1.pkl') # 预训练模型训练的模型
test(model)
if __name__ == '__main__':
train() # 训练
# run_test() # 测试