Skip to main content
  1. Posts/

从特征表示到生成语义:深入理解 Latent Variable 与 Latent Space

·7071 words·15 mins
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

起因
#

最近在看 MIT 6.S191 Lecture 4——Generative Modeling。在这之前我对生成模型基本没什么了解,刚建立了一个很粗的认知:

生成模型试图从训练数据中学习数据分布,然后从学到的分布中采样,生成新的数据。

继续往下看,第一次碰到了 latent variable(潜变量)和 latent space(潜空间)。

我的第一反应非常强烈:

这不就是某种 embedding 吗?

因为它们看起来都在做一件事:把复杂数据 \(x\) 变成某个低维或抽象的向量表示 \(z\)。image 变成向量,token 变成 embedding,image 变成 latent representation——形式上几乎一模一样。之前写 embedding 那篇时,讨论的就是这种从离散 symbol 到连续向量的映射。

但继续想下去,又觉得有什么地方不一样。Embedding 给我的感觉是"往后走"——从输入到表示再到预测。而 latent variable 好像反过来了——先有一个隐藏状态,再生成我们看到的数据。

后来把这个直觉一层层拆开,才发现这个看似自然的类比,其实把两种完全不同的建模方向与语义目标混为一谈了。

Latent 这个词到底什么意思
#

先不看公式,先看这个英文词本身。

Latent:潜在的、隐藏的、没有直接显现出来的。

在统计建模和机器学习的语境里,latent 的含义更具体一些:存在于模型中,但没有被直接观察到。

举个例子。我们观察到一张人脸图片 \(x\)——几十万个像素值,每个值精确描述了某个位置的颜色。但我们可以假设,这张图片背后存在某种隐藏的状态 \(z\),它和姿态、表情、光照、身份、拍摄角度等因素有关。

不过需要说明的是,真实模型学到的潜在维度很少能如此干净地与人类直觉概念一一对应,往往多个因素相互交织(即因素纠缠,entanglement),后文会进一步展开。

可观测变量与潜变量
#

把上面的区分稍微正式一点。

Observable variable(可观测变量):我们直接拿到手的数据。图片的像素值、音频的波形、一段文本里的 token。

Latent variable(潜变量):模型中假设存在,但训练数据没有直接告诉我们的 hidden variable。

它们之间的核心关系是:

$$ Z \to X $$

模型假设:先有某种隐藏状态 \(Z\),然后由 \(Z\) 通过某个过程生成了我们看到的 \(X\)。

这和 RNN 中 hidden state 的概念有点呼应——都是"没有直接出现在数据里,但模型内部维护的某种状态"。区别在于,RNN 的 hidden state 是序列处理过程中逐步更新的中间表示,而这里的 latent variable 是从概率建模角度假设存在的未观测变量。

Latent 不等于低维
#

这里有一个容易踩的坑。

我最开始不自觉地把 latent variable 理解成"把高维数据压缩成低维向量"。确实,很多实际模型里 latent representation 的维度远低于原始数据——几百维的向量 vs 几十万像素。

但低维是常见的设计选择,不是 latent 的定义。

Latent 的定义来自 unobserved / hidden。即使 \(\text{dim}(Z) \gt \text{dim}(X)\),\(Z\) 仍然可以是 latent variable——只要它没有被直接观察到。把 latent variable 定义成"低维向量"会在后面遇到一些概念上对不上的情况。

Z 和 z 的区别
#

顺便理清一个符号问题。

\(Z\) 是 latent random variable(潜在随机变量)——一个可以取不同值的随机变量,服从某个分布 \(p(z)\)。

\(z\) 是某一次具体的取值——一个 latent code 或者 latent vector,比如 \(z = [0.2, -0.7, 1.3, \ldots]\)。

打个比方:\(Z\) 是"骰子",\(z\) 是"这次扔出来的 3"。后面讨论 latent space 时,讨论的是所有可能的 \(z\) 取值构成的空间。

生成语义:z → x
#

回到 latent variable 和 embedding 的对比。

之前那篇 embedding 的文章里,整条链路是:

1input x → embedding → representation → downstream task (prediction)

方向是"从输入到表示再到预测"。Embedding 的目的是把对象变成适合后续计算的 representation。

但 latent variable 在生成模型里的故事是另一个方向:

1latent z → generation process → observable x

模型假设存在某个 hidden state \(z\),通过某个生成过程产出了我们观察到的数据 \(x\)。\(z\) 不是从 \(x\) “算出来的”——至少在建模逻辑上,\(z\) 在先,\(x\) 在后。

生成方向与推断方向

这是 latent variable 和普通 embedding 的第一个真正区别:latent variable 带有生成语义。它不只是"把数据变成向量",而是"假设 observable data 背后存在一个没有被直接观察到的 hidden state,并且从这个 hidden state 出发可以生成数据"。

