通常のVAE(変分オートエンコーダ)は、データ全体の分布 $p(\bm{x})$ を学習して新しいデータを生成します。しかし実務では「指定したラベルの数字を生成したい」「いまの動作モードを踏まえたうえで正常かどうか判定したい」のように、何らかの条件を踏まえて生成・判定したい場面が頻繁にあります。素のVAEは「どんなデータが来るか」しか知らないので、こうした条件付きの要求には応えられません。
この「条件 $\bm{c}$ を踏まえる」をVAEに組み込んだのが 条件付きVAE(CVAE: Conditional VAE) です。CVAEは無条件の $p(\bm{x})$ ではなく、条件付き分布 $p(\bm{x} \mid \bm{c})$ を学習します。これにより、
- 問い1: ラベルや属性 $\bm{c}$ を指定して、その条件に合うデータだけを生成するにはどうすればよいか?
- 問い2: 「同じ値でも文脈次第で正常にも異常にもなる」状況を、どう数式とモデルで扱うか?
に答えられるようになります。応用は広く、条件付き画像生成(クラスや属性を指定して生成)と、コンテキスト条件付き異常検知(動作モードや操作イベントを条件に、$p(\bm{x}\mid\bm{c})$ が低い観測を異常とする)が代表例です。後者は、たとえば産業設備で「加熱機器をONにした後の温度上昇は正常、操作なしの上昇は異常」のように、条件によって正常の意味が変わる問題にそのまま効きます。

両者の違いは図の通りシンプルです。VAE(左)はエンコーダ・デコーダが $\bm{x}$ と $\bm{z}$ だけを扱うのに対し、CVAE(右)は条件 $\bm{c}$ をエンコーダとデコーダの両方に与えます。たったこれだけの変更が、無条件生成を条件付き生成に変えます。本記事ではこの仕組みを、ELBOの導出からスクラッチ実装まで追っていきます。
前提知識
この記事はVAEの理論を前提にします。ELBOや再パラメータ化が不安なら、先に次を読むとスムーズです。
まずは「VAEに条件を足すと、確率モデルとして何が変わるのか」を整理します。
VAEからCVAEへ
VAEは潜在変数モデル $p_{\bm{\theta}}(\bm{x}) = \int p_{\bm{\theta}}(\bm{x}\mid\bm{z}) p(\bm{z})\, d\bm{z}$ を学習しました。CVAEでは、ここに条件 $\bm{c}$ を加え、条件付きの周辺尤度を考えます。
$$ \begin{equation} p_{\bm{\theta}}(\bm{x}\mid\bm{c}) = \int p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})\, p(\bm{z}\mid\bm{c})\, d\bm{z} \end{equation} $$
ここで登場する分布は、すべて $\bm{c}$ で条件付けられています。
- デコーダ $p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})$: 潜在変数 $\bm{z}$ と条件 $\bm{c}$ から $\bm{x}$ を生成する
- 事前分布 $p(\bm{z}\mid\bm{c})$: 条件付きの潜在事前分布。多くの実装では簡単のため $\bm{c}$ に依存しない $\mathcal{N}(\bm{0},\bm{I})$ を使う
- エンコーダ(近似事後) $q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})$: $\bm{x}$ と $\bm{c}$ から潜在分布を推論する
条件 $\bm{c}$ は用途に応じて様々な形を取ります。クラスラベルなら one-hot ベクトル、属性なら実数ベクトル、時系列の文脈なら過去の観測や操作イベント列の埋め込みです。重要なのは、$\bm{c}$ をエンコーダとデコーダの両方に与えること。デコーダだけに条件を与えても生成は条件付けられますが、エンコーダが条件を知らないと近似事後の推論が条件と食い違い、学習が不安定になりやすいのです。

