主要功能:

  • 异常检测(红框框出图像异常区域)
  • 特征提取(从图像中提取指定的特征)

相对深度自编码器的改进: 提高对异常数据的敏感程度,即两极分化正常样本和异常样本的重构误差。

模型

  1. 编码器(Encoder)

    $$ z = f_e(x; \theta_e) $$

    $\theta_e$表示 Encoder 网络的权重,$f_e(x; \theta_e)$表示对输入变量 x 进行编码操作,降维输入图像张量

  2. 记忆模块(Memory Module)

  3. 解码器(Decoder) $$ \hat{x} = f_d(\hat{z}; \theta_d) $$ $\theta_d$表示 Decoder 网络的权重,$f_d(\hat{z}; \theta_d)$表示对输入变量$\hat{z}$进行解码操作,将数据还原成图像

可视化数据

基于 PyTorch 的 PCA 简单实现 主成分分析(PCA)降维原理、特征值分解与 SVD 分解 PCA 的原理和 pytorch 实现 Python 实现 PCA 降维

记忆模块

下面具体介绍 MemAE 的记忆模块。

memory 为一个包含 N 个行向量的矩阵$\pmb{M} \in R^{N \times C}$,每个行向量$m_i$表示 M 记忆模块的行。

记忆模块的计算流程如下:

  1. 编码器输出张量 z 和记忆矩阵 M 内积和 softmax 归一化,输出$\omega$ $$ \omega = \frac{exp(z * m_i)}{\sum_{i=1}^{N} z *m_i} $$

说明: 论文公布的源代码里面没有使用论文描述的余弦相似度,而是输入值和记忆模块内积,再进行 softmax。作者的回答是使用余弦相似度,导致$\omega$全都趋近 0,所以作者认为余弦相似度不适合该模型,所以改用矩阵内积。所以这里把计算公式修改成与源代码一致。 https://github.com/donggong1/memae-anomaly-detection/issues/12#issuecomment-659951371 2. 利用设定阈值$\lambda$,对$\omega$进行稀疏化,低于阈值$\lambda$的元素置 0。

$$ \hat{\omega_i} = \frac{max(\omega_i-\lambda,0)\omega_i}{|\omega_i-\lambda|+\varepsilon} $$

记忆模块的损失函数:

$$ E(\hat{\omega^t})=\sum^{T}_{i=1}-\hat{\omega} *log(\hat{\omega_i}) $$

损失函数是个针对记忆模块 1 的计算结果权重的信息熵,增加$\omega$的稀疏性,限制特征的个数,实现降维的同时,避免不重要信息的影响。

表示一个记忆力元素(memory item),维度为 C,等于编码器输出特征 z 的维度。因此通过记忆网络得到 query 可以表示为:

记忆模块视为神经网络训练参数的一部分

损失函数

熵函数取最小,提高记忆模块的稀疏性,增加模型的约束条件,避免过拟合

$$ L\left(\theta_{e}, \theta_{d}, \mathbf{M}\right)=\frac{1}{T} \sum_{t=1}^{T}\left(R\left(\mathbf{x}^{t}, \hat{\mathbf{x}}^{t}\right)+\alpha E\left(\widehat{\mathbf{w}}^{t}\right)\right) $$

其中$R=|\mathrm{x}-\widehat{\mathrm{x}}|_{2}^{2}$表示重构误差,超参数$ \alpha $原文作者选择的是 0.0002

源代码分析

数据集

  • minist 数据集(图像数据集,手写体,PyTorch 等深度学习框基本自带)
  • UCSD Anomaly Detection Dataset(视频数据集,视频流的图片都提取出来,加利福尼亚大学圣迭戈分校提供的异常检测数据集,非步行和异常步行视为异常,比如骑自行车、滑板、开车、轮椅等等)

mnist 数据集比较常用,就不介绍了。

