💬

今更ながら生成AIの概念を理解してみる

2024/09/01に公開

目的・背景

最近、AI系のセミナーに行くことが多くなったのですが、コロナ前と違ってAIといっても生成AIが当たり前になってきていますね。

ChatGPTなどのツールを使って仕事をするとかは当たり前になってきていますし、その考え方を使って独自のChatBotを作ろうというのも、普通に行われています。
私も、GPT-4が出た直後は、仕事の関係で色々と触れていたのですが、その後はフリーランスとなり別の仕事をすることが多かったので、言葉や概念をしっかりと理解しきれていませんでした。

ただ、流石にそろそろ学ばないとヤバいと思い始めて、セミナーに行ったり本を読んだりしています。

そこで、よく聞く言葉が「RAG」です。
社内のデータを用いて生成AI(そこからのChatBotなど)をつくれますよ、という概念はわかるのですが、いまいち定義が曖昧でした。

そこで今回は、それらの定義や関係するライブラリ・ソリューションについて整理してみたいと思います。

私なりの理解を書いていますので、間違っている点があればぜひご指摘ください。

RAG(Retrieval-augmented generation)という概念

まず、RAG(Retrieval-augmented generation)について、理解した限りの内容からイメージをまとめてみました。

超シンプルですが、そんなにズレてはいないと思います。
これを見ると、普通にリレーショナル データベース管理システム(RDBMS)ですね。

ユーザーの部分は、SQLを投げるためのインターフェイス。LLMがSQLを作成しデータベースに問い合わせる。これだけ聞くととてもシンプルです。

このデータベースを用意することで、「社内のデータを用いてChatBotができる」といったことが可能になっています。

しかし、実際に中で使われている技術は、既存のRDMBSとは大きく異なるのでしょう。
自然言語という非定型なものをもとに、LLMが問合せ内容を理解して、それをDBから探し出す。
さらに探し出した内容を、また自然言語として違和感がない形で回答を返す。

そういう仕組みがRAGの中にはありそうです。

RAGを構成するものたち

そのRAGを構成するものについて、少し深掘りをしてみます。

LLM(Large Language Model)

まずは、LLMについてです。
これは、皆さんご存じのChatGPTの裏側である、GPT-4(4V, 4o)や、Gemini, ClaudeといったAPI型のクローズドソースで有償(一部無償)のものから、Elyza LLM, Fugaku-LLMなど無料で利用できるオープンソースとして公開されているものまで多数あります。

また、モデルの役割としても、「テキスト生成のモデル」「埋め込みモデル」といった種類があります。(BERTのような「入力テキスト処理のモデル」や、マルチモーダルなモデルもありますが、今回は対象外)

例えば、GPTであれば「テキスト生成のモデル」が「GPT-4o」「GPT-4o mini」のような一般的によく聞く名称のモデルです。一方「埋め込みモデル」は、「text-embedding-3-large」といわれるようなものです。

「埋め込みモデル」、名称は地味ですが、とても重要な役割を持ちます。
それは自然言語をベクトル化すること。ベクトル化については、Word2Vec以降様々な方法でなされてきましたが、それをLLMで使いやすいようにベクトル化してくれるためのモデルということのようです。

ベクトルDB

そのベクトル化したデータを格納し、LLMによって問合せされるベクトルDBにも、様々な種類があるようです。

ちょっと調べても多数あって、調べきれなかったのですが、大きくメモリDBとクラウド型DBに分かれるように理解しました。

メモリ型で有名なものは、「FAISS」「Chromadb」など。
Pythonのプログラムで、普通にPip Installしたら使えて、ベクトル化したデータを格納したDBをローカルに出力することができます。
私なりの理解では、テーブルデータを格納するduckdbのようなイメージです。

一方のクラウド型は、「Pinecone」「Weaviate」などが有名らしいです。
こちらは、別途ユーザ登録をしてクラウド上にベクトルDBを構築する。それに対して問い合わせをするという使い方のようです。
私なりの理解では、BigQueryやSnowflakeでしょうか。

フレームワーク

このLLMやベクトルDBの操作を一々つくったり、制御したりとは結構大変ですね。
それをまとめて対応してくれるフレームワークがあるようです。

その代表的なものが、次の2つです。

