Translate

2021年9月21日火曜日

勝手日本語翻訳→「HuggingFace と Unity を使ってスマートロボットAIを構築する~あなたの命令に従うロボット」

タイムラインにちょっとおもしろそうなハンズオンのリンクが回ってきた

どうもHugging Face上のAPIとUnityで作ったロボットを連携させて、指示文書を英語で入力すると、それに合わせた動作をUnity内のロボットが動いてくれるというプログラムをつくることができるらしい。

ただ..AIはAPIまかせだし、Unity部分はそっくりコピペなので、いちから手順を学べるかというとそうではない。

ただ手順通りにすれば、すぐに動かすことはできそうな状態のようによめたので、シルバーウィークの暇な時間を潰すのにはいいかもしれない。

以下、勝手に日本語化した翻訳文だけど、参照は at your own riskでお願いします。

なお画像については原文画像へのリンクで貼り付けているので、別ウィンドウで開いてみてください

-----

HuggingFace と Unity を使ってスマートロボットAIを構築する~あなたの命令に従うロボット


今日は、プレイヤのテキスト入力に基づいてアクションを実行するこの愛らしいスマートロボットを構築します。


深層言語モデルを使ってテキスト入力を解釈し、行動リストのなかから最も適切なアクションを見つけます。

このシステムで興味深いのは、従来のゲーム開発とは対照的に、すべてのインタラクションをハードコーディングする必要がないことです。
代わりに、与えられたユーザ入力に最も適切なロボットの可能なアクションを選択する言語モデルを使用します。


このプロジェクトの構築には、以下のものを使用します:

  • Unity Game Engine (2020.3.18.f1 and +)
  • (MixとJamによって作成された)Jambo Robot アセット
  • Hugging Face


この記事は、Unity に関する基礎的なスキルをすでに持っている人を対象としています。
ロボットとの対話をためしてみたいだけの場合は、プロジェクトページに移動し、圧縮されたファイルをダウンロードして、ドキュメントに従ってください。
 


では、はじめましょう!

文章類似(Sentence Similarity)判定能力


いきなり実装に飛び込む前に、プロジェクトがどのように機能して、文章類似判定が何であるかを理解する必要があります。

プロジェクトはどのように機能するのか?


このプロジェクトでは、プレイヤにより多くの自由を与えたいと思っています。
ボタンをクリックするだけでロボットに注文を出すのではなくテキストを介してロボットと対話してもらいたいのです。
ロボットにはあらかじめ行動リストをもっていて、プレイヤの指示に最も近いアクション(存在する場合は)を選択する文章類似判定モデルを使用します。


たとえば、「ちょっと赤い箱をつかんで (hey grab me the red box)」と書いた場合、ロボットは「ちょっと赤い箱をつかんで」が何を意味しているのかについてプログラムされていません。
しかし、文章類似判定モデルモデルは、この指示と「赤い立方体を持ってくる」というアクションを結び付けます。


文章類似判定モデルに仕事をまかせるという手法のおかげで、プレーヤによる入力とロボットの行動可能なすべてのインタラクションをを手作業でマッピングするという面倒なプロセスを必要とせずに、信頼性の高いキャラクタAIを構築することができます

文章類似判定とは何か?


文章類似判定は、ソースとなる文章と文章リストが与えられた場合に、文章リストの各文がソースとなる文章とどのくらい類似しているかを計算できる言語モデルでおこないます。
たとえば、ソースとなる文章が「やあ、ここだよ! (Hey there)」の場合 「こんにちは! (Hello)」という行動をあらわす文章がとても近いと判断します。


ここでは、文章変換モデル all-mpnet-base-v2 を使用します。
このモデルを使用すると、入力テキストを指定して、実行するのに最も適切なアクションをロボットに「決定」させることができます。
モデルはすでにトレーニングされているため、すぐに使用することができます。

ステップ1:文章類推判定モデルを選択する

HuggingFace 入門


HuggingFace には、多くのすばらしい言語モデルと、それらをプロジェクトに直接プラグインするためのAPI(Accelerated Inference API)が含まれています。
ただし、最初に アカウントを作成 する必要があります。


アカウントを作成したら、Accelerated Inference APIダッシュボードに移動し、プロファイル(右上隅)とAPIトークンをクリックします。


APIトークンをコピーします。
これは、APIを使用できるようにするために必要なキーです。

