[零基础][最简单的教程]图像多分类问题的解决——基于resnet50的pytorch的实现_resnet pytorch 多分类-程序员宅基地

技术标签: 深度学习  pytorch  图像识别  


前言

文章的宗旨是使小白不懂原理也可以快速入手,傻瓜式操作。

整个文章的逻辑从所有深度学习的模型的通用框架开始介绍,再推广到用pytorch进行图像识别问题的模型基本框架。由于笔者也是初学深度学习与pytorch,因此仅以此篇,分享给同样初学pytorch,且对深度学习的一些原理不是很了解的朋友,帮助大家更快的上手一个图像分类项目
PS:如果读者觉得啰嗦,直接跳到代码部分进行复制就好了,那部分教你如何正确的复制与微调。


提示:以下是本篇文章正文内容,下面案例可供参考

一、开始任务的前提条件

1.装了anoconda与pytorch

具体教程可以搜B站UP主“深度之眼官方账号”的视频“pytorch安装指南!”
在这里插入图片描述
建议在安装前把原来机器上的python给卸载了,编译器直接用pycharm,这样就可以避免出现一堆莫名其妙的BUG,导致安装一天都失败
注1:如果浏览器下载慢的话,可以把链接复制到迅雷下载,迅雷会员便宜。
注2:用pycharm创建项目后,要给它的虚拟环境配置解释器,得用anoconda里的python.exe,所以你得记住anoconda的安装路径是在哪里,不然后期慢慢搜索会很折磨的
注3:如果你没有GUP,跟着教程安装的pytorch时,得下载cpu版本,而不是cuda版本

二、深度学习通用框架

本部分转载至知乎的一篇文章,强烈建议大家先看一眼,保证心中有数。
链接: 点击进入知乎原文.

所有的深度学习模型过程都可以形式化如下图:
在这里插入图片描述
1、输入处理模块 (X 输入数据,变成网络能够处理的Tensor类型)

2、模型构建模块 (主要负责从输入的数据,得到预测的y^, 这就是我们经常说的前向过程)

3、定义代价函数和优化器模块 (注意,前向过程只会得到模型预测的结果,并不会自动求导和更新,是由这个模块进行处理)

4、构建训练过程 (迭代训练过程,就是上图表情包的训练迭代过程)

这几个模块分别与上图的数字标号1,2,3,4进行一一对应!

三、修改代码前,使用者需要明确的问题

本框架无须过多的理解深度学习的很多概念,只要你完成三个事情即可复制本代码,改一下直接使用。

1.图片分成三个集,并放在正确的位置

①图片分集
假设你有5个类,每个类有10张图片,你可以把每个类的图片都按照8:1:1的比例分成训练集,验证集和测试集,每个集中都装着5个文件夹,这5个文件夹名字为类名,装着这个类的图片。
训练集的作用是训练数据,验证集的作用是在每轮模型训练后,时时测试模型的性能
测试集是,当所有轮数都跑完之后,再测试,看看模型性能究竟如何
那么我分完之后,这三份图片应该放哪里呢?

②正确的位置
假设你的.py文件放在文件夹pythonProject里,那么你需要在文件夹A里面再创建一个文件夹,名为dataset 。然后你进入dataset,在dataset里面创建三个文件夹,分别为train,valid,test

(这步一定要做对,除了文件夹pythonProject的名字可以任意取,其余文件夹的名字必须和我一样)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.明确你是几分类问题

如果你有5类,你就是5分类问题,到时候代码里只用改几处就可以了

3.明确你是使用的模型网络

本文使用的模型是resnet50,你可以根据你需要的网络自行修改,我会在代码里指出哪里需要修改

点击链接,里面提供了pytorch支持的模型网络
链接: 点击查看pytorch支持的网络.

四、图像分类通用pytorch框架——具体代码实现

跟深度学习的通用框架一样,分四步
一、输入处理模块
0.引入库
1.数据增强
2.加载数据集
二、模型构建模块
三、定义代价函数和优化器模块
四、构建训练过程
3.训练与验证
4.训练与验证结果的可视化展示
5.测试

一、输入处理模块

0.引入库

代码如下:

这个不要更改,直接复制就行

#0导入库
import torch
from torchvision import datasets, models, transforms
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import time

import numpy as np
import matplotlib.pyplot as plt
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
print("0导入库完成")

1.数据增强

代码如下(示例):
这个也直接复制就行

