异常检测是工业质检、金融反欺诈、医疗影像分析等领域的核心需求。传统方法如孤立森林、One-Class SVM在低维数据上表现不错,但面对高维图像或时序数据时往往力不从心。生成对抗网络提供了一条新思路:让生成器只学习正常样本的数据分布,推理阶段再用重构误差和判别器特征差异来衡量输入是否“像正常数据”。本文完整走一遍用Python实现这套方案的流程。

GAN做异常检测的核心原理
标准GAN由生成器和判别器两部分组成。生成器接收一个随机噪声向量,输出一张伪造图像;判别器则负责区分真实图像和生成图像。两者在对抗训练中共同进化,最终生成器学到的就是真实数据的概率分布。
异常检测的关键假设在于:如果训练集中只包含正常样本,那么生成器只见过正常数据的分布。当一张异常图像进来时,模型无法在隐空间中找到对应的向量把它重构出来,重构误差会显著偏大。这就是AnoGAN(Anomaly Detection with GAN)的基本思想。
具体评分包含两个部分:一是图像空间的重构残差,衡量生成结果和输入在像素层面的差距;二是判别器中间层的特征匹配损失,衡量两者在语义特征层面的差距。两个分数加权求和,得到最终的异常分数。相比单纯用像素差,特征匹配对光照变化、轻微位移更加鲁棒。
用Python搭建DCGAN模型
我们以工业零件图像为例,使用TensorFlow和Keras搭建一个深度卷积GAN。图像尺寸统一为64x64灰度图,生成器用转置卷积逐级上采样,判别器用普通卷积逐级下采样。
import tensorflow as tf
from tensorflow.keras import layers, Model
def build_generator(latent_dim=100):
inputs = layers.Input(shape=(latent_dim,))
x = layers.Dense(8 * 8 * 256, use_bias=False)(inputs)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Reshape((8, 8, 256))(x)
# 逐级上采样:8x8 -> 16x16 -> 32x32 -> 64x64
for filters in [128, 64, 32]:
x = layers.Conv2DTranspose(filters, 4, strides=2, padding='same', use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.LeakyReLU(0.2)(x)
out = layers.Conv2DTranspose(1, 4, strides=1, padding='same', activation='tanh')(x)
return Model(inputs, out, name='generator')
def build_discriminator():
inputs = layers.Input(shape=(64, 64, 1))
x = inputs
for filters in [32, 64, 128]:
x = layers.Conv2D(filters, 4, strides=2, padding='same')(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
features = layers.Flatten()(x) # 这一层的特征用于异常评分
out = layers.Dense(1, activation='sigmoid')(features)
return Model(inputs, [out, features], name='discriminator')注意判别器这里返回了两个输出:最终的真假概率和展平后的特征向量。这个特征向量在推理阶段非常关键,它将用于计算特征匹配损失。训练循环中,生成器和判别器交替更新,用二元交叉熵作为对抗损失即可。训练数据务必只放正常样本,这是整套方法成立的前提,如果混入异常样本,生成器会把异常模式也学进去,检测能力直接失效。
训练轮数需要观察生成质量来定,一般几百个epoch后生成图像应当接近真实零件的轮廓。学习率建议生成器0.0002、判别器0.0002,Adam优化器的beta1设为0.5,这是GAN训练的常用配置,能显著减少训练震荡。
推理阶段:隐空间搜索计算异常分数
AnoGAN推理时需要在隐空间中搜索一个噪声向量z,使得生成器输出与待测图像最接近。这个搜索过程本质上是一个优化问题:以z为变量,最小化重构误差和特征匹配误差的加权和。下面给出实现。
import numpy as np
class AnoDetector:
def __init__(self, generator, discriminator, latent_dim=100, lam=0.1):
self.G = generator
self.D = discriminator
self.latent_dim = latent_dim
self.lam = lam # 特征匹配项的权重
def score(self, x, steps=200, lr=0.01):
# 初始化隐向量,多随机重启可以避免局部最优
best_score, best_z = np.inf, None
for _ in range(3):
z = tf.Variable(tf.random.normal([1, self.latent_dim]))
opt = tf.keras.optimizers.Adam(lr)
for _ in range(steps):
with tf.GradientTape() as tape:
fake = self.G(z, training=False)
# 图像空间残差
res_loss = tf.reduce_mean(tf.abs(fake - x))
# 判别器特征匹配残差
_, f_fake = self.D(fake, training=False)
_, f_real = self.D(x, training=False)
feat_loss = tf.reduce_mean(tf.abs(f_fake - f_real))
loss = res_loss + self.lam * feat_loss
grad = tape.gradient(loss, [z])
opt.apply_gradients(zip(grad, [z]))
if loss.numpy() < best_score:
best_score, best_z = loss.numpy(), z.numpy()
return best_score
detector = AnoDetector(generator, discriminator)
score = detector.score(test_image)
print('异常分数:', score)梯度下降的步数和随机重启次数直接影响检测精度与推理速度的平衡。步数太少搜索不充分,分数不稳定;步数太多则推理耗时成倍增加,不适合在线检测场景。工程上一般取200步、3次重启。如果对实时性要求高,可以改用Encoder结构直接把图像映射到隐空间,这就是后来的EGBAD和f-AnoGAN的改进方向,推理速度能提升两个数量级。
阈值设定与常见坑点
拿到异常分数后,需要确定判定阈值。常见做法是在验证集上用正常样本计算分数分布,取95分位数或99分位数作为阈值。更严谨的方式是准备少量带标签的验证集,绘制PR曲线找F1分数最高的点。异常检测任务通常正负样本极不平衡,准确率指标会失真,务必看精确率和召回率。
实践中有几个坑需要提醒。第一,训练数据必须严格清洗,混入百分之几的异常样本就可能让模型漏检。第二,图像归一化要训练推理一致,如果训练用tanh对应的[-1, 1]范围,推理时忘记做同样变换,分数会完全失真。第三,GAN训练本身不稳定,若生成器 collapse 到只输出单一图像,所有样本的重构误差都差不多,模型等于失效,训练过程中要持续抽查生成样本质量。
如果业务场景对推理速度敏感,建议在本文方案基础上引入编码器实现快速隐向量估计;如果是时序数据,可以把卷积结构换成LSTM或1D卷积,整体评分框架完全通用。GAN异常检测的价值在于它只依赖正常样本,这在异常样本稀缺、标注成本高的真实业务里,往往是最现实的选择。
GAN异常检测生成对抗网络Python深度学习修改时间:2026-09-04 06:32:32