1. 准备工作:从零搭建SSD训练环境

第一次接触SSD目标检测算法时,我也曾被各种环境配置问题搞得焦头烂额。经过多次实践,我总结出一套最稳定的环境搭建方案。PyTorch版本选择很关键,建议使用1.7.1这个经典版本,既不会太老缺失新功能,又避开了新版的一些兼容性问题。

安装核心依赖其实很简单:

pip install torch==1.7.1 torchvision==0.8.2
pip install opencv-python numpy matplotlib

这里有个容易踩的坑:CUDA驱动版本必须与PyTorch要求严格匹配。我曾在笔记本上折腾半天才发现是显卡驱动太旧。建议先用nvidia-smi查看CUDA版本,再对照PyTorch官网的版本矩阵选择安装包。如果使用CPU训练(虽然速度会慢很多),直接安装CPU版本的PyTorch即可。

验证环境是否正常:

import torch
print(torch.__version__)  # 应该输出1.7.1
print(torch.cuda.is_available())  # 如果是GPU环境应该返回True

建议单独为这个项目创建conda环境,避免与其他项目的依赖冲突。我常用的conda命令如下:

conda create -n ssd_train python=3.8
conda activate ssd_train

2. 数据准备:打造规范化的VOC格式数据集

2.1 数据集目录结构搭建

VOC2007格式看似复杂,其实结构非常清晰。在我的项目里,目录树长这样:

Dataset/
├── Annotations/       # 存放XML标注文件
├── JPEGImages/        # 存放原始图片
└── ImageSets/
    └── Main/          # 存放划分好的数据集列表

用Python自动创建这些目录:

import os

def create_voc_dirs(root_path):
    dirs = ['Annotations', 'JPEGImages', 'ImageSets/Main']
    for d in dirs:
        os.makedirs(os.path.join(root_path, d), exist_ok=True)
        
create_voc_dirs('Dataset')  # 一键创建所有目录

2.2 图片批量重命名技巧

原始图片命名混乱是常见问题。这个脚本可以批量规范命名(如000001.jpg, 000002.jpg):

import os
from tqdm import tqdm  # 进度条工具

def rename_images(folder):
    files = [f for f in os.listdir(folder) if f.lower().endswith(('.jpg', '.png'))]
    for i, filename in enumerate(tqdm(files)):
        ext = os.path.splitext(filename)[1]
        new_name = f"{i:06d}{ext}"  # 6位数字补零
        os.rename(
            os.path.join(folder, filename),
            os.path.join(folder, new_name)
        )

rename_images('Dataset/JPEGImages')  # 执行重命名

2.3 使用LabelImg高效标注

推荐使用LabelImg进行标注,几个提高效率的技巧:

  1. 修改默认保存路径为Annotations目录(按Ctrl+R设置)
  2. 如果只有单一类别,在"View"菜单设置默认标签
  3. 常用快捷键:
    • W:创建标注框
    • A/D:上一张/下一张
    • Ctrl+S:快速保存

标注完成后,每个XML文件会包含这样的目标信息:

<object>
    <name>cat</name>
    <bndbox>
        <xmin>100</xmin>
        <ymin>200</ymin>
        <xmax>300</xmax>
        <ymax>400</ymax>
    </bndbox>
</object>

2.4 数据集智能划分

用这个脚本自动划分训练集/验证集/测试集(默认比例7:2:1):

import os
import random
from sklearn.model_selection import train_test_split

def split_dataset(anno_dir, output_dir):
    xml_files = [f[:-4] for f in os.listdir(anno_dir) if f.endswith('.xml')]
    
    # 先分测试集,再分训练验证集
    train_val, test = train_test_split(xml_files, test_size=0.1, random_state=42)
    train, val = train_test_split(train_val, test_size=0.2, random_state=42)
    
    def write_to_file(filepath, data):
        with open(filepath, 'w') as f:
            f.write('\n'.join(data))
    
    write_to_file(f'{output_dir}/train.txt', train)
    write_to_file(f'{output_dir}/val.txt', val)
    write_to_file(f'{output_dir}/test.txt', test)
    write_to_file(f'{output_dir}/trainval.txt', train_val)

split_dataset('Dataset/Annotations', 'Dataset/ImageSets/Main')

3. 模型配置:修改代码适配自定义数据集

3.1 关键配置文件修改

在config.py中添加你的数据集配置(以火焰检测为例):

fire = {
    'num_classes': 2,  # 类别数+1(背景)
    'lr_steps': (80000, 100000),  # 学习率调整步数
    'max_iter': 120000,  # 总迭代次数
    'feature_maps': [38, 19, 10, 5, 3, 1],
    'min_dim': 300,
    'steps': [8, 16, 32, 64, 100, 300],
    'min_sizes': [30, 60, 111, 162, 213, 264],
    'max_sizes': [60, 111, 162, 213, 264, 315],
    'aspect_ratios': [[2], [2, 3], [2, 3], [2, 3], [2], [2]],
    'variance': [0.1, 0.2],
    'clip': True,
    'name': 'FIRE',
}

3.2 创建数据集加载文件