UCSD 数据集是固定摄像头记录的行人视频,视频拆分成图片。UCSD 划分为两个子集,对应不同的场景。每个子集划分为训练集和测试集,训练集都是正常样本,测试集中夹杂者异常样本,同时记录异常数据所在图像的区域。 UCSD 数据集预处理:

  1. tif 格式文件转 jpg(tif 和 jpg 是一种图片格式文件)

  2. 测试集异常图片提取标签

  3. 设定条件提取视频数据

模型

模型主要架构如开头所示,自编码器的编码器和解码器之间增加一个缓冲区(记忆模块),自编码器部分流程和神经网络训练基本一致,这里主要分析下记忆模块部分。

记忆模块参数非负

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
class MemoryUnit(nn.Module):

    def __init__(self, mem_dim, fea_dim, shrink_thres=0.0025):
        super(MemoryUnit, self).__init__()
        self.mem_dim = mem_dim
        self.fea_dim = fea_dim
        self.weight = Parameter(torch.Tensor(self.mem_dim,
                                             self.fea_dim))  # M x C
        self.bias = None
        self.shrink_thres = shrink_thres
        # self.hard_sparse_shrink_opt = nn.Hardshrink(lambd=shrink_thres)

        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1. / math.sqrt(self.weight.size(1))
        # 连续型均匀分布初始化记忆模块参数
        self.weight.data.uniform_(-stdv, stdv)
        if self.bias is not None:
            self.bias.data.uniform_(-stdv, stdv)

    def forward(self, input):
        att_weight = F.linear(input,
                              self.weight)  # Fea x Mem^T, (TxC) x (CxM) = TxM
        att_weight = F.softmax(att_weight, dim=1)  # TxM
        # ReLU based shrinkage, hard shrinkage for positive value
        if (self.shrink_thres > 0):
            att_weight = hard_shrink_relu(att_weight, lambd=self.shrink_thres)
            # att_weight = F.softshrink(att_weight, lambd=self.shrink_thres)
            # normalize???
            att_weight = F.normalize(att_weight, p=1, dim=1)
            # att_weight = F.softmax(att_weight, dim=1)
            # att_weight = self.hard_sparse_shrink_opt(att_weight)
        mem_trans = self.weight.permute(1, 0)  # Mem^T, MxCvvv
        output = F.linear(
            att_weight,
            mem_trans)  # AttWeight x Mem^T^T = AW x Mem, (TxM) x (MxC) = TxC
        return {'output': output, 'att': att_weight}  # output, att_weight

    def extra_repr(self):
        return 'mem_dim={}, fea_dim={}'.format(self.mem_dim, self.fea_dim
                                               is not None)


# NxCxHxW -> (NxHxW)xC -> addressing Mem, (NxHxW)xC -> NxCxHxW
class MemModule(nn.Module):

    def __init__(self, mem_dim, fea_dim, shrink_thres=0.0025, device='cuda'):
        super(MemModule, self).__init__()
        self.mem_dim = mem_dim
        self.fea_dim = fea_dim
        self.shrink_thres = shrink_thres
        self.memory = MemoryUnit(self.mem_dim, self.fea_dim, self.shrink_thres)

    def forward(self, input):
        s = input.data.shape
        l = len(s)

        if l == 3:
            x = input.permute(0, 2, 1)
        elif l == 4:
            x = input.permute(0, 2, 3, 1)
        elif l == 5:
            x = input.permute(0, 2, 3, 4, 1)
        else:
            x = []
            print('wrong feature map size')
        x = x.contiguous()
        x = x.view(-1, s[1])
        #
        y_and = self.memory(x)
        #
        y = y_and['output']
        att = y_and['att']

        if l == 3:
            y = y.view(s[0], s[2], s[1])
            y = y.permute(0, 2, 1)
            att = att.view(s[0], s[2], self.mem_dim)
            att = att.permute(0, 2, 1)
        elif l == 4:
            y = y.view(s[0], s[2], s[3], s[1])
            y = y.permute(0, 3, 1, 2)
            att = att.view(s[0], s[2], s[3], self.mem_dim)
            att = att.permute(0, 3, 1, 2)
        elif l == 5:
            y = y.view(s[0], s[2], s[3], s[4], s[1])
            y = y.permute(0, 4, 1, 2, 3)
            att = att.view(s[0], s[2], s[3], s[4], self.mem_dim)
            att = att.permute(0, 4, 1, 2, 3)
        else:
            y = x
            att = att
            print('wrong feature map size')
        return {'output': y, 'att': att}


