Pytorch-Lightning之小试

关于Pytorch-Lightning的优势和相关知识,下面的博客做了很好的介绍,因为本人课题需要,所以参考该博客尝试搭建一个基于Pytorch-Lightning的深度估计系统。Pytorch Lightning 完全攻略

前言

使用Pytorch Lightning框架的深度估计系统是有的,比如SC-DpethV3,但是我要改进的系统其所用框架是Pytorch,当我参考SC-DpethV3来转换目标系统时,陷入了半懂半不懂的境地(可能是我本人之前没有接触过深度学习),因此我想从0自己搭一个基于Pytorch-Lightning的深度估计框架出来,也方便我下一步的课题开展。

Pytorch Lightning的基本架构

这里我使用的是Pytorch Lightning 完全攻略提出的通用架构,架构链接。在此十分感谢博主Takanashi​的辛苦工作!
在README文件中作者对Pytorch Lightning以及其总结的模板进行了介绍,模板结构如下:
在这里插入图片描述
根目录下主要放两个文件:main.py和utils.py(辅助用),main.py包含三个功能:1、定义参数解析器parser,指定一些参数。2、callback函数设置:自动存档,Early Stop及LR Scheduler等,在pl.Trainer中会用到。3、将模型接口,数据集接口,训练器实例化。
data和modle两个文件夹中放入__init__.py文件,做成包,将接口类导入

from .data_interface import DInterface
from .model_interface import MInterface

这样做就可以再main.py中直接从文件夹导入接口类如from model import MInterface
data_interface.py和model_interface.py主要用来定义接口类:DInterface和MInterface,这两个接口可以控制不同的模型,不需要再修改,只需要修改传入的参数即可。
作者还提醒使用严格的命名-snakecase

一、main.py

本页结构清晰,有三部分:1、加载回调的函数load_callbacks() ;2、主函数main(args) ;3、程序入口if __name__ == '__main__':
**程序入口:**设置网络参数,调用main函数。
将参数修改为自己要设置的,参考的是目标系统,有些参数可能就不用,但是暂时先保留着。
将每个参数的含义注释清楚,将未用到的删除。
论文中的数学公式使用了SSIM(结构相似度),但是代码中却未使用SSIM。
关于dataloader workers:
1、每次dataloader加载数据时:dataloader一次性创建num_worker个worker,(也可以说dataloader一次性创建num_worker个工作进程,worker也是普通的工作进程),并用batch_sampler将指定batch分配给指定worker,worker将它负责的batch加载进RAM。
2、然后,dataloader从RAM中找本轮迭代要用的batch,如果找到了,就使用。如果没找到,就要num_worker个worker继续加载batch到内存,直到dataloader在RAM中找到目标batch。一般情况下都是能找到的,因为batch_sampler指定batch时当然优先指定本轮要用的batch。num_worker设置得大,好处是寻batch速度快,因为下一轮迭代的batch很可能在上一轮/上上一轮…迭代时已经加载好了。坏处是内存开销大,也加重了CPU负担(worker加载数据到RAM的进程是CPU复制的嘛)。num_workers的经验设置值是自己电脑/服务器的CPU核心数,如果CPU很强、RAM也很充足,就可以设置得更大些。
3、如果num_worker设为0,意味着每一轮迭代时,dataloader不再有自主加载数据到RAM这一步骤(因为没有worker了),而是在RAM中找batch,找不到时再加载相应的batch。缺点当然是速度更慢。最好设置为CPU的数量。
main函数:
设置随机种子,我看目标代码预训练的seed设置的是0.

 pl.seed_everything(args.seed)

关键点路径的获取,好像是用来加载预训练的模型的。

load_model_path_by_args(args)

需要预训练模型的路径,模型的版本名称,模型的版本号,在scv3中先设置了logger日志器,再设置检查点的保存和加载。
因此参考scv3先设置日志记录器:日志的保存目录–ckpts,日志的名字
并在回调函数列表中添加ModelCheckpoint
从之前的检查点恢复:
litemono 是使用预训练的编码器(在imagenet上预训练过的需要下载),代码中load_model(self)加载的模型名称是测试和评估时在命令行特别注明的,就是训练好的模型权重;self.load_pretrain()函数使用的是在ImageNet上预先训练的主干(深度编码器)的权重–mypretrain。
scv3中的检查点恢复指的应该是整个系统的而不是某个编码器的。并且scv3的训练命令行中也没有指明检查点。对于我来说,这个检查点是不存在的,因为litemono的ckpts是分散的不是pl完整系统的。
将模板代码在的加载检查点路径函数删去,直接在参数列表设置,并使用系统直接加载,**后面的参数是否是必要的?**可以不加

model = MInterface(args)
model = model.load_from_checkpoint(args.ckpt_path, strict=False, args = args)

下面是训练器,如何将设置的检查点回调放到Trainer训练器中,从scv3代码看,直接将检查点函数放到Trainer参数callbacks(列表)中也是可以的所以我直接callbacks=callback_list ,对于其他参数limit_train_batches(每个epoch运行多少个batch,litemono有限制么?好像没有),limit_val_batches,num_sanity_val_steps(在开始训练之前,健康检查运行n批val。这可以捕获验证中的任何错误,而无需等待第一次验证检查。培训师默认使用2个步骤。在此处关闭或修改它。litemono未做设置)
关于Trainer更细节的需要前往官网了解。
下面就是正常的拟合