図がCVAEの全体構成です。VAEと骨格は同じで、エンコーダ $q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})$ が $\bm{\mu},\log\bm{\sigma}^2$ を出し、再パラメータ化 $\bm{z}=\bm{\mu}+\bm{\sigma}\odot\bm{\varepsilon}$ で $\bm{z}$ をサンプリングし、デコーダ $p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})$ が再構成します。違いは、赤い矢印が示すように条件 $\bm{c}$ がエンコーダとデコーダの両方に注入される点だけです。では、この条件付きモデルの学習目標であるELBOを導きましょう。
条件付きELBOの導出
VAEと同じ要領で、対数条件付き周辺尤度 $\log p_{\bm{\theta}}(\bm{x}\mid\bm{c})$ の下界を導きます。出発点は、近似事後 $q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})$ と真の事後 $p_{\bm{\theta}}(\bm{z}\mid\bm{x},\bm{c})$ のKLダイバージェンスです。これは非負なので、
$$ D_{\mathrm{KL}}\!\left(q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c}) \,\|\, p_{\bm{\theta}}(\bm{z}\mid\bm{x},\bm{c})\right) \geq 0 $$
が常に成り立ちます。条件付きのベイズの定理 $p_{\bm{\theta}}(\bm{z}\mid\bm{x},\bm{c}) = \dfrac{p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})\,p(\bm{z}\mid\bm{c})}{p_{\bm{\theta}}(\bm{x}\mid\bm{c})}$ をこのKLに代入して整理します。具体的には、KLの定義に代入して対数を分解すると
$$ D_{\mathrm{KL}}(q \,\|\, p_{\text{post}}) = \mathbb{E}_{q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})}\!\left[\log q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c}) – \log p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c}) – \log p(\bm{z}\mid\bm{c}) + \log p_{\bm{\theta}}(\bm{x}\mid\bm{c})\right] $$
となります。ここで $\log p_{\bm{\theta}}(\bm{x}\mid\bm{c})$ は $\bm{z}$ に依存しないので期待値の外へ出せます。これを $\log p_{\bm{\theta}}(\bm{x}\mid\bm{c})$ について移項すると、次の関係が得られます。
$$ \begin{align} \log p_{\bm{\theta}}(\bm{x}\mid\bm{c}) =\ & \mathbb{E}_{q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})}\!\left[\log p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})\right] \\ & – D_{\mathrm{KL}}\!\left(q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c}) \,\|\, p(\bm{z}\mid\bm{c})\right) \\ & + D_{\mathrm{KL}}\!\left(q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c}) \,\|\, p_{\bm{\theta}}(\bm{z}\mid\bm{x},\bm{c})\right) \end{align} $$
右辺の最後のKL項は非負なので、これを落とせば下界(条件付きELBO)が得られます。
$$ \begin{equation} \log p_{\bm{\theta}}(\bm{x}\mid\bm{c}) \geq \underbrace{\mathbb{E}_{q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})}\!\left[\log p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})\right]}_{\text{再構成誤差}} – \underbrace{D_{\mathrm{KL}}\!\left(q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c}) \,\|\, p(\bm{z}\mid\bm{c})\right)}_{\text{KL正則化項}} \end{equation} $$

図のように、形はVAEのELBOとまったく同じ「再構成誤差 − KL正則化」です。唯一かつ本質的な違いは、すべての分布が $\bm{c}$ で条件付けられていること。再構成項は「$\bm{c}$ を踏まえて $\bm{x}$ を復元できるか」を、KL項は「条件付き近似事後を事前分布 $p(\bm{z}\mid\bm{c})$ に近づける」ことを測ります。VAEのELBOを完全に理解していれば、CVAEは「全部に $\bm{c}$ を付けるだけ」で導けるわけです。
同じELBOは、VAEのときと同様にイェンセンの不等式からも導けます。$\log p_{\bm{\theta}}(\bm{x}\mid\bm{c}) = \log \mathbb{E}_{q}\!\left[p_{\bm{\theta}}(\bm{x},\bm{z}\mid\bm{c}) / q_{\bm{\phi}}(\bm{z}\mid\bm{x},\bm{c})\right]$ と書き、$\log$ の凹性から期待値を中に入れれば、まったく同じ下界が得られます。どちらの導出をたどっても、ELBOと真の対数尤度の差が「近似事後と真の事後のKL」になっている、という構造は変わりません。
事前分布を $p(\bm{z}\mid\bm{c})=\mathcal{N}(\bm{0},\bm{I})$ と置けば、KL項はVAEと同じ解析解
$$ D_{\mathrm{KL}} = -\frac{1}{2}\sum_{j=1}^{d}\left(1 + \log\sigma_j^2 – \mu_j^2 – \sigma_j^2\right) $$
がそのまま使えます。
なお、事前分布を条件依存にする($p(\bm{z}\mid\bm{c})$ を $\bm{c}$ ごとに変える)設計もあり、条件によって潜在の事前構造が大きく異なる場合に有効です。しかし多くの場合、$\mathcal{N}(\bm{0},\bm{I})$ 固定でも条件の影響はデコーダ $p_{\bm{\theta}}(\bm{x}\mid\bm{z},\bm{c})$ が十分に吸収できるため、本記事でも簡潔さを優先して $\mathcal{N}(\bm{0},\bm{I})$ を採用します。残る実装上の問題は「条件 $\bm{c}$ を具体的にどうネットワークに入れるか」です。
条件の注入:最も基本的な「連結」
条件 $\bm{c}$ をネットワークに与える最も単純で確実な方法が連結(concatenation)です。入力ベクトルに条件ベクトルをそのままくっつけて、1つの大きな入力としてネットワークに渡します。