image_transforms = {
    
    'train': transforms.Compose([
        transforms.RandomResizedCrop(size=256, scale=(0.8, 1.0)), #随机裁剪到256*256
        transforms.RandomRotation(degrees=15),#随机旋转
        transforms.RandomHorizontalFlip(p=0.5), #依概率水平旋转
        transforms.CenterCrop(size=224),#中心裁剪到224*224符合resnet的输入要求
        transforms.ToTensor(),#填充
        transforms.Normalize([0.485, 0.456, 0.406],#转化为tensor,并归一化至[0,-1]
                             [0.229, 0.224, 0.225])
    ]),
    'valid': transforms.Compose([
        transforms.Resize(size=256),#图像变换至256
        transforms.CenterCrop(size=224),
        transforms.ToTensor(),#填充
        transforms.Normalize([0.485, 0.456, 0.406],
                             [0.229, 0.224, 0.225])
    ])
}
print("1.数据增强完成")

2.加载数据集

代码如下(示例):
这里只有一个参数需要改,就是num_classes=5这一行
需要把5改成你自己的类数

#2加载数据集
dataset = 'dataset'
train_directory = os.path.join(dataset, 'train') #训练集的路径,os.path.join()函数是路径拼接函数
valid_directory = os.path.join(dataset, 'valid') #验证集的路径
test_directory = os.path.join(dataset , 'test')  #测试集路径
batch_size = 32 #分成32组
num_classes = 5 #图像的类,这里需要改,其余不变!!!

data = {
    
    'train': datasets.ImageFolder(root=train_directory, transform=image_transforms['train']),
     #imagefolder(root, transform),前者是图片路径,后者是对图片的变换,生成的数据类型是dataset
    'valid': datasets.ImageFolder(root=valid_directory, transform=image_transforms['valid']),
    'test': datasets.ImageFolder(root=test_directory, transform=image_transforms['valid'])
}       #把dataset类型的数据放在数组里,便于通过键值调用

train_data_size = len(data['train'])#训练集的大小
valid_data_size = len(data['valid'])#验证集的大小
test_data_size = len(data['test'])#验证集的大小


train_data = DataLoader(data['train'], batch_size=batch_size, shuffle=True)
  #DataLoader(dataset, batch_size, shuffle) dataset数据类型;分组数;是否打乱
valid_data = DataLoader(data['valid'], batch_size=batch_size, shuffle=True)
test_data = DataLoader(data['test'], batch_size=batch_size, shuffle=True)

print("训练集数据量为:{},验证集数据量为:{},验证集数据量为{}".format(train_data_size, valid_data_size,test_data_size))
  #分别打印出训练集和验证集的样本数量
print("2.加载数据完毕")

二、加载模型

代码如下(示例):
这里有两处需要修改
1.nn.Linear(256,5)
其中这个5需要改成你的所需分类数
2.resnet50 = models.resnet50(pretrained=True)
可以把resnet50换成你想要的网络模型,如你要用alexnet,则改成:
alexnet = models.alexnet(pretrained=True)


#3加载模型,迁移学习

resnet50 = models.resnet50(pretrained=True) #开启预训练

for param in resnet50.parameters():#由于预训练的模型中的大多数参数已经训练好了,因此将requires_grad字段重置为false。
    param.requires_grad = False
   #为了适应自己的数据集,将ResNet-50的最后一层替换为,
    #将原来最后一个全连接层的输入喂给一个有256个输出单元的线性层,
   # 接着再连接ReLU层和Dropout层,然后是256 x 5的线性层,输出为5通道的softmax层。
fc_inputs = resnet50.fc.in_features
resnet50.fc = nn.Sequential(
    nn.Linear(fc_inputs, 256),
    nn.ReLU(),
    nn.Dropout(0.4),
    nn.Linear(256, 5),
    nn.LogSoftmax(dim=1)
)


三、定义损失函数、优化器

代码如下(示例):
复制即可

#定义损失函数和优化器。
loss_func = nn.NLLLoss()
optimizer = optim.Adam(resnet50.parameters())
print("3.模型载入完毕")

四、构建训练过程(训练、验证、测试)

3.训练与验证

代码如下(示例):
这个直接复制,这个代码段定义了一个训练与验证函数(把这个过程封装起来了)

