CV项目课程笔记01_小小白的第一个最简单的训练脚本

该文介绍了如何使用Python和PyTorch进行花朵分类的项目,包括数据集的划分、FlowersDataset的处理、模型训练及验证。具体涉及数据预处理、split_flower_dataset.py脚本、flower_dataset.py中Dataset类的实现、train.py的训练代码以及模型_trainer.py中的训练和验证方法。整个流程涵盖了从数据准备到模型训练的关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CV项目课程笔记_malaedu

花朵分类_01

01. 模型训练的代码框架
在这里插入图片描述

“维基百科中对Ground Truth在机器学习领域的解释是:
在机器学习中,“ground truth”一词指的是训练集对监督学习技术的分类的准确性。这在统计模型中被用来证明或否定研究假设。“ground truth”这个术语指的是为这个测试收集适当的目标(可证明的)数据的过程。

02. 102 Flowers Dataset
数据集划分步骤如下:
Step 1 根据图像路径得到所有的图像列表
Step 2 根据比例获得训练集和验证集的列表 8:2
Step 3 根据列表把图像复制到新的文件夹下

split_flower_dataset.py
中的copy_file()方法:(import的包自己导一下哈)

#split_flower_dataset.py
def copy_file(img_list, target_dir, setname="train"):
	img_dir = os.path.join(target_dir, setname)
	os.makedirs(img_dir, exist_ok=True)
	for p in img_list:
		shutil.copy(p, img_dir)
	print(f"{setname} dataset: copy {len(img_list)} images to {img_dir}")

Then,

#split_flower_dataset.py
if __name__ == "__main__":
	##step 1  根据图像路径得到所有的图像列表
	img_dir = r"图像路径"
	img_list = [os. path.join(img_dir, name) for name in os.listdir(img_dir)]
	##随机打乱,设置随机数种子
	random.seed(10086)
	random.shuffle(img_list)
	##step 2  根据比例获得训练集和验证集的列表 8:2
	train_ratio = 0.8
	valid_ratio = 0.2
	num_img = len(img_list)
	num_train = int(num_img * train_ratio)
	num_valid = num_img - num_train
	##前80%和后20%
	train_list = img_list[: num_train]
	valid_list = img_list[num_train: ]
	##step 3 根据列表把图像复制到新的文件夹下
	##dirname():获得上一级目录; abspath():获得绝对路径
	target_dir = os.path.abspath(os.path.dirname(img_dir))
	copy_file(train_list, target_dir, "train")
	copy_file(valid_list, target_dir, "valid")

03. Coding
四个.py文件:

文件名用途
split_flower_dataset.py划分数据集
flower_dataset.py读取数据集
train.py训练代码
model_trainer.py模型训练参数

几个关键类:

类名/包名用途
torch.utils.data.Dataset数据集的表示类,可通过索引实现读取硬盘当中的数据,返回一个样本
torch.utils.data.Dataloader数据加载器。负责调用dataset,对数据进行采样,组装成batch形式
torch.nn.Module神经网络模块基类,__init__()实现层定义,在forward函数中调用网络层来实现神经网络前向传播
torch.optim一些常用的优化器,负责更新神经网络参数
torch.optim.lr_scheduler一些常用的learning rate调整策略,负责更新优化器中的learning rate

flower_dataset.py
tips:
pass是占位符,可以先构建好框架,再写里面的详细内容。
Any和Dict是在from typing import Any, Dict
Dataset是在from torch.utils.data import Dataset
Image是在from PIL import Image

#flower_dataset.py
#继承Dataset类
class FlowerDataset(Dataset):
	def __init__(self, img_dir, transform=None) -> None:
		super().__init__()
		self.img_dir = img_dir
		self.img_infos = [] #包含path,label,...
		self._get_img_info()
		self.transform = transform

	def __getitem__(self, index) -> Any:
		img_info: Dict = self.img_infos[index] 
		#两个key:path, label
		img_path, label_id = img_info["path"], img_info["label"]

		# PIL 优势:适配torchvision.transform 劣势:边缘端非py部署不支持PIL读取
		img = Image.open(img_path).convert("RGB")
		
		# 另一种读取方式方法opencv cv2
		# img = cv2.read(img_path) #BGR
		# img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

		if self.transform is not None:
			img = self.transform(img)
		return img, label_id

	def __len__(self):
		return len(self.img_infos)
		
	def _get_img_info(self):#单下划线是private的
		'''根据图片文件夹路径,获得所有图片信息'''
		#获得.mat文件路径
		label_file = os.path.join(os.path.dirname(self.img_dir), "imagelabels.mat")
		assert os.path.exists(label_file)
		#读取.mat文件
		from scipy.io import loadmat
		# shape是[1, 8189] 0-1:label_id 1-2:label_id, ...
		label_array = loadmat(label_file)["labels"]
		#loadmat(label_file)是字典类型,关键字是labels
		#label_array是numpy.array 1行, 8189列
		#min_id:1 max_id:102 1-102 因为pytorch:0-101,所以需要统一调整
		label_array -= 1 # from 0
		
		#根据图像名得到对应的label_id
		for img_name in os.listdir(self.img_dir):
			path  = os.path.join(self.img_dir, img_name)
			if not img_name[6:11].isdigit():#是否是数字
				continue
			img_id = int(img_name[6:11])
			#获得列的下标
			col_id = img_id - 1
			cls_id = int(label_array[:, col_id]) # from 0
			self.img_infos.append({"path": path, "label": cls_id})


