LoginSignup
12
3

watsonx.aiのLLMでLangChainとMilvusを使ってPDFの内容をQ&Aしてみた(=RAG)

Last updated at Posted at 2024-04-04

ついにwatsonx.dataでベクトル・データベースMilvusが使えるようになりました

この日のために学習しておいた(?)「watsonx.aiのLLMでLangChainを使ってPDFの内容をQ&Aをする」、こちらの内容をベクトルデーターベースとしてMilvusを使ってやってみたいと思います。

もちろんwatsonx.dataのMilvusでなくてもOKです。
参考:M1 Mac上でcolimaを使ってベクトルDB Milvusをインストール

ここで書いているコードはJupyter notebookとしてhttps://github.com/kyokonishito/notebooks/blob/main/watsonx-langchain-milvus-PDF.ipynb
からダウンロードできます。

LangChain+Milvus参考Document:

1. 前準備

1.1 watsonx.aiの準備

watsonx.aiのLLMでLangChainを使ってPDFの内容をQ&Aをする の「1. 前準備」」の準備は全て実施お願いします。

ここで、APIキーPROJECT ID の取得お願いします。

1.2 Milvusの準備

watsonx.dataのMilvusでもいいし、dockerのMilvusでもいいので、使えるMilvusを準備してください。
使用するMilvusの以下の情報を取得しておいてください:

  • ホスト名またはIPアドレス
  • ポート番号
  • ユーザーID(設定している場合)
  • パスワード(設定している場合)
  • SSL接続の場合はサーバーのCA証明書ファイル(pem)
  • (必要な場合) gRPC route
     
    SW版のwastonx.dataの必要情報はConnecting to Milvus service
    を参照してください。

2. ではwatsonx.aiのLLMでLangChainとMilvusを使ってPDFの内容をQ&Aにトライ

最初の方の手順はほぼほぼ
watsonx.aiのLLMでLangChainを使ってPDFの内容をQ&Aをする」の

  • 2-1. 必要なライブラリーの入手
  • 2-3.LangChainで使えるLLMの取得
    と同じです。まあベクトルデータベースが違うだけなので、LLM部分は同じですね。
    ただもう半年も経っているとライブラリも新しくなるし、新しいLLMも出たので、ちょっと変えてあります。
    またMilvusを扱うので、chromaの代わりにpymilvusライブララリが必要です。

今回も以下のURLでダウンロードできるIBM Database Dojoの資料「Dojo_Db2RESTAPI_20230727_配布用.pdf」のPDFの内容をもとに質問に回答する仕組みをLangChainを使用して構築してみます。いわゆるRAGという手法になります。

PDFダウンロードURL: https://files.speakerdeck.com/presentations/cc34f85fe9b5467d8782a41e5fa39b78/Dojo_Db2RESTAPI_20230727_%E9%85%8D%E5%B8%83%E7%94%A8.pdf

2-1. 必要なライブラリーの入手

以下は主要な必須ライブラリです:

  • ibm-watson-machine-learning
    • watson.aiのLLMを使用するためのライブラリです
    • LangChainフレームワークを使用するためにはv1.0.321以上が必要です
    • 常に新しいLLMが出てたりするので、できれば最新版を使用するにしましょう
    • 今回は1.0.352を使いました

  • langchain
    • 今回の主役、言語モデルを利用したアプリケーション開発のためのフレームワーク
    • 今回は0.1.14を使いました

必要なライプラリーをpipで導入しておきます。

pip install -U ibm-watson-machine-learning
pip install -U langchain
pip install pypdf
pip install pymilvus
pip install unstructured
pip install sentence_transformers
pip install flask-sqlalchemy 

2-2. (オプション)LangChainで使えるLLMの確認

以下のコードで使えるLLMを確認します。

from ibm_watson_machine_learning.foundation_models.utils.enums import ModelTypes

print([model.name for model in ModelTypes])

ibm-watson-machine-learning v1.0.352では以下の出力でした:

['FLAN_T5_XXL', 'FLAN_UL2', 'MT0_XXL', 'GPT_NEOX', 'MPT_7B_INSTRUCT2', 'STARCODER', 'LLAMA_2_70B_CHAT', 'LLAMA_2_13B_CHAT', 'GRANITE_13B_INSTRUCT', 'GRANITE_13B_CHAT', 'FLAN_T5_XL', 'GRANITE_13B_CHAT_V2', 'GRANITE_13B_INSTRUCT_V2', 'ELYZA_JAPANESE_LLAMA_2_7B_INSTRUCT', 'MIXTRAL_8X7B_INSTRUCT_V01_Q']

