RAGを使った生成AIチャットサンプル（つくば市保育園チャット）

初めまして。今回はRAGを使ってつくば市の保育園情報に関して答えてくれるAIチャットを構築したいと考えております。


# 目的
現在つくば市では下記のURLに民間保育園に関する情報が記載されています。
https://www.city.tsukuba.lg.jp/soshikikarasagasu/kodomobuyojihoikuka/gyomuannai/6/1005843.html

ご覧いただくと分かりますが、料金設定や開園時間などかなり細かい情報まで書いていますね。このようにまとめていただいているのはありがたいですが、逆に量が多すぎてほしい情報を得にくいという状況にもあります。

ただChatGPTに聞いてもハルシネーションが起こってしまう可能性もあります。例えばChatGPTにつくば市の保育園の情報を聞いてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326463/cebe9846-65e0-c7fc-9e5e-5cf3c8cedd1b.png)

2歳児という返答がありましたが、正解は3歳以上なんです。
そこで、できるだけ正しい回答をしつつも自身にあった情報を提供してくれる「つくば市の保育園情報に関して答えてくれるAIチャット」を作ろうと考えました。

# AIチャットの方針

この章ではAIチャットにて使う手法とAIモデルの全体像について紹介します。

## 利用する手法
生成AIをカスタマイズしてAIチャットを作る方法にはさまざまな種類があります。それぞれの概要と特徴は以下の通りです。

+ RAG：日本語訳は検索拡張生成。通常の生成AIに対して参照元となるデータベースを与えることで、より正確な回答ができるようになる。そのためより広域なデータに基づいた回答が可能に
+ グラウンディング：参照元のデータベースが与えられており、そのデータベースにある情報以外は答えられない。外部知識を排除するためハルシネーションの可能性が低くなる
+ ファインチューニング：特殊な固有名詞が出てくる場合に、その単語を再学習させたうえで、生成AIへ回答をさせる。そのため固有の回答スタイルや用語の理解力を強化できる

その中で今回はRAGの方法を活用します。現在多くのAIチャットで使われている手法であり、構築の手間も簡単であるためです。

## 今回のRAGの全体像

今回のRAGの全体像は以下のようになっております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326463/d3d60c04-dfe7-76cc-be62-51be128b0be7.png)

データの流れを口頭で説明すると以下の通りです。

**1. つくば市の保育情報を取得**
つくば市の保育情報は以下のPDFに格納されているため、こちらを取得。
https://www.city.tsukuba.lg.jp/material/files/group/65/R061024hoikusyo.pdf
この情報のままでは使えないので、表データから非構造のテキストデータへ一度変換しています。

**2. 文章のチャンク化**
テキストデータを細かく区切ることをチャンク化といいます。最初はチャンクサイズが400でoverlapを50に設定しています。

**3. チャンク化されたデータをベクトル化**
チャンク化されたデータをそれぞれEmbeddingすることにより、ベクトルデータベース（DB）として保存しています。最初はtext-embedding-3-smallモデルを使っています。

**4. ユーザーの質問をベクトル化**
3と同様にユーザーからの質問をベクトル化しています。このベクトル化からOpenAIのAPIを通して行っています。

**5. ベクトルDBと質問ベクトルの類似度を計算して、答えとなりうる情報を取得**
3で作ったベクトルDBと4の質問ベクトルの類似度を計算して、答えとして適切なチャンクを複数指定します。よくこの指定されたチャンクはコンテキストと呼ばれます。

**6. プロンプト情報を踏まえて回答を生成**
5で指定した答えになりうるチャンク情報とプロンプトで指定された内容を踏まえて、質問への回答を生成する。今回はチャンク情報を複数指定したため、そのすべての情報をもとに生成しています。

# 評価指標
次に今回利用する評価指標に関して記述をしていきます。今回はRAGASというフレームワークを利用して、自動で評価を実施していきます。

## RAGASとは

LLMの評価を強化するツールを提供するライブラリのこと。複数の評価指標があり多面的に生成した文章を評価できることがメリット

## RAGASにある評価指標の解説