if __name__ == "__main__":
	img_dir = r"路径"
	#实例化Dataset
	dataset = FlowerDataset(img_dir)
	#元组
	img, label_id= dataset[1000]#实际上调用的是__getitem__方法
	data_size = len(dataset)#实际上调用的是__len__方法

train.py 会调用flower_dataset.py的FlowerDataset,不需要单独运行flower_dataset.py

#train.py
import torch
import os
import time
from torchvision import transforms, models
from flower_dataset import FlowerDataset
from model_trainer import ModelTrainer
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import transforms, models


if __name__ == "__main__":
	# 1. 参数配置
	train_dir = r"自己改好train的路径"
	valid_dir = r"自己改好valid的路径"
	batch_size = 64
	max_epoch = 40
	num_cls = 102 #102种flowers 
	lr0 = 0.01 #初始学习率
	momentum = 0.9 #动量因子 奖赏机制 if是对的 则沿着这个继续
	weight_decay = 1e-4 #抑制模型过拟合 要小
	milestones = [25, 35] #当epch是40时,在25和35的地方
	decay_factor = 0.1 #下降因子。每次下降的时候乘上这个数

	norm_mean, norm_std = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
	log_interval = 10 #每隔多少间隔打印一个log 单位是iter:每一次迭代(每更新一次)
	#用时间命名
	time_str = time.strftime("%Y%m%d")
	output_dir = f"outputs/{time_str}"
	os.makedirs(output_dir, exist_ok=True)

	#2. 数据相关
	# 实例化dataset(train和valid)
	# train compose组装
	train_transform = transforms.Compose([
		transforms.Resize(256),
		
		##(256)和((256,256))的区别
		##(256)是等比例缩放,图像较短的边缩成256,较长的边乘以缩放比例进行变化
		##而((256,256))是强制转成这个size
		
		transforms.RandomCrop(224),
		# RandomCrop()随机裁剪。模型最终的输入大小[224,224]
		transforms.RandomHorizontalFlip(p=0.5),
		# RandomHorizontalFlip(p=0.5) 随机水平(左右)翻转。
		# RandomVerticalFlip(p=0.5)是随机垂直(上下)翻转
		transforms.ToTensor(),
		## 1. 将图像0-225的8位无浮点数 转成-> 0-1的folat型 
		##	2. 图像读入是HWC高宽通道 转成-> CHW 
		##	3. 转成->BCHW B是batch
		transforms.Normalize(norm_mean, norm_std) #归一化。减去均值,除以方差。为了让模型收敛的更快
	])
	train_dataset = FlowerDataset(img_dir=train_dir, transform=train_transform)

	# valid 组装
	valid_transform = transforms.Compose([
		transforms.Resize((224,224)),
		transforms.ToTensor(),
		transforms.Normalize(norm_mean, norm_std)#归一化。减去均值,除以方差。
	])
	valid_dataset = FlowerDataset(img_dir=valid_dir, transform=valid_transform)
	# 组装dataloader。shuffle打乱。num_workers多少个子进程(0和1的区别)
	train_loader = DataLoader(train_dataset, batch_size, shuffle=True, num_workers=2)
	valid_loader = DataLoader(valid_dataset, batch_size, shuffle=False, num_workers=2)


	#3. 实例化网络模型
	model = models.resnet18(pretrained=True) # imagenet上预训练的 全连接层fc 1000个类别,本项目是102种flowers所以要重定义fc
	#原始维度
	in_features = model.fc.in_features
	#线性层全连接层 改写out_features
	fc = nn.Linear(in_features=in_features, out_features=num_cls)
	model.fc = fc

	#把模型转到GPU上
	device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
	model.to(device=device)

	#4. 优化器相关
	#loss函数 交叉熵损失
	loss_fn = nn.CrossEntropyLoss()
	#优化器实例化 SGD:随机梯度下降
	optimizer = optim.SGD(model.parameters(), lr=lr0, momentum=momentum, weight_decay=weight_decay)

	#学习率的下降策略实例化
	lr_scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=decay_factor)

	#5. for循环
