gradio ChatIterfaceとllama-cpp-pythonで、Llama2との対話型チャットボットを作成する

# 

# はじめに
[gradio](https://www.gradio.app/)の[ChatIterface](https://www.gradio.app/docs/chatinterface)と[llama-cpp-python](https://github.com/abetlen/llama-cpp-python)を使って、[Llama2](https://github.com/facebookresearch/llama)と対話できるチャットボットを作成します。

# 環境
- Thinkpad X1 carbon(CPU:Core i7 8th Gen, Mem:16GB)
- Windows 10 home
- Visula Studio 2017
- cmake 3.27
- Python 3.8.10
- llama-cpp-python 0.1.77
- gradio 3.39.0

# 環境構築
## gradioインストール
今回は ChatInterfaceを利用します。[changelog](https://www.gradio.app/changelog)を見ると、ChatInterface利用可能はバージョンは、3.37以上のようなので、gradioを最新にしておきます。結果として3.39が入っています。
```Powershell
pip install -U gradio 
```
## llama2の導入
WindowsのCPUで、Pythonを使って動かすので、llama-cpp-pythonを入れます。[github](https://github.com/abetlen/llama-cpp-python)のWindows remarksに従って、環境変数を設定し、pipで入れます。もともとcmakeやコンパイラが入っていたので、すんなりインストールできましたが、ない場合は、cmakeやコンパイラをインストールする必要があると思います。
```Powershell
$env:CMAKE_ARGS = "-DLLAMA_OPENBLAS=on"
$env:FORCE_CMAKE = 1
pip install llama-cpp-python
```

## llama2パラメータダウンロード
huggingfaceで[ggml版](https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML)をダウンロードします。数年前に購入したノートPCで動かすため、Llama2で最も小さい[Llama-2-7B](https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML)を利用します。
# `ChatInterface`の基本的な構成
`ChatInterce`は、チャットとその履歴を引数にした関数で実行する形式となっています。以下に、チャットを入力すると、yes、noをランダムに返すだけのシンプルな例を表示しています。`ChatInterface`を利用することで、数行でチャットインターフェースが作れます。
`predict`関数にLLMで回答するようにすれば、チャットボットが完成します。
```Python
import random
import gradio as gr
def predict(message, history):
    return random.choice(["Yes", "No"])
gr.ChatInterface(random_response).launch()
```
`predict`関数には、デフォルトでは、`message`と`history`の2つの引数をとります。第１引数の`message`には、ユーザーが入力したチャットの文字列が入ります。`hisotry`には、過去のユーザーと、システムのチャットの履歴が、リスト型で渡され、`[['{uesr_message}', '{system_message}'], ...]`となっています。`message`と`hisotry`を使って、履歴を所定のテンプレートに従って、プロンプトを作成し、Llama2に与えると、チャット履歴を考慮した回答を返してくれます。今回は、チャットボットを作成する方法として、2種類試します。[create_chat_completionメソッド](https://abetlen.github.io/llama-cpp-python/#llama_cpp.llama.Llama.create_chat_completion)を利用する方法と、[huggingfaceのデモで利用されているテンプレート](https://huggingface.co/spaces/huggingface-projects/llama-2-7b-chat) (便宜上、デモテンプレートと呼ぶことにします）を利用する方法です。それぞれのテンプレートによる回答の違いも確認してみます。

# create_chat_completionメソッドを利用する方法
llama-cpp-pythonには、openai同様に、[`create_chat_completion`](https://abetlen.github.io/llama-cpp-python/#llama_cpp.llama.Llama.create_chat_completion)という、チャットに対応したメソッドが用意されています。`role`と`content`のキーを持つ辞書型データをリストにしたチャット履歴データを`create_chat_completion`メソッドに与えると、
```
### Human:{user_msg_1}
### Assistant:{system_answser}
### Human:{user_msg_2}
### Assistant:
```
と変換され、そのプロンプトを[`create_completion`](https://abetlen.github.io/llama-cpp-python/#llama_cpp.llama.Llama.create_completion)メソッドに入力し、回答が出力されます。[`create_completion`](https://abetlen.github.io/llama-cpp-python/#llama_cpp.llama.Llama.create_completion)メソッドが、Llama2による回答結果を返すメソッドになります。
`create_chat_completion`メソッドを使って、`predict`関数を作成します。徐々に結果を表示されるようにしたいので、`create_chat_completion`の引数に`stream=True`を与え、`yield`で回答を返してもらいます。コード内の`msg['choice][0]['delta']`の`content`に回答が入っているのですが、回答初めの返り値は、`content`でなく、`role`が出力されるので、キーに`content`が含まれるものだけをチャットに出力します。
```Python
def predict(message, history):
    messages = []
    for human_content, system_content in history:
        message_human = {
            "role":"user",
            "content":human_content+"\n",
        }
        message_system = {
            "role":"system",
            "content":system_content+"\n",
        }
        messages.append(message_human)
        messages.append(message_system)
    message_human = {
        "role":"user",
        "content": message+"\n",
    }
    messages.append(message_human)
    # Llama2回答
    streamer = llama.create_chat_completion(messages, stream=True)

    partial_message = ""
    for msg in streamer:
        message = msg['choices'][0]['delta']
        if 'content' in message:
            partial_message += message['content']
            yield partial_message
```

<details><summary>全コード</summary><div>

```Python
import gradio as gr
from llama_cpp import Llama

model_path = "llama-2-7b-chat.ggmlv3.q4_K_M.bin"
llama = Llama(model_path)

def predict(message, history):
    messages = []
    for human_content, system_content in history:
        message_human = {
            "role":"user",
            "content": human_content+"\n",
        }
        message_system = {
            "role":"system",
            "content": system_content+"\n",
        }
        messages.append(message_human)
        messages.append(message_system)
    message_human = {
        "role":"user",
        "content":message+"\n",
    }
    messages.append(message_human)
    # Llama2回答
    streamer = llama.create_chat_completion(messages, stream=True)

    partial_message = ""
    for msg in streamer:
        message = msg['choices'][0]['delta']
        if 'content' in message:
            partial_message += message['content']
            yield partial_message

gr.ChatInterface(predict).queue().launch()
```

</div></details>


# create_chat_completionメソッドを利用しない方法(huggingfaceのデモテンプレートを利用する方法）

[huggingfaceのblog](https://huggingface.co/blog/llama2)を見ると、次の入力テンプレート紹介されています。
```


 

{{system_prompt}}




{{user_msg_1}} 
 {{model_answer_1}} 


 {{ user_msg_2}} 

…
```
huggingfaceにある[llama2のチャットデモ](https://huggingface.co/spaces/huggingface-projects/llama-2-7b-chat)でも、`

...`のテンプレートが利用されています。このデモテンプレートに従い、チャット履歴をもとにプロンプトを作成します。huggingfaceのデモの記述のまま、プロンプトを作成する`get_prompt`関数を作ります（コピペです）。`get_prompt`関数の引数`system_prompt`には、`DEFAULT_SYSTEM_PROMPT`のテキストデータ（おまじない）を入れます。

```Python
DEFAULT_SYSTEM_PROMPT="""You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information."""

def get_prompt(message: str, chat_history: list[tuple[str, str]],
               system_prompt: str) -> str:
    texts = [f'

 
\n{system_prompt}\n
\n\n']
    # The first user input is _not_ stripped
    do_strip = False
    for user_input, response in chat_history:
        user_input = user_input.strip() if do_strip else user_input
        do_strip = True
        texts.append(f'{user_input} 
 {response.strip()} 


 ')
    message = message.strip() if do_strip else message
    texts.append(f'{message} 
')
    return ''.join(texts)

```

これを使用して、`predict`関数を作成します。デモテンプレートで作成したプロンプトは、[`create_completion`](https://abetlen.github.io/llama-cpp-python/#llama_cpp.llama.Llama.create_completion)に入力し、回答を予測します。これも徐々に表示させるため、`stream=True`にします。回答結果は、dict形式で、`choices`の`text`にあります。
```Python
def predict(message, history):
    
    prompt = get_prompt(message, history, DEFAULT_SYSTEM_PROMPT)
    # Llama2回答
    streamer = llama.create_completion(prompt, stream=True)

    partial_message = ""
    for msg in streamer:
        message = msg['choices'][0]
        if 'text' in message:
            new_token = message['text']
            if new_token != "<":
                partial_message += new_token
                yield partial_message
```

<details><summary>全コード</summary><div>

```python
import gradio as gr
from llama_cpp import Llama
from typing import List,Tuple
DEFAULT_SYSTEM_PROMPT = """\
You are a helpful, respectful and honest assistant. Always answer as helpfully as possible, while being safe. Your answers should not include any harmful, unethical, racist, sexist, toxic, dangerous, or illegal content. Please ensure that your responses are socially unbiased and positive in nature.
If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.\
"""
model_path = "llama-2-7b-chat.ggmlv3.q4_K_M.bin"
llama = Llama(model_path)

def get_prompt(message: str, chat_history: List[Tuple[str, str]],
               system_prompt: str) -> str:
    texts = [f'

 
\n{system_prompt}\n
\n\n']
    # The first user input is _not_ stripped
    do_strip = False
    for user_input, response in chat_history:
        user_input = user_input.strip() if do_strip else user_input
        do_strip = True
        texts.append(f'{user_input} 
 {response.strip()} 


 ')
    message = message.strip() if do_strip else message
    texts.append(f'{message} 
')
    return ''.join(texts)

def predict(message, history):
    
    prompt = get_prompt(message, history, DEFAULT_SYSTEM_PROMPT)
    streamer = llama.create_completion(prompt, stream=True)
    partial_message = ""
    for msg in streamer:
        message = msg['choices'][0]
        if 'text' in message:
            new_token = message['text']
            if new_token != "<":
                partial_message += new_token
                yield partial_message
        
gr.ChatInterface(predict).queue().launch()
```
</div></details>


# 回答結果の比較
2種類の質問をしてみます。一つ目は、Pythonが何かを聞くもので、2つ目は、Pythonのサンプルコードを書いてもらうものです。1つ目の質問では、チャット履歴を考慮しているかを確認します。2つ目は、テンプレートの違いを見ます。
## 質問１
Pythonが何かを聞いた後に、指示語を使って何人の人が使っているかを質問します。

### create_chat_completionメソッド
![question_1_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/d3086daa-8fd4-dc40-d8b6-64f3fbc32a09.png)
![question_1_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/f53b23a3-4adb-26a8-49a9-695eba7e916e.png)


### デモテンプレート
![question_2_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/a2c1e868-ac01-1e35-3529-1c7c1badab71.png)
![question_2_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/01ebdd23-8d8d-f5a1-b6d0-d49843d08670.png)

それぞれの2つ目の質問で`Python`と書いていませんが、どちらも**the language**を**Python**と認識して回答しています。多少違いがありますが、どちらも問題なく回答しているように見えます。

## 質問２
Pythonでサンプルプログラムを書いてもらいます。
### create_chat_completionメソッド
![code_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/b3f4e265-4841-a196-7655-75cdf1928a6f.png)

### デモテンプレート
![code_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/585573/6f3c3023-d55c-0e8a-95f0-4c05f86635af.png)

こちらには違いがでて、デモテンプレートを使用したものは、ユーザーの意図の確認が表示され、すぐにコード例を表示してもらえませんでした。

[Llama2の論文](https://arxiv.org/pdf/2307.09288.pdf)では、回答のsaftyの議論にかなり割かれており、Llama2は、`DEFAULT_SYSTEM_PROMPT`の記述内容のもと、saftyやhelpfulに考慮されて、よく教育されているのでしょうか。`DEFAULT_SYSTEM_PROMPT`なしで、回答してもらうと、すぐにコードを書いてくれました（回数を試していないので、ただのブレかもしれませんが）。簡単なチャット例では、`create_chat_completion`を使っても、デモテンプレートを使っても、問題なく回答が返ってきたと感じています。

# 参考
- https://github.com/abetlen/llama-cpp-python
- https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGML
- https://huggingface.co/blog/llama2
- https://huggingface.co/spaces/huggingface-projects/llama-2-7b-chat
- https://abetlen.github.io/llama-cpp-python