【LLM】AIに知らないことを回答させる【RAG】

## 記事の内容
* RAGの雑な説明
* RAGを雑に手作りする

## 対象
* 10分でRAGを理解したい人
* 文系でも読めるようにプログラムに説明をつけてます

## RAGの説明
### 言葉の意味
Retrieval-augmented Generation
検索によって強化された生成

### 基本的なアイデア
LLMは学習してない内容に適切な回答をすることができない。
→ 回答に必要な情報をLLMが参照できるようにしよう。


### 例
LLMは以下のようなプロンプトに対してうまく回答することはできないでしょう。
```
昨日発売された小説『○○』の主人公の好物を教えてください
```

プロンプトに情報を追加すればうまく答えてくれるでしょう。
```
昨日発売された小説『○○』の主人公の好物を教えてください

以下の情報を参考にしてください。
------
タイトル『○○』
作者 ××
むかしむかしあるところに...
------
```

これは以下のステップで実現されます。
1. 人間がプロンプトを作成
1. 回答に必要な情報を探す (Retrieval)
1. プロンプトに追加 (Augment)
1. LLMで回答を生成 (Generate)

この2-4をシステム側でやるのがRAGです。

### 何がいいのか
* 学習していない質問に答えてくれる
  * 会社の制度
  * 昨日あったこと
* 幻覚 (hallucination) が防げる
* ファインチューニングと比べて
  * 必要なコンピューティングリソースが小さい
  * 必要な時間が短い
  * トレーニングデータが不要
  * モデルが忘却 (Catastrophic forgetting) しない

## RAGを雑に作ってみる
### 仕様
* データソースはWikipediaを利用
* 取得するページのワードは人間が指定
* テキストを分割（チャンキング）してプロンプトと類似度を求める
* 類似度の高いチャンクを参考情報としてプロンプトに追加

### プログラム
#### Wikipediaのページをテキストで取得する
```python
from bs4 import BeautifulSoup
import requests
def get_wikipedia_text(topic):
    url = "https://ja.wikipedia.org/wiki/" + topic
    response = requests.get(url)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")
    content = soup.find("main", class_="mw-body")
    text = ""
    for paragraph in content.find_all("p"):
        text += paragraph.get_text()
    return text
```
戻り値
```
『吾輩は猫である』（わがはいはねこである）は、夏目漱石の長編小説であり、処女小説である。1905年（明治38年）1月、『ホトトギス』にて発表されたのだが、好評を博したため、翌1906年（明治39年）8月まで継続した。上、1906年10月刊、中、1906年11月刊、下、1907年5月刊。
中学の英語教師苦沙弥先生の日常と、書斎に集まる美学者迷亭、理学者寒月、哲学者東風らといった明治の知識人たちの生活態度や思考を飼い猫の目を通して、ユーモアに満ちたエピソードとして描いた作品。
表面的にすぎない日本の近代化に対する、漱石の痛烈な文明批評・社会批判が表れている風刺小説。なお実際、本作品執筆前に、夏目家に猫が迷い込み、飼われることになった。その猫も、ずっと名前がなかったという。
「吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。」という書き出しで始まり、中学校の英語教師である珍野苦沙弥の家に飼われている猫である「吾輩」の視点から、珍野一家や、そこに集う彼の友人や門下の書生たち、「太平の逸民」（第二話、第三話）の人間模様が風刺的・戯作的に描かれている。
着想は、E.T.A.ホフマンの長編小説『牡猫ムルの人生観』だと考えられている[1][注 1][注 2]。
また『吾輩は猫である』の構成は、『トリストラム・シャンディ』の影響とも考えられている[2][3]。
漱石が所属していた俳句雑誌『ホトトギス』では、小説も盛んになり、高浜虚子や伊藤左千夫らが作品を書いていた。こうした中で虚子に勧められて漱石も小説を書くことになった。それが1905年1月に発表した『吾輩は猫である』で、当初は 最初に発表した第1回のみの、読み切り作品であった[4]。しかもこの回は、漱石の許可を得た上で虚子の手が加えられており[4]、他の回とは多少文章の雰囲気が異なる。だがこれが好評になり、虚子の勧めで翌年8月まで、全11回連載し、掲載誌『ホトトギス』は売り上げを大きく伸ばした（元々俳句雑誌であったが、有力な文芸雑誌の一つとなった）[4][注 3]。
タバコではじまり、ビールで終わる。皮肉にも大きな池で始まり、水甕（みずがめ）で終わる構成になっている。
主人公「吾輩」のモデルは、漱石37歳の...

```

