メインコンテンツまでスキップ

DDPMとは?

概要 (画像は、Geminiで作成されたものです)

DDPMの概要

DDPM(Denoising Diffusion Probabilistic Models)は、非平衡熱力学からの考察に影響を受けた潜在変数モデルのクラスである「拡散確率モデル」を用いて、 高品質な画像合成を提示する手法です。論文「Denoising Diffusion Probabilistic Models」で提案され、現在の画像生成AI(Stable Diffusionなど)の基礎となる重要なブレイクスルーとなりました。

生成モデルの学習において、DDPMは元の信号が破壊されるまで徐々にノイズを追加する「拡散過程(forward process)」を定義し、それを逆転させるようにマルコフ連鎖の遷移を学習します。このプロセスを微小なガウスノイズの追加によって構成することで、サンプリングを条件付きガウス分布に限定することができ、極めてシンプルなニューラルネットワークのパラメータ化が可能になります。

DDPMは画像生成の品質において大きな飛躍を遂げ、無条件のCIFAR10データセットで9.46のInceptionスコアと、当時最先端となる3.17のFIDスコアを獲得しました。また、256x256のLSUNデータセットでもProgressiveGANと同等のサンプル品質を達成しています。

VAEやGANとの違い

VAEとGAN
(画像は、Geminiで作成されたものです)

DDPM(Denoising Diffusion Probabilistic Models)がノイズから極めて高品質なデータを生成できる最大の理由は、従来の生成モデルが挑んでいた「困難な一発変換」を避け、「複雑なタスクを極限まで細かく分解して学習させる」 というアプローチをとっているからです。

例えば、AIに対して「完全にランダムなノイズの塊から、一瞬で完璧な猫の画像を描け」と指示しても、正解の可能性が無限にありすぎて、一発でマッピングを学習するのは非常に困難です。

DDPMはこの課題を、以下のように極めて単純な「ノイズ除去の微小ステップ」の連続へと細分化しました。

  1. 数学的に簡単に作れる完全なランダムノイズ xTx_T を用意する。
  2. AIに「この画像から、ほんの1ステップ分だけノイズを取り除いて(ノイズを予測して引いて)」という非常に簡単なタスクを指示する。
  3. この微小なノイズ除去を数百〜数千回繰り返すことで、最終的に高精細な画像 x0x_0 に到達する。

つまり、「複雑すぎる1回の大変換を、AIが予測しやすい単純なステップの連続に分解したこと」 が、DDPMがこれほど高精度に動作する本質的な理由です。

これに対し、従来の代表的な生成モデルであるVAE(変分オートエンコーダー)GAN(敵対的生成ネットワーク) は、まさに「ランダムなノイズから一発で画像を生成する(One-shot)」というアプローチをとっていました。

これらのモデルがどのような技術的な壁にぶつかっていたのかを紐解くと、なぜDDPMのような拡散モデルが主役に躍り出たのかが明確になります。

VAE(変分オートエンコーダー)の技術的限界

VAEが一発生成に挑む中で抱えていた主な課題は、以下の3点に集約されます。

  1. 画像が「ぼやける」問題(平均化のジレンマ)
    VAEは入力画像を小さな潜在変数に圧縮し、そこから元の画像を復元(再構成)するように学習します。学習時には「元の画像」と「生成した画像」のピクセル単位のズレ(誤差)を最小化しようとしますが、限られた情報から一発で複雑な模様や輪郭を正確に描画しようとすると、AIは「正解の位置」に迷います。不確実な状況で誤差を最小にしようとした結果、AIは「あり得る複数のパターンの平均値」を出力してしまいます。これが、テクスチャが潰れて全体的にぼやけてしまう原因です。
  2. 強烈な「情報ボトルネック」による細部の喪失
    VAEは、数百万ピクセルもある高解像度画像を、わずか数百次元の小さな「潜在空間」に無理やり押し込み、かつその空間が綺麗なガウス分布(ノイズ)に従うよう強い制約(KLダイバーシンス)をかけます。デコーダー(生成器)は、この極限まで圧縮された抽象的な情報のみを頼りに一瞬で画像を再構築しなければなりません。このボトルネックがあまりにも強烈なため、髪の毛の1本1本や背景の細かな質感(高周波成分)を復元する容量が足りず、のっぺりとした画像になりがちでした。
  3. 「単純な分布」から「複雑な分布」への一発変換の限界
    数学的に見ると、完全なランダムノイズ(単純な正規分布)の空間を、たった1回のニューラルネットワークの計算で、実世界の画像(非常に複雑で多峰性な分布)の空間へと一気に歪めてマッピングするのは、モデルの表現力に対する要求が異常に高いタスクです。一歩で頂上までジャンプしようとするため、途中で破綻しやすくなります。

GAN(敵対的生成ネットワーク)の技術的アキレス腱

GANは、「偽札造り(Generator:生成器)」と「警察(Discriminator:識別器)」の騙し合いによって学習を進める非常にユニークなモデルです。VAEの「ぼやける」問題を克服し、圧倒的にシャープでリアルな画像を生成できることで一時代を築きましたが、この「敵対的(Adversarial)な学習方式」そのものが、致命的な課題を内包していました。

  1. 学習の不安定さ(バランスの崩壊)
    GANの学習がうまくいくには、生成器と識別器の能力が常に「拮抗」している必要がありますが、この調整は極めて困難です。
    • 警察(識別器)が強すぎる場合: 識別器の目が厳しすぎると、生成器がどんな画像を出しても「偽物」と瞬時に見破られてしまいます。こうなると、生成器は「どう修正すれば本物に近づけるのか」というヒント(勾配)を得られず、学習がストップします(勾配消失問題)。
    • 偽札造り(生成器)が強すぎる場合: 逆に生成器が、識別器の「盲点」を突くようなデタラメなノイズパターンを偶然見つけてしまうと、識別器は騙されっぱなしになり、まともな画像が生成されなくなります。 ハイパーパラメータの調整が「職人芸」レベルで難しく、わずかな設定の違いで学習が簡単に崩壊していました。
  2. モード崩壊(Mode Collapse:多様性の喪失)
    生成器は「とにかく識別器を騙せれば勝ち」というルールで動いているため、「絶対に騙せる『100点満点の猫の画像』を1種類だけ見つけたら、永遠にそれだけを生成し続ける」という局所最適解に陥ることがあります。例えば、多様なデータを学習させても、出力されるのは「特定のパターン」ばかりになり、学習データの全体分布をカバーできずバリエーションが極端に狭まってしまいます。これが、Diffusionモデルに主役の座を奪われる最大の要因となりました。

