# Deep-One-Class-Classification论文解读及代码分析

# 背景

神经网络在多类别数据上取得了不错的成果,如图像分类,目标检测,图像分割中,但面对实际中的应用,往往面对训练数据少,缺少某个类别数据的挑战。考虑如医学图像的辅助诊断,有可能只能提供健康类别的图像,但要算法识别出异常的图像,这时应用神经网络做分类,面对单个类别该如何训练呢?实际生活中存在大量的此种应用场景,如异常检测,网络入侵检测,欺诈检测等。

# 1.简介

论文:Deep One-Class Classification
Lukas (opens new window)

代码:PyTorch Implementation of Deep SVDD (opens new window)

这篇论文是德国柏林洪堡大学的Lukas Ruff发表在ICML2018上的工作。柏林洪堡大学已诞生57位诺贝尔奖获得者,实力惊人。这篇论文主要的工作是提出了Deep Support Vector Data Description(Deep SVDD),该方法可以直接基于异常目标函数对单类别数据进行训练,区别于以往的先在大规模数据集训练再在单类别数据集上微调的做法。

如异常检测,正常的数据服从高斯分布,异常的数据不服从高斯分布,模型训练的目标就是准确表征“正常”,与模型偏差较大的即异常点。只有正常类别的数据做训练,因此也可以看做单分类问题One-Class Classification

对于图像数据,其维度通常较大,传统的单分类方法如One-Class SVMKernel Density Estimation很难取得理想的效果,且还需要先对数据做大量的特征工程的工作。

Deep SVDD通过寻找一个最小球面,使该最小封闭球面包围数据的网络表征, 来训练一个神经网络模型。最小化训练数据表征的封闭球面可以使网络提取出变化数据中的共同特征。

# 2.相关工作

# 2.1基于核的单分类方法

训练数据所属的空间表示为:

表示正定核(PSD kernel),,数据的联合再生希尔伯特空间(Reproducing kernel Hilbert space)RKHS表示为数据特征的映射。因此,上式中表示再生希尔伯特空间上的点积。

最常用的单分类方法可能是One-Class SVM,其在数据的特征空间中寻找最大间距超平面。

SVDD(Support Vector Data Description)与OC-SVM类似,但SVDD是通过学习一个超球面而非像SVM那样学习一个超平面。SVDD的目标是寻找一个中心为,半径为,的最小超球面,使其在特征空间中包围大部分数据。在超球面之外的数据就是异常数据。

常规的SVDDOC-SVM需要对数据先进行特征工程处理,且基于核的方法计算扩展性差。

# 2.2深度学习方法在异常检测中的应用

深度学习在异常检测Anomaly Detection DeepAD上的应用方式可以分成两种,一种是混合式的,数据的特征表征先单独学习,然后再使用提取的特征在OC-SVM等浅层的方法上实现分类。另外一种是全深度学习的方法,直接基于异常检测的目标函数进行表征学习。Deep SVDD提出了一种全深度学习的方法用于无监督异常检测。Deep SVDD通过最小化网络输出的超球面体积来训练神经网络以提取数据分布中的公共特征。

深度自编码是基于深度学习异常检测的主流方法。基于深度自编码的方法通常通过最小化重建误差来训练。自编码器通过重建数据来学习正常数据间的共同特征,异常数据不包含这些特征而导致重建误差较大,由此识别出异常数据。

异常检测常用的编码器有去噪自编码器,稀疏自编码器,变分自编码器,深度卷积自编码器等。自编码器的目标是数据降维,不能直接应用在AD上,要选择自编码器合适的数据压缩度。可以使自编码器的输入输出具有相同的尺寸,也可以将输入压缩成一个常数,选择合适的压缩比比较困难。

除了自编码器,生成对抗网络GAN也有在AD上的应用。

# 3.Deep SVDD

# 3.1目标函数

与单分类目标一起同时学习数据的表征,使用两个神经网络的联合训练以把数据映射到最小包围超球面上。

输入空间:
输出空间:
的映射神经网络:,有个隐含层,权重分别为,表示层的权重。Deep SVDD的目标是同时学习网络参数以求得输出空间中半径为中心为的最小体积包围圆。

上的训练数据可定义Deep SVDDsoft-boundary目标函数:

Jsoft(W,R)=minR,WR2+1vni=1nmax{0,||ϕ(xi;W)c||2}+λ2l=1L||Wl||F2

上式中,最小化就能最小化超球面,第二项是对超球面外的点的惩罚,超参数控制着球的体积与边界的松弛程度,类似SVM中的松弛变量,最后一项是训练参数的正则损失。

当训练数据中即有正样本又有负样本时,可以使用上述的损失函数进行训练,但若训练数据中仅有一个类别,对训练数据做单分类的时候,可以将上述损失函数简化为:

JOC(W)minW1ni=1n||ϕ(xi;W)c||2+λ2l=1L||Wl||F2

测试时网络输出的应用:

s(x)=||ϕ(x;W)c||2

计算网络的输出与训练数据上的中心点之间的距离,当小于某个阈值时认为是符合同一个类别的,否则为其他类别。

# 3.2几个重要的命题

  • 命题1:零值权重解表示网络权重都为0,对于值权重,网络对于任何输入都会有相同的输出,即,根据前面的推导可以知道当时,目标函数有最优解,此时.因此,在优化目标函数时不能将超球面的中心当做优化变量,否则将导致零值权重,此时超球面半径为零,也称这种现象为超球面塌陷。在训练网络时,超球面中心可通过以下方式指定:取在初始网络权重上训练数据输出的均值。

  • 命题2:不能有偏置项,对于选择的超球面中心,如果网络的隐含层有偏置项,将会导致目标函数的最优解为,同样导致超球面塌陷。网络的某个隐含层可表示为,网络的参数时,,对于任意的输入都有相同的输出,因此可以选择使得,这时将同样导致,造成超球面塌陷。

  • 命题3:不能使用有界的激活函数,假设有上界的激活函数,,特征对于所有的输入数据都为正,即对于,那么对于任意的都存在使得对于选定的超球面中心由与激活函数的有界性将同样会导致特征上超球面的半径为,导致超球面塌陷。

  • 命令4:松弛变量的性质,对于目标函数中的超参数,其反映了对异常点的容许上界和对正常样本数据的容许下界。

# 4.代码分析

本文作者使用的数据集为MNIST (opens new window)CIFA10 (opens new window),网络结构使用的是Lecun1998年提出的**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

f(x)={x0.01x

自编码器网络的损失函数是基于原图与自编码器的输出计算均方误差来实现的:

# 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.pytrain方法中,其计算方式为:

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的代码的执行过程

graph TD A[定义dataset]-->B[自编码器训练]-->C[初始化网络权重] B-->D[计算超球中心c] B-->E[初始化超球R=0] F[训练单分类网络] C-->F D-->F E--->F F-->G[测试:计算数据与超球面中心距离]
(adsbygoogle = window.adsbygoogle || []).push({});

# 参考资料