セキュリティ上、このキーは共有しないでください。これは秘密キーです。

Accelerated Inference API


APIキーを取得したら、次はモデルを選択します。

ここでは、文章類推判定モデル all-mpnet-base-v2 を選択しました:


他の文章類推判定モデルを自由に試してもかまいません。

素晴らしいのは、Hosted Inference API を使用してWebサイトでモデルを直接試すことができることです。
テストをしてみましょう:


「赤いブロックをつかんできなさい (fetch the red block)」はロボットのアクション「赤い立方体を持ってくる (bring me the red cube)」に近いので、モデルは正しく機能していることがわかります。

このモデルを選択することにしたので、API URL を取得します。
それには、「Deploy」>「Accelerated Inference」をクリックして、


API_URLがコピーできるモーダルを開きます。

これで API を Unity に接続する準備が整いました。

ステップ2:Hugging Face API を Unity に接続する


次に、Hugging FaceモデルAPIをUnityに接続し、使用可能にする必要があります。

Scenes/Tutorial にある Scripts/Tutorial_ を開きます:


シーンは次のようになります。
ロボットはさまざまなオブジェクト(BlueCube、RedCube、RedPillarなど)に囲まれています。


Scripts/Tutorial にて `HuggingFaceAPI_Tutorial.cs` を開きます:


このスクリプトはPOSTリクエストを処理し、プレーヤの入力が行動リスト内の各ロボットアクション文の類似性スコアを返すようにAPIに要求し、そこから最も高い類似性スコアを持つアクションを選択できるようになります。

`HFScore()` :APIの呼び出し


APIを呼び出して結果を処理するには、コルーチン関数を使用します。
このタイプの関数は実行を待機できるため、実行を続行する前に、そのAPIが応答を返すのを待機する必要があります。

  • 最初に、つぎのようなPOSTリクエストのJSONを作成する。


`HuggingFaceAPI_Tutorial.cs`:


public IEnumerator HFScore(string prompt)
{
        // Form the JSON
        var form = new Dictionary<string, object>();
        var attributes = new Dictionary<string, object>();
        attributes["source_sentence"] = prompt;
        attributes["sentences"] = sentences;
        form["inputs"] = attributes;

        var json = Json.Serialize(form);
        byte[] bytes = System.Text.Encoding.UTF8.GetBytes(json);
}

  • 次に、Webリクエストを行い、応答する。


`HuggingFaceAPI_Tutorial.cs`:

