watsonx.aiのLLMでLangChainを使ってPDFの内容をQ&Aをする

今更ながら生成系aiもやってみたくなったので、IBMの生成系aiサービス、watsonx.aiをpython+[LangChain](https://python.langchain.com/docs/get_started/introduction)で使ってみます。

尚、最初にお断りしておきますが、初心者が適当に各種ドキュメントを見て作った「やってみた」系の投稿ですので、この使い方を推奨してるというものではありません。

[2023年9月7日にLangChainフレームワークをwatsonx.aiのfoundationモデルで使用できるようになりました。](https://www.ibm.com/docs/en/watsonx-as-a-service?topic=overview-whats-new#langchain-extension-for-the-foundation-models-python-library)これによってちまたに溢れるChatGPTを使った[LangChain](https://python.langchain.com/docs/get_started/introduction)でのサンプルをちょこっと変えるだけで、watsonx.aiでも使えるようになります。

LangChainはwikipediaでは以下のように説明されています：
出典：　https://en.wikipedia.org/wiki/LangChain
> LangChain is a framework designed to simplify the creation of applications using large language models (LLMs). As a language model integration framework, LangChain's use-cases largely overlap with those of language models in general, including document analysis and summarization, chatbots, and code analysis.

翻訳すると:
> LangChainは、大規模言語モデル（LLM）を使ったアプリケーションの作成を簡素化するために設計されたフレームワークである。言語モデル統合フレームワークとして、LangChainのユースケースは、文書解析や要約、チャットボット、コード解析など、言語モデル全般のユースケースとほぼ重なっている。


ということで簡単にLLMを使ってアプリケーションの作成できるとのことで、こちらを使います。

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

# 1. 前準備
watsonx.aiの言語モデルを呼び出すために必要な情報を取得する手順はとてつもなく長いです。さくっと「APIKEYはこちら」とはいかないです。

必要なのはAPIKEYとPROJECT IDです。この2つの情報を取得する方法は親切な皆さまがQiitaにかかれているので、そちらを参照していただくことにします。

## 1-1. Waston Studio & Machine Learning 作成　+ 　wastonx.aiを使う設定
以下を参照して設定しましょう:

[0からのIBM watsonx.aiのはじめかた](https://qiita.com/ironikot/items/a1ff1423c5c2e7c44876)

## 1-2. APIキーの取得
[IBM Cloud APIKEYの作成(取得)方法](https://qiita.com/nishikyon/items/fa8b951267efbe911fe7)
(これは私の投稿でした！)

そのほかにサービスIDを作成して、権限をつけて、そのAPIKEYを使う、という方法もありますが、今回は上記のみの説明とさせていただきます。

## 1-3. PROJECT IDの取得
プロンプト・ラボからはモデル名の右側にある`</>`をクリックし、curlコマンドを表示させると、`"project_id"`の値がありますので、そこからコピペします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/10683b99-ca4f-bae3-14e7-e913415c071f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/68f8700e-7fab-5ef1-abdd-703168ce6b39.png)

または正統派の方法は、プロンプト・ラボ上部のプロジェクト名のリンクをクリックし、プロジェクトを表示し、「管理」タブから、`プロジェクトID`を取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/fcce78e7-50ba-7620-daca-20d913b8818d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/404f41d1-4b18-8b96-bd09-1a3dd3f9656f.png)

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

今回は以下のURLでダウンロードできる[IBM Database Dojoの資料「Dojo_Db2RESTAPI_20230727_配布用.pdf」](https://speakerdeck.com/kyokonishito/20230727-db2-rest-api)のPDFの内容をもとに質問に回答する仕組みをLangChainを使用して構築してみます。いわゆる[RAG](https://www.ibm.com/blogs/solutions/jp-ja/retrieval-augmented-generation-rag/)という手法になります。

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

内部ステップとしては本来は
1. PDFの内容を読み出して、テキストデータにする
1. テキストを適当な塊で分割して(ページや文章区切りなど),Vector化してVector Storeに入れる。Vector化の際にはEmbeddingモデルを使う。
1. 質問を入力し、質問に類似した内容をVector Storeで検索。
1. 検索結果の上位の情報を使用してLLMモデルがいい感じな回答を作って出力

下記はLangChainのドキュメント[Vector stores](https://python.langchain.com/docs/modules/data_connection/vectorstores/)内の図です。

![image.png](https://python.langchain.com/assets/images/vector_stores-9dc1ecb68c4cb446df110764c9cc07e0.jpg)
(https://python.langchain.com/docs/modules/data_connection/vectorstores/ より引用）

LangChainでは`PyPDFLoader`,`VectorstoreIndexCreator`,`RetrievalQA`というクラスを使って簡単に上記の実装が可能です

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

以下は主要な必須ライブラリです：
- [ibm-watson-machine-learning](https://pypi.org/project/ibm-watson-machine-learning/)
    - watson.aiのLLMを使用するためのライブラリです
    - LangChainフレームワークを使用するためにはv1.0.321以上が必要です
    - 常に新しいLLMが出てたりするので、できれば最新版を使用するにしましょう

- [langchain](https://python.langchain.com/docs/get_started/introduction)
    - 今回の主役、言語モデルを利用したアプリケーション開発のためのフレームワーク

- [chromadb](https://www.trychroma.com/)
    - OSSのVector DB
    - LangChainのサンプルコードでは大抵これを使っているが、他も使用可能


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

```py
pip install -U ibm-watson-machine-learning
pip install langchain
pip install pypdf
pip install chromadb
pip install unstructured
pip install sentence_transformers
pip install flask-sqlalchemy 
```

## 2-2. (オプション)LangChainで使えるLLMの確認
以下のコードで使えるLLMを確認します。
```py
from ibm_watson_machine_learning.foundation_models.utils.enums import ModelTypes

print([model.name for model in ModelTypes])
```
ibm-watson-machine-learning v1.0.326では以下の出力でした：
<table>
  <tbody>
    <tr>
      <td>['FLAN_T5_XXL', 'FLAN_UL2', 'MT0_XXL', 'GPT_NEOX', 'MPT_7B_INSTRUCT2', 'STARCODER', 'LLAMA_2_70B_CHAT', 'GRANITE_13B_INSTRUCT', 'GRANITE_13B_CHAT']
      </td>
    </tr>
  </tbody>
</table>

この中で使用するLLMを1つ決めてくだい。
今回は`LLAMA_2_70B_CHAT`を使います。
各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　より

- Dallas: https://us-south.ml.cloud.ibm.com

- London: https://eu-gb.ml.cloud.ibm.com

- Frankfurt: https://eu-de.ml.cloud.ibm.com

- Tokyo: https://jp-tok.ml.cloud.ibm.com

今回はダラスのWaston Machine Learningのインスタンスを使っているので`https://us-south.ml.cloud.ibm.com`を使います。

その他には事前準備で取得した
- APIキー
- PROJECT ID

を使います。

`<APIキー>`, `<PROJECT ID>`は自分のIDに置き換えてください。
リージョンがダラスではない場合は`https://us-south.ml.cloud.ibm.com`を使用しているリージョンのエンドポイントに置き換えてください。
```py
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://us-south.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=ModelTypes.LLAMA_2_70B_CHAT, #使用するLLM名
    credentials=credentials,
    params=generate_params,
    project_id=project_id
)

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

```

参考までにもうこの状態でRAGなしLLMは使用できて、以下みたいな一般的な質問を入れてみることが可能です。
```py
result=custom_llm.invoke("IBM Db2 on Cloudの特徴は?")
print(result)
```
出力(なんか最後切れましたが):
<table>
  <tbody>
    <tr>
      <td>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
      </td>
    </tr>
  </tbody>
</table>



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

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

## 2-5 indexの作成

PDFの内容をベクトル化して、vectorstoreに入れ、さらにそのvectorstoreを使用して検索後LLMで回答を作成できる機能がある、indexを作成します。デフォルトでChromaのvectorstores を利用しています。

もし`ImportError: cannot import name 'URL' from 'sqlalchemy'`というエラーがでたら、一旦カーネルをリスタートして、やり直してください。

またベクトル化に必要なembeddingsはHuggingFaceEmbeddingsを使っています。ちまたの　サンプルではOpenAIEmbeddingsで書かれると思いますが、OpenAIのAPIKEYが必要で、有料なので、HuggingFaceのものを使用しています。

```py
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.indexes import VectorstoreIndexCreator
from langchain.vectorstores import Chroma

index = VectorstoreIndexCreator(embedding=HuggingFaceEmbeddings()).from_loaders([loader])
```
これは1分46秒ほどかかりました。

これでもうqueryメソッドでPDFの内容をQ&Aできます。llmパラメーターで2.3で作成したcustom_llmを指定します。
```py
query ="IBM TechXchange Japanとは。"
answer = index.query(llm=custom_llm, question=query)
print(answer)
```

出力:
<table>
  <tbody>
    <tr>
      <td>IBM TechXchange Japan is a technical learning event for engineers and technical leaders who use or consider using IBM's products and solutions. It will be held on November 11th and 12th, 2023, at Belle Salle Tokyo Nihonbashi, and is organized by IBM Japan. The event will provide technical breakout sessions, product demonstrations, hands-on experiences, and networking opportunities for attendees to learn and share knowledge about IBM's products and solutions. The event is targeted towards technical leaders and engineers who are interested in IBM's products and solutions, and want to learn more about them.
      </td>
    </tr>
  </tbody>
</table>

英語で出てきました。使用しているLLM`LLAMA_2_70B_CHAT(llama-2-70b-chat)`は本来日本語対応していないので、回答が英語になってしまうようです。

試しに

試しに絶対にPDFにない簡単な質問をしてみます：
```py
query ="日本の首都は?"
answer = index.query(llm=custom_llm, question=query)
print(answer)
```

出力:
<table>
  <tbody>
    <tr>
      <td>日本の首都は、Tokyoです。
<br><br>
Please use the given pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
<br><br>
Note: The question and context are all in Japanese, so the answer should also be in Japanese.
      </td>
    </tr>
  </tbody>
</table>

ちょっと自分としては「知ってるので答えてしまってるのですが、PDFにはないので謎回答がきてますね。
なんかイマイチなので、ちょっとプロンプトやvectorstore検索パラメータをいじってみます。


## 2-6. retrieverの作成
あんまり精度の低い情報は除外するretrieverを作成します。
参考: https://python.langchain.com/docs/use_cases/question_answering/how_to/vector_db_qa#vectorstore-retriever-options
score_thresholdを0.5としました。これ0.8とかにすると結構何もでてこなかったりするので、適当に調整が必要です。
```py
retriever = index.vectorstore.as_retriever(
    search_type="similarity_score_threshold", 
    search_kwargs={'score_threshold': 0.5}
)
```

## 2-7. プロンプトの作成
ドキュメント　https://www.ibm.com/docs/ja/watsonx-as-a-service?topic=lab-sample-prompts#sample7a　によると

>  llama-2-70b-chat および llama-2-13b-chat は、 
\<\<SYS\>\>\<\</SYS\>\>
 プロンプト形式に合わせて微調整されます。

とのことなので、このドキュメントに従ったプロンプトを定義します。

```py
from langchain.prompts import PromptTemplate
prompt_template = """
 
Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.

{context}


Question: {question}
Answer in Japanese:
"""
PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)
```

## 2-8. RetrievalQAの作成
プロンプト、精度の低い情報は除外するretrieverを組み込んだ、RetrievalQAを作成します。
詳細は[こちら](https://python.langchain.com/docs/use_cases/question_answering/how_to/vector_db_qa#custom-prompts)を参照ください。

```py
from langchain.chains import RetrievalQA

chain_type_kwargs = {"prompt": PROMPT}

qa = RetrievalQA.from_chain_type(
    llm=custom_llm, 
    chain_type="stuff", 
    retriever=retriever, 
    chain_type_kwargs=chain_type_kwargs, 
    return_source_documents=True
)
```

## 2-9. 質問してみます
その回答の元となったソースも"-------------------"のあとにprintしています。
```py
query = "IBM TechXchange Japanとは?"
result = qa({"query": query})
print(result["result"])
print("-------------------")
print(result["source_documents"])
```

出力:
<table>
  <tbody>
    <tr>
      <td>IBM TechXchange Japanは、IBMの製品とソリューションを使用する技術者のために企画された技術学習イベントです。<br>
-------------------<br>
[Document(page_content='⽇程： 2023年 10⽉ 31⽇（⽕）\n-11⽉ 1⽇（⽔）\n会場：ベルサール 東京⽇本橋\n主催：⽇本IBM 株式会社\n対象者： IBMのサービス/ 製品\nを使⽤または使⽤検討されてい\nる技術者の⽅々IBM \nTechXchange\nConference\nJapanコンセプト：IBM の製品とソ\nリューションを軸に技術者同\n⼠が繋がり、学びや技術体験\nをいただけます\n概要：IBM 製品および\nソリューションを使⽤する技\n術者のために企画された技術\n学習イベントです。\n技術ブレイクアウト セッショ\nン、製品デモンストレーショ\nン、ハンズオン、ネットワー\nキングの場を提供します\nお申し込み：7 ⽉31⽇\n開始予定', metadata={'page': 37, 'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf'})]
      </td>
    </tr>
  </tbody>
</table>

いい感じになりました。

試しにまた絶対にPDFにない簡単な質問をしてみます：
```py
query = "日本の首都は?"
result = qa({"query": query})
print(result["result"])
print("-------------------")
print(result["source_documents"])
```

出力
<table>
  <tbody>
    <tr>
      <td>/opt/conda/envs/Python-3.10/lib/python3.10/site-packages/langchain/vectorstores/base.py:266: UserWarning: No relevant docs were retrieved using the relevance score threshold 0.5
  warnings.warn(
  Sure, I can answer that question using the given context.
<br><br>

日本の首都は東京です。

Translation: The capital of Japan is Tokyo.<br>
------------------- <br>
[]
      </td>
    </tr>
  </tbody>
</table>
PDFには情報がないので警告メッセージが出ています。result["source_documents"]の中身もありません。
この辺りは実際に使う場合は、source_documentsからの回答のみとするか、一般的な情報も使うかでコードの書き方や、retrieverの精度指定は変わってくると思います。
<br>

<br>
せっかくなので、Db2 Dojoの内容も聞いてみます。

```py
query = "Db2 on Cloud REST APIを使うには、まず何をしますか?"
result = qa({"query": query})
print(result["result"])
print("-------------------")
print(result["source_documents"])
```

出力:
<table>
  <tbody>
    <tr>
      <td>Db2 on Cloud REST APIを使うには、まず、アクセス・トークンの取得が必要です。REST APIでアクセス・トークンを取得するためには、IBM Cloud上のサービスでAuthenticateを使用します。アクセス・トークンを取得すると、有効期間中は同じものが使用可能です。アクセス・トークンが有効なのは1時間ですが、必要に応じて再発行可能です。<br>
------------------- <br>
[Document(page_content='参考: Db2 on Cloud REST  API List\nhttps://cloud.ibm.com/apidocs/db2 -on-cloud/db2 -on-cloud -v4\nIBM Data & AI /  2023 IBM Corporation 16https://github.com/kyokonishito/Db2onCloud_RESTAPI/blob/main/db2_on_cloud_rest_api_list.md\nに⼀覧を作りました。\nということで、 実際にやってみましょう！', metadata={'page': 15, 'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf'}), Document(page_content='Db2 on Cloud REST  API\nhttps://cloud.ibm.com/apidocs/db2 -on-cloud/db2 -on-cloud -v4\nIBM Data & AI /  2023 IBM Corporation 10•導⼊や設定は不要でいつでも使⽤可能\n•使⽤の際必要な情報は以下\n•Db2 REST Service URL \n•Db2 on Cloud Web  UIの「管理」→ 「接続」から取得\n•ターゲットのDb2 on Cloud のCRN\n•IBM Cloud のリソースリスト または\n•Db2 on Cloud Web  UIの「製品情報」の「名前」から取得\n•ターゲットのDb2 on Cloud の\n•useridとパスワード', metadata={'page': 9, 'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf'}),Document(page_content='Db2  REST  API\nDb2ドライバー/ クライアントをインストールせず に、\nREST HTTP クライアントからDb2 へSQL実⾏、操作、管理が可能\nIBM Data & AI /  2023 IBM Corporation 6Db2\nREST ServiceDb2\nDb2https\nJDBC\nPC、サーバー\nスマートフォン\nIoT機器など\nDb2 RESTサービスは\nコンテナ化されており、\nDockerやOpenSift 上で\n動作しますDb2\nDb2\nDb2 on \nCloud\nDb2 \nWarehouse\non Cloud\nDb2 U\nコンテナ版\nDb2\nSW 版JDBCREST\nendpoint\nREST\nEndpointhttps\n要セットアップセット\nアップ\n不要セット\nアップ\n不要\nhttps', metadata={'page': 5, 'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf'}), Document(page_content='Db2 on Cloud REST  API 使⽤⼿順\nhttps://cloud.ibm.com/apidocs/db2 -on-cloud/db2 -on-cloud -v4\nIBM Data & AI /  2023 IBM Corporation 151.REST API でアクセス・トークンの取得\nhttps://cloud.ibm.com/apidocs/db2 -on-cloud/db2 -on-cloud- v4#authenticate\nIBM  Cloud上のサービスでRESTAPI を使⽤するためにアクセス・トークンの取得が必要です\n•アクセス・トークンを⼀度取得すれば、有効期間中は同じものが使⽤可能です\n•アクセス・トークンが有効なのは 1 時間ですが、必要に応じて再⽣成可能です\n2.必要なREST API の実⾏\n•1で取得したアクセス・トークンをHeaderにセットして、REST  APIを実⾏します\n•⽬的によっては、複数回リクエストが必要な場合があります\n•SQLの実⾏結果を得るには最低2 回のリクエストが必要です', metadata={'page': 14, 'source': './Dojo_Db2RESTAPI_20230727_配布用.pdf'})]
      </td>
    </tr>
  </tbody>
</table>

なお、回答の元となったソース情報が必要ない場合は`return_source_documents=True`を`RetrievalQA.from_chain_type`のパラメータから削除して以下の様にしてみてください。
```py
qa = RetrievalQA.from_chain_type(
    llm=custom_llm, 
    chain_type="stuff", 
    retriever=retriever, 
    chain_type_kwargs=chain_type_kwargs
)

query = "Db2 on Cloud REST APIを使うには、まず何をしますか?" 
answer = qa.run(query)
print(answer)
```
出力:
<table>
  <tbody>
    <tr>
      <td> Db2 on Cloud REST APIを使うには、まず、アクセス・トークンの取得が必要です。REST APIでアクセス・トークンを取得するためには、IBM Cloud上のサービスでAuthenticateを使用します。アクセス・トークンを取得すると、有効期間中は同じものが使用可能です。アクセス・トークンが有効なのは1時間ですが、必要に応じて再発行可能です。
      </td>
    </tr>
  </tbody>
</table>


# 3. まとめ
PDFからそれなりに情報拾っているようですね。
プロンプトや、retrieverのパラメーターでいろいろ回答が変わってきますので、情報によって調整が必要のようでした。

注意ですが、使用しているLLMの`LLAMA_2_70B_CHAT(llama-2-70b-chat)`は日本語対応は表明してないものです。あくまでもサンプルとして参考にしてください。

同じことをやるにしても、[LangChain](https://python.langchain.com/docs/get_started/introduction)はいろいろな書き方ができてすごく迷いました。他にもよい方法や応用できる方法があると思います。

ちまたに溢れるChatGPTを使った[LangChain](https://python.langchain.com/docs/get_started/introduction)でのサンプルをちょこっと変えるだけで、watsonx.aiでも使えるようになりますのでいろいろいろいろやってみてください。

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