いくつかの記事を見る限りは、LangChainがトップシェア・LlamaIndexがセカンドシェアと競っているようですね。

実際にどちらも使ってみましたが、プログラム開発はどちらも大きな差がなく簡単に使えました。
機能面でももっと高度な開発をしていこうとすると、差を実感するのかもしれませんが、今回の基礎レベルでは、そこまで差はないようでした。

このフレームワークが持つ役割ですが、単にLLMからベクトルDBへ問い合わせをする部分だけでなく、DBを作るための元データをロードしてくる部分なども担ってくれるようです。

実際に作ってみた

自分なりの学習まとめもかねて、実際に、Google Colabで作ってみました。

LLMは、今回はGoogle Colabで動かす前提なので、ハイスペックでなくても動く、API型のGPT-4o(その中でも費用が格安な、gpt-4o-mini)を使います。

データベースは、まだ差がわかるほどの知識がないので、Chromadbを使っています。

フレームワークは、こちらもあまり開発難易度に差がなさそうだったので、LangChainを使います。

扱うデータですが、これはせっかくなら自分なりのデータをDBに入れたいと思い、Notionの記事でまとめている、Snowflakeの資格取得についての複数の記事を対象にしたいと思います。

まずは初期設定

ライブラリをインストールします。
Colabの標準でないライブラリが多いため、通常のデータ分析よりも色々とインストールが必要ですね。

!pip install langchain langchain_community langchain_openai openai chromadb

データベースを永続化するために、Google Driveを事前にマウントしておきます。

# Google Drive マウント

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

また、GPTモデルを使うための設定もしてしまいましょう。
OpenAIのAPI KeyはColabの環境変数に入れているので、それを取得しています。

from google.colab import userdata
from openai import OpenAI
import os

client = OpenAI(api_key=userdata.get('OPENAI_API_KEY')) # Colabの環境変数から取得
model="gpt-4o-mini"

元データの読み込み

Notionの記事は、エクスポートして「notion_data.zip」として保存し、Colabのローカルにアップロードしています。

これを、読み込ませてみます。

import shutil
from langchain.document_loaders import NotionDirectoryLoader
shutil.unpack_archive("notion_data.zip", "notion_data")
loader = NotionDirectoryLoader("notion_data")
docs = loader.load()

読み込ませたテキストを、chunk_sizeで分割しています。

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(docs)

こういうことが、簡単に作れるフレームワークは優秀ですね。
さらに、NotionDirectoryLoaderというモジュールがあることがすごいです。他にもPDFを読み込んだり色々モジュールがあるようです。

ベクトルDBを作成

さて、テキストをベクトルDBに入れてしまいましょう。
embeddingsというところで、前述の埋め込みモデルを使っています。

dbを作る際の設定ですが、非常に簡単です。
「documents」で分割したtextsを指定、embeddingで埋め込みモデルを指定。
「persist_directory」にマウントしたGoogleDriveを指定することで、Colabを終了してもベクトルDBが残るようにします。

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

embeddings = OpenAIEmbeddings(openai_api_key=userdata.get('OPENAI_API_KEY'))

# GoogleDriveにデータベースを保存する
db = Chroma.from_documents(documents=texts, embedding=embeddings, persist_directory="/content/drive/MyDrive/Colab Notebooks/ChromaDB")

このDBを永続化することで、ベクトルDBを上記で指定したPathにバックアップが完了します。

db.persist()

動作確認をしてみましょう。

db.as_retriever().get_relevant_documents("SQLを最適化するにはどうするか?")

これで、関係するtextを見つけてきてくれます。

ちなみに、バックしたDBを読み込むときには、次のようにするだけです。こちらも簡単。

# バックアップを読み込み
new_db = Chroma(embedding_function=embeddings, persist_directory="/content/drive/MyDrive/Colab Notebooks/ChromaDB")

エージェント作成

では、このLLM + ベクトルDBに問い合わせをするためのエージェントを作りたいと思います。
ここで指定している「temperature」は、ランダム性を持たせるかを決めるもので、0に近いと確率が高いもの、1に近いと確率分布したがってランダムにデータを取得するらしいです。