**1. Faithfulness**
LLMによって生成された回答がコンテキストに基づいているかどうかを図る指標。この時LLMで解答された内容は個別の文脈に分割された上で、回答内容とコンテキストがあっているか否かを判定する。
それぞれの質問ごとに 0(不合致）か1（合致）のどちらかが出力される。
**2. answer_relevancy**
LLMを利用して、生成された答えから想定される質問を生成する。生成された質問と元の質問がどれだけ似ているのかをコサイン類似度で計算する。コサイン類似度の数値がそのまま評価結果となる。
**3. context_precision**
まず取得されたコンテキスト内の各チャンクが質問に関連しているかどうかを評価します。それぞれのコンテキストと質問内容を比べて、コンテキストと質問の内容が一致しているかどうかを評価します。
**4. context_recall**
こちらは取得されたコンテキストが正解とどの程度一致しているかを測定する指標。まず正解をいくつかの文章に分ける。その次にその正解がコンテキストの情報と一致しているかどうかを評価する。
**5. context_entity_recall**
正解とコンテキストの両方に存在するエンティティ（今回は単語のこと）の数をもとに、その相関関係を示す指標。正解の文章に入っている単語が、コンテキストにも入っているほど数値が高くなる。
**6. answer_similarity（Answer semantic similarity）**
コンテキストと回答がどれだけ類似しているかを評価する指標。正解の文章とコンテキストの文章をそれぞれベクトル化して、コサイン類似度でお互いの類似度を数値化して出した指標。
**7. answer_correctness**
得られた回答の正確さを評価する指標。生成された回答と正解はいくつかの文脈に分割される。その文脈からTP（True Positive）,FP（False Positive）,FN（False Negative）を抽出する。
その数値からF1-Scoreを算出したうえでAnswer semantic similarityとの重み付け平均を算出する。

### 参考

https://zenn.dev/mizunny/articles/cf11a1ab1a5e3a
https://tech-lab.sios.jp/archives/41949#gai_yao
https://zenn.dev/mrmtsntr/articles/c4cf6f859c6f69


# RAGのベースライン構築

大変お待たせいたしました。ここからRAGの実装と改善を実施していきます。まずはベースラインを構築して、そこから評価を改善できるように修正を加えていきます。
今回はgoogle colaboratryを使っています。


### 1.ライブラリのインポートとOPEN_AIの利用準備

まずは利用するライブラリのインポートを行います。ポイントは以下の2点です。
+ 今回はGoogleドライブにデータを保存させて、そのデータを取り出す予定です。そのためマウントさせています
+ from ragas.metrics importで、各種評価指標をインポートしています

そして、OPEN_AIのAPI_KEYを取得して、その情報を設定します。そのうえでOpenAIのインスタンス化して、clientへ格納します。

~~~python
!pip install openai==1.25.1
# Google Driveをマウントする（Colab環境に自分のGoogleドライブを接続し、ファイルを読み書きできるようにする）
from google.colab import drive
drive.mount('/content/drive')

from openai import OpenAI
import numpy as np
import pandas as pd
import os
from sklearn.metrics.pairwise import cosine_similarity
from google.colab import userdata
import langchain

!pip install pymupdf
import pymupdf

!pip install ragas

# RAGASの評価指標をインポート（RAGの性能を評価するためのメトリクス）
from ragas.metrics import (
    context_precision,
    answer_relevancy,
    faithfulness,
    context_recall,
    context_entity_recall,
    answer_similarity,
    answer_correctness
)
from datasets import Dataset

os.environ["OPENAI_API_KEY"] = userdata.get("OPEN_API_KEY")
client = OpenAI()
~~~

### 2.pdfの情報を読み込む

次につくば市保育園のpdf情報をpymupdfで読み込みする。
~~~python
pdf_path = "/content/drive/MyDrive/データサイエンス/RAG/1031_つくば市保育園チャット/R061024hoikusyo.pdf"
pdf = pymupdf.open(pdf_path)
page = pdf[0]
~~~

### 3.pdfをdataframeへ変換

PDFをdataframeへ変換する。そのうえで前処理をしたうえでdfへ格納する。また、df_qaに質問と答えの正解データを格納する。
~~~python
tables=page.find_tables().tables
article_text_df = tables[0].to_pandas()
article_text_df.replace("\n","", regex=True, inplace=True)
df = article_text_df.copy()

df_qa = pd.read_csv("/content/drive/MyDrive/データサイエンス/RAG/1031_つくば市保育園チャット/1103_Q&A.csv")
~~~

### 4.PDFデータの前処理

PDFのデータのカラム名は改行などが含まれているため、名前の変更を行う。
~~~python
col_names =["名称","所在地","定員","電話","保育年齢","送迎バス","平日開園時間","平日閉園時間","土曜開園の有無","教育・保育の内容や特徴","実費徴収","標準の延長保育","短時間保育","特別指導","ホームページ"]
df.columns = col_names
df = df.loc[1:,:]
df["土曜開園の有無"] = df["土曜開園の有無"].str.replace("〇","行っている").str.replace("○","行っている").fillna("行っていない")
df.head(2)
~~~

出力結果は以下の形
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326463/ae7718a7-c5e4-b7d6-8c4e-775ecceceae4.png)

