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个数据集,按照44+3+2+1\frac{4}{4+3+2+1}4+3+2+14 、34+3+2+1\frac{3}{4+3+2+1}4+3+2+13 、24+3+2+1\frac{2}{4+3+2+1}4+3+2+12 、14+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]随机放大或者缩小[1e,e][\frac{1}{\sqrt{e}}, \sqrt{e}][e1,e]倍(服从正态分布,倍数是1的概率最大, 放大e\sqrt{e}e或缩小1e\frac{1}{\sqrt{e}}e1的概率最低),得到新的长宽[wjittered,hjittered][w_{jittered}, h_{jittered}][wjittered,hjittered]
test_anno=[x, y, w, h]中心点坐标[x+w2,y+h2][x+\frac{w}{2}, y+\frac{h}{2}][x+2w,y+2h]随机偏移[−12wjittered×hjittered×4.5,+12wjittered×hjittered×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的概率最大, 偏移量为12wjittered×hjittered×4.5\frac{1}{2}\sqrt{w_{jittered}\times h_{jittered}}\times 4.521wjittered×hjittered×4.5的概率最低)
最终得到[xjittered,yjittered,wjittered,hjittered][x_{jittered}, y_{jittered}, w_{jittered}, h_{jittered}][xjittered,yjittered,wjittered,hjittered]
③prutils.jittered_center_crop根据以上处理的结果裁剪
将输入图片按照[xjittered,yjittered,wjittered,hjittered][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]的[xcenter,ycenter,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]),计算平均值为[xcenter,ycenter,w,h][x_{center}, y_{center}, w, h][xcenter,ycenter,w,h]、标准差为[0.1, 0.1, 0.1, 0.1]的扰动,白话就是[xcenter,ycenter,w,h][x_{center}, y_{center}, w, h][xcenter,ycenter,w,h]概率最高,得到扰动后的[xperturbed,yperturbed,wperturbed,hperturbed][x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}][xperturbed,yperturbed,wperturbed,hperturbed]
计算[xperturbed,yperturbed,wperturbed,hperturbed][x_{perturbed}, y_{perturbed}, w_{perturbed}, h_{perturbed}][xperturbed,yperturbed,wperturbed,hperturbed]和[xcenter,ycenter,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
❷ 第二种扰动方法(高斯混合模型):
高斯混合模型,即使用多个高斯函数去近似概率分布:
pGMM=Σk=1Kp(k)p(x∣k)=Σk=1Kαkp(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)
其中,KKK为模型个数(相当于num_proposals=16),就是用了多少个单高斯分布;αk\alpha_kαk是第kkk个单高斯分布的概率,Σk=1Kα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)是的第kkk个均值为μ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)
模型个数KKK
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采样后的中心点坐标(这里是中心点坐标的偏差,相当于xi−xx_i - xxi−x),然后根据平均值,计算xix_ixi
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的对应算法图中的绿色线框,算法图如下

本文详细介绍了数据流的构建过程,包括使用ATOMSampler进行数据采样,通过LTRLoader组织数据,以及训练器如何分发数据给模型。数据采样方法涉及不同数据集的概率抽取和视频帧的选择策略。此外,还讨论了数据处理步骤,如图片的亮度调整和归一化,以及使用高斯混合模型添加噪声生成提案。整个流程涵盖了从数据采样到模型训练的关键环节。
1314

被折叠的 条评论
为什么被折叠?