#### テキストをチャンクに分割
```python
from langchain.text_splitter import SpacyTextSplitter
def chunk_text(text):
    text = text.replace("\n","")
    text = SpacyTextSplitter(separator="[SEP]", pipeline="ja_ginza", chunk_size=400, chunk_overlap = 20).split_text(text)
    return "".join(text).split("[SEP]") 
```
戻り値
```
['『吾輩は猫である』',
 '（わがはいはねこである）は、夏目漱石の長編小説であり、処女小説である。',
 '1905年（明治38年）1月、『ホトトギス』にて発表されたのだが、好評を博したため、翌1906年（明治39年）8月まで継続した。',
 '上、1906年10月刊、中、1906年11月刊、下、1907年5月刊。',
 '中学の英語教師苦沙弥先生の日常と、書斎に集まる美学者迷亭、理学者寒月、哲学者東風らといった明治の知識人たちの生活態度や思考を飼い猫の目を通して、ユーモアに満ちたエピソードとして描いた作品。',
 '表面的にすぎない日本の近代化に対する、漱石の痛烈な文明批評・社会批判が表れている風刺小説。',
 'なお実際、本作品執筆前に、夏目家に猫が迷い込み、飼われることになった。',
 'その猫も、ずっと名前がなかったという。',
 '「吾輩は猫である。',
 ...
 ]
```

#### チャンクとプロンプトの類似度を求める
```python
import spacy
def get_similarity(texts, prompt, topic):
    prompt = prompt.replace(topic,"")
    nlp = spacy.load('ja_ginza')
    similarity = []
    for text in texts:
        similarity.append(nlp(text).similarity(nlp(prompt)))
    return similarity
```
戻り値
```
[0.8719587218667135, 0.8414548598386105, 0.762461425952295, 0.6925417069380403, 0.8424593371132324, 0.7689011486045065, 0.8468756994444322, 0.795053236562795, 0.7781240605851715, 0.7197590316430656, 0.8712955295580906, 0.8753743672378088, 0.564091392858644, 0.8852024732907289, 0.8611001540228234, 0.8573801954786284, 0.8195178374230856, 0.8571437531010478, 0.578973248940548, 0.655554226753693, 0.7786023102941645, 0.853217130716636, 0.8088680954614914, 0.8606967805703012, 0.7159985683168582, 0.7767277725094501, 0.747928032933286, 0.7373193929039583, 0.8764344207453313, 0.8086330421508037, 0.7208677325060624, 0.8714617949862404, 0.8032777403314841, 0.8370163734279931, 0.7014596190623459, 0.704137276561204, 0.7858748222316846, 0.7674872382038567, 0.8262946572263777, 0.6908513768895612, 0.8106272910613479, 0.8843908418809565, 0.8230422768808365, 0.8412509095838033, 0.8718912603288065, 0.7633151601361573, 0.5536500433964437]
```