「この中で使用するLLMを1つ決めてください。」と前回は書きましたが、なぜか使えるのに出てこないLLMがあります。そのうち出てくるのかと思うのですが・・・。

今回はIBMが開発した日本語対応LLM「granite-8b-japanese」を使います。上記に出てきてないです(😢)
各LLMの説明はこちら:
https://www.ibm.com/docs/ja/watsonx-as-a-service?topic=solutions-supported-foundation-models

2-3.LangChainで使えるLLMの取得

まずはwatsonx.aiのAuthentication用のエンドポイントのURLを取得しておきます。Waston Machine Learningのインスタンスを作成してリージョンで決まります。
https://ibm.github.io/watson-machine-learning-sdk/setup_cloud.html#authentication より

今回は東京のWaston Machine Learningのインスタンスを使っているのでhttps://jp-tok.ml.cloud.ibm.comを使います。尚「granite-8b-japanese」は2024/4/2現在日本語データ・センターでのみ使用可能です。

その他には事前準備で取得した

  • APIキー
  • PROJECT ID

を使います。

<APIキー>, <PROJECT ID>は自分のIDに置き換えてください。
リージョンが東京ではない場合はhttps://jp-tok.ml.cloud.ibm.comを使用しているリージョンのエンドポイントに置き換えてください。

granite-8b-japanese」のmodel_idはibm/granite-8b-japaneseです。

from ibm_watson_machine_learning.foundation_models.utils.enums import ModelTypes
from ibm_watson_machine_learning.foundation_models import Model
from ibm_watson_machine_learning.metanames import GenTextParamsMetaNames as GenParams
from ibm_watson_machine_learning.foundation_models.extensions.langchain import WatsonxLLM

credentials = {
    "url": "https://jp-tok.ml.cloud.ibm.com", # watsonx.aiのAuthentication用のエンドポイントのURL
    "apikey": "<APIキー>"
}
project_id = "<PROJECT ID>"

# 使用するLLMのパラメータ
generate_params = {
    GenParams.MAX_NEW_TOKENS: 500,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1
}

# モデルの初期化
model = Model(
    model_id="ibm/granite-8b-japanese", #使用するLLM名
    credentials=credentials,
    params=generate_params,
    project_id=project_id
)

# LangChainで使うllm
custom_llm = WatsonxLLM(model=model)

参考までにもうこの状態でRAGなしLLMは使用できて、以下みたいな一般的な質問を入れてみることが可能です。

result=custom_llm.invoke("IBM Db2 on Cloudの特徴は?")
print(result)

出力(なんか最後切れましたが):

IBM Db2 on Cloudは、IBMが提供するクラウド上の関係データベースサービスです。以下は、IBM Db2 on Cloudの特徴です。
  1. スケーラビリティ: IBM Db2 on Cloudは、自動スケール機能を備えており、ワークロードの変動に対応するために、データベースのストレージやパフォーマンスを dynamical にスケールすることができます。
  2. 高可用性: IBM Db2 on Cloudは、高可用性を実現するために、自動的にバックアップやフェールオーバーを行い、データベースのダウンタイムを最小限に抑えます。
  3. 安全性: IBM Db2 on Cloudは、データの安全性を確保するために、暗号化、認証、および承認などの安全機能を提供します。
  4. 統合性: IBM Db2 on Cloudは、他のIBM Cloudサービスと統合されており、たとえば、IBM Cloud Storage、IBM Cloud Data Science Experience、IBM Cloud Analyticsなどとの統合が可能です。
  5. 単純な料金体系: IBM Db2 on Cloudは、単純な料金体系を採用しており、使用量に応じて課金されます。これにより、コストの効率化や予算の割り当てが可能になります。
  6. 専門的なサポート: IBM Db2 on

2-4. PDFLoaderの作成

PDFを読み込むためにPDFLoaderを作成します
./troublebook.pdfhttps://files.speakerdeck.com/presentations/cc34f85fe9b5467d8782a41e5fa39b78/Dojo_Db2RESTAPI_20230727_%E9%85%8D%E5%B8%83%E7%94%A8.pdf
からダウンロードしたPDFファイルのパスを指定してください。