比較:なぜDiffusion(DDPM)が主流になったのか?

GANとDiffusion(DDPM)の特性を比較すると、なぜ覇権が交代したのかが明確になります。

特徴GAN(一発生成)Diffusion / DDPM(反復ノイズ除去)
学習の安定性❌ 極めて不安定(敵対的学習)⭕️ 非常に安定(単純な誤差最小化)
画像の多様性❌ モード崩壊を起こしやすい⭕️ 実データの分布を忠実に再現
画質・シャープさ⭕️ 非常に高い⭕️ 非常に高い
テキストとの連携❌ 制御が困難⭕️ ガイダンスが組み込みやすい

Diffusionモデルは、GANのピーキーな「敵対的学習」を捨て、「ノイズを少しずつ予測して引く」という、堅実で数学的に安定したアプローチ(尤度の最大化)を採用しました。これにより、GANの最大の弱点であった「学習の不安定さ」と「モード崩壊」を見事に克服。さらに、Stable DiffusionやMidjourneyのような「テキスト(プロンプト)の指示に沿って、多様で高精細な画像を描き分ける」という高度な制御において、Diffusionモデルの安定性と網羅性が決定的な強みとなったのです。

DDPMはどう壁を越えたか?

DDPMは、VAEが苦しんだ課題を 「タスクの分割」 によって美しく解決しました。

  • ボトルネックをなくした: VAEのように画像を小さく圧縮せず、画像と同じ解像度のままノイズを加えることで、強烈な情報ボトルネックを排除しました。
  • 平均化を回避した: 一発でピクセルを予測するのではなく、「ほんの少しノイズを引く」という簡単なステップを1,000回繰り返すため、モデルが迷って「平均化(ぼやけ)」を起こす隙を与えません。結果として、VAEでは難しかった「シャープで高精細な細部」を持つ画像を、安定して生成できるようになったのです。

DDPMの処理概要

処理概要
(画像は、Geminiで作成されたものです)

DDPMの画像生成プロセスは、従来のVAEやGANのような「一発での変換」ではなく、「極めて微小な変化を多数積み重ねる(マルコフ連鎖)」 ことで成り立っています。このプロセスは、大きく分けて以下の2つの方向性(連鎖)で構成されています。

  1. Forward Process(拡散過程:データの分解)
    データ分布 x0q(x0)x_0 \sim q(x_0) に対して、分散スケジュール β1,,βT\beta_1, \ldots, \beta_T に従って段階的にガウスノイズを追加していく固定されたマルコフ連鎖です。最終的なステップ TT では、元の画像データは完全に破壊され、単純で数学的に扱いやすい純粋なガウスノイズ xTx_T となります。 従来のVAEのように「強烈な情報ボトルネック」を設けてデータを無理に低次元に圧縮するのではなく、元の画像と同じ解像度(ピクセル空間)を維持したまま段階的にノイズを加えていく点が特徴です。

  2. Reverse Process(逆過程:段階的な復元)
    純粋なノイズ p(xT)=N(xT;0,I)p(x_T) = \mathcal{N}(x_T; 0, I) から出発し、学習されたガウス遷移を用いて徐々にノイズを除去していくマルコフ連鎖です。 ニューラルネットワークは、「一瞬で完璧な画像を構築する」という困難なタスクに挑むのではなく、「ステップ tt の画像 xtx_t から、ほんの1ステップ分のノイズ ϵ\epsilon のみを予測する」という、極限まで単純化されたタスク(ノイズの予測)を学習します。この微小な予測を t=Tt=T から t=1t=1 まで何度も繰り返す(マルコフ連鎖)ことで、モデルが迷って「平均化(ぼやけ)」を起こす隙を与えず、最終的に元の複雑なデータ分布に従う高品質な画像 x0x_0 を再現することができます。

なぜマルコフ連鎖がうまくいくのか?

DDPMが、VAEやGANのようなOne-shot(一発生成)モデルと一線を画し、「マルコフ連鎖」という反復的なアプローチを採用することで成功しているのには、非常に強力な数学的な恩恵があるからです。 最大の理由は、「学習を圧倒的に効率化できる魔法のショートカット(閉形式)」 が使えることと、「逆算(ノイズ除去)を単純なガウス分布で近似できること」 の2点です。

理由1:任意のステップttへ「一足飛び」できるショートカット

マルコフ連鎖とは「現在の状態は、1つ前の状態にのみ依存する(xtx_txt1x_{t-1}だけで決まる)」という性質です。DDPMでは、順拡散過程(ノイズを加える過程)を以下のようなガウス分布を用いたマルコフ連鎖として定義します。

q(xtxt1)=N(xt;1βtxt1,βtI)q(x_t | x_{t-1}) = \mathcal{N}(x_t; \sqrt{1 - \beta_t} x_{t-1}, \beta_t \mathbf{I})

普通に考えると、ステップ1000の画像 x1000x_{1000} を作るには、x0x1x1000x_0 \to x_1 \to \dots \to x_{1000} と1000回ノイズを足す計算が必要です。これは学習時に非常に非効率です。

しかし、マルコフ連鎖であり、かつ追加するノイズが「ガウス分布」であるという性質を利用すると、「ガウス分布同士を足し合わせても、結果はやはりガウス分布になる」という数学的な特権(再生性)が使えます。 αt=1βt\alpha_t = 1 - \beta_t とし、αˉt=i=1tαi\bar{\alpha}_t = \prod_{i=1}^t \alpha_iα\alphaの累積積)と定義すると、元の画像 x0x_0 から任意のステップ tt の画像 xtx_t を、1回の計算で直接求めることができます(Reparameterization Trick)。

q(xtx0)=N(xt;αˉtx0,(1αˉt)I)q(x_t | x_0) = \mathcal{N}(x_t; \sqrt{\bar{\alpha}_t} x_0, (1 - \bar{\alpha}_t) \mathbf{I})

これは、標準正規分布からサンプリングしたノイズ ϵN(0,I)\epsilon \sim \mathcal{N}(0, \mathbf{I}) を使って、次のように書き換えられます。

xt=αˉtx0+1αˉtϵx_t = \sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon

このショートカットにより、学習時に「ステップ500のノイズ予測」をさせたい場合でも、500回の計算をシミュレートする必要がなく、元の画像 x0x_0 と乱数 ϵ\epsilon から一瞬で目的の xtx_t を生成できます。これにより、並列計算が容易になり、学習速度が飛躍的に向上します。

