使用Torch结合模型进行训练的整体流程
本文详细介绍了深度学习项目全流程的实现方法。主要内容包括:1) 数据准备阶段,通过标准化目录结构和分层抽样划分数据集;2) 模型配置,采用动态类或EasyDict管理超参数;3) 模型训练核心,包括自定义Dataset类实现数据加载、封装Trainer类管理训练过程,以及主训练脚本实现训练流程控制;4) 模型评估,通过准确率、混淆矩阵和分类报告等多维度评估模型性能。文章强调工程化实践,如GPU优先
一、数据准备
数据整理:将杂乱的数据集按列别整理到标准的目录结构,确保格式统一,便于后续处理。
数据整理主要分为三步:
- 分析数据类别:扫描原始数据,确定所有类别及其分布
- 创建目录结构:建立train、valid、test目录,每个目录下包含所有类别的子目录
- 生成元数据并划分:创建包含文件路径和类别信息的元数据,使用分层抽样按70-15-15比例划分为训练集、验证集和测试集
- 组织物理文件:根据划分结果将文件复制到对应的目录中
import pandas as pd
import os
import shutil
from sklearn.model_selection import train_test_split
def prepare_data_only(raw_data_dir, output_dir):
# 1. 收集元数据
metadata = []
for filename in os.listdir(raw_data_dir):
if filename.endswith(('.jpg', '.png', '.jpeg')):
category = filename.split('_')[0]
metadata.append({
'filename': filename,
'category': category,
'file_path': os.path.join(raw_data_dir, filename)
})
df = pd.DataFrame(metadata)
categories = df['category'].unique()
print(f"发现 {len(categories)} 个类别: {list(categories)}")
# 2. 创建目录结构
for split in ['train', 'valid', 'test']:
for category in categories:
os.makedirs(os.path.join(output_dir, split, category), exist_ok=True)
# 3. 划分数据集
train_df, temp_df = train_test_split(
df, test_size=0.3, stratify=df['category'], random_state=42
)
val_df, test_df = train_test_split(
temp_df, test_size=0.5, stratify=temp_df['category'], random_state=42
)
# 4. 复制文件到对应目录
for split_name, split_df in [('train', train_df), ('valid', val_df), ('test', test_df)]:
for _, row in split_df.iterrows():
src = row['file_path']
dst = os.path.join(output_dir, split_name, row['category'], row['filename'])
shutil.copy2(src, dst)
# 保存划分信息
split_df.to_csv(os.path.join(output_dir, f'{split_name}_metadata.csv'), index=False)
print("数据准备完成!")
return train_df, val_df, test_df
# 使用:这个函数只需要运行一次!
# train_meta, val_meta, test_meta = prepare_data_only('./raw_data', './processed_data')
二、模型配置
参数配置:设置超参数、配置模型路径、数据路径定义数据增强策略。
超参数是模型训练前需要手动设定的参数,它们控制着学习过程和模型结构。合理的设置超参数能够显著影响模型的性能;良好的路径管理能够确保代码的可移植性、避免硬编码并为团队协作提供方便。
整个参数配置的过程中需要将配置分解为独立的组件,以便于维护和修改,并且需要保证实验的可重复性和类的可扩展性,为后续的模型训练、评估和部署奠定基础。
方法一:创建对应的配置类,通过动态类来确保可扩展性
class CifarConfig:
pass
cfg = CifarConfig()
cfg.pb = True # 是否采用渐进式采样
cfg.mixup = False # 是否采用mixup
cfg.mixup_alpha = 1. # beta分布的参数. beta分布是一组定义在(0,1) 区间的连续概率分布。
cfg.label_smooth = False # 是否采用标签平滑
cfg.label_smooth_eps = 0.01 # 标签平滑超参数
cfg.train_bs = 128
cfg.valid_bs = 128
cfg.workers = 4
cfg.lr_init = 0.1
cfg.momentum = 0.9
cfg.weight_decay = 1e-4
cfg.factor = 0.1
cfg.milestones = [160, 180]
cfg.max_epoch = 200
cfg.log_interval = 20
# 数据预处理设置
cfg.norm_mean = [0.4914, 0.4822, 0.4465]
cfg.norm_std = [0.2023, 0.1994, 0.2010]
normTransform = transforms.Normalize(cfg.norm_mean, cfg.norm_std)
cfg.transforms_train = transforms.Compose([
transforms.Resize(32),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ToTensor(),
normTransform
])
cfg.transforms_valid = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
normTransform
])
方法二:简单的参数配置可以使用采用EasyDict()来实现,这样就不用通过写配置类中的str魔术方法来配合日志的获取了。
cfg = EasyDict() # 访问属性的方式去使用key-value 即通过 .key获得value
cfg.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
cfg.max_epoch = 150 # 150
cfg.crop_size = (360, 480)
# 梯度裁剪
cfg.hist_grad = True
cfg.is_clip = False
cfg.clip_value = 0.2
# batch size
cfg.train_bs = 8
cfg.valid_bs = 4
cfg.workers = 4
# 学习率
cfg.lr_init = 0.1 # pretraied_model::0.1
cfg.factor = 0.1
cfg.milestones = [75, 130]
cfg.weight_decay = 1e-4
cfg.momentum = 0.9
cfg.log_interval = 10
三、模型训练
1、数据加载
实现自定义的dataset类,加载数据集,应用数据增强和预处理。
PyTorch 的数据加载核心是DataLoader,它可以自动实现并行数据加载、批处理、打乱顺序等。但 DataLoader需要一个标准接口来知道如何获取每一个数据及其标签。这个标准接口就是 Dataset 类。通过实现__len__() 和 __getitem__()方法,我们告诉 DataLoader 数据的总量以及如何按索引获取第 i 个样本。
import torch
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import pandas as pd
import os
class CustomDataset(Dataset):
"""
自定义数据集类
"""
def __init__(self, annotations_file, img_dir, transform=None):
"""
初始化函数,通常在这里读取数据索引或元信息,并设置数据变换。
一次性将所有文件的路径和标签读入内存,而不是读入所有数据,节省内存。
`transform` 参数使得数据增强和预处理可以灵活配置。
"""
self.img_labels = pd.read_csv(annotations_file)
self.img_dir = img_dir
self.transform = transform
def __len__(self):
"""
返回数据集的长度。
为什么必须实现?DataLoader需要知道在一个epoch中要迭代多少次。
"""
return len(self.img_labels)
def __getitem__(self, idx):
"""
根据索引idx返回一个样本(数据和标签)。
为什么必须实现?DataLoader在每次迭代时就是调用这个方法来获取一个batch的数据。
"""
# 1. 根据索引从元信息中定位数据(如图片路径和标签)
img_path = os.path.join(self.img_dir, self.img_labels.iloc[idx, 0])
image = Image.open(img_path) # 懒加载:用到时才读入图片
label = self.img_labels.iloc[idx, 1]
# 2. 应用预处理和数据增强
if self.transform:
image = self.transform(image) # 例如 ToTensor(), Normalize()
# 3. 返回张量格式的数据和标签
return image, label
# 使用示例
from torchvision import transforms
# 定义训练和验证时的数据变换(规则:训练需要增强,验证只需基础预处理)
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(), # 数据增强,增加模型泛化能力
transforms.Resize((224, 224)),
transforms.ToTensor(), # 将PIL图像或NumPy数组转换为PyTorch张量,并归一化到[0,1]
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 标准化
])
val_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
# 创建Dataset实例
train_dataset = CustomDataset("train_labels.csv", "train_images/", transform=train_transform)
val_dataset = CustomDataset("val_labels.csv", "val_images/", transform=val_transform)
# 创建DataLoader
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)
2、训练核心:Train()类
训练过程包含很多固定的组件:模型、损失函数、优化器、学习率调度器等。同时,训练和验证的逻辑非常相似(前向传播、计算损失),但又有关键区别(是否计算梯度、是否更新参数)。将其封装成类,可以将这些组件和状态(如当前epoch、最佳准确率)很好地管理起来,使代码更清晰,并且易于扩展功能(如早停、模型保存、日志记录)。
import torch
import torch.nn as nn
from tqdm import tqdm # 用于显示进度条
class Trainer:
def __init__(self, model, criterion, optimizer, device, scheduler=None):
"""
初始化训练器。
将组件作为参数传入: 提高灵活性,可以轻松更换不同的模型、损失函数等。
"""
self.model = model
self.criterion = criterion
self.optimizer = optimizer
self.device = device
self.scheduler = scheduler # 学习率调度器,非必须
# 记录训练过程中的指标,用于后续分析
self.train_losses = []
self.val_losses = []
self.train_accuracies = []
self.val_accuracies = []
# 将模型移动到指定设备(GPU/CPU)
self.model.to(self.device)
def train_epoch(self, dataloader):
"""在一个epoch上训练模型"""
self.model.train() # 设置模型为训练模式(影响Dropout和BatchNorm等层)
running_loss = 0.0
correct = 0
total = 0
# 使用tqdm包装数据加载器,显示进度条
pbar = tqdm(dataloader, desc="Training")
for batch_idx, (data, targets) in enumerate(pbar):
# 1. 将数据移动到设备
data, targets = data.to(self.device), targets.to(self.device)
# 2. 前向传播
outputs = self.model(data)
loss = self.criterion(outputs, targets)
# 3. 反向传播与优化
self.optimizer.zero_grad() # 清空上一步的梯度,防止累积
loss.backward() # 反向传播,计算梯度
self.optimizer.step() # 更新模型参数
# 4. 统计信息
running_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
# 更新进度条信息
pbar.set_postfix({
'Loss': f"{running_loss/(batch_idx+1):.4f}",
'Acc': f"{100.*correct/total:.2f}%"
})
epoch_loss = running_loss / len(dataloader)
epoch_acc = 100. * correct / total
self.train_losses.append(epoch_loss)
self.train_accuracies.append(epoch_acc)
return epoch_loss, epoch_acc
def validate_epoch(self, dataloader):
"""在一个epoch上验证模型"""
self.model.eval() # 设置模型为评估模式(关闭Dropout等)
running_loss = 0.0
correct = 0
total = 0
# 在验证阶段不计算梯度,节省内存和计算资源
with torch.no_grad():
pbar = tqdm(dataloader, desc="Validation")
for batch_idx, (data, targets) in enumerate(pbar):
data, targets = data.to(self.device), targets.to(self.device)
outputs = self.model(data)
loss = self.criterion(outputs, targets)
running_loss += loss.item()
_, predicted = outputs.max(1)
total += targets.size(0)
correct += predicted.eq(targets).sum().item()
pbar.set_postfix({
'Loss': f"{running_loss/(batch_idx+1):.4f}",
'Acc': f"{100.*correct/total:.2f}%"
})
epoch_loss = running_loss / len(dataloader)
epoch_acc = 100. * correct / total
self.val_losses.append(epoch_loss)
self.val_accuracies.append(epoch_acc)
return epoch_loss, epoch_acc
核心要点:
- model.train()和model.eval()是必须遵循的规则,他控制了Dropout和BatchNorm等层的行为,训练的时候需要这些层工作,验证和测试的时候就需要这些层关闭。
- troch.no_grad():在验证和测试的时候使用,可以大幅减少内存消耗并增加计算,因为其禁用了梯度计算。
- optimizer.zero_grad():PyTorch默认会累积梯度,所以在每次反向传播前需要手动清空。
3、模型训练
模型训练脚本是主训练脚本,它是项目的“总控制器”或“配置中心”。在这里,我们实例化所有组件,并定义整个训练流程(跑多少个epoch,何时验证,何时保存模型)。这种结构使得超参数和实验设置一目了然。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
# 假设我们从一个模型文件中导入我们的模型
from models.my_model import MyCNN
from data.dataset import CustomDataset
from core.trainer import Trainer
def main():
# 配置超参数和设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
num_epochs = 50
learning_rate = 0.001
# 1. 初始化组件
print("Initializing components...")
model = MyCNN(num_classes=10)
criterion = nn.CrossEntropyLoss() # 分类任务常用损失
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 使用学习率调度器,在验证损失停滞时降低学习率
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5)
# 2. 创建数据加载器
train_loader = ...
val_loader = ...
# 3. 初始化训练器
trainer = Trainer(model, criterion, optimizer, device, scheduler)
# 4. 训练循环
print("Starting training...")
best_val_acc = 0.0
for epoch in range(num_epochs):
print(f"\nEpoch [{epoch+1}/{num_epochs}]")
# 训练一个epoch
train_loss, train_acc = trainer.train_epoch(train_loader)
# 验证一个epoch
val_loss, val_acc = trainer.validate_epoch(val_loader)
# 根据验证损失调整学习率
trainer.scheduler.step(val_loss)
# 保存最佳模型(规则:在验证集上性能最好的模型)
if val_acc > best_val_acc:
best_val_acc = val_acc
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'best_acc': best_val_acc,
}, 'best_model.pth')
print(f"==> Saved new best model with Val Acc: {best_val_acc:.2f}%")
print("Training Finished!")
if __name__ == '__main__':
main()
关键要点:
- 设备检测:代码优先使用GPU,这个是深度学习训练的默认规则。
- 保存最佳模型:我们根据验证集准确率来保存模型,而不是训练集准确率。这是为了防止过拟合,并选择泛化能力最强的模型,这是机器学习中一个核心的工程实践。
- 学习率调度:ReduceLROnPlateu 是一个常用的策略,当模型性能不再提升时自动降低学习率,有助于模型收敛到更优点。
四、模型评估
全面的性能评估:在测试集上评估训练好的模型,得出准确率(整体性能的直观指标)、混淆矩阵(解释类别间的混淆关系)、分类报告(详细的precision \ recall \ f1-score)指标。
评估指标选择原则:多维度评估模型性能、识别模型的优势和弱点、为错误分析提供依据。
def evaluate_flower(model, test_generator, config):
"""
全面评估模型性能
工程化评估方法:
- 使用保留的测试集保证评估客观性
- 多指标综合评估
- 可视化评估结果
"""
# 1. 基础准确率评估
test_loss, test_accuracy = model.evaluate(test_generator)
print(f"测试集准确率: {test_accuracy:.4f}")
# 2. 预测结果
y_pred = model.predict(test_generator)
y_pred_classes = np.argmax(y_pred, axis=1)
y_true = test_generator.classes
# 3. 混淆矩阵 - 分析类别间混淆情况
cm = confusion_matrix(y_true, y_pred_classes)
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.title('混淆矩阵')
plt.ylabel('真实标签')
plt.xlabel('预测标签')
plt.savefig('confusion_matrix.png')
# 4. 分类报告 - 详细的性能指标
class_report = classification_report(
y_true,
y_pred_classes,
target_names=test_generator.class_indices.keys()
)
print("分类报告:")
print(class_report)
# 5. 额外指标
precision = precision_score(y_true, y_pred_classes, average='weighted')
recall = recall_score(y_true, y_pred_classes, average='weighted')
f1 = f1_score(y_true, y_pred_classes, average='weighted')
print(f"加权精确率: {precision:.4f}")
print(f"加权召回率: {recall:.4f}")
print(f"加权F1分数: {f1:.4f}")
return {
'accuracy': test_accuracy,
'confusion_matrix': cm,
'classification_report': class_report,
'precision': precision,
'recall': recall,
'f1_score': f1
}
五、最终所有的流程

更多推荐



所有评论(0)