“从果倒推因”——这个直觉哪里对
#

发现上面这个区别之后,我产生了一个更进一步的直觉:

Embedding 是"因"——拿输入去预测结果。 Latent variable 是"果"——我们看到的只是结果(图片像素),然后试图倒推背后的隐藏原因。

说得再直白一点:

  • 生成方向:\(z \to x\)
  • 推断方向:\(x \to z\)

现实中我们只能观察到 \(x\),但我们想知道"到底是什么样的 \(z\) 生成了这个 \(x\)"。这确实有一种"从结果反推隐藏解释"的味道。

这个直觉有一半非常重要。

“有一半"的意思是:generative direction 和 inference direction 确实构成了两个方向,而且"观察 \(x\),推断背后的 hidden state \(z\)“确实是很多生成模型训练时在做的事情。

需要收紧的地方在"因果"两个字上。

Latent variable 并不自动等于 causal variable(因果变量)。模型里的 \(z\) 是模型为了解释数据而引入的 hidden state,但它未必对应现实世界中某个真实的因果机制。一个生成模型学到的 \(z\) 可能只是一种数学上能把数据分布拟合好的 hidden representation,不是说"世界上真的有一个变量叫 \(z\),它在物理上导致了这张图片的产生”。

所以更准确的说法是:

把 latent variable 理解成"生成数据背后的隐藏解释"很有帮助。但最好把"解释"理解成模型中的 hidden explanation,而不是严格因果推断意义上的现实因果变量。

“从果倒推因"是一个很好用的直觉——只要不把"因"理解得太硬。

Latent Space 到底是什么
#

搞清楚了 latent variable 之后,下一个概念就是 latent space(潜空间)。

我最初的理解是:“latent space 是一种比 pixel space 更 high-level 的 representation。”

这个直觉方向对了一半,但需要修正。

更准确地说:latent space 是 latent variable \(Z\) 的所有可能取值构成的空间。 如果 \(z \in \mathbb{R}^d\),那 latent space 就是 \(\mathbb{R}^d\)。

它本身并不要求"更 high-level"或者"更抽象”。只是一个好的 latent representation——也就是模型学得好的时候——往往确实比原始像素更能反映数据背后的结构。

给数据换一套坐标系
#

这是我后来觉得最有帮助的一个理解角度。

Pixel space 的坐标是什么?每个维度对应一个像素通道的数值:pixel[0,0,R]、pixel[0,0,G]、pixel[0,0,B]、pixel[0,1,R] ……一张 \(256 \times 256 \times 3\) 的图片就是一个约 20 万维空间中的一个点。

这套坐标系非常精确地描述了一件事:图片最终长什么样。

但它未必适合描述另一件事:图片背后的结构是什么。

一个简单的例子:同一个人,只是把头稍微转了 5 度。

从人类视角看,两张图几乎一样——同一个人、同一个表情、同一个背景,只是角度微调了一点。但从像素角度看,大量像素值都发生了变化——脸部轮廓位移了、阴影重新分布了、背景被遮挡的区域也变了。

像素距离 vs 语义距离

像素距离可能很大,语义差异却很小。

反过来也成立:两张完全不同的脸,恰好在某些像素位置数值接近,像素距离可能不大,但语义上毫无关系。

像素空间非常适合精确记录图片,却未必适合描述图片背后的结构。