from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader("./Dojo_Db2RESTAPI_20230727_配布用.pdf") #ダウンロードしたPDFを指定

2-5. PDF ドキュメントの内容をページで分割する

読み込むPDFはもともとパワポ資料で、1ページの文字数もさほど多くなく、ページで内容がまとまっているので、ページで分割して、ベクトルデーターベースに入れてみます。もしページの文字数が多い場合はさらに分割する検討をしてみてください。
以下でページ分割します。

pages = loader.load_and_split() 

2-6. embeddingsの取得

ここではオープンソースの多言語のテキスト埋め込み用のモデルであるMultilingual-E5-largeを使ってみました。

from langchain.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-base")

2-7. MilvusにデータをInsert

langchain_community.vectorstoresにはDocument形式のデータをベクトル化してInsertしてくれる機能from_documentsがあります。それを使用します。

Milvusへの接続情報をconnection_argsに辞書形式でセットします

設定するkeyはAPI Docを参照してください。

Local dockerのMilvusのconnection_args例(userid, password設定無し):

my_connection_args ={
 'host':'localhost', 
 'port':'19530'
}

SSL接続Milvusのconnection_args例(userid, password設定あり, SSL接続、server_nameあり):

my_connection_args ={
 'host':'xxxxxx.ibm.com', 
 'port':'35382',
 'user':'yyyyyyyy',
 'password':'zzzzzzzz',
 'server_pem_path':'/Users/nishito/presto.crt',
 'secure':True,
 'server_name':'watsonxdata'
}

Documentをベクトル化して保存 (Milvus.from_documents)

主なパラメータ: 詳細はAPI Docを参照

  • drop_old: True/False 現在のコレクションを削除するかどうか。デフォルトは False です。追記する場合はFalseにしてください。
  • collection_name: 使用するMilvus collection。デフォルトは "LangChainCollection" です。
from langchain_community.vectorstores import Milvus

vector_db = Milvus.from_documents(
    pages,
    embeddings,
    connection_args=my_connection_args,
    drop_old=True,
    collection_name = 'LangChainCollection'
)

collectionは存在しなければ作成されます。

Attuで見てみると39ページ分ベクトル化されてデータが入りました:
image.png
image.png
スキーマも自動で定義されています(自分で定義することも可能です)
image.png

尚、一度データベースに入れたので、次回そのデータを使う場合は、データベースに接続するのみでOKです。

Milvusへの接続(DBにInsert不要の場合)

from langchain_community.vectorstores import Milvus

vector_db = Milvus(
    embeddings,
    connection_args=my_connection_args,
    collection_name = 'LangChainCollection'
)

これでベクトル検索できる準備が整いました!

2-8. テキスト類似検索してみます

一旦、LLMを使わずテキスト類似検索してみます。

query = "IBM TechXchange Japanとは?"
docs = vector_db.similarity_search(query)

for doc in docs:
    print({"content": doc.page_content[0:100], "metadata": doc.metadata} )

出力

{'content': '⽇程: 2023年 10⽉ 31⽇(⽕)\n-11⽉ 1⽇(⽔)\n会場:ベルサール 東京⽇本橋\n主催:⽇本IBM 株式会社\n対象者: IBMのサービス/ 製品\nを使⽤または使⽤検討されてい\nる技術者の⽅', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 37, 'pk': 448802461356741735}}
{'content': 'github.com/kyokonishito\n2\ntwitter.com/KyokoNishito\nwww.linkedin.com/in/kyokonishitoqiita.com/nishiky', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 1, 'pk': 448802461356741699}}
{'content': '⽇本IBM Db2 & Database コミュニティ/ 各種イベントご紹介\nDb2 およびDB 各製品に 関連するコミュニティ活動および各種イベントをさらに強化して参 ります。\n最新の情報を鮮度⾼く', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 38, 'pk': 448802461356741736}}
{'content': '実際にやってみましょう!\nIBM Data & AI /  2023 IBM Corporation', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 16, 'pk': 448802461356741714}}

ページの内容で類似度が高い上位4つが入ります。
正直下位の方は関係あるのかな?って感じですね。パラメーターkで取得数は変更できます。

