基于经典网络架构训练图像分类模型(超详细批注和完整思路!!!)

说明:

        这里我们将要完成一个图像分类任务的套路整理和讲解。希望可以帮助大家学习人工智能!

导入的包:

import os
import matplotlib.pyplot as plt
import numpy as np
import torch
from torch import nn
import torch.optim as optim
# import matplotlib
# matplotlib.use('Agg')
# pip install torchvision
from torchvision import transforms, models, datasets
# https://pytorch.org/docs/stable/torchvision/index.html
import time
import copy
import json
from PIL import Image

思路:

一.数据处理与预处理操作

    1.数据集的路径设定

    2.数据增强处理(数据集不够的情况,有时候也可以去网上爬)

data_transforms = {
    'train': transforms.Compose([transforms.RandomRotation(45),  # 随机旋转,-45到45度之间随机选
                                 transforms.CenterCrop(224),  # 从中心开始裁剪
                                 transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转 选择一个概率概率
                                 transforms.RandomVerticalFlip(p=0.5),  # 随机垂直翻转
                                 transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),
                                 # 参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
                                 transforms.RandomGrayscale(p=0.025),  # 概率转换成灰度率,3通道就是R=G=B
                                 transforms.ToTensor(),
                                 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # 均值,标准差
                                 ]),
    'valid': transforms.Compose([transforms.Resize(256),
                                 transforms.CenterCrop(224),
                                 transforms.ToTensor(),
                                 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                 ]),
}

    3.加载数据集并分组

    4.查看数据是否有误(好习惯得养成)

二.加载models中提供的模型,并且直接用训练的好权重当做初始化参数

    1.选择模型(这里我们选择的是resnet152,现在基本上都不用vgg了)

    2.是否需要用人家训练好得特征来做(根据你的情况而定) 

    3.是否用GPU(gpu好处不用多说,gpu一般比cpu快十倍左右)

train_on_gpu = torch.cuda.is_available()

if not train_on_gpu:
    print('CUDA is not available.  Training on CPU ...')
else:
    print('CUDA is available!  Training on GPU ...')

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

        注意:优化器、损失函数、数据和标签需要放在device上。

model.to(device)
loss_function.to(device)
datas.to(device)
labels.to(device)

三.学习迁移(说白了就是在人家模型上面修修改改后拿来自己用)

    1.设置冻结函数(就是你想让人家的哪些参数不变)

def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False

       如果 feature_extracting 为 True,则会将参数的 requires_grad 属性设置为 False,这样就会阻止这些参数在反向传播过程中更新其梯度。

    2.修改模型

    一般我们只需要修改最后一层(比如人家的是4096到1000,但我们只需要分出10类即可),通常来说有两种修改方法:

原来的网络模型:

a.直接在最后一层上面修改 ( 4096到1000 )

module.classifier[6] = nn.Linear(4096,10)

b.在最后一层后面再加一层 (4096到1000, 1000再到10 )

model.add_module('add_linear',nn.Linear(1000,10))

    3.设置哪些层需要训练

四.优化器设置

optimizer_ft = optim.Adam(params_to_update, lr=1e-2)

       常用的随机梯度下降方法有Adam和SGD,通常Adam的效果会更好一些。

       SGD仅使用当前批次的梯度来更新参数,它是一种基于单一梯度估计的方法,因此更新可能会受到噪声的干扰。

        Adam结合了梯度的一阶矩估计(平均梯度)和二阶矩估计(平均梯度的平方根),从而更准确地估计梯度。这使得Adam在噪声较大的情况下更稳定。

