学习如何适应结构化输出空间进行语义分割-Learning to Adapt Structured Output Space for Semantic Segmentation
题目:在输出空间进行适应。
论文内容分析:
》提出通过对抗学习的进行像素级的语义分割的域适应方法。
》在输出空间的域适应可以有效地对齐源域和目标的场景布局和局部上下文。
作者的目的是:源域、目标域的图像的分割预测Ps,Pt更接近。在目标预测的对抗损失下,将判别器的梯度传递到生成器(分割模型),让生成器(分割模型)对目标域图像分割,生成与源域分割预测类似的目标域分割预测。
》提出多层次对抗学习来适应分割模型的不同层次的特征,改善性能。
参考深度监督网络【1】,在低层次特征空间中(low-level feature),引入对抗模块,从而保证分割网络对输入的目标域图像所提取的低层特征与源域类似。
作者所提出的方法由 分割网络 和 判别器组成。源域图像和目标域图像共享一个分割网络。
代码分析:
第5层卷积层,第6层卷积层的输入图像的大小相同,但是特征通道数量不同。5,6两层的卷积核的数量,种类相同,由4种不同空洞大小,填充参数的卷积(每种卷积的填充参数与其空洞大小参数相同),
最后将不同的卷积操作的结果的输出进行相加(如图3所示),提取到不同感受野的特征。
第5层卷积层的输入的特征通道数量为1024, 输出特征通道数量为2(分割的类数量),
第6层卷积层的输入的特征通道数量为2048, 输出特征通道数量为2(分割的类数量)。
2. 训练过程分析
for i_iter in range(args.num_steps):
loss_seg_value1 = 0
loss_adv_target_value1 = 0
loss_D_value1 = 0
loss_seg_value2 = 0
loss_adv_target_value2 = 0
loss_D_value2 = 0
optimizer.zero_grad()
adjust_learning_rate(optimizer, i_iter)
optimizer_D1.zero_grad()
optimizer_D2.zero_grad()
adjust_learning_rate_D(optimizer_D1, i_iter)
adjust_learning_rate_D(optimizer_D2, i_iter)
for sub_i in range(args.iter_size):# 梯度累加
# train G
# don't accumulate grads in D
for param in model_D1.parameters():
param.requires_grad = False
for param in model_D2.parameters():
param.requires_grad = False
# train with source
_, batch = trainloader_iter.next() # 加载输入源域图像,训练分割模型
images, labels, _, _ = batch
images = Variable(images).cuda(args.gpu)
pred1, pred2 = model(images)
pred1 = interp(pred1)
pred2 = interp(pred2)
loss_seg1 = loss_calc(pred1, labels, args.gpu)
loss_seg2 = loss_calc(pred2, labels, args.gpu)
loss = loss_seg2 + args.lambda_seg * loss_seg1
# proper normalization
loss = loss / args.iter_size
loss.backward()# 计算得到分割损失,反向传播,但是梯度不更新
loss_seg_value1 += loss_seg1.data.cpu().numpy()[0] / args.iter_size
loss_seg_value2 += loss_seg2.data.cpu().numpy()[0] / args.iter_size
# train with target
_, batch = targetloader_iter.next()# 加载目标域图像
images, _, _ = batch
images = Variable(images).cuda(args.gpu)
pred_target1, pred_target2 = model(images)
pred_target1 = interp_target(pred_target1) # 对model输出结果进行上采样
pred_target2 = interp_target(pred_target2)
D_out1 = model_D1(F.softmax(pred_target1))# 为什么对model的输出进行softmax函数计算?
D_out2 = model_D2(F.softmax(pred_target2))
loss_adv_target1 = bce_loss(D_out1,
Variable(torch.FloatTensor(D_out1.data.size()).fill_(source_label)).cuda(
args.gpu))
loss_adv_target2 = bce_loss(D_out2,
Variable(torch.FloatTensor(D_out2.data.size()).fill_(source_label)).cuda(
args.gpu))
loss = args.lambda_adv_target1 * loss_adv_target1 + args.lambda_adv_target2 * loss_adv_target2
loss = loss / args.iter_size
loss.backward()
loss_adv_target_value1 += loss_adv_target1.data.cpu().numpy()[0] / args.iter_size
loss_adv_target_value2 += loss_adv_target2.data.cpu().numpy()[0] / args.iter_size
# train D
#设置判别器的参数requires_grad=True, 可以对其参数进行求导
# bring back requires_grad
for param in model_D1.parameters():
param.requires_grad = True
for param in model_D2.parameters():
param.requires_grad = True
# train with source
pred1 = pred1.detach()
pred2 = pred2.detach()
D_out1 = model_D1(F.softmax(pred1))
D_out2 = model_D2(F.softmax(pred2))
loss_D1 = bce_loss(D_out1,
Variable(torch.FloatTensor(D_out1.data.size()).fill_(source_label)).cuda(args.gpu))
loss_D2 = bce_loss(D_out2,
Variable(torch.FloatTensor(D_out2.data.size()).fill_(source_label)).cuda(args.gpu))
loss_D1 = loss_D1 / args.iter_size / 2
loss_D2 = loss_D2 / args.iter_size / 2
loss_D1.backward()
loss_D2.backward()
loss_D_value1 += loss_D1.data.cpu().numpy()[0]
loss_D_value2 += loss_D2.data.cpu().numpy()[0]
# train with target
pred_target1 = pred_target1.detach()
pred_target2 = pred_target2.detach()
D_out1 = model_D1(F.softmax(pred_target1))
D_out2 = model_D2(F.softmax(pred_target2))
loss_D1 = bce_loss(D_out1,
Variable(torch.FloatTensor(D_out1.data.size()).fill_(target_label)).cuda(args.gpu))
loss_D2 = bce_loss(D_out2,
Variable(torch.FloatTensor(D_out2.data.size()).fill_(target_label)).cuda(args.gpu))
loss_D1 = loss_D1 / args.iter_size / 2
loss_D2 = loss_D2 / args.iter_size / 2
loss_D1.backward()
loss_D2.backward()
loss_D_value1 += loss_D1.data.cpu().numpy()[0]
loss_D_value2 += loss_D2.data.cpu().numpy()[0]
optimizer.step()
optimizer_D1.step()
optimizer_D2.step()
print('exp = {}'.format(args.snapshot_dir))
print(
'iter = {0:8d}/{1:8d}, loss_seg1 = {2:.3f} loss_seg2 = {3:.3f} loss_adv1 = {4:.3f}, loss_adv2 = {5:.3f} loss_D1 = {6:.3f} loss_D2 = {7:.3f}'.format(
i_iter, args.num_steps, loss_seg_value1, loss_seg_value2, loss_adv_target_value1, loss_adv_target_value2, loss_D_value1, loss_D_value2))
if i_iter >= args.num_steps_stop - 1:
print 'save model ...'
torch.save(model.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(args.num_steps_stop) + '.pth'))
torch.save(model_D1.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(args.num_steps_stop) + '_D1.pth'))
torch.save(model_D2.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(args.num_steps_stop) + '_D2.pth'))
break
if i_iter % args.save_pred_every == 0 and i_iter != 0:
print('taking snapshot ...')
torch.save(model.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(i_iter) + '.pth'))
torch.save(model_D1.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(i_iter) + '_D1.pth'))
torch.save(model_D2.state_dict(), osp.join(args.snapshot_dir, 'GTA5_' + str(i_iter) + '_D2.pth'))
通过分析发现,模型每次迭代一个epoch,并没有迭代所有的训练数据,而是一个epoch,模型迭代 args.iter_size 次,每次加载batch size 个数据;模型在迭代 args.iter_size 次之前 使用 optimizer.zero_grad(),optimizer_D1.zero_grad(),optimizer_D2.zero_grad() 对分割网络,判别器的的梯度进行清0;模型在迭代训练的时候,只进行损失函数的计算,没有进行反向传播,而是在迭代完args.iter_size之后,模型进行反向传播,为什么通过这种方式进行参数更新,以及模型的训练?其数学意义是什么?相关的解释:梯度累加[1][2]。
判别器训练。文章中z为0的时候,样本从目标域中采样得到;z=1,样本从源域采样。但是代码中判别器训练,目标域对应的z是1, 源域对应0 ? 为什么
3.对抗训练方式分析:
模型先训练分割网络,然后训练判别器。
分割网络训练阶段:
》1. 先对判别器的参数的 requires_grad 设置为False, 因此在优化分割网络,只优化判别器的参数。
》2.加载源域图像,以及对应的标签,训练分割网络,计算分割交叉熵损失函数,并反向传播;
》3. 加载目标域图像,输入到分割网络中,得到对每个类的预测概率图(segmentation softmax output)pred_target1,pred_target2;判别器利用如下损失函数进行函数计算,并反向传播。
4. 学习率调整方式
判别器的初始学习率:0.0001
分割网络的初始学习率:0.00025
学习调整方式:随着迭代次数增加,而下降。
def lr_poly(base_lr, iter, max_iter, power):
return base_lr * ((1 - float(iter) / max_iter) ** (power))
其值随着迭代次数增加的变化曲线
Github 有帮助的ISSUEs
1. https://github.com/wasidennis/AdaptSegNet/issues/88
在输出层次,仅使用单层LS-GAN域适应。
(2)关于Batch Normalizaiton 层参数的requires_grad = False。
https://github.com/wasidennis/AdaptSegNet/issues/79
(3)关于对抗损失函数
https://github.com/wasidennis/AdaptSegNet/issues/33
参考文献:
【1】https://towardsdatascience.com/what-is-gradient-accumulation-in-deep-learning-ec034122cfa
【2】https://www.cnblogs.com/lart/p/11628696.html