図のように、エンコーダには $[\bm{x};\bm{c}]$(入力と条件の連結)を、デコーダには $[\bm{z};\bm{c}]$(潜在変数と条件の連結)を入力します。$\bm{c}$ がカテゴリ(クラスラベルなど)なら one-hotベクトルにして連結するのが定石です。連結はモデルを選ばない汎用テクニックで、MLPでもCNNでも使えます。
補足すると、条件の注入法は連結だけではありません。条件からスケールとシフトを生成して中間特徴を変調する FiLM、条件を系列として参照する クロスアテンション など、条件が高次元・系列・時間依存の場合に強い手法があります。ただしどれも「$\bm{c}$ をネットワークの計算に介入させて出力を条件付ける」という発想は共通で、連結はその最も素朴な実装です。まずはこの連結でCVAEの本質を掴みましょう。それでは実装に入ります。
Pythonでスクラッチ実装する
理論を最短で体得するには、自分で実装するのが一番です。重いデータは使わず、条件ごとに分布が違う2D合成データでCVAEを動かし、「条件を指定して作り分けられるか」を目で確認します。
合成データの生成
3つのクラス(条件 $\bm{c}\in\{0,1,2\}$)が、2D平面の異なる位置にガウス分布を持つデータを作ります。
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
rng = np.random.default_rng(0)
K, n_per = 3, 400
centers = np.array([[0, 2.5], [-2.5, -1.5], [2.5, -1.5]], dtype=np.float32)
X = np.vstack([rng.normal(centers[k], 0.5, (n_per, 2)) for k in range(K)]).astype(np.float32)
y = np.repeat(np.arange(K), n_per)
C = np.eye(K, dtype=np.float32)[y] # 条件をone-hot化
Xt, Ct = torch.tensor(X), torch.tensor(C)
centers は各クラスの中心で、三角形に配置しています。条件 C は3次元のone-hotベクトルです。このデータで「条件 $\bm{c}=k$ を指定したら、$k$ 番目のガウスを生成する」CVAEを学習します。