#### 類似度が高いチャンクからコンテキストを生成する
```python
def generate_context(texts ,similarity, count):
    # 類似度が高いチャンクを愚直につなげるだけ
    texts_similarity = list(zip(texts, similarity))
    similar_contexts = sorted(texts_similarity, key=lambda sim: sim[1], reverse=True)[0:count]
    text = ""
    for text_similarity in similar_contexts:
        text += text_similarity[0] + "\n"
    return text 
```
戻り値
```
『吾輩ハ鼠デアル』（1907年（明治40年）9月刊）、『我輩ハ小僧デアル』（1908年3月刊）などである。三島由紀夫も少年時代（中等科1年）に『我はいは蟻である』（1937年）という童話的な小品を書いており、「我はいは暗い暗い部屋の中で生れ出た。」 という幼虫からの書き出しで始まり、変身前の自分を「うじ」と呼んで嫌う人間どもを「人間とは可笑しな動物」と言い、蛹から蟻になった「我はい」が重いビスケットを背負ってそれを舐めて美味しかったエピソードなどが描かれている[9][10]。
また『吾輩は猫である』の構成は、『トリストラム・シャンディ』の影響とも考えられている[2][3]。
最終回で、迷亭が苦沙弥らに「詐欺師の小説」を披露するが、これはロバート・バーの『放心家組合』のことである。この事実は、大蔵省の機関誌『ファイナンス 』1966年4月号において、林修三によって初めて指摘された[8]。
着想は、E.T.A.ホフマンの長編小説『牡猫ムルの人生観』だと考えられている[1][注 1]
例をあげると、窃盗犯に入れられた次の朝、苦沙弥夫婦が警官に盗まれた物を聞かれる件（第五話）は『花色木綿（出来心）』の、寒月がバイオリンを買いに行く道筋を言いたてるのは『黄金餅』の、パロディである。
どこで生れたかとんと見当がつかぬ。」という書き出しで始まり、中学校の英語教師である珍野苦沙弥の家に飼われている猫である「吾輩」 の視点から、珍野一家や、そこに集う彼の友人や門下の書生たち、「太平の逸民」（第二話、第三話）の人間模様が風刺的・戯作的に描かれている。
2019年には演出家ノゾエ征爾による『吾輩は猫である』が東京芸術祭2019で上演された（これは夏目漱石の 作品を下敷きにしつつ、大胆に換骨奪胎し、総勢80名弱のキャストで新基軸の劇世界を作ったものとのこと[12]）
漱石が所属していた俳句雑誌『ホトトギス』では、小説も盛んになり、高浜虚子や伊藤左千夫らが作品を書いていた。
小さな墓標の裏に「こ の下に稲妻起る宵あらん」と安らかに眠ることを願った一句を添えた後、猫が亡くなる直前の様子を「猫の墓」（『永日小品』所収）という随筆に書き記している。
だがこれが好評になり、虚子の勧めで翌年8月まで、全11回連載し、掲載誌『ホトトギス』は売り上げを大きく伸ばした（元々俳句雑誌であったが、有力な文芸雑誌の一つとなった）
```


