【DLAI×LangChain講座④】 Question and Answer


背景

LangChainは気になってはいましたが、複雑そうとか、少し触ったときに日本語が出なかったりで、後回しにしていました。

DeepLearning.aiでLangChainの講座が公開されていたので、少し前に受講してみました。その内容をまとめています。

ちなみに:ゆっくりやっていたら、DLAI×LangChain講座の2が公開されてしまいました!早速受講はしてみたのですが、なかなかグレードアップしていて感動しました。急いでいこうと思います。(けど何かおまけはつけたい。。)

第3回はこちらです。

今回は第4回Chainsの中のRetrieval QAについてです。ここでは、CSVデータを読み込んで質問に答える仕組みを紹介しています。

Retrieval QA | 🦜️🔗 Langchain

アプローチ

DeepLearning.aiのLangChain講座の4の内容をまとめます。

サンプル

import os
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import CSVLoader
from langchain.vectorstores import DocArrayInMemorySearch
from IPython.display import display, Markdown
from langchain.indexes import VectorstoreIndexCreator

まずは、CSV形式の1000個の衣服データを読み込みます。

file = 'data/OutdoorClothingCatalog_1000.csv'
loader = CSVLoader(file_path=file)

必要に応じて、docarrayのインストールを行ってください。

#pip install docarray

VectorstoreIndexCreator

DocArrayInMemorySearchを使います。名前的にメモリ上に載せるベクトル検索エンジンですかね。

index = VectorstoreIndexCreator(
    vectorstore_cls=DocArrayInMemorySearch
).from_loaders([loader])

以下のクエリを投げてみます。UVカット機能のあるシャツをリスト化するという指示です。

query ="Please list all your shirts with sun protection \
in a table in markdown and summarize each one."
response = index.query(query)
display(Markdown(response))

UPF 50+という、98%の有害な光線を防ぐシャツをリストアップしてくれました。この処理の前に以下のコマンドを実行して確認してみると、text-davinci-003のAPIを叩いているみたいです。また、ソースコードを調べると、Embeddingには、OpenAI Embeddingモデルがデフォルトで指定されていました。

import langchain; langchain.debug = True

ChatOpenAI

次は、ChatOpenAIで実行する例です。まずは、Loaderからドキュメントを読み込みます。

loader = CSVLoader(file_path=file)
docs = loader.load()
docs[0]
Document(page_content=": 0\nname: Women's Campside Oxfords\ndescription: This ultracomfortable lace-to-toe Oxford boasts a super-soft canvas, thick cushioning, and quality construction for a broken-in feel from the first time you put them on. \n\nSize & Fit: Order regular shoe size. For half sizes not offered, order up to next whole size. \n\nSpecs: Approx. weight: 1 lb.1 oz. per pair. \n\nConstruction: Soft canvas material for a broken-in feel and look. Comfortable EVA innersole with Cleansport NXT® antimicrobial odor control. Vintage hunt, fish and camping motif on innersole. Moderate arch contour of innersole. EVA foam midsole for cushioning and support. Chain-tread-inspired molded rubber outsole with modified chain-tread pattern. Imported. \n\nQuestions? Please contact us for any inquiries.", metadata={'source': 'data/OutdoorClothingCatalog_1000.csv', 'row': 0})

一行のデータが一つのDocumentになっています。ソースファイルや行番号もメタデータに入っていますね。

次にOpenAIEmbeddingsを呼び出します。

from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()

テキストを入力すると、以下のように1536次元のベクトルが得られます。

embed = embeddings.embed_query("Hi my name is Harrison")
print(len(embed))
print(embed[:5])
1536
[-0.022206753492355347, 0.006513645406812429, -0.018233178183436394, -0.039207618683576584, -0.014360198751091957]

ドキュメントとEmbeddingモデルを指定し、DocArrayInMemorySearchを構成します。いくつも作り方があり、よくわかんなくなりますね。

db = DocArrayInMemorySearch.from_documents(
    docs, 
    embeddings
)

試しに、UVカット機能を持つシャツを検索してみます。