五.损失函数设置

    1.损失函数的选择(【机器学习】损失函数的选择总结-CSDN博客

    2.这里选择的是二分类经典损失函数二元交叉熵损失(Binary Cross-Entropy Loss)

criterion = nn.NLLLoss()

        值得一提的是,我们的这个模型中最后一层已经LogSoftmax()了,所以不能直接用nn.CrossEntropyLoss()来计算了,因为nn.CrossEntropyLoss()相当于logSoftmax()和nn.NLLLoss()整合,我们只需要用nn.NLLLoss()即可。

六.训练模块

    1.创建准确率和损失字典(通常来说这两个值是我们最关心的)

val_acc_history = []
    train_acc_history = []
    train_losses = []
    valid_losses = []
    LRs = [optimizer.param_groups[0]['lr']]  # LRs 将记录每个轮次开始时的学习率

    2.创建一个字典来保存效果最好的模型

best_model_wts = copy.deepcopy(model.state_dict()) 

    3.梯度清零(在每次反向传播之前需要执行这一步)

optimizer.zero_grad()

        这一步将优化器负责的所有模型参数的梯度设置为零。这很重要,因为在反向传播期间,每批数据都会累积梯度,如果在每个批次或迭代开始时不重置它们,则最终可能会得到不正确的梯度值。

    4.前向传播

outputs = model(inputs)  # outputs里面是每一个类别的概率
loss = criterion(outputs, labels)

        在前向传播中,输入经过模型,产生输出。然后,计算损失函数,该函数衡量模型输出与实际标签之间的差异。

    5.反向传播

loss.backward()

        在反向传播中,它触发了自动求导(Automatic differentiation)的过程。深度学习框架(如PyTorch或TensorFlow)会根据损失函数计算模型参数的梯度,即损失函数对每个可学习参数的偏导数。

        而后会进行梯度传播,这会将算得到的梯度从损失函数反向传播到模型的每一层。一旦梯度传播完成,模型的参数就可以通过优化算法进行更新。

    6.参数更新

optimizer.step()

       它会根据向后传递(反向传播)期间计算的梯度执行参数更新。换句话说,它会在减少损失的方向上更新模型的参数。

    7.计算损失和准确率

    8.保存模型

七.第一次训练(用别人的参数和修改过后的模型)

model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(model_ft, dataloaders,
            criterion, optimizer_ft,                                                                                                                        
            num_epochs=50,# 可以根据情况改num_epochs的值
            is_inception=(                                                                                
            model_name == "inception"))  

八.第二次训练(用自己的参数从头到尾完整训练,所以训练时间可能会翻几倍)

# 再继续训练所有的参数,学习率调小一点
optimizer = optim.Adam(params_to_update, lr=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

# 损失函数
criterion = nn.NLLLoss()

# Load the checkpoint
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])
optimizer.load_state_dict(checkpoint['optimizer'])
# model_ft.class_to_idx = checkpoint['mapping']

model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(model_ft, dataloaders,                                                                                     
            criterion, optimizer,                                                                                                   
            num_epochs=30,# 可以根据情况改num_epochs的值                                                                                                                                                                      
            is_inception=(                                                                                                  
            model_name == "inception"))

    1.为什么要训练第二次?通常来说在第一次训练后的基础上再训练完整的一次可以更好地拟合你的数据集,从而提高你的准确率,使模型的效果提升upup~

    2.为什么第二次训练的学习率要调低一些?在深度学习中,将学习率适当调小一点进行第二次训练是一个常见的策略,通常称为学习率退火(learning rate annealing)学习率调度(learning rate scheduling)。简单来说就是可以减小过拟合的风险,因为在第一次训练中,模型可能已经适应了训练数据的细节和噪声。

九.加载训练好的模型

十.测试数据预处理

十一.展示结果

完整代码:

# 数据读取与预处理操作
data_dir = './jinggai_data/'
train_dir = data_dir + '/train'
valid_dir = data_dir + '/valid'

# 制作好数据源:
# data_transforms中指定了所有图像预处理操作
# ImageFolder假设所有的文件按文件夹保存好,每个文件夹下面存贮同一类别的图片,文件夹的名字为分类的名字

data_transforms = {
    'train': transforms.Compose([transforms.RandomRotation(45),  # 随机旋转,-45到45度之间随机选
                                 transforms.CenterCrop(224),  # 从中心开始裁剪
                                 transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转 选择一个概率概率
                                 transforms.RandomVerticalFlip(p=0.5),  # 随机垂直翻转
                                 transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),
                                 # 参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
                                 transforms.RandomGrayscale(p=0.025),  # 概率转换成灰度率,3通道就是R=G=B
                                 transforms.ToTensor(),
                                 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  # 均值,标准差
                                 ]),
    'valid': transforms.Compose([transforms.Resize(256),
                                 transforms.CenterCrop(224),
                                 transforms.ToTensor(),
                                 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                 ]),
}

