チュートリアル / Bifrost for Maya Rigging Challenge~一歩先のリグ・アニメーションに挑戦~
第6回:機械学習でリュックの連動リグ〜導入編〜

  • Maya
  • アニメ
  • キャラクター・リグ
  • ゲーム
  • コラム
  • シミュレーション
  • チュートリアル
  • 上級者
  • 中級者
  • 映画・TV

みなさん、こんにちは。

本コラムではMayaのプラグイン"Bifrost"を使って、リグ、アニメーション、物理シミュレーションなどの観点から作成例を紹介していきます。今回は「機械学習で事前に用意したポーズデータを近似する連動リグ」を作ってみたいと思います。

成果物は以下のGIFの通りです。導入編と実践編の全2回に分けてやっていきます!

※導入編ではほとんどBifrostを使いません。いきなりグラフから入るとあまりにも理解不能になってしまいそうなので、まずは概要をしっかり押さえるための回とさせていただければと思います。実践編ではBifrostをモリモリ使っていきますのでお楽しみに!

今回の成果物
今回の成果物

【環境】
・Windows 11
・NVIDIA GeForce RTX 4060 8GB
・Maya 2026
・Bifrost 2.13.0.0
・Python 3.11.9
・PyTorch 2.7.0+cu118
・numpy 2.2.6
・matplotlib 3.10.3
・tensorboard 2.19.0(実践編で使用)
※機械学習関連のコンパウンドは Bifrost 2.12 以降で使用できます。

状況の確認

まずはこちらをご確認ください。上半身とリュックのリグです。リュックの各コントローラはuvPinを使ってメッシュコンストレイントしてあります。ある程度は体に追従するようにしてありますが、リュック本体が大きく伸びてしまったり、肩を上げてもリュックが引き上げられなかったりと、細かい連動はありません。手作業で調整できる範疇ではあるのでこのままでも大きな問題はありませんが、もう少し補正を入れておきたいと思います。

上半身とリュックのリグ

ドリブンキーやRBFでポーズを登録していく手もありますが、今回のケースは入力側の要素が多い(背骨複数と両肩)ため、ちょっと組み合わせの設計が難しそうな気がします。ということで、クロスシミュレーションから得られた最適なポーズを機械学習モデルで近似し、複雑な連動の表現にチャレンジしてみます!

ニューラルネットワーク概要

Bifrostで機械学習に取り組むにあたり、Maya Learning Channelにて非常に分かりやすい動画が4本公開されていますので、そちらも合わせて確認いただくことを強くお勧めいたします。本コラムと内容が重複する部分も多数あります。

Machine Learning in Bifrost - YouTube

機械学習で取り組むことにメリットがありそうな課題のひとつとして挙げられるのは『数式などのルールで定義しにくい複雑な何か』です。例えば「2ボーンIK」なんかはエンドの位置から決められた計算式を使って一発で関節角度を解くことが出来るためこれには該当しません。「肘が90°曲がったら補助骨のtzを+5する」のようなドリブンキー系も単純です。それに対し「背骨と左肩と右肩それぞれの姿勢によって目標の状態が決まり、かつ動作が直線的ではない。」というような今回の状況は人の手でルールを決めることが困難なため、機械に任せてしまうことが有効手段になるかもしれません。

ざっくり言うと、人が定義した関数に値を入力すると出力値が得られるのに対して、入力値と出力値が分かっている状態でそれを満たす関数を探すのが機械学習になります。RBF補間と近いアプローチになりますが、もっと入力が高次元で複雑な場合を想像していただければと思います。さらに、負荷の高い計算結果をシンプルな近似モデルに落とし込むことでパフォーマンスが向上する可能性も秘めています。

機械学習の説明の概要

ではさっそく、ニューラルネットワークの中でも基本的な構造である多層パーセプトロン(MLP:Multi-Layer Perceptron)について確認しておきます(*1)。詳細な解説は専門家にお任せするとして(*2)、Bifrostで扱うための最小限の要素だけ確認していきます。