### 5.非構造のテキストデータへ変換

先ほど話した通り、表形式のデータを一度非構造のテキストデータへ変換する。ただしそのまま結合させてしまうと、保育園名が分からないため、保育園名と各カラムデータを一緒に格納できるようにする。
~~~python
def create_unstructured_text(row):
    # 必要なカラムを文字列として結合
    return (
        f"保育園の名称は{row['名称']}。"
        f"{row['名称']}の所在地は{row['所在地']}。"
        f"{row['名称']}の定員は{row['定員']}。"
        f"{row['名称']}の電話は{row['電話']}。"
        f"{row['名称']}の保育年齢は{row['保育年齢']}。"
        f"{row['名称']}の送迎バスは{row['送迎バス']}。"
        f"{row['名称']}の平日開園時間は{row['平日開園時間']}。"
        f"{row['名称']}の平日閉園時間は{row['平日閉園時間']}。"
        f"{row['名称']}の土曜開園の有無は{row['土曜開園の有無']}。"
        f"{row['名称']}の教育・保育の内容や特徴は{row['教育・保育の内容や特徴']}。"
        f"{row['名称']}の実費徴収は{row['実費徴収']}。"
        f"{row['名称']}の標準の延長保育は{row['標準の延長保育']}。"
        f"{row['名称']}の短時間保育は{row['短時間保育']}。"
        f"{row['名称']}の特別指導は{row['特別指導']}。"
        f"{row['名称']}のホームページURLは{row['ホームページ']}。"
        "そして次の保育園の情報を紹介します。"
    )

# 新しいカラムに非構造化データを格納
df['非構造化データ'] = df.apply(create_unstructured_text, axis=1)
description = df['非構造化データ'].str.cat()
description

~~~

出力結果は以下の通り

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3326463/3be34ea6-c417-d415-b2fd-0a3fa4befbaa.png)

### 6.オーバーラップ設定をしてチャンクへ分割

そして保育園情報をチャンク別に格納する。chunk_sizeは各チャンクの単語数。overlapは戻って処理するときの単語数。

例えば、以下の文章に関して、chunk_sizeが20でoverlapが5と設定されている場合には……
吉沼保育園の教育・保育の内容や特徴は知育＝日課活動やプリントあそびなどを行い、また、週1回、外国人講師による英語レッスンを取り入れる事で、幼児の基礎能力開発を進める。体育＝子ども達の発達段階に応じた体の動きを十分考慮し、健康で丈夫な身体とたくましい精神力を養うため、活発な運動を取り入れる。

チャンク1：吉沼保育園の教育・保育の内容や特徴は知育
チャンク2：特徴は知育＝日課活動やプリントあそびなど
チャンク3：あそびなどを行い、また、週1回、外国人講
チャンク4：、外国人講師による英語レッスンを取り入れ
チャンク5：を取り入れる事で、幼児の基礎能力開発を進

のように、overlapの単語数分戻って処理する。単純にテキストを分割すると単語の途中で途切れる可能性がある。ただオーバーラップすることで次のチャンクに単語の情報が含まれて文脈が保たれやすくなる。

~~~python
def chunk_text(text , chunk_size , overlap):
  # 少しずつ戻って処理をするために、overlapを設定している。
  chunks = []
  start = 0
  while start + chunk_size <= len(text):
    chunks.append(text[start:start+chunk_size])
    start += (chunk_size - overlap)
  if start < len(text):
    chunks.append(text[-chunk_size:])
  return chunks
~~~

### 7.保育園情報をベクトル化

text-embedding-3-smallモデルを使って、保育園情報をベクトル化する。response.data[0].embeddingにはEmbeddingされた文章ごとのベクトルデータが保管されている。

