Unsupervised Anomaly Detection with Generative Adversarial Networks to Guide Marker Discovery
Intro
本文提出利用GAN来做异常检测,大致思想为,先使用正常方式训练生成器G,训练完成后固定G的参数,给定一张图片x,在latent space里去查询与之最匹配的z,认为如果能找到一个合适的匹配,那么输入x就应该服从训练生成图片的分布(近似认为是训练GAN时正常图片的分布),即为正常图片,否则为异常。基于此思想,作者设计了用于衡量匹配度的标准,用于区分正常样本和异常和样本。
Unsupervised Manifold Learning of Normal Anatomical Variability
这一部分就是正常的GAN的训练过程,与之前其他异常检测的工作一样,这里采用分patch训练的方式,将原图划分为k个patch,通过对抗训练将latent space采样的z映射为image space的图像。
常规的GAN训练loss:
Mapping new Images to the Latent Space
GAN学习到的是如何将latent space中的采样结果z映射到image space的图像,但是反过的映射却并没有学习到,因此一个简单的想法就是去查询与输入图像最接近的对应z。
也即给定输入x和一个已经经过训练的生成器G,找到一个z,使得G(z)和x的差异最小。
写成loss形式就是:
显然整个loss里只有一个参数z,可以通过对z求梯度进行迭代优化,不再赘述。上式在文章中称之为Residual Loss,因为是残差形式。
然而作者并没有只使用这一个loss去优化z,因为上式只使用了GAN在训练时的生成器,并没将判别器加以利用,因此添加了判别器部分的discrimination loss,认为之前训练的判别器也具备查询能力,显然如果判别器的输出概率值在输入图片x和G(z)之间的差异较小,也可以认为这个z就是我们要找的z。文章觉得直接匹配输出层的特征要更加合适,于是并不是直接使用的判别器的概率输出,而是输出特征。
这里f表示判别器特征提取函数。
因此,总的loss就可以写为:
对z求梯度优化即可。
Detection of Anomalies
上面的loss可以作为评判样本是否异常的标准,显然loss越小,说明在训练中见过相似的图片,即输入与训练样本同分布,所以判定为正常样本,否则判定为异常样本。
即将loss改写为下式:
通过残差部分(R(x) = |x - G(z_T)|)可以得到异常区域,即差异的亮度可以表示异常区域和异常成都,而判别部分D(x)则可以反映置信水平。
效果见图:
Coding
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
class G(nn.Module):
def __init__(self):
super(G,self).__init__()
self.layer1 = nn.Sequential(
nn.Linear(100,7*7*512),
nn.BatchNorm1d(7*7*512),
nn.ReLU(),
)
self.layer2 = nn.Sequential(
nn.ConvTranspose2d(512,256,3,2,1,1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.ConvTranspose2d(256,128,3,1,1),
nn.BatchNorm2d(128),
nn.LeakyReLU(),
)
self.layer3 = nn.Sequential(
nn.ConvTranspose2d(128,64,3,1,1),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.ConvTranspose2d(64,1,3,2,1,1),
nn.Tanh()
)
def forward(self,z):
out = self.layer1(z)
out = out.view(out.size()[0],512,7,7)
out = self.layer2(out)
out = self.layer3(out)
return out
class D(nn.Module):
def __init__(self):
super(D,self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(1,8,3,padding=1), # batch x 16 x 28 x 28
nn.BatchNorm2d(8),
nn.LeakyReLU(),
nn.Conv2d(8,16,3,stride=2,padding=1), # batch x 32 x 28 x 28
nn.BatchNorm2d(16),
nn.LeakyReLU(),
#('max1',nn.MaxPool2d(2,2)) # batch x 32 x 14 x 14
)
self.layer2 = nn.Sequential(
nn.Conv2d(16,32,3,stride=2,padding=1), # batch x 64 x 14 x 14
nn.BatchNorm2d(32),
nn.LeakyReLU(),
#nn.MaxPool2d(2,2),
nn.Conv2d(32,64,3,padding=1), # batch x 128 x 7 x 7
nn.BatchNorm2d(64),
nn.LeakyReLU()
)
self.fc = nn.Sequential(
nn.Linear(64*7*7,1),
nn.Sigmoid()
)
def forward(self,x):
out = self.layer1(x)
out = self.layer2(out)
out = out.view(out.size()[0], -1)
feature = out
out = self.fc(out)
return out,feature
class AnoGAN(nn.Module):
def __init__(self,la = 0.1):
super(AnoGAN,self).__init__()
self.la = la
self.G = G()
self.D = D()
def forward(self,x,z):
fake_x = self.G(z)
L_R = F.l1_loss(fake_x,x)
_,f_x = self.D(x)
_,f_fake_x = self.D(fake_x)
L_D = F.l1_loss(f_fake_x,f_x)
return (1-self.la) * L_R + self.la * L_D
if __name__ == "__main__":
anogan = AnoGAN()
#x = torch.randn(2,1,28,28)
z = Variable(torch.nn.init.normal(torch.zeros(2,100),mean=0,std=0.1),requires_grad=True)
#print(anogan(x,z))
optimizer = torch.optim.Adam([z],lr=1e-3)
datas = [torch.randn(2,1,28,28)]
for epoch in range(2):
for x in datas:
optimizer.zero_grad()
loss = anogan(x,z)
print(loss)
loss.backward()
optimizer.step()