文章目录
- 摘要🐇
- 1 项目结构🐇
- 2 划分训练集和测试集🐇
- 3 计算mean和Standard🐇
- 3.1 标准化的作用🐇
- 3.2 归一化的作用🐇
- 4 训练🐇
- 4.1 导入项目使用的库🐇
- 4.2 设置随机因子🐇
- 4.3 设置全局参数🐇
- 4.4 图像预处理与增强🐇
- 4.5 读取数据🐇
- 4.6 设置Loss🐇
- 4.7 设置模型🐇
- 4.7.1 如何确定改哪层?🐇
- 4.8 设置优化器和学习率调整算法🐇
- 4.9 训练函数
- 4.10 验证函数🐇
- 4.11 训练、验证、保存模型🐇
- 4.12 继续训练🐇
- 4.13 结果展示🐇
- 5 测试🐇
- 总结🐇
🐇🐇🐇🐇🐇🐇 🐇 欢迎阅读 【AI浩】 的博客🐇 👍 阅读完毕,可以动动小手赞一下👍 🌻 发现错误,直接评论区中指正吧🌻 📆 这是一篇讲解GoogLeNet实战的文章📆 💯专栏目录: 经典主干网络精讲与实战💯
摘要🐇
GoogLeNet作为2014年ILSVRC在分类任务上的冠军,以6.65%的错误率力压VGGNet等模型,在分类的准确率上面相比过去两届冠军ZFNet和AlexNet都有很大的提升。在前面的文章我介绍了GoogLeNet的网络结构和数据集的制作,这篇文章我将和大家一起完成GoogLeNet模型的实战。
数据集选用第三篇 制作数据集制作的数据集,模型用Pytorh自带的GoogLeNet。在这篇文章中,我尽量的简化代码,只保留最基本的逻辑,让每一个初学者能够看明白。通过这篇文章你能学到:
1、如何训练模型?
2、如何推理?
3、如何读取数据集、处理数据集?
4、如何使用余弦退火调整学习率?
5、如何保存权重文件和整个模型文件?
6、如何使用评价指标,如ACC、ReCall等指标评价模型。
7、如何使用matplotlib.pyplot绘制acc和loss曲线图?
1 项目结构🐇
GoogLeNet_Demo
├─trainvals
│ ├─双子座
│ ├─双鱼座
│ ├─处女座
│ ├─天秤座
│ ├─天蝎座
│ ├─射手座
│ ├─山羊座
│ ├─巨蟹座
│ ├─水瓶座
│ ├─狮子座
│ ├─白羊座
│ └─金牛座
├─test
├─makedata.py
├─mean_std.py
├─train.py
└─test.py
trainvals:数据集,接下来我们将其划分为训练集和验证集。
test:测试集
makedata.py:划分数据集的方法
mean_std.py:计算mean和std的值。
ema.py:EMA脚本
train.py:训练GoogLeNet模型
2 划分训练集和测试集🐇
import glob
import os
import shutil
from sklearn.model_selection import train_test_split
image_list=glob.glob('trainvals/*/*.*')
print(image_list)
file_dir='data'
if os.path.exists(file_dir):
print('true')
shutil.rmtree(file_dir)#删除再建立
os.makedirs(file_dir)
else:
os.makedirs(file_dir)
trainval_files, val_files = train_test_split(image_list, test_size=0.2, random_state=42)
train_dir='train'
val_dir='val'
train_root=os.path.join(file_dir,train_dir)
val_root=os.path.join(file_dir,val_dir)
for file in trainval_files:
file_class=file.replace("\\","/").split('/')[-2]
file_name=file.replace("\\","/").split('/')[-1]
file_class=os.path.join(train_root,file_class)
if not os.path.isdir(file_class):
os.makedirs(file_class)
shutil.copy(file, file_class + '/' + file_name)
for file in val_files:
file_class=file.replace("\\","/").split('/')[-2]
file_name=file.replace("\\","/").split('/')[-1]
file_class=os.path.join(val_root,file_class)
if not os.path.isdir(file_class):
os.makedirs(file_class)
shutil.copy(file, file_class + '/' + file_name)
核心思路:
- 1、将所有的图片读取到list中。
- 2、sklearn.model_selection的train_test_split方法将list切分。这一步需要安装sklearn这个库,安装命令:
pip install sklearn
- 3、保存切分结果。
运行结果:
如上结果,我们就将训练集和测试集划分好了。
3 计算mean和Standard🐇
使用深度学习进行图像分类或者图像检测时,首先需要对图像进行数据预处理,常见的对图像的预处理有两种办法,一种标准化处理,另一种是归一化处理。
3.1 标准化的作用🐇
图像标准化是将数据通过去均值实现中心化的处理,根据凸优化理论与数据概率分布相关知识,数据中心化符合数据分布规律,更容易取得训练之后的泛化效果。
3.2 归一化的作用🐇
归一化不改变图像信息,只是把像素从0-255变成0~1的范围,加快训练网络的收敛性,使得所有样本的输入信号其均值接近于0或与其均方差相比很小。
总之,为了使模型更加快速的收敛和具有更好的泛化性,我们需要计算出mean和std的值,新建mean_std.py,插入代码:
from torchvision.datasets import ImageFolder
import torch
from torchvision import transforms
def get_mean_and_std(train_data):
train_loader = torch.utils.data.DataLoader(
train_data, batch_size=1, shuffle=False, num_workers=0,
pin_memory=True)
mean = torch.zeros(3)
std = torch.zeros(3)
for X, _ in train_loader:
for d in range(3):
mean[d] += X[:, d, :, :].mean()
std[d] += X[:, d, :, :].std()
mean.div_(len(train_data))
std.div_(len(train_data))
return list(mean.numpy()), list(std.numpy())
if __name__ == '__main__':
train_dataset = ImageFolder(root=r'data1', transform=transforms.ToTensor())
print(get_mean_and_std(train_dataset))
运行结果:
([0.48214436, 0.42969334, 0.33318862], [0.2642221, 0.23746745, 0.21696019])
生成两个list,一个list是mean的结果,一个list是std的结果。
4 训练🐇
新建train.py脚本。
4.1 导入项目使用的库🐇
import torch.optim as optim
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim
import torch.utils.data
import torch.utils.data.distributed
import torchvision.transforms as transforms
from torchvision.models import googlenet
import os
from torchvision import datasets
import json
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
4.2 设置随机因子🐇
设置随机因子后,再次训练时,图像的加载顺序不会改变,能够更好的复现训练结果。代码如下:
def seed_everything(seed=42):
os.environ['PYHTONHASHSEED'] = str(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.backends.cudnn.deterministic = True
4.3 设置全局参数🐇
if __name__ == '__main__':
#创建保存模型的文件夹
file_dir = 'checkpoints/googlenet'
if os.path.exists(file_dir):
print('true')
os.makedirs(file_dir,exist_ok=True)
else:
os.makedirs(file_dir)
# 设置全局参数
model_lr = 1e-4
BATCH_SIZE = 16
EPOCHS = 1000
DEVICE = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
classes = 12
resume = None
Best_ACC = 0 #记录最高得分
SEED=42
seed_everything(42)
start_epoch=1
设置存放权重文件的文件夹,如果文件夹存在删除再建立。
接下来,设置全局参数,比如:学习率、BatchSize、epoch等参数,判断环境中是否存在GPU,如果没有则使用CPU。
注:建议使用GPU,CPU太慢了。
参数的详细解释:
model_lr:学习率,根据实际情况做调整。
BATCH_SIZE:batchsize,根据显卡的大小设置。
EPOCHS:epoch的个数,一般300够用。
classes:类别个数。
resume:再次训练的模型路径,如果不为None,则表示加载resume指向的模型继续训练。
Best_ACC:记录最高ACC得分。
start_epoch:开始的epoch,默认是1,如果重新训练时,需要给start_epoch重新赋值。
SEED:随机因子,数值可以随意设定,但是设置后,不要随意更改,更改后,图片加载的顺序会改变,影响测试结果。
4.4 图像预处理与增强🐇
# 数据预处理7
transform = transforms.Compose([
transforms.RandomRotation(10),
transforms.GaussianBlur(kernel_size=(5,5),sigma=(0.1, 3.0)),
transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.44127703, 0.4712498, 0.43714803], std= [0.18507297, 0.18050247, 0.16784933])
])
transform_test = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.44127703, 0.4712498, 0.43714803], std= [0.18507297, 0.18050247, 0.16784933])
])
数据处理和增强比较简单,加入了随机10度的旋转、高斯模糊、色彩饱和度明亮度的变化等比较常用的增强手段,做了Resize、归一化和标准化处理。
这里注意下Resize的大小,由于选用的GoogLeNet模型输入是224×224的大小,所以要Resize为224×224。
4.5 读取数据🐇
# 读取数据
dataset_train = datasets.ImageFolder('data/train', transform=transform)
dataset_test = datasets.ImageFolder("data/val", transform=transform_test)
with open('class.txt', 'w') as file:
file.write(str(dataset_train.class_to_idx))
with open('class.json', 'w', encoding='utf-8') as file:
file.write(json.dumps(dataset_train.class_to_idx))
# 导入数据
train_loader = torch.utils.data.DataLoader(dataset_train, batch_size=BATCH_SIZE,pin_memory=True, shuffle=True)
test_loader = torch.utils.data.DataLoader(dataset_test, batch_size=BATCH_SIZE,pin_memory=True, shuffle=False)
-
使用pytorch默认读取数据的方式,然后将dataset_train.class_to_idx打印出来,预测的时候要用到。
-
对于train_loader和test_loader ,pin_memory设置为True,可以加快运行速度,训练需要将shuffle设置为True,用于将图片加载的顺序打乱,验证时不用将图片的顺序打乱。
-
将dataset_train.class_to_idx保存到txt文件或者json文件中。
class_to_idx的结果:
{'双子座': 0, '双鱼座': 1, '处女座': 2, '天秤座': 3, '天蝎座': 4, '射手座': 5, '山羊座': 6, '巨蟹座': 7, '水瓶座': 8, '狮子座': 9, '白羊座': 10, '金牛座': 11}
4.6 设置Loss🐇
# 实例化模型并且移动到GPU
criterion_val = torch.nn.CrossEntropyLoss()
设置loss函数,loss函数设置为交叉熵。
4.7 设置模型🐇
# 设置模型
model_ft = googlenet(pretrained=True,aux_logits=True)
print(model_ft)
num_ftrs=model_ft.fc.in_features
model_ft.fc =nn.Linear(num_ftrs,classes)
num_ftrs_aux1 = model_ft.aux1.fc2.in_features
model_ft.aux1.fc2 = nn.Linear(num_ftrs_aux1, classes)
num_ftrs_aux2=model_ft.aux2.fc2.in_features
model_ft.aux2.fc2 = nn.Linear(num_ftrs_aux2, classes)
if resume:
model = torch.load(resume)
model_ft.load_state_dict(model['state_dict'])
Best_ACC = model['Best_ACC']
start_epoch = model['epoch'] + 1
model_ft.to(DEVICE)
print(model_ft)
- 设置模型为googlenet,pretrained设置为true,表示加载预训练模型,aux_logits设置为True,则表示启用辅助分类器。 将model_ft.fc 这个全连接层的输出修改为classes,将model_ft.aux1.fc2和 model_ft.aux2.fc2这两个辅助分类器的输出也改为classes。执行
print(model_ft)
,查看是否修改成功,如下图所示:
- 如果resume为True,则加载模型接着resume指向的模型接着训练,使用模型里的Best_ACC初始化Best_ACC,使用epoch参数初始化start_epoch。
4.7.1 如何确定改哪层?🐇
很多人有这样的疑问,不同的模型,最后一层全连接都不相同,我们如何找到最后一层的全连接层?我通过这篇文章揭晓一下:
model_ft = alexnet(pretrained=True)
print(model_ft)
首先,我们声明模型的对象后,使用print直接打印模型,然后寻找最后一层:
通过上图可以看到,最后一层的全连接在classifier这个Sequential下面,index为6的位置。
所以,我们先将这个层的in_features取出来,代码如下:
num_ftrs=model_ft.classifier[6].in_features
然后,给他重新赋个全连接,代码如下:
model_ft.classifier[6]=nn.Linear(num_ftrs,classes)
4.8 设置优化器和学习率调整算法🐇
# 选择简单暴力的Adam优化器,学习率调低
optimizer = optim.AdamW(model_ft.parameters(),lr=model_lr)
cosine_schedule = optim.lr_scheduler.CosineAnnealingLR(optimizer=optimizer, T_max=20, eta_min=1e-6)
- 优化器设置为adamW。
- 学习率调整策略选择为余弦退火。
4.9 训练函数
# 定义训练过程
def train(model, device, train_loader, optimizer, epoch):
model.train()
sum_loss = 0
correct=0 #预测正确的数量
total_num = len(train_loader.dataset)
print(total_num, len(train_loader))
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device,non_blocking=True), target.to(device,non_blocking=True)
output, aux1, aux2 = model(data)
loss_out = criterion(output, target)
loss_aux = criterion(aux2, target)
loss = loss_out * 0.7 + loss_aux * 0.3
optimizer.zero_grad()
loss.backward()
optimizer.step()
print_loss = loss.data.item()
_, pred = torch.max(output.data, 1)
correct += torch.sum(pred == target)
sum_loss += print_loss
if (batch_idx + 1) % 10 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, (batch_idx + 1) * len(data), len(train_loader.dataset),
100. * (batch_idx + 1) / len(train_loader), loss.item()))
ave_loss = sum_loss / len(train_loader)
correct = correct.data.item()
acc = correct / total_num
print('epoch:{},loss:{}'.format(epoch, ave_loss))
return ave_loss,acc
训练的主要步骤:
1、 model.train()切换成训练模型。启用 BatchNormalization 和 Dropout。初始化sum_loss 为0,计算训练集图片的数量赋给total_num。correct设置为0
2、进入循环,将data和target放入device上,non_blocking设置为True。如果pin_memory=True的话,将数据放入GPU的时候,也应该把non_blocking打开,这样就只把数据放入GPU而不取出,访问时间会大大减少。
如果pin_memory=False时,则将non_blocking设置为False。
3、data输入model,输出预测结果,然后再计算loss。这里注意,我们除了计算output的loss,还要计算一个辅助分类器的loss,根据论文描述选择其中一个辅助分类器,按照7:3的比例配置loss。
4、 optimizer.zero_grad() 梯度清零,把loss关于weight的导数变成0。
5、反向传播求梯度。
6、获取loss,并赋值给print_loss 。
7、torch.sum(pred == target)计算当前Batch内,预测正确的数量,然后累加到correct 。
8、sum_loss 累加print_loss ,求得总的loss。所以,单个epoch的loss就是总的sum_loss 除以train_loader的长度。
等待一个epoch训练完成后,计算平均loss。然后将其打印出来。并返回loss和acc。
4.10 验证函数🐇
验证过程增加了对预测数据和Label数据的保存,所以,需要定义两个list保存,然后将其返回!
# 验证过程
def val(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
total_num = len(test_loader.dataset)
print(total_num, len(test_loader))
val_list = []
pred_list = []
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
for t in target:
val_list.append(t.data.item())
output = model(data)
loss = criterion(output, target)
_, pred = torch.max(output.data, 1)
for p in pred:
pred_list.append(p.data.item())
correct += torch.sum(pred == target)
print_loss = loss.data.item()
test_loss += print_loss
correct = correct.data.item()
acc = correct / total_num
avgloss = test_loss / len(test_loader)
print('\nVal set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
avgloss, correct, len(test_loader.dataset), 100 * acc))
return val_list, pred_list,avgloss,acc
验证的过程和训练的过程大致相似,主要步骤:
1、model.eval(),切换验证模型,不启用 BatchNormalization 和 Dropout。
2、定义参数:
test_loss : 测试的loss
correct :统计正确类别的数量。
total_num:验证集图片的总数。
val_list :保存验证集的Label数据。
pred_list :保存预测的Label数据。
3、torch.no_grad():反向传播时就不会自动求导了。
4、进入循环,迭代test_loader:
5、acc = correct / total_num,计算出acc。 avgloss = test_loss / len(test_loader)计算loss。 最后返回val_list, pred_list,loss,acc将label保存到val_list。
将data和target放入device上,non_blocking设置为True。
遍历target,将Label保存到val_list 。
将data输入到model中,求出预测值,然后输入到loss函数中,求出loss。在验证集中,不用求辅助分类器的loss。
调用torch.max函数,将预测值转为对应的label。
遍历pred,将预测的Label保存到pred_list。
correct += torch.sum(pred == target),计算出识别对的数量,并累加到correct 变量上。
4.11 训练、验证、保存模型🐇
# 训练
log_dir = {}
train_loss_list, val_loss_list, train_acc_list, val_acc_list, epoch_list = [], [], [], [], []
for epoch in range(start_epoch, EPOCHS + 1):
epoch_list.append(epoch)
train_loss, train_acc=train(model_ft, DEVICE, train_loader, optimizer, epoch)
train_loss_list.append(train_loss)
train_acc_list.append(train_acc)
log_dir['train_acc'] = train_acc_list
log_dir['train_loss'] = train_loss_list
val_list, pred_list, val_loss, val_acc =val(model_ft, DEVICE, test_loader)
val_loss_list.append(val_loss)
val_acc_list.append(val_acc)
log_dir['val_acc'] = val_acc_list
log_dir['val_loss'] = val_loss_list
log_dir['best_acc'] = Best_ACC
if val_acc>=Best_ACC:
Best_ACC=val_acc
torch.save(model_ft, file_dir + "/" + 'best.pth')
state = {
'epoch': epoch,
'state_dict': model_ft.state_dict(),
'Best_ACC': Best_ACC
}
torch.save(state, file_dir + "/" + 'model_' + str(epoch) + '_' + str(round(val_acc, 3)) + '.pth')
fig = plt.figure(1)
plt.plot(epoch_list, train_loss_list, 'r-', label=u'Train Loss')
# 显示图例
plt.plot(epoch_list, val_loss_list, 'b-', label=u'Val Loss')
plt.legend(["Train Loss", "Val Loss"], loc="upper right")
plt.xlabel(u'epoch')
plt.ylabel(u'loss')
plt.title('Model Loss ')
plt.savefig(file_dir + "/loss.png")
plt.close(1)
fig2 = plt.figure(2)
plt.plot(epoch_list, train_acc_list, 'r-', label=u'Train Acc')
plt.plot(epoch_list, val_acc_list, 'b-', label=u'Val Acc')
plt.legend(["Train Acc", "Val Acc"], loc="lower right")
plt.title("Model Acc")
plt.ylabel("acc")
plt.xlabel("epoch")
plt.savefig(file_dir + "/acc.png")
plt.close(2)
循环调用train函数和val函数,train函数返回train_loss, train_acc,val函数返回val_list, pred_list, val_loss, val_acc。loss和acc用于绘制曲线。
将val_list, pred_list和dataset_train.class_to_idx传入模型,计算模型指标。
判断acc是否大于Best_ACC,如果大于则保存模型,这里保存的是整个模型。
接下来是保存每个epoch的模型,新建state ,字典的参数:
- epoch:当前的epoch。
- state_dict:权重参数。 model_ft.state_dict(),只保存模型的权重参数。
- Best_ACC:Best_ACC的数值。
然后,调用 torch.save保存模型。
最后使用plt.plot绘制loss和acc曲线图
4.12 继续训练🐇
将resume设置为模型的路径即可开启继续训练。如下:
resume = 'checkpoints/GoogLeNet/model_430_0.92.pth'
然后点击train.py,就可以继续训练了。如下图:
4.13 结果展示🐇
loss曲线图:
acc曲线图:
5 测试🐇
测试,我们采用一种通用的方式。测试集存放的目录如下图:
GoogLeNet_demo
├─test
│ ├─白羊座_2.jpg.jpg
│ ├─处女座_136.jpg
│ ├─金牛座_142.jpg
│ ├ ......
└─test.py
代码如下:
import torch.utils.data.distributed
import torchvision.transforms as transforms
from PIL import Image
from torch.autograd import Variable
import os
classes = ('双子座', '双鱼座', '处女座', '天秤座',
'天蝎座', '射手座', '山羊座',
'巨蟹座', '水瓶座', '狮子座', '白羊座', '金牛座')
transform_test = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.51819474, 0.5250407, 0.4945761], std=[0.24228974, 0.24347611, 0.2530049])
])
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model=torch.load('checkpoints/googlenet/best.pth')
model.eval()
model.to(DEVICE)
path = 'test/'
testList = os.listdir(path)
for file in testList:
img = Image.open(path + file).convert('RGB')
img = transform_test(img)
img.unsqueeze_(0)
img = Variable(img).to(DEVICE)
out = model(img)
# Predict
_, pred = torch.max(out.data, 1)
print('Image Name:{},predict:{}'.format(file, classes[pred.data.item()]))
测试的主要逻辑:
1、定义类别,这个类别的顺序和训练时的类别顺序对应,一定不要改变顺序!!!!
2、定义transforms,transforms和验证集的transforms一样即可,别做数据增强。
3、 加载model,并将模型放在DEVICE里,
4、循环 读取图片并预测图片的类别,在这里注意,读取图片用PIL库的Image。不要用cv2,transforms不支持。循环里面的主要逻辑:
- 使用Image.open读取图片,将其转为RGB格式。
- 使用transform_test对图片做归一化和标椎化。
- img.unsqueeze_(0) 增加一个维度,由(3,224,224)变为(1,3,224,224)
- Variable(img).to(DEVICE):将数据放入DEVICE中。
- model(img):执行预测。
- _, pred = torch.max(out.data, 1):获取预测值的最大下角标。
运行结果:
总结🐇
到这里,这篇文章就写完了,通过这篇文章,你能学到:
1、如何训练模型?
2、如何推理?
3、如何读取数据集、处理数据集?
4、如何使用余弦退火调整学习率?
5、如何保存权重文件和整个模型文件?
6、如何使用评价指标,如ACC、ReCall等指标评价模型。
7、如何使用matplotlib.pyplot绘制acc和loss曲线图?
有没有发现,我们的实战代码正在逐渐的丰富起来。我打算通过这种逐步增加难度的方式,让大家更容易接受!