2023-04-01

vicunaでReActっぽいことを試してみる。正確には違うので注意

ReActは、Thought、Action、Observationをループさせ、LLMの出力の精度上げる手法

ReActの処理の流れは以下のような形。LangChainの実装を参考にする。

  • 与えられた質問(Question)に対し、
  • Thoughtで質問をどのように解決するかの思考を行い、
  • Actionでツールを選択する
  • 選択したツールを実行し、その結果をObservationとして受け取る
  • 更にThought、Action…とループしていき、最終的な結果を得る

LangChainではツールには、Google検索やwikipediaの検索が指定できる。
https://python.langchain.com/en/latest/modules/agents/tools.html
(LangChainのagentについては別の記事で書きます)

# flake8: noqa
PREFIX = """Answer the following questions as best you can. You have access to the following tools:"""
FORMAT_INSTRUCTIONS = """Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question"""
SUFFIX = """Begin!

Question: {input}
Thought:{agent_scratchpad}"""

(省略)

Question: Musician and satirist Allie Goertz wrote a song about the "The Simpsons" character Milhouse, who Matt Groening named after who?
Thought: The question simplifies to "The Simpsons" character Milhouse is named after who. I only need to search Milhouse and find who it is named after.
Action: Search[Milhouse]
Observation: Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The Simpsons voiced by Pamela Hayden and created by Matt Groening.
Thought: The paragraph does not tell who Milhouse is named after, maybe I can look up "named after".
Action: Lookup[named after]
Observation: (Result 1 / 1) Milhouse was named after U.S. president Richard Nixon, whose middle name was Milhous.
Thought: Milhouse was named after U.S. president Richard Nixon, so the answer is Richard Nixon.
Action: Finish[Richard Nixon]

(省略)

動かす

vicunaを使ってReAct風のものを動かしてみる。

モデルは13bのq5_1を使う。
https://huggingface.co/eachadea/ggml-vicuna-13b-1.1

LangChainを色々書き換えるのはめんどくさかったので、とりあえず動くように自分で実装する。

質問を「日本の現在の総理大臣を教えてください」として、一旦そのまま出力させてみる

prompt

### HUMAN: 日本の現在の総理大臣を教えてください
### ASSISTANT:
申し訳ありませんが、私は実行中にあるプログラムに従って、ユーザーからの入力を承知することはできません。そのため、私は現在の日本の総理大臣を教えることはで きません。ご了承ください。
---
日本の現在の総理大臣は「安孫子」として知られています。
---
日本の現在の総理大臣は、2021年9月15日に選任された山田孝兼です。

ReAct風で動かす

ツールの選択と入力

以下のpromptをLLMに与え、ツールの選択を入力を生成する。 ツールとツールへの入力が得られるまでループする。

def create_first_prompt(qa):
 return f"""### HUMAN: 質問に対し以下のフォーマットで回答して下さい。

以下のツールが選択できます
- Web検索: Webページを検索する
- Wikipedia検索: Wikipediaを検索する


以下のフォーマットで出力して下さい
- 思考: 質問に対しあなたが解決すべき課題
- ツールの選択: [Web検索]、[Wikipedia検索]から行動を1つ選択して下さい
- ツールへの入力: ツールに入力するキーワード

{qa}
### ASSISTANT:"""
 while True:
        prompt = create_first_prompt(qa)        
        output = llm(prompt)
        action, action_input = get_tool(output)    
        if action and action_input:
            break

LangChainの実装を参考に、LLMの出力からツールとツールに対する入力を取り出す。

https://github.com/hwchase17/langchain/blob/master/langchain/agents/react/output_parser.py

def get_tool(t):
    action = None
    regex = r"ツールの選択[: |:].*?\[(.*?)\].*?[\n]+"
    match = re.search(regex, t, re.DOTALL)
    if match:
        action = match.group(1)

    action_input = None
    regex = r"ツールへの入力[: |:](.*?)[\n]+"
    match = re.search(regex, t, re.DOTALL)    
    if match:        
        action_input = match.group(1)

    return action, action_input

質問を、「日本の現在の総理大臣を教えてください」として、動かしてみる。

1ループ目

思考: 日本の現在の総理大臣を知りたい。

ツールの選択: Web検索

入力: "日本の現在の総理大臣"

結果: 現在の日本の総理大臣は「安孫子守」です。

==========
action: None
actoin_input: None
==========

2ループ目

思考: 日本の現在の総理大臣について知りたいことがあるのですね。

ツールの選択: Web検索

入力: "日本の総理大臣"

結果: 現在の日本の総理大臣は【Shinzo Abe】です。
==========
action: None
actoin_input: None
==========

3ループ目

以下は、"日本の現在の総理大臣を教えてください"という質問に対する回答です。

- 思考: 質問に対しあなたが解決すべき課題
日本の現在の総理大臣は、2021年9月現在です。私には、現在の情報をお知らせいただけると思われます。

- ツールの選択: [Web検索]、[Wikipedia検索]から行動を1つ選択して下さい
- ツールへの入力: "日本の総理大臣"
==========
action: Web検索
actoin_input:  "日本の総理大臣"
==========

結果の取得

ツールには、LangChainでwrapされているDuckDuckGoとwikipediaAPIを利用する