#3训练模型 与 验证模型
def train_and_valid(model, loss_function, optimizer, epochs=25):
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")#设备自行判断
    history = []
    best_acc = 0.0
    best_epoch = 0

    for epoch in range(epochs):
        epoch_start = time.time() #每轮开始时间记录
        print("Epoch: {}/{}".format(epoch + 1, epochs)) #显示这是第多少轮

        model.train()  #启用 Batch Normalization 和 Dropout。(随机去除神经元)

        train_loss = 0.0
        train_acc = 0.0
        valid_loss = 0.0
        valid_acc = 0.0

        for i, (inputs, labels) in enumerate(train_data):#训练数据
            inputs = inputs.to(device)
            labels = labels.to(device)

            # 因为这里梯度是累加的,所以每次记得清零
            optimizer.zero_grad()

            outputs = model(inputs)

            loss = loss_function(outputs, labels)

            loss.backward()

            optimizer.step()

            train_loss += loss.item() * inputs.size(0)

            ret, predictions = torch.max(outputs.data, 1)
            correct_counts = predictions.eq(labels.data.view_as(predictions))

            acc = torch.mean(correct_counts.type(torch.FloatTensor))

            train_acc += acc.item() * inputs.size(0)

        with torch.no_grad():#用于通知dropout层和batchnorm层在train和val模式间切换。
            model.eval()#model.eval()中的数据不会进行反向传播,但是仍然需要计算梯度;

            for j, (inputs, labels) in enumerate(valid_data):#验证数据
                inputs = inputs.to(device)   #从valid_data里获得输入和标签
                labels = labels.to(device)

                outputs = model(inputs)  #模型的输出

                loss = loss_function(outputs, labels) #损失计算

                valid_loss += loss.item() * inputs.size(0)

                ret, predictions = torch.max(outputs.data, 1) #在分类问题中,通常需要使用max()函数对tensor进行操作,求出预测值索引。
                #dim是max函数索引的维度0 / 1,0是每列的最大值,1是每行的最大值
                #在多分类任务中我们并不需要知道各类别的预测概率,所以第一个tensor对分类任务没有帮助,而第二个tensor包含了最大概率的索引,所以在实际使用中我们仅获取第二个tensor即可。
                correct_counts = predictions.eq(labels.data.view_as(predictions))

                acc = torch.mean(correct_counts.type(torch.FloatTensor))

                valid_acc += acc.item() * inputs.size(0)

        avg_train_loss = train_loss / train_data_size
        avg_train_acc = train_acc / train_data_size

        avg_valid_loss = valid_loss / valid_data_size
        avg_valid_acc = valid_acc / valid_data_size

        history.append([avg_train_loss, avg_valid_loss, avg_train_acc, avg_valid_acc])

        if best_acc < avg_valid_acc:
            best_acc = avg_valid_acc
            best_epoch = epoch + 1

        epoch_end = time.time()

        print(
            "Epoch: {:03d}, Training: Loss: {:.4f}, Accuracy: {:.4f}%, \n\t\tValidation: Loss: {:.4f}, Accuracy: {:.4f}%, Time: {:.4f}s".format(
                epoch + 1, avg_valid_loss, avg_train_acc * 100, avg_valid_loss, avg_valid_acc * 100,
                epoch_end - epoch_start
            ))
        print("Best Accuracy for validation : {:.4f} at epoch {:03d}".format(best_acc, best_epoch))
        torch.save(model.state_dict(), '.\dataset'+'_model_' + str(epoch + 1) + '.pt')
        
    return model, history

注1:这里是一个开关作用,当你想训练的时候,就把istrain的值设置为1。当整个代码全部执行完之后,如果你还想测试,但不想再训练,就把istrain的值设置为0
注2:nun_epochs=30的意思是我要训练30轮。这个值可以由你自己定。当你调试代码的时候,这个值可以设置为1或2,帮助你快速判断代码是否出错。

istrain=1
if istrain:
    num_epochs = 30
    trained_model, history = train_and_valid(resnet50, loss_func, optimizer, num_epochs)

这一步运行完之后,正确的话会在窗口看到以下的文字:
在这里插入图片描述
需要你记住这里的一个数字,最佳轮数。
对于我而言,这次准确率最佳的轮数是第18轮。所以我需要记住18这个数。对于你而言,这个数可能不是18,可能是1-30里的任何一个数,记住它,最佳轮数。(PS:大家应该看的懂英语吧)

4.训练与可视化展示

代码如下(示例):
可以直接复制
ispicshow=1时会展示,等于0时不展示