# relu based hard shrinkage function, only works for positive values
def hard_shrink_relu(input, lambd=0, epsilon=1e-12):
    output = (F.relu(input - lambd) * input) / (torch.abs(input - lambd) +
                                                epsilon)
    return output

hard shrinkage for sparse addressing(稀疏定位硬压缩)

提高记忆矩阵稀疏性,设定阈值,矩阵中低于阈值的参数直接设为 0

代码里面没有使用论文中的余弦相似度,而是输入值和记忆模块内积,再进行 softmax,作者的回答是使用余弦相似度,导致记忆模块权重全都趋近 0,所以作者认为余弦相似度不适合该模型,所以改用矩阵内积

为了能够反向传播和简便性,这里使用 ReLU 激活函数筛选矩阵参数 hard_shrink_relu:

$$ \frac{max(\omega_i-\lambda,0)}{|\omega_i-\lambda|+\epsilon} $$

mnist 数据集训练

mem_dim: 记忆模块大小 正常样本标签为 0,异常样本为 1

cifar10

数据结果展示

当前项目

需要可视化训练过程

重构误差函数需要进一步改良

记忆自编码器无监督转有监督学习

实际上这是一个错误的改进方法,因为有监督学习不适合异常检测。现实生活中,异常发生机率非常小,数据难采集,而且异常样本不可预测,这些条件直接拒绝了有监督学习的应用。 不过,本人做完了才意识到,所以还是记录下。

有监督反向训练损失函数方法

参考论文: Deep Semi-Supervised Anomaly Detection

1
2
3
4
5
6
7
@InProceedings{ruff2020deep,
  title     = {Deep Semi-Supervised Anomaly Detection},
  author    = {Ruff, Lukas and Vandermeulen, Robert A. and G{\"o}rnitz, Nico and Binder, Alexander and M{\"u}ller, Emmanuel and M{\"u}ller, Klaus-Robert and Kloft, Marius},
  booktitle = {International Conference on Learning Representations},
  year      = {2020},
  url       = {https://openreview.net/forum?id=HkgH0TEYwH}
}

批处理操作,会对影响 ROC_AUC 指标有影响,原因是使用 PyTorch 自带损失函数,默认是批处理里的数据求和,跟手动编写的函数不一样

  1. 训练输入异常样本时,原来的损失函数添加负号。发现,这个跟损失函数
  2. 异常样本输入,损失函数取倒数

无监督转半监督

FAQ

Python pytorch 多进程运行

扩展资料

opencv 和 pillow 比较

个人猜想

  • 不在编码器和解码器之间增加记忆模块,而是在其他地方增加,是否会取得更好的效果

  • 编码器之间增加 skip connection

  • 中间输入增加随机噪声

  • 新模型的改进不会只拘泥于一个旧模型,可以是其他模型,比如降噪自编码器的功能是剔除原有数据的噪声,通过无监督学习的方法,这个功能在原有自编码器功能上是没有,完全是基于功能进行改进的

  • 编码器中间还是结尾增加 SVDD 模块

pretrain 只用了 mse,训练的时候增加了记忆模块和 svdd test 只用了 mse

r6. 经历了 3 次碰撞, 0.048ms p8. a. (1-1/N)^(N-1),. p=1/N b. 1/e=0.37 p9. max(p(1-p)^2(N-1))-1/2e

使用自己的网络模型,分别测试 svdd、autoencoder、mvdd 等方法 IAED 的网络模型和 Deep SVDD 的网络模型不一样