理由2:逆過程も「ガウス分布」で近似できるという数学的裏付け

もう一つの大きな理由は、非平衡熱力学の分野で知られている重要な性質です。

本来、ノイズを加える順過程 q(xtxt1)q(x_t | x_{t-1}) の逆向き、つまり「ノイズから元のデータを復元する過程 q(xt1xt)q(x_{t-1} | x_t)」の真の分布を計算することは、データ全体の分布を知る必要があるため不可能です。

しかし、「マルコフ連鎖の1ステップの遷移(βt\beta_t)が十分に小さい場合、その逆過程も同じ関数形(この場合はガウス分布)で近似できる」という強力な数学的裏付けがあります。 つまり、1回のステップで加えるノイズがごく微量であれば、逆向きに戻るステップもまた、平均と分散を持つ単純なガウス分布としてモデル化できるのです。

pθ(xt1xt)=N(xt1;μθ(xt,t),Σθ(xt,t))p_\theta(x_{t-1} | x_t) = \mathcal{N}(x_{t-1}; \mu_\theta(x_t, t), \Sigma_\theta(x_t, t))

これにより、AIのタスクは「複雑な分布を学習する」ことから「単純なガウス分布の平均・分散を予測する」ことに単純化されます。 さらに、マルコフ連鎖であることを利用して変分下界(ELBO)を展開すると、複雑な確率計算は最終的に 「AIが予測したノイズ ϵθ\epsilon_\theta」と「実際に加えたノイズ ϵ\epsilon」の単なる引き算(二乗誤差:MSE) という、極めてシンプルで安定した損失関数に帰着します。

LsimpleEt,x0,ϵ[ϵϵθ(αˉtx0+1αˉtϵ,t)2]L_{simple} \approx \mathbb{E}_{t, x_0, \epsilon} \left[ || \epsilon - \epsilon_\theta(\sqrt{\bar{\alpha}_t} x_0 + \sqrt{1 - \bar{\alpha}_t} \epsilon, t) ||^2 \right]

なぜ生成時は「一足飛び」できないのか? - One-shotでない理由

ここで重要なのは、この「一足飛び」のショートカットは学習時にデータを壊す順過程でのみ使える特権だという点です。生成(推論)時には使えません。

  • 順過程(学習時): 正解画像 x0x_0 が手元にあるため、数式を使って任意の xtx_t の状態を一発で計算できます。
  • 逆過程(生成時): 手元にあるのは完全なノイズ xTx_T だけで、正解の x0x_0 は未知です。ここで「xTx_T から x0x_0 を一足飛びで予測せよ」と命令すれば、それはVAEやGANが直面したのと同じ「One-shot問題」となり、高品質な生成は困難になります。

DDPMが賢いのは、この問題を以下のように回避している点です。

  1. AI(U-Net)には常に「現在の画像 xtx_t に含まれるノイズ成分 ϵ\epsilon だけを予測して」という単純なタスクを課す。
  2. その予測結果を使い、一足飛びで x0x_0 を作るのではなく、その予測方向へ「ほんの1歩だけ」進むxt1x_{t-1} を作る)ための道しるべとして利用する。

実は数学的には、AIが「ノイズ ϵ\epsilon を予測する」という行為は、「現在の xtx_t と予測したノイズから、仮の正解画像 x^0\hat{x}_0 を一足飛びで予測している」ことと等価です。 しかし、ノイズが多い段階での x^0\hat{x}_0 は非常にぼやけています。DDPMは、このぼやけた中間予測を最終出力とせず、あくまで次のステップへ進むための一時的なガイドとして利用します。この反復的なプロセスにより、徐々にノイズが晴れ、最終的にOne-shot生成の「ぼやけ」を回避した高精細な画像に辿り着くのです。

DDPMの構成技術要素(詳細)

DDPMが高い生成品質を達成した背景には、前述した数学的な理論を具現化するための、具体的なネットワークアーキテクチャや学習戦略の工夫があります。

  1. U-Netアーキテクチャとアテンション
    逆過程におけるノイズ予測器 ϵθ(xt,t)\epsilon_\theta(x_t, t) の実装には、PixelCNN++ のバックボーンに似たU-Netが使用されています。これは、入力画像 xtx_t と同じ解像度のノイズ ϵ\epsilon を出力する必要があるため、解像度を維持しながら特徴抽出を行うU-Netアーキテクチャが非常に適しています。VAEのように情報を圧縮してボトルネックを通過させるのではなく、スキップコネクションを通じて豊富な空間情報を保持しながらノイズ予測を行う点が、高周波な詳細(テクスチャなど)の復元に貢献しています。 また、16x16の特徴マップ解像度においてセルフアテンション(Self-attention) ブロックが使用され、画像全体のグローバルな依存関係を捉えることで、より高品質な生成を可能にしています。

  2. タイムステップの埋め込み (Time Embedding)
    逆過程のマルコフ連鎖 pθ(xt1xt)p_\theta(x_{t-1} | x_t) では、モデル(U-Net)はすべてのタイムステップ tt でパラメータを共有します。しかし、ノイズの量が多い初期ステップと、ノイズが少ない終盤ステップとでは、モデルが行うべきノイズ除去のタスクは異なります。 そこで、現在どのステップの処理を行っているのかをネットワークに伝えるため、Transformerの位置エンコーディングに似た 正弦波位置埋め込み(sinusoidal position embedding) を用いて時間 tt をベクトル化し、U-Netの各残差ブロックに注入しています。これにより、単一のモデルが1からTTまでのすべてのステップに対応できるようになります。

  3. 簡略化された目的関数 (Simplified Training Objective)
    これが、前述の「理由2」で解説した数学的な単純化の恩恵を最も直接的に受けている部分です。 本来、拡散モデルは負の対数尤度の変分下限(ELBO)を最適化することで学習されます。しかし、DDPMの著者らは、厳密な変分下限を計算する代わりに、加えたノイズ ϵ\epsilon とネットワークが予測したノイズ ϵθ(xt,t)\epsilon_\theta(x_t, t)平均二乗誤差(MSE)を最小化する「簡略化された目的関数 LsimpleL_{simple} を用いることで、サンプルの品質が向上することを発見しました。

    Lsimple=Et,x0,ϵ[ϵϵθ(xt,t)2]L_{simple} = \mathbb{E}_{t, x_0, \epsilon} \left[ || \epsilon - \epsilon_\theta(x_t, t) ||^2 \right]

    この簡略化は、複雑な確率モデリングの問題を、シンプルで安定した回帰問題(ノイズ予測)に落とし込むことを意味します。また、論文ではさらに、小さい tt(微細なノイズ)の損失の重みを下げ、より難しい大きな tt のノイズ除去タスクにネットワークを集中させる重み付き変分下限が、さらなる品質向上に繋がることも示唆しています。

    備考

    数式的な背景に興味がある方へ 本来最小化すべき「負の対数尤度の変分下界(ELBO)」が、数学的にどのような展開を経てこのシンプルな平均二乗誤差(MSE)へと帰着するのか、その一連の導出ステップについては、解説記事「DDPMにおける変分下界(ELBO)の導出とノイズ予測 MSE への帰着」で極めて丁寧に紐解いています。難解な数式変形を手元でトレースしたい方はぜひ参考にしてください。

  4. プログレッシブな非可逆圧縮としての解釈
    DDPMのサンプリング手順は、自己回帰モデル(PixelCNNなど)によるデコーディングを一般化したような、一種のプログレッシブなデコーディング(Progressive decoding) として解釈できます。 VAEが情報を一度に圧縮・復元するのに対し、DDPMは逆過程を通じて情報を段階的に「解凍」していきます。これは、優れた帰納的バイアスによって、画像データを効率的に非可逆圧縮(Lossy compression)する手法と見なすことができ、DDPMの生成能力の高さを別の側面から説明するものです。

