自費出版した夢日記について答えてくれるAIボットを作ってみた

# はじめに
この記事は [NTTテクノクロスアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/ntt-tx) の11日目の記事です。

こんにちは、NTTテクノクロスの山本です。普段はLLMの活用方法を検討したり、社員が安全にLLMを使用できる環境を構築して提供する業務に従事しています。

LLMを扱うという社内でも貴重な業務をさせていただいていることもあり、せっかくなので今回初めてアドベントカレンダー、というよりは初めて社外向けのブログ投稿をさせていただきます。

今回は私が数年前趣味で執筆した、寝るときに見た夢を日記にして自費出版した本を使って「自分の見た夢を教えてくれるChatGPTのAIボット」を作る過程を紹介したいと思います。

:::note info 
LLMとはLarge Language Model（大規模言語モデル）の略で、自然言語処理の分野で開発された、巨大なニューラルネットワークベースのモデルです。これらのモデルは、数十億から数千億のパラメータを持ち、インターネット上の大量のテキストデータを使用して訓練されます。その結果、これらのモデルは、人間の言語を理解し、生成する能力を持つようになります。
:::

## 事前準備
### APIキーの発行
今回は"Langchain"というPythonのライブラリを使って簡易にChatGPT(OpenAI API)に独自知識を与える仕組みを構築します。
これらを組み合わせるには「OpenAI」のAPIキーが必要なので、事前に発行しておきましょう。
「OpenAI」のAPIキーは[「OpenAI」の公式サイト](https://platform.openai.com/docs/overview)でアカウントを作成すればAPIキーの発行が可能です。
![openai_api_key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629393/c1f727b6-3e67-80aa-a771-911d53f791f0.png)

発行したAPIキーは環境変数として設定する必要がありますので、以下のコマンドで設定します。
```bash
export OPENAI_API_KEY=<「OpenAI」 API のAPIキー>
```

### ライブラリのインストール
以下のコマンドで、ChatGPT+独自知識の仕組みを動かすために必要なライブラリをインストールします。
```bash
pip install langchain
pip install openai
```
:::note
LangChainのバージョンは日々更新されており、アップデートにより機能の仕様が変わったり依存ライブラリが変更されることも珍しくありません。
バージョンを上げたら動かなくなった！ということにならないように、うまく動作したらそのバージョンがいくつかわからなくならないように気を付けてください。
:::

## 実装してみる
まずは質問を入力したらターミナル上にprintする仕組みを実装してみます。冒頭でも述べましたが今回対象とするドキュメントはこちらです。
![yd.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629393/89295731-6870-6ff9-df10-51afcc10de71.jpeg)
これは私が3年ほど前に趣味で作成した、寝ているときに見たおもしろい夢を日記にしてその内容で自費出版した本です。「山本(yamamoto)が見た夢(dreams)」という意味で「yDREAMS」です。それ以外のタイトルの意図はないです。（この本の作成においてもpython-docxとopenpyxlというライブラリを使用していたり、話したいことはたくさんあるのですが本質とずれるため今回は割愛します）

今回はこの本の原稿の内容をRAG[^1]という手法でLLMに与えることで、自分の見た夢を回答してくれるAIの実現に挑戦します。

:::note
RAGは「Retrieval-Augmented Generation」の略で、主に対話型AIや質問応答システムに関連します。このモデルは生成（Generation）と検索（Retrieval）の2つの主要な要素から成り立ち、外部の情報源から検索した情報を生成に活用して、学習していない知識の情報をもとにテキストを生成できるようになります。
:::

まずは必要なライブラリをインストールし、LangChainのチャットモデルオブジェクトを生成します。今回は「GPT-3.5 Turbo」のモデルを使用します。読み込むドキュメントはPDFファイルなので、「PyPDFLoader」というクラスを使用します。
```python
from langchain.prompts import PromptTemplate
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.document_loaders import PyPDFLoader

llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
```

次にプロンプトの設定を行います。LangChainでは内部処理で質問を加工してOpenAIにリクエストを送っているのですが、デフォルトではプロンプトは英語で書かれており英語で回答が返ってくることがあるため日本語のプロンプトを独自に設定しました。
```python
prompt_template = """資料は「yDREAMS」という夢の内容を記した本です。
各ページにはYYYY/MM/DD の形式で日付が記載されており、その日付に見た夢の内容が記されています。
また、作者は山手線を徒歩で一周したことがあり、その時の内容も記載されています。
この本の作者は、巻末に紹介のある人物である「山本昂輝」です。あなたは「山本昂輝」としてユーザからの質問に対して、丁寧かつ長文で回答してください。

{context}

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

次にPDFファイルの読み込みとベクトル化を行います。テキストはEmbeddingモデルでベクトル化を行うことで言語モデルが処理できる形に変換しています。
```python
# PDF
loader = PyPDFLoader('./data/yDREAMS.pdf')
pages = loader.load_and_split()

# 単語や文章等をベクトル表現に変換する(embeddings)
embeddings = OpenAIEmbeddings()
db = Chroma.from_documents(pages, embedding=embeddings, persist_directory="./data/chroma")
db.persist()

qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=db.as_retriever(), chain_type_kwargs=chain_type_kwargs)
```

これで準備は完了です。実際に質問をしてみて夢の内容を教えてくれるか試してみましょう！
```python
# 「OpenAI」に質問
query = "2018年にはどんな夢をみた？"
result = qa.run(query)
print(result)
```
結果は...
```
2018年には、私は「赤ちゃんを育てる」という夢を見ました。その時、私は社会人になる前の最後の夢を見ていました。この夢が私にとって最後の夢であるべきかどうか、私は考えていました。
```
夢の内容を教えてくれるだけでなく、当時の私の心境まで付随して答えてくれました。

## UIを実装
せっかくなので画面も作成してチャットボットらしさを出してみました。WebサーバはFastAPIで実装しています。
ベクトル化したドキュメントはインデクスというバイナリ形式のファイルで実行時に保存されるようになっているので、毎回PDFを読み込まなくても大丈夫です。
```bash:ライブラリインストール
pip install fastapi
pip install uvicorn
pip install jinja2
```

```python:manage.py
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.middleware.cors import CORSMiddleware