数据集接口data_interface.py

litemono是怎么加载自己的数据的?
1、通过字典的方式定义了两种数据类型kitti_raw和kitti_odom的处理类KITTIRAWDataset和KITTIOdomDataset(获得图像路径)

datasets_dict = {"kitti": datasets.KITTIRAWDataset,
                 "kitti_odom": datasets.KITTIOdomDataset}
self.dataset = datasets_dict[self.opt.dataset]

KITTIRAWDataset的继承关系为:KITTIRAWDataset<-KITTIDataset<-MonoDataset<-data.Dataset
主要干了两个事,获得图像的路径,产生深度真值(利用激光雷达和标定参数)。如果是kiitidepth数据集可以直接获得真值深度图
根据在参数列表中的设置,确定是哪个处理类-这里确定是KITTIRAWDataset。
2、找到位于本地文件目录/splits/eigen_zhou/xxx_files.txt 的文件路径

fpath = os.path.join(os.path.dirname(__file__), "splits", self.opt.split, "{}_files.txt")

3、确定训练集和验证集的文件名还有图像的格式:如果一个数据集没有提前分割好,在pl中应该如何设置?

train_filenames = readlines(fpath.format("train"))
val_filenames = readlines(fpath.format("val"))
img_ext = '.png' if self.opt.png else '.jpg'

4、确定train_files.txt中数据的训练样本数量以及总步数=训练样本数量/每批次样本数 x epochs的数量 也就是总共需要多少个batch

num_train_samples = len(train_filenames)
self.num_total_steps = num_train_samples // self.opt.batch_size * self.opt.num_epochs

5、实例化KITTIRAWDataset和DataLoader:数据集的创建以及向训练传递数据

train_dataset = self.dataset(
     self.opt.data_path, train_filenames, self.opt.height, self.opt.width,
     self.opt.frame_ids, 4, is_train=True, img_ext=img_ext)
 self.train_loader = DataLoader(
     train_dataset, self.opt.batch_size, True,
     num_workers=self.opt.num_workers, pin_memory=True, drop_last=True)

6、验证数据集的创建并向验证传递数据。内置函数iter()并将self.val_loader作为参数传递,从而创建一个迭代器,之后在代码中的val(self)函数会使用next()函数会调用迭代器的__next__()方法,从而返回迭代器中的下一个值。直到迭代器中的所有值都返回后,抛出一个StopIteration异常。

val_dataset = self.dataset(
    self.opt.data_path, val_filenames, self.opt.height, self.opt.width,
    self.opt.frame_ids, 4, is_train=False, img_ext=img_ext)
self.val_loader = DataLoader(
    val_dataset, self.opt.batch_size, True,
    num_workers=self.opt.num_workers, pin_memory=True, drop_last=True)
self.val_iter = iter(self.val_loader)

如何修改?scv3可适当参考,另外发现一篇博客讲解如何使用LightningDataModule的,LightningDataModule的使用
scv3首先进行初始化并保存了超参数,通过get_training_size获得training_size,指定图像的尺寸,并获取伪真值(为了设置伪真值自定义了一个训练集函数)。这个不好改啊。scv3提供伪真值的训练集了。
我想用scv3数据集来训练改完后的litemono(因为我要验证其对动态场景的稳定性),也就是说数据集接口部分主要参考scv3。
需要注意的是litemono对数据的要求有无冲突,litemono后续用train_loader 进行模型训练求解输出和损失,这些操作依赖数据的color_aug属性?这是一个在MonoDataset类中进行了色彩增强后的图像。能否在接口改为scv3后也添加一个这个属性?这个属性很重要么,训练部分就是将所有增强后的图像放入到网络模型中。
先看一下scv3数据集的结构分布吧
在这里插入图片描述
结构很清晰,主要包含两个大文件夹,Training和Testing。Training包含三类文件夹:1、场景;2、训练txt文本(跟litemono一样,包含训练所用的数据名);3、验证txt文本。
每个场景一堆有序图像,相机内参txt文件,真值深度文件夹,伪深度图文件夹
Testing包含三种文件夹:1、color用于测试的图像文件夹;2、真值深度文件夹;3、用于动态区域评估的语义分割掩膜。
修改数据接口:
1、初始化
将**vars(args)都替换为args。
保存超参数

self.save_hyperparameters()

litemono对图像尺寸的要求是height=192,width=640,必须是32的整数倍?跟全连接层有关,含有全连接层的网络必须将图像resize为特定尺寸,比如yolov5就是要求为32的整数倍。
参考scv3将获取图像尺寸函数写到utils.py中
将加载伪真值取消默认使用。
数据预处理:SCV3使用一系列自定义的图像处理防止训练过拟合,litemono也有数据增强部分。

self.train_transform = custom_transforms.Compose([
    custom_transforms.RandomHorizontalFlip(),
    custom_transforms.RandomScaleCrop(),
    custom_transforms.RescaleTo(self.training_size),
    custom_transforms.ArrayToTensor(),
    custom_transforms.Normalize()]
)

