当前位置:   article > 正文

【Intel校企实践】基于Resnet18模型与Senet模块的猫狗分类_se_resnet18

se_resnet18

 一、作业简介:猫狗大战

1.1 问题描述:

       在这个问题中,你将面临一个经典的机器学习分类挑战——猫狗大战。你的任务是建立一个分类模型,能够准确地区分图像中是猫还是狗

1.2 预期解决方案:

       通过训练一个机器学习模型,使其在给定一张图像时能够准确地预测图像中是猫还是狗。模型应该能够推广到未见过的图像,并在测试数据上表现良好。我们期待您将其部署到模拟的生产环境中——这里推理时间和二分类准确度(F1分数)将作为评分的主要依据。

1.3 数据集:

数据集:链接:百度网盘 请输入提取码
提取码:esip

二、数据预处理

2.1 数据集特征

       本题是基于CNN的猫狗图像识别,其中训练数据集包括25000张图片,其中类别为猫的图片有12500张图片,类别为狗的图片有12500张图片,比例为1:1。数据集命名方式为type.num.jpg。

2.2 数据集结构

       本项目数据集共由两部分组成,分别包含为test,train,文件夹。其中test文件由train文件划分,比例为8:2。

2-1 数据集结构

       train与test文件下包含两个子文件,分别为dog和cat。

图2-2 子文件夹结构

       其中,dog子文件夹下包含了分类为狗的图像,cat子文件夹下包含了分类为猫的图像。

​ 图2-3 test目录下cat子文件夹图像展示

​ 图2-4 test目录下dog子文件夹图像展示

       数据集划分,通过定义make_dir()创建与源文件类似的文件路径函数,将每类数据文件进行操作,通过random给出随机数,按照8:2的比例划分,并将图片转移至对应的文件夹里

关键代码如下:

  1. def make_dir(source, target):
  2. #############################
  3. # 创建和源文件相似的文件路径函数
  4. # :param source: 源文件位置
  5. # :param target: 目标文件位置
  6. #############################
  7. dir_names = os.listdir(source)
  8. for names in dir_names:
  9. for i in ['train', 'test']:
  10. path = target + '/' + i + '/' + names
  11. if not os.path.exists(path):
  12. os.makedirs(path)
  13. def divideTrain_Test(source, target):
  14. #############################
  15. # 创建和源文件相似的文件路径
  16. # :param source: 源文件位置
  17. # :param target: 目标文件位置
  18. #############################
  19. # 得到源文件下的种类
  20. pic_name = os.listdir(source)
  21. # 对于每一类里的数据进行操作
  22. for classes in pic_name:
  23. # 得到这一种类的图片的名字
  24. pic_classes_name = os.listdir(os.path.join(source, classes))
  25. random.shuffle(pic_classes_name)
  26. # 按照8:2比例划分
  27. train_list = pic_classes_name[0:int(0.8 * len(pic_classes_name))]
  28. test_list = pic_classes_name[int(0.8 * len(pic_classes_name)):]
  29. # 对于每个图片,移入到对应的文件夹里面
  30. for train_pic in train_list:
  31. shutil.copyfile(source + '/' + classes + '/' + train_pic, target + '/train/' + classes + '/' + train_pic)
  32. for test_pic in test_list:
  33. shutil.copyfile(source + '/' + classes + '/' + test_pic, target + '/test/' + classes + '/' + test_pic)

2.3 数据增强

       本文在transforms.Compose(),首先对图像大小进行调整,transforms.Resize(),使其调整为224*224;transforms.RandomVerticalFlip()依概率p对PIL图片进行垂直翻转。

       其次,通过transforms.Normalize ()对三个通道进行标准化,数据全部符合均值为0、标准差为1的标准正太分布。

关键代码如下:

  1. transform = transforms.Compose([
  2. transforms.Resize([224, 224]), # 调整大小,使原始图像至224*224大小
  3. transforms.RandomVerticalFlip(p=0.4), # 依据概率p对PIL图片进行垂直翻转
  4. transforms.ToTensor(),
  5. transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 对三个通道进行标准化,数据全部符合均值为0、标准差为1的标准正态分布
  6. ])

