はじめに
はじめまして、DX Technology Unitの芹澤です。普段はAI関連技術を用いた研究開発に携わっています。
昨今、ChatGPTを初めとした大規模言語モデル (Large Language Model; 以下LLM) が話題になっており、様々な質問に対して非常に優れたアウトプットが得られるようになりました。一方、LLMを企業で使用する場合、社内特有のデータを参照する必要があるため、社内特化LLMシステムを構築することが必須となります。
弊社では、社内の業務効率化を目的とした生成AIの活用を促進する「生成AIプロジェクト」が立ち上がり、ChatGPTをはじめとしたLLMの検証作業を進めています。その中で生成AIが社内情報を参照できるようにする方法について検証を進めており、Retrieval Augmented Generation (RAG) と呼ばれる手法を用いて、ChatGPTが外部データを参照し、対話形式で外部データの情報を取得する方法についてオープンデータを用いて検証を行いました。今回はその結果を共有させていただきます。
なぜRAGを使うのか
生成AIが社内情報を参照できるようにするには、主にプロンプトで情報を与える方法とLLMそのものをFine-tuningする方法、RAGによって外部データを保存したデータベース (Data Base; 以下DB) から呼び出す方法の3つがあります。
プロンプトで情報を与える方法は非常に簡単です。LLMに対してプロンプトで社内情報を与えた上で、「その中から回答してください」と質問をすることで社内情報を参照できるようになります。ただし、このやり方では逐一プロンプトで情報を与える必要があること、主要なLLMであるChatGPTでは入力プロンプトの文字数制限があること、与える情報が多くなりすぎると文章の複雑性が増してLLMの回答精度が低下する傾向があることから、膨大な社内情報を参照するシステムとして利用するのは現実的ではありません。
LLMのFine-tuningでは、Parameter-Efficient Fine-Tuning (PEFT) と呼ばれるHugging Faceで公開されているライブラリが多く利用されており、中でもLow-Rank Adaptation (LoRA) と呼ばれる手法やその派生手法が多く利用されています。LoRAは事前学習済モデルの99.9%以上のパラメータを固定した上で追加学習を行い、全体をFine-tuningした場合と同程度の精度を保ちつつ計算効率化を実現しています。ただし、それでも大規模言語モデルをFine-tuningするにはハイスペックGPU環境で長時間学習を行う必要があり、コストが課題となります。
一方、RAGはLangChainと呼ばれるLLMの機能拡張ライブラリを活用して開発されることが多くなっています。RAGの詳細は次章で説明しますが、社内情報など追加するデータをDBに保存し、そちらをLLMが参照して出力を作るという形になります。この方法ではモデルの学習を行っていないため、計算コストがあまりかからず、比較的簡単に実装・検証を進めることができます。そのような理由から、最初のステップとしてRAGによる検証を進めました。
RAGについて
RAGは文章検索で関連文章を抽出してLLMにプロンプトとして渡す手法で、フローは以下のような形になっています。今回はRAGの手法の中でも一般的な文章をベクトル変換し、ベクトルデータベース (Vector DB) に保存して類似度検索 (Similarity Search) を行う方法を取りました。
ステップとしては、Storeフェーズ、Retrievalフェーズ、Generationフェーズに分けることができます。
- Storeフェーズ
社内情報など追加で与えたい情報をまとめたテキストファイルから文章を抽出し、Embedding (文章のベクトル変換) をしてベクトル化します。このベクトル化したデータをDBに保存することで、Vector DBを構築します。 - Retrievalフェーズ
ユーザからの質問文 (クエリ) を基にDB上から必要な情報のみを抽出します。抽出にはベクトル間のSimilarity Searchを用いています。 - Generationフェーズ
Retrievalフェーズで抽出した情報とユーザの質問をLLMにプロンプト上で渡し (これをAugmentationと呼びます)、質問文に対する回答を作成します。
実装方法
1. Storeフェーズ
1.1. 環境構築
以下の通りライブラリをインポートします。LangChainは更新が早いのですが、ここではver0.0.163での動作確認をしています。
import pandas as pd
from datasets import load_dataset
from langchain import PromptTemplate, LLMChain
from langchain.llms import OpenAI
from langchain.indexes import VectorstoreIndexCreator
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
1.2. 追加データの準備
現在弊社で進めている生成AIプロジェクトでは、コミュニケーションツールの高度化を目指しており、対話型の社内データを活用することを想定しています。そのため、今回紹介する実装例では、参照する追加データとしてDatabricksのデータを日本語に翻訳したデータセット (kunishou, 2023-05-11, databricks-dolly-15k-ja) を使用します。これは対話型データセットであり、テストデータとして最適です 。社内特化システムを作りたい場合は、こちらを社内データに変更することで特化システムを作ることができます。
今回のデータセットは画像の通り、instruction (質問) とoutput(回答) に分かれています。
dataset = load_dataset('kunishou/databricks-dolly-15k-ja')
df_databricks = pd.DataFrame(dataset['train'])
df_databricks = df_databricks[["instruction", "output"]]
df_databricks.head()
このデータセットを、Embeddingがしやすいようリスト形式に変換します。今回は質問と回答が交互に来るリスト形式にしていますが、質問と回答で1文章にするなど、追加データセットの形式によっても後々出力される回答に影響が出るため、工夫が出来るポイントの1つとなっています。
dialog = []
for i in range(len(df_databricks)):
dialog.append(df_databricks["instruction"][i])
dialog.append(df_databricks["output"][i])
1.3. DatabricksデータのEmbedding
追加データをベクトルとして扱えるよう、Embeddingします。Embeddingには主に単語区切りのWord Embeddingと文章区切りのSentence Embeddingがあり、有名なモデルとしてはそれぞれword2vec やOpenAIEmbeggingsなどがあります。今回はSentence Embedding ModelとしてHuggingFaceEmbeddingsにあるモデル(oshizo, 2023, sbert-jsnli-luke-japanese-base-lite)を使用しました。
ベクトル化した文章を保存するにあたり、今回はセットアップが簡単な類似度検索ライブラリであるFAISSを使用しました。他にも、ChromaDBやElasticsearchDBなどLangChainの公式ページに記載のあるDBを使用することができます。
embeddings = HuggingFaceEmbeddings(model_name="oshizo/sbert-jsnli-luke-japanese-base-lite")
db = FAISS.from_texts(dialog, embeddings)
2. Retrievalフェーズ
2.1. 類似性検索
プロンプトとして与える入力と先ほど構築した追加データのDBを使用して、類似性検索を行います。queryとして与えた質問文をEmbedding Modelでベクトル化し、1.3.で作成したVector DBから類似度検索で上位N件の文章を抽出します。その後、抽出した複数文章をLLMの情報源文章 (コンテキスト) として1つにまとめます。
今回の例では、「ヴァージン・オーストラリア航空はいつから運航を開始したのですか?」という質問を渡して類似性比較を行っており、関連する上位4件の文章が抽出されて1つのコンテキストにまとめられています。
query = "ヴァージン・オーストラリア航空はいつから運航を開始したのですか?"
embedding_vector = embeddings.embed_query(query)
docs = db.similarity_search_with_score_by_vector(embedding_vector, k=4)
context = "".join([document[0].page_content for document in docs])
print(context)
3. Generationフェーズ
3.1. LLMの準備
今回の検証では、高い精度で回答を作成することで知られるChatGPTを利用します。弊社では元々Azure環境を利用していたため、Azure OpenAI Servieceを利用してChatGPTのモデルをデプロイし利用しました。engine部分にデプロイしたモデルの名前とmodel_nameにモデルバージョンをそれぞれ記載します。
API KEYを環境設定していない場合は、OpenAIが公式でアナウンスしているページ(Best Practices for API Key Safety )を参考に、事前に設定しておいてください。
llm = OpenAI(engine="arise_gen_ai", model_name='gpt-35-turbo', temperature=0.2)
3.2. LLMで回答作成
3.1.で準備したChatGPTと2.1.で抽出したコンテキストを用いて回答を作成します。
LangChainでは 質問以外のプロンプトをテンプレート化することで、都度プロンプトを作成することを避け、入力・出力をコントロール出来るようにしています。
最初に、テンプレートとなるプロンプトを作成します。ここで「[参考]部分の情報を使って質問に回答して下さい。」と回答方法を指定することで、類似性検索で抽出したコンテキストから回答を作成させます。作成した template を PromptTemplate としてLLMChain に渡すことでLangChainを実行する準備をします。後はllm_chain.runで質問を投げることで、ChatGPTで回答を作成します。
ちなみにこの部分はいくつか書き方があり、RetrivalQAやload_qa_chainを使う方法などもあるので、興味がある方は調べてみてください。
今回の例では、質問に対しVector DBから抽出したコンテキストを元に「ヴァージン・オーストラリア航空は2000年8月31日に運航を開始しました。」という回答を作成してくれています。
template ="""
[参考]部分の情報を使って質問に回答してください。
[質問]
{question}
[参考]
{context}
"""
prompt = PromptTemplate(template=template, input_variables=["context","question"])
llm_chain = LLMChain(prompt=prompt, llm=llm)
print(llm_chain.run({"question":"ヴァージン・オーストラリア航空はいつから運航を開始したのですか?","context":context}))
まとめ
以上、ChatGPTとLangChainを活用したRAGを紹介しました。
今回の実装例では上手くいった出力例を載せましたが、プロンプトのテンプレートを少し変更するだけで上手く回答が作成されないことも多々ありました。例えば、Vector DBから類似度検索で上位4件を持ってきた文章を用いてChatGPTに回答を作成してもらう際、抜き出した文章全てを使って冗長な文章を作成してしまう、ということがありました。これについては、テンプレートプロンプトで「回答は簡潔に回答してください。」と付け加えることで解決しました。このようにプロンプトエンジニアリングの難しさを感じており、試行錯誤中です。
また、今回はLLMモデルとしてChatGPTを用いていますが、こちらは別のLLMに変更しても同様のことが出来ます。そこで GPT4allと呼ばれる比較的小規模なモデルでも検証を行いましたが、英語では上手く回答が作成される一方、日本語だと良い回答が作成できませんでした。このことから、RAGでLLMを利用する場合は、基のLLMが日本語に十分対応できていることも重要になってくるという確認が出来ました。
本記事の内容は弊社で取り組んでいる技術検証の一部になります。弊社では本テーマ以外にもKDDIのデータを対象とした技術開発に取り組んでいます。今回紹介した内容や、その他弊社の取り組みに興味がございましたら、ぜひお声がけください。