LLM(gemma)&LangChainで会社のQABotっぽい物を作成する

# はじめに
先日Googleから発表されたLLM(Gemma)を触ってみましたので、簡単にまとめてみます。

実行環境にはGoogle Colab(無料版)を使用しています。

## 今回やること
ただ動かすだけでは面白くないので、会社のHP( https://www.haw.co.jp )情報をLangChainのRetrievalQAを使用してLLM(Gemma)で扱えるようにして、会社に関するQAのチャットボットっぽい物を作成したいと思います。

## Gemmaとは？
Googleが2024年2月22日に公開した、オープンソースの大規模言語モデルマルチモーダルAIのGeminiよりも軽量で、商用利用も可能です。

https://ai.google.dev/gemma?hl=ja

### モデルについて
現在公開されているのは以下の4種です。

- gemma-2b
- gemma-2b-it
- gemma-7b
- gemma-7b-it

何もないのがベースモデルで、`it`がついているのが命令モデルのようです。

## LangChain・RetrievalQAについて
LangChainは、自然言語処理(NLP)の機能拡張を効率的に実装するためのライブラリです。
そのLangChainの中にある機能の1つが、RetrievalQA（検索型質問応答）です。
RetrievalQAを使用することで、大量のテキストデータから関連する情報を検索し、質問に対する回答を生成することができます。

## 実装
VectorDBに会社のHPから取得したテキストデータをベクトル化して保存しておき、ユーザからの質問をRetrievalQAに渡し、VectorDBに検索をかけて、その結果をGemmaに渡す流れです。

### 事前準備

#### Googleのライセンスに同意する
Gemmaを使用するにはHugging FaceアカウントでGoogleのライセンスに同意する必要があります。

https://huggingface.co/google/gemma-7b

以下のような画面が出てくると思いますので、進めて同意してください。
![スクリーンショット 2024-02-23 12.52.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/a2ab86b0-ec73-efd2-84df-79e18afa959b.png)

#### HF_TOKEN設定
Hugging Faceのアカウントページからトークンを発行し、それをcolabのシークレット(左の鍵マーク)に`HF_TOKEN`として設定します。

![スクリーンショット 2024-02-26 11.28.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/b1487f8a-1580-3565-6f87-c7f104541b3b.png)

#### インストール
transformersの最新版とその他、使用するパッケージをインストールします。
```
!pip install -U transformers > /dev/null
!pip install bitsandbytes accelerate > /dev/null
!pip install langchain > /dev/null
```

### Gemma
#### とりあえず動かす
試しに以下のサンプルを動かしてみます。
https://huggingface.co/google/gemma-7b-it

今回はcolab無料版(T4)で普通では当然動くわけないので、8ビット量子化設定で動かします。

```python
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig


quantization_config = BitsAndBytesConfig(load_in_8bit=True)

tokenizer = AutoTokenizer.from_pretrained('google/gemma-2b-it')
model = AutoModelForCausalLM.from_pretrained(
    'google/gemma-2b-it',
    quantization_config=quantization_config
)
```
GPU RAMがかなりギリギリです。
(撮影ミスで暗くなっていますが気にしないでください。)
![スクリーンショット 2024-02-26 13.13.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/6771857a-bcfa-de31-9636-a9217643bb5b.png)


適当に質問してみます。
```python
input_text = '日本で一番高い山の名前と標高は何ですか？'
input_ids = tokenizer(input_text, return_tensors='pt').to('cuda')

outputs = model.generate(**input_ids)
print(tokenizer.decode(outputs[0]))
```
![スクリーンショット 2024-02-26 13.11.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/db4fae1c-6c32-63aa-bb2e-90602d0fdcbe.png)

出力のデフォルトが20トークンまでみたいなので途中で切れました。
`max_new_tokens`を増やして再度実行してみます。
![スクリーンショット 2024-02-23 14.00.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/06632812-26ee-e32f-485d-50eaa7807ef8.png)

余計なものついていたり、同じことを繰り返したりしてますが、問題なさそうですね。

#### Langchainで使用できるようにする
`pipeline`を通すことにより、Langchainで使用できるようになります。
```python
from langchain.llms import HuggingFacePipeline
from transformers import pipeline


pipe = pipeline(
    'text-generation',
    model=model,
    tokenizer=tokenizer,
    max_new_tokens=100,
)
local_llm = HuggingFacePipeline(pipeline=pipe)
```

先ほどのプロンプトをLCELで書き直して実行すると同じ結果が得られます。
```python
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema import StrOutputParser


base_template = '''
<bos><start_of_turn>user
{question}
<end_of_turn><start_of_turn>model
'''

qa_chain = (
    {'question': RunnablePassthrough()}
    | PromptTemplate.from_template(base_template)
    | local_llm
    | StrOutputParser()
)
res = qa_chain.invoke('日本で一番高い山の名前と標高は何ですか？')
print(res)
```


### VectorDB構築
VectorDBは色々とありますが、今回はFAISS(Facebook AI Similarity Search)を使用します。
その他の設定は以下のようにします。
- データ範囲: 会社HPのTOPページから直接アクセスできる自社ドメインのページ全て
- テキストの分割チャンク: 100文字(適当)
- embeddingモデル: 'intfloat/multilingual-e5-large'

```python
from langchain.indexes import VectorstoreIndexCreator


urls = [
    'https://www.haw.co.jp/',
    ...
]
docs = UnstructuredURLLoader(urls=urls).load()

# チャンクの分割
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,  # チャンクの最大文字数
    chunk_overlap=10,  # オーバーラップの最大文字数
)

# VectorDB構築
vectorstore = FAISS.from_documents(
    docs, 
    embedding=HuggingFaceEmbeddings(
        model_name='intfloat/multilingual-e5-base'
    )
)
```

#### QABotの構築
vectorstoreとLLMでchainを構築します。
```python
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema import StrOutputParser


base_template = '''
<bos><start_of_turn>system
あなたはAIアシスタントです。
次の情報のみに基づいて、userの質問に答えてください。

{context}
<end_of_turn>
<start_of_turn>user
{question}
<end_of_turn>
<start_of_turn>model
'''


qa_chain = (
    {'context': vectorstore.as_retriever(search_kwargs={'k': 3}), 'question': RunnablePassthrough()}
    | PromptTemplate.from_template(base_template)
    | local_llm
    | StrOutputParser()
)
```

これで完成したので実際に質問してみます。
```python
qa_chain.invoke('会社について教えて')
```
![スクリーンショット 2024-02-23 15.10.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/81820486-7f5b-510f-e588-418d86445a42.png)

メモリが足りませんでした。。。
色々と試しましたが、どうしようもなかったので、今回は下位モデルの`2b`を使用します。
```python
quantization_config = BitsAndBytesConfig(load_in_8bit=True)

tokenizer = AutoTokenizer.from_pretrained('google/gemma-2b-it')
model = AutoModelForCausalLM.from_pretrained(
    'google/gemma-2b-it',
    quantization_config=quantization_config,
)
```
ちなみに量子化8bitでないとシステム側のRAMが足りなくなりました。

先ほどのプロンプトで動作確認
```python
input_text = '日本で一番高い山の名前と標高は何ですか？'
input_ids = tokenizer(input_text, return_tensors='pt').to('cuda')

outputs = model.generate(**input_ids)
print(tokenizer.decode(outputs[0]))
```
![スクリーンショット 2024-02-26 15.37.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/80c53102-9494-4292-0154-a69d6a5383a7.png)

若干日本語があやしいですが、このまま進めます。

再度会社について聞いてみます。
```python
def run_qa(question):
    res = qa_chain.invoke(question)
    print('---')
    print(f'Q: {question}')
    print(f'A: {res}')
    print('---')


q = '会社名を教えて'
run_qa(q)
```
![スクリーンショット 2024-02-26 15.41.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3591134/95bb207e-342c-2eb2-ffbc-902abff06286.png)
日本語がおかしいですが、一応回答できています。

続けて色々聞いてみると以下の結果になりました。
```
---
Q: 会社の所在地はどこ？
A: 我没有访问到任何公司地点的知识，因此我无法回答这个问题。
---
---
Q: 会社の製品一覧を箇条書きで出して
A: 製品一覧は削除されますが、以下は例です。

* アイソレーション
* リッジサーバー
* サーバ
* プログラミングフレームワーク
* データ分析ツール
---
---
Q: 会社の概要を教えて
A: 会社の概要は、会社がどのようなビジネスを展開しているかを理解するための重要な情報です。以下の要素は typicalな会社概要の一部として含まれます。

* **Company Name:** 会社の名前
* **Industry:**ビジネスの分類類別
* **Description:** 会社の目的、業務内容、および主要な製品やサービス
* **History:** 会社の設立日、創業者、および初期の活動
* **Management Team:** 管理者の姓名、背景、および経験
* **
---
```

中国語で回答したりよくわからない返答でした。

何度かやり直してみましたが、2Bだとあまり良い結果は得られませんでした。


# まとめ
7bを動かせなかったので少し残念な結果になってしまいました。

フォローしておくと、今回のテーマから外れるので書いていなかったのですが、簡単なPythonのコード生成なども意外とよくできていたので、モデル自体はとても良いものだと思いました。
頑張ればテストコード自動生成とかもできるかもしれません。

MetaのLlama2だと70Bなので、業務用レベルのPCが必要でしたが、今回のGemmaは7bなので、ハイエンドPCを構築していれば、一般人でも手を出せるところまで来た感じがします。
(個人的にはノートPCなどで普通に使えるようになる、もう一歩が欲しいですが。)

一般企業も手を出しやすい領域まできたのではないでしょうか？
(乗り遅れないように今後も注目したいです。)

Llama2登場時に派生が大量に出てきたように、今後もGemmaの派生が大量に出てきそうな感じがします。

ローカルLLM界隈がまた賑やかになりそうで、楽しみです。
(日本語モデルも充実して欲しいですね。)

今回は簡単にQAを作成してみましたが、ファインチューニングやRLHFを試してみたりしても面白いかと思います。

皆様もぜひお試しください。