第 8 章

Pytracking代码框架

·约 11 分钟

我们的跟踪器代码基于 Pytracking 框架(Martin Danelljan 团队开源的),目前主流跟踪器(OSTrack、MixFormer、SeqTrack 等)基本都用这套结构或它的变体。搞懂这个框架,后面读别人的代码、改自己的模型都会很顺。

先看看项目目录长什么样。以实验室的 dtrack 项目为例:

dtrack/
├── experiments/                          # 实验配置
│   └── ostrack/                          #   每个实验一个 YAML 文件
│       ├── dinov3l_384_got10k.yaml       
│       └── dinov3l_384_got10k_lora.yaml  
├── lib/
│   ├── config/ostrack/config.py          # 默认配置(所有超参的默认值在这里)
│   ├── models/ostrack/                   # 模型定义
│   │   ├── ostrack.py                    #   模型类 + build_ostrack()
│   │   └── vit_dinov3.py                 #   骨干网络
│   ├── train/                            # ---- 训练侧 ----
│   │   ├── run_training.py               #   训练入口
│   │   ├── train_script.py               #   调度器(后面会讲)
│   │   ├── base_functions.py             #   数据集加载、优化器构建
│   │   ├── actors/ostrack.py             #   Actor:前向 + 算 loss
│   │   ├── trainers/ltr_trainer.py       #   训练循环
│   │   ├── data/sampler.py               #   采样器
│   │   ├── dataset/                      #   各数据集的读取类
│   │   │   ├── got10k.py
│   │   │   ├── lasot.py
│   │   │   └── ...
│   │   └── admin/local.py                #   ⚠️ 本地路径(数据集在哪、输出存哪)
│   └── test/                             # ---- 测试侧 ----
│       ├── tracker/ostrack.py            #   Tracker 类
│       ├── parameter/ostrack.py          #   加载 checkpoint + 配置
│       └── evaluation/local.py           #   ⚠️ 测试侧的本地路径
├── tracking/test.py                      # 测试入口
├── pretrained_models/                    # 预训练权重
└── output/                               # 所有输出(checkpoint、日志、测试结果)

训练侧的代码全在 lib/train/,测试侧全在 lib/test/。另外注意那两个 local.py——代码搬到新机器上之后第一件事就是改这两个文件里的路径。

1. 训练流程

假设你现在要在 GOT-10k 上跑一个训练实验。

第一步:找到你要跑的实验配置。 去 experiments/ostrack/ 目录下找 YAML 文件,比如 dinov3l_384_got10k_lora_film.yaml。打开看看长什么样:

TRAIN:
  EPOCH: 100
  LR: 2e-4
  BATCH_SIZE: 8
DATA:
  TRAIN:
    DATASETS_NAME:
      - GOT10K_train_full
    DATASETS_RATIO:
      - 1
MODEL:
  PEFT:
    ENABLE: true
    LORA_RANK: 16

这里面只写了需要改的参数。没写的参数(比如图片大小、骨干网络类型)全部用 lib/config/ostrack/config.py 里的默认值。所以看一个实验的完整配置,要把 YAML 和 config.py 对照着看。

第二步:确认 local.py 路径。 打开 lib/train/admin/local.py,检查里面的数据集路径和工作空间路径是否指向你当前机器上的正确位置。

第三步:启动训练。

python lib/train/run_training.py \
    --script ostrack \
    --config dinov3l_384_got10k_lora_film

两个参数的含义:

参数意思
--script ostrack用 train_script.py 里的 ostrack 分支来组装模型和训练逻辑
--config dinov3l_384_got10k_lora_film用 experiments/ostrack/ 下同名的 YAML 配置文件

敲回车之后发生了什么?

run_training.py 把参数传给 train_script.py。train_script.py 是整个训练的调度中心,它根据 --script 的值决定:用哪个函数建模型、用哪个 Actor、用哪个优化器。具体来说,它依次做四件事:

  1. 建模型:调 build_ostrack(cfg) 创建模型实例。
  2. 建优化器:调 base_functions.py 里的优化器构建函数,配好学习率和调度器。
  3. 建 Actor:创建 OSTrackActor。Actor 是"训练逻辑的容器"——它接收一个 batch 的数据,调用模型跑前向,算 loss,把 loss 返回给训练循环。模型本身只管"输入图片 → 输出预测",怎么算 loss、怎么组织训练数据,是 Actor 的事。
  4. 建 Trainer:创建 LTRTrainer,把 Actor、DataLoader、优化器组装在一起,开始 epoch 循环。

训练循环里,每个 batch 的数据流是这样的:

数据集 (got10k.py)
  → TrackingSampler: 从同一个视频里随机抽两帧,一帧当模板、一帧当搜索区域
  → 数据增强(裁剪、颜色抖动、翻转等)
  → DataLoader 打包成 batch
  → Actor 接收 batch:
      → 从 data 里拆出模板图、搜索图、标注框
      → 喂给 model.forward() 得到预测框和响应图
      → 算 loss(L1 + GIoU + focal loss)
      → loss.backward()
  → Trainer 更新参数、到 epoch 末尾存 checkpoint