SCV3使用了def __call__函数来执行custom_transforms.py中定义的类,按照列表中的顺序执行。
RandomHorizontalFlip:以0.5的概率随机水平翻转给定的numpy数组
RandomScaleCrop:随机将图像放大至15%,并将其裁剪为与以前相同的大小。
RescaleTo:重新缩放图像以进行训练或验证
ArrayToTensor:将numpy.ndarray(H x W x C)的列表以及内部矩阵转换为torch的列表。形状为(CxHxW)的具有本征张量的FloatTensor。
Normalize:将tensor中的每个元素减去self.mean,然后除以self.std。这是一个简单的批量标准化(Batch Normalization)操作,可以提高模型的性能和泛化能力。
使用scv3的数据预处理方法对数据预处理,这和transforms.Compose是一样的(不如自定义的灵活)

2、实例化数据集def setup(self, stage=None):
训练数据集的实例化

self.train_dataset = TrainFolder(
    self.hparams.hparams.dataset_dir,
    train=True,
    transform=self.train_transform,
    sequence_length=self.hparams.hparams.sequence_length,
    skip_frames=self.hparams.hparams.skip_frames,
    use_frame_index=self.hparams.hparams.use_frame_index,
    with_pseudo_depth=self.load_pseudo_depth
)

使用了类TrainFolder,继承于data.Dataset
需要参数包含:1、数据集路径;2、训练标识(真);3、图像变换器;3、用于训练的图像序列长度=3;4、跳过帧数为1;5、数据集的名称;6、使用帧索引;7、使用伪真值深度。
固定随机种子为0;
找到train.txt文件,并打开,循环文件中对应的数据名称,形成一个新的路径读取到一个列表self.scenes中。
将其他参数实例化
调用crawl_folders函数:1、循环self.scenes中的数据;2、将scene下cam.txt的内参转为矩阵;3、对图像进行排序imgs = sorted(scene.files('*.jpg')) ;4、如果使用了use_frame_index,则按照frame_index对图像再次排序imgs = [imgs[d] for d in frame_index] ;5、如果使用伪真值则对伪真值图像按frame_index进行排序pseudo_depths = [pseudo_depths[d] for d in frame_index] ;6、调用generate_sample_index生成采样的索引(根据跳过帧和序列长,计算demi_length为1,生成一个shifts偏移列表范围是(-1,2,1),移除shifts中索引为1的元素,也就是2,然后从第二个图像循环到倒数第二个停止,作为目标图像,参考图像为目标图像的前一帧和后一帧–
这不是跟litemono的[0,-1,1]类似么,咋整得这么复杂?
sample_index_list是这个样子:[{'tgt_idx': i, 'ref_idx': [i-1,i+1]},{'tgt_idx': i+1, 'ref_idx': [i+1-1,i+1+1]},{},{},……] 也就是说列表里的每一个元素就是一个字典。这个字典里放了一个目标图像索引,和两个参考图像的索引。

for i in range(demi_length * k, num_frames-demi_length * k):
    sample_index = {'tgt_idx': i, 'ref_idx': []}
    for j in shifts:
        sample_index['ref_idx'].append(i+j)
    sample_index_list.append(sample_index)

);7、循环采样图像索引for sample_index in sample_index_list::其中sample_index 为sample_index_list中的每一个字典,通过该字典将图像中对应位置的目标图像,参考图像,伪真值及相机内参给sample,sample是一个字典,并将该字典作为sequence_set列表的一个元素添加。并将该列表给self.samples。
到这里TrainFolder的实例化过程结束但后续使用中可通过输入索引值直接返回目标图像和参考图像。还有__getitem__
有def getitem:原来在python中__getitem__(self,key)方法被称为魔法方法:目的:如果给类定义了__getitem__方法,则当按照键取值时,可以直接返回__getitem__方法执行的结果
输入索引值后, __getitem__会读取该索引对应的一个目标图像和两个参考图像以及一个伪深度真值,并通过transform函数进行处理,再返回处理后的各个图像和内参。

评估数据集的实例化

self.val_dataset = ValidationSet(
    self.hparams.hparams.dataset_dir,
    transform=self.valid_transform,
    dataset=self.hparams.hparams.dataset_name
)

该函数先找到val.txt,并将txt文件中的所有场景数据生成一个新的列表;实例化图像变换器,数据集名称,执行crawl_folders函数。
crawl_folders函数:遍历数据列表中的每一个数据文件夹;对文件夹中的图像jpg格式进行排序并读取current_imgs;判断数据集的名字,‘nyu’, ‘bonn’, 'tum’这三种将png格式的深度图读取,‘ddad’, 'kitti’这两种将npz格式的数据读取;将读取的数据放入列表中 imgs.extend(current_imgs) depths.extend(current_depth) 循环完后返回。
__getitem__函数 读取imgs中对应索引的图像和depths深度值,并对图像进行transform处理。

接口类完成,注意接下来生成模型类——很多参数要跟接口类一致。

模型接口model_interface.py

先了解litemono的模型接口

1、定义了深度估计和位姿估计模型的空字典

self.models = {}
self.models_pose = {}

训练深度模型和位姿模型的参数列表

