使用pytorch搭建ResNet并基于迁移学习训练
model.py文件
ResNet在PyTorch的官方代码中共有5种不同深度的结构,深度分别为18、34、50、101、152(各种网络的深度指的是“需要通过训练更新参数”的层数,如卷积层,全连接层等)
根据Block类型,可以将这五种ResNet分为两类:(1) 一种基于BasicBlock,浅层网络ResNet18, 34都由BasicBlock搭成;(2) 另一种基于Bottleneck,深层网络ResNet50, 101, 152乃至更深的网络,都由Bottleneck搭成。Block相当于积木,每个layer都由若干Block搭建而成,再由layer组成整个网络。每种ResNet都是4个layer(不算一开始的7×7 7\times77×7卷积层和3×3 3\times33×3maxpooling层),如图1,conv2_x对应layer1,conv3_x对应layer2,conv4_x对应layer3,conv5_x对应layer4。方框中的“×2 \times2×2”、“×3 \times3×3”等指的是该layer由几个相同的结构组成。
直角矩形框指的是卷积层或pooling层,如“3×3,64,stride=2,padding=3 3 \times 3, 64, stride = 2, padding = 33×3,64,stride=2,padding=3”指该卷积层kernel size为3×3 3 \times 33×3,输出channel数为64,步长为2,padding为3。矩形框代表的层种类在方框右侧标注,如“conv1”。
这些都了解之后,图中所示结构就已经很清楚了,顺便提一句,卷积层的输出尺寸(单边)是这么计算的:
以conv1为例,input_size=224,padding=3,kernel_size=7,stride=2 input_size=224, padding=3, kernel_size=7, stride=2input_size=224,padding=3,kernel_size=7,stride=2。将它们带入公式,发现除不尽,所以前面的除法准确地说是求商。可以得出output_size=112 output_size=112output_size=112。下面的max pooling类似,求得output_size=56 output_size=56output_size=56。
import torch.nn as nn
import torch
class BasicBlock(nn.Module):#定义残差结构18层 34层
expansion = 1#对应残差结构的主分支的卷积核个数有没有发生变化,如18层和34层残差结构每个残差结构第一层和第二层是一样的
def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):#定义初始函数,输入特征矩阵深度和输出特征矩阵深度(对应主分支上卷积核个数),下采样深度默认为none(对应虚线的残差结构,降维)
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,#卷积层1,首先输入特征矩阵深度,输出矩阵深度,卷积核大小3,步距,padding默认唯一,步距为1默认不会改变高和宽,偏置bias不使用
kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)#定义bn层1,输入参数对应卷积层1输出矩阵深度
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#定义下采样方法
def forward(self, x):#定义正向传播过程
identity = x#定义捷径上的输出值
if self.downsample is not None:#如果下采样函数为none对应实线残差结构,如果输入了downsample,则将x输入到下采样函数中,计算出捷径分支的输出
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 += identity#将输出+捷径上的输出再进行relu激活函数
out = self.relu(out)
return out
class Bottleneck(nn.Module):#定义50层 101层 152层残差结构
expansion = 4#残差结构所使用的残差结构的变化,第三层卷积层个数是第一层和第二层卷积层个数的4倍
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,#out_channels指的是第一层和第二层卷积核个数
kernel_size=1, stride=1, bias=False) # squeeze channels
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)#卷积核大小为3
self.bn2 = nn.BatchNorm2d(width)
# -----------------------------------------
self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion,#输出矩阵深度,第三层卷积层个数是第一层和第二层卷积层个数的4倍所以要乘以4也就是前面定义的expansion
kernel_size=1, stride=1, bias=False) # unsqueeze channels
self.bn3 = nn.BatchNorm2d(out_channel*self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
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)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):#定义网络框架部分
def __init__(self,
block,#残差结构
blocks_num,#所使用残差结构残差数目,例:如果是34层的残差数目则为3 4 6 3的list列表
num_classes=1000,#训练集的分类个数
include_top=True,#方便在ResNet基础上搭建更复杂的网络
groups=1,
width_per_group=64):
super(ResNet, self).__init__()
self.include_top = include_top#将include_top传入到类变量当中
self.in_channel = 64#输入特征矩阵深度是通过max pool得到的特征矩阵深度
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)#定义第一层卷积层 7x7 为了让输入矩阵的高和宽缩减为原来的一半
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, blocks_num[0])#对应conv2_x所对应的一切残差结构利用所定义的_make_layer函数
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:
self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # output size = (1, 1)#平均池化,通过自适应平均池化下采样程,所得到的特征矩阵高和宽都为1和1
self.fc = nn.Linear(512 * block.expansion, num_classes)#全连接层 展平后节点个数高和宽都为1,也就与深度相关,输出节点个数就=分类类别个数
for m in self.modules():#对卷积层进行初始化操作
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
def _make_layer(self, block, channel, block_num, stride=1):#channel对应残差结构中卷积层1所对应的卷积核个数
downsample = None
if stride != 1 or self.in_channel != channel * block.expansion:#对于18层和34会直接跳过该if语句
downsample = nn.Sequential(#对于50层 101层 152层会进入if语句生成下采样函数
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):#将之后实线残差结构加入进去,从第二层开始stride=2
layers.append(block(self.in_channel,
channel,
groups=self.groups,
width_per_group=self.width_per_group))
return nn.Sequential(*layers)#通过非关键字参数形式传入到nn.Sequential函数
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)
if self.include_top:
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
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)
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)
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)
def resnext50_32x4d(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnext50_32x4d-7cdf4587.pth
groups = 32
width_per_group = 4
return ResNet(Bottleneck, [3, 4, 6, 3],
num_classes=num_classes,
include_top=include_top,
groups=groups,
width_per_group=width_per_group)
def resnext101_32x8d(num_classes=1000, include_top=True):
# https://download.pytorch.org/models/resnext101_32x8d-8ba56ff5.pth
groups = 32
width_per_group = 8
return ResNet(Bottleneck, [3, 4, 23, 3],
num_classes=num_classes,
include_top=include_top,
groups=groups,
width_per_group=width_per_group)
trai.py文件
import os
import json
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets
from tqdm import tqdm
from model import resnet34
#基于迁移学习方法使用少量epoch即可达到很高的准确率
def main():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
data_transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
"val": transforms.Compose([transforms.Resize(256),#将图片最小边缩短为256
transforms.CenterCrop(224),#再使用中心裁剪裁剪为224
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
data_root = os.path.abspath(os.path.join(os.getcwd(), "../..")) # get data root path
image_path = os.path.join(data_root, "data_set", "flower_data") # flower data set path
assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
transform=data_transform["train"])
train_num = len(train_dataset)
# {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
batch_size = 16
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # number of workers
print('Using {} dataloader workers every process'.format(0))
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size, shuffle=True,
num_workers=0)
validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"),
transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
batch_size=batch_size, shuffle=False,
num_workers=0)
print("using {} images for training, {} images for validation.".format(train_num,
val_num))
net = resnet34()#实例化resnet34
# load pretrain weights
# download url: https://download.pytorch.org/models/resnet34-333f7ec4.pth
model_weight_path = "./resnet34-333f7ec4.pth"
assert os.path.exists(model_weight_path), "file {} does not exist.".format(model_weight_path)
net.load_state_dict(torch.load(model_weight_path, map_location=device))#载入模型权重
# for param in net.parameters():
# param.requires_grad = False
# change fc layer structure
in_channel = net.fc.in_features#net.fc代表定义网络结构时的fc
net.fc = nn.Linear(in_channel, 5)#重新定义分类个数,结合数据集改动
net.to(device)
# define loss function
loss_function = nn.CrossEntropyLoss()
# construct an optimizer
params = [p for p in net.parameters() if p.requires_grad]
optimizer = optim.Adam(params, lr=0.0001)
epochs = 3
best_acc = 0.0
save_path = './resNet34.pth'
train_steps = len(train_loader)
for epoch in range(epochs):
# train
net.train()
running_loss = 0.0
train_bar = tqdm(train_loader)
for step, data in enumerate(train_bar):
images, labels = data
optimizer.zero_grad()
logits = net(images.to(device))
loss = loss_function(logits, labels.to(device))
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
epochs,
loss)
# validate
net.eval()
acc = 0.0 # accumulate accurate number / epoch
with torch.no_grad():
val_bar = tqdm(validate_loader)
for val_data in val_bar:
val_images, val_labels = val_data
outputs = net(val_images.to(device))
# loss = loss_function(outputs, test_labels)
predict_y = torch.max(outputs, dim=1)[1]
acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
val_bar.desc = "valid epoch[{}/{}]".format(epoch + 1,
epochs)
val_accurate = acc / val_num
print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
(epoch + 1, running_loss / train_steps, val_accurate))
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
print('Finished Training')
if __name__ == '__main__':
main()
predict.py文件
import os
import json
import torch
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
from model import resnet34
def main():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
data_transform = transforms.Compose(
[transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
# load image
img_path = "../../data_set/flower_data/flower_photos/roses/110472418_87b6a3aa98_m.jpg"
assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
img = Image.open(img_path)
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)
# read class_indict
json_path = './class_indices.json'
assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
json_file = open(json_path, "r")
class_indict = json.load(json_file)
# create model
model = resnet34(num_classes=5).to(device)#实例化模型
# load model weights
weights_path = "./resNet34.pth"
assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
model.load_state_dict(torch.load(weights_path, map_location=device))
# prediction
model.eval()#使用eval模式
with torch.no_grad():#不对损失梯度进行跟踪
# predict class
output = torch.squeeze(model(img.to(device))).cpu()#压缩batch维度
predict = torch.softmax(output, dim=0)
predict_cla = torch.argmax(predict).numpy()
print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
predict[predict_cla].numpy())
plt.title(print_res)
print(print_res)
plt.show()
if __name__ == '__main__':
main()
迁移学习简介
*迁移学习是一种机器学习的方法,指的是一个预训练的模型被重新用在另一个任务中。*
迁移学习(Transfer learning) 顾名思义就是把已训练好的模型(预训练模型)参数迁移到新的模型来帮助新模型训练。考虑到大部分数据或任务都是存在相关性的,所以通过迁移学习我们可以将已经学到的模型参数(也可理解为模型学到的知识)通过某种方式来分享给新模型从而加快并优化模型的学习效率不用像大多数网络那样从零学习。
其中,实现迁移学习有以下三种手段:
- Transfer Learning:冻结预训练模型的全部卷积层,只训练自己定制的全连接层。
- Extract Feature Vector:先计算出预训练模型的卷积层对所有训练和测试数据的特征向量,然后抛开预训练模型,只训练自己定制的简配版全连接网络。
- Fine-tuning:冻结预训练模型的部分卷积层(通常是靠近输入的多数卷积层,因为这些层保留了大量底层信息)甚至不冻结任何网络层,训练剩下的卷积层(通常是靠近输出的部分卷积层)和全连接层。
习(Transfer learning) 顾名思义就是把已训练好的模型(预训练模型)参数迁移到新的模型来帮助新模型训练。考虑到大部分数据或任务都是存在相关性的,所以通过迁移学习我们可以将已经学到的模型参数(也可理解为模型学到的知识)通过某种方式来分享给新模型从而加快并优化模型的学习效率不用像大多数网络那样从零学习。
其中,实现迁移学习有以下三种手段:
- Transfer Learning:冻结预训练模型的全部卷积层,只训练自己定制的全连接层。
- Extract Feature Vector:先计算出预训练模型的卷积层对所有训练和测试数据的特征向量,然后抛开预训练模型,只训练自己定制的简配版全连接网络。
- Fine-tuning:冻结预训练模型的部分卷积层(通常是靠近输入的多数卷积层,因为这些层保留了大量底层信息)甚至不冻结任何网络层,训练剩下的卷积层(通常是靠近输出的部分卷积层)和全连接层。