Checkpoint 存在哪? 在 output/checkpoints/train/ostrack/<config名>/ 下面,每个 epoch 一个文件:

output/checkpoints/train/ostrack/dinov3l_384_got10k_lora_film/
├── OSTrack_ep0001.pth.tar
├── OSTrack_ep0002.pth.tar
├── ...
└── OSTrack_ep0100.pth.tar

文件名开头的 OSTrack 是模型类名(type(net).__name__),自动生成的。如果你改了模型类的名字,checkpoint 文件名也会跟着变。

2. 测试流程

训练跑完了,checkpoint 出来了,怎么测?

启动命令:

python tracking/test.py \
    --tracker_name ostrack \
    --tracker_param dinov3l_384_got10k_lora_film \
    --dataset got10k_test
参数映射到哪里
--tracker_name ostracklib/test/tracker/ostrack.py 里的 Tracker 类
--tracker_param dinov3l_384_got10k_lora_filmlib/test/parameter/ostrack.py 里的同名函数
--dataset got10k_test测试数据集

parameter/ostrack.py 的作用就是告诉框架用哪个 checkpoint、用什么配置,里面指定了 checkpoint 文件路径和加载的 YAML。

tracker/ostrack.py 定义了 Tracker 类,核心就两个方法:

  • initialize(image, info):处理第一帧。收到第一帧图片和标注框后,裁出模板区域,提取模板特征并缓存起来。
  • track(image):处理后续每一帧。根据上一帧的预测位置裁出搜索区域,和缓存的模板一起送进模型,解码出预测框返回。

测试框架自动帮你做逐帧循环:读一个视频序列 → 第一帧调 initialize → 后面每一帧调 track → 全部结果写到文件里。

结果会保存在 output/test/tracking_results/ostrack/<param名>/ 下面,每个视频一个 .txt 文件,每行一帧的预测框坐标。评估脚本读这些文件和标注对比,算出 AUC、Precision 等指标。

训练侧和测试侧有对应关系。 改了训练侧的模型结构,测试侧通常也要同步改。

3. 多阶段训练与参数冻结

前面讲的是训练和测试流程。实际研究中模型有好几个模块(骨干网络、特征调制、级联精炼、轨迹利用……),不一定全部一起训。

增量式训练 vs 端到端训练

两种常见的训练策略:

增量式:先训模块 A,冻住 A,再训模块 B,冻住 A+B,再训模块 C……一级一级叠上去。好处是每一级可以单独验证有没有效果,调试很清晰。坏处是后面的模块永远没机会改善前面模块的特征——你冻住了它,它就不动了。

端到端:把多个模块放在同一个 loss 下一起训,模块之间可以互相适应。效果通常更好,但出了问题不好定位到底是谁的锅。

实验室的做法:探索新模块时用增量式训练,跑几个 epoch 快速看信号;确认有效后切到端到端训练出正式结果。

参数冻结要改两个地方

第一个位置:run_training.py 里控制 requires_grad,冻结参数。

训练启动时会调一个冻结函数(比如 mark_lora_and_heads_trainable()),逻辑是先把所有参数的 requires_grad 设为 False,再白名单解冻你想训的参数。requires_grad=False 的参数不会收到梯度,optimizer 不会更新它。

# 简化示意
def mark_lora_and_heads_trainable(model):
    # 先全冻
    for param in model.parameters():
        param.requires_grad = False
    # 再解冻白名单
    for name, param in model.named_parameters():
        if 'lora_' in name or 'box_head' in name:
            param.requires_grad = True

第二个位置:Actor 里控制 train()/eval() 模式,冻结统计量。

这是另一件完全独立的事。PyTorch 的 model.train() 和 model.eval() 控制的是 BatchNorm 和 Dropout 的行为:train() 模式下 BN 会用当前 batch 的统计量更新 running mean/var;eval() 模式下用之前积累的固定值。Actor 里会对冻结的模块强制切 eval():

# Actor.train() 简化示意
def train(self, mode=True):
    self.net.train(mode)         # 整体设为 train
    self.net.backbone.eval()     # 冻结的骨干 → 强制 eval

为什么两件事都要做? 如果你只设了 requires_grad=False 但没切 eval(),会出现一个很隐蔽的问题:参数确实没更新,但 BN 的 running mean/var 统计量被改了。权重没变,推理行为却在漂移。

train() 模式eval() 模式
requires_grad=True✅ 正常训练参数更新但 BN 用固定统计量(少见)
requires_grad=False⚠️ 参数不更新,但 BN 统计量在偷偷漂移!✅ 完全冻结

至此,你应该对Pytracking代码框架的训练链路、测试链路、代码修改流程都有基本了解了。

训练链路:run_training -> train_script -> base_functions -> dataloader -> build 方法 -> actor -> trainer -> models

测试链路:test -> params -> tracker

创建新实验:yaml -> setting -> 训练链路 -> 测试链路

注意:一定要用git管理代码版本。 每新出一个代码版本,整个文件夹复制一份,复制时可以排除output权重文件夹,但git记录应该保留。