Blenderに機械学習をさせよう!【GeometryNodes】

まずはこちらの動画をごらんください。

Blender上で8×8ピクセルの手書き文字認識のニューラルネットワークの学習を行う様子です。
左は学習中の推論結果、右はテスト画像240枚中何%正しく推論できたかを表示しています。

ランダムなパラメーターから出発して学習を開始し、私の実行環境では数秒程度でほぼ90%近い精度の機械学習モデルをつくることができました。


以前と何が違うか

実は以前(10ヶ月前)にも、こちらの記事でBlender上で機械学習モデルを動かすという試み自体は行ったのですが、これは既に学習済みのモデルのパラメーターをテクスチャとして読み込んで推論するだけのものでした。

今回は、推論だけでなく学習も(!)Blender上で行おうという試みです。

全体像

ノード全体は以下のような構成になっています。

本質的な部分は可視化部分を除いた左半分ですので、この記事では左側のみ解説します。

特に、「Model」グループの中身はこのようになっています。
V字に配置されていますが、左側で順伝播Forward Propagation)、右側で逆伝播Back Propagation)を行っています。

より詳細な実装を知りたい方は、GitHubにも公開していますので、
そちらをご確認ください。

学習用・評価用データの準備

学習用データは事前のsklearnのdatasets.load_digits()から生成し、テクスチャとして読み込みました。
学習用に1500枚、検証用に240枚用意し、それぞれ同じ列に同じラベルの数字が並ぶように調整してあります。

色空間はGeometryNodesにも影響があるので「リニア」等にしておく必要があります。

今回のGeometryNodesでは、学習データやパラメーターなど、ほとんどすべての数値は「ポイント」ジオメトリの「半径」として扱います。

テクスチャから色情報を取得し、ポイント半径として設定しニューラルネットワークに渡します。(同時に、正解ラベルを表すジオメトリも出力します。)

これは「1」

GeometryNodes上での値の管理方法

パラメーターの学習にはBlender 3.6 LTSから追加されたSimulation Zoneを用います。

Simulation Zoneを用いることで、パラメーターを更新して次のフレームでまた使う、ということが実現できます。

Affine層などはパラメーターを持つわけですが、これをどのように管理するか、という問題があります。

ここでは、パラメーター用のジオメトリをひとつつくり、そこでニューラルネットワーク内のすべてのパラメーターを管理するようにしました。

具体的には、「半径」をパラメーターとする「ポイント」です。これが"メモリー"の役割を果たします。
今回は2410個のパラメーターを利用しているため、ポイント数も2.4Kと表示されています。

「radius」属性の値がパラメーター

しかし、すべてのパラメーターをぶちこんでしまっているので、このままではどのポイントが何のパラメーターを表しているかわかりません。

そこで、適当なインスタンスを1つだけ生成し、特定の名前の整数属性を格納することで、あるパラメーターが「何番目から始まるか」と「何個続くか」の情報を読み出せるようにしました。

例えば「affine1__w」はインデックス0から連続する2048個のパラメーターで構成されます。
適当なインスタンスを生成するだけのグループ「Initialize Parameters」

つまりインスタンスのドメインでインデックス(ポインター)の管理をし、ポイントのドメインが値そのものを持っている、という実装になっています。

実際に値はインデックスサンプルノードなどを駆使することで取得できます。

ドメインをインスタンスにするの、わすれがち

GeometryNodesで値を管理する仕組みに関しては以上となります。

ここからは、ニューラルネットワークそのものの説明になります

順伝播(Forward Propagation)

まず今回使用しているニューラルネットワークですが、

  • 1層目 Affine

  • 2層目 ReLU

  • 3層目 Affine

からなる非常にシンプルな構成です。
Loss関数にはSoftmaxCrossEntropyを用いています。

左から右に順伝播します。

pythonであればforwardメソッドとbackwardメソッドを持ったクラスなどを定義できるのですが、GeometryNodesではノードがループするようなつながりを持てないため、しぶしぶ「Affine Forward」「Affine Backward」「ReLU Forward」「ReLU Backward」のように個別にForwardとBackwardのグループをつくることにしました。

Affine Forward

Affine Forwardの具体的な中身はこんな感じです。

「Affine Forward」

なかなか複雑なので全部を説明するのは大変なうえに、多分この記事を読んでいる人もちゃんと理解する気がある人なんていないだろう、と思っているのでざっくりいうと、「新しいポイントを生成して、入力のいい感じのインデックスの半径を拾って新しい半径を設定し、出力にしている」という感じです。

特に、Affineは結局行列の積なわけですが、これは「要素コピー」「インデックスサンプル」「フィールド蓄積」という3種類のノードをいい感じに組み合わせることで実現可能です。

ちょっと前までは行列の積はだいぶ無理やり計算していたのですが、
今はこれらのノードがあるおかげでかなりスマートに記述できるようになりました。

順伝播のタイミングで、もしパラメーターがまだ定義されていない場合(=パラメータージオメトリのインスタンスドメインが所定の名前の属性を持っていない場合)は、新しいパラメーターを生成します。

今回活性化関数としてReLUを利用しているため、AffineのWeightはHeの初期値、Biasは0で初期値を設定しています。

Heの初期値は活性化関数としてReLUを利用している場合の良い初期値として知られており、平均が $${0}$$ 、標準偏差が $${\sqrt{\frac{2}{n}}}$$ (ただし $${n}$$ は入力数)であるような正規分布として初期値を設定するというものですが、GeometryNodesにそういうノードはないので、一様乱数にBox–Muller変換を用いることで実現します。