~~~python
def vectorize_text(text):
  response = client.embeddings.create(
      input = text,
      model = "text-embedding-3-small"
  )
  return response.data[0].embedding
~~~

### 8.質問とベクトルDBの類似度を計算

question_vectorが質問をベクトル化したものであり、vectorsはベクトルDBである。質問とベクトルDBのそれぞれの類似度を調べている。

~~~python
def find_most_similar(question_vector,vectors,documents):

  similarities = []
  for index ,vector in enumerate(vectors):
    similarity =  cosine_similarity([question_vector],[vector])[0][0]
    similarities.append([similarity,index])

  similarities.sort(reverse=True , key=lambda x:x[0])
  top_documents = [documents[index] for similarity , index in similarities[:2]]

  return top_documents
~~~

### 9.質問のベクトル化と返答をする関数を設計

OpenAIのAPIを使って、プロンプトの設計とモデルの指定を行っている。

~~~python
def ask_question(qustion,context):
  prompt = f'''
  以下の質問に以下の情報をベースにして答えてください。
  [ユーザーの質問]
  {question}

  [情報]
  {context}
  '''
  print(prompt)

  response = client.completions.create(
      model = "gpt-3.5-turbo-instruct",
      prompt = prompt,
      max_tokens=500
  )

  return response.choices[0].text
~~~

### 10.今までの流れをパイプライン化

これまでのベクトルDB作成から答えの出力までの一連の流れをパイプライン化する

~~~python
def qa_pipeline(description , question , chunk_size , overlap):
  text_chunks = chunk_text(description , chunk_size , overlap)
  vectors = [vectorize_text(doc) for doc in text_chunks]
  question_vector = vectorize_text(question)
  similar_documents = find_most_similar(question_vector , vectors , text_chunks)
  response = ask_question(question , similar_documents)
  return response , similar_documents
~~~

### 11.各種値を指定して、結果を出力

チャンクサイズ、オーバーラップ、質問を設定して結果を出力させている。これで質問への回答がきちんとできることが確認できました。
~~~python
chunk_size = 400
overlap = 50
question = df_qa.iloc[0,0]

response , similar_documents = qa_pipeline(description , question , chunk_size , overlap)
print(response)
~~~

出力結果は以下の通り

以下の質問に以下の情報をベースにして答えてください。
  [ユーザーの質問]
  吉沼保育園の所在地は？

  [情報]
  ['保育園の名称は吉沼保育園。吉沼保育園の所在地は吉沼1110番地1。吉沼保育園の定員は180名。吉沼保育園の電話は865-0050。～～～（以下略）']

吉沼保育園の所在地は、吉沼1110番地1です。

### 12.評価をするためのデータセット作成

RAGの評価をしてもらうためにはデータセットを作成する必要があります。
そのため、まずは11と同じ手順で解答してほしい質問に対してLLMの回答を生成させます。

次に以下のようにカテゴリ分けをしたうえで、データセットを作成します。
questions：質問内容
reponses：LLMの回答内容
references：正しい答え
contexts：LLMが回答する際に利用したコンテキスト

~~~python
q_start = 0
q_end = 11

questions = df_qa.iloc[q_start:q_end,0].to_list()
references = df_qa.iloc[q_start:q_end,1].to_list()

contexts = []
responses = []

for question in questions:
  response,similar_documents = qa_pipeline(description , question , chunk_size , overlap)
  responses.append(response)
  contexts.append(similar_documents)

ds = Dataset.from_dict(
    {
        "question": questions,
        "response": responses,
        "contexts": contexts,
        "reference": references,
    } )
~~~

### 13.RAGASを使って評価を行う

metricsに評価指標を格納しておき、evaluateで評価を行う。そして各評価指標の平均値をdf_meanへ格納している。

~~~python
metrics = [
    context_precision,
    answer_relevancy,
    faithfulness,
    context_recall,
    context_entity_recall,
    answer_similarity,
    answer_correctness
]

result = evaluate(ds, metrics=metrics)
df_result = result.to_pandas()
df_mean = df_result.iloc[:,4:].mean()
df_mean
~~~

結果は以下の通り。全体平均は0.6696387143でした。

| 評価指標 | 数値 |
|:-----------|:------------|
| context_precision |0.818182|
| answer_relevancy  | 0.860757|
| faithfulness  | 0.771032 |
| context_recall | 0.659091 |
| context_entity_recall | 0.295455 |
| semantic_similarity | 0.851277 |
| answer_correctness | 0.431677 |