#### プロンプトを生成
```python
def generate_augmented_prompt(prompt, topic, context):
    return f'

あなたは誠実で優秀な日本人のアシスタントです。
質問: {prompt}\n\n以下は「{topic}」に関する情報です。質問に回答する際に必要であれば追加情報として利用してください。\n---------------\n{context}
'
```
戻り値
```


あなたは誠実で優秀な日本人のアシスタントです。

質問: 『吾輩は猫である』の作者と主人公をおしえてください

以下は「吾輩は猫である」に関する情報です。質問に回答する際に必要であれば追加情報として利用してください。
---------------
『吾輩ハ鼠デアル』（1907年（明治40年）9月刊）、『我輩ハ小僧デアル』（1908年3月刊）などである。三島由紀夫も少年時代（中等科1年）に『我はいは蟻である』（1937年）という童話的な小品を書いており、「我はいは暗い暗い部屋の中で生れ出た。」という幼虫からの書き出しで始まり、変身前の自分を「うじ」と呼んで嫌う人間どもを「人間とは可笑しな動物」と言い、蛹から蟻になった「我はい」が重いビスケットを背負ってそれを舐めて美味しかったエピソードなどが描かれている[9][10]。
また『吾輩は猫である』の構成は、『トリストラム・シャンディ』の影響とも考えられている[2][3]。
最終回で、迷亭が苦沙弥らに「詐欺師の小説」を披露するが、これはロバート・バーの『放心家組合』のことである。この事実は、大蔵省の機関誌『ファイナンス』1966年4月号において、林修三によって初めて指摘された[8]。
着想は、E.T.A.ホフマンの長編小説『牡猫ムルの人生観』だと考えられている[1][注 1]
例をあげると、窃盗犯に入れられた次の朝、苦沙弥夫婦が警官に盗まれた物を聞かれる件（第五話）は『花色木綿（出来心）』の、寒月がバイオリンを買いに行く道筋を言いたてるのは『黄金餅』の、パロディである。
どこで生れたかとんと見当がつかぬ。」という書き出しで始まり、中学校の英語教師である珍野苦沙弥の家に飼われている猫である「吾輩」の視点から、珍野一家や、そこに集う彼の友人や門下の書生たち、「太平の逸民」（第二話、第三話）の人間模様が風 刺的・戯作的に描かれている。
2019年には演出家ノゾエ征爾による『吾輩は猫である』が東京芸術祭2019で上演された（これは夏目漱石の作品を下敷きにしつつ、大胆に換骨奪胎し、総勢80名弱のキャストで新基軸の劇世界を作ったものとのこと[12]）
漱石が所属していた俳句雑誌『ホトトギス』では、小説も盛んになり、高浜虚子や伊藤左千夫らが作品を書いていた。
小さな墓標の裏に「この下に稲妻起る宵あらん」と安らかに眠ることを願った一句を添えた後、猫が亡くなる直前の様子を「猫の墓」（『永日小品』所収）という随筆に書き記している。
だがこれが好評になり、虚子の勧めで翌年8月まで、全11回連載し、掲載誌『ホトトギス』は売り上げを大きく伸ばした（元々俳句雑誌であったが、有力な文芸雑誌の一つとなった）


```


#### LLMで生成
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
def generate_by_llm(prompt):
    # ローカルで動かせるやつ
    model_name = "elyza/ELYZA-japanese-Llama-2-7b-fast-instruct" 
    device = "cuda" # GPUがあれば。
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto").to(device)
    token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
    output_ids = model.generate(
        token_ids.to(model.device),
        max_new_tokens=2000,
        pad_token_id=tokenizer.pad_token_id,
        eos_token_id=tokenizer.eos_token_id,
    )
    output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1) :], skip_special_tokens=True)
    return output
```

#### prompt作成から回答生成まで
```python
prompt = "『吾輩は猫である』の作者と主人公をおしえてください。"
topic = "吾輩は猫である" # promptを形態素解析して固有名詞を抽出すれば自動化できる
text = get_wikipedia_text(topic)
texts = chunk_text(text)
similarity = get_similarity(texts, prompt, topic)
context = generate_context(texts ,similarity, 20)
augmented_prompt = generate_augmented_prompt(prompt, topic, context)
answer = generate_by_llm(augmented_prompt)
print(answer)
```


### LLM (ELYZA-japanese-Llama-2-7b-fast-instruct) が知らないことを質問してみる
いい感じに答えられた。

Q.『吾輩は猫である』の作者と主人公をおしえてください。
RAGなし
```
 承知しました。

『吾輩は猫である』は、宮沢賢治による童話です。主人公は「狐の王子」です。

また、作者は宮沢賢治です。
```
RAGあり
```
 質問: 『吾輩は猫である』の作者と主人公をおしえてください。

答え: 作者: 夏目漱石、主人公: 吾輩 (ねこ)
```


Q. テレビ番組「めざせ!切り出し職人」の放送時間と番組の内容を教えてください。

RAGなし
```
 「めざせ!切り出し職人」は、NHK Eテレで放送されているテレビ番組です。

放送時間は、毎週月曜日の19:00～19:30です。