public IEnumerator HFScore(string prompt)
{
  ...
 
  // Make the web request
        UnityWebRequest request = UnityWebRequest.Put(model_url, bytes);
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Authorization", "Bearer " + hf_api_key);
        request.method = "POST"; // Hack to send POST to server instead of PUT

        yield return request.SendWebRequest();

        // If the request return an error set the error on console.
        if (request.isNetworkError || request.isHttpError)
        {
            Debug.Log(request.error);
            Debug.Log(request.downloadHandler.data);
            yield return request.error;
        }
        else
        {
            JSONNode data = request.downloadHandler.text;
            // Process the result
            yield return ProcessResult(data);
        }
    }

  • 応答は `[0.7777,0.19,0.01]` のような文字列となる。これを操作するには、`float` 配列に変換する必要がある( `ProcessResult(data) にて処理)。

`ProcessResult()` :結果を `float` 配列に変換し、最大スコアと最大スコアインデックスを発見する


APIは文字列 `[0.77, 0.18, 0.01,…]` を返しますが、ここでは最高のスコアとそのインデックスを見つける必要があります。

したがって、結果文字列を浮動小数点数の配列 `[0.77、0.19、0.01]` に変換し、最大スコアと最大スコアインデックスを見つける必要があります。

この処理は `ProcessResult()` 関数でおこないます。

`HuggingFaceAPI_Tutorial.cs`:

/// <summary>
    /// We receive a string like "[0.7777, 0.19, 0.01]", we need to process this data to transform it to an array of floats
    /// [0.77, 0.19, 0.01] to be able to perform operations on it.
    /// </summary>
    /// <param name="result">json return from API call</param>
    private IEnumerator ProcessResult(string result)
    {
        // The data is a score for each possible sentence candidate
        // But, it looks something like this "[0.7777, 0.19, 0.01]"
        // First, we need to remove [ and ]
        string cleanedResult = result.Replace("[", "");
        cleanedResult = cleanedResult.Replace("]", "");

        // Then, we need to split each element of the array and convert to float
        string[] splitArray = cleanedResult.Split(char.Parse(","));
        float[] myFloat = splitArray.Select(float.Parse).ToArray();

        // Now that we have a array of floats, we can find the max score and the index of it.
        // We don't need to return these 2 variables, we'll access them directly since they are public.
        maxScore = myFloat.Max();
        maxScoreIndex = myFloat.ToList().IndexOf(maxScore);

        yield return null;
    }



この関数により、正しいアクションを選択するために `RobotBehavior` で呼び出す `maxScoreIndex` 変数と `maxScore` 変数を更新しました。

インスペクタへ入力する


ロボットの動作に取り組む前の最後のステップは、APIキーとモデルのURLをインスペクタに入力することです。
シーン内の `HuggingFaceAPI` オブジェクトをクリックし、インスペクタで `Model_url` と `Hf_api_key` を更新します。

APIキーを定義する場合は、プロジェクトを共有しないでください。

ステップ3:ロボットの動作を構築する


Unityプロジェクトを HuggingFace Model API に接続したので、ロボットの動作を定義する必要があります。
ロボットにはさまざまなアクションがあり、アクションの選択はAPI出力に依存するという考え方です。

まず、有限ステートマシンを定義する必要があります。
これは、各状態が特定の動作を定義する単純なAIです。

次に、状態を選択するユーティリティ関数を作成し、実行する一連のアクションを作成します。

ステートマシンを定義する


ここでのステートマシンは、各状態は動作を表します。
状態の例として、整列して「こんにちは」と言う、などになります。

エージェントの状態に基づいて、一連のアクションを実行します。
ここでは、7つの状態を定義します。


まず、すべてのアクション可能な状態を `State` という列挙型として作成します。

`JummoBehavior_Tutorial.cs` :

/// <summary>
/// Enum of the different possible states of our Robot
/// </summary>
private enum State
{
  Idle,
  Hello, // Say hello
  Happy, // Be happy
  Puzzled, // Be Puzzled
  MoveTo, // Move to a pillar
  BringObject, // Step one of bring object (move to it and grab it)
  BringObjectToPlayer // Step two of bring object (move to player and drop the object)
}


常に状態をチェックする必要があるため、それぞれのケースが状態であるスイッチシステムを使用して、ステートマシンを `Update()` メソッドに定義します。

状態のケースごとに、エージェントの動作を定義します。

たとえば、状態 `Hello` では、ロボットはプレーヤに向かって移動し、プレーヤに正しく向き合ってから、`hello` アニメーションを起動してから、アイドル状態に戻る必要があります。


すべてを定義しましょう:

`JummoBehavior_Tutorial.cs` :

private void Update()
    {
        // Here's the State Machine, where given its current state, the agent will act accordingly
        switch(state)
        {
            default:
            case State.Idle:
                Debug.Log("STATE IDLE");
                break;

            case State.Hello:
                agent.SetDestination(playerPosition.position);
                if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
                {
                    RotateTo();
                    anim.SetBool("hello", true);
                    state = State.Idle;
                }
                break;

            case State.Happy:
                agent.SetDestination(playerPosition.position);
                if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
                {
                    RotateTo();
                    anim.SetBool("happy", true);
                    state = State.Idle;
                }
                break;

            case State.Puzzled:
                Debug.Log("CASE STATE PUZZLED");
                agent.SetDestination(playerPosition.position);
                if (Vector3.Distance(transform.position, playerPosition.position) < reachedPositionDistance)
                {
                    RotateTo();
                    anim.SetBool("puzzled", true);
                    state = State.Idle;
                }
                break;

            case State.MoveTo:
                agent.SetDestination(goalObject.transform.position);
                
                if (Vector3.Distance(transform.position, goalObject.transform.position) < reachedPositionDistance)
                {
                    state = State.Idle;
                }
                break;

            case State.BringObject:
                // First move to the object
                agent.SetDestination(goalObject.transform.position);
                if (Vector3.Distance(transform.position, goalObject.transform.position) < reachedObjectPositionDistance)
                {
                    Grab(goalObject);
                    state = State.BringObjectToPlayer;
                }
                break;

            case State.BringObjectToPlayer:
                agent.SetDestination(playerPosition.transform.position);
                if (Vector3.Distance(transform.position, playerPosition.transform.position) < reachedObjectPositionDistance)
                {
                    Drop(goalObject);
                    state = State.Idle;
                }
                break;
        }
    }


これで、さまざまな状態ごとの動作を定義できました。

ここでの魔法は、どの状態がプレーヤ入力に最も近いかを定義する言語モデルであるということから生まれました。
そして、ユーティリティ関数では、この状態を呼び出します。

ユーティリティ関数を定義する


アクションリストは次のとおりです:

 

  • SentenceがAPIに供給される
  • Verbは状態をあらわしている
  • Noun(存在する場合)は、対話するオブジェクト(柱、立方体など)をさす


このユーティリティ関数は、プレーヤの入力テキストとの類似性スコアが最も高い文に関連付けられている動詞と名詞を選択します。

しかし最初に多くの奇妙な入力テキストを取り除くために、類似性スコアのしきい値を設定する必要があります(しきい値を `0.20` に設定していますが、このしきい値はアクション数に比例した値を設定する必要があります)。

たとえば、「すべてのウサギを見て (Look all the rabbits)」と言った場合、考えられるアクションはどれも関係ありません。
このため、スコアが最も高いアクションを選択するのではなく、State Puzzled という困惑したアニメーションでロボットをうごかします。


スコアが高い場合は、`state` に対応する `verb` と、(存在する場合は)`noun`( `goalObject` )を取得します。

`verb` に対応する `state` を設定します。
これにより、対応する動作が有効になります。

`JummoBehavior_Tutorial.cs` :


/// <summary>
    /// Utility function: Given the results of HuggingFace API, select the State with the highest score
    /// </summary>
    /// <param name="maxValue">Value of the option with the highest score</param>
    /// <param name="maxIndex">Index of the option with the highest score</param>
    private void Utility(float maxScore, int maxScoreIndex)
    {
        // First we check that the score is > of 0.3, otherwise we let our agent perplexed;
        // This way we can handle strange input text (for instance if we write "Go see the dog!" the agent will be puzzled).
        if (maxScore < 0.20f)
        {
            state = State.Puzzled;
        }
        else
        {
            // Get the verb and noun (if there is one)
            goalObject = GameObject.Find(actionsList[maxScoreIndex].noun);

            string verb = actionsList[maxScoreIndex].verb;

            // Set the Robot State == verb
            state = (State)System.Enum.Parse(typeof(State), verb, true);
        }
    }



これで、ロボットと対話する準備が整いました。

ステップ4:ロボットと対話する


このステップでは、エディタの再生ボタンをクリックするだけです。
そうすれば、あなたはいくつかの指示をだして結果を見ることができます。

指示の入力が終了したら、Enterキーを押すことを忘れないでください。

いくつかのバグ、特にモデルの読み込みのバグがある可能性があります。

ロボットがアイドルモードのままになっている場合は、初回モデル読込中の可能性があるため、45秒ほど待ちます。
コンソールでエラーメッセージを確認してください。

次は?


アクションを追加するにはどうすればよいですか?

例を見てみましょう:

  • `YellowPillar` ゲームオブジェクトをコピーし移動
  • 名前を `GreenPillar` に変更
  • 新しいマテリアルを作成し、それを緑に設定 (RGB:)
  • `GreenPillar` にマテリアルを配置


新しいゲームオブジェクトを配置したので、文を追加する必要があります。
`Jammo_Player` をクリックします。
アクションリストで、プラスボタンをクリックして、次の新しいアクション項目を入力します:

 

  • Move to green pillar
  • Move To
  • GreenColumn


以上になります。



今日はこれですべてになります。

指示に基づいてアクションを実行できるロボットを作成する手順を紹介しました。

これはすばらしいことです。

次にできることとしては、Scripts/Final にあるコードを深く掘り下げ、Scenes/ Finalの最終シーンを確認することなどです。

必要な部分についてはすでにコメントしているので、それは比較的簡単にできるとおもいます。

言語モデルとトランスフォーマについてもっと知りたい場合は、Hugging Face コースをチェックしてください(無料です):

既存アプリケーションをK8s上でコンテナ化して動かす場合の設計注意事項メモ

既存アプリをK8sなどのコンテナにして動かすには、どこを注意すればいいか..ちょっと調べたときの注意事項をメモにした。   1. The Twelve Factors (日本語訳からの転記) コードベース   バージョン管理されている1つのコードベースと複数のデプロイ 依存関係 ...