Box-Muller変換は次のようなもので、独立な0~1の範囲の一様乱数 $${X}$$ 、 $${Y}$$ から、標準正規分布に従う乱数 $${Z}$$ を生成します。

$$
Z = \sqrt{-2\log{X}} \cos{2\pi Y}
$$

Heの初期値(Box–Muller変換)

ReLU Forward

さて、一方でReLU層は非常にシンプルな実装ができます。

「ReLU Forward」

このようにして、

  • Affineでパラメーター行列との積を求める

  • ReLUで活性化する

という処理が行なえます。

これを数珠繫ぎにして順伝播させ、最終的にSoftmaxCrossEntropyグループでSoftmaxを通した推論結果と、逆伝播用の誤差を出力させます。

SoftmaxCrossEntropy

SoftmaxとCrossEntropyは本来別のものですが、Softmaxを使用した際のCrossEntropy誤差はSoftmaxの結果から正解ラベルを引くだけですぐに求めることができるので、一緒にすることで若干伝播をスキップできます。

「SoftmaxCrossEntropy」

逆伝播(Backward Propagation)

さて、本題とも言える誤差逆伝播です。

順伝播に対応する逆伝播用のグループを接続します。

逆伝播では各グループの入力として、「誤差用のジオメトリ」と「パラメーター用のジオメトリ」に加えて、「順伝播当時に流れてきた入力のジオメトリ」も使用します。

Affine Backward

与えられた誤差をもとに、修正すべきパラメーターを修正し、新しい誤差を次のグループに伝播します。

「Affine Backward」

学習係数などのハイパーパラメーターはGeometryNodes全体で一括で変更できるようにパラメーター出力のみを担う専用のグループを作成し、そこから値を拾うように設計しています。

「Global Parameter」

ReLU Backward

ReLUはパラメーターを持たないのでパラメーターに関してはノータッチで流しつつ、誤差はの方はReLU Foarwardで出力が0にされた箇所のみ0に変更して流します。

「ReLU Backward」

これでニューラルネットワークの学習に必要なパーツが揃いました。

これで実際に学習がうまく進んだのかどうかを評価します。

評価

評価用データセットから240枚のポイントを生成するMake Points Allというグループを作ってあるので、これを学習のときに使ったのと全く同じ(!)Modelグループに流し、シミュレーション出力から得られる学習済みパラメーターを用いることで、学習モデルの評価を行うことができます。

 

今回の構成では(初期値や学習に使われるトレーニングデータの取り出し方を決めるシード値にもよりますが)、大体92%ぐらいの精度で文字認識ができるモデルを作ることができました。

「91.7%」

ネットワークを変えてみる

多層にする

さて、せっかくGeometryNodesのようなノードベースシステムを使っているのですから、GUIでネットワークを変更できる利点を享受しないともったいないですよね

先程は

  • Affine(64, 32)

  • ReLU

  • Affine(32, 10)

からなる簡単なニューラルネットワークでしたが、もう1層増やして

  • Affine(64, 96)

  • ReLU

  • Affine(96, 32)

  • ReLU

  • Affine(32, 10)

にしてみます。

AffineとReLUを1層ずつ追加

実際にこれで学習させてみると、少し良い結果が得られました。

「93.8%」

活性化関数を変えてみる

多層化したうえで、ReLUだった箇所を「Swish」に変えてみます。

Swishは、引数とSigmoidに引数を渡したものとの積で求めることができ、関数の形としてはReLUに近いものの、微分不可能な点がなくなり、なめらかになっています。

Swish関数
Wikimedia Commonsより引用
https://commons.wikimedia.org/wiki/File:Swish.svg

実装します。

シグモイド関数「Sigmoid Function」
「Swish Forward」
「Swish Backward」

先程のReLUの場合と同じシード値を用いて学習させてみましたが、ReLUよりも早い時点でより高い精度が出せていました。

「94.2%」

このようにノードを繋ぎ変えるだけで試行錯誤ができて楽しいです。

ところで…

余談ですが、もしノードベースで試行錯誤したい、というだけであればNeural Network Consoleなどもおすすめです。

実際今回使ったニューラルネットワークもNeural Network Consoleを用いて事前に試行錯誤したものを再現しています。

YouTubeにかなりわかりやすいチュートリアルがあるので、機械学習に興味がある方はぜひ触ってみると楽しいと思います。

その他参考にしたもの

インターネット上で見つかる機械学習周りの情報の多くはTensorflowやPyTorchのようなライブラリが前提となっていたり、誤差逆伝播法のざっくりとした説明をした動画などはあっても、具体的な実装がよくわからなかったりします。

今回の取り組みのように、一切ライブラリに頼らずにニューラルネットワークを構築したい、という場合にはこちらの書籍がおすすめです。

また、ChatGPTにもだいぶ助けられました。

GeometryNodesに実装する前に検証用に書いたpythonコード全文を見せて、「ここに新しい活性化関数を実装して」みたいに言えば、すんなり新しいクラスを追加してくれます。

さいごに

先程も書きましたが、以上の実装したGeometryNodesを含むBlendファイルはGitHubで公開しています。

興味があればぜひお手元でお試しください。
Blender 3.6 LTS以降でのみ動作します。)


この記事が気に入ったらサポートをしてみませんか?