見出し画像

Gradioの`ChatInterface`こと始め

気分転換に、Xのポストで見かけたGradioのChatInterfaceに取り組んでみました。解説ページは以下。

わからないことがいっぱいあるので、ページをファイルに保存して、Claude 3 opusに読み込ませていろいろ質問しながら進めてみました。

はい、Gradioの新しい機能であるChatInterfaceについて理解を深めたいと思います。ChatInterfaceを使うことで、少ないコード量でチャットボットのUIを簡単に作成できることがわかりました。

主なポイントは以下の通りです:

1. チャット関数を定義する際は、messageとhistoryの2つの引数をこの順序で取る必要がある。messageはユーザー入力の文字列、historyはそれまでの会話の履歴をリストのリストで表したもの。

2. チャット関数が文字列を返すと通常のレスポンス、yieldを使って順次レスポンスを返すとストリーミング形式のチャットボットになる。

3. ChatInterfaceにはタイトルや説明の追加、テーマやCSSでの見た目のカスタマイズ、サンプル例の表示、各種ボタンの設定など、Interfaceクラスと共通の引数が多数ある。

4. additional_inputsパラメータで、システムプロンプトやトークン数の設定など、追加の入力コンポーネントをチャットボットUIに組み込める。

5. HostされたGradioアプリは、/chatエンドポイントから簡単なAPIで呼び出せる。PythonまたはJavaScriptのGradioクライアントを使うのが良い。

6. LangChain+OpenAI、OpenAI直接、Hugging Faceのオープンソースモデルなど、ChatInterfaceは様々な言語モデルと連携できる。

7. さらにカスタマイズしたチャットボットを作る場合は、低レベルのBlocksAPIの使用が推奨される。

以上のように、ChatInterfaceはGradioでチャットボットのデモを手軽に作れる強力な機能だと理解しました。色々な言語モデルと組み合わせて使えるのも魅力的ですね。カスタマイズ性も高いので、ユースケースに合わせて柔軟に作り込めそうです。

サンプルのpredict関数の引数の説明です。

Gradioの`ChatInterface`では、`predict`関数の引数として`message`と`history`が予め定義されています。

`ChatInterface`のドキュメントには以下のように記載されています:

