PAE 的模型架构:编码器、解码器、潜在空间分布假设
好,咱们进入第四章。前几章一直在聊“PAE 是什么”、“它能解决什么问题”。现在,我把袖子撸起来,带你拆这个模型的内部结构。
PAE 全称是 Probabilistic Autoencoder,概率自编码器。说白了,它就是在普通自编码器的中间层——那个所谓的“瓶颈层”里,塞进一套概率分布假设。为什么要这么做?因为现实世界的数据,尤其是异常数据,它的分布往往是混沌的、多峰的。你光靠一个点向量去表示它,很容易漏掉关键信息。
我刚开始做工业质检项目时,用普通 AE 去检测芯片表面瑕疵。效果还可以,但总有几个漏报。后来换成 PAE,才发现问题出在“点向量”上。普通 AE 的潜在空间是一个点,而 PAE 的潜在空间是一个概率分布。这两个东西,差距非常大。
编码器(Encoder)—— 把数据压缩到核心表达
编码器的工作,从结构上看就是一个神经网络,输入是高维的原始数据(比如 256×256 的图片),输出是两个向量:均值 μ 和方差 σ²。当然,实际实现中我们会输出 log_var,方便数值稳定。
为什么是均值-方差对?因为 PAE 假设潜在变量 z 服从一个标准正态分布 N(0, I)。编码器要做的是,对每个输入样本 x,给出它对应的潜变量 z 应该是什么分布——而不是精确的 z 值。
核心思路: 编码器输出的是“分布的参数”,而不是“潜变量的点值”。
# 伪代码示意:编码器输出层
def encoder(x):
hidden = Dense(128, activation='relu')(x)
z_mean = Dense(latent_dim)(hidden) # 均值 μ
z_log_var = Dense(latent_dim)(hidden) # 对数方差 log(σ²)
return z_mean, z_log_var
我记得第一次写这层代码时,忘记给 z_log_var 加个 clamp,导致训练中方差直接爆炸,loss 变成 NaN。后来加了个 clip_by_value,才稳住。嗯,这里要敲个黑板。
解码器(Decoder)—— 从潜在空间重建数据
解码器就是编码器的逆过程。它接收潜变量 z,经过一个与编码器对称的网络,输出重建数据 x'。但这里有个关键区别:解码器看到的 z,并不是直接从编码器输出的 μ 和 σ² 拿来的,而是通过一个“重参数化技巧”采样得到的。
重参数化是什么?我换个说法你就懂了:你有一个高斯分布 N(μ, σ²),你想从这个分布里采样一个 z 送进解码器。正常的随机采样是没法反向传播梯度的。于是 PAE 用了这个小把戏——先从一个标准正态分布 N(0, I) 里采样 ϵ,然后计算 z = μ + σ * ϵ。这样,梯度就能穿过 μ 和 σ 传回编码器了。
避坑指南: 我曾经在采样时直接用 torch.randn_like(μ) 生成 ϵ,结果忘记把 σ 乘上去,导致潜变量始终是标准正态,模型完全没学到特征。那个 debug 花了我一个下午。
# 重参数化采样
def sampling(args):
z_mean, z_log_var = args
eps = tf.random.normal(shape=tf.shape(z_mean))
z = z_mean + tf.exp(0.5 * z_log_var) * eps
return z
潜在空间的分布假设 —— 什么才算“正常”?
这是 PAE 最精华的地方。我们假设:正常数据的潜变量 z 应该服从一个标准正态分布 N(0, I)。换句话说,如果某个样本的 z 离原点很远,或者它的分布形状特别奇怪,那它大概率是异常。
为什么这个假设合理?因为编码器在训练时会被迫去拟合这个先验。我们用 KL 散度来惩罚编码器输出的分布和标准正态之间的差距。KL 散度越小,说明潜变量越“正常”。
我见过不少新手把 KL 散度当作损失函数的一部分直接加进去,但没调权重。结果模型只顾着重建,KL 散度接近零,但潜变量毫无语义信息。正确的做法是给 KL 项一个合适的 β 系数——你猜对了,这就是 β-VAE 的思路。
注意: KL 散度权重太小,模型退化成普通 AE;权重太大,重建质量急剧下降。通常从 0.001 开始调,逐步增大,直到找到平衡点。
编码器与解码器的对称设计
结构上,编码器和解码器通常是对称的。比如编码器是 3 层卷积 + Flatten + 全连接,解码器就是全连接 + Reshape + 3 层转置卷积。但这不是硬规定。我做过一个项目,数据是 1 维的振动信号,我就用了对等的全连接层,效果也很好。
下面这张图表,总结了普通 AE、VAE 和 PAE 在结构上的关键差异。你一眼就能看出 PAE 到底多了什么。
| 维度 | 普通自编码器 (AE) | 变分自编码器 (VAE) | 概率自编码器 (PAE) |
|---|---|---|---|
| 潜在空间形式 | 单个点向量 | 概率分布 (μ, σ²) | 概率分布 (μ, σ²) |
| 采样方式 | 无 | 重参数化采样 | 重参数化采样 |
| 分布假设 | 无 | 标准正态先验 | 标准正态先验 + 额外正则 |
| 损失函数 | 重建损失 | 重建损失 + KL 散度 | 重建损失 + KL 散度 + 异常评分项 |
| 异常检测方式 | 重建误差 | 重建误差 / 对数似然 | 重建误差 + KL 散度双重判据 |
坦白说,我早期做异常检测时,以为加个 KL 就是 VAE,后来才发现 PAE 在训练时有额外的目标——它用“先验分布”作为异常判据的一部分。这招在工业异音检测中特别灵。
动手配一个简单的 PAE(Python 示例)
理论讲完了,咱们上点真家伙。下面是用 Keras 搭的一个极简 PAE,输入是 784 维的 MNIST 数字。我故意把 latent_dim 设成 2,好让你能可视化潜空间。
import tensorflow as tf
from tensorflow.keras import layers, Model
latent_dim = 2
input_dim = 784
# 编码器
inputs = layers.Input(shape=(input_dim,))
h = layers.Dense(256, activation='relu')(inputs)
z_mean = layers.Dense(latent_dim)(h)
z_log_var = layers.Dense(latent_dim)(h)
# 采样层
def sampling(z_mean, z_log_var):
eps = tf.random.normal(shape=tf.shape(z_mean))
return z_mean + tf.exp(0.5 * z_log_var) * eps
z = layers.Lambda(lambda args: sampling(*args))([z_mean, z_log_var])
# 解码器
decoder_h = layers.Dense(256, activation='relu')
decoder_out = layers.Dense(input_dim, activation='sigmoid')
outputs = decoder_out(decoder_h(z))
# 构建模型
pae = Model(inputs, outputs)
# 损失:重建 + KL 散度
def vae_loss(inputs, outputs, z_mean, z_log_var):
reconstruction_loss = tf.reduce_mean(tf.square(inputs - outputs))
kl_loss = -0.5 * tf.reduce_mean(
1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var)
)
return reconstruction_loss + kl_loss * 0.001 # beta = 0.001
pae.compile(optimizer='adam', loss=lambda y, pred: vae_loss(y, pred, z_mean, z_log_var))
关键参数: KL 权重 0.001 是我试出来的。如果数据噪声大,可以适当提到 0.005。
好了,结构讲清楚了。下面这张图把 PAE 的完整数据流画了出来——从输入到编码器,到潜空间采样,再到解码器重建。注意潜空间那里标注的“N(0, I)”——它就是整个异常判定的立足之本。
嗯,看到这里你应该明白了:PAE 不是简单地在 AE 中间加层全连接。它是把整个“分布假设”嵌进了模型骨架里。编码器负责“估计分布参数”,解码器负责“基于采样重建”,而 KL 散度惩罚则是那个无形的考官,时刻提醒模型——你的潜空间要接近标准正态。
下一层咱们会深入聊“异常评分”怎么算。到这章为止,结构地基已经打稳了。