在 ICML 2015的论文 Unsupervised Domain Adaptation by Backpropagation (DANN)网络中提出了一个梯度翻转层(Gradient Reverse Layer,GRL),这个层是用来干什么的?

在训练阶段我们要做的是如下两个任务,第一个则是实现源域数据集准确分类,实现现图像分类误差的最小化;第二个任务则是要混淆源域数据集和目标域数据集,实现域分类误差的最大化,混淆目标域数据集与源域数据集。现在来看一下是怎么实现这两个任务的
上述模型中主要有3个部分:
- 绿色部分:是用来特征提取的
- 蓝色部分:对源域进行训练,这部分的标签是输入数据的真实标签,在测试的时候就是测试集的标签,得到的就是分类误差
- 红色部分:一个二分类器,这部分的标签是源域标签/目标域标签。如果输入为训练集(源域),那么这里就是测试集(目标域)的标签;如果输入为测试集(目标域),那么这里就是训练集(源域)的标签。这部分目的是使源域和目标域之间的分类损失最小
一、为什么要GRL
上述的蓝色部分对源域(训练集)数据进行训练,随着轮回次数(epoch)的增多,该部分会发生过拟合现象;而红色部分的目标是最大化二分类误差,从而将源域数据向目标域方向移动,目标域数据向源域方向移动,最完美的理想情况是让源域和目标域的数据能够落于同一个分布上。

为什么要移动呢?因为我们对训练集(源域)经过不断优化找到的分割线在目标域中表现很差,所以把这两个数据集往一块靠拢,等到两部分数据集并拢到一块的时候,在源域(训练集)上能够达到优秀分类的模型自然在测试集上也能达到优秀的效果。

原文链接:https://blog.csdn.net/qq_30091945/article/details/104478550
二、GRL的一些要点(调试必看)
这部分主要是记录别人在使用GRL的过程中总结出的一些要点,通过重点关注这些要点从而实现有的放矢。
- 验证分类器有效:在调试GRL前,先单独对分类器进行训练,确保网络能够进行分类;
- 验证GRL是否工作:在输出结果层(最后一层)后面加上GRL,令整个梯度都是反向的,并且将GRL参数设置为常数1,观察训练loss是否越来越大;
- 以上功能确保正常后,即可将GRL加到网络进行训练。理想的网络分类器损失先下降后增加的趋势,最后网络无法对其分类。
然而,实验中出现了分类器损失一直保持不变,既开始分类器一直学不到东西,通过修改学习率、优化器等皆无果,后面想起GRL还有一个参数,对其进行调整,网络终于开始能学着分类。所以很重要的是GRL的参数α(既反向梯度所乘系数),我采用的是原论文的系数设置,随着迭代次数的增加,α由0增加到1。但是在调试时,发现α设置为0.001是能满足前面第3点提到的趋势,大一点则损失不降,因此最后设置是α由0增加到0.002。所以不止分类器和GRL是否有效工作很重要,GRL的参数α也很重要,但发现分类器损失一动不动时,试着把α减小一个数量级在跑跑看。
题外话:在调试时,可以选择val set或者少量数据进行训练,这样更快能看出方法是否有效,而不是一整个数据集丢进去跑,太浪费时间了!毕竟迭代的次数多了,才能看得出网络损失的趋势。同时,在进行实验时,一定要对数据集和标签进行可视化(我做的是图像的目标检测),粗略的过一遍,确保数据集的处理没有问题。 做了好几个项目,还总是没有吸取教训,希望说的点能帮到大家。
原文链接:https://blog.csdn.net/qq_36552489/article/details/105234425
三、GRL的实现
这部分主要是使用PyTorch来实现的,定义了一个梯度翻转层,其中的backward表示的就是对梯度进行翻转。
定义梯度翻转层GRL
from torch.autograd import Functionclass ReverseLayerF(Function):@staticmethoddef forward(ctx, x, alpha):ctx.alpha = alphareturn x.view_as(x)@staticmethoddef backward(ctx, grad_output):output = grad_output.neg() * ctx.alphareturn output, None
使用定义好的梯度翻转层GRL
在上面定义好了以后,就要使用该层,在哪里使用,又怎么使用呢?
现在,我们回想整个过程,我们使用的GRL层在红色区域的前面,所以同样在代码中就在这个地方使用即可。

