Langchain, GPT-3.5-turbo, Google Search API を使って、ChatGPT相当のエンジンにGoogle 検索を読み込ませ、日本語で回答を得ることに成功しました。これは一般公開される、いわゆるコンシューマー向けのサービスとしては便利ですが、逆に ChatGPT や Bing と同様に、一般的になってしまいます(検索するドメインを絞ることはできますが)。
業務でチャットボットを使う場合にはどちらかというと、自社の知見に特化したチャットボットが欲しいと思うのではないでしょうか。顧客向けにしろ、自社向けにしろ、どこの誰が書いたか分からない一般的な答えを出しても、顧客価値につながりませんからね。
ということで、LangchainとGPT-3.5-turbo API, それから OpenAPI のEmbedding API を使って、ドキュメントを埋め込んだQ&A Botの実験をしてみました。
参考:https://langchain.readthedocs.io/en/latest/modules/indexes/getting_started.html
参考:https://langchain.readthedocs.io/en/latest/modules/indexes/vectorstore_examples/faiss.html
検証結果:https://ict-worker.com/ai/jsmeca-consultant-vs-chatgpt.html
FAISS と OpenAI API Embedding
FAISS
インストールしていない場合は、インストールを行います。
py -m pip install faiss-cpu
環境によって、モジュール名はfaissと-cpuなしで動作する場合もあるみたいです(バージョンによる?)。また、faiss-gpuをLangchainが使ってくれるかは未検証です。
ここで使われる近傍探索 (Neighborhood Search) は、最適化問題を解く手法の一つです。最適化問題とは、与えられた条件下で目的関数を最小化または最大化する問題のことです。最適化問題には、線形計画法や非線形計画法などの手法がありますが、近傍探索はそれらの中でも特に局所解を求めるための手法です。
近傍探索では、ある解の周囲にある解を順次探索していきます。この探索範囲を「近傍」と呼びます。探索を行う際には、近傍内の解の中から最適解を選び出します。この最適解が、探索開始点の近傍内であれば、より良い解を得ることができます。
近傍探索の代表的な手法には、山登り法 (Hill Climbing) や焼きなまし法 (Simulated Annealing) 、遺伝的アルゴリズム (Genetic Algorithm) などがあります。これらの手法は、それぞれ探索の仕方や近傍の定義が異なりますが、近傍探索の基本的な考え方は共通しています。
近傍探索は、最適化問題に対する解の探索範囲が限られている場合や、解の個数が膨大な場合に有効です。しかし、解の探索範囲が広い場合や、解の数が非常に多い場合には、最適解を見つけるために時間がかかる場合があります。また、局所解に陥ることがあるため、グローバルな最適解を求めることができない場合もあります。
このFAISS と OpenAI の Embedding API を使って、GPT-3.5の膨大な学習の結果を利用しつつ、局所的な解を求めようとか、そういう感じです。
Embedding用のライブラリ群を読み込む
# Embedding用
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
# Vector 格納 / FAISS
from langchain.vectorstores import FAISS
# テキストファイルを読み込む
from langchain.document_loaders import TextLoader
Embedding に使う、API用のラッパーの他、テキストを読み込むためのLoader, そして、一度に大量のテキストを読み込ませられないので、CharacterTextSplitterで分割します。また、Embedding APIで Vector 化されたデータを格納・探索するためのFAISSラッパーを読み込みます。
実際にChatGPTと組み合わせてみる
import, API キーの設定
# Q&A用Chain
from langchain.chains.question_answering import load_qa_chain
# ChatOpenAI GPT 3.5
from langchain.chat_models import ChatOpenAI
from langchain import LLMChain
from langchain.prompts.chat import (
# メッセージテンプレート
ChatPromptTemplate,
# System メッセージテンプレート
SystemMessagePromptTemplate,
# assistant メッセージテンプレート
AIMessagePromptTemplate,
# user メッセージテンプレート
HumanMessagePromptTemplate,
)
from langchain.schema import (
# それぞれ GPT-3.5-turbo API の assistant, user, system role に対応
AIMessage,
HumanMessage,
SystemMessage
)
# env に読み込ませるAPIキーの類
import key
# 環境変数にAPIキーを設定
import os
os.environ["OPENAI_API_KEY"] = key.OPEN_API_KEY
embedしたvectorを元に、Q&Aを実行したいので load_qa_chain と LLMChainを読み込みます。
Embedの実行
loader = TextLoader('reiwa4-1.txt')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(docs, embeddings)
今回は、データの用意が簡単かつ扱いがシンプルなテキストファイルをembedします。ただ、ドキュメントをみた感じ、TextLoaderで読み込むファイルの文字コードを明示的に設定する方法が見当たらなかったため、実際にはその他の形式の方が便利かもしれません。
読み込んだテキストは、 text_splitter でchunk単位に分割します。
最終的に、OpenAIのEmbedding API を用いてvectorを計算してFAISSのデータベースとします。
Embeddingを利用したQ&Aの実行
query = "取引先からの要求は?"
embedding_vector = embeddings.embed_query(query)
docs_and_scores = db.similarity_search_by_vector(embedding_vector)
# load_qa_chainを準備
chain = load_qa_chain(ChatOpenAI(temperature=0), chain_type="stuff")
# 質問応答の実行
print(chain({"input_documents": docs_and_scores, "question": query}, return_only_outputs=True))
EmbeddingをQ&Aに活用する場合、問い合わせに使用するqueryについてもvector計算し、データベースから計算します。今回の例ではFAISSを使っていますが、Chromaなどの別のDBでもLangchainから実行する限りは同様の手順になります。
最終的に、GPT-3.5-turboを用いた解にするためにload_qa_chainを作成し、実行します。
実行結果:https://ict-worker.com/ai/jsmeca-consultant-vs-chatgpt.html
FAISS DBの保存と読み出し
FAISS DBの保存
Langchainの公式ドキュメントでは、サンプルにChromaを用いていますが、これはオンメモリのDBらしく、実行ごとに作成する必要があります。安定的に稼動するサービスであればそれでもいいのかもしれませんが、実験で起動する度に計算していてはもっと大きなドキュメントを埋め込むと計算に時間がかかってしまい、不便です。更に、GPT-3.5/4よりはずっと安いとはいえ、Embedでも OpenAIのAPI費用がかかります。
そのため、今回はFAISS DBを採用し、計算したvector DBをローカルに保存することにします。
loader = TextLoader('reiwa4-1.txt')
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(docs, embeddings)
db.save_local("faiss_index")
保存は、FAISS でデータベースを作成し、save_local メソッドで行います。引数は、保存先のパスとなります。この場合、カレントディレクトリにfaiss_indexというデータベースファイルが作成されることになります。
このまま検索・回答の生成ももちろん可能ですが、作成専用のスクリプトとして終了させてもいいでしょう。
FAISS DBの読み出しと検索、回答の生成
embeddings = OpenAIEmbeddings()
db = FAISS.load_local("faiss_index", embeddings)
query = "取引先からの要求は?"
embedding_vector = embeddings.embed_query(query)
docs_and_scores = db.similarity_search_by_vector(embedding_vector)
# load_qa_chainを準備
chain = load_qa_chain(ChatOpenAI(temperature=0), chain_type="stuff")
# 質問応答の実行
print(chain({"input_documents": docs_and_scores, "question": query}, return_only_outputs=True))
DBの読み込みにはload_localメソッドを利用します。ここでは、DBのパスの他に、Embeddingに使用する(使用した)オブジェクトも引数に指定する必要があります。
以降は、作成した場合と同様に扱えます。Langchainのお陰で非常にシンプルに扱えますね。
終わりに
Embeddingを使うと参考資料をもとに GPT-3.5-turbo に回答を生成させることが可能になります。
この応用範囲は広く、社内向けのマニュアルを vector 計算させておき、マニュアルを検索させる代わりに簡潔に回答を出させることや、自社サービス・WebサイトなどのFAQ, マニュアルを学習させて顧客向けに展開することもできるでしょう。
特に今は、公式LINEなどのアプリケーション、Web上のチャットなどチャット様式のインターフェースを備えたGUIの実装が多くあります。これらのアプリと接続することで、安価かつ迅速に高性能なチャットボットが実現できます(不特定多数の顧客向けに提供する場合は、プロンプトインジェクションなどの攻撃に備えたり、不適切な回答をさせないような仕組みが必要になりますが…)。
また、個人で利用する場合、個人的な読書記録や思いつき、勉強した内容をデータベース化しておくというのも面白そうです。うろ覚えの知識やアイディアを後から思い出し活用しやすくなりそうです。
6件のフィードバック
まさにこういうことをやりたいと思っていました。開発について相談させていただけないでしょうか。
こんにちは。コメントありがとうございます。
お役に立てるかは分かりませんが、分かる事でしたら記事にしたりできますので書いていただけますと幸いです!
いくらか調べてみたのですがやり方がわかりません。
ストリーミング出力をするようにするためにはどうすれば良いか、良ければ記事にしていただけると幸いです。
https://ict-worker.com/ai/langchain-stream.html
こちらに基本的な内容を記事にしました。よければ参考にしてください。
ご回答いただきありがとうございます。またコメント二重で送信されていましたね大変申し訳ございません
ご多忙の中大変恐縮ですが、
ストリーミング出力をするにはどうすれば良いのでしょうか。