図が生成したトイデータです。3つのクラスが平面上の別々の位置に分かれています。同じモデルが、条件 $\bm{c}$ の指定だけでこの3つを作り分けられるようにするのが目標です。
CVAEモデルの定義
エンコーダは $[\bm{x};\bm{c}]$ を受けて $\bm{\mu},\log\bm{\sigma}^2$ を、デコーダは $[\bm{z};\bm{c}]$ を受けて $\bm{x}$ を出力します。VAEに「条件の連結」を足しただけの素直な構造です。
class CVAE(nn.Module):
def __init__(self, xdim=2, cdim=3, h=64, zdim=2):
super().__init__()
# エンコーダ: [x;c] -> (μ, log σ²)
self.e1 = nn.Linear(xdim + cdim, h)
self.emu = nn.Linear(h, zdim)
self.elv = nn.Linear(h, zdim)
# デコーダ: [z;c] -> x
self.d1 = nn.Linear(zdim + cdim, h)
self.dout = nn.Linear(h, xdim)
def encode(self, x, c):
h = F.relu(self.e1(torch.cat([x, c], dim=1))) # 条件を連結
return self.emu(h), self.elv(h)
def decode(self, z, c):
return self.dout(F.relu(self.d1(torch.cat([z, c], dim=1)))) # 条件を連結
def forward(self, x, c):
mu, logvar = self.encode(x, c)
z = mu + torch.exp(0.5 * logvar) * torch.randn_like(mu) # 再パラメータ化
return self.decode(z, c), mu, logvar
torch.cat([x, c], dim=1) が条件の連結そのものです。encode でも decode でも条件 c を一緒に渡しているのがポイントで、これが条件付きELBOの $q(\bm{z}\mid\bm{x},\bm{c})$ と $p(\bm{x}\mid\bm{z},\bm{c})$ に対応します。
VAEからCVAEへの実装変更は、突き詰めると次の3点だけです。①エンコーダ入力を $\bm{x}$ から $[\bm{x};\bm{c}]$ に、②デコーダ入力を $\bm{z}$ から $[\bm{z};\bm{c}]$ に、③学習・生成時に条件 $\bm{c}$ を一緒に渡す。 損失関数(ELBO)もKLの解析解も再パラメータ化も、VAEのコードがそっくりそのまま流用できます。理論の「全部に $\bm{c}$ を付けるだけ」が、実装でも「入力に $\bm{c}$ を連結するだけ」として現れているわけです。
学習
損失は条件付きELBOの符号反転、すなわち「再構成誤差 + KL正則化」です。連続値の2Dデータなので再構成は二乗誤差(ガウスデコーダ)を使います。
model = CVAE()
opt = torch.optim.Adam(model.parameters(), lr=5e-3)
for epoch in range(600):
opt.zero_grad()
recon, mu, logvar = model(Xt, Ct)
rec_loss = F.mse_loss(recon, Xt, reduction='sum') # 再構成誤差
kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) # KL正則化
loss = rec_loss + kl
loss.backward()
opt.step()
print("最終損失/サンプル:", round(loss.item() / len(Xt), 3))
2次元・小さなネットワークなので、CPUでも数秒で学習が終わります。VAEの損失に対する変更はゼロで、モデル側で条件を連結しただけ——これがCVAEの実装の軽さです。
ひとつ実務的な注意点があります。条件 $\bm{c}$ が強力で、デコーダが $\bm{c}$ だけから $\bm{x}$ をほぼ復元できてしまう場合、潜在変数 $\bm{z}$ が使われなくなる posterior collapse(KL項が0に張り付き、$\bm{z}$ が無視される現象)が起きることがあります。今回のように $\bm{z}$ が「クラス内のばらつき」を担う設計では問題になりにくいですが、条件がほぼ出力を決めてしまうタスクでは、学習初期にKL項の重みを徐々に上げる KLアニーリング などの対策が有効です。
条件を指定して生成する
学習したCVAEで、事前分布 $\bm{z}\sim\mathcal{N}(\bm{0},\bm{I})$ からサンプルし、条件 $\bm{c}$ を指定してデコードします。ガウスデコーダの観測分散を反映するため、生成にわずかな観測ノイズを加えています。
model.eval()
with torch.no_grad():
for k in range(K):
z = torch.randn(300, 2)
c = torch.zeros(300, 3); c[:, k] = 1 # 条件をc=kに固定
gen = model.decode(z, c).numpy() + rng.normal(0, 0.4, (300, 2)).astype(np.float32)
print(f"c={k} 生成平均 {gen.mean(0).round(2)} 真の中心 {centers[k]}")
実行すると、生成平均は c=0: [-0.03, 2.51]、c=1: [-2.48, -1.51]、c=2: [2.46, -1.52] となり、真の中心 [0,2.5] / [-2.5,-1.5] / [2.5,-1.5] をほぼ正確に再現しました。

左が元データ、右がCVAEの生成結果です。同じ潜在分布 $\mathcal{N}(\bm{0},\bm{I})$ から出発しながら、条件 $\bm{c}$ を変えるだけで3つのクラスを正しく作り分けられていることがわかります。これがCVAEの威力で、無条件のVAEでは「どれかわからない数字」しか作れないのに対し、CVAEは「指定したクラス」をピンポイントで生成できます。
潜在空間に何が起きているか
CVAEの潜在空間を覗くと、VAEとは違う興味深い構造が見えます。

VAEでは潜在空間にクラスごとのクラスタができましたが、CVAEの潜在変数 $\bm{z}$ はクラスによらず $\mathcal{N}(\bm{0},\bm{I})$ に重なっています。これは、クラスを区別する情報を条件 $\bm{c}$ が担うため、潜在変数 $\bm{z}$ は「クラス以外のばらつき」だけを表せばよくなるからです。役割分担——$\bm{c}$ が「何のクラスか」、$\bm{z}$ が「その中でのバリエーション」——が自然に生まれているのです。
この役割分担は、条件付け一般に共通する大切な性質です。条件 $\bm{c}$ で説明できる変動は $\bm{c}$ に任せ、$\bm{z}$ は残差的な変動だけを担います。だからこそ、条件さえ指定すれば $\bm{z}$ をランダムに振るだけで、その条件に合った多様なサンプルが得られます。この「条件で大枠を決め、潜在で細部を変える」構造は、後に学ぶ運転条件と運転状態の分離(disentanglement)の素朴な原型とも言え、コンテキストを扱うモデル設計の出発点になります。
このことは、条件を変えたときの生成を見るとさらにはっきりします。

