初识CNN03——预训练与迁移学习

发布于:2025-08-18 ⋅ 阅读:(12) ⋅ 点赞:(0)

系列文章目录

初识CNN01——认识CNN
初识CNN02——认识CNN2



一、数据集获取

1.1 数据集分类

计算机视觉数据集主要分为两类,其差异体现在标注形式与任务类型上:

1. 图像分类数据集 (Image Classification Datasets)

  • 标注形式
    以目录层级结构隐式存储标签,每个子目录名即为类别标签(如 train/dog/xxx.jpg, train/cat/yyy.jpg)。
  • 标注特点
    标注信息轻量化(仅需类别名称),无需额外标注文件,目录结构即标签映射。

典型代表:ImageNet、CIFAR-10、MNIST

2.密集标注数据集 (Dense Annotation Datasets)

  • 标注形式
    需独立的标注文件(如JSON/XML)存储目标位置与语义信息,常见格式包括:
    • 目标检测:边界框坐标 + 类别(COCO格式的 .json)
    • 图像分割:像素级掩码图(PNG)或多边形坐标(VOC格式的 .xml)
  • 标注特点
    标注信息复杂,需专用工具制作。

典型代表:COCO(检测/分割)、PASCAL VOC(检测/分割)、Cityscapes(语义分割)

1.2 数据集来源

1.3 图片本地化

使用公开数据集时,会自动保存到本地。如果已下载,就不会重复下载。但所下载的数据集并非图片形式,如果需要以图片的形式保存到本地以方便观察和重新处理,可以按照如下方式处理。

dir = os.path.dirname(__file__)
def save2local():
    trainimgsdir = os.path.join(dir, "MNIST/trainimgs")
    testimgsdir = os.path.join(dir, "MNIST/testimgs")
    if not os.path.exists(trainimgsdir):
        os.makedirs(trainimgsdir)
    if not os.path.exists(testimgsdir):
        os.makedirs(testimgsdir)

    trainset = torchvision.datasets.MNIST(
        root=datapath,
        train=True,
        download=True,
        transform=transforms.Compose([transforms.ToTensor()]),
    )
    for idx, (img, label) in enumerate(trainset):
        labdir = os.path.join(trainimgsdir, str(label))
        os.makedirs(labdir, exist_ok=True)
        pilimg = transforms.ToPILImage()(img)
        # 保存成单通道的灰度图
        pilimg = pilimg.convert("L")
        pilimg.save(os.path.join(labdir, f"{idx}.png"))

    # 加载测试集
    testset = torchvision.datasets.MNIST(
        root=datapath,
        train=False,
        download=True,
        transform=transforms.Compose([transforms.ToTensor()]),
    )
    for idx, (img, label) in enumerate(testset):
        labdir = os.path.join(testimgsdir, str(label))
        os.makedirs(labdir, exist_ok=True)
        pilimg = transforms.ToPILImage()(img)
        # 保存成单通道的灰度图
        pilimg = pilimg.convert("L")
        pilimg.save(os.path.join(labdir, f"{idx}.png"))

    print("所有图片保存成功")

二、训练过程可视化