假设以下代码中的x表示绿色部分提取出的最后特征,则使用步骤如下所示
# 1.先将绿色部分最后的特征投入GRLreverse_x = ReverseLayerF.apply(x, alpha)# 2.之后的网络结构就是上述的红色部分的结构,这个自定义self.domain_classifier = nn.Linear(reverse_x.shape[1], 2)domain_output = self.domain_classifier(reverse_x)
四、参考PyTorch
上述只是给出了GRL实现的代码,并没有详细的解释为什么这么做,这个部分可以参考PyTorch的官网。
在PyTorch框架下,我们在进行反馈的时候都会简单地加上一句话model.backward(),这句代码中的backward()函数已经帮助我们计算了反馈所需要的梯度。而这么个简单的backward运算符实际上是两个在Tensors上操作的函数,就是我们上面定义的forward()和backward(),这两个函数位于torch.autograd.Function类中。
其中,forward()函数计算输入张量(tensor)的输出张量;backward()函数返回输出张量的梯度,这个梯度的大小和一个常数有关,这个常数就是forward()函数里的变量。
在PyTorch里,我们可以自定义forward()和backward()这两个函数,这样在使用model.backward()的时候调用的就是自己定义的forward和backward函数了。
在下面这个例子中,我们的模型为一个线性函数#card=math&code=y%20%3D%20a%2BbP_3%28c%2Bdx%29&id=tFho3),在给定的
区间范围内,对某个
值进行拟合,找到拟合后的
,
,
,
值。其中的
函数为
%3D%5Cfrac%7B1%7D%7B2%7D(5x%5E3-3x)#card=math&code=P_3%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%285x%5E3-3x%29&id=QCJN7),看一下这个例子中如何对
函数重写
forward和backward函数。
- 前向过程:
%3D%5Cfrac%7B1%7D%7B2%7D(5x%5E3-3x)#card=math&code=P_3%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%285x%5E3-3x%29&id=ZIxcF)
- 回退过程:
%3D%5Cfrac%7B1%7D%7B2%7D3(5x%5E2-1)%20%3D%201.5(5x%5E2-1)#card=math&code=P_3%27%28x%29%3D%5Cfrac%7B1%7D%7B2%7D%2A3%285x%5E2-1%29%20%3D%201.5%2A%285x%5E2-1%29&id=ugSPL)
# -*- coding: utf-8 -*-import torchimport mathclass CustomBackward(torch.autograd.Funciton):'''通过引入torch.autograd.Function帮助实现自顶forward和backward函数'''@staticmethoddef forward(ctx, input):'''在forward阶段,因为y=1/2(5x^3 - 3x),所以直接返回该值即可.为了方便在backward阶段使用变量,可以使用ctx缓存任意对象。'''ctx.save_for_backward(input)return 0.5*(5*input**3 - 3*input)@staticmethoddef backward(ctx, grad_output):'''在backward阶段,因为损失函数为链式推导的原因,我们得到了一个与输出的损失函数有关的梯度张量grad_output。现在,我们要根据链式求导法则,把这个梯度返还到forward阶段,使用的是P3'(x)'''input, = ctx.saved_tensorsreturn grad_output * 1.5 * (5*input**2 - 1)dtype = torch.floatdevice = torch.device("cpu")# 通过设定 requires_grad=False表示我们对某个变量自动计算梯度而不用手动计算x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)y = torch.sin(x)# 在上面的P3(x)中有4个需要拟合的变量即a, b, c和da = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)learning_rate = 5e-6for t in range(2000):# 在前向阶段就需要使用自定义的forward函数,通过apply函数调用P3 = LegendrePolynomial3.applyy_pred = a + b * P3(c + d * x)# 计算损失函数loss = (y_pred - y).pow(2).sum()if t % 100 == 99:print(t, loss.item())# 自动计算反馈回去的梯度loss.backward()# 更新参数a, b, c, dwith torch.no_grad():a -= learning_rate * a.gradb -= learning_rate * b.gradc -= learning_rate * c.gradd -= learning_rate * d.grad# 手动设置a, b, c, d的梯度为0a.grad = Noneb.grad = Nonec.grad = Noned.grad = Noneprint(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
参考链接:https://pytorch.org/tutorials/beginner/pytorch_with_examples.html#pytorch-defining-new-autograd-functions
五、GRL的使用
上面定义了一个GRL翻转层,这个层怎么使用呢?知乎上这位博主给了一个”hello, world”,内容如下
5.1定义GRL
from torch.autograd import Functionclass GRL(Function):@staticmethoddef forward(ctx, x, alpha):ctx.alpha = alphareturn x.view_as(x)@staticmethoddef backward(ctx, grad_output):output = grad_output.neg() * ctx.alphareturn output, None
5.2 使用该层
通过上面的论文我们知道,GRL是在特征提取层以后的,后接域分类器,所以整个网络的框架如下
class DANN(nn.Module):def __init__(self,num_classes=10):super(DANN,self).__init__()# 1.define the feature extraction layerself.features=nn.Sequential(nn.Conv2d(3,32,5),nn.ReLU(inplace=True),nn.MaxPool2d(2),nn.Conv2d(32,48,5),nn.ReLU(inplace=True),nn.MaxPool2d(2),)self.avgpool=nn.AdaptiveAvgPool2d((5,5))# 2.define the classification layerself.task_classifier=nn.Sequential(nn.Linear(48*5*5,100),nn.ReLU(inplace=True),nn.Linear(100,100),nn.ReLU(inplace=True),nn.Linear(100,num_classes))# 3.define the domain classification layerself.domain_classifier=nn.Sequential(nn.Linear(48*5*5,100),nn.ReLU(inplace=True),nn.Linear(100,2))self.GRL=GRL()def forward(self,x,alpha):x = x.expand(x.data.shape[0], 3, image_size,image_size)x=self.features(x)x=self.avgpool(x)x=torch.flatten(x,1)task_predict=self.task_classifier(x)x=GRL.apply(x,alpha)domain_predict=self.domain_classifier(x)return task_predict,domain_predict