```python
def predict(message, history):
# process message and history
# return the assistant's response
return response

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

ここで、

- `message`:ユーザーが入力した最新のメッセージを表す文字列です。
- `history`:これまでのユーザーとアシスタントのやり取りを表すリストです。各要素は`[user_message, assistant_response]`の形式のリストです。

`ChatInterface`は、ユーザーが新しいメッセージを送信するたびに`predict`関数を呼び出します。その際、`message`引数には現在のユーザーメッセージが渡され、`history`引数にはそれまでの会話履歴が渡されます。

`predict`関数内では、これらの引数を使用して必要な処理を行い、アシスタントの応答を生成して返します。`ChatInterface`は、返された応答を画面に表示し、会話履歴を更新します。

したがって、Gradioの`ChatInterface`を使用する際には、`predict`関数の引数として`message`と`history`を使用することが想定されています。これらの引数名はGradioによって予め定義されており、適切な形式でデータが渡されます。

さらに詳しい説明はこちら


messageについて:
ユーザーが入力フィールドに入力した内容が、文字列としてmessage引数に渡されます。チャット関数内でこのmessageを使ってボットの応答を生成します。

historyについて:
ChatInterfaceは自動的に会話の履歴を管理してくれます。チャット関数が呼び出されるたびに、それまでの会話履歴がhistory引数としてリストのリストで渡されます。

historyの構造は以下のようになっています:

[
[user_input_1, bot_response_1],
[user_input_2, bot_response_2],
...
[user_input_n, bot_response_n]
]

各要素は[ユーザー入力, ボットの応答]のペアを表すリストです。リストは時系列順に並んでいます。

例えば、以下のようなhistoryがチャット関数に渡されるとします:

[
["こんにちは", "こんにちは!何かお手伝いできることはありますか?"],
["今日の天気を教えて", "今日の東京の天気は晴れのち曇りです。最高気温は25度の予想です。"]
]

この場合、過去に2つのやりとりがあったことを表しています。ユーザーが「こんにちは」と入力し、ボットが「こんにちは!何かお手伝いできることはありますか?」と応答。次にユーザーが「今日の天気を教えて」と入力し、ボットが「今日の東京の天気は晴れのち曇りです。最高気温は25度の予想です。」と応答したことがわかります。

チャット関数内でhistoryを参照することで、過去の会話の文脈を考慮した応答を生成できます。ChatInterfaceが自動でhistoryを管理してくれるので、開発者は会話履歴の保持について意識する必要がほとんどありません。

historyにはそのまま履歴が残っているので、LLMに入力するためには以下のことを考えないといけない。

その質問は大変重要なポイントを突いていますね。おっしゃる通り、多くの言語モデルではチャット形式に適したプロンプトを渡す必要があります。しかし、historyにはユーザーの生の入力とボットの応答をそのまま保持しておくべきです。

Gradioのチュートリアルで紹介されているLangChainとOpenAIの例を見ると、いずれもhistoryを適切に処理しています。

LangChainの例:

def predict(message, history):
history_langchain_format = []
for human, ai in history:
history_langchain_format.append(HumanMessage(content=human))
history_langchain_format.append(AIMessage(content=ai))
history_langchain_format.append(HumanMessage(content=message))
gpt_response = llm(history_langchain_format)
return gpt_response.content

OpenAIの例:

def predict(message, history):
history_openai_format = []
for human, assistant in history:
history_openai_format.append({"role": "user", "content": human })
history_openai_format.append({"role": "assistant", "content":assistant})
history_openai_format.append({"role": "user", "content": message})

response = client.chat.completions.create(model='gpt-3.5-turbo',
messages= history_openai_format,
temperature=1.0,
stream=True)
...

どちらの例でも、history引数はそのまま使わずに、言語モデルに合わせたフォーマットに変換してから使用しています。LangChainの場合はHumanMessageとAIMessageに、OpenAIの場合は役割とコンテンツを持つ辞書に変換しています。

変換後のフォーマットはllm関数やclient.chat.completions.createメソッドに渡されてレスポンスの生成に使われますが、history自体は変更されません。チャット関数からは変換前のhistoryがそのまま返されるので、ユーザー入力とボット応答の履歴が正しく保持されます。

このように、言語モデルに渡すプロンプトへの変換処理とChatInterfaceが管理するhistoryは分離して扱うのが一般的です。これにより、ユーザーとのインタラクションの記録を正確に残しつつ、言語モデルを適切に利用できます。

もっと理解を深めてみる。


`history`は以下のような構造のリストです:

```python
[
[user_input_1, assistant_response_1],
[user_input_2, assistant_response_2],
...
[user_input_n, assistant_response_n]
]
```

ここで、各サブリスト(例:`[user_input_1, assistant_response_1]`)の第1要素はユーザーの入力を表し、第2要素はアシスタントの応答を表しています。

コードブロック内のループでは、各サブリストの第1要素に`'user'`というラベルを付け、第2要素に`'assistant'`というラベルを付けています。そして、これらのラベル付きの要素を`prompt`リストに追加(append)しています。

```python
for human, assistant in history:
prompt.append({'role': 'user', 'content': human})
prompt.append({'role': 'assistant', 'content': assistant})
```

このループは、`history`内の各サブリストに対して以下の処理を行っています:

1. `human`変数に第1要素(ユーザーの入力)を割り当て、`assistant`変数に第2要素(アシスタントの応答)を割り当てます。

2. `human`の内容を`'user'`というラベルと共に辞書(`{'role': 'user', 'content': human}`)として`prompt`リストに追加します。

3. `assistant`の内容を`'assistant'`というラベルと共に辞書(`{'role': 'assistant', 'content': assistant}`)として`prompt`リストに追加します。

結果として、`prompt`リストは以下のような構造になります:

```python
[
{'role': 'user', 'content': user_input_1},
{'role': 'assistant', 'content': assistant_response_1},
{'role': 'user', 'content': user_input_2},
{'role': 'assistant', 'content': assistant_response_2},
...
{'role': 'user', 'content': user_input_n},
{'role': 'assistant', 'content': assistant_response_n}
]
```

このように、`history`内の各サブリストの第1要素と第2要素にそれぞれ`'user'`と`'assistant'`のラベルを付け、その内容を辞書として`prompt`リストに追加しています。

ということで、以下のコードの部分がようやく理解できました。

    history_openai_format = []
    for human, assistant in history:
        history_openai_format.append({"role": "user", "content": human })
        history_openai_format.append({"role": "assistant", "content":assistant})
    history_openai_format.append({"role": "user", "content": message})

さて、Macbookを使っているので、MLXアーキテクチャでのLLMを使ってみます。シンプルに、mlx-lmモジュールで提供されている generateを使ってみることにします。help(generate)で出た説明は以下です。

generate(model: mlx.nn.layers.base.Module, tokenizer: transformers.tokenization_utils.PreTrainedTokenizer, prompt: str, temp: float = 0.0, max_tokens: int = 100, verbose: bool = False, formatter: Optional[Callable] = None, repetition_penalty: Optional[float] = None, repetition_context_size: Optional[int] = None, top_p: float = 1.0) -> str
Generate text from the model.

Args:
model (nn.Module): The language model.
tokenizer (PreTrainedTokenizer): The tokenizer.
prompt (str): The string prompt.
temp (float): The temperature for sampling (default 0).
max_tokens (int): The maximum number of tokens (default 100).
verbose (bool): If ``True``, print tokens and timing information
(default ``False``).
formatter (Optional[Callable]): A function which takes a token and a
probability and displays it.
repetition_penalty (float, optional): The penalty factor for repeating tokens.
repetition_context_size (int, optional): The number of tokens to consider for repetition penalty.

あと、今回は以下のモデルを使ってみます。

例に従って、tokens=1024, temperature=0.5, で設定することにします。

いろいろ試行錯誤しましたが、シンプルに以下のスクリプトでGradioが動きました。デバッグ用のterminal出力のprint文は残してあります。

import gradio as gr
from mlx_lm import load, generate

model, tokenizer = load("stabilityai/stablelm-2-zephyr-1_6b")

chat_template = tokenizer.chat_template or (
    "{% for message in messages %}"
    "{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}"
    "{% endfor %}"
    "{% if add_generation_prompt %}"
    "{{ '<|im_start|>assistant\n' }}"
    "{% endif %}"
)


def predict(message, history):
    prompt = []

    for human, assistant in history:
        prompt.append({'role': 'user', 'content': human})
        prompt.append({'role': 'assistant', 'content': assistant})
        
    prompt.append({'role': 'user', 'content': message})
    print (f"履歴の形はこんな感じ:\n {history}")
    print (f"promptの形はこんな感じ:\n {prompt}")
    inputs = tokenizer.apply_chat_template(prompt,
                                           tokenize=False,
                                           add_generation_prompt=True,
                                           chat_template=chat_template)
    print (f"入力される最終プロンプトはこんな感じ:\n{inputs}")
    response = generate(model, tokenizer, prompt=inputs, temp = 0.5, max_tokens = 1024, verbose=False) 
    return response.strip()

gr.ChatInterface(predict).launch()

上記のchat_templateとtokenizer.apply_chat_template部分はいつものように、下記を参考にさせてもらっています。

実行結果画面はこちらです。
(mlxとmlx-lm, gradioを導入した上で走らせてください。)

この時点でのterminalの出力は以下です。

Fetching 9 files: 100%|████████████████████████| 9/9 [00:00<00:00, 78972.25it/s]
Running on local URL: http://127.0.0.1:7860

To create a public link, set `share=True` in `launch()`.
履歴の形はこんな感じ:
[]
promptの形はこんな感じ:
[{'role': 'user', 'content': 'Hello'}]
入力される最終プロンプトはこんな感じ:
<|user|>
Hello<|endoftext|>
<|assistant|>

履歴の形はこんな感じ:
[['Hello', "Hello! How can I help you today? If you have any questions or need information, feel free to ask. I'm here to help you in any way I can."]]
promptの形はこんな感じ:
[{'role': 'user', 'content': 'Hello'}, {'role': 'assistant', 'content': "Hello! How can I help you today? If you have any questions or need information, feel free to ask. I'm here to help you in any way I can."}, {'role': 'user', 'content': 'Tell me who you are in detail.'}]
入力される最終プロンプトはこんな感じ:
<|user|>
Hello<|endoftext|>
<|assistant|>
Hello! How can I help you today? If you have any questions or need information, feel free to ask. I'm here to help you in any way I can.<|endoftext|>
<|user|>
Tell me who you are in detail.<|endoftext|>
<|assistant|>

Streaming出力表示とかもできそうですが、今回はここまでで満足しました。

勝手に対話履歴がhistoryに保存されていくのは便利ですね。


#AI #AIとやってみた #やってみた #Gradio #大規模言語モデル #Huggingface #MLX #MacbookPro

この記事が参加している募集

やってみた

AIとやってみた

この記事を最後までご覧いただき、ありがとうございます!もしも私の活動を応援していただけるなら、大変嬉しく思います。