（コピペだけ！）今更GoogleColab上にRAGを実装する

# はじめに

本記事では、タイトル通りGoogleColab上に私が実装した[RAG](https://aws.amazon.com/jp/what-is/retrieval-augmented-generation/)のソースコードを解説を少し交えつつ公開していく記事になります。

用いるライブラリやツールは、一部ログインが必要となりますが、無料で利用できるように実装しておりますのでご安心して貼り付けてください！

今回は大分県の特産品や食べ物について詳しく説明・おすすめしてくれるAIの作成に向けて行った作業になります。

## 参考にしたサイト一覧
- RAGの実装

https://qiita.com/tinymouse/items/4d359674f6b2494bb22d

- 大分県の情報

https://www.visit-oita.jp/gourmets/#genre01

## 実装

まずはお決まりのマウントです

```python
from google.colab import drive
drive.mount('/content/drive')
```

### 必要なライブラリのインストール・インポート

次に今回の実装にあたって必要なライブラリたちを準備します。
少し時間がかかりますので堪えてください！

[FunctionCalling](https://zenn.dev/kazuwombat/articles/1f39f003298028)用など、必要のないライブラリも存在しますのでむやみやたら実行したくない方は必要に応じて選別してください。

> 私が実行するとランタイムの再起動を促されました。
> 何も気にせず再起動してもう一度実行すると問題なくインストールできます。

```python
%pip install --upgrade google-generativeai
%pip install -U -q google-generativeai
%pip install transformers accelerate sentencepiece
%pip install bitsandbytes
%pip install unstructured
%pip install selenium
%pip install sentence-transformers
%pip install chromadb
%pip install langchain
%pip install langchain-community
%pip install langchain-google-genai
%pip install sentence-transformers
%pip install langchain langchain_community
%pip install docling
%pip install faiss-cpu
%pip install unstructured
%pip install langchain-google-genai
%pip install langchain-experimental

import pathlib
import time
import json
import sys
import os
import langchain
import transformers
import torch
import bitsandbytes
import google.generativeai as genai
from google.colab import userdata
import langchain.llms
import langchain.prompts
import langchain.text_splitter
import langchain.embeddings
import langchain.vectorstores
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_experimental.text_splitter import SemanticChunker
import langchain_community.document_loaders
from langchain_community.vectorstores.faiss import FAISS
from langchain_core.prompts import PromptTemplate
from langchain_community.embeddings.huggingface import HuggingFaceEmbeddings
from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.chains import RetrievalQA
from docling.document_converter import DocumentConverter
```

### APIkeyの準備

[Google studio](https://ai.google.dev/aistudio?hl=ja)より、APIキーを取得してcolabにインポートします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3818832/aa348fc7-c7f9-85f0-9e9f-108844a98573.png)

インポート出来たら以下のコードを実行してください

```python
# APIキーの初期化
genai.configure(api_key=userdata.get('GOOGLE_API_KEY'))
os.environ["GOOGLE_API_KEY"] = userdata.get("GOOGLE_API_KEY")
```

### モデルの初期化

本記事ではgeminiの無料利用枠を利用しますが、他のモデルを利用されたい方は以下を参考にしてみてください。

https://ai.google.dev/gemini-api/docs/models/gemini?hl=ja&_gl=1*1sjo1r8*_up*MQ..*_ga*MTgzODQ5MzQ0Mi4xNzMzNzg3NjU4*_ga_P1DBVKWT6V*MTczMzc4NzY1OC4xLjAuMTczMzc4NzY1OC4wLjAuNTc4NDcxNzI3

今回の実装ではモデルを2つもちいて実装していますが良い感じにコードを書けば一つでも実装可能だと思うので頑張ってみてください！

※2つ用いてもRAGには影響がないようにしてあります。

```python
# モデルを準備
# "gemini-1.5-flash-8b-lates"
model = genai.GenerativeModel('models/gemini-1.5-flash-latest')
llm = ChatGoogleGenerativeAI(model="models/gemini-1.5-flash-latest", temperature=0.5,api_key=userdata.get('GOOGLE_API_KEY'))
```

## 処理の流れ

さて、いよいよRAGの中身の実装に入っていきますが最初にこの処理の流れについて紹介しておきます。
この図は一般的なRAGの処理フローです。
[引用元](https://www.fronteo.com/ai-learning/about-rag)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3818832/83551199-582a-27a9-47cb-daafc3eb80cb.png)


今回の実装ではデータベースへの問い合わせを行う②の情報量を上げるために別のモデルにプロンプトから重要なキーワードのみを抜き出してデータベースを検索するような流れとなっています。

### 食べ物の特徴を抽出する

最初にプロンプトから食べ物の特量を抽出するコードを記述します。

```python
def get_gourmets_data(prompt):
    content = f'''
        <rule>
        あなたは文章から食べ物についての特徴を単語で答えるするAIです。
        <example>にはhumanにより入力された場合に出力すべき文字をassistantにより示している。
        <example>に示した形式に従い、余計な文言を付け足すな。
        </rule>
        <item>
        {prompt}
        </item>
        <example>
        human: あなたは今、大分県の魚を使った食べ物を探しています。3つおいしい新鮮な食べ物を紹介してください
        assistant: [魚・新鮮]
        </example>
        '''
    response = model.generate_content(content)
    # 無料版のリクエスト制限回避
    time.sleep(4)
    response = response.text.replace("\n", "").rstrip()

    print("reselt:", response)
    return response
```

### RAGによる関連情報の検索

次に先ほど抽出した特徴を用いて検索を行います。
今回の実装ではウェブサイト内のテキストをすべて抜き出した後、適当な文字数ごとに分割して検索される言葉の意味に近いブロックを上から10ブロックを保存します。

```python
def get_gourmets_info(content:str):
    #プロンプトから食べ物に関する情報のみを取得する
    content = get_gourmets_data(content)


    gourmets_urls = [
        'https://www.visit-oita.jp/gourmets/#genre01',
    ]

    # ウェブページの内容を読込む
    loader = langchain_community.document_loaders.UnstructuredURLLoader(
        urls=gourmets_urls
    )
    gourmets_docs = loader.load()

    # 読込した内容を分割する（再帰的に分割）
    text_splitter = langchain.text_splitter.RecursiveCharacterTextSplitter(
        chunk_size=50,
        chunk_overlap=10,
    )
    # ベクトル化する準備
    gourmets_docs = text_splitter.split_documents(gourmets_docs)

    embedding = langchain.embeddings.HuggingFaceEmbeddings(
        model_name="intfloat/multilingual-e5-base"
    )
    # 読込した内容を保存
    gourmets_vectorstore_1 = langchain.vectorstores.Chroma.from_documents(
        documents=gourmets_docs,
        embedding=embedding
    )

    query = content

    gourmets_docs = gourmets_vectorstore_1.similarity_search(query=query, k=10)

    for index, doc in enumerate(gourmets_docs):
        print("%d:" % (index + 1))
        print(doc.page_content)
    return gourmets_vectorstore_1
```

### 生成AIによる文章作成

最後に、抽出してきたデータをもとに本来のプロンプトに対する回答となる文章をモデルに考えさせます

```python
def get_food(query:str):
    # プロンプトを準備
    template = """
    <bos><start_of_turn>system
    次の文脈を使用して、最後の質問に答えてください。
    {context}
    <end_of_turn><start_of_turn>user
    {query}
    <end_of_turn><start_of_turn>model
    """
    prompt = langchain.prompts.PromptTemplate.from_template(template)

    # チェーンを準備
    chain = (
        prompt
        | llm
    )
    # 検索する
    gourmets_vectorstore_1 = get_gourmets_info(query)
    docs = gourmets_vectorstore_1.similarity_search(query=query, k=10)

    content = "\n".join([f"Content:\n{doc.page_content}" for doc in docs])
    response = chain.invoke({
        'query': query,
        'context': content,
    })

    time.sleep(4)
    print("------------------------------------------------------------------")
    # print(response.content)

    return response.content
```

### 作成したプログラムを呼び出す

ここでゴールです。
本来知りたかった大分県の食べ物について質問してみます。

```python
if __name__ == "__main__":
  query = "あなたは今、大分市の魚を使った食べ物を探しています。おいしい新鮮な食べ物を3つ紹介してください。"
  question_info = get_food(query)
  print(question_info)
  ```

  実行結果
  ```
  1. **かぼすブリを使った料理:** テキストで紹介されているかぼすを餌に育てたブリは、大分県産で抗酸化作用により鮮度が保たれているとあります。大分市内の魚料理店や、スーパーマーケットなどでこのブリを使った刺身、照り焼き、しゃぶしゃぶなどを探してみるのが良いでしょう。  新鮮なブリの旨味とかぼすの爽やかな酸味の組み合わせは絶品です。

2. **大分市近海で獲れた旬の魚料理:** 大分市は海に面しているため、地元で獲れた新鮮な魚介類を使った料理が豊富です。  地元の市場や魚料理専門店で、その日の旬の魚を使った料理を尋ねてみましょう。  刺身、寿司、煮付けなど、様々な調理法で楽しめます。

3. **豊後料理に含まれる魚料理:** テキストに「豊後料理」が登場します。これは大分の郷土料理をアレンジした料理で、地元の食材をふんだんに使用している可能性が高いです。豊後料理を提供するレストランを探し、メニューに含まれる魚料理を試してみるのも良いでしょう。

これらの提案は、テキストの情報と一般的な知識に基づいた推測です。  より具体的な情報を得るには、大分市の観光情報サイトやグルメサイトなどを参照することをお勧めします。

```

## 参考：素のGeminiに同じ質問をしてみた結果

RAGの効果を感覚でもいいので確かめるため、同じモデルに同じ質問をしてみました。

```
大分市で新鮮な魚料理を楽しめるお店を3つご紹介します。

田嶋鮮魚 は、地元で獲れた新鮮な魚介類を豊富に取り揃えています。刺身や焼き魚、煮魚など、さまざまな料理を楽しめます。
久野（ひさの）鮮魚店 は、創業100年以上の老舗鮮魚店です。新鮮な魚介類はもちろん、加工品や惣菜も販売しています。
魚勝 は、大分市都町にある海鮮居酒屋です。新鮮な魚介類を使った刺身や焼き魚、鍋料理などが楽しめます。
これらの店舗は、いずれも地元で愛されているお店です。ぜひ足を運んで、新鮮な魚料理を味わってみてください。
```

RAGとは全然違う結果になりましたね！

# さいごに

実装しつつ大分県はもともと魚介系が有名だから普通に質問してもある程度のことは答えてくるよな...と不安になりながら実装しましたが、目的通り応答に差がみられる結果でよかったです！

これからも面白そうな技術を見つけたら少しづつ試していこうと思います。