self.parameters_to_train = []
self.parameters_to_train_pose = []

2、指定网络训练的平台 #在pl中就不需要了

self.device = torch.device("cpu" if self.opt.no_cuda else "cuda")

3、尺度的数量–3,帧的id序号列表[0,-1,1] —3,给位姿网络的帧数–2

self.num_scales = len(self.opt.scales)
self.frame_ids = len(self.opt.frame_ids)
self.num_pose_frames = 2 if self.opt.pose_model_input == "pairs" else self.num_input_frames

4、深度估计模型设置
models字典中的键encoder 对应的值为深度编码器类,需要确定训练的模型名称(lite-mono,lite-mono-small,lite-mono-tiny,lite-mono-8m),每个训练步骤中以一定的概率随机“丢弃”网络中的某些路径,将其输出设置为零=0.2,图像宽度和图像高度。

self.models["encoder"] = networks.LiteMono(model=self.opt.model,
                                                   drop_path_rate=self.opt.drop_path,
                                                   width=self.opt.width, height=self.opt.height)

编码器放到cuda上

self.models["encoder"].to(self.device)

加载编码器需要的参数:nn.Module.parameters() 获取模型的参数,优化器中会用到。

self.parameters_to_train += list(self.models["encoder"].parameters())

深度解码器depth,num_ch_enc为Encoder模型中各个层输出的特征图的通道数,尺度列表

self.models["depth"] = networks.DepthDecoder(self.models["encoder"].num_ch_enc,
                                             self.opt.scales)

将解码器放到设备上并将网络参数放入parameters_to_train 列表中

self.parameters_to_train += list(self.models["depth"].parameters())

5、位姿估计模型设置
位姿模型的编码器用的是resnetencoder ,需要设置网络层数-18,权重初始化为预训练的,输入的图像为2张

self.models_pose["pose_encoder"] = networks.ResnetEncoder(
    self.opt.num_layers,
    self.opt.weights_init == "pretrained",
    num_input_images=self.num_pose_frames)

将编码器放到gpu上运行并获取编码器的模型参数

self.models_pose["pose_encoder"].to(self.device)
self.parameters_to_train_pose += list(self.models_pose["pose_encoder"].parameters())

位姿模型的解码器,输入参数:编码器的的通道数量,输入的特征–1,要预测的帧的数量–2

self.models_pose["pose"] = networks.PoseDecoder(
    self.models_pose["pose_encoder"].num_ch_enc,
    num_input_features=1,
    num_frames_to_predict_for=2)

将模型放到gpu上并获取解码器参数

self.models_pose["pose"].to(self.device)
self.parameters_to_train_pose += list(self.models_pose["pose"].parameters())

6、掩膜预测模型设置
使用的是深度估计解码器,但是将输出通道变为2个?同样也获取其模型参数

self.models["predictive_mask"] = networks.DepthDecoder(
    self.models["encoder"].num_ch_enc, self.opt.scales,
    num_output_channels=(len(self.opt.frame_ids) - 1))
self.models["predictive_mask"].to(self.device)
self.parameters_to_train += list(self.models["predictive_mask"].parameters())

7、设置优化器参数
深度估计优化器 学习率是0.0001,weight_decay是 1e-2

self.model_optimizer = optim.AdamW(self.parameters_to_train, self.opt.lr[0], weight_decay=self.opt.weight_decay)

位姿估计优化器 学习率是0.0001 weight_decay是 1e-2

self.model_pose_optimizer = optim.AdamW(self.parameters_to_train_pose, self.opt.lr[3], weight_decay=self.opt.weight_decay)

8、学习率调度器 ChainedScheduler (按照顺序调用多个串联起来的学习率调整策略,不同的是ChainedScheduler里面的学习率变化是连续的。),我觉得这个可以保留下来
需要下载额外的解压后这个文件夹放到创建的虚拟环境的site-packages里。

self.model_lr_scheduler = ChainedScheduler(
                    self.model_optimizer, #torch.optim库中的任何优化器
                    T_0=int(self.opt.lr[2]), #(int)第一个循环步长,第一次重新启动的迭代次数。
                    T_mul=1, #乘法系数默认值:-1。,重新启动后系数会增加T_i
                    eta_min=self.opt.lr[1], #最低学习率。默认值:0.001。
                    last_epoch=-1,  #最后一个训练周期(epoch)的索引 默认是-1
                    max_lr=self.opt.lr[0], #热身的最大学习率。默认值:0.1。
                    warmup_steps=0, #线性热身步长。默认值:0。达到max_lr所需的迭代次数
                    gamma=0.9 #衰减因子值介于0-1之间(1.0无衰减)
                )

9、加载权重文件 self.load_model()
但是在训练命令行中未出现load_weights_folder
首先获取到这个权重所在文件夹,然后循环models_to_load中的模型名称"encoder", “depth”, “pose_encoder”, “pose”

for n in self.opt.models_to_load

注意这些都是pth文件 : “encoder.pth”, “depth.pth”, “pose_encoder.pth”, “pose.pth”
如果是"pose_encoder.pth"和"pose.pth":
n是上面的各个网络模型