DDPMの実装(概念的なシンプルな実装)

ここでは、論文に基づくDDPMのシンプルなPyTorch実装の概念を示します。

ライブラリのインストール

PyTorch等の必要なライブラリをインストールします。

!pip install torch torchvision diffusers matplotlib tqdm

パラメータとスケジュールの定義

Forward processで使用される分散スケジュール βt\beta_t などを定義します。

import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt


# ハイパーパラメータの設定
T = 1000 # 論文に記載されているタイムステップ数
beta_1 = 1e-4
beta_T = 0.02

# 線形なベータスケジュールを作成
betas = torch.linspace(beta_1, beta_T, T)
alphas = 1.0 - betas
alphas_bar = torch.cumprod(alphas, dim=0)
sqrt_alphas_bar = torch.sqrt(alphas_bar)
sqrt_one_minus_alphas_bar = torch.sqrt(1.0 - alphas_bar)

このコードブロックでは、DDPMの順過程(Forward process)で画像を徐々にノイズ化するために必要なハイパーパラメータと、数式で使用される各種スケジューラーを定義しています。

それぞれの定義が数式とどのように対応しているか、詳しく解説します。

  • タイムステップ数 T: 画像を何ステップに分けてノイズ化(およびノイズ除去)するかを指定します。論文同様に T = 1000 を設定しています。
  • 分散スケジュール betas (βt\beta_t): 各ステップで追加されるノイズの分散パラメータです。ここでは beta_1 = 1e-4 (β1\beta_1) から beta_T = 0.02 (βT\beta_T) まで、torch.linspace を用いて線形(Linear)に増加するようスケジュールを設定しています。ステップが進むほど追加されるノイズの量が多くなることを意味します。
  • 情報維持率 alphas (αt\alpha_t): αt=1βt\alpha_t = 1 - \beta_t として定義されます。そのタイムステップにおいて、ノイズを加えられる前の元の画像データがどの程度維持されるかを表す比率です。
  • 累積情報維持率 alphas_bar (αˉt\bar{\alpha}_t): αˉt=s=1tαs\bar{\alpha}_t = \prod_{s=1}^t \alpha_s として定義されます。コード内では累積積を計算する torch.cumprod 関数を用いて、初期ステップ t=1t=1 からステップ tt までの α\alpha の掛け合わせを求めています。
  • 元画像およびノイズの係数 sqrt_alphas_bar (αˉt\sqrt{\bar{\alpha}_t}), sqrt_one_minus_alphas_bar (1αˉt\sqrt{1 - \bar{\alpha}_t}): これらは順過程の任意のステップ tt において、途中のステップを逐次計算することなく、元画像 x0\mathbf{x}_0 から直接ノイズ画像 xt\mathbf{x}_t を一発でサンプリングするための係数です。

これらの事前計算されたテンソルを使用することで、順過程におけるサンプリング式:

xt=αˉtx0+1αˉtϵ(ϵN(0,I))\mathbf{x}_t = \sqrt{\bar{\alpha}_t}\mathbf{x}_0 + \sqrt{1 - \bar{\alpha}_t}\boldsymbol{\epsilon} \quad (\boldsymbol{\epsilon} \sim \mathcal{N}(\mathbf{0}, \mathbf{I}))

を、任意のタイムステップ tt で定数時間(O(1)\mathcal{O}(1))でサンプリング可能にしています。

モデルの定義(U-NetとTime Embeddingの実装)

ノイズを予測するニューラルネットワーク(ϵθ\epsilon_\theta)を定義します。 入力画像 xtx_t と同じ解像度のノイズ ϵ\epsilon を出力し、高周波な詳細を復元するため、 スキップコネクションを持つU-Netアーキテクチャを使用します。 また、現在のステップ tt を伝えるための正弦波位置埋め込み(Time Embedding)と、グローバルな特徴を捉えるセルフアテンションを組み込みます。   

# 1. タイムステップの正弦波位置埋め込み (Time Embedding)
class SinusoidalPositionEmbeddings(nn.Module):
def __init__(self, dim):
super().__init__()
self.dim = dim

def forward(self, time):
device = time.device
half_dim = self.dim // 2
embeddings = math.log(10000) / (half_dim - 1)
embeddings = torch.exp(torch.arange(half_dim, device=device) * -embeddings)
embeddings = time[:, None] * embeddings[None, :]
embeddings = torch.cat((embeddings.sin(), embeddings.cos()), dim=-1)
return embeddings

# 2. セルフアテンションブロック (Self-Attention)
class SelfAttention(nn.Module):
def __init__(self, channels):
super().__init__()
self.channels = channels
# 画像全体のグローバルな依存関係を捉えるためのMulti-head Attention
self.mha = nn.MultiheadAttention(channels, 4, batch_first=True)
self.ln = nn.LayerNorm([channels])

def forward(self, x):
b, c, h, w = x.shape
# Attentionに入力するため、空間次元を1次元に平坦化 (b, h*w, c)
x_reshaped = x.view(b, c, h * w).swapaxes(1, 2)
x_ln = self.ln(x_reshaped)
attention_value, _ = self.mha(x_ln, x_ln, x_ln)
attention_value = attention_value + x_reshaped
return attention_value.swapaxes(2, 1).view(b, c, h, w)