for epoch in range(max_epoch):
	#一次epoch的训练
	#按batch形式取数据
	#前向传播
	#计算loss
	#反向传播计算梯度
	#更新权重
	#统计loss 准确率
	loss_train, acc_train, conf_mat_train, path_error_train = ModelTrainer.train_one_epoch(
		train_loader, model,
		loss_f=loss_fn,
		optimizer=optimizer,
		scheduler=lr_scheduler,
		epoch_idx=epoch,
		device=device,
		log_interval=log_interval,#每隔多少间隔打印一个log
		max_epoch=max_epoch,
	)

	#一次epoch验证
	#按batch形式取数据
	#前向传播
	#计算loss
	#统计loss 准确率
	loss_valid, acc_valid, conf_mat_valid, path_error_valid = ModelTrainer.valid_one_epoch(
		valid_loader, 
		model,
		loss_fn,
		device=device,
	)
	#保存模型
	checkpoint = {
		"model": model.state_dict(),
		"epoch": epoch,
		#"model": model.state_dict(),保存模型权重和名称
		#另一种可以存成"model": model
	}
	
	torch.save(checkpoint, f"{output_dir}/model.pth")
	# output_dir在前面自定义

class MultiStepLR(_LRScheduler)的描述
在这里插入图片描述

model_trainer.py
包含两个静态方法(@staticmethod):train_one_epoch(paras…) 和valid_one_epoch(paras…)

import torch
import numpy as np
from collections import Counter

class ModelTrainer:

    @staticmethod
    def train_one_epoch(data_loader, model, loss_f, optimizer, scheduler, epoch_idx, device, log_interval, max_epoch):
        model.train()  ## model变为可训练的状态

        num_cls = model.fc.out_features
        conf_mat = np.zeros((num_cls, num_cls))
        loss_sigma = []
        loss_mean = 0
        acc_avg = 0
        path_error = []
        
        #不断循环data_loader取数据
        for i, data in enumerate(data_loader):
            # inputs, labels, path_imgs = data
            inputs, labels = data   # batch的类型 4维BCHW
            inputs, labels = inputs.to(device), labels.to(device) #移动到GPU上

            # forward & backward
            outputs = model(inputs)
            loss = loss_f(outputs.cpu(), labels.cpu())
            optimizer.zero_grad()  ###***优化器梯度清零***
            loss.backward() #反向传播
            optimizer.step() #参数更新

            # 统计loss
            loss_sigma.append(loss.item())
            loss_mean = np.mean(loss_sigma)#loss均值

            # 统计混淆矩阵
            _, predicted = torch.max(outputs.data, 1)
            for j in range(len(labels)):
                cate_i = labels[j].cpu().numpy()
                pred_i = predicted[j].cpu().numpy()
                conf_mat[cate_i, pred_i] += 1.
            acc_avg = conf_mat.trace() / conf_mat.sum()

            # 每10个iteration 打印一次训练信息
            if i % log_interval == log_interval - 1:
                print("Training: Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".
                            format(epoch_idx + 1, max_epoch, i + 1, len(data_loader), loss_mean, acc_avg))
        # print("epoch:{} sampler: {}".format(epoch_idx, Counter(label_list)))
        return loss_mean, acc_avg, conf_mat, path_error

    @staticmethod
    def valid_one_epoch(data_loader, model, loss_f, device):
        model.eval()

        num_cls = model.fc.out_features
        conf_mat = np.zeros((num_cls, num_cls))
        loss_sigma = []
        path_error = []

        for i, data in enumerate(data_loader):
            # inputs, labels, path_imgs = data
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)

            #上下文管理器,使得不再计算梯度
            with torch.no_grad():
                outputs = model(inputs)
            # 计算loss
            loss = loss_f(outputs.cpu(), labels.cpu())

            # 统计混淆矩阵
            _, predicted = torch.max(outputs.data, 1)
            for j in range(len(labels)):
                cate_i = labels[j].cpu().numpy()
                pred_i = predicted[j].cpu().numpy()
                conf_mat[cate_i, pred_i] += 1.

            # 统计loss
            loss_sigma.append(loss.item())
        #avg
        acc_avg = conf_mat.trace() / conf_mat.sum()

        return np.mean(loss_sigma), acc_avg, conf_mat, path_error



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值