model_dict = self.models_pose[n].state_dict() #存储了网络结构的名字和对应的参数
pretrained_dict = torch.load(path) #加载所在的路径中预训练的模型参数文件 如pose.pth等
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict} #这是一个字典推导式,用于筛选pretrained_dict中的键。#它只保留那些同时存在于pretrained_dict和model_dict中的键-值对。换句话说,它确保只加载与当前模型结构相匹配的预训练参数。
model_dict.update(pretrained_dict) #将筛选后的预训练参数(即pretrained_dict)更新到当前模型的参数(即model_dict)中
self.models_pose[n].load_state_dict(model_dict) #将更新后的model_dict加载回self.models_pose[n]模型中

深度估计也是同理
加载优化器的状态

optimizer_load_path = os.path.join(self.opt.load_weights_folder, "adam.pth")
optimizer_pose_load_path = os.path.join(self.opt.load_weights_folder, "adam_pose.pth")

将状态放到optimizer_dict 等中,在更新到优化器里

optimizer_dict = torch.load(optimizer_load_path)
optimizer_pose_dict = torch.load(optimizer_pose_load_path)
self.model_optimizer.load_state_dict(optimizer_dict)
self.model_pose_optimizer.load_state_dict(optimizer_pose_dict)

10、加载预训练self.load_pretrain() ,在load_model()函数中加载的是litemono训练后的权重,不过与load_model()不同的是,训练命令行指定了mypretrain 这个参数。这个是在imagenet上训练后的编码器权重。

self.opt.mypretrain = os.path.expanduser(self.opt.mypretrain)
path = self.opt.mypretrain
model_dict = self.models["encoder"].state_dict()
pretrained_dict = torch.load(path)['model']
pretrained_dict = {k: v for k, v in pretrained_dict.items() if (k in model_dict and not k.startswith('norm'))}
model_dict.update(pretrained_dict)
self.models["encoder"].load_state_dict(model_dict)

11、这个在后面训练中会用到,就是将深度图转为点云,将点云转为深度图。从三个尺度生成

self.backproject_depth = {}
self.project_3d = {}
for scale in self.opt.scales:
    h = self.opt.height // (2 ** scale)
    w = self.opt.width // (2 ** scale)
    self.backproject_depth[scale] = BackprojectDepth(self.opt.batch_size, h, w)
    self.backproject_depth[scale].to(self.device)
    self.project_3d[scale] = Project3D(self.opt.batch_size, h, w)
    self.project_3d[scale].to(self.device)

到这里litemono训练的初始化就结束了,接下来会执行def train(self):函数进行具体的训练,万里长征第一步啊。

litemono进行训练

1、def train(self)训练函数 根据设置的epoch次数循环,执行run_epoch()函数,每次执行完成后保存模型

for self.epoch in range(self.opt.num_epochs):
    self.run_epoch()
    if (self.epoch + 1) % self.opt.save_frequency == 0:
        self.save_model()

2、run_epoch() 对一个epoch中的数据进行处理
首先将模型设置为训练模式,启动学习率调度器的步长,每一个epoch启动一次

self.set_train()
self.model_lr_scheduler.step()
self.model_pose_lr_scheduler.step()

2.1、从train_lodar中加载数据 for batch_idx, inputs in enumerate(self.train_loader): 将数据输入process_batch并返回输出和损失

outputs, losses = self.process_batch(inputs)

**process_batch:**将输入项中的所有数据放到gpu上,根据位姿网络的模型–separate_resnet 只将图片id为0的喂给深度编码器,再调用深度解码器获得输出

features = self.models["encoder"](inputs["color_aug", 0, 0])
outputs = self.models["depth"](features)

接着预测掩膜,这里可以用scv3的动态掩膜替换反正输出是掩膜数据就行

outputs["predictive_mask"] = self.models["predictive_mask"](features)

将预测的位姿也更新到输出里,对于predict_poses 函数:1、首先生成一个字典 键为帧id(0 -1 1),每个id对应一个图像;2、生成一个位姿输入图像列表[-1 0]以及 [0 1] pose_inputs = [pose_feats[f_i], pose_feats[0]] 3、将列表中的图像张量按照列方向进行拼接并传入位姿编码器 pose_inputs = [self.models_pose["pose_encoder"](torch.cat(pose_inputs, 1))] 4、用位姿解码器输出预测的欧拉角和平移axisangle, translation = self.models_pose["pose"](pose_inputs) 5、通过transformation_from_parameters函数 将结果整合为一个4*4的矩阵。

outputs.update(self.predict_poses(inputs, features))

生成扭曲(重新投影)的彩色图像并保存到“输出”字典中:

self.generate_images_pred(inputs, outputs)

1、循环三个尺度;2、获取当前视差,将当前尺度给源尺度 source_scale = scale ;3、使用disp_to_depth(disp, self.opt.min_depth, self.opt.max_depth)函数获取 尺度视差和深度(将网络的sigmoid输出转为深度预测?什么操作);4、将深度值给对应的尺度outputs[("depth", 0, scale)] = depth 5、循环目标图像的参考图像,取出对每个参考图像的位姿变换阵T = outputs[("cam_T_cam", 0, frame_id)] 6、计算的深度图的点云并再计算该点云的深度图;7、将点云的深度图给sample这个键 outputs[("sample", frame_id, scale)] = pix_coords ;8、使用grid_sample进行双线性插值 将输入inputs[("color", frame_id, source_scale)] warp 到 outputs[("color", frame_id, scale)]