这里使用Tensor Board进行讲解。(附官方文档

2.1 准备工作

安装:安装的是执行指令,是一个本地化的服务器

pip install tensorboard

导入tensorboard操作模块

from torch.utils.tensorboard import SummaryWriter

指定tensorboard日志保存路径:可以指定多个实例对象

dir = os.path.dirname(__file__)
tbpath = os.path.join(dir, "tensorboard")
# 指定tensorboard日志保存路径
writer = SummaryWriter(log_dir=tbpath)

2.2 训练过程曲线

记录训练数据

# 记录训练数据到可视化面板
writer.add_scalar("Loss/train", loss, epoch)
writer.add_scalar("Accuracy/train", acc, epoch)

训练完后记得关闭

writer.close()

在训练完成后,查看训练结果,在当前目录下,打开控制台窗口:

tensorboard --logdir=runs

控制台会提示一个访问地址,通常为http://localhost:6006/,用浏览器直接访问即可。

2.3 模型可视化

保存网络结构到tensorboard

# 保存模型结构到tensorboard
writer.add_graph(net, input_to_model=torch.randn(1, 1, 28, 28))
writer.close()

启动tensorboard,在graphs菜单即可看到模型结构

# 获取模型参数并循环记录
params = net.named_parameters()
for name, param in params:
	writer.add_histogram(f"{name}_{i}", param.clone().cpu().data.numpy(), epoch)

2.4 记录训练数据

tensorboard中的add_image函数用于将图像数据记录到TensorBoard,以便可视化和分析。这对于查看训练过程中生成的图像、调试和理解模型的行为非常有用,如帮助检查预处理是否生效。

#查看预处理的旋转是否生效
for i, data in enumerate(trainloader, 0):
    inputs, labels = data
    if i % 100 == 0:
        img_grid = torchvision.utils.make_grid(inputs)
        writer.add_image(f"r_m_{epoch}_{i * 100}", img_grid, epoch * len(trainloader) + i)

2.5 补充

  • 尽可能使用新版本的pyTorch

    pip install tensorboard
    
  • 在创建实例对象时使用默认值

    # 导入训练过程可视化工具tensorboard
    from torch.utils.tensorboard import SummaryWriter
    # 实例对象默认将结果写入 ./runs/
    writer = SummaryWriter()
    

    启动tensorboard:

    tensorboard --logdir=runs
    
  • 不要在IDE(如vsCode)里面安装tensorboard插件

三、验证结果数据化

3.1 数据结果Excel

训练模型后,可以将模型预测结果导出为结构化Excel文件,便于定量分析。首先需要导入数据分析模块pandas。

import pandas as pd

把推理结果softmax化,并放到全局的列表里面。

#定义全局的 pd_data :10分类,加了分类id 和  分类名称, 12个
pd_data = np.empty((0, 12))

#在训练的过程中把训练结果整理进来
out_softmax = torch.softmax(out, dim=1).cpu().detach().numpy()
target_y = y.cpu().detach().numpy()
#根据目标值找到分类名
target_name = np.array([vaild_dataset.classes[i] for i in target_y])
# 把真实所属分类追加到数据中
out_softmax = np.concatenate(
    (out_softmax, target_y.reshape(-1, 1), target_name.reshape(-1, 1)), axis=1
)
#数据追加
pd_data = np.concatenate((pd_data, out_softmax), axis=0)

保存数据到CSV。

# 数据有了,找到列名:和前面追加的目标值及类名要呼应上
columnsn = np.concatenate(
    (vaild_dataset.classes, np.array(["target", "真实值"])), axis=0
)
pd_data_df = pd.DataFrame(pd_data, columns=columnsn)

# 把数据保存到Excel:CSV
csvpath = os.path.join(dir, "vaild")
if not os.path.exists(csvpath):
    os.makedirs(csvpath)

pd_data_df.to_csv(os.path.join(csvpath, "vaild.csv"))

3.2 模型指标矩阵化

3.2.1 混淆矩阵

混淆矩阵是一种特定的表格布局,用于可视化监督学习算法的性能,特别是分类算法。在这个矩阵中,每一行代表实际类别,每一列代表预测类别。矩阵的每个单元格则包含了在该实际类别和预测类别下的样本数量。通过混淆矩阵,我们不仅可以计算出诸如准确度、精确度和召回率等评估指标,还可以更全面地了解模型在不同类别上的性能。
在这里插入图片描述
混淆矩阵的四个基本组成部分是:

  1. True Positives(TP):当模型预测为正类,并且该预测是正确的,我们称之为真正(True Positive);
  2. True Negatives(TN):当模型预测为负类,并且该预测是正确的,我们称之为真负(True Negative);
  3. False Positives(FP):当模型预测为正类,但该预测是错误的,我们称之为假正(False Positive);
  4. False Negatives(FN):当模型预测为负类,但该预测是错误的,我们称之为假负(False Negative)

3.2.2 常见指标

在这里插入图片描述
实际上,宁肯错杀一千不肯放过一个,就是提高召回率,而降低精确度的做法

3.2.3 模型指标计算及可视化

csvpath = os.path.join(dir, "vaild")
# 读取CSV数据
csvdata = pd.read_csv(
    os.path.join(csvpath, "vaild.csv"), encoding="GBK", index_col=0
)
# 拿到真实标签
true_label = csvdata["target"].values
print(true_label, type(true_label), len(true_label))

# 获取预测标签
predict_label = csvdata.iloc[:, :-2].values
print(predict_label, predict_label.shape)
# 预测分类及分数的提取
predict_label_ind = np.argmax(predict_label, axis=1)
predict_label_score = np.max(predict_label, axis=1)
print(predict_label_ind, predict_label_score)
# 根据预测值和真实值生成分类报告
report = classification_report(y_true=true_label, y_pred=predict_label_ind)
print(report)

在这里插入图片描述

四、预训练和迁移学习

预训练就是在原始的已经学习了基本特征的权重参数基础之上,继续进行训练,而不是每次都从0开始。

原始权重参数来源:

  1. 官方经典网络模型的预训练参数:别人已经训练好了;
  2. 也可以是自己训练好的权重文件;

4.1 迁移学习步骤

  • 导入
from torchvision.models import resnet18, ResNet18_Weights
  • 初始化
weight = ResNet18_Weights.DEFAULT
model = resnet18(weights=weight)
model.to(device)
  • 保存初始权重文件
# 保存模型权重文件到本地
if not os.path.exists(os.path.join(mdelpath, f"model_res18.pth")):
    torch.save(model.state_dict(), os.path.join(mdelpath, f"model_res18.pth"))
  • 修改网络结构
    ResNet18默认有1000个类别,和我们的需求不匹配需要修改网络结构。所以我们需要重新加载resnet18模型并修改网络结构。
# 重新加载网络模型:需要根据分类任务进行模型结构调整
pretrained_model = resnet18(weights=None)
# print(pretrained_model)
in_features_num = pretrained_model.fc.in_features
pretrained_model.fc = nn.Linear(in_features_num, 10)
  • 调整权重参数
    以满足调整网络结构后的新模型,主要在全连接层。
# 加载刚才下载的权重参数
weight18 = torch.load(os.path.join(mdelpath, f"model_res18.pth"))
print(weight18.keys())
# 全连接层被我们修改了,需要删除历史的全连接层参数
weight18.pop("fc.weight")
weight18.pop("fc.bias")

# 获取自己的模型的参数信息
my_resnet18_dict = pretrained_model.state_dict()

# 去除不必要的权重参数
weight18 = {k:v for k, v in weight18.items() if k in my_resnet18_dict}

#更新
my_resnet18_dict.update(weight18)

  • 新参数+新模型
# 处理完后把最新的参数更新到模型中
pretrained_model.load_state_dict(my_resnet18_dict)
model = pretrained_model.to(device)

4.2 冻结层

对于不在需要训练的网络层,可以把梯度更新关闭:根据具体需求来,会影响训练效果。

# 冻结层:自己打印和调试,是完全可以的
for name, value in pretrained_model.named_parameters():
    if name != "fc.weight" and name != "fc.bias":
        value.requires_grad = False

# 开始筛选需要进行梯度更新的参数,而不是全部
params_grade_true = filter(lambda x: x.requires_grad, pretrained_model.parameters() )

# 创建优化器
# optimizer = optim.Adam(model.parameters(), lr=learning_rate)
optimizer = optim.Adam(params_grade_true, lr=learning_rate)

五、模型移植

在这里插入图片描述

Open Neural Network Exchange(ONNX,开放神经网络交换)格式,是一个用于表示深度学习模型的标准,可使模型在不同框架之间进行转移。ONNX的规范及代码主要由微软,亚马逊 ,Face book 和 IBM等公司共同开发,以开放源代码的方式托管在Github上。目前官方支持加载ONNX模型并进行推理的深度学习框架有: Caffe2, PyTorch, PaddlePaddle, TensorFlow等。
ONNX官网

5.1 导出ONNX

安装依赖包

pip install onnx
pip install onnxruntime

导出ONNX模型

import os
import torch
import torch.nn as nn
from torchvision.models import resnet18

if __name__ == "__main__":
    dir = os.path.dirname(__file__)
    weightpath = os.path.join(
        os.path.dirname(__file__), "pth", "resnet18_default_weight.pth"
    )
    onnxpath = os.path.join(
        os.path.dirname(__file__), "pth", "resnet18_default_weight.onnx"
    )

    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = resnet18(pretrained=False)
    model.conv1 = nn.Conv2d(
        #
        in_channels=3,
        out_channels=64,
        kernel_size=3,
        stride=1,
        padding=0,
        bias=False,
    )
    # 删除池化层
    model.maxpool = nn.MaxPool2d(kernel_size=1, stride=1, padding=0)
	# 修改全连接层
    in_feature = model.fc.in_features
    model.fc = nn.Linear(in_feature, 10)
    model.load_state_dict(torch.load(weightpath, map_location=device))
    model.to(device)
    # 创建一个实例输入
    x = torch.randn(1, 3, 224, 224, device=device)
    # 导出onnx
    torch.onnx.export(
        model,
        x,
        onnxpath,
        #
        verbose=True, # 输出转换过程
        input_names=["input"],
        output_names=["output"],
    )
    print("onnx导出成功")

5.2 ONNX可视化

在线查看桌面版
ONNX在做推理时不再需要导入网络,且适用于Python、JAVA、PyQT等各种语言,不再依赖于PyTorch框架;

5.3 ONNX推理

ONNX在做推理时不再需要导入网络,且适用于Python、JAVA、PyQT等各种语言,不再依赖于PyTorch框架;

import onnxruntime
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import requests
from io import BytesIO
import torchvision.transforms as transforms

# 1. 下载测试图像
def download_image(url):
    response = requests.get(url)
    img = Image.open(BytesIO(response.content)).convert('RGB')
    return img

# 2. 图像预处理
def preprocess_image(img, input_size=(224, 224)):
    # 创建预处理管道
    preprocess = transforms.Compose([
        transforms.Resize(input_size),
        transforms.ToTensor(),
        transforms.Normalize(
            mean=[0.485, 0.456, 0.406],  # ImageNet标准化参数
            std=[0.229, 0.224, 0.225]
        )
    ])
    
    # 应用预处理并添加批次维度
    input_tensor = preprocess(img)
    input_batch = input_tensor.unsqueeze(0).numpy()  # 转换为NumPy数组
    return input_batch

# 3. 加载ONNX模型并进行推理
def onnx_inference(model_path, input_data):
    # 创建推理会话
    session = onnxruntime.InferenceSession(model_path)
    
    # 获取输入名称
    input_name = session.get_inputs()[0].name
    
    # 执行推理
    outputs = session.run(None, {input_name: input_data})
    return outputs

# 4. 后处理:解析分类结果
def postprocess(output, topk=5):
    # 获取ImageNet类别标签
    classes_url = "https://raw.githubusercontent.com/anishathalye/imagenet-simple-labels/master/imagenet-simple-labels.json"
    classes = requests.get(classes_url).json()
    
    # 获取top-k预测结果
    probs = output[0].squeeze()  # 移除批次维度
    topk_indices = np.argsort(probs)[-topk:][::-1]
    
    # 返回结果列表
    results = []
    for idx in topk_indices:
        results.append({
            "class_id": idx,
            "class_name": classes[idx],
            "probability": float(probs[idx])
        })
    return results

# 5. 可视化结果
def visualize_results(img, results):
    plt.figure(figsize=(10, 6))
    plt.imshow(img)
    plt.axis('off')
    
    # 显示预测结果
    text = "\n".join([f"{res['class_name']}: {res['probability']:.2%}" 
                      for res in results])
    plt.figtext(0.5, 0.01, text, wrap=True, 
                horizontalalignment='center', fontsize=12)
    plt.show()

# 主函数
def main():
    # 配置参数
    MODEL_PATH = "resnet50.onnx"  # 替换为你的ONNX模型路径
    IMAGE_URL = "https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered/validation/dogs/dog.2000.jpg"
    
    try:
        # 步骤1: 获取图像
        print("下载图像...")
        img = download_image(IMAGE_URL)
        
        # 步骤2: 预处理
        print("预处理图像...")
        input_data = preprocess_image(img)
        
        # 步骤3: ONNX推理
        print("执行ONNX推理...")
        outputs = onnx_inference(MODEL_PATH, input_data)
        
        # 步骤4: 解析结果
        print("解析预测结果...")
        results = postprocess(outputs)
        
        # 步骤5: 可视化
        print("可视化结果...")
        visualize_results(img, results)
        
        # 打印详细结果
        print("\nTop-5 预测结果:")
        for res in results:
            print(f"{res['class_name']}: {res['probability']:.2%}")
    
    except Exception as e:
        print(f"发生错误: {str(e)}")

if __name__ == "__main__":
    main()

总结

本文简要介绍了数据集的获取,训练过程与训练结果的可视化,以及预训练与迁移学习,还有模型移植等方面的内容。


网站公告

今日签到

点亮在社区的每一天
去签到