2.4 构建数据集

       本文事先对数据文件进行分类处理,使数据文件具有如下结构

​ 图2-5 文件结构图

       这里直接采用ImageFolder对文件进行读取,ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一类别的图片,文件夹名为类名。

关键代码如下:

  1. # ImageFolder读取文件
  2. train_data = datasets.ImageFolder(r'.\data\train', transform=transform)
  3. test_data = datasets.ImageFolder(r'.\data\test', transform=transform)

2.5 确定验证集

       数据集文件没有划分验证集,故读取train文件后,需要对train进行验证集划分,验证集比例为0.2。

键代码如下:

  1. num_train = len(train_data)
  2. indices = list(range(num_train)) # 确定验证集图片个数
  3. np.random.shuffle(indices) # 打乱顺序
  4. split = int(np.floor(valid_size * num_train))
  5. train_idx, valid_idx = indices[split:], indices[:split] # 起始从第几个:末尾从第几个:步长
  6. train_sampler = SubsetRandomSampler(train_idx) # 确定采样的顺序,后面在制作train_loader的时候用这个列表得索引值取样本
  7. valid_sampler = SubsetRandomSampler(valid_idx)

三、使用卷积神经网络实现猫狗分类

3.1 卷积神经网络

       卷积神经网络(Convolutional Neural Network,CNN),主要分为3个部分:输入层(Input), 卷积层(Conv),池化层(Pool), 和 全连层(FC)。

3.2 Resnet-18架构

       残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的。ResNet 在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了冠军。残差神经网络的主要贡献是发现了“退化现象(Degradation)”,并针对退化现象发明了 “快捷连接(Shortcut connection)”,极大的消除了深度过大的神经网络训练困难问题

       本文采用的是Resnet-18模型,ResNet18的基本含义是,网络的基本架构是ResNet,网络的深度是18层。

       下图是一个基本残差块。它的操作是把某层输入跳跃连接到下一层乃至更深层的激活层之前,同本层输出一起经过激活函数输出。

​ 图3-1 Resnet架构图

3.3 Resnet-18网络解析

       卷积层,该层卷积核大小为7*7,stride为2

       池化层,该层卷积核大小为3*3,stride为1

       Conv2_x,该卷积核大小为3*3,stride为1,这里有两个层,不改变数据的大小和通道数

       Conv3_x,通过一个1*1的卷积层,输出size为128*28*28,通道翻倍,数据大小减半

       Conv4_x,同样1*1卷积,输出size为256*14*14

       Conv5_x,和上述一样,输出size为512*7*7

       平均池化层,最后输出size为512*1*1

​ 图3-2 Resnet-18结构图

3.4 Resnet-18网络确定

       本模块实现残差部分,layer中,实现卷积-归一化-激活-卷积-归一化的过程,该部分不会改变特征图尺寸,shortcut实现残差,shortcut模块就是将原来的特征图通过1*1卷积提高通道数。

关键代码如下:

  1. class BasicBlock(nn.Module):
  2. def __init__(self, in_channels, out_channels, stride=[1, 1], padding=1) -> None:
  3. super(BasicBlock, self).__init__()
  4. # 残差部分
  5. # 一、layer部分不改变特征图尺寸
  6. # 卷积-归一化-激活-卷积-归一化
  7. self.layer = nn.Sequential(
  8. nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride[0], padding=padding, bias=False), # 尺寸不变
  9. # 卷积层,二维卷积 inchannel思维张量,out期望的四维输出张量数,kernel卷积和
  10. nn.BatchNorm2d(out_channels),
  11. nn.ReLU(inplace=True), # 原地替换 节省内存开销
  12. nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride[1], padding=padding, bias=False), # 尺寸不变
  13. nn.BatchNorm2d(out_channels)
  14. )
  15. # 二、shortcut 部分,实现残差
  16. # 如果输入输出通道不同,或者卷积步长做了改变,那就用if中的模型,否则不变。这里肯定是输入输出通道要改变的
  17. # 这里的shortcut模块就是将原来的特征图通过1*1卷积提高通道数
  18. # 由于存在维度不一致的情况 所以分情况
  19. # 卷积-归一化
  20. self.shortcut = nn.Sequential()
  21. if stride[0] != 1 or in_channels != out_channels:
  22. self.shortcut = nn.Sequential(
  23. # 卷积核为1 进行升降维
  24. # 注意跳变时 都是stride==2的时候 也就是每次输出信道升维的时候
  25. nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride[0], bias=False), # 卷积
  26. nn.BatchNorm2d(out_channels)
  27. )

       最后用forward顺接整个过程,其中out += self.shortcut(x)较为关键,也是Resnet的精华所在,shortcut卷积后和原特征图相加,如果相加后对模型提升并不明显也不影响后续进程。

  1. def forward(self, x): # block的前向反馈,包含主体的2个卷积和shortcut可能触发的一个卷积
  2. out = self.layer(x)
  3. # SE模块
  4. # out = self.se(out)
  5. out += self.shortcut(x) # shortcut卷积后和原特征图相加
  6. out = F.relu(out)
  7. return out