cam_points = self.backproject_depth[source_scale](
    depth, inputs[("inv_K", source_scale)])
pix_coords = self.project_3d[source_scale](
    cam_points, inputs[("K", source_scale)], T)

计算损失compute_losses(self, inputs, outputs):1、循环3个尺度for scale in self.opt.scales:;2、获得该尺度下对应的图像disp = outputs[("disp", scale)] color = inputs[("color", 0, scale)] target = inputs[("color", 0, source_scale)] ;3、循环参考帧求重投影损失pred = outputs[("color", frame_id, scale)] reprojection_losses.append(self.compute_reprojection_loss(pred, target)) --目标图像与预测图像(参考图像warp后的图)的直接作差,再求均值(重投影损失),再将重投影损失拼接起来;4、将预测掩膜在scale下的视差作为因子与上面的重投影损失相乘;5、设置掩膜的损失为weighting_loss = 0.2 * nn.BCELoss()(mask, torch.ones(mask.shape).cuda()) 求均值后加到损失loss中 ;6、重投影损失求均值给combined中,并求取张量中的最小值和对应索引to_optimise, idxs = torch.min(combined, dim=1) ,其均值也加入loss中 ;7、对视差图disp,沿着高度和宽度求均值,再求出disp的归一化值 再求平滑损失get_smooth_loss(norm_disp, color) :对视差图和图像进行边缘提取并计算 grad_disp_x *= torch.exp(-grad_img_x) grad_disp_y *= torch.exp(-grad_img_y) 将两者均值相加就是平滑损失smooth_loss 再将平滑损失乘以对应的权重除以 缩放因子 ,将结过加到loss中 ,在三个尺度循环完成后求均值就是最后的损失losses–是个字典

2.2 清空上一批次的梯度值并进行反向传播

self.model_optimizer.zero_grad()
self.model_pose_optimizer.zero_grad()
losses["loss"].backward()

2.3 执行优化器进行参数更新

self.model_optimizer.step()
self.model_pose_optimizer.step()

2.4 将log信息进行终端打印

self.log_time(batch_idx, duration, losses["loss"].cpu().data)

2.5 计算深度真值误差(这不是特别准确,因为它是整个批次的平均值,因此仅用于指示验证性能)
最后返回 abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3
compute_depth_losses:利用真值生成掩膜,处理后作用与真值和预测值,再调用 compute_depth_errors计算上述的 abs_rel, sq_rel, rmse, rmse_log, a1, a2, a3指标
2.6 将事件写到tensorboard events file中

self.log("train", inputs, outputs, losses)

2.7 执行val()操作 验证模型在单独一个minibatch
一个批次训练完后就执行,设置为验证模式后 执行 self.process_batch(inputs)和self.compute_depth_losses(inputs, outputs, losses) 求出输出结构和损失,真值损失,再记录到tensorboard 中,再设置为训练模型开始下一个batch的训练。

3、每个epoch的保存网络模型self.save_model()
设置权重保存的文件夹
将对应的模型名字保存为对应的pth文件,包括优化器–深度估计优化器和位姿估计优化器

save_path = os.path.join(save_folder, "{}.pth".format(model_name))
to_save = model.state_dict()
torch.save(to_save, save_path)

改写litemono模型接口为pl

1、初始化部分改写
将litemono的初始化部分写到 pl模型接口中的def load_model(self):
忽略了关于self.device = torch.device的部分
代码中 在frame_ids列表后面添加一个字符 s 表示使用立体对进行训练 后面应该不会用到,因此先忽略
网络的参数获取,在pl里是在优化器配置中使用,litemono是将所有深度相关的参数组成一个列表放到优化器中优化,和litemono保持一致吧。
忽略预测预测掩膜,将scv3的动态掩膜加进来
在加载权重文件那里,优化器的权重应该放到函数def configure_optimizers(self): 这里。
将深度图与点云互相转换的部分在def load_model(self):函数中实例化后,初始化部分便结束了。需要注意的是:有关 torch.device和预测掩膜的我都忽略了–pl会帮我完成torch.device这部分工作,预测掩膜我准备用scv3的动态掩膜替换。
改整个模型接口,维护pl的可读性
1、在初始化部分实例化封装后的深度估计网络和位姿网络

self.depthnet = depthnet.DepthNet(hparams)
self.posenet = posenet.PoseNet(hparams)