Latent space 可以理解成:模型为复杂数据建立的另一套内部坐标系统。在这套新坐标下,模型尝试用自己的维度来编码数据的内在结构。如果学得好,原来在像素空间里需要大量维度才能描述的变化(比如"转头 5 度”),在潜空间里可能只对应某些维度上的微小移动。

注意:这不是简单的降维。降维只是把维度变少了,但 latent space 更关键的特性是表示方式本身变了。核心是 representation change——用一套对数据结构更友好的坐标替代原来对存储更友好的坐标。

为什么叫 Space
#

Latent variable 是一个向量。为什么不直接叫 latent vector,要叫 latent “space”?

因为真正有价值的不只是某一个具体的 \(z\),而是所有可能的 \(z\) 之间的关系。

一个 space 意味着点与点之间存在结构。在 latent space 中,值得关注的结构包括:

  • distance(距离):两个 latent code 之间有多"远"?在一个好的潜空间里,语义相近的数据对应的 latent code 也应该彼此接近。
  • neighborhood(邻域):一个 latent code 附近还有什么?如果 \(z_A\) 附近的 \(z\) 都对应同一类数据,那说明这个区域在语义上是连贯的。
  • direction(方向):从一个 latent code 沿某个方向移动,生成的数据会如何变化?
  • interpolation(插值):在两个 latent code 之间取中间状态,结果是否合理?

插值最能说明问题。假设有两个 latent code \(z_A\) 和 \(z_B\),对应两张不同的图片。取它们的线性插值:

$$ z(t) = (1-t) z_A + t z_B, \quad t \in [0, 1] $$
Latent space 插值

如果潜空间的结构是好的,那么 \(t\) 从 0 变到 1 的过程中,对应生成的图片也会平滑变化——从 \(z_A\) 对应的图片逐渐过渡到 \(z_B\) 对应的图片,中间不会突然出现无意义的噪声。

为什么这很重要?因为它在观察一件事:两个合理状态之间的区域,是不是仍然对应合理的数据。 如果是,说明模型学到的不只是一些孤立的样本点,而是捕获了某种连续的数据结构。如果中间全是噪声,说明潜空间里有大片"空洞",模型对数据分布的理解是碎片化的。

这里可以借用一个类比:地图。

现实世界里有山川河流、街道建筑,非常复杂。地图为它建立了经度/纬度坐标系。地图不是世界本身,但有了坐标之后,距离、方向、邻近关系都变得可以度量了。

Latent space 也类似。它不是数据本身,而是模型为数据建立的一张"内部结构地图"。不过有一个区别:经纬度的语义非常明确(东西方向、南北方向),而 latent space 的坐标轴语义通常没有这么清晰——后面会讲到这一点。

Pixel Space vs Latent Space
#

把两者放在一起对比,可能更清楚:

Pixel space 描述的是 observable appearance——“这个位置的 RGB 数值是多少”。它直接回答"图片最终长什么样"。

Latent space 描述的是 hidden representation——“这个样本处于怎样的隐藏状态”。它尝试回答"让图片长成这样的内部状态是什么"。

一个恰当的总结是:pixel space 是存储空间,latent space 是结构空间。前者精确但不反映高层关系,后者抽象但可能更接近数据的内在组织方式。

不过要注意一点:说"更接近内在组织方式"不等于说潜空间一定学到了人类能理解的明确概念。模型可能编码了与姿态、光照、表情有关的信息,但这些因素在 latent dimensions 中通常是混合的——一个维度可能同时包含姿态和光照的一部分信息,一个"微笑"的概念可能分布在多个维度上。

为什么 Latent Space 经常看起来有高层语义
#

一个自然的问题:既然 latent space 本质上只是一个向量空间,为什么很多生成模型的 latent space 在实验中表现得像是编码了高层语义?

原始 pixel dimension 的含义非常底层——“某个空间位置的某个颜色通道的数值”。但 latent representation 中的某些变化方向,可能对应:

  • 姿态变化
  • 形状变化
  • 风格变化
  • 光照变化
  • 物体结构变化

所以 latent space 常常显得比 pixel space 更抽象。

但这里要区分两件事:

latent space has structure(潜空间有结构)

every latent dimension is interpretable(每个潜在维度都可解释)

这两者完全不是一回事。

前面提到的 entanglement(因素纠缠)正是这个问题。我们很容易想象一种理想情况:\(z_1\) 控制年龄,\(z_2\) 控制微笑,\(z_3\) 控制光照——每个维度对应一个清晰的人类可理解的因素。但真实模型通常不是这样。一个维度可能同时编码了姿态、光照和身份的一部分信息。一个高层概念也可能分散在多个维度中。

如果把因素纠缠的反面叫做 disentanglement(解耦——让不同潜在因素尽可能分开表示),那确实有很多研究在试图实现这个目标。但这是一个额外的设计追求,不是 latent space 自带的性质。

Latent Space 不是"宇宙真实坐标系"
#

还有一个更根本的点。

如果模型学到了一个 \(z \in \mathbb{R}^{128}\) 的 latent space,这并不意味着"世界上真的存在 128 个隐藏变量,模型把它们发现出来了"。

更准确的理解:这是模型找到的一种能够解释/表示数据的 hidden representation。而且这种表示通常不是唯一的。对 latent space 做某些可逆变换(比如旋转、缩放),可能得到一套完全不同的坐标,但它们仍然能同样好地表示数据。

这个问题在统计学里有个正式名称:identifiability(可辨识性)——模型的参数(包括 latent representation)能否从数据中被唯一确定。很多情况下答案是否定的,但这里不展开。

记住一个结论就够了:latent space 是模型建立的一套内部表示坐标,而不是自动等于真实世界唯一正确的隐藏结构。

回到最初的问题:和 Embedding 到底是什么关系
#

绕了一圈,现在可以认真回答最开始的那个直觉了。

先说相似的地方。Latent variable 和 embedding 确实非常像:

  1. 它们都在做 representation learning——把复杂对象变成某种可以计算的表示。
  2. 结果都是一个向量。
  3. 这些向量构成一个 representation space。
  4. 在这个空间里,相似的对象往往彼此接近。
  5. 从神经网络实现上看,一个 latent code 在数值形式上完全可能就是一个普通的 embedding-like 向量。

所以"这不就是 embedding 吗"这个直觉有相当的道理。

但关键区别在建模语义。

Embedding 更强调的问题是:怎样把对象表示成一个适合后续计算的 representation? Token 变成 embedding 向量是为了输入 Transformer,图片变成 feature vector 是为了做分类或检索。方向是 \(x \to \text{representation} \to \text{task}\)。

Latent variable 更强调的问题是:我假设 observable data 背后存在一个没有直接观察到的 hidden variable,且这个 hidden variable 通过某个生成过程产出了 data。 方向是 \(z \to x\)。这里 \(z\) 不只是"方便计算的表示",它在建模逻辑中扮演了"数据的隐藏生成解释"这个角色。

Embedding vs Latent Variable

所以结论是:

Latent code 往往可以看成一种 representation,甚至在数值形式上就是一个 embedding-like 向量。但不是所有 embedding 都是 latent variable。 一个 token embedding 是 representation,但它没有"observable data 背后的 hidden generative explanation"这层语义。一个生成模型的 latent code 既是 representation,又是 latent variable。

两者的重叠区域很大,但 latent variable 多出来的那一层——hidden-variable / generative semantics——正是生成模型的核心所在。

为什么这一切对生成模型很重要
#

最后把视角拉高一层。

假设我们直接在 pixel space 里试图生成图片。一张 \(256 \times 256 \times 3\) 的图片所在的空间有大约 20 万个维度。在这个极高维空间里随机选一个点,得到的几乎一定是噪声——毫无意义的像素组合。

原因很直观:真实图片只是这个巨大空间中极其微小的一部分。之前讨论概率密度时提到过 \(p(x)\)——数据的概率分布。真实图片对应的区域,在整个像素空间中的占比几乎为零。直接在这个空间里乱撞,碰到合理数据的概率微乎其微。

Latent-variable modeling 提供了一个完全不同的思路:

不要直接在复杂的数据空间中生成数据。而是:

  1. 构造一个比较简单、有结构的 latent space。
  2. 在其中采样一个 latent state \(z\)——这通常容易得多,比如从一个标准正态分布 \(p(z)\) 里抽。
  3. 再通过一个学到的映射把 \(z\) 变成复杂的 observable data \(x\)。
1simple latent state z
23   generation process
45complex observable data x

这个思路的核心可以用一句话概括:把"直接描述一个极复杂的数据分布"的问题,重新表达成"一个比较简单的隐藏空间 + 一个把隐藏状态映射为复杂数据的生成过程"。

复杂性没有消失,但它被拆分了。简单的先验分布 \(p(z)\) 负责提供"从哪里开始",学到的生成过程负责"怎么把起点展开成最终看到的数据"。

至于这个思路具体怎么实现——怎么训练、怎么让模型学到好的 latent space、怎么处理 \(p(x)\) 和 \(p(z)\) 之间的关系——这些是 VAE、GAN、Diffusion 等具体生成模型各自要回答的问题,留到后面再写。

写在最后
#

回看最初的那个反应:“latent variable 不就是某种 embedding 吗?”

现在我的理解变了。

它确实是一种 representation,所以和 embedding 很像——都是把复杂对象变成向量,形成一个可以度量距离和方向的空间。但 latent variable 更重要的语义是:模型假设 observable data 背后存在一个没有直接观察到的 hidden state,并且从 \(z \to x\) 的方向建模。

Latent space 则是这些 hidden states 所处的表示空间。它真正重要的地方,不是简单地把数据维度降低了,而是给复杂数据建立了一套更适合描述内部结构的坐标系统。

而生成模型面对的核心困难,是现实数据分布极其复杂。Latent-variable modeling 的一个核心思想,是不要直接在复杂的数据空间里生成世界,而是先找到一个更简单的隐藏表示,再把这个隐藏状态展开成我们最终看到的数据。

参考资料
#

Related

9 年后重温 CNN:剥掉算子细节后,真正留下了什么

最近在重新过 MIT 6.S191 Lecture 3(卷积神经网络)。2017 年刚接触 CV 那会儿,CNN 算是吃饭的家伙,每天都在调。后来精力逐渐转到 ML Infra,成天跟 GPU 显存、通信拓扑和算子优化打交道,卷积网络的很多具体细节就慢慢生疏了——写个 nn.Conv2d 时 weight 的四维形状到底怎么排、Kaiming 初始化的方差怎么推、感受野怎么算,冷不丁被问到,还得在脑子里卡壳一下。