batch_size = 8
# image_datasets 将是一个字典,包含两个键值对,分别对应训练集和验证集的图像数据集。
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'valid']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in
               ['train', 'valid']}  # shuffle将数据集洗牌可以帮助模型更好地学习
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}

class_names = image_datasets['train'].classes

# 读取标签对应的实际名字
with open('cat_to_name.json', 'r') as f:
    cat_to_name = json.load(f)


# # 展示下数据
def im_convert(tensor):
    """ 展示数据"""

    image = tensor.to("cpu").clone().detach()  # 创建副本,并使用detach()分离它确保不会影响原始张量。
    image = image.numpy().squeeze()  # squeeze() 方法来移除维度中的尺寸为1的维度,以得到一个更紧凑的数组。
    image = image.transpose(1, 2, 0)  # 将图像数据调整为 H x W x C
    image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))  # 归一化的逆操作
    image = image.clip(0, 1)  # 确保像素值在 0 到 1 的范围

    return image


#
#
# fig = plt.figure(figsize=(20, 12))  # 指定了图形的大小为 20x12 英寸
# columns = 4
# rows = 2
#
# dataiter = iter(dataloaders['valid'])  # 遍历验证数据集dataloaders['valid']中的批次(batches)
#
# # 修改这里,使用for循环来获取数据
# for inputs, classes in dataiter:
#     for idx in range(columns * rows):
#         ax = fig.add_subplot(rows, columns, idx + 1, xticks=[], yticks=[])  # # idx + 1 表示在当前行和列中的位置
#         ax.set_title(cat_to_name[str(int(class_names[classes[idx]]))])
#         plt.imshow(im_convert(inputs[idx]))
#     plt.show()

# 加载models中提供的模型,并且直接用训练的好权重当做初始化参数
model_name = 'resnet'  # 可选的比较多 ['resnet', 'alexnet', 'vgg', 'squeezenet', 'densenet', 'inception']
# 是否用人家训练好的特征来做
feature_extract = True
# 是否用GPU训练
train_on_gpu = torch.cuda.is_available()

if not train_on_gpu:
    print('CUDA is not available.  Training on CPU ...')
else:
    print('CUDA is available!  Training on GPU ...')

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")


# 用于设置模型的参数是否需要梯度更新
def set_parameter_requires_grad(model, feature_extracting):
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False


# 如果 feature_extracting 为 True,则将参数的 requires_grad 属性设置为 False,这样就会阻止这些参数在反向传播过程中更新其梯度。

model_ft = models.resnet152()
print("原来的resnet模型:", model_ft)


# 参考pytorch官网例子
def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):
    # 选择合适的模型,不同模型的初始化方法稍微有点区别
    model_ft = None
    input_size = 0

    if model_name == "resnet":
        """ Resnet152
        """
        model_ft = models.resnet152(pretrained=use_pretrained)
        set_parameter_requires_grad(model_ft, feature_extract)  # 这里在对模型修改前应设置模型的参数是否需要梯度更新
        num_ftrs = model_ft.fc.in_features
        model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, 5),
                                    nn.LogSoftmax(dim=1))
        input_size = 224

    else:
        print("Invalid model name, exiting...")
        exit()

    return model_ft, input_size


# 设置哪些层需要训练
model_ft, input_size = initialize_model(model_name, 102, feature_extract, use_pretrained=True)
print("改动过后的resnet模型:", model_ft)

# GPU计算
model_ft = model_ft.to(device)

#  模型保存
filename = 'checkpoint.pth'

# 是否训练所有层
params_to_update = model_ft.parameters()
print("Params to learn:")
if feature_extract:
    params_to_update = []
    for name, param in model_ft.named_parameters():
        if param.requires_grad:  # 参数的requires_grad默认是True,所以后面修改的参数是True
            params_to_update.append(param)
            print("\t", name)