3.5 Resnet-18网络构建

       在Resnet-18中,堆叠了多个残差块来构建整个网络,每个残差块会将输入的特征图进行处理,并输出更加丰富的特征图。需要注意的是,在Resnet-18中,第一卷积层没有残差块,所以需要单独写。

       在__init__中,num_classes值为1,最后输出一个0-1之间的数,故这里二分类判断,大于0.5为一类,小于0.5为另一类。

关键代码如下:

  1. class ResNet18(nn.Module):
  2. def __init__(self, BasicBlock, num_classes=1) -> None: # num_class是1,因为最后要输出一个0-1之间的数,小于0.5的作为一类,大于0.5的又作为一类
  3. super(ResNet18, self).__init__()
  4. self.in_channels = 64
  5. # 第一卷积层作为单独的 因为没有残差块。224*224*3
  6. self.conv1 = nn.Sequential(
  7. nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False),
  8. nn.BatchNorm2d(64), # 卷积后,对应上一层的所有特征图做归一化
  9. # nn.ReLU(inplace=True)
  10. nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
  11. # 最大池化,3x3 的池化层窗口,112*112*64-56*56*64 (112-3)/2 +1 =55.5=56
  12. )
  13. # conv2_x 4个卷积是 3x3,64层的
  14. self.conv2 = self._make_layer(BasicBlock, 64, [[1, 1], [1, 1]]) # 56*56*64-56*56*64
  15. # conv3_x 4个卷积是 3x3,128层的
  16. self.conv3 = self._make_layer(BasicBlock, 128, [[2, 1], [1, 1]]) # 56*56*64-56*56*128
  17. # conv4_x 4个卷积是 3x3,256层的
  18. self.conv4 = self._make_layer(BasicBlock, 256, [[2, 1], [1, 1]]) # 56*56*128-56*56*256
  19. # conv5_x 4个卷积是 3x3,512层的
  20. self.conv5 = self._make_layer(BasicBlock, 512, [[2, 1], [1, 1]]) # 56*56*256-56*56*512
  21. self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 自适应平均池化
  22. self.fc = nn.Linear(512, num_classes) # 设置网络中的全连接层的,输入的二维张量的大小,即输入的[batch_size, size]中的size

       利用make_layer()函数实现对基本残差块的堆叠,关键代码如下:

  1. def _make_layer(self, block, out_channels, strides): # 整合
  2. layers = [] # 每个convi_x的结构保存在layers列表
  3. for stride in strides: # 通过循环堆叠其余残差块
  4. layers.append(block(self.in_channels, out_channels, stride))
  5. self.in_channels = out_channels
  6. return nn.Sequential(*layers) # ‘*’的作用是将list转换为非关键字参数传入

       同样通过forward()顺接整个过程,关键代码如下:

  1. def forward(self, x): # 顺接过程
  2. out = self.conv1(x) # 只有最开始的一个卷积
  3. out = self.conv2(out) # 4个64层 3*3卷积
  4. out = self.conv3(out) # 4个128层 3*3卷积
  5. out = self.conv4(out) # 4个256层 3*3卷积
  6. out = self.conv5(out) # 4个512层 3*3卷积
  7. out = self.avgpool(out) # 平均池化
  8. out = out.reshape(x.shape[0], -1) # 平展
  9. out = torch.sigmoid(self.fc(out)) # 这里加入sigmoid以保证在0-1之间。而且最好调用torch.sigmod而非F.sigmod
  10. return out