#4可视化展示
ispicshow=0
if ispicshow:
    history = np.array(history)
    plt.plot(history[:, 0:2])
    plt.legend(['Tr Loss', 'Val Loss'])
    plt.xlabel('Epoch Number')
    plt.ylabel('Loss')
    plt.ylim(0, 1)
    plt.savefig(dataset + '_loss_curve.png')
    plt.show()

    plt.plot(history[:, 2:4])
    plt.legend(['Tr Accuracy', 'Val Accuracy'])
    plt.xlabel('Epoch Number')
    plt.ylabel('Accuracy')
    plt.ylim(0, 1)
    plt.savefig(dataset + '_accuracy_curve.png')
    plt.show()

5.测试

代码如下(示例):

首先定义了一个测试函数(封装了测试过程)
在这个测试函数的第一行,即
resnet50.load_state_dict(torch.load(’.\dataset’ + ‘model’ + ‘18’ + ‘.pt’))这行
这里面有个数字,18.他就是刚才要你记住的最佳轮数。把它换成你自己机器运行后显示的最佳轮数即可

#6测试模型
def test(model,loss_function):
    resnet50.load_state_dict(torch.load('.\dataset' + '_model_' + '18' + '.pt'))
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 设备自行判断
    test_loss = 0.0
    test_acc = 0.0
    test_start = time.time()
    with torch.no_grad():  # 用于通知dropout层和batchnorm层在train和val模式间切换。
        model.eval()  # model.eval()中的数据不会进行反向传播,但是仍然需要计算梯度;
    for j, (inputs, labels) in enumerate(test_data):  # 验证数据
        inputs = inputs.to(device)  # 从test_data里获得输入和标签
        labels = labels.to(device)

        outputs = model(inputs)  # 模型的输出

        loss = loss_function(outputs, labels)  # 损失计算

        test_loss += loss.item() * inputs.size(0)

        ret, predictions = torch.max(outputs.data, 1)  # 在分类问题中,通常需要使用max()函数对tensor进行操作,求出预测值索引。

        correct_counts = predictions.eq(labels.data.view_as(predictions))

        acc = torch.mean(correct_counts.type(torch.FloatTensor))

        test_acc += acc.item() * inputs.size(0)


    avg_test_loss = test_loss / test_data_size
    avg_test_acc = test_acc / test_data_size
    test_end = time.time()

    print(
        "test: Loss: {:.4f}, Accuracy: {:.4f}%, Time: {:.4f}s".format(
            avg_test_loss, avg_test_acc * 100,
            test_end - test_start
        ))

istest=1表示可以进行训练,等于0表示不能
如果你用的模型不是resnet,则你需要把 test(resnet50,loss_func)中的resnet50改成你自己选择的模型网络。例如,如果你最开始选的是用alexnet,则需要改成:test(alexnet,loss_func)

istest=1
if istest:
    test(resnet50,loss_func)

至此,傻瓜式教学就结束了,不懂原理的同学可以根据自己的需要改改就能用了


总结

本文其实是对另一个作者的代码理解后的重新组织,帮助更多的小白快速入手。相对于原文章( 点击阅读原文.),本文封装了更多的细节,只提供了几个对开发者或使用者修改的接口,一定程度上降低了图像分类的门槛。

如果大家想继续深入学习,而不是玩玩而已的话,还是需要先慢慢打好基础再来进入实战。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Gambler_Yushen/article/details/114855882

智能推荐

【转载】给未来的电子工程师_《给未来的电子工程师》-程序员宅基地

文章浏览阅读1.4k次。看这篇帖子的,我想都是电子爱好者或电类专业学生。不知道大家都处于 什么一个阶段,这篇帖子是写给入门者的,要解决一个问题:初学者应重点掌握什么电子知识,大学阶段如何学习?_《给未来的电子工程师》

Android 4.4 Settings 应用初步分析-程序员宅基地

文章浏览阅读170次。 一次偶然要在设置里面增加一个菜单,需要修改到settings_headers.xml 文件(res/layout/xml) 文件,所以就觉得要看一下这个流程.就做一下笔记,语言组织能力不行啊.分析Android 源码的时候导入单个应用的时候一般是会有很多错误的,因为需要导入系统编译之后生成的jar包才能消除eclipse 里面的哪些红色xx.1.Settings的UI2.流程分析..._android4.4 settings应用分析

国内chrome插件网站推荐-程序员宅基地

文章浏览阅读967次。https://www.chromefor.com/转载于:https://www.cnblogs.com/johnyhe/p/10795382.html

ubuntu18 离线安装 jdk11_ubuntu离线安装jdk11-程序员宅基地