目視で結果を確認すると、11問中7問正解、4問不正解でした。

# RAGの修正

次にこの章でRAGに対していろいろな修正を加えてみて、どんな結果になるのかを一緒に見ていければと思います。

## 1.チャンクサイズを400から1000へ

現在はチャンクサイズが400だが、1000にしたらより文脈の意味を理解できると考えた。

~~~python
chunk_size = 400
overlap = 50
q_start = 0
q_end = 11

#以下へ変更
chunk_size = 1000
overlap = 50
q_start = 0
q_end = 11
~~~

| 評価指標 | 数値 |
|:-----------|:------------|
| context_precision |0.385972|
| answer_relevancy  |0.6015|
| faithfulness  |0.643154|
| context_recall |0.355|
| context_entity_recall |0.059091|
| semantic_similarity |0.83227|
| answer_correctness |0.37015|

全体平均は0.5966661429となっており、改善はみられなかった
目視で結果を確認すると、11問中4問正解、7問不正解でした。

## 2.選択するチャンクの数を2から5へ

関連するチャンクを選ぶ際に、5つのチャンクを選ぶように変更しました。
~~~python
def find_most_similar(question_vector,vectors,documents):
  similarities = []
  for index ,vector in enumerate(vectors):
    similarity =  cosine_similarity([question_vector],[vector])[0][0]
    similarities.append([similarity,index])
  similarities.sort(reverse=True , key=lambda x:x[0])
  top_documents = [documents[index] for similarity , index in similarities[:2]]
  return top_documents

#以下へ変更
def find_most_similar(question_vector,vectors,documents):
  similarities = []
  for index ,vector in enumerate(vectors):
    similarity =  cosine_similarity([question_vector],[vector])[0][0]
    similarities.append([similarity,index])
  similarities.sort(reverse=True , key=lambda x:x[0])
  top_documents = [documents[index] for similarity , index in similarities[:5]]
  return top_documents
~~~

|評価指標|数値|
|:----|:----|
|context_precision|0.806439|
|answer_relevancy|0.883555|
|faithfulness|0.666515|
|context_recall|0.659091|
|context_entity_recall|0.212121|
|semantic_similarity|0.855031|
|answer_correctness|0.437931|

全体平均は0.64となり0.05ポイント減少をした。
目視で結果を確認すると、11問中6問正解、5問不正解でした。

## 3.Embeddingモデルの変更

text-embedding-3-smallモデルを性能が良いといわれているtext-embedding-ada-002へ変更。

~~~python
def vectorize_text(text):
  response = client.embeddings.create(
      input = text,
      model = "text-embedding-3-small"
  )
  return response.data[0].embedding

#以下へ変更
def vectorize_text(text):
  response = client.embeddings.create(
      input = text,
      model = "text-embedding-ada-002"
  )
  return response.data[0].embedding
  ~~~
|評価指標|数値|
|:----|:----|
|context_precision|0.681818|
|answer_relevancy|0.714809|
|faithfulness|0.672009|
|context_recall|0.609091|
|context_entity_recall|0.272727|
|semantic_similarity|0.852692|
|answer_correctness|0.378577|

全体平均は0.597となっており、正答率も11問中6問と改善は見られなかった。

## 4.プロンプトの変更

正しい情報を出力できるようプロンプトに変更を加えた

~~~python
  prompt = f'''
  以下の質問に以下の情報をベースにして答えてください。
  [ユーザーの質問]
  {question}
  [情報]
  {context}
  '''

#以下のように変更
  prompt = f'''
  #条件
  あなたはつくば市の保育園に関するエキスパートです。
  #制約条件
  ・与えられた情報から正しいものを選択して答えてください。
  ・質問の中に保育園の名前が含まれる場合には、その保育園の情報を適切に答えてください。
  #依頼
  以下の質問に以下の情報をベースにして答えてください。
  [ユーザーの質問]
  {question}
  [情報]
  {context}
  '''
~~~

|評価指標|数値|
|:----|:----|
|context_precision|0.818182|
|answer_relevancy|0.885731|
|faithfulness|0.682684|
|context_recall|0.654545|
|context_entity_recall|0.272727|
|semantic_similarity|0.838835|
|answer_correctness|0.331812|

全体平均は0.64となっており、正答率は11問中3問だった。