# 3. Time Embeddingを注入する残差ブロック
class Block(nn.Module):
def __init__(self, in_ch, out_ch, time_emb_dim):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU(),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.BatchNorm2d(out_ch),
nn.ReLU()
)
self.time_mlp = nn.Linear(time_emb_dim, out_ch)

def forward(self, x, t_emb):
# タイムステップの埋め込みを空間次元に拡張して特徴マップに加算
time_emb = self.time_mlp(t_emb)[..., None, None]
return self.conv(x) + time_emb

# 4. U-Net本体
class UNet(nn.Module):
def __init__(self, in_channels=3, out_channels=3, time_emb_dim=128):
super().__init__()

# 時間の埋め込み層
self.time_mlp = nn.Sequential(
SinusoidalPositionEmbeddings(time_emb_dim),
nn.Linear(time_emb_dim, time_emb_dim),
nn.ReLU()
)

# Encoder (情報を抽出しながら解像度を下げる)
self.down1 = Block(in_channels, 64, time_emb_dim) # 32x32 -> 32x32
self.pool1 = nn.MaxPool2d(2) # 32x32 -> 16x16

self.down2 = Block(64, 128, time_emb_dim) # 16x16 -> 16x16
self.sa1 = SelfAttention(128) # 16x16解像度でのセルフアテンション
self.pool2 = nn.MaxPool2d(2) # 16x16 -> 8x8

# Bottleneck (最深部)
self.bot1 = Block(128, 256, time_emb_dim) # 8x8 -> 8x8
self.bot_sa = SelfAttention(256) # ボトルネック用のアテンション
self.bot2 = Block(256, 256, time_emb_dim) # 8x8 -> 8x8

# Decoder (スキップコネクションで空間情報を保持しながら解像度を上げる)
self.up1 = nn.Upsample(scale_factor=2) # 8x8 -> 16x16
self.up_conv1 = Block(256 + 128, 128, time_emb_dim) # Encoderの特徴を結合(256+128)
self.sa2 = SelfAttention(128) # 16x16解像度でのセルフアテンション

self.up2 = nn.Upsample(scale_factor=2) # 16x16 -> 32x32
self.up_conv2 = Block(128 + 64, 64, time_emb_dim) # Encoderの特徴を結合(128+64)

# 出力層 (入力画像と同じチャンネル数のノイズを予測)
self.out = nn.Conv2d(64, out_channels, 1)

def forward(self, x, t):
# タイムステップの埋め込みベクトルを生成
t_emb = self.time_mlp(t)

# Encoder
x1 = self.down1(x, t_emb)
p1 = self.pool1(x1)

x2 = self.down2(p1, t_emb)
x2_sa = self.sa1(x2)
p2 = self.pool2(x2_sa)

# Bottleneck
b = self.bot1(p2, t_emb)
b = self.bot_sa(b) # アテンションを適用
b = self.bot2(b, t_emb)

# Decoder
u1 = self.up1(b)
u1 = torch.cat([u1, x2_sa], dim=1) # 豊富な空間情報を保持するスキップコネクション
u1 = self.up_conv1(u1, t_emb)
u1 = self.sa2(u1)

u2 = self.up2(u1)
u2 = torch.cat([u2, x1], dim=1) # 豊富な空間情報を保持するスキップコネクション
u2 = self.up_conv2(u2, t_emb)

return self.out(u2)

# モデルのインスタンス化
device = "cuda" if torch.cuda.is_available() else "cpu"
model = UNet().to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

この実装は、高品質な生成を支える以下の重要なメカニズムをコードレベルで表現しています。

  • タイムステップの埋め込み (Time Embedding): Transformerの位置エンコーディングに似た正弦波位置埋め込み(sinusoidal position embedding)を用いて時間 tt をベクトル化しています。 単一のモデルがすべてのステップに対応できるよう、このベクトルをU-Netの各残差ブロックに注入しています。
  • スキップコネクションによる空間情報の保持: VAEのように情報を圧縮してボトルネックを通過させるのではなく、 Decoder側で torch.cat を用いてEncoder側の特徴マップを結合(スキップコネクション)しています。 これにより、豊富な空間情報を保持しながらノイズ予測を行うことができ、テクスチャなどの高周波な詳細の復元に貢献しています。
  • 16x16でのセルフアテンション: SelfAttention クラスを定義し、画像サイズが16x16になった時点(down2 の出力時と up1 の出力時)で適用しています。 これにより、畳み込み層だけでは捉えきれない画像全体のグローバルな依存関係を捉え、より高品質な生成を可能にしています。

これらの工夫が施されたU-Netが、後のAlgorithm 1の F.mse_loss で加えたノイズ ϵ\epsilon と予測したノイズ ϵθ(xt,t)\epsilon_\theta(x_t, t) の平均二乗誤差(MSE)を最小化する「簡略化された目的関数 LsimpleL_{simple}」によって最適化されます。 この安定した回帰問題の反復が、画像を効率的に解凍していくプログレッシブな非可逆圧縮(Progressive decoding)の要となっています。

学習の定義 (Algorithm 1)

簡略化された目的関数 LsimpleL_{simple} を用いて学習を行う1ステップを定義します。

def train_step(x_0):
model.train()
optimizer.zero_grad()

batch_size = x_0.shape[0]

# 1. タイムステップtを一様分布からランダムにサンプリング
t = torch.randint(0, T, (batch_size,), device=device).long()

# 2. ガウスノイズ epsilon を生成
noise = torch.randn_like(x_0).to(device)

# 3. Forward process: x_t を計算 (x_0 と noise から閉形式で求まる)
# ★修正箇所: インデックス参照 [t] を行う前にテンソルを device に移動させます
sqrt_a_bar_t = sqrt_alphas_bar.to(device)[t].view(-1, 1, 1, 1)
sqrt_one_minus_a_bar_t = sqrt_one_minus_alphas_bar.to(device)[t].view(-1, 1, 1, 1)

x_t = sqrt_a_bar_t * x_0 + sqrt_one_minus_a_bar_t * noise

# 4. モデルでノイズを予測
predicted_noise = model(x_t, t)

# 5. 損失関数の計算 (単純な平均二乗誤差: MSE)
loss = F.mse_loss(predicted_noise, noise)

loss.backward()
optimizer.step()

return loss.item()