*1) MLPはあくまでニューラルネットワークの構造のうちのひとつです。他にもCNNなど、より複雑な構造もありますが、簡易的な制御においてはMLPで十分に効果を発揮できると思います。ちなみに、現在Bifrostに用意されているコンパウンドで作成できる構造はMLPのみになります。
*2) おすすめ動画:
ニューラルネットワークの仕組み | Chapter 1, 深層学習(ディープラーニング)
深層学習の仕組み, 勾配降下 | Chapter 2, 深層学習(ディープラーニング)

下のような図を見たことがある方も多いかと思いますが、まさにこれがMLPの構造です。

MLPの構造

もう少ししっかりと書くと次の図のようになります。ここでは、入力層→隠れ層1つ→出力層という構成のネットワークを例にしています。左端に並んでいるx1, x2, x3が入力値、右端のy1, y2が出力値です。

MLPの構造詳細

中間層の出力hを式で表すと以下の通りになります。

中間層の出力hを表す式

・h(l):第l層の出力
・W(l):第l層の重み行列
・b(l):第l層のバイアス
・f:活性化関数(ReLUやSigmoidなど)

各層では、重み行列 W(l)と前の層の出力 h(l-1) の行列積を計算し、バイアス b(l) を加えます。さらに、活性化関数 f を適用して出力が得られます。このように、すべての入力ノードと出力ノードが接続された層を“全結合層”と呼びます。活性化関数はいろいろな種類がありますが、例えば以下のReLUのような非線形なものを使います。

ReLUの例

学習対象は重み行列Wとバイアスbになります。訓練データとして用意した入力xと出力yの関係をできるだけ再現できるよう、Wとbを調整していく工程が“学習”です。出力が正解からどれだけズレているか(損失:Loss)を測り、そのズレ(誤差)を元にWとbを調整していく"誤差逆伝播"という手法で学習が行われます。

この調整をたくさん繰り返すことで、ネットワークは『入力と出力の関係を近似する関数』として機能するようになります。

PyTorchの準備

前項のニューラルネットワークを、機械学習のライブラリ PyTorchを使って試してみたいと思います。まずは全体の流れを確認します。

①訓練データの準備
②モデルの定義~学習(PyTorch)
③結果の確認(Bifrost)

①はBifrostで作成しても良いですし、スクリプトで作成しても良いです。たくさん用意できればツールは問いません。②はMayaもBifrostも使用せずスタンドアロンのPythonを使っていきます。③はBifrostです。

ではまず実行環境を整えていきます。VS CodeとスタンドアロンのPythonがインストール済みの前提で(*3)、PyTorchが使える仮想環境を作るところから始めていきます。

*3) Pythonがインストール済みでない場合は下記よりインストールをお願いします。Maya上で使っているPythonとは別になります。エディタはなんでも構いませんが、特にこだわりが無ければVS Codeがオススメです。
Python 3.11.9 ダウンロードページ
(他のバージョンでも問題ありません)
VS Code ダウンロードページ

ターミナルで以下のコマンドを順に実行し、仮想環境(venv)を作成してアクティブにします。

python -m venv venv
venv\scripts\activate

仮想環境が有効になったら必要なパッケージをpipでインストールしていきます。

使用するパッケージは下記の通りです。
・PyTorch 2.7.0+cu118
・numpy 2.2.6
・matplotlib 3.10.3
・tensorboard 2.19.0(実践編で使用)

PyTorchは後ほど入れますので、他3つをインストールしてしまいます。とりあえず最新バージョンで揃える場合は1行目のコマンドを、本記事と全く同じバージョンで揃える場合は2行目のコマンドを実行します。お好みでどうぞ!

pip install numpy matplotlib tensorboard
pip install numpy==2.2.6 matplotlib==3.10.3 tensorboard==2.19.0

次にPyTorchをインストールします。インストールコマンドは環境によって変わるので、PyTorch公式ページを参照します。私の環境はWindows、Pythonのpipコマンドを使用、NVIDIAのグラボなのでCUDA、を選択します。すると下部にインストールコマンドが表示されますので、こちらをターミナルにコピペして実行します。

PyTorchの環境
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

正常にインストールできたか確認しましょう。ターミナルに以下のコマンドを1行ずつ順に打っていきます。

python
import torch
torch.cuda.is_available()
実行結果

