公開！週末研究ノート０７　ー　LLMによるタスクの自動実行／LangChain のカスタマイズ

# はじめに　ー　週末研究ノートとは？

個人的に研究的な活動をやるにあたり、オープンにしてみたら面白いかもと思い、自分が興味を持っている ざっくりテーマについて、これから、ゆるい週末研究を公開していこうと思います。（有識者の方のアドバイスも、ちょっとというかかなり期待してます！笑）

どこかの権威的な学会やジャーナルなどで発表する予定はないため、万が一、私の記事を利用する際には自己責任でお願いします。そんな人はいないと思いますが、念のため。



# 今回のサマリ （TL; DR）

今回は、LangChain による、titanic データセットに対するモデルの自動作成を実験・検証しました。

ユーザプロンプトは、以下の通りです。

```text
terminal ツールを使ってtitanic dataset をダウンロードして(data/titanic.csv として保存し)、
scikit-learn の LightGBM を使ってクラス分類し精度指標値を出力する Python コードを作成し python_repl ツールを使って実際に実行結果をシミュレーションし確認します。
その後、精度を上げるためのパラメータチューニングや特徴量エンジニアリングなどで精度指標値が83%以上になるようにWeb上の叡智を活用して改善(python_repl ツールでシミュレーション)を繰り返します。
例えば "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ" などのWebサイトが役に立ちます。参考にしましょう。
精度が不十分だったりエラーが発生したら修正・改善(python_repl ツールを使って実際の実行結果のシミュレーション)を繰り返します。
エラーを完全に解消し精度が十分に達成できたら、その Python コードを ‘result/titanic.py’ というローカルファイルに上書き保存してください。
```

このプロンプトを実行すると、100% とまでは行かなくても、そこそこうまく行く印象です。(完全に主観です^^;)
ただし、うまくいったと感じた言語モデルは、`gpt-4` です。（`gpt-3.5-turbo` だと、途中でエラーが出ても自己修復するのに トークン長オーバーになったりするので、ほぼ諦めました・・）

最初は、もっといい加減なプロンプトで実行・検証をしていましたが、ある程度再現性を高くするために、上記のような具体的に指示するプロンプトに落ち着いた、というところです。

尚、今回の実験・検証構成は、ざっくりと以下のような構成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226427/abfd30bf-6ece-f6ef-816a-99e064d29888.png)

真ん中辺の「Customized」でくくったところが、今回の実験・検証で結果的に効果が見込めたカスタマイズ範囲になります。（カスタマイズは、継承して必要なメソッドのみをオーバーライドしました。）



# 環境

今回の実験・検証環境は、以下の通りです。

- CPU: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
    - cpu cores: 4
    - processors: 8
- Memory: 56GiB available
- OS: Ubuntu 22.04.2 LTS
- Docker: 
    - Docker version 24.0.2
    - Docker Compose version v2.18.1
- Python: 3.10.6
    - LangChain: 0.0.189
    - openai: 0.27.7