在data目录新建fire.py,基于voc0712.py修改关键部分:

FIRE_CLASSES = ('fire',)  # 你的类别名称
FIRE_ROOT = 'path/to/your/dataset'  # 数据集根目录

class FIREAnnotationTransform:
    def __init__(self):
        self.class_to_ind = {'fire': 0}  # 类别到索引的映射

class FIREDetection:
    def __init__(self, root, image_sets='trainval', transform=None):
        self.ids = []
        for line in open(os.path.join(FIRE_ROOT, 'ImageSets/Main/trainval.txt')):
            self.ids.append((FIRE_ROOT, line.strip()))

别忘了修改data/init.py注册新数据集:

from .fire import FIREDetection, FIREAnnotationTransform, FIRE_CLASSES, FIRE_ROOT

3.3 修改SSD模型文件

在ssd.py中更新类别判断逻辑:

self.cfg = (coco, voc, fire)[num_classes == 2]  # 根据类别数自动选择配置

3.4 训练脚本调整

修改train.py中的数据集选择部分:

parser.add_argument('--dataset', choices=['VOC', 'COCO', 'FIRE'], default='FIRE')
parser.add_argument('--dataset_root', default=FIRE_ROOT)

# 在训练函数中
elif args.dataset == 'FIRE':
    cfg = fire
    dataset = FIREDetection(root=args.dataset_root, 
                          transform=SSDAugmentation(cfg['min_dim'], MEANS))

4. 训练技巧与问题排查

4.1 训练参数优化建议

这些参数经过我多次实验验证:

parser.add_argument('--batch_size', default=8, type=int)  # 根据显存调整
parser.add_argument('--lr', '--learning-rate', default=1e-3, type=float)
parser.add_argument('--momentum', default=0.9, type=float)
parser.add_argument('--weight_decay', default=5e-4, type=float)

小批量数据训练技巧:

  • 当数据少于1000张时,建议使用迁移学习
  • 设置更小的学习率(如1e-4)
  • 增加数据增强强度

4.2 常见报错解决方案

  1. StopIteration错误
# 修改train.py中的迭代器调用
try:
    images, targets = next(batch_iterator)
except StopIteration:
    batch_iterator = iter(data_loader)
    images, targets = next(batch_iterator)
  1. Tensor类型警告
# 将.data[0]改为.item()
loc_loss += loss_l.item()
conf_loss += loss_c.item()
  1. 数据加载报错: 检查XML文件是否与图片一一对应,我常用这个验证脚本:
from PIL import Image

for xml_file in os.listdir('Annotations'):
    img_file = xml_file.replace('.xml', '.jpg')
    try:
        Image.open(f'JPEGImages/{img_file}')  # 尝试打开图片
        ET.parse(f'Annotations/{xml_file}')  # 尝试解析XML
    except Exception as e:
        print(f'Error in {xml_file}: {str(e)}')

4.3 训练过程监控

推荐使用TensorBoard监控训练:

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter()
for iteration in range(args.max_iter):
    # ...训练代码...
    writer.add_scalar('Loss/total', loss.item(), iteration)
    writer.add_scalar('LR', optimizer.param_groups[0]['lr'], iteration)

5. 模型保存与应用

5.1 断点续训实现

修改train.py支持断点恢复:

parser.add_argument('--resume', default=None, type=str,
                   help='Checkpoint state_dict file to resume from')

if args.resume:
    print(f'Resuming from {args.resume}')
    ssd_net.load_state_dict(torch.load(args.resume))

5.2 模型导出与测试

训练完成后,用这个脚本测试单张图片:

def detect(image_path, model, transform, threshold=0.6):
    image = cv2.imread(image_path)
    h, w = image.shape[:2]
    x = transform(image)[0]
    x = x.unsqueeze(0)
    
    with torch.no_grad():
        detections = model(x)
    
    # 处理检测结果
    for i in range(detections.size(1)):
        j = 0
        while detections[0, i, j, 0] >= threshold:
            score = detections[0, i, j, 0]
            pt = detections[0, i, j, 1:] * torch.Tensor([w, h, w, h])
            cv2.rectangle(image, (int(pt[0]), int(pt[1])), 
                         (int(pt[2]), int(pt[3])), (0, 255, 0), 2)
            j += 1
    return image

5.3 模型优化技巧

  1. 冻结部分层(适合小数据集):
for param in ssd_net.vgg.parameters():
    param.requires_grad = False
  1. 学习率预热
def warmup_lr(iter, warmup_iter, initial_lr):
    if iter < warmup_iter:
        return initial_lr * (iter / warmup_iter)
    return initial_lr
  1. 多尺度训练: 在SSDAugmentation中增加随机缩放:
transforms.append(
    RandomSampleCrop(scale=(0.3, 1.0), ratio=(0.5, 2.0))
)

训练完成后,建议用测试集做全面评估:

def evaluate(model, dataset):
    model.eval()
    results = []
    for i in range(len(dataset)):
        image, targets = dataset.pull_item(i)
        detections = model(image.unsqueeze(0))
        # 计算mAP等指标
        ...
    return results
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