文章浏览阅读1.2k次。ubuntu18 离线安装 jdk111.下载jdk2.解压文件3.配置环境变量4.创建软链接到/usr/bin5.测试1.文件下载2.解压文件3.配置环境变量4.创建软链接到/usr/bin5.测试1.下载jdk2.解压文件3.配置环境变量4.创建软链接到/usr/bin5.测试1.文件下载进入官网下载,但下载可能需要注册比较麻烦可以2.解压文件将压缩包进行解压,最好解压到/usr/local/java/ 目录3.配置环境变量jdk 11取消了jre 文件不需要配置命令行输._ubuntu离线安装jdk11

Android 单元测试(四) 之AndroidJUnitRunner基础-程序员宅基地

文章浏览阅读1.3w次。AndroidJUnitRunner,Google官方的android单元测试框架之一,适用于 Android 且与 JUnit 4 兼容的测试运行器!测试运行器可以将测试软件包和要测试的应用加载到设备、运行测试并报告测试结果。 此测试运行器的主要功能包括:JUnit 支持访问仪器信息测试筛选测试分片要求 Android 2.2(API 级别 8)或更高版本。JUnit 支..._androidjunitrunner

【多目标优化】Pareto最优解很少_多目标规划中帕累托支配解少该怎么办-程序员宅基地

文章浏览阅读1.3w次。一个随机产生的100BCell群体,其中能有多少是Pareto最优解?我的答案是很少,少到几乎接近0了,偶尔才有一两个,显然我觉得自己在什么地方搞错了。我们说BCell a优于BCell b当且仅当 a在各个目标上都不劣于b,并且至少一个目标上优于b,这是Pareto最优解的定义。于是在一个群体中的非劣解,必须优胜于其他如99个个体,这个概率实在是很小。还是证明100个群体中最多只有一个Paret_多目标规划中帕累托支配解少该怎么办

随便推点

Oracle常用查看表结构命令_oracle 查看表结构命令-程序员宅基地

文章浏览阅读5.5k次。获取表: select table_name from user_tables; //当前用户的表 select table_name from all_tables; //所有用户的表 select table_name from dba_tables; //包括系统表 select table_name from dba_tabl_oracle 查看表结构命令

点击tabbar刷新页面_点击tabbar页面刷新-程序员宅基地

文章浏览阅读5k次。.h文件#import @interface TabbarRootViewController : [email protected]文件#import "TabbarRootViewController.h"#import "NoticeTableViewController.h"@implementation TabbarRootViewControll_点击tabbar页面刷新

linux 个人知识管理软件,知识管理工具, 自由格式数据库, 笔记软件以及个人信息管理...-程序员宅基地

文章浏览阅读206次。myBase Desktop 7.3.5 for Linux/MacOSX/Windows 2020-2-20新增 [数学公式 MathJax for Markdown]、[自动锁屏保护]、[粘贴图片为附件]、[完整复制带图片内容],以及部分问题修订,详见:更新日志;数学公式插件:缺省安装后软件会尝试从 CDN 实时下载并渲染 Markdown 文档中的数学公式,通常无需额外手动安装,但需保持网络..._linux知识管理软件

STUFF函数介绍及使用场景_staff函数-程序员宅基地

文章浏览阅读2.7k次,点赞3次,收藏5次。STUFF ( character_expression , start , length ,character_expression )参数character_expression一个字符数据表达式。character_expression 可以是常量、变量,也可以是字符列或二进制数据列。start一个整数值,指定删除和插入的开始位置。如果 start 或 length 为负,则返回空..._staff函数

Webstorm实时预览失败原因_webstorm打开.md文件内容显示为空-程序员宅基地

文章浏览阅读862次。今天在webstorm按照网上的教程安装live edit,并且在chrome安装插件jetbrainside supportdebug实时预览时报错failed to attach debugger原因是目前 debug 时不需要Chrome扩展程序。在最新的Chrome版本该功能无法可靠运行。解决办法webstorm左上角点击File --> Setting --> Build,Execution,Deployment --> Debugger --> Live_webstorm打开.md文件内容显示为空

调用图灵机器人做一个简单的机器人聊天-程序员宅基地

文章浏览阅读222次。做一个简单的网页版机器人聊天。首页,创建页面html<div id="box"> <div class="box-head"> <span class="h-span">小明机器人</span> </div> <div class="box-body"&..._h5调用图灵机器人做聊天软件