CVPR2021 Best Paperの『GIRAFFE』を徹底解説！　オブジェクト単位で変形でき、静止画から教師なしでDisentangleできる3Dレンダリングベースの生成モデルとは？

[2021年のディープラーニング論文を1人で読むAdvent Calendar](https://qiita.com/advent-calendar/2021/solo-dl-papers)23日目の記事です。今日読むのは「CVPR2021 Best Paper」に選ばれた**GIRAFFE**という論文です。著者はドイツのマックスプランクインテリジェントシステム研究所とテュービンゲン大学の方です。

![23_tr_d_cars.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/a63f789e-1481-76ae-184f-7bb790ea1b34.gif)
![23_add_clevr6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/33a6044e-c84b-ef5e-3279-ab777916c678.gif)

この論文は分類としては生成モデル（GAN）なのですが、NeRF（Neural Radiance Fields）の系列を汲んでいて、従来のGANとは全く異なったアプローチをとります。GANが初めてという方だとかなり読むのが厳しい内容です。逆に3Dレンダリングの技術（特に**ボリュームレンダリング**）をフル活用しているので、3Dに詳しい方なら逆にわかりやすいかもしれません。

自分はボリュームレンダリングを知ったのがこれが初めてで、NeRFの系列も読んだのが初だったのですが、「5次元のパラメーターによるカメラのコントロール」「ボリュームレンダリング」を知ったら理解できました。その**前提知識も含めて丁寧に解説**していきます。この論文はコードを読むより、**論文を読むのが一番わかりやすい**です。**プロジェクトページに生成結果が多く**あるので、それを先に見るとアウトラインがつかめます。

* タイトル：**GIRAFFE: Representing Scenes As Compositional Generative Neural Feature Fields**
* URL：https://openaccess.thecvf.com/content/CVPR2021/html/Niemeyer_GIRAFFE_Representing_Scenes_As_Compositional_Generative_Neural_Feature_Fields_CVPR_2021_paper.html
* 出典：Michael Niemeyer, Andreas Geiger; Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 2021, pp. 11453-11464
* コード：https://github.com/autonomousvision/giraffe
* プロジェクトページ：https://m-niemeyer.github.io/project-pages/giraffe/index.html

<iframe width="560" height="315" src="https://www.youtube.com/embed/fIaDXC-qRSg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


## 指定位置にあるから2D画像をレンダリングしたい
この論文（GIRAFFE）を理解するには、まずNeRFにはじまるNeural Radiance Fieldsのお気持ちを理解しておかないといけません。やりたいことは**3次元空間で特定の位置にカメラをおいて、カメラから見た2D画像をレンダリング**することです。CGの世界を想像してみてください。この図は[NeRFの論文](https://arxiv.org/abs/2003.08934)からの引用ですが、

![23_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/55a4ee21-2a2e-3000-9d74-836900e27d57.png)

(a)のような状況です。今いるのは**3次元空間**です。(a)では2本の矢印が出ていますが、左側に向かって進んでいる矢印に注目しましょう。矢印上にはいくつかの点がありますが、進む方向と逆の矢印の先に**カメラ**があると思ってください（図では$(x, y, z, \theta, \phi)$と書かれているところです）。このカメラがあったときの、**2D投射画像**を算出したいのです。

ではどう投射画像を計算したいのかということですが、**線の上にある3次元の点のもっている情報を、集約計算していく**というのが基本的な発想になります。

これは[英語版WikipediaのRay Castingのページ](https://en.wikipedia.org/wiki/Ray_casting)からのものですが、

![23_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/9deb3a5f-0286-e6b5-fa9e-9374fa094ce8.jpeg)

奥から手前に向かっていくつかの線が出ていますよね。一番奥の線の交わっているところにカメラがあります。「なぜ線をいっぱい出しているのか」という点ですが、4隅の線同士を結んで、平面で囲めばそこが**スクリーン**となるからです。手前のx, y, zと書いてある平面がスクリーンです。

このスクリーンをカメラからの距離に応じて作っていき、そのスクリーンを集約していけば目的の投射画像ができるというのが基本的な発想です。図では4本しか線が引いていませんが、スクリーン上のグリッドに応じて**いくつも線を引く**ことになります。この線はカメラからの光線とみなせるので、CGの世界では**レイ（Ray）**と呼ばれます。

今述べた手法は**レイキャスティング（Ray Casting）**というCGで広く使われている手法です。これをニューラルネットワークベースに落とし込んだのが、NeRFやGIRAFFEというわけです。集約計算は後で見ていくとして、次は「カメラのパラメーターがなぜ5次元なのか」をみていきましょう。

## カメラのパラメーターが5次元である理由
NeRFの図ではカメラのパラメーターを$(x, y, z, \theta, \phi)$という5次元で表していました。**なぜ5次元なのでしょうか**？　GIRAFFEの論文ではここがいきなりポンと与えられているので、これを理解できないと開幕つまずくことになります。この理由を考えてみました。

$(x, y, z)$というのは**3次元内の座標**を表すので特に問題ないでしょう。これはカメラの位置そのものを表しています。

問題は$\theta, \phi$です。これは角度を表していますが、なぜ角度が2つでいいのでしょうか。x, y, zという3つの軸があるのだから、xy-yz-zxのように回転角が3つ必要ではないでしょうか。

### 大砲の気持ちになるのです
すごく唐突ですが、**3Dの世界で大砲を打ち出すゲーム**を想像してください。

![23_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/1efa52ef-a723-6812-70d7-96247753cec1.jpeg)

今この大砲は固定だとします。**弾を任意の方向に打ち出すためには、どうコントロールすればよい**でしょうか？

![23_04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/1de49ff0-0481-ab86-56cf-46983c62d6b2.jpeg)

固定の大砲を、**「上下左右」にコントロールできれば任意の方向に発射できる**ことになります。実際の大砲は動かないので、大砲の向きを変えているつまり、大砲を**垂直方向・水平方向に回転している**だけなのです。

![23_05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/5eb2076e-858b-dca3-2dbf-6f1aa7da7967.jpeg)

位置が固定のとき、大砲を打ち出す方向は、大砲から見たら$\theta, \phi$という水平・垂直回転角が与えられれば一意に決まるということを示しています。だから**回転角が2つでいい**のです。

大砲＝カメラとみれば、**固定のカメラから任意の方向へレイを出したいときは、回転角を2つ与えればレイを定義できる**ということを意味します。

### 極座標として考える
もう少し数学的な概念を使って説明すると、極座標として考えるのがわかりやすいです。平面の極座標では平面上の点は、$(r, \theta)$（図では$\phi$ですが）という、**半径・回転角**の2つのパラメーターで表記されます（[図はWikipediaのPolar coordinate systemから](https://en.wikipedia.org/wiki/Polar_coordinate_system)です）。

![23_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/a7e772c1-0aab-348f-a8d7-1e2026081a03.png)

$(r, \theta)$という2つのパラメーターが必要なのは**点を表現**する場合で、原点から出る任意の直線を表現したい場合は、**パラメーターが1個落ちて回転角だけ**でいいです。なぜなら、この直線は「回転角が同一で半径が異なる点の集合」だからです。**パラメーターが1個落ちるというのが重要**です。

では3Dの極座標はどうでしょうか。3Dの極座標は、$(r, \theta, \phi)$という**半径＋回転角2つ**で表現できます。

![23_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/e479fa35-8635-3ac5-bf1f-71735f61708b.png)

2Dと同様、あくまでこれは点を決める場合なので、線を決めたい場合は半径を落としてOKです。つまり、**2つの回転角$\theta, \phi$で、3D空間で原点を通る直線は表現できる**ということになります。

### カメラの定義に戻ると
GIRAFFEの論文では、3D空間上でのカメラのパラメーターを、座標$\mathbf{x}\in\mathbb{R}^3$、見る方向$\mathbf{d}\in\mathbb{S}^2$の**5次元**で管理しています。**なぜ5次元なのかもう理解できましたね**。

特に記述はありませんでしたが、$\mathbb{S}$は球の「Sphere」を意味しているものだと考えられます。回転角であるから値域の観点から実数全体とは異なるよというでしょうか。コードを見たら複素数のように扱いが異なる値ではありませんでした。

論文ではいきなり「座標は3次元、方向は2次元」と書いているので、ここの背景を理解しないまま読み進めるとなにがなんだかわからなくなってしまうと思われます。

## ボリュームレンダリング
この論文を読む上で、3Dグラフィックの**ボリュームレンダリング**を知っておくと理解しやすいです。

先程「レイ上の点を集約計算することで、カメラから見た投射画像が求められるよ」と述べましたが、**集約計算をどうするのか**かこの部分です。

![23_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/99de67be-5836-7d16-830e-67912e975093.png)

[Wikipediaのボリュームレンダリングのページ](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%AA%E3%83%A5%E3%83%BC%E3%83%A0%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0)からの図ですが、これはレイに対して真上から見下ろすように捉えた図です。

図では物体上の各ピクセルのように表記されている点ですが、3D上では体積を伴った箱（**ボクセル**）として管理されています。今レイに対して集約計算するので、このように上から見てピクセルのように扱うことができます。

各ボクセルは**RGB値**と、**不透明度**を持っています。基本的にはレイに沿ってRGB値と不透明度を掛けるという方針ですが、カメラから見て手前のあるボクセルなのか、奥にあるボクセルなのかで不透明度の扱いが異なる（光の減衰を考慮する）ため、もう少し難しい計算をします。

今図の(2)のように、レイ上にボクセル値がサンプリングできたものとしましょう。ここから描画画像のRGB値は次の積分で計算されます。

$$I=\int_0^D c(x(\lambda))e^{-\int_0^\lambda \alpha(x(\lambda'))d\lambda'}d\lambda \tag{1}$$

$\lambda :0\to D$はレイ上のパラメーターです。$c(\cdot)$は**レイ上のボクセルのRGB値**、$\alpha(\cdot)$は同様に**不透明度**です。

指数関数の中での微分は$\lambda'$という別のパラメーターになっています。この積分は直感的には、「RGB値を不透明度で減衰させているが、遠くにあるボクセルほど不透明度の累積が積もっていって減衰され、寄与が小さくなる」ということでしょうか。式(1)は連続関数での話なので、実際は和や積を使った離散の表現をします。この式(1)を知っておくと、この論文の見通しが利きやすくなります。

ボリュームレンダリングの原理についてはこちらのページがわかりやすいので参照してください。この記事もこの記事を参考にしています。

[https://www.ikeda-shoponline.com/ctsoft/technical_information/volume_visual/](https://www.ikeda-shoponline.com/ctsoft/technical_information/volume_visual/)

## Neural Features Fieldとしてのオブジェクト
「カメラのパラメーター管理」「ボリュームレンダリングの原理」という2つの道具が導入できたので、いよいよGIRAFFEの論文の内容を読解していきます。GIRAFFEの内容相当難しいのですが、**論文がとにかくわかりやすく書かれているので、論文を読むのが一番わかりやすい**です。初手コードだと多分なにやってるんだかわからなくなります。

### Positional Encoding
カメラのパラメーターを、座標$\mathbf{x}\in\mathbb{R}^3$、見る方向$\mathbf{d}\in\mathbb{S}^2$が与えられます。これを**Volume density（体積密度）**$\mathbf{\sigma}\in\mathbb{R}^+$と、RGB値$\mathbf{c}\in\mathbb{R}^3$にマッピングしたいのです。

「Volume densityとはなんぞや？」と思いますが、**直感的にはボクセルの不透明度に相当**します。ただ、体積密度の空間は**厳密にはボクセルと異なる**ので、GIRAFFEの論文ではボクセルとは呼んでいません。「ボクセルの不透明度に近いもので、RGB値と合わせてボリュームレンダリングで積分計算したいんだな」と思っておくと見通しが利きやすくなります。

最初にPositional Encodingのための関数を導入します。

$$\gamma(t, L) = (\sin(2^0t\pi), \cos(2^0t\pi), \cdots, \sin(2^Lt\pi), \cos(2^Lt\pi))\tag{2}$$

これは**カメラのパラメーターをPositional Encoding**するための関数です。[Transformerで使われるPositional Encoding](https://qiita.com/halhorn/items/c91497522be27bde17ce)と結構近いですね。これを導入する意義は、回転角に対してより正準な姿勢を捉えることが可能になるからです（性能を上げるための手法です）。

実際に、Positional Encodingあり/なしで比較したのが以下の図で、

![23_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/6bf712c3-45e6-a807-b816-0b8299cb82e6.png)

(a)がPositional Encodingあり、(b)がPositional Encodingを使わずに別の手法であるRandom Fourier Featuresを使った方法です。回転角を固定したときに、(b)は椅子の種類によって出力される角度がずれてしまっています。

### Neural Radiance Fields
「Radiance Fields」とは直訳すれば**輝度場**ですが、NeRFの論文とも共通する内容です。カメラのパラメーターはを輝度場のパラメーター$(\sigma, c)$にマッピングしたいのです。このマッピングの関数を$f$とすると、

$$f_\theta: \mathbb{R}^{L_x}\times \mathbb{R}^{L_d} \to \mathbb{R}^{+}\times\mathbb{R}^3 \\ (\gamma(\mathbf{x}), \gamma(\mathbf{d})) \to (\mathbf{\sigma}, \mathbf{c})\tag{3}$$

これはカメラのパラメーターから輝度場へのマッピングです。$f_\theta$は多層パーセプトロン（MLP）を使います。$\gamma(\cdot)$は式(2)のPositional Encodingの関数で、関数適用後の要素数が$L_x, L_d$です。Volume densityである$\sigma$の要素数が$\mathbb{R}^+$と謎の値ですが、「正の適当な要素数」という意味です。

### Generative Neural Feature Fields
式(3)のNeRFには生成モデルの性質がありませんでしたが、これにGANのような**生成モデルの性質を加えます**。典型的なGANは$z\to I$のように、乱数から画像を直接生成しましたが、GIRAFFEの場合は輝度場（Neural Radiance Fields）を生成します。実装上は単純で、乱数を引数に加えます。ここで$\mathbf{z}_s$は**オブジェクトの形**を、$\mathbf{z}_a$は**外観**を表す乱数です。

$$g_\theta: \mathbb{R}^{L_x}\times \mathbb{R}^{L_d}\times \mathbb{R}^{M_s}\times \mathbb{R}^{M_a} \to \mathbb{R}^{+}\times\mathbb{R}^3 \\ (\gamma(\mathbf{x}), \gamma(\mathbf{d}), \mathbf{z}_s, \mathbf{z}_a) \to (\mathbf{\sigma}, \mathbf{c})\tag{4}$$

これは[GRAF](https://arxiv.org/abs/2007.02442)という、先行研究のモデリングです。GRAFではRGB値を直接出力していましたが、本論文（GIRAFFE）では出力に**ワンクッションおいて$M_f$という特徴量の次元へとマッピング**します。

$$h_\theta: \mathbb{R}^{L_x}\times \mathbb{R}^{L_d}\times \mathbb{R}^{M_s}\times \mathbb{R}^{M_a} \to \mathbb{R}^{+}\times\mathbb{R}^{M_f} \\ (\gamma(\mathbf{x}), \gamma(\mathbf{d}), \mathbf{z}_s, \mathbf{z}_a) \to (\mathbf{\sigma}, \mathbf{f})\tag{5}$$

最終的なRGB値へのマッピングは、「**Neural Rendering**」という別のモジュールを使います。最初これは微分可能なレンダリングモジュールかなと身構えてしまいましたが、ただのConv2D中心のニューラルネットワークだったので安心しました。

$h_\theta$が本論文の提案手法で、この関数がこれからよく登場してきます。

### オブジェクトの表現
この論文では**オブジェクト単位で移動可能**な生成モデルを売りにしているので、オブジェクトの表現を定義する必要があります。

Blender等3Dソフトをいじった方なら馴染み深いですが、3Dオブジェクトの変形はある基準となるオブジェクト（立方体、球など）があったときに「**スケール、移動、回転**」の3要素で表されます。例えばBlenderの「トランスフォーム」の部分を見ると、まさに3要素から構成されるパラメーターとなっています。

![23_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/37e46efa-325c-1440-4e9d-cd8d9151c982.png)

**位置＝移動（Translate）、回転（Rotation）、スケール（Scale）**に対応します。変形を$\mathbf{T}$とすると、

```math
\mathbf{T}=\{\mathbf{s}, \mathbf{t}, \mathbf{R}\}
```

で表されます。$\mathbf{s}$がスケール、$\mathbf{t}$が移動、$\mathbf{R}$が回転を表します。ここで$\mathbf{s}, \mathbf{t}\in\mathbb{R}^3$のベクトルです。Blenderの表記見れば一目瞭然ですよね。$\mathbf{R}\in SO(3)$は3×3の回転行列。いくら回転しても、どの軸で回転しても最終的には3×3行列に帰着できます。これは二次元の回転と同じです。

回転はアフィン変換の関数で処理されることが実用上多いですが、そもそもスケールも移動も定義上はアフィン変換なので、$\mathbf{T}$は**アフィン変換**です。アフィン変換同士は合成できるので、$\mathbf{T}$は3×3行列として表現できます。これを$k(\mathbf{x})$とすれば、

```math
k(\mathbf{x})=\mathbf{R}\cdot \begin{bmatrix}s_1 & & \\ & s_2 & \\ & & s_3 \end{bmatrix}\cdot\mathbf{x}+\mathbf{t}\tag{6}
```

この変換は**オブジェクトの点→シーン**へのものです。画像データとして観測されているのは、「もととなるオブジェクトに何らかの変換が加えられたときの、レンダリングされたシーン」なので、（オブジェクトの移動とは独立した）正規のオブジェクトの特徴$\mathbf{f}$を求める必要があります。これ求めるには、$k^{-1}(x)$という**逆変換**を適用する必要があります。実装上は逆行列を求めるだけなので単純です（アフィン変換で表現したかったのはこれが理由です）。

実践的には、**正規のオブジェクト空間**（canonical object space）で特徴を評価し、オブジェクトの移動が与えられたシーンの空間でボリュームレンダリングを行います。これは「オブジェクトの変形のパラメーターの影響を排除して、オブジェクト固有の表現を獲得したい」のだなと、解釈できます。

$$(\sigma, \mathbf{f})=h_\theta\Bigl(\gamma(k^{-1}(\mathbf{x})), \gamma(k^{-1}(\mathbf{d})), \mathbf{z}_s, \mathbf{z}_a\Bigr)\tag{7}$$

同時にこれは**1つのシーンに複数のオブジェクトを配置することが可能に**なります。

## シーンの統合
### オブジェクトのスケールや移動のコントロール
今シーンに$N$個のエントリーがあるとします。$N$個のエントリーのうち、**1個は背景で、残る$N-1$個はオブジェクト**です。

![23_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/9cb97f39-8e3e-44de-349c-5fb3cb79dd08.png)

全体のアウトラインはこのようになります。「Shape（形状） and Appearance（外観）」というのは、$\mathbf{z}_s, \mathbf{z}_a$に対応[^1]し、オブジェクトごとに定義します。

[^1]: あくまで自分の勝手な想像ですが、形状と外観を分けて定義しているのは、一般的な3Dのオブジェクトがメッシュとテクスチャのように、形と外観を独立して管理しているからだと思われます。将来的にはそういった拡張をしたいのかなと思いました。

### 複数のエントリーへの拡張
ところで式(6)の$k(\mathbf{x})$は、オブジェクトの点→シーンへの変換でしたので、実際は変形のパラメーター$\mathbf{s, t, R}$よって条件付けられた、$k(\mathbf{x}|\mathbf{s, t, R})$であることがわかります。$\mathbf{s, t}$についてエントリー数$N$ごとに別々にパラメーターをもたせ、$\mathbf{s}_N, \mathbf{t}_N$とするように拡張します（なぜか回転は拡張していません）。

デモ動画ではボールや自動車を動かしていましたが、この**パラメーター拡張によって、オブジェクト単位の移動が実現**できるようになっています。式(7)をこの拡張にあわせると（これは自分が勝手に書いたものなのでこの表記があっているかはわかりませんが）、

$$(\sigma^N, \mathbf{f}^N)=h^N_\theta\Bigl(\gamma({k^N}^{-1}(\mathbf{x})), \gamma({k^N}^{-1}(\mathbf{d})), \mathbf{z}^N_s, \mathbf{z}^N_a \Bigl| \mathbf{s}_N, \mathbf{t}_N\Bigr)\tag{8}$$

と表記できるのではないかと思います。あくまで$\mathbf{x, d}$はカメラ固有の値で、オブジェクトの移動は$\mathbf{s}_N, \mathbf{t}_N$で表現します。アウトラインは以下の図になります。

![23_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/e094a1df-ca38-a11c-fa2d-1caaf76e757d.png)

凝ったスタイルはないものの、過不足なく表している良い図で、$N$個のエントリーに対して、形状と外観の乱数$\mathbf{z}^N\_s, \mathbf{z}^N\_a$、オブジェクトの変形$\mathbf{T}\_N$を与えます。$h\_\theta^N$では、$\mathbf{T}\_N$によらない正規のオブジェクト（オブジェクトの固有表現）を学習し、$\mathbf{T}\_N$によってアラインメントされた特徴量$(\mathbf{f}\_{ij}^N, \mathbf{\sigma}\_{ij}^N)$を得ます。

ここで$ij$は「**レイ上のサンプリング点×レイの数**」を表すものと考えられます。最初ボクセル上の座標かと思ったのですが、**レイ上の座標**に置き換えられているようです。レイキャスティングとは、先程見たようにカメラの位置からいくつものレイを投射するものでした。レイは開始地点が同じで方向だけ変わるので、$j$番目のレイは$\mathbf{b}\_j$で表されます。

ボリュームレンダリングの説明で、レイを上から見下ろすような図を紹介しましたが、あれは特定のレイにおけるサンプリング点の集合です。あるレイ$\mathbf{b}\_j$に対し、サンプリング点を$N_s$作ります。そのサンプリング点のインデックス$ij$です。つまりここでの$(\mathbf{f}\_{ij}^N, \mathbf{\sigma}\_{ij}^N)$は何を表すのかというと、

* $\mathbf{f}_{ij}^N$：$\mathbf{b}_j$のレイにある$i$番目のサンプリング点の、$N$番目のエントリーに対する（RGB値のような実際はもっと次元数の多い）特徴量
* $\mathbf{\sigma}_{ij}^N$：$\mathbf{b}_j$のレイにある$i$番目のサンプリング点の、$N$番目のエントリーに対する（不透明度のような実際はもっと次元数の多い）Volume density

にあたります。これらをエントリー数$N$の周りで足し合わせて（**Composition Operator**）、全体の特徴量を計算し、ボリュームレンダリング→ニューラルレンダリングします（詳細は後述）。これで、カメラの位置に対する2Dのレンダリング画像ができるというアウトラインです。

### 学習可能なパラメーターが少ない
GIRAFFEの特徴として、**Conv2DやLinear（Dense）といったニューラルネットワーク特有の学習可能なパラメーターがほとんどない**点があります。[Generatorのコード](https://github.com/autonomousvision/giraffe/blob/main/im2scene/giraffe/models/generator.py)を見ると驚くのですが（なにがやっているのを理解するのは大変なので流し見程度がいいです）、ニューラルネットワークの層がなく、**ほとんどPyTorchの関数で行列計算を決定的に行っている**のがわかります。

これはいわゆる3Dレンダリングソフトがやっている実装をPyTorchベースで表現したもので、全体図の**青のブロック**がそれにあたります。一方で**学習可能な係数を持っているのはオレンジのブロックのみ**です。後で確認しますが、従来のGANと比べて圧倒的に少ない係数で動いているのがGIRAFFEです。

### Composition Operator
「Composition Operator」では$N$個の**エントリー周りについて統合**をします。$i$番目のエントリーについて考えます。$h_{\theta_i}^i$のよって推定された、**Volume density**$\sigma_i\in\mathbb{R}^+$と、**特徴ベクトル**$\mathbb{f}_i\in\mathbb{R}^{M_f}$があります。ここでのVolume densityと特徴ベクトルは、カメラのパラメーター$(\mathbf{x, d})$によってアラインメントされたものなのでComposition Operatorを$C(\mathbf{x, d})$とすると、

```math
C(\mathbf{x, d})=(\mathbf{\sigma, f})=\Bigl(\bar{\sigma}, \frac{1}{\bar{\sigma}}\sum_{i=1}^N\sigma_i\mathbf{f}_i\Bigr), \qquad \bar{\sigma}=\sum_{i=1}^N\sigma_i\ \tag{9}
```

と表現できます（論文の式がややこしかったので少し表現変えました）。**Volume densityの統合は単なる平均、特徴ベクトルの統合はVolume densityによる重み付け平均**です。

## シーンレンダリング
### 3Dのボリュームレンダリング
ここでようやくボリュームレンダリングになります。ここでのボリュームレンダリングはRGB値の直接の推定ではなく、$M_f$**次元の特徴量**$\mathbf{f}$の**レンダリング**です。$M_f=3$とすれば普通のボリュームレンダリングです。計算方法は特に変わらないので特段変わったことはしません。

今、カメラの状態$\xi$に対し、ある$\mathbf{d}$方向に**レイ**が与えられたとしましょう。レイ上のサンプリングポイントを$\{\mathbf{x}\_j\}\_{j=1}^{N\_s}$とします。$N\_s$はサンプリング数です。先程の説明では、$i$が同一レイのサンプリング、$j$がレイのインデックスを表していましたが、インデックスが式との間で衝突してしまったようで、**論文表記だと逆転**しています。

ボリュームレンダリングでやりたいのは「**レイの数 × サンプリング数**$N_s$」のサンプリング方向の軸を統合して「**レイの数**」という次元にしたいのです。式で表せば、あるレイに対し、

```math
\pi_{\rm{vol}}:(\mathbb{R}^+\times \mathbb{R}^{M_f})^{N_s} \to \mathbb{R}^{M_f}, \quad \{\sigma_j, \mathbf{f}_j\}_{j=1}^{N_s}\to\mathbf{f} \tag{10}
```

ここで式(10)の$\mathbf{f}$はあるレイについての特徴量ですが、これらの**レイは投射したスクリーンに対するピクセルに相当**します。つまり、各レイは投射された2D画像の（粗い）1ピクセルとみなせます。ただ、実際は**出力画像の解像度よりももっと少ない数のレイ**にするので、後の「Neural Render」モジュールで超解像度のような処理を行います。

ボリュームレンダリングの数値計算の式を用い（[『Optical Models for Direct Volume Rendering』という論文が元](https://courses.cs.duke.edu/spring03/cps296.8/papers/max95opticalModelsForDirectVolumeRendering.pdf)）、

```math
\begin{align}\mathbf{f}&=\sum_{j=1}^{N_s}\tau_j\alpha_j\mathbf{f}_j \\ \tau_j&=\prod_{k=1}^{j-1}(1-\alpha_k)\\ \alpha_j&=1-e^{-\sigma_j\delta_j} \\\delta_j&=\|\mathbf{x}_{j+1}-\mathbf{x}_j\|_2\end{align}
```

と計算されます。何の式だか意味不明ですが、普通のボリュームレンダリングの式(1)を再掲すると、

$$I=\int_0^D c(x(\lambda))e^{-\int_0^\lambda \alpha(x(\lambda'))d\lambda'}d\lambda \tag{1}$$

であるため、**連続関数の積分を離散化したもの**と理解できます。数学的な理論展開が気になる方は元の論文を読んでください。理解のために、$\delta_j=\delta$と一定値で表せるように簡略化してみると、

$$\tau_j=\prod_{k=1}^{j-1}e^{-\sigma_k\delta}=-\exp\Bigl[\sum_{k=1}^{j=1}{\sigma_k\delta}\Bigr]$$

と簡単な式で表すことができます。これは∫がΣに置き換わっているので、まさに積分の離散の場合です。

なお、このボリュームレンダリングによって得られた画像（特徴マップ）の解像度は$16^2$と、本来の解像度よりも粗い解像度になっています。実際に出力するRGBの投射画像は$64^2, 256^2$といった解像度とします。ここに変換するのが次に説明する「2D Neural Rendering」のモジュールです。

### 2D Neural Rendering
ニューラルレンダリングのオペレーターを

$$\pi_\theta^{\rm{neural}}: \mathbb{R}^{H_V\times W_V \times M_f}\to \mathbb{R}^{H\times W\times 3}$$

とします。ここは**普通のニューラルネットワーク**で、$\mathbf{I}_V\in\mathbb{R}^{H_V\times W_V \times M_f}$を最終的な合成画像$\hat{\mathbf{I}}\in\mathbb{R}^{H\times W\times 3}$にマッピングするモジュールです。論文の図を見るのがわかりやすいです。

![23_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/bab9888b-35fa-12f9-f78c-6facef837f86.png)

$U_{P_{NN}}$はNearest Neighborによる拡大、$U_{P_{BL}}$はBilinearによる拡大を示します。

### Discriminatorの構造
Dは普通のConvNetです。合成画像の解像度によってモデル構造が違います。4×4のカーネルという、偶数の畳み込みカーネルを使っているのが珍しいです。

![23_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/8feb8dbd-02aa-479c-dae3-47408e21bfbe.png)

## ハイパラ設定
今までの内容を振り返る意味からもハイパラを見ていきましょう。

* $\{h\_{{\theta}\_i}^i\}\_i^{N-1}$：形状と外観の乱数、オブジェクトの変形の行列、カメラのパラメーターが与えられたときの**オブジェクトの特徴場**。**オブジェクト間で重みは共有**し、MLP＋ReLUで構成。8レイヤーで隠れ層の次元は128とする。出力はVolume densityの$\sigma^i$、特徴量$\mathbf{f}^i$でそれぞれ1、$M_f=128$次元。背景の特徴場$h_{{\theta}\_N}^N$については、次元と層の数を半分にする（**オブジェクトとは別の重み**にする）。
* カメラのPositional Encodingの$L$の数は、座標の$\mathbf{x}$で10、方向の$\mathbf{d}$で4
* 1つのレイに対するサンプリング数$M_s=64$
* 3Dのボリュームレンダリング（2Dニューラルレンダリングの前）の特徴マップ$\mathbf{I}\_v$の解像度は16
* Generatorの係数に0.999でDecayする移動平均つき
* バッチサイズ32のRMSProp。Dの学習率は1e-4、Gの学習率は5e-4
* 解像度が$256^2$で出力するときは$M_f=256$とし、Gの学習率を2.5e-4とした

## データセット設定
データセット設定が面白くて、最初自分はカメラのパラメーターと画像が紐付けられたデータでしか訓練できないのかと思ったのです。ただそんなことはなく、**CelebA HQやFFHQ、LSUN ChurchesといったこれまでのGANで使われていたデータセットに対しても生成成功**しています。

最初はいきなり普通の画像でやらずに、「[Chairs](https://arxiv.org/abs/1809.09761)」というデータセットで行っています。これは3Dソフトで様々な角度からレンダリングされた画像です。これは「Chairs」の論文からです。

![23_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/b2d99953-3d83-07e6-8776-d6dcbb20d2e4.png)

挑戦的な試みとして1つのオブジェクトで、リアルな世界のデータセット（LSUN Churches、FFHQ、CompCars）でもトライしています。CompCarsとはこのようなデータです（[公式サイト](http://mmlab.ie.cuhk.edu.hk/datasets/comp_cars/)からの引用です）。

![23_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/c953ef96-9d42-0a6c-5785-92c05cc02b30.png)

CompCarsではオブジェクトの位置のバリエーションを出すためにRandomCropを導入しています。個人的に不思議なのは、**このような単一オブジェクトあたり1つの視点から撮られていないデータセットでも、カメラの位置、オブジェクトの位置やスケールといったパラメーターをコントロール可能な表現を獲得**できていることです。これらのパラメーターは訓練時には乱数で入力していましたが、そんなアバウトなやり方でもDisentangleしてくれるというのがGIRAFFEのとても不思議なところです。似た感覚は最初にDCGANを見たときも感じたので、慣れれば当たり前になるのかもしれません。

CompCarsに対してどの程度の生成が可能になったかは、[プロジェクトページ](https://m-niemeyer.github.io/project-pages/giraffe/index.html)にも動画としてまとまっています。CompCarsはデータセット全体で見れば、いろんな視点から車を撮っていたのでうまくいったのかもしれません。

論文としてはリアルのデータと、合成データを行ったりきたりしており、まだ「視点やオブジェクトの位置でほぼほぼOne-shotなリアルなデータ」を前提とした想定ではなさそうです。これは「Clevr-2345」というデータセットに対して、訓練の進みを可視化した図です。

![23_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/1d657cfa-1484-8dbd-eb57-7ffb7041d3d1.png)

訓練のかなり早い段階で、教師なしのDisentangleに成功しています。こういった合成データに対しては成功率が高そうです。

## 教師なしでどの程度コントロールできるか
「教師なしでDisentangle」でこの論文のテーマになっていますが、いろんな項目に対してDisentangleしようとしているのがわかります。

* 背景とオブジェクト
* 個々の物体の回転や平行移動、形状や外観
* カメラの仰角の変更
* 形状を変えずに外観のみ変更
* 学習データの分布外のパラメーターの生成

![23_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/77bf2453-83a5-33a4-b521-e522ededca68.png)

**訓練データを超えたデータに対する生成**です（左）。これはネットワークを単なるCNNのモデルとしてやるだけではまず不可能で、3Dのレンダリングプロセスを相当決め打ちして、コントロール可能なパラメーターを導入したおかげだと思われます。「構成されたシーン表現」をGANに導入するのが大事なのでしょう。

![23_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/ff024f9e-6ae0-7726-992a-73787c24dafd.png)

右が先行研究との比較です。物体の回転で差がわかりやすいです。同じNeRFベースのGRAFでは、回転角によっては車の形状に大きな歪みが生じています。GRAFとの大きな違いは、**ボリュームレンダリングをRGBでダイレクトにやらずに、もっと次元数の大きい特徴マップに対して行う**ということでしたので、GRAFのようにRGBでやると表現が足りていないことがわかります。

定量評価は生成画像のFIDでやっています（低いほうが良い）です。見て明らかなレベルなので、定量評価貼るまでもないと思いますが、一貫して先行研究より良いです。

![23_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/34caf425-0649-dcc8-3e36-e183de4518d7.png)

## 計算量も軽い
GRAFとの大きな違いは次元数だけではなく、**計算量とも関係**しています。GIRAFFEのボリュームレンダリングは「16×16」という低解像度の特徴マップを出力し、あとはCNNで解像度を引き上げるというものでした。

実際にGRAFと比較すると、レンダリング速度が**64×64で110.1ms→4.8ms**、**256×256が1595.0ms→5.9ms**と大幅に改善しています。

またGIRAFFEはモデルの係数が少ないことも特徴で、Generatorの**パラメーター数は0.41M**です。

![23_21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/7a71f767-5a4b-5c23-82c7-4f492c13e5eb.png)

**こんなに少ないパラメーター数で教師なし3D-Disentangle**ができるというのは、今までの2DGANばかり見てきた自分にとってはにわかには信じがたいのです。Disentangleが強いGANといえばStyleGANの系列でしたが、それでもこんな少ない係数ではできないですし、3Dの物体の変形を定義はまずできなかったと思われます。

## 訓練時はカメラや変形のパラメーターが乱数？？
個人的に一番気になったのは、「なぜカメラのパラメーターや、オブジェクトの変形行列が与えられていないにも関わらず、最終的にそれらに関してコントロール可能な表現を獲得できているのか」という点です。多分これを紐解くには訓練時にコードを見る必要がありそうです。

Generatorの[forwardの関数](https://github.com/autonomousvision/giraffe/blob/39da6b434b5c14893a35f009f04c05e43c1b2bc6/im2scene/giraffe/models/generator.py#L84)を見てみましょう。

```python
    def forward(self, batch_size=32, latent_codes=None, camera_matrices=None,
                transformations=None, bg_rotation=None, mode="training", it=0,
                return_alpha_map=False,
                not_render_background=False,
                only_render_background=False):
        if latent_codes is None:
            latent_codes = self.get_latent_codes(batch_size)

        if camera_matrices is None:
            camera_matrices = self.get_random_camera(batch_size)

        if transformations is None:
            transformations = self.get_random_transformations(batch_size)

        if bg_rotation is None:
            bg_rotation = self.get_random_bg_rotation(batch_size)

        if return_alpha_map:
            rgb_v, alpha_map = self.volume_render_image(
                latent_codes, camera_matrices, transformations, bg_rotation,
                mode=mode, it=it, return_alpha_map=True,
                not_render_background=not_render_background)
            return alpha_map
        else:
            rgb_v = self.volume_render_image(
                latent_codes, camera_matrices, transformations, bg_rotation,
                mode=mode, it=it, not_render_background=not_render_background,
                only_render_background=only_render_background)
            if self.neural_renderer is not None:
                rgb = self.neural_renderer(rgb_v)
            else:
                rgb = rgb_v
            return rgb
```

とても単純なコードで、実際のレンダリングプロセスは```self.volume_render_image```や```self.neural_renderer```で行っています。

ポイントとなるのは、```camera_matrices is None```や```transformations is None```のケースです。FFHQやCelebAの場合、データセットにこれらのパラメーターがないので「どうやって訓練させるか」がすごい気になったのです。これらのパラメーターがない場合は**乱数で訓練**させています。

「ない場合は乱数でやる」というのはわかったのですが、もし合成データなどでパラメーターがとれた場合はどうしているのでしょうか。これは[training.py](https://github.com/autonomousvision/giraffe/blob/6c1aacadaeac1740691de2e20a6e1ade021184cb/im2scene/giraffe/training.py)のコードを見るとわかります。

```python
    def train_step_generator(self, data, it=None, z=None):
        generator = self.generator
        discriminator = self.discriminator

        toggle_grad(generator, True)
        toggle_grad(discriminator, False)
        generator.train()
        discriminator.train()

        self.optimizer.zero_grad()

        if self.multi_gpu:
            latents = generator.module.get_vis_dict()
            x_fake = generator(**latents)
        else:
            x_fake = generator()

        d_fake = discriminator(x_fake)
```

Generatorのforwardの部分を見てみましょう。```if self.multi_gpu```は複数GPUのためのコードなので無視していいです。訓練時は```x_fake = generator()```としているのがわかります。引数を何も与えない場合は乱数で訓練されるので、**データセットの如何に関わらず、訓練時はカメラのパラメーターや変形行列は何も与えていない（乱数として訓練させている）**という衝撃の事実がわかりました。

よく考えると、訓練時にカメラや変形行列を与えていたら「Conditional GAN」と呼ばれるはずです。例えば2DGANのACGAN（クラスのラベルを訓練時に与える）は典型的なConditional GANですが、Conditional GANだったら**教師なしでDisentangle**という表現は使わないはずです。カメラや変形のパラメーターを乱数で与えているからこそ、教師なしと呼べるのです。この事実に気づいたので、個人的には腑に落ちました。

## このモデルの限界
個人的に疑問だったのが「これデータセットによってうまくいくかどうか相当変わるな」という点です。どういった点に限界があるのかという点は、論文にも書かれています。

### データセットのバイアス
データセットに固有のバイアスがある場合、変動要因を分離するのに困難するとのことです。下の図はCelebA-HQですが、**圧倒的にカメラ目線の画像が多いため、回転させると目や髪の向きが固定されてしまいます**。

![23_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/1294615d-5b53-80ab-2f5b-6cc32522cd49.png)

### オブジェクトの変形の分布
このモデルでは、**カメラのポーズやオブジェクト変形を一様分布**と仮定して訓練していますが、データセットの実際の分布とミスマッチが問題となることがあります。例えば、背景に境界があるChurchや、前景が背景を含むCompCarsでは、**背景と前景の分離に失敗**することがあります。

![23_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/232088/723873e2-2893-9eeb-4adf-7d778a44d961.png)

これを見ていると、背景と前景のテクスチャが似ていると起こりやすいのでしょうかね。このミスマッチを解消をするための分布設計については、本論文では「今後の課題」としています。

## まとめと感想
この論文では、これまでの2DGANの枠組みを飛び越えて、「カメラの位置や角度」「オブジェクトのスケール、移動、回転」「オブジェクトの形状や外観」を、**データセットによる追加の教師なしで3Dレンダリングベースで生成できる**という衝撃的な内容でした。「これはBest Paper Awardsとるわ」って感じの内容で、細部を読み込んでいるとあまりにその凄さに震え上がった内容でした。

基本となる枠組みはボリュームレンダリングという、従来の3Dレンダリングをベースとしています。3Dレンダリングの枠組みをGANに導入するだけでこれだけのすごいことが起こるので、今後こういうアプローチが流行っていくと思います。3Dレンダリングの部分の知識がなくて、付け焼き刃で調べたのですが、ここが複雑になっていくとだんだんついていくのが大変になっていくので、「GIRAFFE程度で勘弁してください」という気持ちは多少ありました。

今までGANのDisentangleというと、[3日目に紹介](https://qiita.com/koshian2/items/c69cd51a2087d9fa898a)したような「StyleGANを訓練してその潜在変数をいじる」というようなケースが有望なやり方でした。StyleGANの場合は訓練したモデルがあり、その潜在変数を解釈・操作するというとても**ボトムアップ**な方法です。NeRFもそうですが、GIRAFFEの場合は、「こういう操作をしたいから、3Dレンダリングをベースとしたこういうフレームワークを作って訓練」というかなり**トップダウン**なやり方です。事実、計算量パラメーターのコントロール可能性でもうまく行っています。GANの論文が出たのが2014年ですが、たった7年でここまで進化するなんて驚きですね。もう5年ぐらいしたら3Dのゲーム画面みたいなGANができているかもしれません。

感想としては「とにかく読むのが難しいけど、それに見合った感動は与えてくれる」という不思議な論文した。「なんで乱数から訓練して、形状や変形を学習できるの？」という不思議な感覚は、最初にDCGANを見たときのそれをまさに思い出させるようで、懐かしい感覚になりました。難しい概念ながら、**論文がとにかく丁寧に書かれていて**、「論文をよむのが一番早いし楽」というのがとても良かったです。ただ実装は相当沼なので、1から実装はやめておいたほうがいいと思います（3Dレンダリングの部分がうまくライブラリになれば楽になりそう）。

GIRAFFEの解説いくつか散見されるのですが、ここまで突っ込んで読んだ記事は多分日本で（もしかしたら世界でも？）自分が初めてだと思われるので、NeRFの系列も含めて皆さんの参考になれば幸いです。

## 告知
このアドベントカレンダーが本になりました！
**https://koshian2.booth.pm/items/3595424**
Amazonでも扱いあります詳しくは👉　https://shikoan.com