1.数据流
数据采样方法在“/ltr/train_settings/bbreg/atom.py”中,通过dataset_train = sampler.ATOMSampler(*args)
封装
dataset_train = sampler.ATOMSampler([lasot_train, got10k_train, trackingnet_train, coco_train],
[1, 1, 1, 1],
# 这里的samples_per_epoch=batch_size x n,如果batch是1,在训练中显示的就是
# [train: num_epoch, x / batch_size * n] FPS: 0.3 (4.5) , Loss/total: 43.69654 , Loss/segm: 43.69654 , Stats/acc: 0.56702
# 由於batch_size * n構成了一個Epoch中所有的TensorDict形式的數據數量,通過LTRLoader包裝成batch結構後,就剩 "n" 個TensorDict,這裏就是1000個
samples_per_epoch=1000*settings.batch_size,
max_gap=50,
processing=data_processing_train)
通过继承了torch.utils.data.dataloader.DataLoader
类的LTRLoader()
方法,在"/ltr/trainers/ltr_trainer.py"中用for i, data in enumerate(loader, 1)
来遍历数据,具体作用就是把采样的数据按照一定规则打包,输出一个[batch, n_frames, channels, H, W]格式的数据
loader_train = LTRLoader('train',
dataset_train,
#
training=True,
batch_size=settings.batch_size,
# 数据读取线程数
num_workers=settings.num_workers,
# 在DDP模式下,没有这个参数
shuffle=True,
# 例如,99个数据,batch=30,最后会剩下9个数据,这时候就把这9个数据扔掉不用
drop_last=True,
# 在loader中,意思是:按batch_size抽取的数据,在第“1”维度上拼接起来,大概就是num_sequences
stack_dim=1)
封装进训练器,训练器的作用就是把数据分发给网络模型部分,然后让网络进行前向过程,计算损失,通过损失函数,来给网络赋能,并且更新数据。
trainer = LTRTrainer(actor, [loader_train, loader_val], optimizer, settings, lr_scheduler)
之所以把数据加载模块封装成[loader_train, loader_val]
,是因为在"/ltr/trainers/ltr_trainer.py"中,需要实现每隔n个Epoch进行一次validation,具体实现方法为:
# selfl.loaders就是[loader_train, loader_val]
for loader in self.loaders:
if self.epoch % loader.epoch_interval == 0:
# 这里就是利用 for i, data in enumerate(loader, 1)来把数据放入网络
self.cycle_dataset(loader)
2.数据采样方法
数据采样函数是"/ltr/data/sampler.py"中的class ATOMSamlper
,其继承于class TrackingSampler
,可以这么理解,采样函数实际上就是class TrackingSampler
,而class ATOMSamlper
的作用就是给其父类初始化一些参数。
①数据集随机抽取
在函数中有一组列表,self.p_datasets = [4, 3, 2, 1]
,就意味着4个数据集,按照
4
4
+
3
+
2
+
1
\frac{4}{4+3+2+1}
4+3+2+14 、
3
4
+
3
+
2
+
1
\frac{3}{4+3+2+1}
4+3+2+13 、
2
4
+
3
+
2
+
1
\frac{2}{4+3+2+1}
4+3+2+12 、
1
4
+
3
+
2
+
1
\frac{1}{4+3+2+1}
4+3+2+11 的概率进行抽取数某一据集。
p_total = sum(p_datasets)
self.p_datasets = [x / p_total for x in p_datasets]
# 这里的self.datasets是在ltr/train_settings/bbreg/atom.py中封装的[lasot_train, got10k_train, trackingnet_train, coco_train]
# 这里的dataset返回的是ltr/dataset下面的各类函数,例如lasot.py中的class Lasot(BaseVideoDataset):
dataset = random.choices(self.datasets, self.p_datasets)[0]
②某一数据集中的视频序列随机抽取
首先通过dataset.get_num_sequences()
获取数据集中一共有多少个视频序列
然后在众多视频序列中抽取一个视频序列(一个视频序列又包含了很多帧图片)
seq_id = random.randint(0, dataset.get_num_sequences() - 1)
③在某一视频序列种采样,如Got-10k数据集
❶ interval采样:
在一个视频序列中,随机抽取一张图片,作为base_frame;
在base_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )分别抽取一张train_frame和一张test_frame;
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;为何在 ± \pm ± 50这么大的范围内都抽不到呢?
因为需要抽取含有目标的视频帧,有时候视频中不含有可见目标,此时需要增大搜索范围
❷ casual采样:
以视频序列的中间点作为参考帧,即base_frame
在base_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )分别抽取一张train_frame和一张test_frame;
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;
❸ default采样:
没有参考帧base_frame,直接先随机在视频序列中抽取train_frame
在train_frame前后 max_gap=50的范围内(即 ± \pm ± 50 )随机抽取一张test_frame;
train_frame和test_frame可能会重复
如果没有抽到,则max_gap + gap_increase,扩大范围去抽取;
④在某一非视频序列中采样,例如COCO数据集
train_frame_ids = [1] * self.num_train_frames
test_frame_ids = [1] * self.num_test_frames
直接抽取
3.采样后的数据处理方法
处理函数继承的基类
class BaseProcessing:
"""
处理类用于在传入网络之前,处理数据, 返回一个数据集
例如,可以用于裁剪目标物体附近的搜索区域、用于不同的数据增强
"""
def __init__(self, transform=transforms.ToTensor(), train_transform=None, test_transform=None, joint_transform=None):
"""
参数:
transform : 用于图片的一系列变换操作
仅当train_transform或者test_transform是None的时候才用
train_transform : 用于训练图片的一系列变换操作
如果为None, 取而代之的是'transform'值
test_transform : 用于测试图片的一系列变换操作
如果为None, 取而代之的是'transform'值
注意看,虽然在train_settings中设置的是transform_val,但是赋值的是transform_test=transform_val
所以,test_transform和transform_val是一回事
joint_transform : 将'jointly'用于训练图片和测试图片的一系列变换操作
例如,可以转换测试和训练图片为灰度
"""
self.transform = {'train': transform if train_transform is None else train_transform,
'test': transform if test_transform is None else test_transform,
'joint': joint_transform}
def __call__(self, data: TensorDict):
raise NotImplementedError
①self.transform['joint']
处理
就是先把所有图片都ToTensor,还有0.05的概率把图片变成灰度图。
transform_joint = tfm.Transform(tfm.ToGrayscale(probability=0.05))
# 这里的self.transform['joint']指向基类中的self.transform
data['train_images'], data['train_anno'] = self.transform['joint'](image=data['train_images'],
bbox=data['train_anno'])
②self._get_jittered_box
对bbox进行扰动
利用_get_jittered_box
生成带扰动的bbox,该扰动只对test_anno有效,train_anno不会产生扰动。扰动控制通过self.scale_jitter_factor
和self.center_jitter_factor
实现,其中mode是控制标志位。
self.scale_jitter_factor = {'train': 0, 'test': 0.5}
self.center_jitter_factor = {'train': 0, 'test': 4.5}
最终组合成:
def _get_jittered_box(self, box, mode):
"""
抖动一下输入box,box是相对坐标的(cx/sw, cy/sh, log(w), log(h))
参数:
box : 输入的bbox
mode: 字符串'train'或者'test' 指的是训练或者测试数据
返回值:
torch.Tensor: jittered box
"""
# randn(2) 生成两个服从(0,1)的数,范围是【-1,+1】前一个对应w,后一个对应h
# 对于train,scale_jitter_factor=0,所以 jittered_size=box[2:4]
jittered_size = box[2:4] * torch.exp(torch.randn(2) * self.scale_jitter_factor[mode])
# 计算jitter_size后的x * y * w * h然后开方,乘以center_jitter_factor['train' or 'test'],作为最大偏移量
# 对于train,center_jitter_factor=0,所以 max_offset=0
max_offset = (jittered_size.prod().sqrt() * torch.tensor(self.center_jitter_factor[mode]).float())
# 计算中心抖动 [x + w/2 + max_offset * (torch.randn(2)[0] - 0.5), y + h/2 + max_offset * (torch.randn(2)[1] - 0.5)]
jittered_center = box[0:2] + 0.5 * box[2:4] + max_offset * (torch.rand(2) - 0.5)
return torch.cat((jittered_center - 0.5 * jittered_size, jittered_size), dim=0)
具体效果描述:
test_anno=[x, y, w, h]长宽 [ w , h ] [w, h] [w,h]随机放大或者缩小 [ 1 e , e ] [\frac{1}{\sqrt{e}}, \sqrt{e}] [e1,e]倍(服从正态分布,倍数是1的概率最大, 放大 e \sqrt{e} e或缩小 1 e \frac{1}{\sqrt{e}} e1的概率最低),得到新的长宽 [ w j i t t e r e d , h j i t t e r e d ] [w_{jittered}, h_{jittered}] [wjittered,hjittered]
test_anno=[x, y, w, h]中心点坐标 [ x + w 2 , y + h 2 ] [x+\frac{w}{2}, y+\frac{h}{2}] [x+2w,y+2h]随机偏移 [ − 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 , + 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 ] [-\frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5, +\frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5] [−21wjittered×hjittered×4.5,+21wjittered×hjittered×4.5] (服从正态分布, 偏移量为0的概率最大, 偏移量为 1 2 w j i t t e r e d × h j i t t e r e d × 4.5 \frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.5 21wjittered×hjittered×4.5的概率最低)
最终得到 [ x j i t t e r e d , y j i t t e r e d , w j i t t e r e d , h j i t t e r e d ] [x_{jittered}, y_{jittered}, w_{jittered}, h_{jittered}] [xjittered,yjittered,wjittered,hjittered]
③prutils.jittered_center_crop
根据以上处理的结果裁剪
将输入图片按照 [ x j i t t e r e d , y j i t t e r e d , w j i t t e r e d , h j i t t e r e d ] [x_{jittered}, y_{jittered}, w_{jittered}, h_{jittered}] [xjittered,yjittered,wjittered,hjittered]、真实标注、search_area_factor和output_size裁剪出需要的尺寸,并取得裁剪后的图片中Boundingbox的对应坐标。
④再次transform
将上述过程的结果进行transform,参数封装在“/ltr/train_settings/bbreg/atom.py”中。
transform_train = tfm.Transform(tfm.ToTensorAndJitter(0.2),
tfm.Normalize(mean=settings.normalize_mean,
std=settings.normalize_std))
关于tfm.ToTensorAndJitter(0.2)
,就是服从正态分布的概率,让图片在
[
0.8
,
1.2
]
[0.8, 1.2]
[0.8,1.2]之间进行亮度调整, 即不变的概率最大,
×
0.8
\times 0.8
×0.8和
×
1.2
\times 1.2
×1.2的概率最低。
class ToTensorAndJitter(TransformBase):
"""
继承了TransformBase,所有下面的transform_image和transform_mask会在TransformBase
通过transform_func = getattr(self, 'transform_' + var_name),来调用具体用了哪个函数
"""
def __init__(self, brightness_jitter=0.0, normalize=True):
super().__init__()
self.brightness_jitter = brightness_jitter
self.normalize = normalize
def roll(self):
return np.random.uniform(max(0, 1 - self.brightness_jitter), 1 + self.brightness_jitter)
def transform(self, img, brightness_factor):
img = torch.from_numpy(img.transpose((2, 0, 1)))
# 这里的brightness_factor是随机参数,其实就是roll的返回值
return img.float().mul(brightness_factor / 255.0).clamp(0.0, 1.0)
关于tfm.Normalize(mean=settings.normalize_mean, std=settings.normalize_std)
,就是使用平均值settings.normalize_mean = [0.485, 0.456, 0.406]
和标准差settings.normalize_std = [0.229, 0.224, 0.225]
对图片进行归一化
class Normalize(TransformBase):
def __init__(self, mean, std, inplace=False):
super().__init__()
# settings.normalize_mean = [0.485, 0.456, 0.406]
self.mean = mean
# settings.normalize_std = [0.229, 0.224, 0.225]
self.std = std
# 计算得到的值不会覆盖之前的值
self.inplace = inplace
def transform_image(self, image):
return tvisf.normalize(image, self.mean, self.std, self.inplace)
⑤利用self._generate_proposals()
给data['test_anno']
添加噪声
data['test_anno']
就是根据① ② ③ ④ ⑤过程生成的bbox
self.proposal_params = {'min_iou': 0.1, 'boxes_per_frame': 16, 'sigma_factor': [0.01, 0.05, 0.1, 0.2, 0.3]}
def _generate_proposals(self, box):
"""
通过给输入的box添加噪音,生成proposal
"""
# 生成proposal
num_proposals = self.proposal_params['boxes_per_frame']
# .get(key,'default')查找键值‘key’,如果不存在,则返回‘default’
proposal_method = self.proposal_params.get('proposal_method', 'default')
if proposal_method == 'default':
proposals = torch.zeros((num_proposals, 4))
gt_iou = torch.zeros(num_proposals)
for i in range(num_proposals):
proposals[i, :], gt_iou[i] = prutils.perturb_box(box,
min_iou=self.proposal_params['min_iou'],
sigma_factor=self.proposal_params['sigma_factor'])
elif proposal_method == 'gmm':
proposals, _, _ = prutils.sample_box_gmm(box,
self.proposal_params['proposal_sigma'],
num_samples=num_proposals)
gt_iou = prutils.iou(box.view(1, 4), proposals.view(-1, 4))
# map to [-1, 1]
gt_iou = gt_iou * 2 - 1
return proposals, gt_iou
❶ 第一种扰动方法:
计算
data['test_anno]
的 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]
在'sigma_factor': [0.01, 0.05, 0.1, 0.2, 0.3]
随机抽取一个值,例如0.1,然后变成perturb_factor=[0.1, 0.1, 0.1, 0.1]
的Tensor
利用random.gauss(bbox[0], perturb_factor[0])
,计算平均值为 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]、标准差为[0.1, 0.1, 0.1, 0.1]的扰动,白话就是 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]概率最高,得到扰动后的 [ x p e r t u r b e d , y p e r t u r b e d , w p e r t u r b e d , h p e r t u r b e d ] [x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}] [xperturbed,yperturbed,wperturbed,hperturbed]
计算 [ x p e r t u r b e d , y p e r t u r b e d , w p e r t u r b e d , h p e r t u r b e d ] [x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}] [xperturbed,yperturbed,wperturbed,hperturbed]和 [ x c e n t e r , y c e n t e r , w , h ] [x_{center}, y_{center}, w, h] [xcenter,ycenter,w,h]的IOU
将扰动系数perturb_factor *= 0.9
将上述过程循环100次,得到结果,如果在100次以内就得到了
box_iou > min_iou
的结果,直接输出一组box_per, box_iou
将上述过程进行16次,得到16组
box_per, box_iou
, 就是num_proposals
❷ 第二种扰动方法(高斯混合模型):
高斯混合模型,即使用多个高斯函数去近似概率分布:
p
G
M
M
=
Σ
k
=
1
K
p
(
k
)
p
(
x
∣
k
)
=
Σ
k
=
1
K
α
k
p
(
x
∣
μ
k
,
Σ
k
)
p_{GMM} = \Sigma^{K}_{k=1}p(k)p(x|k) = \Sigma^{K}_{k=1}\alpha_k p(x|\mu_k, \Sigma_k)
pGMM=Σk=1Kp(k)p(x∣k)=Σk=1Kαkp(x∣μk,Σk)
其中,
K
K
K为模型个数(相当于num_proposals=16),就是用了多少个单高斯分布;
α
k
\alpha_k
αk是第
k
k
k个单高斯分布的概率,
Σ
k
=
1
K
α
k
=
1
\Sigma^{K}_{k=1}\alpha_k = 1
Σk=1Kαk=1;
p
(
x
∣
μ
k
,
Σ
k
)
p(x|\mu_k, \Sigma_k)
p(x∣μk,Σk)是的第
k
k
k个均值为
μ
k
\mu_k
μk,方差为
Σ
k
\Sigma_k
Σk高斯分布的概率密度
代码实现:
方差
Σ
k
\Sigma_k
Σk
# proposal_sigma = [[a, b], [c, d]]
center_std = torch.Tensor([s[0] for s in proposal_sigma])
sz_std = torch.Tensor([s[1] for s in proposal_sigma])
# stack后维度[4,1,2]
std = torch.stack([center_std, center_std, sz_std, sz_std])
# 2
num_components = std.shape[-1]
# 4
num_dims = std.numel() // num_components
# (1,4,2)
std = std.view(1, num_dims, num_components)
模型个数 K K K
k = torch.randint(num_components, (num_samples,), dtype=torch.int64)
# 输出[16, 4], std=[1, 4, 2],由于这里k只有0和1,作用就是把最后一个维度复制成为16,索引方式就是index=0或1
std_samp = std[0, :, k].t()
Bbox经过GMM采样后的中心点坐标(这里是中心点坐标的偏差,相当于 x i − x x_i - x xi−x),然后根据平均值,计算 x i x_i xi
x_centered = std_samp * torch.randn(num_samples, num_dims)
# rel左上角和长宽的对数表示bbox
proposals_rel = x_centered + mean_box_rel
⑥组合输出
data['test_images']
和data['train_images']
图片(单张),在LTRLoader中才会打包出Batch
data['test_anno']
采样后经过self._generate_proposals,变换成:
data['test_proposals']
包含16个bbox
data['proposal_iou']
包含16个扰动bbox的IoU
data['train_anno']
真实的bbox,不经过self._generate_proposals的bbox
4. 网络模型
重难点:
图中红色虚线框中的部分是模型训练部分,绿色实线框是在模型推理中通过共轭梯度下降产生的一组filters,整张图就是包含了训练和推理的完整模型图。完整的inference部分如下图:
图中有两个5次迭代,classification的5次迭代对应算法中的红色线框,IoU_predict的对应算法图中的绿色线框,算法图如下