from langchain.chat_models import ChatOpenAI
from langchain.chains.conversation.memory import ConversationBufferWindowMemory
from langchain.chains import RetrievalQA

llm = ChatOpenAI(
  openai_api_key = userdata.get('OPENAI_API_KEY'),
  model_name = model,
  temperature = 0.5
)
qa = RetrievalQA.from_chain_type(
  llm=llm,
  chain_type="stuff",
  retriever=db.as_retriever(search_kwargs={"k": 10})
)

さあ、実際に問い合わせをしてみましょう。

qa.run("SQLを最適化するにはどうするか?")

回答は次のようになりました。

SQLを最適化するためには、次の順番でチューニングを行うことが推奨されています。

1. **Rows**: 
   - From, Join, Where句を使って、取得する行数を減らす。

2. **Group**: 
   - Group by, Having句を使用して、集計処理を行う。

3. **Select**: 
   - Select, Distinct, Order by, Limit句を使用して、必要なデータだけを選択する。

また、無駄な処理を排除し、Order byは最後に1回だけ行うようにすることが重要です。

「SQLを最適化する」というだけだと、一般論としてインデックスの使用や、データベースの設計といった話になりますが、今回Notionから持ってきたSnowflake試験用の記事には、クエリの最適化のみをまとめていましたので、想定通りの結果が返ってきました。

さらに、会話の履歴を残しながら、ChatBotのようにできるエージェント化をしましょう。

toolsというところに、qa.runの関数を渡して、それをinitialize_agentの引数にするようです。
conversational_memoryという変数で、会話の記録を残して、

from langchain.agents import Tool
from langchain.agents import initialize_agent
from langchain.agents import AgentType

tools = [
  Tool(
    name='SnowProCoreExam資料',
    func=qa.run,
    description=(
      'Snowflakeの資格取得に関する資料'
    )
  )
]

conversational_memory = ConversationBufferWindowMemory(
  memory_key='chat_history',
  k=10,
  return_messages=True
)

agent = initialize_agent(
    agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
    tools=tools,
    llm=llm,
    verbose=True,
    max_iterations=3,
    early_stopping_method='generate',
    memory=conversational_memory
)

このエージェントを使って会話をしてみましょう。

agent("私はAIに関して、新たな会社を起業しようとしています。相談相手になってください。")

回答は次の通り。

> Finished chain.
{'input': '私はAIに関して、新たな会社を起業しようとしています。相談相手になってください。',
 'chat_history': [],
 'output': 'もちろん、AIに関する新しい会社を起業することについて相談します。具体的にどのようなアイデアやビジョンをお持ちですか?また、特にどの分野や市場に焦点を当てたいと考えていますか?'}

上記は一般的な回答なので、そこに対してSnowflakeというワードを入れて会話を続けます。

agent("Snowflakeをビジネスに使うにはどうすればいい?")

回答は次の通り。

> Finished chain.
{'input': 'Snowflakeをビジネスに使うにはどうすればいい?',
 'chat_history': [HumanMessage(content='私はAIに関して、新たな会社を起業しようとしています。相談相手になってください。'),
  AIMessage(content='もちろん、AIに関する新しい会社を起業することについて相談します。具体的にどのようなアイデアやビジョンをお持ちですか?また、特にどの分野や市場に焦点を当てたいと考えていますか?')],
 'output': 'Snowflakeをビジネスに活用する方法として、データ分析とビジュアライゼーション、データサイエンスと機械学習、データ共有、データ統合、セキュリティとコンプライアンスが挙げられます。これらの機能を活用することで、企業はデータドリブンな意思決定を行い、競争力を高めることができます。具体的な活用方法は企業のニーズや業種によって異なるため、個別のケースに応じた戦略を検討することが重要です。'}

すると、chat_historyとして先の会話も参照にしたうえで、回答が返ってきていることを確認できました。

まとめ

ということで、長めの記事になってしまいましたが、LLMやらRAGやらというワードと、それについてのライブラリが色々あってよく分からない状態だった、私の理解を正しくするために、整理してみました。

実際に作ってみると、フレームワークの優秀さから、非常に簡単に作れることがわかりました。

ぜひ、より様々なデータをもとに、自分なりのエージェントを作っていくということをしていきたいですね。

参考ホームページ

Discussion