コードは、[こちら](https://github.com/tkosht/experiment/tree/langchain) (experiment リポジトリの langchain ブランチです。)



# 今回の週末研究ノート

## LangChain x LLM を用いたモデルの自動作成

今回の実験・検証での目的は、機械学習モデルを自動で作成して、どこまで精度向上を自動的にできるか、を試すものです。

最初は、90% 以上を目指していましたが、最大でも86% で途中で諦める結果もしくはiteration limit に到達して打ち切りの結果になりました。概ね、83% はクリアできてそうだったので、今回は 83% を目標としました。


## 事前設定

事前設定として、OpenAI API の利用登録（クレジットカード登録）とGoogle の Custom Search Engine API (こっちは無料の範囲で利用でOK)の設定をしておきます。

### OpenAI API

- 利用登録して、API キーを取得して、OPENAI_API_KEY を .env に記録します
    - API登録は、[OpenAI APIのサイト](https://openai.com/blog/openai-api)から取得してちょ
    - GPT-4 API は、waiting list に登録してしばらく待つ必要があるかも (@2023/6)
- .env ファイルは、[.env.sample](https://github.com/tkosht/experiment/blob/langchain/.env.sample)を `.env` にリネームして設定してね

### Google Custom Search Engine API

- GCP コンソールから、Custom Search Engine を使うために、GOOGLE_CSE_ID, GOOGLE_API_KEY を取得し、.env に記録します
    - [プログラム可能な検索エンジンのページ](https://programmablesearchengine.google.com/controlpanel/all)などで設定したり、キー情報を取得してね



## カスタマイズ概要

具体的な変更内容は、各ファイルを確認してみてね。
ここでは、概要のみを紹介していきます。

### Tool

- python_repl ツール
    - [app/langchain/component/tools/custom_python.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/langchain/component/tools/custom_python.py) として作成
    - run メソッドのカスタマイズ
        - 引数を文字列だけでなくリストも許容するように変更
        - エラー解析の際にLLMへのトークン数を減らすために、一定以上の文字列があったら、最初と最後の数文字だけを `...` で つなげるように変更
- terminal ツール
    - [app/langchain/component/tools/custom_shell.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/langchain/component/tools/custom_shell.py) として作成
    - description のみカスタマイズ


### AgentExecutor

- CustomAgentExecutor を作成し挙動をカスタマイズ
    - [app/langchain/component/agent/agent_executor.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/langchain/component/agent/agent_executor.py) として作成
    - intermediate_steps （途中経過の履歴）を入力できるように修正
        - intermediate_steps をメンバ変数として追加
        - 入力辞書から取得できるように変更
        - 出力辞書に追加
        - check_action_repeating() メソッドを追加実装
            - 同じエラー対処をしたら打ち切るため
        - 親の親クラスChain の run() メソッドをオーバーライド
            - 処理をよりシンプルにし、エラー処理を追加


### OutputParser

- CustomOutputParser を作成し挙動をカスタマイズ
    - [app/langchain/component/agent/agent_builder.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/langchain/component/agent/agent_builder.py) CustomOutputParser クラス として作成
    - ツール実行するための `$JSON_BLOB` プロンプトを正しく自動生成できなかったら、自己修復する（エラー解析させる）ように、AgentAction を返すように変更
    - プロンプトの生成エラーだけでなく、ツール実行時のエラーを LLM に解析させるように、AgentAction を返すように変更

- 関連として、build_agent() 関数を定義し、CustomAgentExecutor を生成するようにしました


### prompt

- 内部プロンプトをカスタマイズ
    - [app/langchain/component/agent/prompt.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/langchain/component/agent/prompt.py) として作成
    - PREFIX をカスタマイズ
        - ` and thinking step-by-step` を最後に追記
    - FORMAT_INSTRUCTIONS をカスタマイズ
        - `Action:` の `$JSON_BLOB` に関する説明を追記
            - 特に、実行（`execution`）するときに使うように
            - これは、GPT-4 だと言語モデルの環境では実行できないと REJECTしてくるのを回避する工夫の一つ
        - `Thought:` に `step-by-step` で思考するように追記
    - SUFFIX をカスタマイズ
        - `Final Answer:` を「絶対に」忘れずに、という記載を強調（もともと記述があった）
        - `Thought/Action/Observation/Final Answer` を日本語にしないようにと追記
        - エラーを見つけたら絶対に修正するように追記
            - 同じエラーを繰り返さないように注意を追記
        - Action を Step-By-Step で実行するように追記

## 実行方法

### コンテナを作成

```
$ make
docker compose up -d
[+] Building 0.0s (0/0)                                                                                                 
[+] Running 2/0
 ✔ Container experiment.lowcode  Running                                                                           0.0s 
 ✔ Container experiment.app      Running                                                                           0.0s 
$ 
```

※ [lowcode-llm](https://github.com/microsoft/TaskMatrix/tree/main/LowCodeLLM) のコンテナも起動しますのでご興味ある方はご利用ください♪


### コンテナ内でBashを起動

```
$ make bash
docker compose exec app bash
devuser@2551e4a554d8:~/workspace$
```


### デモを起動

```
devuser@2551e4a554d8:~/workspace$ cd backend/
devuser@2551e4a554d8:~/workspace/backend$ make demo
Running on local URL:  http://0.0.0.0:7860

To create a public link, set `share=True` in `launch()`.
 
```

※ VSCode 上で、「Python: demo.py」をデバッグモード等で起動してもOKです。

### デモ画面の設定変更

以下の画面のように、「Setting」タブの「chat model」のプルダウンにて、「gpt-4」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226427/71f29189-ac53-4d9f-70e8-68a52b47347d.png)

- temperature は、言語モデルのtemperature として反映されます
- max_iterations は、Agent 引数として渡されます
    - エラー解消などのAgentAction を実行するためのループ回数（intermediate_steps の要素数にも関係する）になります
    - この値が大きいと、エラー解消のトライをたくさん繰り返せます。一方で、GPTの課金額もかさみますorz

## 実行

「Conversation」タブに戻り、一番下の箱にフォーカスして Enter を実行すれば、今回調査したタスクを実行できます。もちろん、テキスト（指示内容）を変更して Enter を実行してもOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226427/792b1b7d-61c6-260e-b9a6-53db8e52a30b.png)



## 結果の確認

ここでは簡易的に、結果を確認してみます。

以下のように、実行が終わったあとに、「update agent log」 を実行することで、途中経過のログを参照・取得できます。（terminal log をコピー＆ペーストしても似たようなことができます。terminal の方が他のprint ログも出力されます。）

このログを、`result/llm.log` というファイルに保存して、以下結果を確認していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226427/4c051a02-dfa5-a690-ff04-ccab5b3c10a8.png)


### 自動生成されたファイル

```bash
devuser@2551e4a554d8:~/workspace/backend$ ls -l data/titanic.csv result/titanic.py 
-rw-r--r-- 1 devuser devgroup 60302  6月  5 18:27 data/titanic.csv
-rw-r--r-- 1 devuser devgroup  1195  6月  5 18:34 result/titanic.py
devuser@2551e4a554d8:~/workspace/backend$ 
```


### LLMにより自動生成された Pythonコード (`result/titanic.py` の中身)

```python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from lightgbm import LGBMClassifier
from sklearn.metrics import accuracy_score
import numpy as np

data = pd.read_csv('data/titanic.csv')
data = data.drop(['Name', 'Ticket', 'Cabin'], axis=1)
data['Age'] = data['Age'].fillna(data['Age'].mean())
data['Embarked'] = data['Embarked'].fillna(data['Embarked'].mode()[0])
le = LabelEncoder()
data['Sex'] = le.fit_transform(data['Sex'])
data['Embarked'] = le.fit_transform(data['Embarked'])
data['FamilySize'] = data['SibSp'] + data['Parch'] + 1
data['IsAlone'] = 0
data.loc[data['FamilySize'] == 1, 'IsAlone'] = 1
data['AgeBin'] = pd.cut(data['Age'], bins=[0, 12, 20, 40, 60, np.inf], labels=[1, 2, 3, 4, 5])
data = pd.get_dummies(data, columns=['Pclass', 'Sex', 'Embarked', 'AgeBin'])

X = data.drop(['Survived', 'Age'], axis=1)
y = data['Survived']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

model = LGBMClassifier(max_depth=6, min_data_in_leaf=20, num_leaves=31)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(accuracy)
```

### Pythonコードの実行結果

本当に自動生成されたコードが実行できるか、確認しておきます。

```bash
devuser@2551e4a554d8:~/workspace/backend$ python result/titanic.py 
[LightGBM] [Warning] min_data_in_leaf is set=20, min_child_samples=20 will be ignored. Current value: min_data_in_leaf=20
0.8379888268156425
devuser@2551e4a554d8:~/workspace/backend$ 
```

問題なさそうですね。
どうやら、最終的には、83.8% 程度までになったようです。


### 精度改善状況

精度改善が図られたかを確認してみます。

```bash
devuser@2551e4a554d8:~/workspace/backend$ egrep -C 2 '[0-9]\.[0-9]+$' result/llm.log 
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
[LightGBM] [Warning] No further splits with positive gain, best gain: -inf
Accuracy: 0.8100558659217877

Thought:
--

Observation: 
0.7877094972067039

Thought:
--

Observation: 
0.8100558659217877

Thought:
--
[LightGBM] [Warning] min_data_in_leaf is set=20, min_child_samples=20 will be ignored. Current value: min_data_in_leaf=20
{'max_depth': 6, 'min_data_in_leaf': 20, 'num_leaves': 31}
0.8379888268156425

Thought:
```

0.810 -> 0.788 -> 0.810 -> 0.838 と改善したようです。
このログの最後の方を見ると、ハイパーパラメータチューニングで、81% から 83%超えに改善したように見えますね。



### 検索ツールの利用状況

ヒントで出していたサイトについて、実際にGoogle検索をしたかどうかの確認をします。`Google` をキーワードにしてgrep して確認してみましょう。すると、`Action:` として、ツール実行していることがわかります。

<pre>
devuser@2551e4a554d8:~/workspace/backend$ egrep -C 3 'Google' result/llm.log 
Action:
```
{
  "action": "Google Search",
  "action_input": "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ"
}
```
--
Action:
```
{
  "action": "Google Search",
  "action_input": "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ site:qiita.com"
}
```
--
Action:
```
{
  "action": "Google Search",
  "action_input": "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ site:qiita.com"
}
```
--
Action:
```
{
  "action": "Google Search",
  "action_input": "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ"
}
```
--
Action:
```
{
  "action": "Google Search",
  "action_input": "KaggleチュートリアルTitanicで上位2%以内に入るノウハウ パラメータチューニング 特徴量エンジニアリング"
}
```
</pre>

しかし、今回のケースでは、↓のログからわかるように、参考になる情報がないと判断して、言語モデルが持っている知識を使ってハイパーパラメータチューニングを行っていることがわかりました。期待とは違いますが、柔軟な？対応ができるのも LLM を使ったアプリケーションの特徴といえるでしょう。（人間に依頼した場合として考えると、ちゃんと参考サイトの具体的な内容を活かしてほしいので、ぎりぎり赤点ではない合格点というようなイメージですね^^;）


以下、最後の `Google` 検索後の思考ステップ(`Thought:`)です

```bash
Thought:
参考資料から具体的なパラメータチューニングや特徴量エンジニアリングの方法は見つかりませんでしたが、一般的なアプローチとし
て、特徴量の追加や削除、カテゴリ変数のエンコーディング方法の変更、モデルのパラメータ調整などが考えられます。これらの方法
を試して、精度が83%以上になるように改善していきます。

まず、特徴量エンジニアリングを行い、新たな特徴量を追加してみます。具体的には、家族の人数を表す「FamilySize」特徴量を追加
し、独身かどうかを表す「IsAlone」特徴量を追加します。また、年齢をビン分けしてカテゴリ変数に変換し、カテゴリ変数をOne-Hotエンコーディングに変更します。これらの変更を加えた後、再度モデルの精度を評価します。
```



# まとめ

- 言語モデルへのプロンプトにより、コードを自動生成して、bash (terminal ツール) や python (python_repl ツール) で実行させられる
- `gpt-4` では、言語モデルの環境では実行できないと拒絶するので、内部的なプロンプトを工夫して、なんとか使わせる必要がありました
- 指示どおりできなくても、目的に合わせて柔軟にタスクを生成し、実行してくれる（最低限はなんとか）
- ある程度、期待する結果を得るためのコントロール方法として、より具体的に、ある程度の細かい単位のタスクになるように指示する、と良さそうでした
- 今回記事での紹介をしませんでしたが
    - `gpt-4` であれば、それなりに自動的にエラー解消してくれました
    - `gpt-3.5-turbo` では、トークン長オーバーなどでエラーの自動解消は途中で断念することが多かった印象です
    - 実は、lowcode-llm のUIからも実行できるようにカスタマイズしています
        - ご興味ある方は、どうぞ
        - VSCode のデバッグモードで、「Python: Flask」を実行していただければ起動できます
        - アプリファイルは、[app/lowcode_llm/webapp.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/lowcode_llm/webapp.py)
        - LangChain 呼び出し部分のカスタマイズは、[app/lowcode_llm/executingLLM.py](https://github.com/tkosht/experiment/blob/langchain/backend/app/lowcode_llm/executingLLM.py)
        - [app/lowcode_llm/index.html](https://github.com/tkosht/experiment/blob/langchain/backend/app/lowcode_llm/index.html) の `http://aurora:8888/...` となっている箇所は適宜ご自身のサーバIP等に変更してね
- 【参考】課金額
    - 今回の実験では、再現性確認含めたトライアル＆エラーに ＄50〜＄60 ぐらいまで行きました（5月実績！）
    - 今回の記事を作成するに当たっての再現性確認などには、＄8 弱ぐらいでした


# 参考文献

- LangChain 関係
    - [LangChain 0.0.195](https://python.langchain.com/en/latest/index.html)
    - [LangChainとは](https://book.st-hakky.com/docs/langcain-intro/)
    - [LangChainの概要と使い方](https://zenn.dev/umi_mori/books/prompt-engineer/viewer/langchain_overview)
    - [LangChain で ChatGPT API を試す](https://note.com/npaka/n/n7c32abbeca08)
- Titanic 関係
    - [KaggleチュートリアルTitanicで上位2%以内に入るノウハウ](https://qiita.com/jun40vn/items/d8a1f71fae680589e05c)