【ChatGPT】とベクトルデータベース(Elasticsearch)による企業内データの活用(いわゆるRAG構成)
LLMが学習していない企業内のデータや最新のデータも有効活用すべき という点は非常に大きな論点
LLMは学習したデータに入っていなかった情報に対する推論はできません。
昨日発売されたばかりの新製品の市場での反応や
インターネットに公開されていない社内ドキュメントに関する情報を答えることは
LLMプロバイダが提供するデフォルトのLLMではできないのです。
LLM以外でも深層学習の世界では伝家の宝刀のように広く知れ渡っており非常に一般的なプラクティス
よってLLMでもこれをすればよいということになります
しかしOpenAI社に限らず、現在世界中のLLMプロバイダが提供しているモデルはインターネットから得られた大量のテキストデータを学習した数千億のパラメータを持つ超巨大推論モデル。
いくらファインチューニングが定石手法と言っても
自分が作った独自データで、果たして、この超巨大モデルが持つ推論の方向性を適切に変えることができるのか?
この問いに対する答えとしては、「結局、機械学習はやってみないとわからない」となり、データを作り学習させるという膨大な時間を要する作業なしに、事前に何か確認する手法というものは残念ながらありません。
そしてファインチューニングが効いているかどうかを確認する作業は
例えるなら、砂場に砂金を投げ込み、再度その砂金を回収しようと試みるようなもの
他の深層学習ではいざ知らず、この“ちょっと変わった”LLMと呼んでいる推論モデルにおいて、この作業が常に効果的かどうかは疑問が残ります。
ベクトルストアを併用しLLMの知識を補完
ファインチューニングでは
LLMが持っていない知識を、LLMに追加で学習させ、LLM自体を賢くするというアプローチでした。
ベクトルストア併用の構成では
LLMが知らない情報をベクトルストアにため込んでおいて
LLM単体ではなく、ベクトルストアとの合わせ技で最終的な回答を作れるようにします。
これはRAG(Retrieval-Augmented Generation)と呼ばれ LLMが持っていない知識をLLM外部の情報ソースで補うことを目的としたものです。
https://scrapbox.io/files/65615c901dfddf001b4738e5.png
「追加の知識」とは一体どういうものなのか?
現在、ベクトルデータベースにため込むドキュメントデータとして主だったものは以下の3つです。
企業内のデータ
企業が持つ社内ドキュメントや、社員が日々作成しているドキュメントデータ
専門性の高いデータ
基本的にTransformerタイプのLLMはファインチューニングやRAGを構成しなくても、なるべくLLM単体で推論ができることを目的として作られています。
ですが、特に専門性の高いデータ(例えば、医療、法律、金融などなど)について言うと、現在一般的に提供されているLLMの推論精度はその分野の専門家が満足するレベルには至っていません。こちらは海外、国内の様々な団体や企業が業界特化型のLLMを開発する市場動向がありますが、まだまだ多くの企業がこのような取り組みを行える状況ではなく、簡単に専門性の高いデータを生成AIの対象にできる構成は需要が高いといっていい状況だと思います。
インターネットに公開されて間もない最新データ
LLMはあるインターネットから一定期間ため込まれた情報を使って学習処理を行った結果作られるものであり、インターネットに公開されて間もない最新データは学習していない可能性があります。
最新の情報までをカバーしてタイムリーに精度高い推論ができることが求められるシステムも多々あります。
このようなデータにも対応できるようにするために
RAG構成の一つとして、LLMとベクトルデータベースの合わせ技があります。
その合わせ技の処理フローとしては以下の図のような感じ
LLMフレームワークと呼んでいるフレームワークがLLMとベクトルストアを連携させる形でpromptから推論します。 https://scrapbox.io/files/65615cc9907d64001c5575a3.png
つまり、LLMが知らない情報が入力された際、ベクトルストアに、そのヒントをもらって、そのヒントをベースに最終的な文章をLLMが生成するという流れ
この構成では、ファイチューニングのようにLLMの再学習は必須ではありません。
従って、学習用データを作る必要はないのですが
その代わりに、ベクトルストアを構築し、運用するという工数が発生します。
(コードや構成自体はOSSで構成したときと全く同じです。)
実行環境
LangChain(とPython実行環境) -> Oracle CloudのData Science Service
Vector Store -> Elastic Search on OCI IaaS(marketplace)
https://scrapbox.io/files/65615cd2017f33001ca6e68a.png
コード概要
まずベクトルデータベースに入れるデータを用意します。
本記事で利用するバージョンのGPTはLangChainについての知識を持っていませんのでそれを補完するためにLangChainについてのドキュメントを作ってみようと思います。
実際の現場ではPDFやワードといったドキュメントからデータを読み込むような処理が多いと思いますのでそれに合わせてPDFで以下のような簡単なドキュメントをつくりsample.pdfというファイル名で保存しました。
https://scrapbox.io/files/6584fb8f84743700276d9998.png
このドキュメントから
テキストデータの抽出
テキストデータのEmbedding処理
ベクトルストアへのデータロードを行い
実際にプロンプトを入力した際にLLMと連携した推論ができるかを確認したいと思います。
まずはOpenAIのAPIキーを入力します。
code:openAIとLangchainのインスト
!pip install langchain
!pip install openai
次に、LangChainのPDFローダーを使ってPDFファイルからテキストデータを抽出します。
code:APIの取得
import os
import getpass
documentsを確認すると、余計な改行コードが入ってしまっていますが、PDFからテキストデータが抽出されていることが分かります。
code:PDFの抽出
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader("/home/datascience/Langchain/sample.pdf")
documents = loader.load_and_split()
テキストデータをロードできました
ロードされたテキストデータをセパレータで区切り文章単位で扱えるようにします。
OpenAIやその他様々なテキストスプリッターがありますが、今回はLangChainに実装されているCharacterTextSplitterを使います。
恐らくこれが一番簡単なテキストスプリッターです。
code:テキストスプリッター
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(separator="\n", chunk_size=4000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)
print(docs)
出力がこちら。
Document(page_content='LangChain とは、 ChatGPT などの大規模言語モデルの機能を拡張できるライブラ\nリです。言語モデルを使用したアプリケーションを開発する際に LangChain を使\nうことで、より高度な機能を実装することが可能です。\n一般的な言語モデルでは、長文のプロンプトの送信や、回答する内容に最新の情\n報を含めることが難しい場合があります。 LangChain を利用すれば、これらの機\n能を追加してアプリを開発できます 。\nLangChain は、機械学習スタートアップの Robust Intelligence に勤務していた\nHarrison Chase によって、 2022 年10 月にオープンソースプロジェクトとし\nて立ち上げられました。このプロジェクトは、 GitHub上の何百人もの寄稿者によ\nる改良、 Twitter上のトレンドの議論、プロジェクトの Discordサーバー上の活発\nな活動、多くの YouTube チュートリアル、そしてサンフランシスコとロンドンで\nのミートアップにより、すぐに人気を集めました。 2023年4月、LangChain は法\n人化し、ベンチマークから 1000万ドルのシード投資を発表した 1週間後、ベン\nチャー企業セコイア・キャピタルから少なくとも 2億ドルの評価額で 2000万ドル\n以上の資金を調達した。', metadata={'source': '/home/datascience/Langchain/sample2.pdf', 'page': 0})
テキストデータが抽出できましたので
このテキストをベクトルに変換し、ベクトルストア(今回の場合Elasticsearch)にロードします。
具体的にはテキストデータをGPTのEmbeddingモデルに連携してベクトル化し
そのベクトルをElasticsearchにロードします。
従って、この処理のコードでは
OpenAIのEmbeddingsのモデルを定義し
urlで指定したElasticsearchに接続後
indexをdemoとしてベクトルデータをロードします。
code:Embeddingする
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import ElasticVectorSearch
# Embeddingに OpenAIのEmbeddingモデルを定義
embeddings = OpenAIEmbeddings()
# ElasticSearchのアクセスポイントとユーザー、パスワードを定義
# テキストデータをベクトル化し、indexをlangchainとしてelasticsearchにロード
db = ElasticVectorSearch.from_documents(
docs, embeddings, elasticsearch_url= url, index_name="langchain"
)
ちゃんとElasticseachにロードされている確認してみましょう。
Elasticsearchに接続し、indexを確認します。
code:Elasticsearchとの接続確認
from elasticsearch import Elasticsearch
indices = Elasticsearch(url).cat.indices(index='*', h='index').splitlines()
for index in indices:
print(index)
以下のように、デフォルトで作成されている.security-7というindex以外に、先ほどロードしたlangchainというindexが確認できます。
code:index確認
.security-7
langchain
実際にロードされたベクトルデータを以下のコードで確認してみます。
code:データの確認
print(Elasticsearch(url).search(index="langchain"))
下記のvectorの項目が先ほどの文章のベクトルに相当します。
GPTのデフォルトのembeddingモデルは1536次元となり、カンマで区切られたこの数値が1536個並んでいるという状況になっています。
code:ベクトルデータ
{'took': 6, 'timed_out': False, '_shards': {'total': 1, 'successful': 1, 'skipped': 0, 'failed': 0}, 'hits': {'total': {'value': 2, 'relation': 'eq'}, 'max_score': 1.0, 'hits': [{'_index': 'demo', '_type': '_doc', '_id': '5337f817-2daf-46fc-8cee-227420b67b1b', '_score': 1.0, '_source':
{'vector': [-0.0147464187139141, 0.01817330850056069, -0.017566183057582382, -0.028089701290861913, 0.014908318894129819, -0.0034353219809854503, -0.015178152838263532, 0.014665469089467514, 0.0065738268096883256, -0.01936057710969099, -0.016257488614798383, 0.012149268398610932, 0.022773974826601876, 0.013768271132090663, -0.002312138607624016,
・・・
中略
・・・
0.0037499468370202024, 0.006994055595685205, -0.012180582640426756, 0.02028748210162566, -0.013347382679390384, -0.030970783955615217, 0.008181089548938522, 0.015161655868430601, -0.003999493557149521, -0.01678033734663821, -0.020746110041611355],
'text': 'LangChain は、大規模言語モデル (LLM)を使用してアプリケーションの作成を簡素化するように設計さ\nれたフレームワークです。言語モデル統合フレームワークとしての LangChain のユースケースは、ド\nキュメント分析と要約、チャットボット、コード分析など、一般的な言語モデルのユースケースと大き\nく重複します。', 'metadata': {'source': '/home/datascience/Langchain/sample.pdf', 'page': 0}}}]}}
Elasticsearchにベクトルデータがロードされていることが確認できましたので
次は、入力されたLLMのモデルを定義し、ベクトルストアとの連携を定義します。
code:ベクトルストアとの連携
from langchain.chains import VectorDBQAWithSourcesChain
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model_name="gpt-3.5-turbo")
qa =VectorDBQAWithSourcesChain.from_chain_type(llm, chain_type="map_reduce", vectorstore=db)
そして、LangChainのAgentとToolsを定義します。この定義がまさにLLMとベクトルストアの連携の要になります。
Agentとは入力テキストの内容に応じてどのToolを使えばよいかを考えてくれるロボットのようなものです。
そしてそのToolは今回の場合、上述のコードで定義した、LLM(GPT)とベクトルストア(elasticsearch)となります。
code:toolsの定義
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.agents import Tool
tools = [
Tool(
name = "elasticsearch_searcher",
func=qa,
description="Langchainの説明"
)
]
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
次に、LangChainのPromptTemplateを定義します。
PromptTempleteはプログラムからLLMに入力するpromptを生成するための機能です。
その際、プロンプトのテンプレートを定義することで、毎回同じフォーマットで回答を得ることができるようになります。
下記のコードでは非常にシンプルなフォーマットにしていますが、その他、few-shot learningやチャットボットのようなフォーマットで出力するような定義も可能です。
code:プロンプトテンプレートの定義
from langchain.chains.qa_with_sources.map_reduce_prompt import QUESTION_PROMPT
from langchain import PromptTemplate
template = """
下記の質問に日本語で答えてください。
質問:{question}
回答:
"""
prompt = PromptTemplate(
template=template,
)
ここまでで全てのパーツが揃いましたので
最後に実際に質問をしてみたいと思います。
下記コードにより、質問文章がPromptとして定義したフォーマットでAgentに渡され、Agentがどのtoolsで定義されたLLMとベクトルストアを使分けて最終的な回答を生成するという処理が実行されます。
code:プロンプト送信
query = "LangChainとは何ですか?"
question = prompt.format(question=query)
agent.run(question)
実行結果は以下のようになりました。
https://scrapbox.io/files/6584fbb8eba2420025130a46.png
ChatGPTのようなつれない回答ではなく、ちゃんとElasticsearchの検索と連携し、LangChainに関する文章が生成されていることがわかります。
その他、様々な質問をしてみます。
code:質問
query = "LangChainプロジェクトはいくらの資金調達に成功しましたか?"
question = prompt.format(question=query)
agent.run(question)
https://scrapbox.io/files/6584fbc6b522d30027a61bf6.png
code:質問
query = "LangChainのオープンソースプロジェクトが立ち上げられた日はいつですか?"
question = prompt.format(question=query)
agent.run(question)
https://scrapbox.io/files/6584fbe0836bb200235b85d6.png
code:質問
query = "LangChainが法人化された日はいつですか?"
question = prompt.format(question=query)
agent.run(question)
https://scrapbox.io/files/6584fbe8b62b1c00246317ea.png
code:質問
query = "LangChain が人気になった理由は何ですか?"
question = prompt.format(question=query)
agent.run(question)
https://scrapbox.io/files/6584fbef859e3000238039bf.png
さいごに
LLMの知らない知識を追加するという意味では、ファインチューニングもその手法の一つです。
LLMを使った賢いアプリケーションを作りたいということであれば、どちらかと言わずベクトルストアもファインチューニングもやってしまえばいいとは思いますが、あえて両者を比較すると以下のようなpros/consがあると感じます。
まずベクトルストアの構成ではモデルの再学習(ファインチューニング)が不要です。
(というかファインチューニングをしなくても知識が追加できるとい手法)
いつ実行されるか、どれくらいの時間がかかるかが不明瞭なファインチューニングという処理処理をする必要がなくなるということは一つのメリットだと思います。
そしてファインチューニングをしないということは学習データを作る必要がないということです。
ファインチューニングでは学習データを特定の形式で作りこむ必要があります。
特にドメインナレッジがない場合のこの作業は大変な工数になり、この作業がなくなるというメリットは非常に大きいです。
ベクトルストアの場合、ドキュメントデータをロードする際はある程度定型のコードを実行して、ドキュメントを丸ごとデータベースにロードするような手法になり、学習データを作りこむような作業に比べると比較的工数は少ないのではないでしょうか。
しかしながら、ベクトルストアの場合はそもそもベクトルストアを構築、運用してゆくという作業コストが必要になります。
また、サービス利用のコスト面でいうと、ファインチューニングはそれ専用の課金が、そしてベクトルストアの場合、そのクラウドサービスの課金が必要となり、こちらはシステム要件によって差が出る点だと思います。