3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルLLMをUnityに実装してみた

Last updated at Posted at 2024-12-01

こんにちは!

83effect(はちさんえふぇくと、eighty-three effect)です!

私達は現在、ローカルLLMを組み込んだUnity製のゲームを開発中です。
ローカルLLMの実行にはLlamaCppPythonを使用する形での実装を試みましたが、こちらの実装が中々に大変でした。
本記事では、実装に際して得た学びを書き残しておきたいと思います。
ローカルLLMを組み込んだUnity製のゲームを制作したいと考えている方の参考になれば幸いです。

そもそもローカルLLMとは?

LLMと言えばChatGPTやGeminiが有名ですが、これらはインターネットを通じて使用するLLMになります。

それに対してローカルLLMは、ローカル環境で(つまりインターネットに接続しなくても)使用できるLLMになります。

以下はゲームへの組み込みにおいての簡単な比較です。

スクリーンショット 2024-10-19 120736.png

環境・技術紹介

環境

Windows 11

使用技術

  • Unity 2022.3.47f1
  • LlamaCppPython 0.3.2
  • pythonnet 3.0.4
  • Python 3.12.7(Embeddable)
  • Llama-3-ELYZA-JP-8B-GGUF

使用技術の解説

  • Unity
    ゲーム開発やリアルタイム3Dコンテンツの制作を行うための総合的な開発プラットフォーム

  • LlamaCppPython
    ローカルLLMをPythonで使用するためのライブラリ

  • pythonnet
    C#アプリケーション内でPythonコードを実行する為に必要なライブラリ

  • Python(Embeddable)
    今回使用するのは組み込み用のPython
    pythonnetでサポートされているバージョンを使用する

  • Llama-3-ELYZA-JP-8B-GGUF
    今回使用するローカルLLM
    量子化されており、CPUでも動かすことが可能

    • ライセンス
      月間アクティブユーザー数が7億人を超えない限り商用でも無料で利用可

実装手順

0. Unityプロジェクトの作成

各自で、Unityのプロジェクトを作成してください。
この際のテンプレートはどれでも大丈夫です。
また、既存のプロジェクトに追加実装する形式でも構いません。

以降の内容では、「2D (Built-In Render Pipeline)」で作成した空のプロジェクトを使用して、実装手順を紹介していきます。

現在は、CPU対応版のみの紹介になります。
GPU対応版は近日追記予定です。

1. Unity上にPythonの実行環境を作る

ローカルLLMを動かすために、LlamaCppPythonというPythonライブラリを使用します。
そのために、まずはUnity上でPythonの実行環境を構築していきます。

なお以降の手順については、こちらの記事を参考にしています。

1-a. pythonnetのインストール

①Project Settings

Edit > Project Settingsを開きます。

image.png

Package Managerを開き、以下の設定を追加します。

image.png

Name: Unity NuGet
URL: https://unitynuget-registry.azurewebsites.net
Scope(s): org.nuget

この時、忘れずにSaveを押しましょう。

Project Settingsでの設定項目はもう1つあります。
Playerを開き、「Configuration / Api Compatibility Level*」を「.NET Framework」に変更します。

image.png

ここでは設定の変更が自動で反映されるため、そのままProject Settingsのウィンドウを閉じます。

②Package Manager

Window > Package Managerを開きます。

image.png

左上のプルダウンを開き、「Packages」を「My Registries」に変更します。

image.png

右上の検索窓で「pythonnet」を検索します。

image.png

「Install」を押します。

image.png

インストールが完了したら、pythonnetのVersionを確認しておいてください(以下では「3.0.4」)。

image.png

タブを閉じます。

1-b. Python Embeddable Packageをダウンロード

Pythonのダウンロードページを開き、Windows embeddable packageをダウンロードします。

先程インストールしたpythonnetのドキュメントを確認し、サポートしているpythonのバージョンを確認します。

Unityにインストールされているバージョンのドキュメントを確認することに注意してください

image.png

pythonnet 3.0.4はpython 3.7 ~ 3.12をサポートしているようです。

ここではPython 3.12.7のWindows embeddable packageをダウンロードします。

「Windows installer」と間違えないように注意しましょう

image.png

zipをダウンロードできたら解凍して、Unityプロジェクトの
Assets\StreamingAssets配下にpython-3.12.7-embed-amd64フォルダを配置してください。
(StreamingAssetsフォルダがなければ作成してください)

1-c. pipの導入

python312._pth ファイルの import site のコメントアウトを削除します。

image.png

次に、コマンドプロンプトを起動し、以下を順に実行していきます。
(PowerShellで実行する場合は一部コマンドが変わるので注意しましょう)

# python-3.12.7-embed-amd64に移動(カレントディレクトリはUnityのプロジェクトフォルダ)
# ダウンロードしたバージョンによって適切に変える
cd Assets\StreamingAssets\python-3.12.7-embed-amd64