# 取得数をkで指定
docs = vector_db.similarity_search(query, k=1)

for doc in docs:
    print({"content": doc.page_content[0:100], "metadata": doc.metadata} )

出力

{'content': '⽇程: 2023年 10⽉ 31⽇(⽕)\n-11⽉ 1⽇(⽔)\n会場:ベルサール 東京⽇本橋\n主催:⽇本IBM 株式会社\n対象者: IBMのサービス/ 製品\nを使⽤または使⽤検討されてい\nる技術者の⽅', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 37, 'pk': 448802461356741735}}

数を絞るのではなくて、スコアで絞りたい場合はsimilarity_search_with_scoreを使います。返されるスコアはL2距離で0から1の値です。スコアは小さいほど(0に近いほど)類似度が高いです。

# スコアは小さいほど(0に近いほど)類似度が高いです。
query = "IBM TechXchange Japanとは?"
docs = vector_db.similarity_search_with_score(query)
for doc, score in docs:
    print({"score": score, "content": doc.page_content[0:100], "metadata": doc.metadata} )

出力

{'score': 0.22915227711200714, 'content': '⽇程: 2023年 10⽉ 31⽇(⽕)\n-11⽉ 1⽇(⽔)\n会場:ベルサール 東京⽇本橋\n主催:⽇本IBM 株式会社\n対象者: IBMのサービス/ 製品\nを使⽤または使⽤検討されてい\nる技術者の⽅', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 37, 'pk': 448802461356741735}}
{'score': 0.33195996284484863, 'content': 'github.com/kyokonishito\n2\ntwitter.com/KyokoNishito\nwww.linkedin.com/in/kyokonishitoqiita.com/nishiky', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 1, 'pk': 448802461356741699}}
{'score': 0.33645570278167725, 'content': '⽇本IBM Db2 & Database コミュニティ/ 各種イベントご紹介\nDb2 およびDB 各製品に 関連するコミュニティ活動および各種イベントをさらに強化して参 ります。\n最新の情報を鮮度⾼く', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 38, 'pk': 448802461356741736}}
{'score': 0.3771980404853821, 'content': '実際にやってみましょう!\nIBM Data & AI /  2023 IBM Corporation', 'metadata': {'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf', 'page': 16, 'pk': 448802461356741714}}

スコア0.3以上はイマイチ感ありますね。もし絞りたい場合はこのスコアで絞ってください。絞るためには自分でそのロジック書く必要があります。

2.9 watsonx.aiのLLMでLangChainとMilvusを使ってPDFの内容をQ&A

やっとお題にたどり着きました!
LangChainのRetrievalQAを使います。

まずはプロンプトなしでQ&A

from langchain.chains import RetrievalQA

retriever = vector_db.as_retriever()
qa = RetrievalQA.from_chain_type(llm=custom_llm, chain_type="stuff", retriever=retriever)
query = "IBM TechXchange Japanとは?" 
answer = qa.invoke(query)
print(answer['result'])

出力

IBM TechXchange Japanは、IBMの製品およびソリューションを軸に技術者同士が繋がり、学びや技術体験を共有できる技術学習イベントです。

ちゃんと文章で回答されました!

試しにPDFにない簡単な質問をしてみます:

query = "日本の首都は?" 
answer = qa.invoke(query)
print(answer['result'])

出力

東京

PDFに情報がなくとも学習済みの情報にあれば自力で回答してしまうようです。
watsonx.aiのLLMでLangChainを使ってPDFの内容をQ&Aをする」では読み込んだPDFの情報のみで回答して欲しかったので、retriever作成の際にsearch_type="similarity_score_threshold"
search_kwargs={'score_threshold': 0.5}
を指定して精度の低い情報は除外するretrieverを作成しました(参考:2-6. retrieverの作成)。
langchain_community.vectorstores.milvus.Milvusにはこの機能がまだ実装されてなく、指定すると検索実施の際にエラーになってしまいます。
API Docには書いてあるのですが、使えません。そのうち使えるようになるのかも。

今のところベクトルDBの内容のみで回答したい場合は、前に説明したsimilarity_search_with_scoreを使い自力でやってみてください。

プロンプトを作ってQ&A

ベクトルDBの内容のみで回答したい場合、プロンプトで指示すればよいのかもしれません。
プロンプトにその旨指示を入れてみます。