## 5.最新版のopenaiを利用

実はopenaiのバージョン1.25.1を利用していたため、最新版に対応させたopenaiを利用。

~~~python
#最新のopenaiのバージョンへ変更

import openai
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from datasets import Dataset  # Huggingface Datasetライブラリ

os.environ["OPENAI_API_KEY"] = userdata.get("OPEN_API_KEY")
openai = OpenAI()

# テキストをチャンク分割する関数
def chunk_text(text, chunk_size, overlap):
    chunks = []
    start = 0
    while start + chunk_size <= len(text):
        chunks.append(text[start:start+chunk_size])
        start += (chunk_size - overlap)
    if start < len(text):
        chunks.append(text[-chunk_size:])
    return chunks

# テキストをベクトル化する関数
def vectorize_text(text):
    response = openai.embeddings.create(
        input=text,
        model="text-embedding-3-small"  # 最新で使用可能な埋め込みモデル
    )
    return response.data[0].embedding  # 辞書アクセスから属性アクセスに修正

# 質問とドキュメントのベクトルの類似度を計算し、最も類似度が高いものを抽出する関数
def find_most_similar(question_vector, vectors, documents):
    similarities = []
    for index, vector in enumerate(vectors):
        similarity = cosine_similarity([question_vector], [vector])[0][0]
        similarities.append((similarity, index))
    similarities.sort(reverse=True, key=lambda x: x[0])
    top_documents = [documents[index] for _, index in similarities[:2]]
    return top_documents

# 質問に応答する関数
def ask_question(question, context):
    prompt = f'''
    以下の質問に以下の情報をベースにして答えてください。
    [ユーザーの質問]
    {question}
    [情報]
    {context}
    '''
    response = openai.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=500
    )
    return response.choices[0].message.content

# QAパイプラインの実行関数
def qa_pipeline(description, question, chunk_size, overlap):
    text_chunks = chunk_text(description, chunk_size, overlap)
    vectors = [vectorize_text(doc) for doc in text_chunks]
    question_vector = vectorize_text(question)
    similar_documents = find_most_similar(question_vector, vectors, text_chunks)
    response = ask_question(question, similar_documents)
    return response, similar_documents

# データの設定
chunk_size = 400
overlap = 50
q_start = 0
q_end = 11

# CSVファイルからQ&Aデータを読み込む
df_qa = pd.read_csv("/content/drive/MyDrive/データサイエンス/RAG/1031_つくば市保育園チャット/1104_Q&A.csv")
questions = df_qa.iloc[q_start:q_end, 0].to_list()
references = df_qa.iloc[q_start:q_end, 1].to_list()

# 実行結果を保存するリスト
contexts = []
responses = []

# 各質問に対してQAパイプラインを実行
for question in questions:
    response, similar_documents = qa_pipeline(description, question, chunk_size, overlap)
    responses.append(response)
    contexts.append(similar_documents)

# Huggingface Dataset形式で保存
ds = Dataset.from_dict(
    {
        "question": questions,
        "response": responses,
        "contexts": contexts,
        "reference": references,
    }
)

# メトリクスリスト
metrics = [
    context_precision,
    answer_relevancy,
    faithfulness,
    context_recall,
    context_entity_recall,
    answer_similarity,
    answer_correctness,
]

# 評価を実行
result = evaluate(ds, metrics=metrics)
df_result = result.to_pandas()
df_mean = df_result.iloc[:, 4:].mean()
print(df_mean)

# CSVに結果を保存
df_save = ds.to_pandas()
df_save.to_csv("No9.csv", index=False)
~~~

|評価指標|数値|
|:----|:----|
|context_precision|0.772727|
|answer_relevancy|0.736517|
|faithfulness|0.716667|
|context_recall|0.681818|
|context_entity_recall|0.234848|
|semantic_similarity|0.859183|
|answer_correctness|0.376694|

全体評価は0.62となっており、正答率は11問中6問。

## 6.最新版のopenai+HyDEの利用

HyDE（Hypothetical Document Embeddings：仮説文書の埋め込み）とは、質問に対して仮の回答を生成し、その仮の回答と検索対象の類似度を比較するモデルです。これにより検索精度の向上につながるとされています。

~~~python
#最新のopenai+Hyde

import openai
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
from datasets import Dataset  # Huggingface Datasetライブラリ
import os

