在编码器将数据压缩为潜在表示 $z$ 之后,解码器网络开始发挥主要作用。它的主要职责是逆转此过程:从瓶颈层获取紧凑的潜在代码 $z$,并重建数据 $\hat{x}$,使其尽可能接近原始输入 $x$。可以将其视为与编码器压缩功能相对应的解压缩算法。解码器,与编码器类似,通常是一个前馈神经网络。一种常见且通常有效的设计方法是将解码器结构设计为编码器架构的镜像。如果编码器由一系列逐步降低维度的层(例如,单元数量递减的密集层)组成,那么解码器可能会采用一系列逐步 增加 维度的层,最终目标是匹配原始输入的形状。例如,如果用于图像数据的编码器使用卷积层和池化层来减小空间维度并增加特征深度,那么对应的解码器可能会使用上采样层(如Keras中的UpSampling2D或PyTorch中的nn.Upsample)和转置卷积层(有时称为反卷积层,如Conv2DTranspose或nn.ConvTranspose2d)来增加空间维度并重建图像。对于由密集层处理的更简单、非空间数据,解码器将只需使用每个后续层中单元数量递增的密集层。digraph G { rankdir=LR; node [shape=box, style=filled, fillcolor="#a5d8ff", fontname="sans-serif"]; edge [fontname="sans-serif"]; subgraph cluster_encoder { label = "编码器"; style=dashed; bgcolor="#e9ecef"; node [fillcolor="#74c0fc"]; Input -> LayerE1 [label=" f(x)"]; LayerE1 -> LayerE2; LayerE2 -> Bottleneck [label=" z "]; } subgraph cluster_decoder { label = "解码器"; style=dashed; bgcolor="#e9ecef"; node [fillcolor="#74c0fc"]; Bottleneck -> LayerD1 [label=" g(z)"]; LayerD1 -> LayerD2; LayerD2 -> Output [label=" \hat{x} "]; } Input [shape=ellipse, fillcolor="#fab005", label="输入"]; Bottleneck [shape=ellipse, fillcolor="#f06595", label="瓶颈层"]; Output [shape=ellipse, fillcolor="#fab005", label="输出"]; LayerE1 [label="层 1"]; LayerE2 [label="层 2"]; LayerD1 [label="层 1"]; LayerD2 [label="层 2"]; }自编码器工作流程的一个视图,其中突显了解码器从潜在代码 $z$ 重建输出 $\hat{x}$ 的作用。解码器架构通常镜像编码器的结构。虽然架构对称性是一个有用的指导原则,但它并非严格要求。重要的是,解码器必须具备将学习到的潜在表示映射回原始数据空间的能力。解码器中的激活函数解码器隐藏层中激活函数的选择通常镜像编码器(例如,ReLU或其变体是促进非线性的常见选择)。然而,解码器 最终输出层 中使用的激活函数特别重要,并且直接取决于原始输入数据 $x$ 的特征和归一化方式。数据归一化到0和1之间: 如果您的输入数据 $x$ 由缩放到 $[0, 1]$ 范围的值组成(常见于灰度图像像素强度或二进制数据),那么 sigmoid 激活函数通常是解码器输出层的合适选择。这可确保重建的输出 $\hat{x}$ 也落在此范围内,与二元交叉熵 (BCE) 等重建损失函数良好匹配。 $$ \text{Sigmoid}(y) = \frac{1}{1 + e^{-y}} $$数据归一化到-1和1之间: 如果输入数据缩放到 $[-1, 1]$ 范围,双曲正切 (tanh) 激活函数是输出层的合适选择。 $$ \tanh(y) = \frac{e^y - e^{-y}}{e^y + e^{-y}} $$无界或标准化数据: 如果您的输入数据由非严格有界的连续值组成(或者可能是均值为零、方差为一的标准化数据),那么 线性 激活函数(即不应用任何激活函数,或 $f(y)=y$)通常是输出层的最佳选择。这使得解码器可以输出整个实数范围的值,并自然地与均方误差 (MSE) 损失函数搭配。数学表示从数学上讲,我们可以将解码器表示为一个以权重和偏置 $\theta_d$ 为参数的函数 $g$。它以潜在向量 $z$ 作为输入,并生成重建输出 $\hat{x}$:$$ \hat{x} = g(z; \theta_d) $$回顾一下,潜在表示 $z$ 是由编码器 $f$ 以及参数 $\theta_e$ 生成的,即 $z = f(x; \theta_e)$,那么整个自编码器过程通过以下组合将输入 $x$ 映射到输出 $\hat{x}$:$$ \hat{x} = g(f(x; \theta_e); \theta_d) $$训练过程通过最小化重建损失 $L(x, \hat{x})$ 来进行,它会调整 $\theta_e$ 和 $\theta_d$,使 $\hat{x}$ 尽可能与 $x$ 相似,从而迫使瓶颈 $z$ 捕获有关数据分布的重要信息。实现要点在TensorFlow/Keras或PyTorch等框架中,构建解码器涉及定义一系列层(例如,Dense、Conv2DTranspose、UpSampling2D),并设置适当的输出维度和激活函数。对于简单的自编码器,这通常可以使用顺序API来完成。对于更复杂的结构,定义自定义模型类可提供更大的灵活性。请记住,要确保最后一层的输出形状精确匹配输入数据的形状,并且其激活函数与数据的范围以及所选的损失函数一致。解码器的设计对于自编码器重建数据的能力非常重要。虽然它通常与编码器对称,但最重要的考量是其从潜在空间映射回数据空间的能力,以及其输出层配置是否正确以匹配输入数据特征。这种重建能力是构建更高级自编码器应用(包括在第4章中介绍的生成式建模)的依据。