import torchがすんなり通ればインストールは完了しています。さらに、torch.cuda.is_available()がTrueになればGPUでの分散処理が可能な状態になります。FalseだとしてもCPUで続行することはできますので、これは必須ではありません。

sin関数の近似モデル学習を試す

sinを近似する意味などまったくありませんが、簡単な例で実際の流れをひと通り試してみます。

訓練データはスクリプト内で学習直前に用意するとして、さっそく学習関連のスクリプトを確認していきます。以下のスクリプトは学習用の関数(train)と学習済みモデルのパラメータをファイルに保存する関数(save_model_params)です。

trainはモデルやデータローダーなどを受け取り、指定したエポック数ぶん学習を行います。戻り値はLossの推移を格納したリストです。

save_model_paramsはモデルの全Linearレイヤーの重みとバイアスをnpyファイルに保存します。このnpyが後ほどBifrostに読み込むファイルになります。

import os
import numpy as np
import matplotlib.pyplot as plt
import torch
from torch import nn, optim
from torch.utils.data import TensorDataset, DataLoader

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

def train(model:nn.Module, train_loader, criterion, optimizer, num_epochs=100):
    """ 学習しつつLossの推移を保存する関数 """

    train_loss_list = []

    model.train()
    for epoch in range(num_epochs):
        total_loss = 0.0
        total_samples  = 0

        for train_x, teacher_y in train_loader:
            train_x = train_x.to(device)
            teacher_y = teacher_y.to(device)

            optimizer.zero_grad()  # 勾配リセット
            pred_y = model(train_x)  # 予測
            loss = criterion(pred_y, teacher_y)  # 損失取得
            loss.backward()  # 勾配計算
            optimizer.step()  # パラメータ更新

            # loss記録
            batch_size = train_x.size(0)
            total_loss += loss.item() * batch_size
            total_samples  += batch_size

        avg_train_loss = total_loss / total_samples
        train_loss_list.append(avg_train_loss)

        print(f'[Epoch {epoch+1:2d}/{num_epochs:2d}], Train Loss:{avg_train_loss:.4f}')

    return train_loss_list

def save_model_params(model, save_dir):
    """ model.netのLinearレイヤーのみパラメータを保存する関数(レイヤー数不問)"""

    print('Save model parameters...')
    os.makedirs(save_dir, exist_ok=True)

    i = 1
    for layer in model.net:
        if isinstance(layer, nn.Linear):
            name = 'layer_{}'.format(str(i).zfill(2))
            w = layer.weight.detach().cpu().numpy()
            b = layer.bias.detach().cpu().numpy()
            weights_path = os.path.join(save_dir, f'{name}_weights.npy')
            biases_path = os.path.join(save_dir, f'{name}_biases.npy')
            np.save(weights_path, w)
            np.save(biases_path, b)
            print('Save', weights_path, biases_path)
            i += 1

次はモデルの定義です。入力と出力がそれぞれ1次元、中間層が2つでノード数は16、8としています。nn.Linearが全結合層、nn.ReLUが活性化関数です。

class Network(nn.Module):
    def __init__(self):
        super(Network, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(1, 16),   # 入力: [θ]
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, 1)    # 出力: [sin(θ)]
        )

    def forward(self, x):
        return self.net(x)

最後にメインの実行部分です。BATCH_SIZE や LEARNING_RATE はハイパーパラメータと呼ばれる、学習精度を高めるために手動で微調整する項目になります。

前半部分ではsin関数の入出力のペアを1000個用意してデータローダーに格納しています。後半でモデル、損失関数、オプティマイザ(*4)を用意し、学習を行った後、学習済みパラメータを保存しています。最後はLossの推移をグラフにプロットして終了です。

*4)
損失関数:
「答えとどれくらいズレているか」を測るためのものです。MSE Loss(平均二乗誤差)は、ズレ(誤差)を二乗して平均した値で、小さいほど正解に近いことを意味します。
オプティマイザ:
重みをどう変えれば損失関数の値が小さくなるかを計算して、少しずつ調整してくれる装置です。Adamは効率よく学習してくれる代表的な方法とのことなので、とりあえずこれを選んでおきます。

