在 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 Function
class ReverseLayerF(Function):
@staticmethod
def forward(ctx, x, alpha):
ctx.alpha = alpha
return x.view_as(x)
@staticmethod
def backward(ctx, grad_output):
output = grad_output.neg() * ctx.alpha
return output, None
使用定义好的梯度翻转层GRL
在上面定义好了以后,就要使用该层,在哪里使用,又怎么使用呢?
现在,我们回想整个过程,我们使用的GRL层在红色区域的前面,所以同样在代码中就在这个地方使用即可。
假设以下代码中的x
表示绿色部分提取出的最后特征,则使用步骤如下所示
# 1.先将绿色部分最后的特征投入GRL
reverse_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 torch
import math
class CustomBackward(torch.autograd.Funciton):
'''
通过引入torch.autograd.Function帮助实现自顶forward和backward函数
'''
@staticmethod
def 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)
@staticmethod
def backward(ctx, grad_output):
'''
在backward阶段,因为损失函数为链式推导的原因,我们得到了一个与输
出的损失函数有关的梯度张量grad_output。
现在,我们要根据链式求导法则,把这个梯度返还到forward阶段,使用的是P3'(x)
'''
input, = ctx.saved_tensors
return grad_output * 1.5 * (5*input**2 - 1)
dtype = torch.float
device = 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和d
a = 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-6
for t in range(2000):
# 在前向阶段就需要使用自定义的forward函数,通过apply函数调用
P3 = LegendrePolynomial3.apply
y_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, d
with torch.no_grad():
a -= learning_rate * a.grad
b -= learning_rate * b.grad
c -= learning_rate * c.grad
d -= learning_rate * d.grad
# 手动设置a, b, c, d的梯度为0
a.grad = None
b.grad = None
c.grad = None
d.grad = None
print(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 Function
class GRL(Function):
@staticmethod
def forward(ctx, x, alpha):
ctx.alpha = alpha
return x.view_as(x)
@staticmethod
def backward(ctx, grad_output):
output = grad_output.neg() * ctx.alpha
return output, None
5.2 使用该层
通过上面的论文我们知道,GRL是在特征提取层以后的,后接域分类器,所以整个网络的框架如下
class DANN(nn.Module):
def __init__(self,num_classes=10):
super(DANN,self).__init__()
# 1.define the feature extraction layer
self.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 layer
self.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 layer
self.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