else:
    for name, param in model_ft.named_parameters():
        if param.requires_grad:
            print("\t", name)

# 优化器设置
# 优化器设置
optimizer_ft = optim.Adam(params_to_update, lr=1e-2)  # Adam能自动调整学习率,通常比SGD好
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)  # 学习率每7个epoch衰减成原来的1/10
# 最后一层已经LogSoftmax()了,所以不能nn.CrossEntropyLoss()来计算了,nn.CrossEntropyLoss()相当于logSoftmax()和nn.NLLLoss()整合
criterion = nn.NLLLoss()


# 训练模块
def train_model(model, dataloaders, criterion, optimizer, num_epochs=25, is_inception=False, filename=filename):
    since = time.time()
    best_acc = 0
    """
    checkpoint = torch.load(filename)
    best_acc = checkpoint['best_acc']
    model.load_state_dict(checkpoint['state_dict'])
    optimizer.load_state_dict(checkpoint['optimizer'])
    model.class_to_idx = checkpoint['mapping']
    """
    model.to(device)

    val_acc_history = []
    train_acc_history = []
    train_losses = []
    valid_losses = []
    LRs = [optimizer.param_groups[0]['lr']]  # LRs 将记录每个轮次开始时的学习率

    best_model_wts = copy.deepcopy(model.state_dict())  # 这个变量在后面的代码中用于保存最佳模型

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 训练和验证
        for phase in ['train', 'valid']:
            if phase == 'train':
                model.train()  # 训练
            else:
                model.eval()  # 验证

            running_loss = 0.0
            running_corrects = 0

            # 把数据都取个遍
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # 清零
                optimizer.zero_grad()  # 在每次反向传播之前需要执行这一步
                # 只有训练的时候计算和更新梯度
                with torch.set_grad_enabled(phase == 'train'):
                    if is_inception and phase == 'train':  # resnet没有用到inception模块
                        outputs, aux_outputs = model(inputs)
                        loss1 = criterion(outputs, labels)
                        loss2 = criterion(aux_outputs, labels)
                        loss = loss1 + 0.4 * loss2
                    else:  # resnet执行的是这里
                        outputs = model(inputs)  # outputs里面是每一个类别的概率
                        loss = criterion(outputs, labels)

                    _, preds = torch.max(outputs, 1)  # 1表示沿着第一个(通常是类别维度),preds包含了这些类别的索引,_表示不关心

                    # 训练阶段更新权重
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # 计算损失
                running_loss += loss.item() * inputs.size(0)  # running_loss用于累积每个批次的损失
                running_corrects += torch.sum(preds == labels.data)  # running_corrects用于累积每个批次中正确分类的样本数量

            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)  # .double() 是用于确保进行浮点数除法

            time_elapsed = time.time() - since
            print('Time elapsed {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))  # :.4f将浮点数格式化为四位小数的形式

            # 得到最好那次的模型
            if phase == 'valid' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())  # state_dict():模型的状态字典包含了模型的所有权重和参数
                state = {
                    'state_dict': model.state_dict(),
                    'best_acc': best_acc,
                    'optimizer': optimizer.state_dict(),
                }
                torch.save(state, filename)
            if phase == 'valid':
                val_acc_history.append(epoch_acc)
                valid_losses.append(epoch_loss)
                scheduler.step(epoch_loss)
            if phase == 'train':
                train_acc_history.append(epoch_acc)
                train_losses.append(epoch_loss)

        print('Optimizer learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))
        LRs.append(optimizer.param_groups[0]['lr'])
        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # 训练完后用最好的一次当做模型最终的结果
    model.load_state_dict(best_model_wts)
    return model, val_acc_history, train_acc_history, valid_losses, train_losses, LRs


# 开始训练!!!
print("----------------第一次训练---------------")
model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(model_ft, dataloaders,
                                                                                            criterion, optimizer_ft,
                                                                                            num_epochs=50,
                                                                                            # 可以根据情况改num_epochs的值
                                                                                            is_inception=(
                                                                                                    model_name == "inception"))
# 再继续训练所有层
print("----------------第二次训练---------------")
for param in model_ft.parameters():
    param.requires_grad = True

# 再继续训练所有的参数,学习率调小一点
optimizer = optim.Adam(params_to_update, lr=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

# 损失函数
criterion = nn.NLLLoss()

# Load the checkpoint
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])
optimizer.load_state_dict(checkpoint['optimizer'])
# model_ft.class_to_idx = checkpoint['mapping']