# pipの導入
curl -L https://bootstrap.pypa.io/get-pip.py | .\python

# 以下のコマンドでインストールできているか確認
.\Scripts\pip list

こちらの環境では以下が出力されました。

Package Version
------- -------
pip     24.3.1

2. LlamaCppPythonをインストール

2-a. 「.whl」ファイルのダウンロード

以下のページから、上記でダウンロードしたPythonと同じバージョンのllama-cpp-pythonを探し、ダウンロードします。

インストール時にのみ使用するファイルのため、配置場所はUnityプロジェクトの外で大丈夫です。
以下ではDownloadsフォルダに配置しています。

【LlamaCppPython0.3.2 / Python 3.12.7を使用する場合】

ファイル名は以下の形式になっています。

llama_cpp_python-0.3.2-cp312-cp312-win_amd64.whl
  • CPU対応

    Latestのタグが付いているのを選べばOKです。

「-cuXXX」のラベルがついているのはGPU版なので注意してください

image.png

2-b. LlamaCppPythonのインストール 

ダウンロードできたら、以下のコマンドでインストールします。

# カレントディレクトリは "\Assets\StreamingAssets\python-3.12.7-embed-amd64"
# Pathは適切に書き換えてください

.\Scripts\pip install <ダウンロードした.whlファイルのPath>

# (例)
# .\Scripts\pip install "C:\Users\<Username>\Downloads\llama_cpp_python-0.3.2-cp312-cp312-win_amd64.whl"
# 以下のコマンドでインストールできているか確認
.\Scripts\pip list

こちらの環境では以下が出力されました。

Package           Version
----------------- -------
diskcache         5.6.3
Jinja2            3.1.4
llama_cpp_python  0.3.2
MarkupSafe        3.0.2
numpy             2.1.3
pip               24.3.1
typing_extensions 4.12.2

インストールを確認出来たら、先程ダウンロードした .whl ファイルは削除して構いません。

3. ローカルLLMのモデルをダウンロード

今回はELYZA社の日本語大規模言語モデル「Llama-3-ELYZA-JP-8B-GGUF」を使用します。
他のモデルでも大丈夫ですが、LlamaCppPythonで動くモデルである必要があります。(基本的にGGUF形式であればOK)

ここでポイントとして、量子化されたモデルを用いることをおすすめします。
量子化についての詳細は他の記事を参照していただきたいのですが、簡単に例えるとCD音源をmp3に変換するような感じで、大体の性能を保ったまま容量を軽くできるのがメリットになっています。

Llama-3-ELYZA-JP-8B-GGUFをダウンロード

このサイトを開き、.ggufファイルをダウンロードします。

image.png

ダウンロードできたら、Assets\StreamingAssets配下に配置してください。
(以下ではAssets\StreamingAssetsの下にGGUFフォルダを作成し、その中にモデルを配置しています)

Assets\StreamingAssetsの配下でないとBuildした際にモデルのファイルがプロジェクトに含まれません!

image.png

3. PythonでローカルLLMを動かすためのコードを記述

C#とPythonのファイルを作成していきます。

C#のサンプルコード

Assets配下にgameManager.csとPythonLifeCycle.csを作成します。

gameManager.cs
using Python.Runtime;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class gameManager : MonoBehaviour
{
    private SemaphoreSlim _semaphoreSlim;

    void OnEnable()
    {
        _semaphoreSlim = new SemaphoreSlim(1, 1);
    }

    void Start()
    {
        LlamaReply();
    }

    void Update()
    {

    }

    public async void LlamaReply()
    {
        IntPtr? state = null;
        try
        {
            await _semaphoreSlim.WaitAsync(destroyCancellationToken);
            state = PythonEngine.BeginAllowThreads();
            string resultText = await Task.Run(() => LlamaPython());
            Debug.Log(resultText);
        }
        catch (OperationCanceledException e) when (e.CancellationToken != destroyCancellationToken)
        {
            Debug.Log("回答の生成に失敗しました");
            throw;
        }
        finally
        {
            if (state.HasValue)
            {
                PythonEngine.EndAllowThreads(state.Value);
            }
            _semaphoreSlim.Release();
        }
    }

    private string LlamaPython()
    {
        string modelPath = Application.streamingAssetsPath + "/GGUF/" + "Llama-3-ELYZA-JP-8B-q4_k_m.gguf"; // TODO: 適切に変える
        Debug.Log(modelPath);
        string systemContent = "語尾に「にゃん」をつけて回答してください。";
        string userContent = "富士山について教えてください。";
        using (Py.GIL())
        {
            using dynamic sample = Py.Import("sample"); // TODO: 適切に変える
            using dynamic result = sample.llamaCppPython(modelPath, systemContent, userContent);
            return result
        }
    }
}
PythonLifeCycle.cs
using Python.Runtime;
using System;
using UnityEngine;

