前言
书接上回,使用自己从零手写的resnet18在cifar10能实现了80的正确率,接下来我们利用torchvision的已训练的模型resnet18进行迁移学习测试(迁移学习通常是用在自己自定义的比较少的数据集,用已经在更多的数据集上训练过的模型在自己的应用场景下训练的操作),而cifar10的数据集已经不少了,不过不管了,试试看看效果。
训练主函数(main.py)
import torch
from torch.utils.data import DataLoader
"""
DataLoader:是一种数据加载器,可设定batch大小,是否随机洗牌、并行加载(多线程加载,多个进程(多CPU加载))
可加载自定义数据集数据
数据拼接(补齐、截断)
配合transform实现数据预处理,数据统计
DATALOADER可以利用for循环当成迭代器使用(迭代器从零设计见 人工智能总结专栏3 线性回归从零设计)
"""
from torchvision import datasets, transforms
"""
datasets:用于下载一些常见数据集如MINIST、CIFAR-10、CIFAR-100、ImageNet等
transforms:进行图像的预处理操作,如随机裁剪、尺寸调整、归一化、颜色变换、旋转等
"""
from torch import nn, optim
"""
nn:是neural networks的缩写,用于构建神经网络层、损失函数(MSE Cross Entropy)等,以及用于优化神经网络的优化器(SGD、adam等)
optim:提供了多种常见的优化算法,如SGD、Adam、RMSProp等
"""
import matplotlib.pyplot as plt # 用于显示图片
from torchvision.models import resnet18
"""
网络训练六步走:(在本程序中总结的六步,不只限于六步)
1、加载数据集
2、加载模型到GPU或CPU
3、加载损失函数到GPU或CPU
4、定义优化器类型和相关参数
5、迭代:计算损失函数,梯度清零、计算梯度、更新梯度
6、保存网络、验证效果
"""
class Flatten(nn.Module):
def __init__(self):
super(Flatten,self).__init__()
def forward(self,x):
shape = torch.prod(torch.tensor(x.shape[1:])).item() #计算除barth第一维之外的所有维度大小累乘
return x.view(-1, shape) #将一个多维数据,变成二维数据,第一维是batch
# 批次大小
batchsz = 100
if __name__ == "__main__":
# 加载cifar10训练集 50000张训练集和10000张测试集
cifar_train = datasets.CIFAR10(root='cifar', # 数据集放在文件名cifar的文件夹下 root=用于提示自己这个参数作用(也可不写)
train=True,
transform=transforms.Compose([
transforms.Resize((32, 32)), # Resize图片长宽为32*32
transforms.ToTensor(), # 默认是JPEG,PNG,GIF,BMP,TIF 等PIL格式,要转换为tensor格式
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
# 数据预处理,将每个通道的像素取值转化合适范围,提高训练稳定性
# 先将数据归一化后,每个通道像素减去对应均值mean再除于标准差std如 (x-0.485)/0.229
]), download=True # 如果文件夹下没有cifar10会进行下载
)
cifar_train = DataLoader(cifar_train, # 得到数据加载器
batch_size=batchsz, # 输入每次迭代返回批次大小
shuffle=True) # 随机打乱数据
cifar_test = datasets.CIFAR10(root='cifar', # 数据集放在文件名cifar的文件夹下 root=用于提示自己这个参数作用(也可不写)
train=False, # 不用于训练
transform=transforms.Compose([
transforms.Resize((32, 32)), # Resize图片长宽为32*32
transforms.ToTensor(), # 默认是JPEG,PNG,GIF,BMP,TIF 等PIL格式,要转换为tensor格式
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
# 数据预处理,将每个通道的像素取值转化合适范围,提高训练稳定性
# 先将数据归一化后,每个通道像素减去对应均值mean再除于标准差std如 (x-0.485)/0.229
]), download=True # 如果文件夹下没有cifar10会进行下载
)
cifar_test = DataLoader(cifar_test, # 得到数据加载器
batch_size=batchsz, # 输入每次迭代返回批次大小
shuffle=True) # 随机打乱数据
# 如果想返回第一个batch的数据,得先通过iter转换为严格的迭代器才能利用__next__方法进行查看
# 但是cifar_train又能直接用for进行迭代
x, label = iter(cifar_train).__next__()
print('x:', x.shape, 'label:', label.shape) # x是100个batch也就是100张图
"""
#如果想显示一张图运行一下代码,cifar10分辨率是32*32比较低,比较模糊
#加载数据时注释掉这个transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])会看清楚些
pil_image=transforms.ToPILImage()(x[1])
print(pil_image)
plt.imshow(pil_image)
plt.axis('off')#不显示坐标轴
plt.show()
"""
if torch.cuda.is_available():
device = torch.device('cuda')
print("CUDA is available")
else:
device = torch.device('cpu')
print("CUDA is not available")
# 2、加载模型到GPU或CPU
trained_model =resnet18(pretrained=True)
model = nn.Sequential(*list(trained_model.children())[:-1], # list先将网络转换为列表,[:-1]从0取到17层(不包含最后一层)
#这部分网络测试输出为[b,512,1,1]
Flatten(), #[b,512,1,1]=>[b,512]
nn.Linear(512,10) #这么操作目的是实得网络的分类数量使用于此应用场景
).to(device)
print(model) # 打印网络结构
# 3、加载损失函数到GPU或CPU
criteon = nn.CrossEntropyLoss().to(device)
# 4、定义优化器类型和相关参数
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(1000):
model.train()
# 通常网络会包含 self.dropout = nn.Dropout(p=0.5) self.batchnorm2 = nn.BatchNorm2d(32)操作
# 执行model.train()那么训练时,让这些正则化操作有效 执行model.eval()是正则化操作会无效或改变
"""
正则化:减轻过拟合
dropout:随机丢弃一些神经元的输出
batch_normal:有效时,用当前batch均值和标准差进行 normalize操作,无效使用之前累计的平均均值和方差进行normalize操作
normalize:减去均值除标准差
"""
for batchidx, (x, label) in enumerate(cifar_train):
# enumerate用于返回:batchidx
x, label = x.to(device), label.to(device) # 将数据放入GPU
logits = model(x) # 网络返回的是shape[batch,10]的数据
loss = criteon(logits, label)
"""
交叉熵公式:
p(i)是输入图像x的情况下,属于某个类别的真实概率,在这里属于类1,概率为1,p(i!=1)的概率都为零
q(i)是预测某个类别的概率
-p(i)logq(i)将i遍历所有类别求和结果就是交叉熵。
CrossEntropyLoss(logits,label):
logits:输入[batch,10]:这里10为类别,该函数会计算sofmax将-1到1左右的值转换为0-1的数值
label:输入对应类别0-9即可,函数会根据交叉熵公式计算,所以label输入应为[batch]
batch通常不为1:函数会计算多个图片对应交叉熵运算平均值
"""
optimizer.zero_grad() # 每次计算梯度的结果是累计的,所以运算前要清零梯度信息
loss.backward() # 计算梯度
optimizer.step() # 更新梯度
print(epoch, loss.item()) # 每个epoch都会打印一次当前损失
model.eval() # 开启测试模式
# 每个训练的epoch都进行一次验证
acc_record = 0
with torch.no_grad(): # 表示接下来操作,计算图不需要计算梯度,提高代码运行效率
total_correct = 0
total_num = 0
for x, label in cifar_test:
x, label = x.to(device), label.to(device)
logits = model(x) # logits[batch,10]
pred = logits.argmax(dim=1) # 在dim=1维度,也就是第二个维度,获取该维度最大值的索引,也就是预测的类别
# pred[batch]
total_correct += torch.eq(pred, label).float().sum().item()
# 计算预测label与真实label相同的图片数量,.float()转为true为1.0 false为0.0,
# sum()所有正确预测求和 item()取数值
total_num += x.size(0) # 取一次预测所有图片数量,并迭代获得所有图片的数量 测试是总共有10000张测试集
acc = total_correct / total_num # 求正确预测概率
if acc_record < acc:
torch.save(model.state_dict(), "transfer_resnet18_model.pth")
print(epoch, "测试集10000张图片正确预测概率:", acc)
测试函数(test.py)
import torch
from torchvision.models import resnet18
import torch.nn as nn
from torchvision import datasets,transforms
from torch.utils.data import DataLoader
save_path = "transfer_resnet18_model.pth"
batchsz=50
class Flatten(nn.Module):
def __init__(self):
super(Flatten,self).__init__()
def forward(self,x):
shape = torch.prod(torch.tensor(x.shape[1:])).item() #计算除barth第一维之外的所有维度大小累乘
return x.view(-1, shape) #将一个多维数据,变成二维数据,第一维是batch
if __name__ =="__main__":
cifar_test= datasets.CIFAR10(root='cifar', #数据集放在文件名cifar的文件夹下 root=用于提示自己这个参数作用(也可不写)
train=False ,#不用于训练
transform=transforms.Compose([
transforms.Resize((32,32)), #Resize图片长宽为32*32
transforms.ToTensor(), #默认是JPEG,PNG,GIF,BMP,TIF 等PIL格式,要转换为tensor格式
transforms.Normalize(mean=[0.485,0.456,0.406],std=[0.229,0.224,0.225])
#数据预处理,将每个通道的像素取值转化合适范围,提高训练稳定性
#先将数据归一化后,每个通道像素减去对应均值mean再除于标准差std如 (x-0.485)/0.229
]),download=True #如果文件夹下没有cifar10会进行下载
)
cifar_test =DataLoader(cifar_test, #得到数据加载器
batch_size=batchsz,#输入每次迭代返回批次大小
shuffle=True)# 随机打乱数据
if torch.cuda.is_available():
device = torch.device('cuda')
print("CUDA is available")
else:
device = torch.device('cpu')
print("CUDA is not available")
trained_model = resnet18(pretrained=True)
model = nn.Sequential(*list(trained_model.children())[:-1], # list先将网络转换为列表,[:-1]从0取到17层(不包含最后一层)
# 这部分网络测试输出为[b,512,1,1]
Flatten(), # [b,512,1,1]=>[b,512]
nn.Linear(512, 10) # 这么操作目的是实得网络的分类数量使用于此应用场景
).to(device)
model.load_state_dict(torch.load(save_path))
total_correct = 0
total_num = 0
for x, label in cifar_test:
x, label = x.to(device), label.to(device)
logits = model(x) # logits[batch,10]
pred = logits.argmax(dim=1) # 在dim=1维度,也就是第二个维度,获取该维度最大值的索引,也就是预测的类别
# pred[batch]
total_correct += torch.eq(pred, label).float().sum().item()
# 计算预测label与真实label相同的图片数量,.float()转为true为1.0 false为0.0,
# sum()所有正确预测求和 item()取数值
total_num += x.size(0) # 取一次预测所有图片数量
acc = total_correct / total_num # 求正确预测概率
print("total_correct:",total_correct)
print("total_num:", total_num)
print("acc:",acc)
效果总结
总得来说比自己手写resnet18效果好了2%,不过迁移学习往往是在自定义的小数据量的数据集下做训练,效果才体现得出来。