四、在GPU上训练

4.1 参数设置

       SGD优化器,SGD全称Stochastic Gradient Descent,随机梯度下降,每次选择一个mini-batch,而不是全部样本,使用梯度下降来更新模型参数。

       ReduceLROnPlateau 学习率调度器,本文学习率设置为0.01

       n_epochs = 20

       valid_loss_min = np.Inf,损失值,从无穷大开始,本文通过二值交叉熵损失函数Binary Cross Entropy Loss,来观测train_loss和vaild_loss

4.2 在GPU上训练

       由于计算机算力限制,本文仅对模型进行了20训练。

  1. for epoch in range(1, n_epochs + 1):
  2. # 设置损失
  3. train_loss = 0.0
  4. valid_loss = 0.0
  5. ##########
  6. # 训练模块
  7. ##########
  8. model.train()
  9. for data, target in train_loader:
  10. # 在gpu上去训练
  11. if train_on_gpu: # 在gpu上运行
  12. data, target = data.cuda(), target.cuda().float().unsqueeze(1)
  13. # 这里的target要注意,原本的数据格式和data是不同的,两个区别:1.差了一个维度2.data是float二target是int。相同点:数据是一样的
  14. # 两者的类型必须完全相同才能放进BCE中去算loss。这个float是把int变float。unsqueeze是把维度加1
  15. # 另外target在后面计算精确度的时候一定注意必须是和从data直接提取的数据的格式相同!!!
  16. optimizer.zero_grad() # 将梯度清零
  17. output = model(data) # 得到输出
  18. # 如果,模型中用的sigmoid,那输出必须都在0-1,否则就有问题
  19. loss = F.binary_cross_entropy(output,target) # 求loss,loss是个tensor是一个数,loss: tensor(2.2984,device='cuda:0',grad_fn=<NllLossBackward>)
  20. loss.backward() # 求方向传播得梯度
  21. optimizer.step() # 反向传播
  22. train_loss += loss.item() * data.size(0)

4.3 验证模块

       通过验证集,对模型性能进行评估,避免模型在训练集上过拟合。

  1. model.eval() # 下面的不做反向传播,即为验证部分
  2. for data, target in valid_loader:
  3. # 在gpu上计算
  4. if train_on_gpu:
  5. data, target = data.cuda(), target.cuda().float().unsqueeze(1)
  6. output = model(data)
  7. loss = F.binary_cross_entropy(output, target) # 计算loss
  8. valid_loss += loss.item() * data.size(0) # 计算验证损失

4.4 查看test数据集上的F1分数与时间

       这里对精确度、recall、F1_score和时间进行了统计。

​ 图4-1 各项数据统计

       关键代码如下:

  1. test_loss = 0.0 # 设置好test_loss认定为一个精度值
  2. class_correct = list(0. for i in range(2)) # [0.0, 0.0] (猫正确,狗正确)
  3. class_total = list(0. for i in range(2)) # [0.0, 0.0] (猫总共,狗总共)
  4. cat_correct = class_correct[0]
  5. dog_correct = class_correct[1]
  6. cat_total = class_total[0]
  7. dog_total = class_total[1]
  8. r1 = class_correct[0] / (class_correct[0] + (class_total[1] - class_correct[1]))
  9. r2 = class_correct[1] / (class_correct[1] + (class_total[0] - class_correct[0]))
  10. f1_cat = 2 * r1 * (class_correct[0] / class_total[0]) / ((class_correct[0] / class_total[0]) + r1)
  11. f1_dog = 2 * r2 * (class_correct[1] / class_total[1]) / ((class_correct[1] / class_total[1]) + r2)
  12. print('recall%5s: %.2f%%' % (classes[0], 100 * r1))
  13. print('recall%5s: %.2f%%' % (classes[1], 100 * r2))
  14. print('F1_score%5s: %.2f' % (classes[0], f1_cat))
  15. print('F1_score%5s: %.2f' % (classes[1], f1_dog))
  16. print('F1_score: %.2f' % ((f1_dog + f1_cat) / 2))
  17. elapsed_time = (end_time - start_time)
  18. print(f'测试集用的时间为: {elapsed_time:.2f} seconds')

