Generative Agents の LLM を Llama2 で実装してみた

# 0. はじめに
生成系AIのブームと[GenerativeAgentsの論文](https://arxiv.org/abs/2304.03442)に感化され、Llama2を用いた実装に挑戦しました。もし間違っている部分があれば、教えていただけると嬉しいです。

作ったコード↓

https://github.com/Kurac-pluse/KurageGA

:::note warn
実装優先で書いたため可読性に欠ける可能性があります。
また最新バージョンのパッケージをインストールしてしまいますと動かない可能性があります。記載の通りバージョン指定を行ってください。
:::

# 1. Generative Agentsとは
「Generative Agents」とは、Stanford UniversityとGoogleのチームによって発表された生成エージェントです。
「大規模言語モデル」と「言語モデルの出力条件となる関連情報を合成・検索するメカニズム」を組み合わせたエージェントアーキテクチャにより、日々の生活やエージェント同士の関係の構築といった動作を創発的に行う事に成功しています。

https://github.com/joonspk-research/generative_agents

# 2. 意義
## 2.1. 課題
前述のアーキテクチャについて、下記の課題点が挙げられます。
- アーキテクチャ内の大規模言語モデルにGPT3.5を用いているため、APIにリスエストを送る毎にコストが発生
    - 具体的には25人で2日分のシミュレーションを回すと数千ドルのトークンクレジットがかかる
- ファイルサイズがとても大きく(647.3MB)、Pythonの他にもJSON, HTML, CSSの知識も必要で編集が困難
- 7B, 13B, 70B（B=Billion）と環境に合わせてパラメータ数を変更できるLlama2と比べると、GPT3.5のAPIは使い勝手が劣る

## 2.2. 手法
そこで今回筆者は　Llama2を用いたGenerative Agentsの自作に取り組みました。
- Point 1
    - LLMにオープンソースのLlama2を使用することでコストを抑える
- Point 2
    - EmbeddingにはE5を用いることで性能アップを図る
- Point 3
    - Lang Chainによりエージェントの動きに一貫性をもたせる

# 3. 環境
- Ubuntu 22.04
- Python 3.9
- Anaconda

# 4. 実装
## 4.1. アーキテクチャの構成
筆者が作成したアーキテクチャの説明を行います。
- エージェントの行動を10分ごとに出力し、合計10日分の出力を得る
- 行動は./personality下のファイルに書かれた人物像データを元に決定する
<details><summary>ファイルの構造</summary>

.
├── LLM
│   └── llm.py,　prompt_template.txt
├── README.md
├── character
│   └── agents.py,　curr_log_template.txt
├── main.py
├── map
│   └── vill.py
├── models
│   └── llama-2-7b-chat.Q5_K_M.gguf
├── personality
│   └── agent1.txt,　agent2.txt
├── personas
│   ├── a_perceive
│   │   └── perceive.py,　perceive_template.txt
│   ├── b_retrieve
│   │   └── retrieve.py
│   └── c_plan
│ 　　 └── dailyplan.py,　dailyplan_template.txt
└── time_management
　　└── daytime.py
</details>

## 4.2. Llama2へのリクエスト
4.2, 4.3ではLlama2とLang Chainに対する説明をサンプルコードを使って行います。
### 4.2.1. パッケージの準備
[Anaconda](https://www.anaconda.com/download)等による仮想環境で作業することをお勧めします。
```.terminal
pip install llama-cpp-python==0.2.20
mkdir models
```
- 下記のサイトからLlama2のモデルをダウンロードし、./models下に保存
- おすすめは「llama-2-7b-chat.Q5_K_M.gguf」
    - [Llama 7B](https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/tree/main)
    - [Llama 13B](https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF/tree/main)
    - [Llama 70B](https://huggingface.co/TheBloke/Llama-2-70B-chat-GGUF/tree/main)

:::note warn
上記のサイト外のモデルを使用する場合、モデルの拡張子によっては、「[llama.cpp](https://github.com/ggerganov/llama.cpp#be-prepared-to-re-convert-and--or-re-quantize-your-gguf-models-while-this-notice-is-up)」からコンバーターをダウンロードして.ggufファイルに変換する必要があります。
:::

### 4.2.2. コードの作成
- 単純な質問に対する答えをLlama2で生成する
```./llm_ex1.py
from llama_cpp import Llama

# Llama2の準備
llm = Llama(model_path="./models/llama-2-7b-chat.Q5_K_M.gguf", verbose=False)
# promptに質問を記入
prompt = """### Instruction: What is the capital of the United States?
### Response:"""

# 推論の実行
output = llm(
    prompt,
    temperature=0.1,
    stop=["System:", "User:", "Assistant:", "Instruction:", "Input:", "Response:", "\n"],
    echo=False,
)
# 出力
print(output["choices"][0]["text"])
```
<details><summary>ファイルの構成</summary>

.
├── llm_ex1.py
└── models
　　└── llama-2-7b-chat.Q5_K_M.gguf
</details>

### 4.2.3. 出力
- ターミナルにて`python llm_ex1.py`で結果を表示
```.terminal
The capital of the United States is Washington, D.C. (Washington DC)
```

## 4.3. Lang Chainを試す
[Lang Chain](https://www.langchain.com/)は大規模言語モデルの機能を拡張できるライブラリです。
外部テキストの読み込みにより、長文プロンプトの受け付けが可能になります。
これにより言語モデルの出力を「過去のやり取りを踏まえた回答」にすることができます。

### 4.3.1. ファイルの準備 & パッケージの準備
- 外部テキストを用いた推論の実行を試す
- ファイルの準備
- [Project Gutenberg](https://www.gutenberg.org/ebooks/11)[^1]から『不思議の国のアリス』の原文（Plain Text UTF-8）をコピーしてテキストファイルalice.txtを作成し、modelsフォルダ下に保存
[^1]: 海外版の青空文庫だと思ってもらえれば大丈夫です。
```./models/alice.txt
The Project Gutenberg eBook of Alice's Adventures in Wonderland
~~省略~~
```
- パッケージの準備
```./llm_ex2.py
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores.faiss import FAISS
from langchain.chains import RetrievalQA
from langchain.llms import LlamaCpp
```
<details><summary>ファイルの構成</summary>

.
├── llm_ex2.py
└── models
　　└── llama-2-7b-chat.Q5_K_M.gguf,　alice.txt
</details>

### 4.3.2. 外部テキストを踏まえた推論の実行
- 4.3.1の`llm_ex2.py`に下記を追加し、Lang Chainを用いた実装にする
- 外部テキストはEmbeddingしてLang Chainに渡せる形（ベクトル表現）にする
- Llama2のモデルを用意し、推論の実行
```./llm_ex2.py
# 外部テキストの読み込み
with open("./models/alice.txt") as f:
    test_all = f.read()

# 分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=20,
)
texts = text_splitter.split_text(test_all)

# Embedding
index = FAISS.from_texts(
    texts=texts,
    embedding=HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large"),
)
index.save_local("storage")

# Llama2のモデルを用意
llm = LlamaCpp(
    model_path="./models/llama-2-7b-chat.Q5_K_M.gguf",
    max_tokens=300,
    stop=["System:", "User:", "Assistant:", "\n"],
    temperature=0.1,
    verbose=False,
    n_ctx=2048,
)

# 推論の実行
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=index.as_retriever(search_kwargs={"k": 4}),
    verbose=False,
)

print(qa_chain.run("What kind of person is Alice?"))
```
### 4.3.3. 出力
- 先ほど同様、ターミナルにて`python llm_ex2.py`を実行
```.terminal
 Alice is a curious and adventurous young girl who loves to explore new places and try new things. She is always eager to learn and discover new things, and she is not afraid to ask questions or seek help when she needs it.
```
[日本語訳]：アリスは好奇心旺盛で冒険好きな少女で、新しい場所を探検し、新しいことに挑戦するのが大好きだ。常に新しいことを学び、発見することに貪欲で、必要なときには遠慮なく質問し、助けを求める。
- 外部テキストの情報（163,916文字）をよく把握していることが分かる

## 4.4. PROMPTの合成
- 4.4では『不思議の国のアリス』の原文をLang Chainの外部テキストに用いたが、Generative Agentsではエージェントの特徴を外部テキストに用いる
- 具体的には、「名前」「性格」「1日の予定」「周囲のオブジェクト」「現在時刻」「本日の行動ログ」を外部テキストに乗せる
- 下記のコードは筆者の[GitHub](https://github.com/Kurac-pluse/KurageGA)の抜粋

```LLM/llm.py
def generate_prompt(agent, around, datetime):
    # 必要な情報を管理するリストdataの作成
    data = []
    data.append(agent.name)
    data.append(agent.personality)
    data.append(agent.today)
    data.append(around)
    data.append(datetime.print_en_time())
    data.append(agent.return_daily_log())

    # promptのテンプレートを用意し、!<INPUT {count}>!を上記のdataの中身と取り換えていく
    with open("./LLM/prompt_template.txt", "r") as f:
        prompt = f.read()
    for count, i in enumerate(data):
        prompt = prompt.replace(f"!<INPUT {count}>!", str(i))
    return prompt
```

## 4.5. エージェントの記憶の整理
- エージェントの一日分の行動ログを要約することで、記憶の整理を行う
- 整理された記憶は明日の1日の予定を立てる際に使用される
- 下記のコードは筆者の[GitHub](https://github.com/Kurac-pluse/KurageGA)の抜粋
```./personas/c_plan/dailyplan.py
from LLM.llm import *

class Dailyplan:
    def __init__(self, wake_up, sleep, breakfast, lunch, dinner):
        self.dplan = []
        self.dplan.append(wake_up)
        self.dplan.append(sleep)
        self.dplan.append(breakfast)
        self.dplan.append(lunch)
        self.dplan.append(dinner)
        self.dplan.append("common human behavior")

    def change_plan(self, agent):
        # 丸1日分の行動ログを外部テキストに用いる
        prompt = agent.return_daily_log()
        # 質問:「昨日の行動記録をもとに、今日あなたが力を入れていること、取り組むべきことを簡単に教えてください。」
        q = "Please briefly tell me what you are focusing on today and what you need to work on based on yesterday's action log."
        # 推論の実行
        llm_response = Llama_request(prompt, q)
        # 出力
        print(llm_response)
        # 結果をリストdplanの末尾に追加
        self.dplan.pop(5)
        self.dplan.append(llm_response)
        agent.today = self.dailyplan_to_string()

    # 明日の1日の予定をまとめる
    def dailyplan_to_string(self):
        # テンプレートを用意し、!<INPUT {count}>!を上記のdplanの中身と取り換えていく
        with open("./personas/c_plan/dailyplan_template.txt", "r") as f:
            my_todays_plan = f.read()
        for count, i in enumerate(self.dplan): 
            my_todays_plan = my_todays_plan.replace(f"!<INPUT {count}>!", i)
        return my_todays_plan
```

# 5. 苦労した点
- GGML形式からGGUF形式へのコンバート
前述のように、Llama2を動かすためにはGGUF形式のファイルが必要です。現在でこそ簡単に手に入るのですが、筆者がコードを書いた時期がGGMLのサポートを終了したタイミングとちょうど重なっていました。そのため、参考にした[記事(1)](https://note.com/npaka/n/n0ad63134fbe2)と同じコードでは欲しい出力が得られないことに困惑しました。エラー文を読み込み、llama-cppパッケージのREADMEとドキュメントを読み込んでようやく拡張子の問題だと気付くとこができ、手順に従ってコンバートすることが出来ました。
　
- 実行時ターミナルに流れるログの消去
参考にしていた[コード](https://github.com/joonspk-research/generative_agents)がOpenAIで実装していたので、自分なりにLlama2に変えながら実装していました。Llama2のデフォルトでは、実行時ログがターミナルに流れます。llama-cppの引数への理解が低かったので、verboseをFalseにすればログが消えると理解するのに時間がかかりました。
　
- import文への理解の浅さ
Pythonを用いたアプリケーションを作るのが初めてだったため、複雑な階層構造におけるimportの使い方に苦戦しました。特に、呼び出し先のファイルの階層が呼び出し元の階層より上にある場合のパスの書き方が分からなく、TRY & ERRの連続でした。この経験を通じて、今ではimport周りに対する理解が強固なものになりました。
　
- 想定とずれた出力を直せない
Lang Chainの外部テキストに行動ログを渡しているのにも関わらず、終了したはずの行動を繰り返し取る現象が見られました。どうやらLlama2はテキスト内の複数個所の参照は苦手としているようです。手を尽くしましたがあまり改善は見られなかったので、今後の課題としたいです。

# 6. 最後に
ぜひ一度Generative Agentsを試してみてください！
![00291-2882789264.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3793561/6e871204-cd7b-e137-cd02-70786019d7de.png) [^2]
[^2]: ここに貼られていても意味もないQRコードですが、筆者が頑張って生成したので見てください。
Stable Diffusionを用いましたが、権利には気を使っているので安心して下さい。

# 7. 参考
以下のサイトを参考にさせていただきました。
(1)

https://note.com/npaka/n/n0ad63134fbe2

(2)

https://note.com/npaka/n/n3164e8b24539

(3)

https://arxiv.org/abs/2304.03442