Metaの大規模言語モデル「Llama 2」との会話をMacBook(M2)でPythonで実装してみた記録

[Supership](https://supership.jp/)の名畑です。今時の漫画アプリって「無料で1日1話読める」というのが多いように思いますが、私、なぜか家に単行本で全巻揃っている漫画もその1日1話無料で毎日1話ずつ読み直したりするんですよね。最近だと[修羅の門](https://gmaga.co/c/shuranomon.html)とか。まとめて読めるのに、わざわざ毎日コツコツと。それがなぜかは自分でも答えが出ていません。

## はじめに

[Meta](https://about.meta.com/ja/)がリリースした大規模言語モデル[Llama 2(ラマ2)](https://ai.meta.com/llama/)。
前回の記事「[Metaの大規模言語モデル「Llama 2」をMacBook(M2)にダウンロードして会話をしてみるまでの記録](https://qiita.com/nabata/items/311c1fc33a16f66d34d8)」ではこの**Llama 2**をローカルに落としてサンプルプログラムを叩いて会話をしてみました。

今回はこの**Llama 2**をPythonで呼び出してみます。
前回と同様にローカル環境です。

## 私の環境

[前回の記事](https://qiita.com/nabata/items/311c1fc33a16f66d34d8)から変わっていません。

[MacBook Pro(Apple M2 Proチップ)](https://support.apple.com/kb/SP889?locale=ja_JP)です。OSは[macOS 13 Ventura](https://www.apple.com/jp/macos/ventura/)です。

Pythonのバージョンは3.10.12です。

```console
$ python --version
Python 3.10.12
```

## モデル

[前回の記事](https://qiita.com/nabata/items/311c1fc33a16f66d34d8)で生成した**ggml-model-q4_0.bin**をそのまま使用します。
こいつを好きな場所に置いてください。

今回は **./models/7B/** に配置したと仮定して進めます。

## llama-cpp-pythonのインストール

LlamaをMacBookなどから呼び出せるようにしてくれる[llama.cpp](https://github.com/ggerganov/llama.cpp)をさらにPythonから呼び出せるようにするBindingsとして[llama-cpp-python](https://github.com/abetlen/llama-cpp-python)というものがあります。

今回はこちらを使わせていただきます。

まずは**pip**でインストールします。

```console
$ pip install llama-cpp-python
Collecting llama-cpp-python
  Downloading llama_cpp_python-0.1.73.tar.gz (1.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 4.6 MB/s eta 0:00:00

略

Successfully built llama-cpp-python
Installing collected packages: typing-extensions, numpy, diskcache, llama-cpp-python
Successfully installed diskcache-5.6.1 llama-cpp-python-0.1.73 numpy-1.25.1 typing-extensions-4.7.1
```

## Pythonで呼んでみる

まずは[公式のGetting Startedのコード](https://llama-cpp-python.readthedocs.io/en/latest/#high-level-api)を参考にして「**Where are you from now?**」という質問をしてみます。**model\_path**は各自の環境に合わせてください。

非常に短いコードです。

```Python
from llama_cpp import Llama
llm = Llama(model_path="./models/7B/ggml-model-q4_0.bin")
output = llm("Q: Where are you from now ? A: ", max_tokens=32, stop=["Q:", "\n"], echo=True)
print(output)
```

下記のような結果が出力されました。「**30 minutes away from where I was born, in the suburbs of Paris. sierpni 2016**」という回答です。
成否はともかくとしてそれっぽい回答ではあります。

```json
{
    "id": "cmpl-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "object": "text_completion",
    "created": 1690084597,
    "model": "./models/7B/ggml-model-q4_0.bin",
    "choices": [
        {
            "text": "Q: Where are you from now ? A: 30 minutes away from where I was born, in the suburbs of Paris. sierpni 2016",
            "index": 0,
            "logprobs": null,
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 12,
        "completion_tokens": 26,
        "total_tokens": 38
    }
}
```

## 会話をしてみる

せっかくなので**質問**ではなく**会話**をしてみましょう。

API Referenceにある[create\_chat\_completion](https://llama-cpp-python.readthedocs.io/en/latest/api-reference/#llama_cpp.llama.Llama.create_chat_completion)を用います。

必須な引数として**ChatCompletionMessage**のListがあります。

**ChatCompletionMessage**の型定義については私の探し方が悪いのかAPI Reference上に見つからなかったので[llama_types.py](https://github.com/abetlen/llama-cpp-python/blob/main/llama_cpp/llama_types.py)のコードを見ました。

```Python
class ChatCompletionMessage(TypedDict):
    role: Literal["assistant", "user", "system"]
    content: str
    user: NotRequired[str]
```

roleとして**assistant、user、system**のいずれかを選び、メッセージを**content**として指定すればいいようです。

会話をするために実際に書いたコードは下記です。会話の履歴を
**messages**に保持して**create\_chat\_completion**に渡しているだけです。

```Python
from llama_cpp import Llama, ChatCompletionMessage

llm = Llama(model_path="./models/7B/ggml-model-q4_0.bin")
messages = list()  # 会話の履歴を格納する配列

while True:
    your_message = input("Your turn: ")
    if your_message == "quit":  # quitと入力されたら終了
        exit()

    chat_completion_message: ChatCompletionMessage = dict(role="user", content=your_message)
    messages.append(chat_completion_message)
    output = llm.create_chat_completion(messages=messages)

    res = output["choices"][0]["message"]
    print("Llama 2's response: " + res["content"])

    if res["role"] == "assistant":  # レスポンス確認
        chat_completion_message = dict(role="assistant", content=res["content"])
        messages.append(chat_completion_message)
    else:
        exit()
```

呼び出して会話をしてみた結果は下記です。
読みやすさのためにシステム系のログは削除しています。
**Your turn**の後ろが私の書いた内容で、**Llama 2's response**の後ろが**Llama 2**によるレスポンスです。

```console
Your turn: Please introduce yourself.
Llama's response: I am an assistant.
Your turn: I give a name. Your name is Tanaka Ichiro.
Llama's response: I am Tanaka Ichiro.
Your turn: Please introduce yourself with your name.
Llama's response: My name is Tanaka Ichiro.
```

与えた名前が次の返答で使われているため、会話が成り立っていそうです。

レスポンスはllama.cppを直接呼んだ時と比べると遅く感じました。それでも数秒以内ではありますが。

## 日本語で会話をしてみた

最後に、日本語での会話もしてみようと思います。

今回はmessagesに初期値を与えてみましょう。
前回の記事を踏まえた内容です。**role**を**system**として設定を記載しています。

```Python
messages = list()
messages.append(dict(role="system", content="Transcript of a dialog, where the User interacts with an Assistant named Bob. Bob is helpful, kind, honest, good at Japanese writing, and never fails to answer the User's requests immediately and with precision."))
messages.append(dict(role="user", content="こんにちは、ボブ"))
messages.append(dict(role="assistant", content="こんにちは。今日はいかがいたしましたか？"))
```

実際に会話をすると下記でした。

```console
Your turn: 日本の首都はどこですか？
Llama's response: 東京です。
Your turn: 日本で最北端の都道府県はどこですか？
Llama's response: 北海道です。
Your turn: あなたの名前を教えてください
Llama's response: ボブです。
```

いい感じですね。

## 最後に

少し触ってみただけですが、かなり使いやすかったです。

他のメソッドも触ってみようかと思います。

触っているだけで楽しくなりますね。

## 宣伝

[SupershipのQiita Organization](https://qiita.com/organizations/supership-inc)を合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方は[Supership株式会社 採用サイト](https://supership.jp/recruit/)よりご確認ください。