# Deep-One-Class-Classification论文解读及代码分析
# 背景
神经网络在多类别数据上取得了不错的成果,如图像分类,目标检测,图像分割中,但面对实际中的应用,往往面对训练数据少,缺少某个类别数据的挑战。考虑如医学图像的辅助诊断,有可能只能提供健康类别的图像,但要算法识别出异常的图像,这时应用神经网络做分类,面对单个类别该如何训练呢?实际生活中存在大量的此种应用场景,如异常检测,网络入侵检测,欺诈检测等。
# 1.简介
这篇论文是德国柏林洪堡大学的Lukas Ruff
发表在ICML2018
上的工作。柏林洪堡大学已诞生57
位诺贝尔奖获得者,实力惊人。这篇论文主要的工作是提出了Deep Support Vector Data Description
(Deep SVDD),该方法可以直接基于异常目标函数对单类别数据进行训练,区别于以往的先在大规模数据集训练再在单类别数据集上微调的做法。
如异常检测,正常的数据服从高斯分布,异常的数据不服从高斯分布,模型训练的目标就是准确表征“正常”,与模型偏差较大的即异常点。只有正常类别的数据做训练,因此也可以看做单分类问题One-Class Classification
。
对于图像数据,其维度通常较大,传统的单分类方法如One-Class SVM
和Kernel Density Estimation
很难取得理想的效果,且还需要先对数据做大量的特征工程的工作。
Deep SVDD
通过寻找一个最小球面,使该最小封闭球面包围数据的网络表征, 来训练一个神经网络模型。最小化训练数据表征的封闭球面可以使网络提取出变化数据中的共同特征。
# 2.相关工作
# 2.1基于核的单分类方法
训练数据
PSD kernel
),RKHS
为
最常用的单分类方法可能是One-Class SVM
,其在数据的特征空间中寻找最大间距超平面。
SVDD
(Support Vector Data Description)与OC-SVM
类似,但SVDD
是通过学习一个超球面而非像SVM
那样学习一个超平面。SVDD
的目标是寻找一个中心为
常规的SVDD
和OC-SVM
需要对数据先进行特征工程处理,且基于核的方法计算扩展性差。
# 2.2深度学习方法在异常检测中的应用
深度学习在异常检测Anomaly Detection Deep
AD上的应用方式可以分成两种,一种是混合式的,数据的特征表征先单独学习,然后再使用提取的特征在OC-SVM
等浅层的方法上实现分类。另外一种是全深度学习的方法,直接基于异常检测的目标函数进行表征学习。Deep SVDD
提出了一种全深度学习的方法用于无监督异常检测。Deep SVDD通过最小化网络输出的超球面体积来训练神经网络以提取数据分布中的公共特征。
深度自编码是基于深度学习异常检测的主流方法。基于深度自编码的方法通常通过最小化重建误差来训练
异常检测常用的编码器有去噪自编码器,稀疏自编码器,变分自编码器,深度卷积自编码器等。自编码器的目标是数据降维,不能直接应用在AD上,要选择自编码器合适的数据压缩度。可以使自编码器的输入输出具有相同的尺寸,也可以将输入压缩成一个常数,选择合适的压缩比比较困难。
除了自编码器,生成对抗网络GAN也有在AD上的应用。
# 3.Deep SVDD
# 3.1目标函数
与单分类目标一起同时学习数据的表征,使用两个神经网络的联合训练以把数据映射到最小包围超球面上。
输入空间:
输出空间:
从Deep SVDD
的目标是同时学习网络参数以求得输出空间
有Deep SVDD
的soft-boundary
目标函数:
上式中,最小化SVM
中的松弛变量,最后一项是训练参数的正则损失。
当训练数据中即有正样本又有负样本时,可以使用上述的损失函数进行训练,但若训练数据中仅有一个类别,对训练数据做单分类的时候,可以将上述损失函数简化为:
测试时网络输出的应用:
计算网络的输出与训练数据上的中心点
# 3.2几个重要的命题
命题1:零值权重解,
表示网络权重都为0,对于 值权重,网络对于任何输入都会有相同的输出 ,即 ,根据前面的推导可以知道当 时,目标函数有最优解,此时 .因此,在优化目标函数时不能将超球面的中心当做优化变量,否则将导致零值权重,此时超球面半径为零,也称这种现象为超球面塌陷。在训练网络时,超球面中心可通过以下方式指定:取在初始网络权重上训练数据输出的均值。且 命题2:不能有偏置项,对于选择的超球面中心
,如果网络的隐含层有偏置项,将会导致目标函数 的最优解为和 ,同样导致超球面塌陷。网络的某个隐含层可表示为 ,网络的参数 时, ,对于任意的输入都有相同的输出,因此可以选择 使得 ,这时将同样导致 ,造成超球面塌陷。命题3:不能使用有界的激活函数,假设有上界的激活函数,
,特征 对于所有的输入数据都为正,即 对于 ,那么对于任意的 都存在 使得 对于选定的超球面中心 由与激活函数的有界性将同样会导致特征 上超球面的半径为 ,导致超球面塌陷。命令4:松弛变量
的性质,对于 目标函数中的超参数 ,其反映了对异常点的容许上界和对正常样本数据的容许下界。
# 4.代码分析
本文作者使用的数据集为MNIST (opens new window)和CIFA10 (opens new window),网络结构使用的是Lecun
1998年提出的**LeNet (opens new window)**,包括两个卷积两个池化和一层全连接层。
网络结构:
class MNIST_LeNet(BaseNet):
"""
因为有全连接层,所有不同的输入size导致fc的输入大小都不一样
故定义了`MNIST_LeNet`和`CIFA10_LeNet`
"""
def __init__(self):
super().__init__()
self.rep_dim = 32
self.pool = nn.MaxPool2d(2, 2)
self.conv1 = nn.Conv2d(1, 8, 5, bias=False, padding=2)
self.bn1 = nn.BatchNorm2d(8, eps=1e-04, affine=False)
self.conv2 = nn.Conv2d(8, 4, 5, bias=False, padding=2)
self.bn2 = nn.BatchNorm2d(4, eps=1e-04, affine=False)
self.fc1 = nn.Linear(4 * 7 * 7, self.rep_dim, bias=False)
def forward(self, x):
x = self.conv1(x)
x = self.pool(F.leaky_relu(self.bn1(x)))
x = self.conv2(x)
x = self.pool(F.leaky_relu(self.bn2(x)))
x = x.view(x.size(0), -1)
x = self.fc1(x)
return x
Deep SVDD
定义了预训练的方法。其基于自编码器来实现,然后使用自编码器中对应的网络权重来初始化对应的网络参数。自编码器网络结构:
class CIFAR10_LeNet_Autoencoder(BaseNet):
"""
先将图片下采样,再上采样,恢复为原来尺寸
"""
def __init__(self):
super().__init__()
self.rep_dim = 128
self.pool = nn.MaxPool2d(2, 2)
# Encoder (must match the Deep SVDD network above)
self.conv1 = nn.Conv2d(3, 32, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.conv1.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d1 = nn.BatchNorm2d(32, eps=1e-04, affine=False)
self.conv2 = nn.Conv2d(32, 64, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.conv2.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d2 = nn.BatchNorm2d(64, eps=1e-04, affine=False)
self.conv3 = nn.Conv2d(64, 128, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.conv3.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d3 = nn.BatchNorm2d(128, eps=1e-04, affine=False)
self.fc1 = nn.Linear(128 * 4 * 4, self.rep_dim, bias=False)
self.bn1d = nn.BatchNorm1d(self.rep_dim, eps=1e-04, affine=False)
# Decoder
self.deconv1 = nn.ConvTranspose2d(int(self.rep_dim / (4 * 4)), 128, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.deconv1.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d4 = nn.BatchNorm2d(128, eps=1e-04, affine=False)
self.deconv2 = nn.ConvTranspose2d(128, 64, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.deconv2.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d5 = nn.BatchNorm2d(64, eps=1e-04, affine=False)
self.deconv3 = nn.ConvTranspose2d(64, 32, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.deconv3.weight, gain=nn.init.calculate_gain('leaky_relu'))
self.bn2d6 = nn.BatchNorm2d(32, eps=1e-04, affine=False)
self.deconv4 = nn.ConvTranspose2d(32, 3, 5, bias=False, padding=2)
nn.init.xavier_uniform_(self.deconv4.weight, gain=nn.init.calculate_gain('leaky_relu'))
def forward(self, x):
x = self.conv1(x)
x = self.pool(F.leaky_relu(self.bn2d1(x)))
x = self.conv2(x)
x = self.pool(F.leaky_relu(self.bn2d2(x)))
x = self.conv3(x)
x = self.pool(F.leaky_relu(self.bn2d3(x)))
x = x.view(x.size(0), -1)
x = self.bn1d(self.fc1(x))
x = x.view(x.size(0), int(self.rep_dim / (4 * 4)), 4, 4)
x = F.leaky_relu(x)
x = self.deconv1(x)
x = F.interpolate(F.leaky_relu(self.bn2d4(x)), scale_factor=2)
x = self.deconv2(x)
x = F.interpolate(F.leaky_relu(self.bn2d5(x)), scale_factor=2)
x = self.deconv3(x)
x = F.interpolate(F.leaky_relu(self.bn2d6(x)), scale_factor=2)
x = self.deconv4(x)
x = torch.sigmoid(x)
return x
可以看到网络中使用的激活函数是没有上界的leaky_relu
自编码器网络的损失函数是基于原图与自编码器的输出计算均方误差来实现的:
# src/optim/ae_trainer.py line:57-59
outputs = ae_net(inputs)
scores = torch.sum((outputs - inputs) ** 2, dim=tuple(range(1, outputs.dim())))
loss = torch.mean(scores)
自编码器预训练的权重会作为LeNet网络的初始化权重,见init_network_weights_from_pretraining
方法:
# src/deepSVDD.py line:100-114
def init_network_weights_from_pretraining(self):
"""Initialize the Deep SVDD network weights from the encoder weights of the pretraining autoencoder."""
net_dict = self.net.state_dict()
ae_net_dict = self.ae_net.state_dict()
# Filter out decoder network keys
ae_net_dict = {k: v for k, v in ae_net_dict.items() if k in net_dict}
# Overwrite values in the existing state_dict
net_dict.update(ae_net_dict)
# Load the new state_dict
self.net.load_state_dict(net_dict)
初始化网络参数后,超球面的中心c
的计算在src/optim/deepSVDD_trainer.py
的train
方法中,其计算方式为:
def init_center_c(self, train_loader: DataLoader, net: BaseNet, eps=0.1):
"""Initialize hypersphere center c as the mean from an initial forward pass on the data."""
n_samples = 0
c = torch.zeros(net.rep_dim, device=self.device)
net.eval()
with torch.no_grad():
for data in train_loader:
# get the inputs of the batch
inputs, _, _ = data
inputs = inputs.to(self.device)
outputs = net(inputs)
n_samples += outputs.shape[0]
c += torch.sum(outputs, dim=0)
c /= n_samples
# If c_i is too close to 0, set to +-eps. Reason: a zero unit can be trivially matched with zero weights.
c[(abs(c) < eps) & (c < 0)] = -eps
c[(abs(c) < eps) & (c > 0)] = eps
return c
DeepSVDD
的代码的执行过程