from langchain.prompts import PromptTemplate
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.chains import RetrievalQA
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI

app = FastAPI()

origins = ["*"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
llm = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")

prompt_template = """この資料は「yDREAMS」という夢の内容を記した本で、各ページには日付とその日見た夢が記載されています。
また、作者は山手線を徒歩で一周したことがあり、その時の内容も記載されています。
この本の作者は、巻末に紹介のある人物である「山本昂輝」です。あなたは「山本昂輝」としてユーザからの質問に対して、丁寧に回答してください。

{context}

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

# 単語や文章等をベクトル表現に変換する(embeddings)
embeddings = OpenAIEmbeddings()
db = Chroma(embedding_function=embeddings, persist_directory="./data/chroma")
qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=db.as_retriever(), chain_type_kwargs=chain_type_kwargs)

# テンプレートエンジンの初期化
templates = Jinja2Templates(directory="templates")

@app.get("/")
def index(request: Request):
    return templates.TemplateResponse('index.html', context={'request': request})

@app.post("/chat/")
async def chat(request: Request):
    data = await request.json()
    text = data['message']
    response = {'message': qa.run(text)}

    return JSONResponse(content=jsonable_encoder(response))
```

```bash:実行例
# uvicorn <ファイル名(.py は除く)>:<FastAPIアプリの変数名> の形式で実行
uvicorn manage:app --host 0.0.0.0 --port 8000 --reload
```
結果...コマンドのみで実行するより見栄えの良いボットになりました。
![ydui.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629393/ad49e803-57d9-37fc-06d9-09d577a3f307.png)

# さいごに
最後までご覧いただきありがとうございます。

今回初めて社外向けに記事を投稿させていただきましたが、実際に自分の取り組んだことを記事として投稿することでより技術の中身を知ることができました。
今回紹介させていただいた内容はLLMの活用方法のほんの一部にしかすぎず、また新しい活用方法や機能がどんどん世の中に生まれています。

ぜひ皆様も自分なりのLLMの活用方法を考え、トライしてみてください。

※記載されている会社名、サービス名および商品名は、各社の登録商標または商標です。

## 参考文献
[^1]:[Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks](https://arxiv.org/pdf/2005.11401.pdf)