Google Colab(無料)でRAGを試してみた ー LangChain + ChromaDB + HuggingFaceで作るゼロ円AI検索
はじめに
前回の記事では、Google Colab(無料枠)でQwen2.5-1.5BをQLoRA×Unslothでファインチューニングしてみました。
今回はその姉妹記事として、もうひとつのLLM活用手法である RAG(Retrieval-Augmented Generation) を試してみます。
使うのはすべて無料のツールだけ。OpenAI APIキーも不要です。
この記事でやること
- Wikipedia日本語記事を自動取得して「知識ベース」を作る
- LangChain + ChromaDB でベクトル検索パイプラインを構築する
- Qwen2.5-1.5B-Instruct に検索結果を渡して回答を生成する
- RAGあり vs なし で回答品質を比較する
対象読者
- Pythonは書けるけど、MLやLLMは詳しくない方
- 「RAGって聞いたことあるけど、何が嬉しいの?」という方
- 前回のファインチューニング記事を読んでくれた方
RAGって何? ファインチューニングと何が違うの?
LLMの性能を引き出す方法として、よく比較されるのが「ファインチューニング」と「RAG」です。
ざっくり言うと、こんな違いがあります。
| ファインチューニング | RAG | |
|---|---|---|
| やること | モデルの重み(パラメータ)を書き換える | モデルに参考文書を渡す |
| 例えるなら | 社員を再教育する | 社員にマニュアルを渡す |
| 得意なこと | 口調や出力形式を変える | 最新情報や専門知識を追加する |
| 知識の更新 | 再学習が必要 | ドキュメントを差し替えるだけ |
| コスト | GPU・時間がかかる | 比較的軽い |
前回の記事ではファインチューニング=「社員の再教育」をやりました。
今回の記事ではRAG=「マニュアルを渡す」をやります。
RAGの仕組み:3ステップで理解する
RAGは Retrieval-Augmented Generation(検索拡張生成) の略で、3つのステップで動きます。
┌─────────────────────────────────────────────────────┐
│ RAGの3ステップ │
│ │
│ ❓ 質問 │
│ │ │
│ ▼ │
│ 【1. Retrieve(検索)】 │
│ 質問をベクトルに変換し、類似するチャンクをDBから検索 │
│ │ │
│ ▼ │
│ 【2. Augment(拡張)】 │
│ 検索で見つけたチャンクをプロンプトに埋め込む │
│ │ │
│ ▼ │
│ 【3. Generate(生成)】 │
│ LLMがプロンプト+参考情報をもとに回答を生成 │
│ │ │
│ ▼ │
│ 💬 回答 │
└─────────────────────────────────────────────────────┘
ポイントは、LLMが自分の知識だけで答えるのではなく、外部のドキュメントを参照しながら答えるということです。
今回作るもの
日本語Wikipedia記事(AI関連5トピック)を知識ベースにして、質問に答えるRAGシステムを作ります。
Wikipedia(5記事)
↓ 取得・分割
チャンク(400文字ごと)
↓ ベクトル化
ChromaDB(ベクトルDB)
↓ 質問で類似検索
関連チャンク上位3件
↓ プロンプトに結合
Qwen2.5-1.5B が回答生成
使用ライブラリ
| ライブラリ | 役割 |
|---|---|
langchain, langchain-core
|
RAGパイプラインの構築 |
langchain-huggingface |
HuggingFace埋め込みモデルの連携 |
langchain-text-splitters |
テキストのチャンク分割 |
chromadb |
ベクトルDB(ローカル動作・無料) |
sentence-transformers |
埋め込みモデルの実行 |
transformers |
Qwen2.5-1.5B の読み込み・推論 |
bitsandbytes |
4bit量子化(メモリ節約) |
wikipedia |
Wikipedia記事の自動取得 |
すべて無料、APIキー不要です。
環境
- Google Colab(無料枠)
- ランタイム:GPU(T4) を選択してください
ランタイムを「T4 GPU」に変更してから実行してください。
「ランタイム」→「ランタイムのタイプを変更」→ T4 GPU
実装
Step 1: ライブラリのインストール
!pip install -q \
langchain \
langchain-community \
langchain-huggingface \
langchain-text-splitters \
langchain-core \
chromadb \
sentence-transformers \
transformers \
wikipedia \
accelerate \
bitsandbytes
print('✅ インストール完了')
LangChainは最近パッケージが細分化されました(langchain-core, langchain-text-splitters 等)。古い記事のimportパスだとエラーになることがあるので注意してください。
Step 2: Wikipedia記事の取得
知識ベースとして、AI関連の日本語Wikipedia記事を5つ取得します。
import wikipedia
import re
wikipedia.set_lang('ja')
TOPICS = [
'人工知能',
'機械学習',
'深層学習',
'自然言語処理',
'Transformer (機械学習モデル)'
]
def fetch_wikipedia_articles(topics):
docs = []
for topic in topics:
try:
page = wikipedia.page(topic, auto_suggest=False)
text = page.content[:3000]
text = re.sub(r'==+.*?==+', '', text)
text = re.sub(r'\n{3,}', '\n\n', text)
docs.append({
'title': topic,
'url': page.url,
'content': text.strip()
})
print(f'✅ 取得成功: {topic} ({len(text)}文字)')
except Exception as e:
print(f'❌ 取得失敗: {topic} → {e}')
return docs
raw_docs = fetch_wikipedia_articles(TOPICS)
print(f'\n合計 {len(raw_docs)} 記事を取得')
各記事の先頭3000文字を取得しています。Colab無料枠のメモリを考慮した制限です。
ハマりポイント: 「トランスフォーマー (機械学習モデル)」だと記事が見つかりません。日本語WikipediaではTransformerの記事タイトルが英語表記の Transformer (機械学習モデル) になっています。
Step 3: チャンク分割
取得した記事を400文字ごとのチャンクに分割します。RAGでは「質問に関連する部分だけ」をLLMに渡すため、文章を小さく切り分けておく必要があります。
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
documents = [
Document(
page_content=doc['content'],
metadata={'title': doc['title'], 'url': doc['url']}
)
for doc in raw_docs
]
# chunk_size=400: 1チャンク最大400文字
# chunk_overlap=50: 前後のチャンクと50文字重複(文脈の途切れ防止)
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=50,
separators=['\n\n', '\n', '。', '、', ' ', '']
)
chunks = splitter.split_documents(documents)
print(f'チャンク総数: {len(chunks)}')
print(f'\n--- チャンクサンプル ---')
print(f'出典: {chunks[0].metadata["title"]}')
print(chunks[0].page_content[:200])
chunk_size と chunk_overlap はRAGの精度に大きく影響するパラメータです。
- chunk_size が大きすぎる → 関係ない情報も含まれてノイズになる
- chunk_size が小さすぎる → 文脈が途切れて意味が通じなくなる
- chunk_overlap → チャンクの境界で文脈が切れるのを防ぐ
今回は日本語テキストなので、400文字・50文字重複にしています。
Step 4: 埋め込みベクトル生成 → ChromaDB保存
チャンクをベクトル(数値の配列)に変換して、ChromaDBに保存します。
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
print('埋め込みモデル読み込み中...')
embeddings = HuggingFaceEmbeddings(
model_name='intfloat/multilingual-e5-small',
model_kwargs={'device': 'cuda'},
encode_kwargs={'normalize_embeddings': True}
)
print('✅ 埋め込みモデル読み込み完了')
print('\nChromaDBにベクトルを保存中...')
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory='./chroma_db'
)
print(f'✅ {len(chunks)}チャンクをベクトルDBに保存完了')
埋め込みモデルは intfloat/multilingual-e5-small を使っています。日本語対応で約120MBと軽量なのが選定理由です。
ここで何が起きているかというと:
- 各チャンク(テキスト)を埋め込みモデルに通す
- テキストが384次元のベクトルに変換される
- ベクトルをChromaDBに保存する
あとで質問が来たら、質問も同じようにベクトル化して「近いベクトル」を持つチャンクを探します。これがベクトル検索です。
Step 5: 検索テスト(LLMなし)
LLMを使わず、ベクトル検索だけを試してみます。「ちゃんと関連するチャンクが取れるか?」の確認です。
retriever = vectorstore.as_retriever(
search_type='similarity',
search_kwargs={'k': 3} # 上位3チャンクを取得
)
test_query = 'トランスフォーマーモデルの仕組みを教えてください'
print(f'質問: {test_query}')
print('-' * 60)
results = retriever.invoke(test_query)
for i, doc in enumerate(results):
print(f'\n【検索結果 {i+1}】出典: {doc.metadata["title"]}')
print(doc.page_content[:200])
print('...')
ここで「Transformer (機械学習モデル)」の記事からチャンクが返ってくれば、ベクトル検索は成功です。
Step 6: Qwen2.5-1.5B の読み込み
前回のファインチューニング記事と同じモデルを、4bit量子化で読み込みます。
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import torch
MODEL_NAME = 'Qwen/Qwen2.5-1.5B-Instruct'
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type='nf4',
bnb_4bit_compute_dtype=torch.float16,
)
print(f'{MODEL_NAME} を読み込み中(4bit量子化)...')
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
quantization_config=bnb_config,
device_map='auto',
)
print(f'✅ モデル読み込み完了')
モデルファイル(約3GB)のダウンロードに5〜10分かかります。同じセッション中の再実行は一瞬です。
Step 7: RAGパイプラインの組み立て
いよいよ本番です。Retrieve → Augment → Generate の3ステップを1つの関数にまとめます。
def rag_answer(question, retriever, model, tokenizer, max_new_tokens=256):
"""
RAGの3ステップ:
1. Retrieve - 質問に関連するチャンクをベクトルDBから検索
2. Augment - 検索結果をプロンプトに埋め込む
3. Generate - LLMで回答を生成
"""
# ---- Step 1: Retrieve ----
docs = retriever.invoke(question)
context = '\n\n'.join([
f'【{doc.metadata["title"]}】\n{doc.page_content}'
for doc in docs
])
# ---- Step 2: Augment ----
system_prompt = 'あなたは日本語で回答する親切なアシスタントです。'
user_prompt = f"""以下の参考情報をもとに、質問に日本語で簡潔に答えてください。
参考情報にない内容は「提供された情報にはありません」と答えてください。
参考情報:
{context}
質問: {question}
回答:"""
# ---- Step 3: Generate ----
messages = [
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': user_prompt}
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer([text], return_tensors='pt').to(model.device)
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.7,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
)
generated = output[0][inputs['input_ids'].shape[1]:]
answer = tokenizer.decode(generated, skip_special_tokens=True)
return answer, docs
print('✅ RAG関数を定義しました')
プロンプトの中で「参考情報にない内容は『提供された情報にはありません』と答えてください」と指示しています。これにより、LLMが知識ベースにない情報を勝手に作り出す(ハルシネーション)のを抑制しています。
Step 8: 実際に質問してみよう!
3つの質問を投げてみます。
questions = [
'機械学習と深層学習の違いは何ですか?',
'自然言語処理はどのような分野に応用されていますか?',
'トランスフォーマーモデルの特徴を教えてください。',
]
for q in questions:
print(f'\n{"="*60}')
print(f'❓ 質問: {q}')
print('='*60)
answer, sources = rag_answer(q, retriever, model, tokenizer)
print(f'💬 回答:\n{answer}')
print(f'\n📚 参照ソース: {[doc.metadata["title"] for doc in sources]}')
実行結果:
============================================================
❓ 質問: 機械学習と深層学習の違いは何ですか?
============================================================
💬 回答:
機械学習と深層学習の主な違いは、学習の階層構造と多層ニューラルネットワークへの
適用範囲です。
1. 階層構造: 機械学習では一般的に、各データセットの一部から全体を学習します。
しかし、深層学習では、より具体的な部分から全体を学びます。
深層学習のモデルは、階層的に異なるレベルでデータを処理します。
2. 多層ニューラルネットワーク: 深層学習の核心は、多層ニューラルネットワークを
使用することです。これは、データの特徴を次々に抽出・分類するような
層状の構造を持ち合わせています。
📚 参照ソース: ['深層学習', '深層学習', '深層学習']
============================================================
❓ 質問: 自然言語処理はどのような分野に応用されていますか?
============================================================
💬 回答:
自然言語処理は、人工知能と言語学の一部として広範囲に応用されています。
特に、機械翻訳や仮名漢字変換など、日常的な自然言語処理のための
アプリケーションが示されています。また、データベース内の情報を
自然言語に変換したり、自然言語の文章をより形式的な
(コンピュータが理解しやすい)表現に変換するなどの技術も含まれています。
📚 参照ソース: ['自然言語処理', '自然言語処理', '自然言語処理']
============================================================
❓ 質問: トランスフォーマーモデルの特徴を教えてください。
============================================================
💬 回答:
トランスフォーマーモデルは、2017年にGoogleの研究者等によって開発され、
主に自然言語処理(NLP)の分野で使用されています。その特徴としては、
自然言語などの時系列データを扱い、翻訳やテキスト要約といったタスクのために
設計されています。また、回帰型ニューラルネットワークと同様に
時系列データを逐次的に処理する必要がないという特徴があります。
そのため、トランスフォーマーでは回帰型ニューラルネットワークよりも
はるかに多くの並列化が可能になる一方で、トレーニング時間が短縮されます。
📚 参照ソース: ['Transformer (機械学習モデル)', 'Transformer (機械学習モデル)', ...]
Wikipedia記事の内容をもとに、それなりに正確な回答が返ってきています。参照ソースも表示されるので、どの記事を根拠にしているかがわかります。
Step 9: 【本題】RAGあり vs なし を比較する
ここが今回のクライマックスです。同じ質問にRAGあり・なしで答えさせて、違いを見てみます。
def answer_without_rag(question, model, tokenizer, max_new_tokens=256):
messages = [
{'role': 'system', 'content': 'あなたは日本語で回答する親切なアシスタントです。'},
{'role': 'user', 'content': question}
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer([text], return_tensors='pt').to(model.device)
with torch.no_grad():
output = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
temperature=0.7,
do_sample=True,
pad_token_id=tokenizer.eos_token_id,
)
generated = output[0][inputs['input_ids'].shape[1]:]
return tokenizer.decode(generated, skip_special_tokens=True)
compare_q = 'トランスフォーマーモデルの特徴を教えてください。'
print(f'質問: {compare_q}\n')
print('【RAGなし(モデルの素の回答)】')
print(answer_without_rag(compare_q, model, tokenizer))
print('\n【RAGあり(Wikipedia情報を参照した回答)】')
answer_rag, _ = rag_answer(compare_q, retriever, model, tokenizer)
print(answer_rag)
実行結果:
【RAGなし(モデルの素の回答)】
トランスフォーマーモデル(Transformers)は、人工知能技術の一種で、特に
自然言語処理(NLP)や機械学習において広く使用されています。
以下にトランスフォーマーモデルの主な特徴を説明します:
1. 非対称性:一般的には、トランスフォーマーモデルは非対称的な構造を持つ。
2. 深層的:モデルは複雑な構造を表現し、通常は深度が深い。
3. 偽想空間:トランスフォーマーでは、文の意味を表現する「仮想空間」が存在する。
4. 一致:モデルは特定の入力に対して一致する出力を生成する。
5. データ独立性:トランスフォーマーはデータの種類や品質に関係なく、同一の動作を行う。
...
【RAGあり(Wikipedia情報を参照した回答)】
トランスフォーマーは、2017年にGoogleの研究者等によって発表された深度学習モデルで、
主に自然言語処理(NLP)の分野で使用されます。特徴的な点として、自然言語などの
時系列データを扱うため、文頭から文末までの順に処理する必要がないことが挙げられます。
これにより、時系列データを逐次的に処理する必要がなくなり、
回帰型ニューラルネットワーク(RNN)と同様ですが、Transformerの場合、
より多くの並列化が可能となり、トレーニング時間が短縮されます。
差は歴然です!
| RAGなし | RAGあり | |
|---|---|---|
| 正確性 | 「偽想空間」「データ独立性」などでたらめ | Wikipediaに基づく正確な説明 |
| 具体性 | 曖昧で一般的 | 「2017年」「Google」など具体的 |
| 問題点 | ハルシネーション(嘘を自信満々に語る) | 参考情報に基づいた回答 |
1.5Bパラメータの小さなモデルでも、RAGで参考情報を渡すことで回答の品質が大きく改善されることがわかります。
まとめ
やったこと
- Wikipedia日本語記事(5トピック)を自動取得
- 400文字ごとにチャンク分割
-
multilingual-e5-smallで埋め込みベクトル化 → ChromaDBに保存 - 質問 → ベクトル検索 → 関連チャンク取得 → Qwen2.5-1.5Bが回答生成
すべてGoogle Colab無料枠で動作、外部APIキー不要です。
ファインチューニングとRAG、どっちを使う?
| やりたいこと | 向いている手法 |
|---|---|
| モデルの口調・出力形式を変えたい | ファインチューニング |
| 最新情報や社内文書を参照させたい | RAG |
| 専門用語を正確に使わせたい | 両方組み合わせる |
| とにかく手軽に試したい | RAG(今回の記事) |
発展:もっと良くするには
- チャンクサイズの調整: 400文字以外も試してみる
-
埋め込みモデルの変更: より大きなモデル(
multilingual-e5-large等)で精度向上 - リランキング: 検索結果をさらにスコアリングして精度を上げる
- PDF・社内文書の投入: Wikipediaの代わりに自分のドキュメントを使う
- ファインチューニング×RAG: 前回の記事と組み合わせて最強構成
