GoogLeNet——网络实战
文章目录
- 摘要🐇
- 1 项目结构🐇
- 2 划分训练集和测试集🐇
- 3 计算mean和Standard🐇
-
- 3.1 标准化的作用🐇
- 3.2 归一化的作用🐇
- 4 训练🐇
- 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 globimport osimport shutilfrom sklearn.model_selection import train_test_splitimage_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 ImageFolderimport torchfrom torchvision import transformsdef 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 optimimport torchimport torch.nn as nnimport torch.nn.parallelimport torch.optimimport torch.utils.dataimport torch.utils.data.distributedimport torchvision.transforms as transformsfrom torchvision.models import googlenetimport osfrom torchvision import datasetsimport jsonimport matplotlib.pyplot as pltimport warningswarnings.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:
将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 变量上。
5、acc = correct / total_num,计算出acc。 avgloss = test_loss / len(test_loader)计算loss。 最后返回val_list, pred_list,loss,acc
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.distributedimport torchvision.transforms as transformsfrom PIL import Imagefrom torch.autograd import Variableimport osclasses = ('双子座', '双鱼座', '处女座', '天秤座', '天蝎座', '射手座', '山羊座', '巨蟹座', '水瓶座', '狮子座', '白羊座', '金牛座')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曲线图?
有没有发现,我们的实战代码正在逐渐的丰富起来。我打算通过这种逐步增加难度的方式,让大家更容易接受!