NTTドコモR&Dの技術ブログです。

Unreal EngineとWebSocketを組み合わせて、オープン日本語LLM(Rinna)を使ってみる

本記事はUnreal Engineからオープン日本語LLMを使ってみるハンズオンの解説記事です。

導入

大規模言語モデルの発展が目覚ましい昨今、その利用方法を考えたときにNPC(Non Player Character)というのは1つの適応先かと考えています。 少し調べただけでも以下のように興味深い事例がたくさん出てきます。

これは、弊社のコミュニケーションサービスMetaMeでも同様でして、2023年12月現在ではウサギ型のNPCが徘徊していて、AIとの対話を体験することができます。

「MetaMeって何?」と興味を持っていただいた方は、ぜひLPや下記Noteなどを訪れていただけると嬉しいです。

note.metame.ne.jp

そこで、今回はそのような仕組みを簡単に作れないかと思い至りました。
仮想空間構築にUnreal Engineを利用し、日本語対応LLMとしてRinnaを活用して、LLMとの対話が可能なNPCの作成を行っていきたいと思います。

本題

想定しているのは、添付画像のような構成です。

構成イメージ

基本的に、

① 対話部分
② WebSocket部分
- 対話側のServer
- UE側のClient

③ NPC制御部分

に分けて説明していきます。 ただし、③については分量の関係やUEの公式が手厚いので、その公式の紹介に留めさせていただければと思います。

③以外を実装すると、下記のような動作が可能です。
(動画はGIFにする関係上1.5倍速にしているのと、CPU実行だとLLMが30秒くらい返答にかかっていたので、中略と書かせていただいています)

1. 対話部分について

モデルはRinnaを使用します。
デフォルトで日本語対応はありがたいですね。

プレス

モデル

また、Rinnaの基本的な扱い方は、こちらのコードを参考にさせていただきました。

2. WebSocket部分

対話側のServer

さらに、UEとの通信を実現するために、Rinnaの実行ロジックをWebSocketのサーバーと組み合わせます。 私は、下記を参考にしました。

実際のソースコードがこちら。
Promptは色々考えましたが、一旦空欄です。

## Rinnaを動かす部分
import time
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from websocket_server import WebsocketServer

tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", use_fast=False)

# 標準
#model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft")
# 自動
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", device_map='auto')
# 自動(VRAM16GB以下でも8GBはNG)
# model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", torch_dtype=torch.float16, device_map='auto')
# CPU指定
# model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft").to("cpu")
# GPU指定
# model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft").to("cuda")
# GPU指定(VRAM16GB以下でも8GBはNG)
# model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-sft", torch_dtype=torch.float16).to("cuda")

first_prompt = ""

prompt = first_prompt

def rinna_start(message):
    prompt = f"ユーザー: {message}<NL>システム: "
    print("Len:" + str(len(prompt)))
    # 時間計測開始
    start = time.time()
    
    token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
    
    with torch.no_grad():
        output_ids = model.generate(
            token_ids.to(model.device),
            do_sample=True,
            max_new_tokens=128,
            temperature=0.7,
            pad_token_id=tokenizer.pad_token_id,
            bos_token_id=tokenizer.bos_token_id,
            eos_token_id=tokenizer.eos_token_id
            )
    output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1):])
    output = output.replace("<NL>", "\n")
    
    # 時間表示
    end = time.time()
    print(end-start)
    print(output)
    prompt = prompt+output+"<NL>"
    return output

## Rinnaを動かす部分ここまで

## ここから先は、WebSocketのサーバー部分

# Called for every client connecting (after handshake)
def new_client(client, server):
    print("New client connected and was given id %d" % client['id'])
    server.send_message_to_all("Hey all, a new client has joined us")

# Called for every client disconnecting
def client_left(client, server):
    print("Client(%d) disconnected" % client['id'])

# Called when a client sends a message
def message_received(client, server, message):
    if len(message) > 200:
        message = message[:200]+'..'
    print("Client(%d) said: %s" % (client['id'], message))
    rinna_message = rinna_start(message)
    server.send_message(client,rinna_message)