os.environ["OPENAI_API_KEY"] = userdata.get("OPEN_API_KEY")
openai = OpenAI()

# テキストをチャンク分割する関数
def chunk_text(text, chunk_size, overlap):
    chunks = []
    start = 0
    while start + chunk_size <= len(text):
        chunks.append(text[start:start+chunk_size])
        start += (chunk_size - overlap)
    if start < len(text):
        chunks.append(text[-chunk_size:])
    return chunks

# HyDEで仮説ドキュメントを生成する関数
def generate_hypothetical_document(question):
    prompt = f"以下の質問に対する仮説的な回答を生成してください:\n質問: {question}"
    response = openai.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=150
    )
    return response.choices[0].message.content

# テキストをベクトル化する関数
def vectorize_text(text):
    response = openai.embeddings.create(
        input=text,
        model="text-embedding-ada-002"  # 最新で使用可能な埋め込みモデル
    )
    return response.data[0].embedding  # 辞書アクセスから属性アクセスに修正

# 質問とドキュメントのベクトルの類似度を計算し、最も類似度が高いものを抽出する関数
def find_most_similar(question_vector, vectors, documents):
    similarities = []
    for index, vector in enumerate(vectors):
        similarity = cosine_similarity([question_vector], [vector])[0][0]
        similarities.append((similarity, index))
    similarities.sort(reverse=True, key=lambda x: x[0])
    top_documents = [documents[index] for _, index in similarities[:5]]
    return top_documents

# 質問に応答する関数
def ask_question(question, context):
    prompt = f'''
    以下の質問に以下の情報をベースにして答えてください。
    [ユーザーの質問]
    {question}
    [情報]
    {context}
    '''
    response = openai.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=500
    )
    return response.choices[0].message.content

# QAパイプラインの実行関数
def qa_pipeline(description, question, chunk_size, overlap):
    text_chunks = chunk_text(description, chunk_size, overlap)
    vectors = [vectorize_text(doc) for doc in text_chunks]
    
    # HyDEで仮説的なドキュメントを生成し、それを質問ベクトルとして使用
    hypothetical_doc = generate_hypothetical_document(question)
    question_vector = vectorize_text(hypothetical_doc)
    
    similar_documents = find_most_similar(question_vector, vectors, text_chunks)
    response = ask_question(question, similar_documents)
    return response, similar_documents

# データの設定
chunk_size = 400
overlap = 50
q_start = 0
q_end = 11

# CSVファイルからQ&Aデータを読み込む
df_qa = pd.read_csv("/content/drive/MyDrive/データサイエンス/RAG/1031_つくば市保育園チャット/1104_Q&A.csv")
questions = df_qa.iloc[q_start:q_end, 0].to_list()
references = df_qa.iloc[q_start:q_end, 1].to_list()

# 実行結果を保存するリスト
contexts = []
responses = []

# 各質問に対してQAパイプラインを実行
for question in questions:
    response, similar_documents = qa_pipeline(description, question, chunk_size, overlap)
    responses.append(response)
    contexts.append(similar_documents)

# Huggingface Dataset形式で保存
ds = Dataset.from_dict(
    {
        "question": questions,
        "response": responses,
        "contexts": contexts,
        "reference": references,
    }
)

# メトリクスリスト
metrics = [
    context_precision,
    answer_relevancy,
    faithfulness,
    context_recall,
    context_entity_recall,
    answer_similarity,
    answer_correctness,
]

# 評価を実行
result = evaluate(ds, metrics=metrics)
df_result = result.to_pandas()
df_mean = df_result.iloc[:, 4:].mean()

# CSVに結果を保存
df_save = ds.to_pandas()
df_save.to_csv("No9.csv", index=False)
df_mean
~~~

|評価指標|数値|
|:----|:----|
|context_precision|0.662626|
|answer_relevancy|0.663047|
|faithfulness|0.59697|
|context_recall|0.681818|
|context_entity_recall|0.295455|
|semantic_similarity|0.853803|
|answer_correctness|0.55436|

全体の平均は0.615で正答率は11問中6問だった。

# サマリー
さまざまな手法を試したが、結局最初の方法がRAGASの評価的にも目視での正答率もどちらも高かった。<br>

最新の手法や制度を改善するやり方などがさまざま存在しているが、結局どんな質問に対してどんな答えをしてほしいのか、そしてどんなデータが入っているのかによって適した方法などがあるのかもと感じた。