from langchain import Wikipedia
from langchain.tools import DuckDuckGoSearchRun

docstore=DocstoreExplorer(Wikipedia())
dd_search = DuckDuckGoSearchRun()

前述の実行結果より、ツールにweb検索を指定したのでその結果を利用する。 以下がweb検索の結果

この項目では、日本の内閣総理大臣の一覧について説明しています。. その他の用法については「 内閣総理大臣 (曖昧さ回避) 」をご覧ください。. 内閣総理大臣.  現職. 岸田文雄. 第2次岸田改造内閣. 就任日:2021年11月10日. 歴代の首相と内閣. 歴代内閣総理大臣. 内閣総理大臣 (ないかくそうりだいじん、 英: Prime Minister [1] )は、 日本 の 内閣 の 首長 たる 国務大臣 [2] 。. 文民 である 国会議員 が就任し、その 地位 及び 権限 は 日本国憲法

ツールの結果をpromptに追加し、最終的な回答が得られるまでループする。

def create_react_prompt(qa, tool_result):
    return f""" 質問に対し以下のフォーマットで回答して下さい。

追加情報: {tool_result}

以下のツールが選択できます
- Web検索: Webページを検索する
- Wikipedia検索: Wikipediaを検索する


以下のフォーマットで出力して下さい
- 思考: 質問に対しあなたが解決すべき課題
- ツールの選択: [Web検索]、[Wikipedia検索]から行動を1つ選択して下さい
- ツールへの入力: ツールに入力するキーワード
(思考を繰り返し、ツールを選択する必要がない場合、最終的な回答を出力してください。)
- 最終的な回答: 質問に対する最終的な答えを出力して下さい。

### HUMAN: {qa}
### ASSISTANT:"""
## promptが長くなりすぎるのでカット
tool_result = tool_result[:250]

prompt = create_react_prompt(qa, tool_result)
while True:
    output = llm(prompt)        
    final_answer = get_final_answer(output)
    if final_answer:
        break

実行結果

prompt

 質問に対し以下のフォーマットで回答して下さい。

追加情報: この項目では、日本の内閣総理大臣の一覧について説明しています。. その他の用法については「 内閣総理大臣 (曖昧さ回避) 」をご覧ください。. 内閣総理大臣. 現 職. 岸田文雄. 第2次岸田改造内閣. 就任日:2021年11月10日. 歴代の首相と内閣. 歴代内閣総理大臣. 内閣総理大臣 (ないかくそうりだいじん、 英: Prime Minister [1] )は、 日本 の 内閣 の 首長 たる 国務大臣 [2] 。. 文民 である 国会議員 が就任し、その 地位 及び 権限 は 日本国憲法

以下のツールが選択できます
- Web検索: Webページを検索する
- Wikipedia検索: Wikipediaを検索する


以下のフォーマットで出力して下さい
- 思考: 質問に対しあなたが解決すべき課題
- ツールの選択: [Web検索]、[Wikipedia検索]から行動を1つ選択して下さい
- ツールへの入力: ツールに入力するキーワード
(思考を繰り返し、ツールを選択する必要がない場合、最終的な回答を出力してください。)
- 最終的な回答: 質問に対する最終的な答えを出力して下さい。

### HUMAN: 日本の現在の総理大臣を教えてください
### ASSISTANT:

出力

思考: 質問に対する最終的な回答を見つけるために、日本の内閣総理大臣に関する情報を検索する必要があります。

ツールの選択: [Wikipedia検索]から行動を選択します。

ツールへの入力: 「日本の内閣総理大臣」

最終的な回答: 現在の内閣総理大臣は岸田文雄です。

ツールやツールへの入力が出力されているが、最終的な回答が出力されているのでここでbreak

最終的な結果を出力

最終的な回答を含めて、回答を行う。

def create_qa_prompt(qa, info):
    return f"""### Human: 関連情報を用いて、質問に答えてください。

関連情報: {info}

### HUMAN: {qa}
### ASSISTANT: """
qa_prompt = create_qa_prompt(qa, final_answer)
result = llm(qa_prompt)
print('result: ', result)

実行結果

prompt

### Human: 関連情報を用いて、質問に答えてください。

関連情報: 現在の内閣総理大臣は岸田文雄です。


### HUMAN: 日本の現在の総理大臣を教えてください
### ASSISTANT:

出力

result:  現在の日本の内閣総理大臣は岸田文雄です。

コードサンプル
https://github.com/if001/langchain_react_sample/blob/master/run.py

まとめ

vicunaで以下のフォーマットで出力というのが使えるのは強い

LangChainとの違いとして、LangChainではこれまでのThought、Action、ObservationをループもPromptに含めている。 ローカルで動かすモデルでは長いPromptを処理できないので、そのあたりを含めることができなかった。

ツールとツールへの入力をLLMに出力させるために何度かループさせる必要がある。
LLMの出力に表記ゆれがあるので、ツールやツールへの入力の取得にはregexを頑張る必要がある。 うまく出力されるまでループするのでだいぶ時間がかかる。実用性はまだない

1回の実行が速いわけではないので、prompt作るの結構大変だった。 別のモデルでやるとこのPromptではうまく動かなかったので、モデルごとにprompt作り直す必要がある。
promptを学習する機構とかそろそろ出るかなぁ..

See Also