IMAGE_SIZE = 32
# ----------------------------------------------------
# 猫の画像 (cat1.jpg ~ cat4.jpg) の読み込みと前処理
# ----------------------------------------------------
# 画像をモデルへの入力サイズにリサイズし、[-1, 1] に正規化
transform = transforms.Compose([
transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)), # 32x32にリサイズ
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # [0, 1] -> [-1, 1]
])

image_paths = ["cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg"]
images = []

for path in image_paths:
try:
img = Image.open(path).convert("RGB")
except FileNotFoundError:
# ファイルがない場合は実行時にエラーにならないよう、ダミー画像を自動生成
img = Image.new("RGB", (IMAGE_SIZE, IMAGE_SIZE), color=(128, 128, 128))
images.append(transform(img))

# 4枚の画像をスタックしてバッチを作成 (Shape: [4, 3, 32, 32])
x_0 = torch.stack(images).to(device)

# 訓練ループの実行 (例: 10000エポック)
epochs = 10000
for epoch in range(epochs):
loss_val = train_step(x_0)
if (epoch + 1) % 1000 == 0:
print(f"Epoch {epoch+1}/{epochs} - Loss: {loss_val:.4f}")

このコードブロックでは、論文の Algorithm 1(学習手順)に基づいた1ステップの更新処理 train_step(x_0) と、実際に猫の画像データ(cat1.jpgcat4.jpg)を読み込んで複数エポック学習させる訓練ループを実装しています。 検証で利用する際には以下の画像をダウンロードして下さい。

cat1
cat1.jpg
cat2
cat2.jpg
cat3
cat3.jpg
cat4
cat4.jpg

(Google AI Studioで生成)

処理の全体の流れと、前述の数式との対応について解説します。

  • 1ステップの学習処理 train_step(x_0):

    • 引数として元画像(ノイズが加えられていないデータ)のテンソルを受け取ります。
    • torch.randint を用いて、ミニバッチ内の画像ごとに 00 から T1T-1 までのタイムステップ tt を一様分布からランダムにサンプリングします。これは、ニューラルネットワークがすべてのタイムステップ of ノイズ除去をバランスよく学習できるようにするためです。
    • 次に、元画像と同じ解像度の標準正規分布に従うガウスノイズ ϵ\boldsymbol{\epsilon}noise)を生成します。
    • 事前計算されたスケジューラ係数 sqrt_alphas_bar (αˉt\sqrt{\bar{\alpha}_t}) と sqrt_one_minus_alphas_bar (1αˉt\sqrt{1 - \bar{\alpha}_t}) を用いて、順過程の式(一括サンプリング公式)に基づきノイズ画像 x_t を作成します。
    • ノイズ画像 x_t とタイムステップ t をモデル(U-Net)に入力し、予測されたノイズ predicted_noise (ϵθ\boldsymbol{\epsilon}_\theta) を取得します。
    • F.mse_loss を用いて、実際に加えたノイズ noise と、モデルが予測したノイズ predicted_noise との単純な平均二乗誤差(MSE)を計算し、これを損失(Loss)としてバックプロパゲーションを行います。これが、簡略化された目的関数 LsimpleL_{\text{simple}} の実装そのものです。
  • 画像の読み込みと前処理:

    • transforms.Compose を用いて、画像を U-Net の入力解像度である 32x32 ピクセルにリサイズ(transforms.Resize)し、テンソル化(transforms.ToTensor)した後、transforms.Normalize によってピクセル値の範囲を [0, 1] から [-1, 1] に変換(正規化)しています。拡散モデルでは一般に、ノイズとの親和性や学習の安定化のためにデータを [-1, 1] で扱うため、この正規化は極めて重要です。
    • 指定された 4 枚の猫の画像(cat1.jpgcat4.jpg)を Image.open で読み込み、前処理を適用した上で、torch.stack でバッチ化されたテンソル x_0(Shape: [4, 3, 32, 32])を作成しています。
  • 訓練ループの実行:

    • 作成したバッチ x_0 を用いて、設定したエポック数(ここでは 1000 エポック)だけ train_step を繰り返し呼び出し、モデルの重みを最適化していきます。

DDPMの学習では、データ x0x_0 に対してランダムなステップ tt のノイズを加え、その加えられたノイズ ϵ\epsilon をモデルに予測させ、真のノイズとのMSE(平均二乗誤差)を計算します。

推論の実行 (Algorithm 2)

学習済みのモデルを用いて、純粋なノイズから画像を生成するサンプリング過程を定義します。

@torch.no_grad()
def sample_image(model, image_size=32, batch_size=1):
model.eval()

# 1. 初期状態: 純粋なガウスノイズ x_T をサンプリング
x = torch.randn((batch_size, 3, image_size, image_size)).to(device)

# 2. t = T, ..., 1 に対してループ
for i in reversed(range(0, T)):
t = torch.tensor([i], device=device).long()

# モデルによって予測されたノイズ
predicted_noise = model(x, t)

# 係数の取得
alpha = alphas[i]
alpha_bar = alphas_bar[i]
beta = betas[i]

# 確率的要素 z の追加 (t > 0 の場合)
z = torch.randn_like(x) if i > 0 else torch.zeros_like(x)

# ランジュバン動力学に似た逆過程の計算
x = (1 / torch.sqrt(alpha)) * (x - ((1 - alpha) / torch.sqrt(1 - alpha_bar)) * predicted_noise)
x = x + torch.sqrt(beta) * z

# [-1, 1]の出力を[0, 1]にスケーリング
x = (x.clamp(-1, 1) + 1) / 2
return x

# ----------------------------------------------------
# 推論の実行と生成された画像の可視化
# ----------------------------------------------------
generated_images = sample_image(model, image_size=IMAGE_SIZE, batch_size=4)

# [B, C, H, W] から matplotlib が解釈可能な [B, H, W, C] 形式に変換し NumPy 配列化
generated_images = generated_images.permute(0, 2, 3, 1).cpu().numpy()

# 1x4 のグリッドで画像を描画
fig, axes = plt.subplots(1, 4, figsize=(12, 3))
for idx, img in enumerate(generated_images):
axes[idx].imshow(img)
axes[idx].axis("off")
axes[idx].set_title(f"Generated {idx+1}")

plt.tight_layout()
plt.show()

このコードブロックでは、論文の Algorithm 2(サンプリング手順)に基づく画像生成関数 sample_image(model) と、生成されたテンソル画像を matplotlib を用いてグリッド表示するための可視化コードを実装しています。