query = "Please suggest a shirt with sunblocking"
docs = db.similarity_search(query)
len(docs)
4

デフォルトの取得数は4になっています。db.similarity_search(query, k=2)などとすることで取得数を調整できます。トークン数制約やドキュメントの長さや回答生成に必要なドキュメントの数に応じて調整するとよさそうですね。

次に、検索したドキュメントをそのまま入れてみます。

llm = ChatOpenAI(temperature = 0.0)
qdocs = "".join([docs[i].page_content for i in range(len(docs))])
response = llm.call_as_llm(f"{qdocs} Question: Please list all your \
shirts with sun protection in a table in markdown and summarize each one.") 
| Shirt ID | Name | Description

講座を受講したときには、ちゃんと表が出力されていましたが、うまく動作しなくなっていました。以下のドキュメントを見ると、「最新モデルリリースの2週間後にそのモデルに更新される」とあるので、0613のモデルに更新されたことが原因だと思われます。

Most capable GPT-3.5 model and optimized for chat at 1/10th the cost of text-davinci-003. Will be updated with our latest model iteration 2 weeks after it is released.

OpenAI Platform#gpt-3-5)

0613の更新は、Function calling機能のためにFine-tuningされたもの(より"操縦可能性"の高いバージョン)です。

- updated and more steerable versions of `gpt-4` and `gpt-3.5-turbo`

Function calling and other API updates

一見関係なさそうですが、このような影響が出ることがあるということです。今回のように、機能的に古いモデルで十分な場合は古いモデルを使い続けられるようにできるといいですね。

おそらくここで、自前のLLMが重要になってきます。実際に賢い推論などは外部のモデルに任せて更新していき、補助的な機能には自前のLLMですばやく安定的に処理できるというのが理想な気がします。

ということでサンプルコードに戻りますが、プロンプトを少し書き換えてみます。InstructionとDataを入れ替えてみました。

response = llm.call_as_llm(f"## Instruction\n Please list all your \
shirts with sun protection in a table in markdown and summarize each one.\n## Data\n {qdocs} ") 
display(Markdown(response))

ちゃんと表が得られましたね。

RetrievalQA

次はお待ちかね、RetrievalQAを構成します。

retriever = db.as_retriever()
qa_stuff = RetrievalQA.from_chain_type(
    llm=llm, 
    chain_type="stuff", 
    retriever=retriever, 
    verbose=True
)

これだけです。クエリを投げてみます。

query = "Please list all your shirts with sun protection in a table \
in markdown and summarize each one."
response = qa_stuff.run(query)
display(Markdown(response))
Entering new  chain...

Finished chain.

| Shirt ID | Name                                 | Description

中身の処理は変わっていないので、やっぱり空の表が返ってきました。ここでは、クエリの文字列を調整するだけでは、正しい表を得られませんでした。

モデル更新とプロンプト調整の問題、難しいですね。LangChainは、高度に抽象化され、プロトを簡単に作れることが強みですが、プロンプトの微調整は色々なところで必要になります。こうなると手間は増えていくので、LangChainを使うべきかの判断は難しいですね。

おまけ

Fixing Prompt in LangChain

今回のおまけでは、LangChainがモデル更新により動作しなくなった場合に、一時しのぎでプロンプトを書き換える方法を紹介します。

そんなことしたらLangChainを使っている意味がないですし、色々注意もありますが、一時しのぎでこうできるかも、という話です。ただ、LLMの性質上、単一タスク(今回はCSVデータからのRetrieval QAによるQ&A)においても100%動作する万能なプロンプトを作るのは難しいと思うので、これをやる必要は出てくるんじゃないかと思います。

まずは、サンプルと同様に必要なモジュールをインポートします。

import os
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file

from langchain.chains import RetrievalQA
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import CSVLoader
from langchain.vectorstores import DocArrayInMemorySearch
from langchain.embeddings import OpenAIEmbeddings
from IPython.display import display, Markdown

同じくRetrievalQAを構成します。

