はじめに
そういうChatGPTプラグインがあることは承知のうえで、PDFの内部をベクトル検索して回答を作成するChatGPTアプリケーションをつくってみましたので、構築のポイントについてまとめた記事になります。なお、OpenAIは Azure OpenAI Services を使っています。必要に応じて適宜読みかえてください。
-
登場するサービスと役割
- PDFの読み込みと段落化 -> Azure FormRecognizer
- ベクトル化と近似度計算 -> embedding-text-ada-002
- 質問に対する回答 -> ChatGPT(gpt-35-turbo)
- 今回はベクトルDBサービスは使っていません。本当にシステム化するなら必須ですね
-
完成したもの
インターネット白書2022 から、人間の代役を果たすデジタルヒューマンのPDFを使ったサンプル画面です。(検索結果がちょっとギリギリですね…このあたりは微調整が必要そうです)
1. Azure FormRecognizerでPDFを解析する
何はともあれPDFの情報を抜き出します。参考にしたサイトは、公式のドキュメントレイアウト分析と、公式の説明サイトです。2022/10から段落情報が取れるようになったので、これを使います。
公式のクイックスタートに沿って、以下のようなコードでPDFを分析します。(諸事情で今回はpythonです。)
document_analysis_client = DocumentAnalysisClient(
endpoint=endpoint, credential=AzureKeyCredential(key)
)
poller = document_analysis_client.begin_analyze_document_from_url(
"prebuilt-layout", formUrl)
result = poller.result()
このresultの最上位レイヤーにparagraphsという段落情報がはいっていて、こいつを使います。
埋め込みのチュートリアルを参考に、ベクトル化にあたって、無駄な文字や余白を削ったり、ada-002が8192トークンまでしか対応していないので文章を削ったりしながら必要な情報を取得します。
for paragraph in result.paragraphs:
# 段落文章でループ
content = paragraph.content
# 事前処理(無駄な文字を削ったりする)
content = re.sub(r'\s+', ' ', content).strip()
content = re.sub(r". ,","",content)
content = content.replace("..",".")
content = content.replace(". .",".")
content = content.replace("\n", "")
content = content.strip()
# 8192トークンを超える文字列のベクトル化はできないので、8192を上限にする
tokenizer = tiktoken.get_encoding("cl100k_base")
tokenized = tokenizer.encode(content)
if len(tokenized) > 8192:
content = tokenizer.decode(tokenized[:8192])
#段落情報と段落番号をもった情報に整形する
emb_contents.append({"id":index,"role":paragraph.role,"content":content})
index += 1
2. AdaでPDFの段落をベクトル化する
1.で取得した段落ごとにベクトル化していきます。今回は配列にベクトル化した情報をいれていますが、本当であればベクトルDBを使いたいところです。
# contentをループ処理
for i in range(len(emb_contents)):
embedding = get_embedding(emb_contents[i]["content"], settings.AOAI_EMB_MODEL)
emb_contents[i]["embedding"] = embedding
3. Adaで質問をベクトル化する
2.まででPDFの段落ごとのベクトル情報がストアされていますので、あとは検索に使うだけです。検索するために質問自体をベクトル化して、近似度を計算させて、近似度でソートします。(このあたりベクトルDBをつかっていればDBがやってくれる範囲ですね)
これで、ベクトル検索マッチ率上位x件のデータ取得が取得できます。
embedding = get_embedding(message, settings.AOAI_EMB_MODEL)
for i in range(len(emb_contents)):
emb_contents[i]["similarity"] = cosine_similarity(embedding, emb_contents[i]["embedding"])
# similarityでソート
emb_contents = sorted(embedding_data, key=lambda x: x["similarity"], reverse=True)
4. ChatGPTで回答を作成する
あとはこれまで集めた情報をChatGPTに入れてあげるだけです。以下では上位1件のベクトル検索結果のみいれてますが、5件くらいは入れたほうがよいでしょうし、段落情報が途中で切れてる可能性もあるので、1件目は後続の段落を複数いれるとかそういう調整は必要になります。
# System Prompt
system_prompt = f"""あなたはドキュメントの執筆者です。
以下のベクトル検索結果のみを使って、ユーザからの質問に回答してください。
**ベクトル検索結果以外の情報は使用してはなりません。**
**情報が足りない場合や不明な場合は、ユーザにどういう質問をすればいいかを回答してください。**
### ベクトル検索結果
{emb_contents[0]["content"] #上位1件のみを使う場合はこれだけ。}
"""
まとめ
ChatGPTを使うと、やはり情報源としてPDFなり社内のドキュメントを使いたいっていうことがあるかなと思います。今回手書きのPDFにも対応して、PDFの内部からベクトル検索で関連文章を取得し、ChatGPTにその情報を踏まえた回答を作成するアプリケーションを作ってみましたが、その有効性は感じ取れたかなと思います。
気になるのは、パラグラフがかなり細かくなってしまうため、大量のPDFがある際に答えがきちんと出せるかというポイントですが、これはドキュメントの性質にもよるのかなと思いますし、調整の見せどころになるのかなと思いました。
また、今回検索結果のトップ5件をもとに回答をつくってますが、トークンの許される範囲でいれるようにしてもいいかもです。
ひとまず期待の動きは実現できたので、実際のドキュメントや提供システムの要件に併せてベースとして参考になればなということで今回の検証は終わりです。