2、首先是DepthNet
初始部分获得编码器和解码器
在预训练权重加载部分,pl也做了自动化处理,在main中加载整个系统的权重,也就是说可以舍弃litemono中对各个编码器的单独加载?
model.load_from_checkpoint是否可以加载多个单独的权重,并对应到相应的网络中?是不是可以并存啊,整个系统保持一个,然后单独的网络模块保存一个?
nn.module 有一个进行权重初始化的函数def init_weights(self):
将权重加载写到这里
并通过遍历键的方法实现权重加载
前向传播:深度估计的编码器输入是,目标图像。
3、其次是PoseNet
初始化部分获得位姿估计的编码器和解码器,并判断是否需要初始化权重
前向传播:将前向传播的输入参数添加一个逆向,以进行上一帧与当前帧的特殊处理。
2、训练部分改写
pl应该不需要再设置为训练模型这个操作了.train()
training_step(self, batch, batch_idx)每一个batch调用一次
直接从batch中获取数据,其中batch_idx一般是在log的时候用到
litemono中的学习率调度器step 是自动调用么? 忽略self.model_lr_scheduler.step()等
不用自己.step()。它也被Trainer自动处理了。
使用batch 将需要的图像数据给self,再调用self.process_batch()处理
顺便把之前的注释补一下
调用深度估计网络获得目标图像 预测的深度和掩膜。
调用位姿估计网络获得目标图像到两个参考图像的位姿
使用字典将上面的返回值封装
下一步应该执行generate_images_pred,该函数详细分析:
函数的接口数据:inputs 和 outputs :inputs 要包含inv_K和K两个键值对,并且这个键值对是有尺度变化的,还要包含color 键值对 --最原始的图像。
outputs 要包含disp depth cam_T_cam sample color color_identity等键值对,
首先disp = outputs[(“disp”, scale)],这个disp是从哪来的?在depth_decoder.py的前向传播中。cam_T_cam 来自位姿估计那部分,depth 、sample 、color 、color_identity是在generate_images_pred中新生成的。
直接在模型接口文件里定义这个函数吧,其他位置确实比较别扭
其中在posenet前向传播那里修改对应尺度的相机外参
将接口改为只有outputs,因为inv_K是相机内参的变换 在litemono的kitti_dataset.py KITTIDataset类初始化中定义,将变换移植到train_folder.py中。intrinsics是一个3*3的阵,