public static class PythonLifeCycle
{
    private const string PythonFolder = "python-3.12.7-embed-amd64"; // TODO: 適切に変える
    private const string PythonDll = "python312.dll"; // TODO: 適切に変える
    private const string PythonZip = "python312.zip"; // TODO: 適切に変える
    private const string PythonProject = "Scripts"; // TODO: 適切に変える

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void PythonInitialize()
    {
        Application.quitting += PythonShutdown;
        Initialize(PythonProject);
    }
    private static void PythonShutdown()
    {
        Application.quitting -= PythonShutdown;
        Shutdown();
    }

    public static void Initialize(string appendPythonPath = "")
    {
        var pythonHome = $"{Application.streamingAssetsPath}/{PythonFolder}";
        var appendPath = string.IsNullOrWhiteSpace(appendPythonPath) ? string.Empty : $"{Application.streamingAssetsPath}/{appendPythonPath}";
        var pythonPath = string.Join(
            ";",
            $"{appendPath}",
            $"{pythonHome}/Lib/site-packages",
            $"{pythonHome}/{PythonZip}",
            $"{pythonHome}"
        );
        var scripts = $"{pythonHome}/Scripts";

        var path = Environment.GetEnvironmentVariable("PATH")?.TrimEnd(';');
        path = string.IsNullOrEmpty(path) ? $"{pythonHome};{scripts}" : $"{pythonHome};{scripts};{path};";
        Environment.SetEnvironmentVariable("PATH", path, EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable("DYLD_LIBRARY_PATH", $"{pythonHome}/Lib", EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable("PYTHONNET_PYDLL", $"{pythonHome}/{PythonDll}", EnvironmentVariableTarget.Process);
#if UNITY_EDITOR
			Environment.SetEnvironmentVariable("PYTHONDONTWRITEBYTECODE", "1", EnvironmentVariableTarget.Process);
#endif

        PythonEngine.PythonHome = pythonHome;
        PythonEngine.PythonPath = pythonPath;

        PythonEngine.Initialize();
    }
    public static void Shutdown()
    {
        PythonEngine.Shutdown();
    }
}

Pythonのサンプルコード

Assets\StreamingAssets\Scripts配下にsample.pyを作成します。

Pythonファイルを編集した場合はUnityを再起動する必要があります。
(どこかで適切に設定すれば大丈夫なのかもしれませんが…)

sample.py
import llama_cpp
from llama_cpp import Llama


def llamaCppPython(modelPath, systemContent, userContent):
    model = Llama(
        model_path=modelPath,
        chat_format="llama-3",  # TODO: 適切に変える
        n_ctx=1024,
    )

    response = model.create_chat_completion(
        messages=[
            {"role": "system", "content": systemContent},
            {"role": "user", "content": userContent},
        ],
        max_tokens=1024,
    )

    resultText = response["choices"][0]["message"]["content"]
    return resultText

ここまでやった結果のフォルダ構成

image.png

4. 【実行】Unity(C#)でPythonの関数を呼び出す

いよいよ実行です。

実行準備

Hierarchyタブの中で右クリックし、「Create Empty」を選択。
「GameObject」を作成。

image.png

image.png

Project > Assets\gameManager.csを、上記で作成した「GameObject」にドラッグ&ドロップ

image.png

「GameObject」の「Inspector」を確認し、「Game Manager」が含まれていればOK

image.png

実行

実行ボタンを押します。

すると、gameManager.cs :56行目の Debug.Log(modelPath);が呼び出され、使用モデルのPathがConsoleに出力されるはずです。

image.png

この状態でしばらく待つと、gameManager.cs :56行目の Debug.Log(modelPath);が呼び出され、返答がConsoleに出力されるはずです。

環境によってはかなり時間がかかりますが、タスクマネージャーを開いてCPUの使用率が上がっていれば返答生成中なので待ちましょう。

image.png


  • 返答

    富士山にゃん!日本で一番高い山で、標高は3,776メートルにゃん。三重県と山梨県の県境に位置していて、世界文化遺産に登録されているにゃん。富士山は、火山で、約25万年前に噴火してできた山にゃん。山体は、約20万年前に噴火した溶岩ドームが固まったものにゃん。富士山は、登山が人気で、多くの人が登頂を目指すにゃん。でも、気候が厳しく、登山道が整備されていない部分もあるので、注意が必要にゃん。

    gamaManager.cs
    string systemContent = "語尾に「にゃん」をつけて回答してください。";
    string userContent = "富士山について教えてください。";
    

    gameManager.cs :57行目で指定したsystemContentの内容に基づいて、userContentの指示に回答してくれました。
    (三重県…?)

最後に

83effectはローカルLLMを組み込んだゲームを制作中です!

体験版を近日リリースする予定なので、是非83effectのXアカウントをフォローしてお待ちいただけますと幸いです!

参考記事

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?