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