潜在変数 $\bm{z}$ をほぼ固定したまま条件 $\bm{c}$ だけを $0,1,2$ と変えると、生成される点が3つのクラス位置へきれいに移動します。$\bm{z}$ ではなく $\bm{c}$ がデコーダの出力を制御していることの直接的な証拠です。「条件付け」が確かに効いていることが、目で確認できました。
ここまでで条件付き生成を理解しました。次に、この $p(\bm{x}\mid\bm{c})$ が異常検知にどう効くかを見ます。これがコンテキストを考慮した異常検知の出発点になります。
応用:条件付き尤度 P(Y|C) による異常検知
VAEが再構成確率や尤度で異常検知に使えたように、CVAEは条件付き尤度 $p(\bm{x}\mid\bm{c})$ で異常を測れます。ポイントは、同じ観測でも条件 $\bm{c}$次第で正常にも異常にもなる点を自然に扱えることです。

図は産業センサーの時系列を模した例です。ある操作イベントの後(赤の破線以降)、センサー値が上昇しています。条件を考慮しない普通の異常検知器は「正常範囲(灰色帯)を超えた」として、この上昇を異常と誤検知します。しかしこれは操作による正常な計画変動です。条件 $\bm{c}$(操作イベントの有無)を入力に持つCVAEなら、「操作ありの条件下ではこの上昇は高い尤度=正常」と判定でき、偽陽性を避けられます。
なぜそう判定できるかは、条件付き尤度の形を見ると明快です。

図のように、CVAEは条件ごとに異なる「正常な範囲」$p(y\mid c)$ を学習します。観測値 $y=2.4$ は、条件 $c=0$(操作なし)の下では低尤度で異常ですが、条件 $c=1$(操作あり)の下では高尤度で正常です。つまり異常スコアを $-\log p(\bm{x}\mid\bm{c})$ と定義すれば、条件に応じて動く正常範囲を1つのモデルで表現できます。
実際の異常スコアには、この条件付き対数尤度のほか、条件 $\bm{c}$ を与えて再構成したときの条件付き再構成誤差 $\|\bm{x}-\hat{\bm{x}}_{\bm{c}}\|^2$ や、条件付きELBOの値そのものがよく使われます。いずれも「その条件のもとで、この観測がどれだけ”らしくない”か」を測っている点は同じです。「変化そのもの」ではなく「文脈の中での逸脱」を捉える——これがコンテキストを考慮した異常検知の核心で、CVAEはその最も基本的な実装になっています。
なお、CVAEの応用は異常検知だけではありません。ラベルを条件にした半教師あり学習、欠損した属性を条件として補う欠損値補完、少数クラスを条件指定で増やすデータ拡張など、「条件を指定して生成・推論する」あらゆる場面で活躍します。条件付き分布 $p(\bm{x}\mid\bm{c})$ をモデル化できることの汎用性の高さがうかがえます。
まとめ
条件付きVAE(CVAE)を、理論からスクラッチ実装、異常検知への応用まで通して見てきました。
- CVAEは無条件の $p(\bm{x})$ ではなく条件付き分布 $p(\bm{x}\mid\bm{c})$ を学習する。エンコーダ・デコーダの両方に条件 $\bm{c}$ を与えるだけで実現できる。
- 条件付きELBOは $\log p(\bm{x}\mid\bm{c}) \geq \mathbb{E}_{q}[\log p(\bm{x}\mid\bm{z},\bm{c})] – D_{\mathrm{KL}}(q(\bm{z}\mid\bm{x},\bm{c})\,\|\,p(\bm{z}\mid\bm{c}))$ で、VAEのELBOに $\bm{c}$ を付けただけの形。
- 条件の注入は連結(concatenation)が最も基本。$[\bm{x};\bm{c}]$ と $[\bm{z};\bm{c}]$ を入力するだけ。
- 2D合成データで、$\bm{z}\sim\mathcal{N}(\bm{0},\bm{I})$ から条件 $\bm{c}$ を指定して各クラスを作り分けられた。潜在変数 $\bm{z}$ はクラス情報を $\bm{c}$ に明け渡し、$\mathcal{N}(\bm{0},\bm{I})$ に整う。
- 条件付き尤度 $p(\bm{x}\mid\bm{c})$ により、「同じ値でも文脈次第で正常/異常が変わる」コンテキスト認識型の異常検知が自然に書ける。
CVAEは「条件付け」という汎用的な発想を、生成モデルに最もクリーンに組み込んだ手法です。ここを起点に、条件の注入をより強力にする手法(FiLM・クロスアテンション)、条件付きの密度を厳密尤度で測る条件付き正規化フロー、潜在因子を分離するβ-VAEへと発展させると、コンテキストを考慮した生成・異常検知の世界が一気に広がります。
次のステップとして、以下もあわせて読むと理解が立体的になります。