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_train2. 数据准备:打造规范化的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进行标注,几个提高效率的技巧:
- 修改默认保存路径为Annotations目录(按Ctrl+R设置)
- 如果只有单一类别,在"View"菜单设置默认标签
- 常用快捷键:
- 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_ROOT3.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 常见报错解决方案
- StopIteration错误:
# 修改train.py中的迭代器调用 try: images, targets = next(batch_iterator) except StopIteration: batch_iterator = iter(data_loader) images, targets = next(batch_iterator)- Tensor类型警告:
# 将.data[0]改为.item() loc_loss += loss_l.item() conf_loss += loss_c.item()- 数据加载报错: 检查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 image5.3 模型优化技巧
- 冻结部分层(适合小数据集):
for param in ssd_net.vgg.parameters(): param.requires_grad = False- 学习率预热:
def warmup_lr(iter, warmup_iter, initial_lr): if iter < warmup_iter: return initial_lr * (iter / warmup_iter) return initial_lr- 多尺度训练: 在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