llm = ChatOpenAI(temperature = 0.0)
file = 'data/OutdoorClothingCatalog_1000.csv'
loader = CSVLoader(file_path=file)
docs = loader.load()
embeddings = OpenAIEmbeddings()
db = DocArrayInMemorySearch.from_documents(
    docs, 
    embeddings
)
retriever = db.as_retriever()

qa_stuff = RetrievalQA.from_chain_type(
    llm=llm, 
    chain_type="stuff", 
    retriever=retriever, 
    verbose=True
)

先ほどのクエリを投げてみます。

query = "Please list all your shirts with sun protection in a table \
in markdown and summarize each one."
response = qa_stuff.run(query)
display(Markdown(response))
| Shirt ID | Name |Description

やはり、安定して空の表を出してきます。

ここで、RetrievalQAの中のプロンプトを確認します。combine_documents_chainは、複数の回答を作成・組み合わせて最終回答を得るChainで、Staff, Refine, Map reduce, Map re-rankなどの手法があります。サンプルコードではStaff(単純にすべてのドキュメントをプロンプトに含める)を使っています。

Documents | 🦜️🔗 Langchain

その中にLLMChainがあり、その中にPromptTemplateがあります。ここで、llmでChatOpenAIを指定していること、chain_typeでStaffを指定していることから、以下の形式になっています。

for m in qa_stuff.combine_documents_chain.llm_chain.prompt.messages:
    print(m)
prompt=PromptTemplate(input_variables=['context'], output_parser=None, partial_variables={}, template="Use the following pieces of context to answer the users question. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.\n----------------\n{context}", template_format='f-string', validate_template=True) additional_kwargs={}

prompt=PromptTemplate(input_variables=['question'], output_parser=None, partial_variables={}, template='{question}', template_format='f-string', validate_template=True) additional_kwargs={}

ChatOpenAIに入力するmessagesの形式で、1つ目にドキュメントと指示のSystemMessage、2つ目にクエリがUserMessageで入っています。

今回は、1つ目のTemplateを書き換えてみます。

print(qa_stuff.combine_documents_chain.llm_chain.prompt.messages[0].prompt.template)
Use the following pieces of context to answer the users question. If you don't know the answer, just say that you don't know, don't try to make up an answer.
----------------
{context}

以下のようにドキュメントと指示の位置を入れ替えます。ここで、input_variables=['context']となっているので、{context}を外すと動かなくなってしまうので注意です。

prompt_template = """\
## Context
{context}
## Instruction
Use the pieces of context to answer the users question. 
If you don't know the answer, just say that you don't know, don't try to make up an answer.
"""
qa_stuff.combine_documents_chain.llm_chain.prompt.messages[0].prompt.template = prompt_template
response = qa_stuff.run(query)
display(Markdown(response))
Entering new  chain...

Finished chain.

無事表を得ることができました。下のメモ書きが長いですが、一応意図通り動きましたね。もっといい方法があればぜひ教えてほしいです。

こういうところを見ると、やっぱりLangChainはプロトタイピング用なのかなーという感じがしますね。とはいえ、そんなこというと、全てはプロトタイプなので、どこからどうするべきかわかりませんが。。

まとめ

DeepLearning.aiのLangChain講座の4の内容をまとめました。

今回は、Chainsの中のRetrievalQAについてでした。ここでは、docarrayを用いた例を紹介しましたが、実は様々なVectorStoreが使えます。例えば、業務ではOpenSearchを使って知識検索をしています。そうすると、Function callingと組み合わせて、フィルタやキーワード検索なども利用でき、より使いやすくなります。

Vector stores | 🦜️🔗 Langchain

参考

【DLAI×LangChain講座③】 Chains|harukary
Retrieval QA | 🦜️🔗 Langchain
OpenAI Platform#gpt-3-5)
Function calling and other API updates
Documents | 🦜️🔗 Langchain
Vector stores | 🦜️🔗 Langchain

サンプルコード

https://github.com/harukary/llm_samples/blob/main/LangChain/LangChain_DLAI_1/L4-QnA.ipynb
https://github.com/harukary/llm_samples/blob/main/LangChain/LangChain_DLAI_1/plus_L4.ipynb

この記事が気に入ったらサポートをしてみませんか?