4.4 保存为Resnet-18模型并使用模型进行推理测试 

       这里对每一轮训练后的模型都进行了保存,同时验证当前轮次是否比上一轮次损失小,如果成立则保存新的损失和新的权重

关键代码如下:

  1. # 保存模型权重
  2. if valid_loss <= valid_loss_min:
  3. # valid_loss_min = np.Inf,第一轮验证一下是否是无穷不是无穷则保存此valid_loss
  4. # 第二轮,验证一下此轮损失是否比上一轮小,小的话则保存新的这个损失并且保存新的权重
  5. print('Validation loss decreased ({:.6f} --> {:.6f}). Saving model ...'.format(
  6. valid_loss_min,
  7. valid_loss))
  8. torch.save(model.state_dict(), 'Res18.pth')
  9. # torch.save(model.state_dict(), 'Res18_Se.pth')
  10. valid_loss_min = valid_loss

  GPU单张图片推理预测

  1. # 6.1GPU单张推理推理测试
  2. state_dict = torch.load('Res18_Se.pth') # 提取训练权重
  3. # state_dict = torch.load('Res18.pth') # 提取训练权重
  4. model.load_state_dict(state_dict)
  5. sample_img, true_label = next(iter(test_loader)) # 选择一张测试图片
  6. if train_on_gpu:
  7. sample_img = sample_img.cuda()
  8. true_label = true_label.cuda()
  9. output = model(sample_img)
  10. pred = torch.tensor([[1] if num[0] >= 0.5 else [0] for num in output]).cuda() # 对batch_size大小数据进行预测
  11. sample_img = sample_img.cpu().numpy()[0]
  12. plt.imshow(np.transpose(sample_img, (1, 2, 0))) # 转置图片的维度顺序
  13. plt.title(f'TRUE LABEL IS: {classes[true_label[0].cpu().numpy()]},'
  14. f' PREDICT LABEL IS: {classes[pred[0].cpu().numpy()[0]]}')
  15. plt.axis('off')
  16. plt.show()

       预测结果如图所示

       本次预测值与真实标签不一致

五、转移至CPU

5.1 创建Resnet-18模型

       将在GPU训练的模型保存至Resnet-18.pth中,在CPU上加载

5.2 尝试在CPU上进行推理


  1. model.eval() # model不反向传播
  2. model.load_state_dict(torch.load('Res18.pth', map_location=torch.device('cpu'))) # 提取训练权重
  3. # model.load_state_dict(torch.load('Res18_Se.pth', map_location=torch.device('cpu'))) # 提取训练权重
  4. device = torch.device('cpu') # 网络传入CPU
  5. print(device)
  6. start_time = time.time() # 测试时间
  7. for data, target in test_loader: # 按照test_loader设置好的batch_size进行采样
  8. output = model(data)
  9. target1 = target.float().unsqueeze(1) # 为了与output格式对应
  10. loss = F.binary_cross_entropy(output, target1)
  11. test_loss += loss.item() * data.size(0)
  12. pred = torch.tensor([[1] if num[0] >= 0.5 else [0] for num in output])
  13. # 通过此行代码求得索引值,这里预测得到的小于0.5的shiwei0,大于0.5的视为1。以此分类
  14. # 获得预测结果的正确与否
  15. correct_tensor = pred.eq(target.data.view_as(pred))
  16. # x.eq(y)判断x和y的值是否相等,作比较以后输出新的tensor 这里的view_as和view一样都是调整格式只不过view是将target调整成和pred一样的格式,这里其实没变他俩格式都是Tensor(20,)
  17. correct = np.squeeze(correct_tensor.numpy()) # 改成np格式[ True True True True True]
  18. # 计算准确值
  19. for i in range(batch_size): # 这里再按照batch_size的值遍历
  20. label = target.data[i] # 第一轮就是第一个图片的标签是3 tensor(3, device='cuda:0')
  21. # label=tensor([0.], device='cuda:0')
  22. class_correct[label] += correct[
  23. i].item() # 【40,46】#注意这里头的label应该是:1.1个整数2.是张量的话只能是包含一个数的张量。所以这里用label做索引,labela选的target的数据就不能去增加维数,也不能变为精度型
  24. class_total[label] += 1 # 【50,50】
  25. end_time = time.time() # 记录结束时间
  26. test_loss = test_loss / len(test_loader.dataset) # 计算平均损失
  27. print('Test Loss: {:.6f}\n'.format(test_loss))