RAG chainの作成 

前回のでRetrievalQA.from_chain_typeからやり方を変えてみました。
参考:

from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate

template = """以下のcontextのみを利用して、ですます調で丁寧に回答してください。contextと質問が関連していない場合は、「不明です。」と回答お願いします。
context: {context}
質問: {question}
回答:"""

rag_prompt = PromptTemplate.from_template(template)

rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt
    | custom_llm
)

質問してみます

PDFにないものを質問

print(rag_chain.invoke("日本の首都は?"))

出力:

東京

正しいですが、「contextと質問が関連していない場合は、「不明です。」と回答お願いします。」と指示したプロンプト無視で回答してしまってますね。

PDFにある質問をしてみます。

print(rag_chain.invoke("IBM TechXchange Japanとは?"))

出力:

 IBM TechXchange Japanは、IBMの製品とソリューションを軸に技術者同士が繋がり、学びや技術体験を共有できる技術学習イベントである。

「ですます調で丁寧に回答してください。」って指示したのですが、プロンプトなしの時は「です」で終わってたのですが、語尾が「である」変わりました。

プロンプトちょっと変えてみます

rom langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import PromptTemplate

template2 = """<|システム|>
あなたはIBMが開発したAI言語モデル、Granite Chatです。あなたは慎重なアシスタントです。あなたは慎重に指示に従います。あなたは親切で無害で、倫理的なガイドラインに従い、前向きな行動ができます。
<|ユーザー|>
あなたは、特別なRetrieval Augmented Generation(RAG)アシスタントとして機能するように設計されたAI言語モデルです。応答を生成するときは、正しさを優先します。つまり、文脈とユーザーのクエリが与えられたときに応答が正しく、文脈に根拠があることを確認します。さらに、レスポンスが与えられたドキュメントまたはコンテキストによってサポートされていることを確認してください。コンテキストやドキュメントを使用して質問に答えることができない場合、次のレスポンスを出力します: 'わかりません。' あなたの回答が質問に関連していることを常に確認してください。説明が必要な場合は、まず説明や理由を述べ、それから最終的な答えを述べてください。
[文書]
{context}
[終了]
{question}
<|アシスタント|>"""

rag_prompt = PromptTemplate.from_template(template2)

rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt
    | custom_llm
)
print(rag_chain.invoke("IBM TechXchange Japanとは?"))

出力:

IBM TechXchange Japanは、IBMの製品やソリューションを軸に技術者同士が繋がり、学びや技術体験を共有できるイベントです。

IBM TechXchange Japan 2023は、10月31日(火)と11月1日(水)の2日間にわたってベルサール東京日本橋で開催されます。

本イベントでは、IBMの製品やソリューションに関する技術ブレイクアウトセッション、製品デモンストレーション、ハンズオン、ネットワーキングなど、技術者にとって魅力的なプログラムを提供します。

また、本イベントは技術者向けのイベントですが、IBMの製品やソリューションに興味のある方、IBMの技術者と交流したい方など、どなたでもご参加いただけます。

IBM TechXchange Japan 2023に参加して、最新のテクノロジーやベストプラクティスについて学び、IBMの技術者とつながり、貴重な経験を得ましょう。

なんか回答が長くなりました。

PDFにない質問もしてみます。

print(rag_chain.invoke("日本の首都は?"))

出力:

東京です。

やっぱり自力で回答してしまいますね。でも「です」がついて丁寧になりました。

プロンプトの書き方はちょっと時間をかけて調整が必要そうです。

まとめ

Milvus+LangChainはまだまだ情報が不足していて調べるのに時間がかかりました。
結局以下の公式のドキュメントに書かれているのがまずは全てっぽいです。

API Doc langchain_community.vectorstores.milvus.Milvus に書かれていても使えないMethodとかありましたので、まだ不完全なのかもしれません。

ただLangChainを使って簡単にMilvusでのRAGパターンを書けるのは事実なので、ぜひ使ってみてください。ついでにwatsonx.dataのMilvusもぜひお試しください!

ここで書いているコードはJupyter notebookとしてhttps://github.com/kyokonishito/notebooks/blob/main/watsonx-langchain-milvus-PDF.ipynb
からダウンロードできますので、とりあえず実行したい方は使ってみてください。

参考: Milvus関連投稿

12
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
3