计算损失项:在模型接口文件定义def compute_losses(self, inputs, outputs):
该函数以及generate_images_pred中:均出现 `inputs[(“color”, 0, scale)] 因此需要对 所有的目标图像,参考图像进行三个尺度的缩放,在train_folder.py的__getitem__中设置。
在计算损失函数部分,默认使用预测源码,并忽略对shape[1]的判断。
到此litemono的主要部分已经改写完成了,下面将动态损失添加进来

首先是动态掩膜 self.dyna_mask()
因为动态掩膜是和光度损失,几何损失一起求解的,一些参数动态掩膜不一定能用上,采用倒推法梳理动态掩膜:
1、最终的动态掩膜是通过拼接求均值得到的

dynamic_mask = torch.cat(dynamic_mask, dim=1).mean(dim=1, keepdim=True)

2、所需要的掩膜样本通过步长为2循环diff_depth_list获取

for i in range(0, len(diff_depth_list), 2):
    tmp = diff_depth_list[i]
    tmp[valid_mask_list[i] < 1] = 0
    dynamic_mask += [1-tmp]

3、diff_depth_list和valid_mask_list则是两个临时量列表获取

diff_depth_list += [diff_depth_tmp1, diff_depth_tmp2]
valid_mask_list += [valid_mask_tmp1, valid_mask_tmp2]

4、两个临时量需要计算compute_pairwise_loss函数求取

diff_depth_tmp1, valid_mask_tmp1 = compute_pairwise_loss(
            tgt_img, ref_img, tgt_depth,
            ref_depth, pose, intrinsics,
            hparams
        )
diff_depth_tmp2, valid_mask_tmp2 = compute_pairwise_loss(
            ref_img, tgt_img, ref_depth,
            tgt_depth, pose_inv, intrinsics,
            hparams
        )

5、在函数compute_pairwise_loss中,diff_depth对应diff_depth_tmp1,valid_mask对应valid_mask_tmp1

diff_depth = (computed_depth-projected_depth).abs() / \
    (computed_depth+projected_depth)
valid_mask_ref = (ref_img_warped.abs().mean(
        dim=1, keepdim=True) > 1e-3).float()
valid_mask_tgt = (tgt_img.abs().mean(dim=1, keepdim=True) > 1e-3).float()
valid_mask = valid_mask_tgt * valid_mask_ref

6、这上面的变量均需要通过inverse_warp函数求得,这需要位姿,应当将位姿网络跟深度网络放在一起初始化

ref_img_warped, projected_depth, computed_depth = inverse_warp(
    ref_img, tgt_depth, ref_depth, pose, intrinsic, padding_mode='zeros')

现在从inverse_warp函数往前改写
改写完后有一个问题:动态掩膜需要变量tgt_img, ref_imgs, tgt_depth, ref_depths, intrinsics, poses, poses_inv
其中目标图像深度和参考图像深度需要计算出来;并且还要计算目标图像到参考图像的位姿:通过修改outputs字典的方法实现。
获得动态掩膜之后:
1、计算法向量损失

loss_normal = (tgt_normal-tgt_pseudo_normal).abs().mean()

2、掩码排序损失

loss_mask_ranking = loss_functions.mask_ranking_loss(self.outputs[("depth", 0, scale)], self.inputs[("tgt_pseudo_depth",scale)], dyna_mask)

3、法向量排序损失

loss_normal_ranking = loss_functions.normal_ranking_loss(self.inputs[("tgt_pseudo_depth",scale)], self.inputs[("color_aug"), 0, scale], tgt_normal, tgt_pseudo_normal)

之后将验证项补充完整即可

3、模型权重保存
使用training_epoch_end函数在每个epoch结束时进行保存
按照litemono 的方法保存文件。

到这里整个框架的训练所需应该都完成了。

4、下面进行测试代码编写。

重点函数为:def test_simple(args):
1、确定权重文件夹不是空的
2、加载预训练完的深度编码器和解码器权重

encoder_path = os.path.join(args.load_weights_folder, "encoder.pth")
decoder_path = os.path.join(args.load_weights_folder, "depth.pth")
encoder_dict = torch.load(encoder_path)
decoder_dict = torch.load(decoder_path)

3、从训练的权重中获得图像的高和宽
feed_height = encoder_dict[‘height’]
feed_width = encoder_dict[‘width’]
4、加载模型并赋值预训练权重,为什么要设为评估模式?

encoder = networks.LiteMono(model=args.model,
                              height=feed_height,
                              width=feed_width)
model_dict = encoder.state_dict()
encoder.load_state_dict({k: v for k, v in encoder_dict.items() if k in model_dict})
encoder.to(device)
encoder.eval()

depth_decoder = networks.DepthDecoder(encoder.num_ch_enc, scales=range(3))
depth_model_dict = depth_decoder.state_dict()
depth_decoder.load_state_dict({k: v for k, v in decoder_dict.items() if k in depth_model_dict})
depth_decoder.to(device)
depth_decoder.eval()

5、是否根据txt文件中的图像进行训练
如果没有txt文件则仅测试单帧图像

paths = [args.image_path]
output_directory = os.path.dirname(args.image_path)

有txt文件,则将文件中每一个文件名保存至路径

for i in range(len(filenames)):
	filename = filenames[i]
	line = filename.split()
	folder = line[0]
	if len(line) == 3:
		frame_index = int(line[1])
		side = line[2]

	f_str = "{:010d}{}".format(frame_index, '.jpg')
	image_path = os.path.join(
		'kitti_data',
		folder,
		"image_0{}/data".format(side_map[side]),
		f_str)
	paths.append(image_path)

6、判断是否有图像文件夹路径

paths = glob.glob(os.path.join(args.image_path, '*.{}'.format(args.ext)))
output_directory = args.image_path

7、轮次预测每一个图像
无梯度模式:with torch.no_grad():
获得图像路径中的每个图像索引和图像位置:for idx, image_path in enumerate(paths):
打开这个位置的图像并转化为RGB:input_image = pil.open(image_path).convert('RGB')
获得输入图像的大小:original_width, original_height = input_image.size
将输入图像的尺寸变为网络要求的:input_image = input_image.resize((feed_width, feed_height), pil.LANCZOS)
将图像转换为张量形式:input_image = transforms.ToTensor()(input_image).unsqueeze(0)
对图像进行预测,获得的是视差图:

input_image = input_image.to(device)
features = encoder(input_image)
outputs = depth_decoder(features)

对预测视差图进行插值变为原来图像的大小:

disp_resized = torch.nn.functional.interpolate(
    disp, (original_height, original_width), mode="bilinear", align_corners=False)

视差图的输出名设置:

output_name = os.path.splitext(os.path.basename(image_path))[0]

获得视差图的深度

scaled_disp, depth = disp_to_depth(disp, 0.1, 100)

保存尺度视差图:深度图的倒数

name_dest_npy = os.path.join(output_directory, "{}_disp.npy".format(output_name))
np.save(name_dest_npy, scaled_disp.cpu().numpy())

将预测视差图由张量转为numpy数组,并将低于95%分位的数值不被包含在内

disp_resized_np = disp_resized.squeeze().cpu().numpy()
vmax = np.percentile(disp_resized_np, 95)

设置归一化器,归一化的范围为vmin=disp_resized_np.min(), vmax=vmax

normalizer = mpl.colors.Normalize(vmin=disp_resized_np.min(), vmax=vmax)

利用matplotlib中的cm模块中的cm.ScalarMappable(norm=None, cmap=None),用norm来规范化数据,cmap为数据找到对应的颜色,得到可映射标量ScalarMappable对象,,做图的时候添加参数c=sm.to_rgba(x)(其中rgba中的rgb是指RGB色彩理论,a是指alpha表示颜色的透亮度)即可。
在这里插入图片描述

mapper = cm.ScalarMappable(norm=normalizer, cmap='magma')

获得热力图效果

colormapped_im = (mapper.to_rgba(disp_resized_np)[:, :, :3] * 255).astype(np.uint8)

[:, :, :3]是移除透明度通道

将数组转为PIL图像并保存到name_dest_im文件中_disp.jpeg格式:

im = pil.fromarray(colormapped_im)
name_dest_im = os.path.join(output_directory, "{}_disp.jpeg".format(output_name))
im.save(name_dest_im)

测试完成。

确定训练命令行

使用脚本运行 ,完成命令行设置,完成预训练模型下载,模型图像尺寸:1024x320。
下载数据集,从scv3
之后配置环境和训练
接下来如何使用tensorboard:tensorboard --logdir ./tb_logs

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值