PORT=9001
server = WebsocketServer(port = PORT)
server.set_fn_new_client(new_client)
server.set_fn_client_left(client_left)
server.set_fn_message_received(message_received)
server.run_forever()

UE側のClient

次は、UEのWebSocketクライアント部分です。

C++実装などもありましたが、今回はできるだけ簡単に試したいのでPluginを使っていきます。

  • 使うPlugin
  • 対応バージョン
    • Simple Blueprint Websocket plugin for Unreal Engine 4
    • Unreal Engine 5 support
      5.2対応はまだみたいなので、リビルドが必要となります。

Pluginのリビルド

参考にさせていただいたのは、下記の記事です。

具体的な手順はこちら

  1. ThirdPersonを作成
    • ここでC++プロジェクトにしておく
  2. Contents配下にPluginsというフォルダを作成
  3. BluePrintWebSocketsをダウンロード&解凍してPluginsに配置
    フォルダ構成例
  4. 作成したThirdPersonの.uprojectをダブルクリック
  5. Pluginのリビルドが走るので「はい」を選択

UEの実装

ここからは、UE内での実装を進めていきます。

  • UI作成
  • WebSocketの通信部分作成 を行っていきます。
UI作成

ここは、本当に最小限で良いので白い箱を2つ作るだけです。 1つをテキスト送信用として、もう1つを受信用として配置します。

WidgetBluePrintを作成し、下記の画像のようにパーツを配置します。

  • 右上には、Textブロック(好みで、背景にimageを配置)
  • 真ん中下方には、TextBoxを配置(こちらが入力欄になります)

画面内のテキストを制御する関数はそれぞれ下記のノードを作成します。

  • 受信部分(右上のTextブロック)
    Textブロックの詳細パネル→コンテンツにあるTextの右側のプルダウンから「バインディングを作成」を選択し、

下記ノードを作成。

Textブロックのノード

  • 送信部分(真ん中下方のTextBox)
    TextBoxの詳細パネル→コンテンツにあるHint Textは好みで変更してください。

同じく詳細パネルの下方にあるイベントのOnTextCommitedのプラスボタンを押下 (これは、テキストを入力してEnterを押した時などに発火)

下記ノードを作成。 Enterを押した時だけを選択するためにCommitMethodから特定のイベントだけを取り出して1の時だけ実行できるように実装。(これは、CommitMethodがEnumであるため。詳細は、リファレンスを参照)

TextBoxのノード

イベントディスパッチャーはここで作成するのをお忘れなく!

WebSocketの通信部分作成

この処理は、BP_ThirdPersonCharacterに書いていきます。

基本的にPluginのリファレンス通りに、下記のようなノードを作成します。 また、UIのセットもここで行います。

次に、UIの表示のきっかけについて記述します。 今回は、NPC(BP_EnemyCharacter)に接触した時を想定して、Hitイベントの後に書いています。

実行と同時に、UIを表示させてデバッグしたい場合は、上記のDoOnce以下をBeginPlayに接続することで、実現できます。

EndPlayに、切断の処理を書きます。
ハンズオンでもお伝えしましたが「閉じるまでがWebSocketです!」

3. NPC制御部分

最後に、NPCの制御の部分です。

NPCには下記画像のように、脳になる判断する部分と感覚器になる部分が実装できます。

今回は、前述の通り詳細の実装については公式のチュートリアルが手厚いので、その紹介に留めます。

ですが、こちら私が実装した時にUE5.2ではうまくいかず改変した部分がありました。 そちらを紹介します。

BTT_FindRandomPatrolというものを作成し、BlackBoardに書き込んだりする部分があるのですが、下記画像の赤い枠の部分を記載しないといけないようでした。

こちら、私も手探りで実装したので、間違っていたりするかもしれないので、ご指摘いただけると嬉しいです。

まとめ

実装部分は、以上となります。

これらを組み合わせると 索敵するNPC→Playerが発見される→接触する→LLMと接続できるUIが起動 となり、裏で構えているRinnaが動作するWebSocketServerに接続できるはずです。

これを走り切れた方々は、かなり時間をかけてお付き合いいただけたと思っています。 読んでいただきありがとうございました。 引き続き、MetaMeのNPCにご注目いただけると嬉しいです!