前回の続きです。
業務で利用できるRAG
システムを構築する際、一般的にはAWSなどのクラウド上でvector database
やAWS Bedrock
などのマネージドサービスを活用します。しかし、クラウドでの構築・運用にはAWSの深い知識が求められ、AWSに詳しくない開発者にとっては難易度が高いかもしれません。また、AWSの各サービスに注目しすぎると、RAG
システムの本質を見失う可能性もあります。
まずはローカル環境で完結するRAG
システムを構築し、理解を深め、その後、業務での実際の利用時にはAWS上で各マネージドサービスを使用して再構築するというアプローチが有効です。これにより、RAG
システムの本質的な理解を深めつつ、実務での適用も視野に入れることができます。
本記事では、外部APIに依存せず、テキストの分割、埋め込み生成、ベクトルストアへの保存、効率的な情報検索、LLM
による回答生成までをすべてローカルで完結できる完全ローカルRAG
の構築方法について解説します。これにより、RAG
システムの基本的な仕組みと実装方法を理解することができます。
1. 基本構成
準備段階と利用段階の2つのフェーズで構成されます。
1.1 準備段階
-
構成コンポーネント:
- ナレッジファイルを保持している特定のフォルダ(AWSの場合は、
S3
がこのフォルダに置き換えます) - 管理用機能(file upload + text分割 + embedding生成 + embeddingデータを
vector database
に保存) - 埋め込み(
Embedding
)モデル - ベクトルストア(今回は
PostgreSQL
のpgvector
pluginを使用) - 会話履歴管理のデータベース(
PostgreSQL
)
- ナレッジファイルを保持している特定のフォルダ(AWSの場合は、
-
ステップ:
- 管理者によるナレッジファイルを更新
-
python vectorstore_preparation.py
コマンドを実行し、以下のことを行う
1.2 利用段階
-
構成コンポーネント:
- チャットアプリケーション(
Streamlit
利用)-
Streamlit
は、生成AIアプリケーションを簡単に作成できるPythonのフレームワークです。ユーザーインターフェースを迅速に構築し、インタラクティブなデータビジュアライゼーションやダッシュボードを提供することができます。
-
- 埋め込みモデル
- ベクトルストア
- LLMモデル:
Ollama
側で管理、REST API
としてアプリケーションに提供
- チャットアプリケーション(
-
ステップ:
- ユーザーがチャットアプリから質問文を入力
- 質問文を
LLM
で複数のsubquery
に自動的に分割 - それぞれの
subquery
をembedding model
でベクトル化 - ベクトル化された質問文を使用してベクトルデータベースから関連テキストチャンクを検索、取得(多めに取得しておく)
- 取得した関連テキストチャンクを
rerank model
で再度順番を並び替えて、上位数個だけ利用する - databaseから過去の会話履歴を取得
- reranking後top3のテキストチャンク + 質問文 + 過去の会話履歴を
LLM
に入力 -
LLM
が回答を生成 - 会話履歴をdatabaseに保存
- ユーザーに回答を出力
2. 機能説明
-
入力欄に質問を入力し、「質問」ボタンを押すと、
RAG
なし(LLM
のみ)とRAG
あり(ベクトルストアに蓄積された外部資料を利用)の両方で回答が生成されます。これにより、結果を比較することができます。 -
画面上で
LLM
モデルを選択することができます。 -
質問文は必要に応じて
LLM
によって複数のsubquery
に自動的に分割されます。他のsubquery
の結果から容易に計算や推論できるsubquery
は自動的に省略されます。 -
簡単な質問の場合は分割せず、質問文をそのままベクトルストアで検索することもあります。
-
RAG
を利用する場合、画面上にはsubquery
ごとに、ベクトルストアから取得された関連テキストチャンクとsubquery
との類似度スコアが表示されます。
-
「
LLM
を使用してrerank
を行う」ボタンを押すと、以下の処理が行われます-
subquery
ごとにベクトルストアから関連テキストチャンクを上位10件取得 - 専用の
rerank
モデルでテキストチャンクの順位を並び替え - 上位3つのチャンクのみを選択
- これらのテキストチャンクを参考情報として
LLM
に渡す
-
-
アプリケーション側(
PostgreSQL
)で会話履歴を保持しています。過去の会話履歴もLLM
に渡すことで、会話のコンテキストを維持できます。「会話履歴をクリア」ボタンで会話履歴を削除することが可能です。chatgpt
と会話する時に、あたかも以前会話した履歴はchatgpt
が覚えてるように見えます。モデル側で過去の会話履歴を覚えているのか?
ChatGPT
との会話で過去の文脈が維持されているように見えるのは、主にアプリケーション側での巧妙な履歴管理とコンテキスト提供によるものです。GPT
モデル自体は長期的な会話の文脈や履歴を保持する機能持っていなくて、各応答を独立して生成しており、アプリケーションが提供する文脈情報に基づいて一貫性のある会話を実現しています。
アプリケーション側でユーザーの会話履歴をデータベースに保存し、会話する時にその履歴を利用するのは一般的なやり方です。 -
「ユーザーロール」を選択すると、ユーザーの役職やロールに応じたアクセス権限を持つコンテンツのみにアクセスできるよう制御することができます。
3. 環境構築と使い方
4. テキスト分割について
4.1 テキスト分割とは
テキスト分割は、RAG
システムにおいて重要な役割を果たす基本的な概念です。これは、小学生が国語の授業で行う段落分けと類似しています。
国語の授業で行う段落分けは、文章を意味のまとまりごとに分ける作業です。小学生は、文章を読みながら、どこで話題が変わるのか、どこで新しい情報が追加されるのかを考え、そのポイントで段落を分けます。これにより、文章が読みやすくなり、情報が整理されます。
RAG
におけるテキスト分割も同様の原理に基づいています。理想的なテキスト分割では、各チャンクが独立して意味を持つ単位となり、明確な主題や中心的な情報を含み、文書の論理的構造(章、節、段落)を尊重します。
- なぜテキストを分割しないとだめなのか?
LLM
は知らないことには答えられません。特に、インターネットに公開されていない社内文書は、ほとんどのLLM
にとって未知の情報です。そのような情報を参考情報としてLLM
に提供することで、より適切な回答を生成できますが、大量の文書を一度にLLM
に入力することは不可能です。そのため、質問に関連するテキストを迅速かつ正確に取得することが非常に重要になります。効率的な情報検索と生成のために、テキストを適切なサイズのチャンク(段落に相当)に分割する必要があるのです。
LLM
(大規模言語モデル)には、一度に処理できる入力テキストの量に制限があります。この制限は「コンテキストウィンドウ」や「トークン制限」と呼ばれることがあります。
この制限がある理由は以下の通りです:
-
計算リソースの制約:大量のテキストを処理するには膨大な計算能力が必要となり、現実的な応答時間内で処理を完了することが困難になります。
-
メモリ制限:
LLM
は入力されたテキスト全体を「記憶」しながら処理を行うため、扱えるテキストの量には上限があります。 -
モデルの設計:
LLM
は特定の長さの入力を想定して学習されており、その範囲を大きく超える入力に対しては性能が低下する可能性があります。
-
-
コストと効率:大量のテキストを処理すると、APIの利用コストが高くなったり、応答時間が長くなったりする問題があります。
このため、大量の文書を効果的に処理するためには、文書を適切な大きさのチャンク(断片)に分割し、必要な部分だけをLLM
に入力する手法が必要となります。
::
- テキストの分割方法が最終的な回答の精度に与える影響
適切な分割は文脈を保持し、関連情報を一緒に保つことで検索精度を向上させ、結果として回答の質も高めます。一方、不適切な分割は重要な情報を分断してしまい、回答に必要な関連情報を取得できなくなる可能性があります。
適切なテキスト分割は、単に情報を小さな断片に分けるだけでなく、意味のある単位で情報をグループ化し、文脈を保持することが重要です。これにより、RAG
システムは必要な情報をより正確に抽出し、高品質な回答を生成することができるのです。
4.2 テキスト分割の方法、精度を上げる方法
文字数による分割(固定チャンクサイズとオーバーラップ)
-
専門用語の説明
- チャンクサイズとは: チャンクの長さを指します。
- オーバーラップとは:テキスト分割する時に、隣接するチャンク間で一部の内容を重複させることで、各チャンクが前後の文脈を共有し、情報の連続性を保つ方法とても有用です。
例えば、1000文字の原文がある場合、オーバーラップなしでは10個のチャンクに分割され、オーバーラップ20文字の場合は12個のチャンクになります。
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=20)
-
オーバーラップの必要性
オーバーラップは、一見すると冗長に思えますが、実際には検索精度を向上させる重要な役割を果たします。- 文脈の保持:文章の意味は前後の文脈に依存することが多いです。オーバーラップにより、チャンクの境界をまたぐ重要な情報や文脈が失われるリスクを減らせます。
- キーワードの分断防止:重要なキーワードや表現がチャンクの境界で分断されるのを防ぎます。これにより、検索時に関連情報を見逃すリスクが低減します。
- 意味のつながりの維持: 文章の途中で切れてしまう場合でも、次のチャンクに続きの内容が含まれることで、意味のつながりが維持されます。
- 類似度計算の精度向上:ベクトル検索では、質問と各チャンクの類似度を計算します。オーバーラップにより、関連情報が複数のチャンクに存在することで、より正確な類似度計算が可能になります。
-
オーバーラップなしで大きなチャンクサイズを使ったらよいのでは?
チャンクサイズが大きすぎると、 質問と関係ない情報も多く含まれるため、ノイズが増える可能性があります。特定の情報を見つけにくくなる可能性があります。逆にLLM
に悪影響を与えてしまう。そしてLLM
には入力トークン数の制限があり、大きすぎるチャンクは処理できない可能性があります。
結論:
一般的には一定のオーバーラップを設けることがよい。しかしどのような状況にも最適なオーバーラップとチャンクサイズは存在しない。文書の種類や文章の構造や文章の量などによって適切なバランスを見つける必要がある。実際に運用する時にも社内文書増えると、以前適切したチャンクサイズとオーバーラップが適切でなくなることがある。そのため、定期的にチャンクサイズとオーバーラップの調整が必要になる。テストで最適な値を見つかるしかなくて、かなり大変そうです。
トークン数による分割
-
文字数による分割の問題:
- 言語による情報量の差異:
異なる言語では、同じ意味を表現するのに必要な文字数が大きく異なる場合があります。例えば、日本語や中国語などの漢字を使用する言語は、アルファベットを使用する言語と比べて少ない文字数で多くの情報を表現できます。そのため、単純に文字数で分割すると、言語によって1チャンクに含まれる情報量に大きな差が生じる可能性があります。 - 単語や文の途中での分割:
文字数だけで分割すると、単語や文の途中で切れてしまう可能性が高くなります。これにより、文脈や意味が失われ、後の処理や分析に悪影響を与える可能性があります。
意味のまとまりの無視:
文字数による分割は、テキストの意味的なまとまりを考慮しません。そのため、関連性の高い情報が異なるチャンクに分散してしまい、後の検索や分析の精度が低下する可能性があります。 - 特殊文字やフォーマットの扱い:
HTML
タグやMarkdown
などの特殊な文字やフォーマットを含むテキストを文字数で分割すると、これらの要素が分断され、元のフォーマットが崩れる可能性があります。 - 言語モデルの性能への影響:
多くの言語モデルはトークンベースで動作します。文字数で分割されたテキストをそのままモデルに入力すると、モデルの性能が最適化されない可能性があります。
これらの問題に対処するため、トークン数による分割や、意味を考慮した分割方法が推奨されています。
- 言語による情報量の差異:
-
トークン数による分割
-
トークン数による分割は、文字数による分割の弊害を克服するための効果的な手法です。
-
多言語テキストを扱う場合(文字数だと言語によって情報量が大きく異なる可能性があるため)
-
より意味のある単位でテキストを分割したい場合
-
トークン数による分割は、使用するトークナイザーに依存するため、文字数による分割よりも若干複雑になる可能性があります。しかし、この方法を使用することで、より精密な分割が可能になり、言語モデルの性能を最大限に引き出すことができます。
-
以下は、
LangChain
ライブラリを使用してトークン数による分割を実装する例ですfrom langchain.text_splitter import TokenTextSplitter text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)
専用の分割モデルで分割
-
BERT
やGPT
などの事前学習済み言語モデルを使用して、文書の意味的な構造を理解し、適切な分割ポイントを特定します。
例えば、Hugging Face
のtransformers
ライブラリを使用して実装できます:
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
def semantic_split(text, max_length=512):
# テキストをトークン化し、BERTモデルで処理
inputs = tokenizer(text, return_tensors="pt", truncation=False)
outputs = model(**inputs)
# 文の境界を特定し、意味的なチャンクに分割
# (ここに具体的な分割ロジックを実装)
メタデータの活用
例えば、部門ごとのドキュメントにメタデータを追加し、ユーザーの所属部門に基づいて検索範囲を絞ることで、検索の精度と効率を向上させることができます。これは、メタデータフィルタリングの効果的な使用例の1つです。以下のような利点があります:
- 検索精度の向上:
- ユーザーの所属部門に関連する文書のみを検索対象とすることで、より関連性の高い結果を得られます。
- 他部門の機密情報や無関係な情報を除外できるため、ノイズが減少します。
- 検索効率の向上:
- 検索対象となるドキュメント数が減少するため、検索速度が向上します。
- ベクトル検索の対象が絞られることで、計算リソースの使用が最適化されます。
- セキュリティの強化:
- 部門ごとのアクセス制御が可能になり、機密情報の保護が容易になります。
文書構造(タグや見出しを利用)による分割
-
HTML
やMarkdown
などの構造化されたテキストの場合、タグや見出しを利用して分割します。 - さらに親セクションの情報を追加することによってより広いコンテキストを維持し、検索の精度を上げることができます。
例えば、以下のようなテキストがあるとします。
# iPhone 16 - 技術仕様
## 仕上げ
### サイズと重量
- 幅:71.6 mm
- 高さ:147.6 mm
- 厚さ:7.80 mm
- 重量:170 g
テキスト分割する時は、見出しやセクションもテキストに追加すると、より広いコンテキストを維持し、検索の精度が高くなります。
iPhone 16 - 技術仕様 > 仕上げ > サイズと重量
- 幅:71.6 mm
- 高さ:147.6 mm
- 厚さ:7.80 mm
- 重量:170 g
5. imbedding生成(ベクトル化)モデルについて
5.1 埋め込みの概念
- 埋め込みとは、テキストや画像、音声などを固定長の数値ベクトルに変換するプロセスです。これにより、テキストの意味や関係性を数学的に表現できます。
-
RAG
システムにおいて、embedding model
は文書や質問をベクトル化する役割を担います。 - 高品質な
embedding
を生成できるモデルを選択することで、関連性の高い文書をより正確に検索できるようになり、RAG
システム全体の精度が向上します。そのため、embedding model
の選択は非常に重要です。 - アーキテクチャによる分類(
Transformer系、RNN系、CNN系
)、ライセンスによる分類(オープンソース/商用)、データの種類による分類(テキストEmbedding、画像Embedding、音声Embedding)などなど考慮要素が非常に多いです。別な記事でこの辺を整理できればいいです。 - 今回は次元数について重点的にまとめます。本記事では、
embedding model
の重要な特性の一つである次元数に焦点を当てて説明します。
5.2 embedding modelで生成されるvectorの次元数(dimenssion
)について
-
モデルによって生成されるベクトルの次元数は異なります。
- 例:
OpenAI
のモデルでは、text-embedding-ada-002
は1536次元、text-embedding-3-small
は512または1536次元、text-embedding-3-large
は256、1024、または3072次元のベクトルを生成します。
- 例:
-
次元数が大きいほどよいかというと、必ずしもそうとは限りません。次元数の選択には以下のトレードオフがあります:
- 大きな次元数:分割されるテキストチャンクは
vector
に変換されるため、vector
の次元数が多ければおおいほど、より多くの情報を保持できる一方、計算コストとストレージが増加 - 小さな次元数:効率的な処理と保存が可能だが、情報の損失が発生する可能性がある
- 最適な次元数は、タスクの複雑さ、必要な精度、利用可能なリソースによって異なります
- 大きな次元数:分割されるテキストチャンクは
-
互換性:
- 異なる次元数のベクトルを直接比較することはできません。
embedding model
を変更する際は、全データの再埋め込みが必要がある
- 異なる次元数のベクトルを直接比較することはできません。
embedding model
を変更する際なぜ全データの再埋め込みが必要なのか?
-
意味空間の違い:
- 各
embedding model
は独自の学習データと手法で訓練されており、その特性が埋め込みに反映されます。 - 異なるモデルは、同じ次元数であっても異なる意味空間を持ちます。つまり、各次元が表す意味や特徴が異なります。
- 各
-
ベクトル値の非互換性:
- 同じ入力テキストでも、異なるモデルは異なるベクトル値を生成します。これらの値は直接比較できません。
-
類似度計算への影響:
- 新旧のベクトルを混在させると、類似度計算の結果が不正確になる可能性があります。
結論として、embedding model
の次元数の選択は、精度、効率性、スケーラビリティのバランスを考慮して行う必要があります。また、選択したモデルとベクトルデータベースが互換性を持つことを確認することが重要です。
ベクトルデータベースを選択・設計する際は次元数を考慮する必要があります。
- 多くのベクトルデータベースは、特定の次元数のベクトルを想定して設計されています。
- 次元数の変更には、データベースのスキーマ変更や全データの再埋め込みが必要になる場合があります。
例えば、pgvector
ではtable作成する時には次元数を指定する必要があり、次元数を変更する場合は新しいテーブルとインデックスを作成し、データを再挿入する必要があります。
CREATE TABLE embeddings_new (
id SERIAL PRIMARY KEY,
embedding vector(384) NOT NULL
-- その他の列
);
6. rerankingについて
6.1 rerankingの考え方
- ベクトルデータベースから関連テキストチャンクを多めに取得する
- 取得した関連テキストチャンクを
rerank model
で再度質問との関係性の順に並び替え、上位数個だけを利用する
6.2 なぜrerankingが必要?
- より関連性の高い情報を提供できる
- ノイズや無関係な情報を効果的に除去できる
- 文脈をより深く理解したコンテキスト情報を得られる
- 結果的に
LLM
の回答の質が向上する
ベクトル検索は意味的な検索ができますが、文章表現を一つの意味ベクトルに圧縮するため、一定の情報量が失われます。そのため、vector
検索では本当に必要な情報がヒットしない可能性があります。
そこで、関連性が低くても多めに情報を取得し、より表現力の高い言語モデルを使ってクエリと抽出したチャンクの類似度を再算出します。その後、類似度の高い順に再度並び替えた上位チャンクのみを使用することが有効な手段となります。
「ベクトル検索せず、最初から表現力の高い言語モデルを使ったほうがよくない?」という疑問が生じるかもしれません。これは、ベクトル検索が単純なベクトルの内積計算であるのに対し、言語モデルは計算コストが非常に高く、ドキュメント全体に適用するのが現実的ではないためです。
reranking
を導入することで、RAG
システムの精度を大幅に向上させることができます。ただし、計算コストが増加するため、計算コストとのトレードオフを考慮する必要があります。
6.3 rerankingの主な手法
-
クロスエンコーダーを使用した再ランキング:
双方向エンコーダー(BERT
等)を使用して、クエリと各ドキュメントのペアをより詳細に評価します。 -
LLM
を使用した再ランキング:
LLMにクエリと各ドキュメントの関連性を評価させます。
より高度な文脈理解が可能ですが、計算コストが高くなります。 -
特徴量ベースの再ランキング:
TF-IDF
スコア、BM25
スコア、エンティティマッチング等の追加特徴量を使用して再ランキングを行います。
比較的軽量で、特定のドメインに特化した特徴量を追加できます。
7. まとめ
RAGシステムを本番環境で運用する企業は、多岐にわたる課題に直面しています。コスト管理、データの権限制御、性能評価の指標設定など、様々な問題がありますが、特に出力結果の信頼性と精度向上が最も難しい課題となっています。
-
RAG
システムの精度向上には、最新の高性能LLM
の導入が効果的です。 - しかし、
LLM
が固定されている場合、vector store
からいかに関連性の高い情報を取得できるかが回答の精度を大きく左右します。この点を改善するためには、いくつかの重要な要素に注目する必要があります。- まず、元のテキストデータの質が精度に大きな影響を与えます。
vector store
にデータを投入する前に、人の目で直接チェックし、文脈が適切に保持されているか確認することが重要です。必要に応じて、データを整形するプロセスを導入することも効果的でしょう。 - 次に、テキスト分割の最適化も非常に重要です。文書を適切なサイズのチャンクに分割し、文脈を保持しながら処理することで、検索精度の向上が期待できます。
- さらに、テキストチャンクのembedding処理にも注意が必要です。embedding時にはデータが圧縮されるため、情報の一部が失われる可能性があります。情報損失を最小限に抑えるには、次元数の多いembeddingモデルを選択することが望ましいですが、同時に計算コストも上昇するため、適切なバランスを取ることが重要です。
- vector検索の限界を補完する手法として、rerankモデルの導入も有効です。vector検索では必要な情報がヒットしない、あるいは類似度スコアが低い場合があります。そこで、関連性が低くても多めに情報を取得し、より高度な言語モデルを使用してクエリと抽出チャンクの類似度を再計算します。その後、類似度の高い順に並べ替えて上位チャンクのみを使用することで、より精度の高い結果を得ることができます。
- 複数の要素や概念を持つ複雑な質問は
LLM
で複数のsubquery
に分割し、subquery
ごとにvector store
から関連情報を検索する手法も効果的です。v各サブクエリは元の質問よりも具体的で焦点が絞られているため、ベクトルストアからより関連性の高い情報を抽出できる可能性が高くなります。これにより、検索結果の精度が向上し、より適切な情報を取得できます。
- まず、元のテキストデータの質が精度に大きな影響を与えます。
8. RAGの限界
RAGシステムは多くの場面で有効ですが、その設計と実装には固有の限界があります。これらの限界を理解することは、システムの適切な活用と改善に不可欠です。
-
テキスト分割の問題
テキストは単純なチャンクに分割され、各チャンクは独立したベクトルとして扱われます。この手法は効率的ですが、チャンク間の関係性や文脈の連続性を適切に表現できないという欠点があります。結果として、元の文書の全体的な構造や意味が失われる可能性があります。 -
大局的理解の困難さ
RAGは特定の情報抽出には長けていますが、「文書群全体のテーマは何か?」といった大局的な質問への対応が難しい場合があります。 取得されるチャンクだけからはそもそも文書の全体像が把握できないためです。 -
抽象的理解の限界
詳細な事実や特定情報の抽出は得意ですが、文書全体の概要や主題把握など、より抽象的で包括的な理解を要する課題には弱点があります。
これらの限界を克服するため、最近ではGraph RAG
などの新しいアプローチが登場しています。これらの新技術により、RAGシステムの適用範囲がさらに拡大することが期待されます。次回Graph RAG
についてまとめてみたいと思います。