番組の内容は、木工の職人さんが木材を切る音とともに、その日のテーマに沿った木工芸術作品を制作していく様子を放送しています。
```
RAGあり
```
 テレビ番組「めざせ!切り出し職人」の放送時間は毎週水曜日 2:34 - 2:54 (木曜深夜) です。

番組の内容は、テレビ朝日の番組を題材に30秒の切り出し動画を制作し、動画の面白ポイントや驚きの編集技術について語るというものです。
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3776243/534defbc-84d8-9cbd-50c1-a62a25562ef8.png)

## 補足
* 採用したLLMとマシンの能力からWikipediaの記事から類似度の高い部分を抽出した。
  * ChatGPTとかClaudeなら丸投げでも問題なく処理できると思う。
* Wikipediaのどこのページからデータを取得してくるか指定したが、形態素解析で固有名詞を抽出すれば自動化は可能。
* 実用的なRAGは巨大なファイルシステムやデータベースに対してElasticsearchを用いて作られたりする



## Appendix
### プログラム全体
```python
from bs4 import BeautifulSoup
import requests

# Wikipediaのページをテキストで取得する
def get_wikipedia_text(topic):
    url = "https://ja.wikipedia.org/wiki/" + topic
    response = requests.get(url)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")
    content = soup.find("main", class_="mw-body")
    text = ""
    for paragraph in content.find_all("p"):
        text += paragraph.get_text()
    return text


from langchain.text_splitter import SpacyTextSplitter

# テキストをチャンク単位で配列にする
def chunk_text(text):
    text = text.replace("\n","")
    text = SpacyTextSplitter(separator="[SEP]", pipeline="ja_ginza", chunk_size=400, chunk_overlap = 20).split_text(text)
    return "".join(text).split("[SEP]") 

import spacy

# チャンクとプロンプトの類似度を求める
def get_similarity(texts, prompt, topic):
    nlp = spacy.load('ja_ginza')
    similarity = []
    for text in texts:
        similarity.append(nlp(text.replace(topic,"")).similarity(nlp(prompt.replace(topic,""))))
    return similarity

# チャンクと類似度からコンテキストを生成
def generate_context(texts ,similarity, count):
    texts_similarity = list(zip(texts, similarity))
    similar_contexts = sorted(texts_similarity, key=lambda sim: sim[1], reverse=True)[0:count]
    text = ""
    for text_similarity in similar_contexts:
        text += text_similarity[0] + "\n"
    return text

# プロンプト生成
def generate_augmented_prompt(prompt, topic, context):
    return f'

あなたは誠実で優秀な日本人のアシスタントです。
質問: {prompt}\n\n以下は「{topic}」に関する情報です。質問に回答する際に必要であれば追加情報として利用してください。\n---------------\n{context}
'

from transformers import AutoModelForCausalLM, AutoTokenizer

# LLMで生成
def generate_by_llm(prompt):
    model_name = "elyza/ELYZA-japanese-Llama-2-7b-fast-instruct"
    device = "cuda" # GPUがあれば。
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto").to(device)
    token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
    output_ids = model.generate(
        token_ids.to(model.device),
        max_new_tokens=2000,
        pad_token_id=tokenizer.pad_token_id,
        eos_token_id=tokenizer.eos_token_id,
    )
    output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1) :], skip_special_tokens=True)
    return output

prompt = "テレビ番組「めざせ!切り出し職人」の放送時間と番組の内容を教えてください。"
topic = "めざせ!切り出し職人"
text = get_wikipedia_text(topic)
texts = chunk_text(text)
similarity = get_similarity(texts, prompt, topic)
context = generate_context(texts ,similarity, 20)
augmented_prompt = generate_augmented_prompt(prompt, topic, context)
answer = generate_by_llm(augmented_prompt)
print(answer)

```


### 参考
https://www.ibm.com/blogs/solutions/jp-ja/retrieval-augmented-generation-rag/
https://medium.com/@bijit211987/when-to-apply-rag-vs-fine-tuning-90a34e7d6d25