5.3 查看test数据集上的F1分数与时间

​ 图5-1 CPU各项数据

       这里可以对比发现,CPU上推理所需时间与GPU相差甚多,CPU上消耗的时间是GPU的5倍左右。

六、模型优化

6.1 Senet模块简介

       SE是Squeeze-and-Excitation(SE)的缩写,该模块的提出主要是考虑到模型通道之间的相互依赖性。SE网络的使用结构如下图所示:

 图6-1 Senet结构

 6.2 Senet模块嵌入Resnet-18模型

       Senet网络的创新点在于关注channel之间的关系,希望模型可以自动学习到不同channel特征的重要程度,在Se-Resnet模型中,只需要Se模块加入到残差单元(应用在残差学习部分)即可,下图是在残差单元应用的结构图

 图6-2 Senet在残差网络的应用

       关键代码如下:

  1. # se模块
  2. class SELayer(nn.Module):
  3. def __init__(self, channel, reduction=16):
  4. super(SELayer, self).__init__()
  5. self.avg_pool = nn.AdaptiveAvgPool2d(1)
  6. self.fc = nn.Sequential(
  7. nn.Linear(channel, channel // reduction, bias=False),
  8. nn.ReLU(inplace=True),
  9. nn.Linear(channel // reduction, channel, bias=False),
  10. nn.Sigmoid()
  11. )
  12. def forward(self, x):
  13. b, c, _, _ = x.size()
  14. y = self.avg_pool(x).view(b, c)
  15. y = self.fc(y).view(b, c, 1, 1)
  16. return x * y.expand_as(x)

6.3 在GPU上训练

       和上文一样,通过GPU进行训练

6.4 查看test数据集上的F1分数与时间

​ 图6-3 Senet在GPU上的数据

6.5 转移至CPU并查看test数据集上的F1分数与时间

​ 图6-4 Senet在CPU上的数据

       Senet模组的加入,模型的精准度、F1_Score和时间仅有小部分提升,在基数为5000test中,F1_Score分数上升1%,所花时间减少2-3s左右。在数据量小的时候有较明显的提升。

七、使用OneApi组件

7.1Transfer Learning with oneAPI AI Analytics Toolkit进行迁移学习

7.2使用Intel Extension for PyTorch进行优化

       在上一章中,我发现使用CPU直接进行训练的话会相当慢,在这里使用Intel Extension for PyTorch大大提高了速度。上文中CPU会消耗大约150s,而通过Intel Extension for PyTorch进行优化后,仅需60s,提升大概缩短了两倍的时间,f1值并没有变化

图7.1 Resnet-18在CPU训练

图7.2 Resnet-18-Se 在CPU训练

       为了避免数据的偶然性,我们重新采用了新的test进行测试,其中dog类有500张,cat类有500.test集共1000张,以下是Resnet18和Resnet18-Se分别在CPU上的各项测试值(以下test集均为1000的新test测试)。

图7.3 Resnet-18在CPU训练

图7.4 Resnet-18-Se在CPU训练

  1. # model.load_state_dict(torch.load('Res18.pth', map_location=torch.device('cpu'))) # 提取训练权重
  2. model.load_state_dict(torch.load('Res18_Se.pth', map_location=torch.device('cpu'))) # 提取训练权重
  3. device = torch.device('cpu') # 网络传入CPU
  4. print(device)
  5. optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 实例化一个优化器,即调整网络参数,优化方式为adam方法
  6. model,optimizer = ipex.optimize(model=model,optimizer=optimizer,dtype=torch.float32)
  7. model.eval() # model不反向传播

7.3保存使用Intel Extension for PyTorch进行优化的模型

  1. torch.save(model.state_dict(), 'new_Res18_Se.pth')
  2. torch.save(model.state_dict(), 'new_Res18.pth')

7.4使用 Intel® Neural Compressor 量化模型 

       这里对优化后的模型new_Res18.pth和new_Res18_Se.pth进行加载

  1. # 加载模型
  2. # model.load_state_dict(torch.load('new_Res18_Se.pth',map_location=torch.device('cpu')))
  3. model.load_state_dict(torch.load('new_Res18.pth',map_location=torch.device('cpu')))
  4. device = torch.device('cpu') # 网络传入CPU
  5. model.eval()

       加载完成以后以准确度为评估函数进行量化

  1. # 定义评估函数
  2. def eval_func(model):
  3. with torch.no_grad():
  4. y_true = []
  5. y_pred = []
  6. for inputs, labels in train_loader:
  7. inputs = inputs.to('cpu')
  8. labels = labels.to('cpu')
  9. preds_probs = model(inputs)
  10. preds_class = torch.argmax(preds_probs, dim=-1)
  11. y_true.extend(labels.numpy())
  12. y_pred.extend(preds_class.numpy())
  13. return accuracy_score(y_true, y_pred)
  14. # 配置量化参数
  15. conf = PostTrainingQuantConfig(backend='ipex', # 使用 Intel PyTorch Extension
  16. accuracy_criterion=AccuracyCriterion(higher_is_better=True,
  17. criterion='relative',
  18. tolerable_loss=0.01))
  19. conf = PostTrainingQuantConfig(backend='default', # or 'qnnpack'
  20. accuracy_criterion=AccuracyCriterion(higher_is_better=True,
  21. criterion='relative',
  22. tolerable_loss=0.01))
  23. # 执行量化
  24. q_model = quantization.fit(model,
  25. conf,
  26. calib_dataloader=valid_loader, # 验证集
  27. eval_func=eval_func)
  28. # 保存量化模型
  29. quantized_model_path = './quantized_Res18'
  30. if not os.path.exists(quantized_model_path):
  31. os.makedirs(quantized_model_path)
  32. q_model.save(quantized_model_path)

       量化成功以后会出现如下代码

7.5使用量化后的模型在 CPU上进行推理

       加载模型

  1. # quantized_model_path = './quantized_Res18_Se'
  2. quantized_model_path = './quantized_Res18'
  3. new_model_path = f'{quantized_model_path}/best_model.pt'
  4. model = torch.jit.load(new_model_path, map_location='cpu')
  5. model.eval() # model不反向传播
  6. model.load_state_dict(torch.load(new_model_path, map_location=torch.device('cpu')))
  7. optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 实例化一个优化器,即调整网络参数,优化方式为adam方法
  8. device = torch.device('cpu') # 网络传入CPU
  9. print(device)

       模型推理并查看test上的F1_Score和时间

图7.5 Resnet-18

图7.6 Resnet-18-Se

八、总结

       以下是对新test集不同版本的F1_Score和时间:

       本地GPU版本Resnet-18(左)和Resnet-18-Se(右):

 

       本地CPU版本Resnet-18(左)和Resnet-18-Se(右):

       Intel Extension for PyTorch 版Resnet-18(左)和Resnet-18-Se(右):

       在此版本中,CPU推理已经有了十分明显的提升,本地CPU需要30余秒,而Intel Extension for PyTorch优化后仅需10秒,是原来的1/3。

       量化优化版本Resnet-18(左)和Resnet-18-Se(右):

       在量化优化版本中,CPU推理又有了明显的提升,从最开始的30秒到最后的6秒,是原来的1/5,由此可见,OneApi优化组件,即可以大幅度下降推理时间,还能保证模型精确度。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/290930
推荐阅读
相关标签
  

闽ICP备14008679号