逆過程(Reverse process)の数式との対応、およびテンソルの変形処理について詳しく解説します。

  • 初期状態のサンプリング:

    • torch.randn を用いて、純粋なガウスノイズ xTN(0,I)\mathbf{x}_T \sim \mathcal{N}(\mathbf{0}, \mathbf{I}) を初期画像(潜在変数)としてサンプリングします。画像生成の出発点となるノイズテンソルの Shape は [batch_size, 3, image_size, image_size] です。
  • 逆過程の再帰ループ:

    • t=Tt = T から t=1t = 1 に向けて逆向きにループを回します(コード内では 0-indexed に合わせて reversed(range(0, T)) としています)。
    • 各ステップにおいて、現在の画像 x (xt\mathbf{x}_t) とタイムステップ t をモデルに入力し、予測されたノイズ predicted_noise (ϵθ(xt,t)\boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t)) を取得します。
    • ループ内で適用される以下のサンプリング更新式は、DDPMの核心部分です。 xt1=1αt(xt1αt1αˉtϵθ(xt,t))+σtz(zN(0,I))\mathbf{x}_{t-1} = \frac{1}{\sqrt{\alpha_t}} \left( \mathbf{x}_t - \frac{1-\alpha_t}{\sqrt{1-\bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \right) + \sigma_t \mathbf{z} \quad (\mathbf{z} \sim \mathcal{N}(\mathbf{0}, \mathbf{I})) これをコード内の演算処理に変換したのが以下の部分です。
      x = (1 / torch.sqrt(alpha)) * (x - ((1 - alpha) / torch.sqrt(1 - alpha_bar)) * predicted_noise)
      x = x + torch.sqrt(beta) * z
      ここで、分散 σt2\sigma_t^2(コード内の beta)に比例した確率的ノイズ z\mathbf{z}(コード内の z)を追加することで、ランジュバン動力学に似たサンプリング過程を実現しています。ただし、最後のステップ t=1t=1(インデックス 0)のときは、それ以上ノイズを追加する必要がないため、z = torch.zeros_like(x) として確率的ノイズをゼロに設定します。
  • 出力値のクランプとスケーリング:

    • モデルの学習時に入力を [-1, 1] に正規化していたため、最終ステップ x0\mathbf{x}_0 の出力値も [-1, 1] 付近に分布します。画像ファイルとして保存・表示するために、x.clamp(-1, 1) で範囲を限定し、さらに (x + 1) / 2 によって [0, 1] の範囲に正規化(スケーリング)して返します。
  • 生成された画像の可視化処理:

    • sample_image から返されたテンソルは、PyTorch of 標準フォーマットである [Batch, Channel, Height, Width] の次元順になっています。
    • これを matplotlib で表示できるようにするため、permute(0, 2, 3, 1) を使用して次元を [Batch, Height, Width, Channel](HWC形式)に並び替えます。
    • cpu().numpy() でテンソルをCPUメモリへ移動し、NumPy配列に変換します。
    • plt.subplots で生成枚数分の表示領域を用意し、imshow で各画像を表示します。axis("off") で不要なピクセル座標軸を非表示にすることで、生成された画像をすっきりとグリッド表示させています。

実行結果

Epoch 1000/10000 - Loss: 0.0288
Epoch 2000/10000 - Loss: 0.0276
Epoch 3000/10000 - Loss: 0.0148
Epoch 4000/10000 - Loss: 0.0135
Epoch 5000/10000 - Loss: 0.0204
Epoch 6000/10000 - Loss: 0.0085
Epoch 7000/10000 - Loss: 0.0079
Epoch 8000/10000 - Loss: 0.0143
Epoch 9000/10000 - Loss: 0.0104
Epoch 10000/10000 - Loss: 0.0065

簡易DDPMによる生成結果

10000エポックの学習後、純粋なノイズから画像を生成した結果、学習データであるcat1.jpgcat4.jpgの特徴(灰色や茶色の毛色、猫らしい輪郭)を捉えた画像が生成されていることが確認できます。 画像の解像度は32x32ピクセルと低いため、細部は不鮮明ですが、DDPMがノイズから意味のある画像を再構築するプロセスを概念的に示しています。

HuggingfaceのDDPMライブラリによる実装

ライブラリのインポート

Hugging Faceの diffusers ライブラリなど、DDPMの実装と学習に必要なライブラリをインストールします。

!pip install torch torchvision diffusers pillow matplotlib
  • torch, torchvision: PyTorch本体と、画像処理関連のユーティリティ。
  • diffusers: Hugging Faceが提供する拡散モデルのためのライブラリ。U-Netモデルやスケジューラなどがプリセットされており、簡単に利用できます。
  • pillow, matplotlib: 画像の読み込みや可視化に使用します。

Hugging Face DDPMによる学習・推論

Hugging Faceのdiffusersライブラリが提供するUNet2DModelDDPMSchedulerを用いて、DDPMの学習と推論をより抽象化されたコードで実行します。これにより、自前でU-Netやスケジューラを実装する必要がなくなり、より簡単に拡散モデルを試すことができます。

import torch
import torch.nn.functional as F
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
from diffusers import UNet2DModel, DDPMScheduler

# ハイパーパラメータの設定
IMAGE_SIZE = 32
device = "cuda" if torch.cuda.is_available() else "cpu"

# ----------------------------------------------------
# 1. 猫の画像 (cat1.jpg ~ cat4.jpg) の読み込みと前処理
# ----------------------------------------------------
transform = transforms.Compose([
transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

image_paths = ["cat1.jpg", "cat2.jpg", "cat3.jpg", "cat4.jpg"]
images = []

for path in image_paths:
try:
img = Image.open(path).convert("RGB")
except FileNotFoundError:
# 画像がない場合のダミー生成も元のまま
img = Image.new("RGB", (IMAGE_SIZE, IMAGE_SIZE), color=(128, 128, 128))
images.append(transform(img))

x_0 = torch.stack(images).to(device)

# ----------------------------------------------------
# 2. Hugging Face のモデルとスケジューラの定義
# ----------------------------------------------------
model = UNet2DModel(
sample_size=IMAGE_SIZE, # 32
in_channels=3,
out_channels=3,
layers_per_block=2,
# 一般的な32x32向けのチャンネル数(4段階)
block_out_channels=(128, 256, 256, 256),
down_block_types=(
"DownBlock2D", # 32x32 -> 16x16
"DownBlock2D", # 16x16 -> 8x8
"AttnDownBlock2D", # 8x8 -> 4x4 (ここでAttention)
"DownBlock2D", # 4x4
),
up_block_types=(
"UpBlock2D", # 4x4 -> 8x8
"AttnUpBlock2D", # 8x8 -> 16x16 (ここでAttention)
"UpBlock2D", # 16x16 -> 32x32
"UpBlock2D", # 32x32
),
).to(device)

# 論文のベータスケジュール(1000ステップ、1e-4 ~ 0.02 の線形)を再現
scheduler = DDPMScheduler(
num_train_timesteps=1000,
beta_start=1e-4,
beta_end=0.02,
beta_schedule="linear"
)

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

# ----------------------------------------------------
# 3. 訓練ループ
# ----------------------------------------------------
epochs = 10000
batch_size = x_0.shape[0]

for epoch in range(epochs):
model.train()
optimizer.zero_grad()

# ガウスノイズの生成
noise = torch.randn_like(x_0).to(device)

# タイムステップをランダムにサンプリング
timesteps = torch.randint(0, scheduler.config.num_train_timesteps, (batch_size,), device=device).long()

# Forward process: スケジューラを使って入力画像にノイズを追加 (数式の計算を自動化)
noisy_images = scheduler.add_noise(x_0, noise, timesteps)

# モデルでノイズを予測
noise_pred = model(noisy_images, timesteps).sample

# 損失関数の計算
loss = F.mse_loss(noise_pred, noise)

loss.backward()
optimizer.step()

if (epoch + 1) % 1000 == 0:
print(f"Epoch {epoch+1}/{epochs} - Loss: {loss.item():.4f}")

# ----------------------------------------------------
# 4. 推論の実行と生成された画像の可視化
# ----------------------------------------------------
model.eval()

# 初期状態: 純粋なガウスノイズ
sample = torch.randn((batch_size, 3, IMAGE_SIZE, IMAGE_SIZE)).to(device)

# 推論用のタイムステップを設定 (1000ステップ)
scheduler.set_timesteps(1000)

with torch.no_grad():
for t in scheduler.timesteps:
# モデルによって予測されたノイズ
residual = model(sample, t).sample

# 逆過程の計算 (自動で係数を計算してノイズを除去)
sample = scheduler.step(residual, t, sample).prev_sample

# [-1, 1]の出力を[0, 1]にスケーリングして NumPy 配列に変換
generated_images = (sample.clamp(-1, 1) + 1) / 2
generated_images = generated_images.permute(0, 2, 3, 1).cpu().numpy()

# 1x4 のグリッドで画像を描画
fig, axes = plt.subplots(1, 4, figsize=(12, 3))
for idx, img in enumerate(generated_images):
axes[idx].imshow(img)
axes[idx].axis("off")
axes[idx].set_title(f"Generated {idx+1}")

plt.tight_layout()
plt.show()

このコードは、Hugging Faceのdiffusersライブラリを使用してDDPMの学習と推論を行う完全なパイプラインです。先ほどの自前実装と比較すると、多くの複雑な処理がライブラリによって抽象化されていることがわかります。

  • UNet2DModel: DDPM論文で提案されたものと同様のU-Netアーキテクチャを提供します。block_out_channelsdown_block_typesup_block_typesといった引数を指定するだけで、柔軟にモデル構造を定義できます。ここでは、論文に合わせて解像度が8x8になる層でAttnDownBlock2DAttnUpBlock2Dを使用し、セルフアテンションを組み込んでいます。
  • DDPMScheduler: 論文で提案された線形(linear)なベータスケジュールをbeta_schedule="linear"と指定するだけで再現します。学習時にはadd_noiseメソッドで自動的にノイズを付加し、推論時にはstepメソッドで逆過程の計算を自動で行うため、複雑な係数計算を自前で実装する必要がありません。
  • 訓練ループ: スケジューラのadd_noiseメソッドを使うことで、順過程の式が隠蔽され、「元の画像、ノイズ、タイムステップ」を渡すだけでノイズ付加後の画像を得られます。
  • 推論ループ: スケジューラのtimestepsstepメソッドを使うことで、逆過程のループが非常にシンプルになります。開発者はノイズ予測モデルの出力residualstepに渡すだけで、自動的に次のステップの画像prev_sampleが計算されます。

このようにdiffusersライブラリを活用することで、研究者や開発者は拡散モデルのコアロジック(U-Netの改善など)に集中しやすくなります。

実行結果

上記のコードを実行すると以下の結果が得られます。

Epoch 1000/10000 - Loss: 0.0106
Epoch 2000/10000 - Loss: 0.0045
Epoch 3000/10000 - Loss: 0.0046
Epoch 4000/10000 - Loss: 0.0017
Epoch 5000/10000 - Loss: 0.0036
Epoch 6000/10000 - Loss: 0.0069
Epoch 7000/10000 - Loss: 0.0010
Epoch 8000/10000 - Loss: 0.0062
Epoch 9000/10000 - Loss: 0.0021
Epoch 10000/10000 - Loss: 0.0076

HuggingFaceのDDPMによる生成結果
diffusersライブラリを用いた実装でも、自前実装と同様に、学習データの特徴を反映した猫のような画像が生成されました。解像度が32x32であるため細部は不明瞭ですが、より構造化されたライブラリを使ってもDDPMの基本的な生成能力が再現できることを示しています。

まとめ

本記事では、現在の画像生成AIの礎を築いた DDPM (Denoising Diffusion Probabilistic Models) について、その理論的背景から具体的な実装までを段階的に解説しました。

記事を通じて、以下の内容を学習しました。

  • 理論的背景: VAEやGANが抱えていた課題を、DDPMが「微小なノイズ除去の反復」というアプローチでどのように克服したかを解説しました。
  • 数学的根拠: マルコフ連鎖を用いることで、学習を効率化するショートカット(閉形式)や、逆過程を単純なガウス分布で近似できる数学的な裏付けを学びました。
  • PyTorchによる概念実装: 論文に沿ったForwardプロセス(ノイズ付加)とReverseプロセス(サンプリング)を、U-Netやタイムステップ埋め込みを含めてゼロから実装しました。
  • Hugging Face diffusersによる実装: UNet2DModelDDPMSchedulerといった高レベルなAPIを用いることで、同じDDPMの処理をより抽象的かつ簡潔に実装する方法を確認しました。

DDPMは、画像だけでなく音声や他のモダリティにも応用が広がっており、生成的機械学習システムにおいて非常に強力で有望な技術です。本記事が、その仕組みの理論的な理解から実践的な実装までの一助となれば幸いです。

本記事の文章・構成の一部に生成AIを使用しています。