BATCH_SIZE = 32
LEARNING_RATE = 4e-4
EPOCH = 300

if __name__ == '__main__':

    model_name = 'sin'
    model_version = 'v1'
    save_dir = os.path.join('models', model_name, model_version)

    # 訓練データの準備(sinの入力と出力のペア)
    theta = np.linspace(-np.pi, np.pi, 1000).reshape(-1, 1) # 入力: -π ~ π の範囲で1000個用意
    theta_norm = theta / np.pi # 正規化
    targets = np.sin(theta) # 出力: sin(θ)

    # torch.tensorに変換
    x_train = torch.tensor(theta_norm, dtype=torch.float32)
    y_train = torch.tensor(targets, dtype=torch.float32)

    # 入力と出力をまとめてデータローダーに
    dataset = TensorDataset(x_train, y_train)
    train_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)

    # モデル定義
    model = Network().to(device)

    # 損失関数とオプティマイザの定義
    criterion = nn.MSELoss()
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)

    # 学習
    train_loss_list = train(model, train_loader, criterion, optimizer, num_epochs=EPOCH)

    # 学習したモデルのパラメータを保存
    save_model_params(model, save_dir)

    # Lossの推移をプロット
    plt.plot(range(1, EPOCH + 1), train_loss_list)
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.title('Training Loss')
    plt.grid(True)
    plt.show()

以上を繋げたスクリプト全文がサンプルデータ「train_sin.py」になります。

ちなみに、公式の参考スクリプトはBifrost Browser > Machine Learning > generate_data のグラフ内に記載されています。こちらは3関節のロボットアーム用に書かれていますのでそのままでは使えませんが、基本構成は同じです。

Bifrost Browser > Machine Learning
Bifrost Browser > Machine Learning
generate_data のグラフ内に参考スクリプトがあります
generate_data のグラフ内に参考スクリプトがあります

では学習を実行してみます。仮想環境が有効であることを確認し、ターミナルで python train_sin.py を実行します。学習が始まりエポックごとのLossが表示されていきます。

学習の実行

全て終わるとLossの推移をプロットしたグラフが表示されます。右に行くにつれてゼロに近づき、できるだけ低い値で横ばいになっていれば学習は概ね順調です。この例では0.0002程度で落ち着いています。

Training Lossの推移

Bifrostで動かす

長らくお待たせいたしました!ようやくBifrostに移ります。グラフは非常にシンプルです。

作成したモデルは1つのfloatを受け取り1つのfloatを返すものですが、float配列を自動ループで入力して、結果をstrandsを使ってグラフのようにビューポートにプロットしてしまいます。比較として本物のsin関数もプロットしてあります。

bifrostGraphShape
bifrostGraphShape
bifrostGraphShape > normalize_and_reshape
bifrostGraphShape > normalize_and_reshape

最も重要なのはモデルの推論(*5)を行うnetworkコンパウンドです。学習時に定義したNetworkクラスの構造と一致させておきます。

*5) 推論:学習済みのモデルを使って、新しい入力に対する出力の予測を行う処理のことです。

bifrostGraphShape > network
bifrostGraphShape > network

保存したパラメータ(npyファイル)の読み込みにはread_NumPyコンパウンドを使います。file_path や type を正確に指定しないとうまくロードできませんのでご注意ください。

bifrostGraphShape > read_model_params
bifrostGraphShape > read_model_params

緑の線がsin関数、青い線がネットワークの出力です。学習した範囲(-π < θ < π)においてはほぼsinと一致していることがわかります。

sinの近似(サンプルデータ:ml_sin.ma)
sinの近似(サンプルデータ:ml_sin.ma)

導入編は以上です!

次回は『実践編』として、冒頭にお見せしたリュックの連動に挑戦します。

今回(導入編)のまとめ


・データがたくさんあれば機械学習で“複雑な何か”を近似できる
・機械学習モデルの訓練はPyTorchというライブラリを使ってMaya外で行う
・BifrostグラフではPyTorchで定義したモデルと同じものを構築し推論を行う


製品購入に関するお問い合わせ
オートデスク メディア&エンターテインメント 製品のご購入に関してご連絡を希望される場合は、こちらからお問い合わせください。