model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(model_ft, dataloaders,
                                                                                            criterion, optimizer,
                                                                                            num_epochs=30,
                                                                                            # 可以根据情况改num_epochs的值
                                                                                            is_inception=(
                                                                                                    model_name == "inception"))


# 加载训练好的模型
model_ft, input_size = initialize_model(model_name, 102, feature_extract, use_pretrained=True)

# GPU模式
model_ft = model_ft.to(device)

#  保存文件的名字
filename = 'checkpoint.pth'

# 加载模型
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])


# 测试数据预处理
# 测试数据处理方法需要跟训练时一直才可以
# crop操作的目的是保证输入的大小是一致的
# 标准化操作也是必须的,用跟训练数据相同的mean和std,但是需要注意一点训练数据是在0-1上进行标准化,所以测试数据也需要先归一化
# 最后一点,PyTorch中颜色通道是第一个维度,跟很多工具包都不一样,需要转换

def process_image(image_path):
    # 读取测试数据
    img = Image.open(image_path)
    # Resize,thumbnail方法只能进行缩小,所以进行了判断
    if img.size[0] > img.size[1]:
        img.thumbnail((10000, 256))
    else:
        img.thumbnail((256, 10000))
    # Crop操作
    left_margin = (img.width - 224) / 2
    bottom_margin = (img.height - 224) / 2
    right_margin = left_margin + 224
    top_margin = bottom_margin + 224
    img = img.crop((left_margin, bottom_margin, right_margin, top_margin))
    # 相同的预处理方法
    img = np.array(img) / 255
    mean = np.array([0.485, 0.456, 0.406])  # provided mean
    std = np.array([0.229, 0.224, 0.225])  # provided std
    img = (img - mean) / std

    # 注意颜色通道应该放在第一个位置
    img = img.transpose((2, 0, 1))

    return img


def imshow(image, ax=None, title=None):
    """展示数据"""
    if ax is None:
        fig, ax = plt.subplots()

    # 颜色通道还原
    image = np.array(image).transpose((1, 2, 0))

    # 预处理还原
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    image = std * image + mean
    image = np.clip(image, 0, 1)

    ax.imshow(image)
    ax.set_title(title)

    return ax


image_path = 'D:\\学习资料\\人工智能生物信息\\jinggai\\jinggai_data\\valid\\1\\well1_0004.jpg'
img = process_image(image_path)
imshow(img)
plt.show()

# 得到一个batch的测试数据
dataiter = iter(dataloaders['valid'])
images, labels = next(dataiter)

model_ft.eval()

if train_on_gpu:
    output = model_ft(images.cuda())
else:
    output = model_ft(images)

# 得到概率最大的那个
_, preds_tensor = torch.max(output, 1)

preds = np.squeeze(preds_tensor.numpy()) if not train_on_gpu else np.squeeze(preds_tensor.cpu().numpy())
preds

# 展示预测结果
fig = plt.figure(figsize=(20, 20))
columns = 4
rows = 2

for idx in range(columns * rows):
    ax = fig.add_subplot(rows, columns, idx + 1, xticks=[], yticks=[])
    plt.imshow(im_convert(images[idx]))
    ax.set_title("{} ({})".format(cat_to_name[str(preds[idx])], cat_to_name[str(labels[idx].item())]),
                 color=("green" if cat_to_name[str(preds[idx])] == cat_to_name[str(labels[idx].item())] else "red"))
plt.show()

数据集可以换成自己的,因为数据集太大,可以自己随便找个试试。如果觉得有帮助可以点个赞哦(嘿嘿),如果有什么地方讲述地不太正确也欢迎指出!

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值