🙌

一番手早く自社サービスの賢いサポートボットを作る方法(openai.Embedding+pinecone+SlashGPT)

2023/09/18に公開

自社サービスの最新情報にもとづくサポートボットを作りたいというのはよくある話だとおもいます。これを openai.embedding+pinecone+SlashGPTでサクッと実現する方法を紹介します。

準備

仕組み概要

OpenAI GPTは2021 9月時点のインターネット、SNSの情報をもとに学習した内容で回答を作成します。その時点で存在していなかった、もしくは更新された 自社サービスの知識をGPTに教える必要があります。短い前提知識であれば質問とあわせて全文入力すればよいですが、2048 token 以上のものは一度に入力できません。そんな場合は下記の方法で 適切な情報のみインプットして回答を得ることができます。

  • まず自社サービスの知識を項目ごとに分割して文章で用意する
  • それぞれの項目に対してEmbeddingしたvector値を計算し、保存しておく
  • ユーザーが特定の質問した際には、その質問文のEmbeddingしたvectorを計算し、事前に計算済みの検索対象の各項目のEmbeddingと比較した上で、近いものからN項目だけを選ぶ
  • 選ばれたもとの文章をプロンプトの一部としてGPTに読み込ませた上で、その読み込ませた文章を使って、ユーザーからの質問に答えるように GPTに指示を出す

このあと、一つずつ解説していきます

SlashGPTとは?

SlashGPT中島聡が開発したChatGPTなどのLLMエージェントを手軽に開発するためのツールです。SlashGPTを使えば、jsonファイルを記述するだけでChatGPTを使ったLLMエージェントやチャットアプリを手軽に、簡単につくることができます。

ChatGPTに自社サービスの最新情報を教える方法

中島聡メルマガ2023/8/8より引用)

人工知能にドメイン・ノリッジを与える方法は複数ありますが、大きく分けると、以下の3つになります。
 
 LLMモデルをゼロから構築
 学習済みのLLMモデルのファイン・チューニング(微調整)
 プロンプト・エンジニアリング
 1つ目のLLMモデルの構築は、技術的にも難しいし、何よりも機械学習の過程に莫大なコストがかかります。モデルそのものは、MetaのLlama2のようにオープンソース化されたものがあるので、その中で適切なものを選べば十分ですが、ちゃんとしたLLMとして使えるようにするには、業務用のデータだけでなく、言葉や一般教養を学習させるための膨大なデータを教育データとして与える必要があります。幸いなことに、データに関しても、オープンなものが沢山あるのでそれが使えますが、学習プロセスには莫大な計算資源が必要であるため、数千万円から数億円の出費(モデルの大きさ、データの量によって大きく異なります)を覚悟しなければなりません。
 
 このアプローチで作られたもので良く知られているのは、経済・金融情報サービスの会社Bloombergが作ったBloombergGPTです。
 
 2番目の手法は、既に基本的な言語や一般教養に関してトレーニング済みのLLMに、業務用のデータを与えて学習させル、ファイン・チューニングと呼ばれる手法です。
 
 ファイン・チューニングには、大きく分けて二つの方法があり、一つ目は、モデルのパラメータ全てをアップデートするフル・ファイン・チューニングと呼ばれる手法で、もう一つは、よりパラメータ数の少ないニューラルネットワークを作り、そちらのパラメータだけをアップデートする、Parameter efficient fine-tuning(PEFT)と呼ばれる手法です。
 
 フル・ファイン・チューニングの方は、LLMが持つパラメータ全てをアップデートするため、パラメータ数の多いLLMで行う場合には、莫大なコストがかかるのが欠点です。さらに、パラメータ全体をアップデートするため、ファイン・チューニングの結果、それまで答えられていた質問にまともに答えられなくなってしまう、「Catastrophic failure」という障害を起こしてしまうリスクがあります。
 
 PEFTは、はるかに少ないパラメータだけをアップデートするため、コストは大幅に安いし、ベースとなるLLMのパラメータは変更しないので、「Catastrophic failure」も起こりません。PEFTについてより深く勉強したい人は、LoRA: Low-Rank Adaptation of Large Language Modelsという論文を読むことをお勧めします。PEFTを実現する手法の一つである LoRA について詳しく解説しています。
 
 3番目の手法は、LLMは汎用なものをそのまま使い、LLMに与えるプロンプトの中に、業務に特化したデータを必要に応じて引っ張って来て与える手法です。以前に、このメルマガで解説した、Embedding Databaseを使って質問文と関連する文章を見つけてきて、それをプロンプトの中に含めて、LLMに応えさせる手法が、これに相当します。

Embeddingとは

単語、文章を N次元のベクトルに変換することです。 どのような文章が入力されたときにどのように変換するかは、機械学習の学習結果によります。 今回は OpenAIが提供している"text-embedding-ada-002" というモデルを利用します。 このモデルは1536次元のVectorデータに変換してくれます。

自社サービス情報整理

こちらはどのような手法で用意してもよいのですが、
サポートの回答につかえそうな粒度で、区切った情報を用意します。
こんかいはすでにWebページでマニュアル等が公開されていることを想定して、WebPageからクロールして整形します。

ChatGPTでクローラー作成

ここでも時間短縮のため、ChatGPTに雛形のコードをつくってもらいます。

pythonで、beatifulsoup を利用して、h1, h2,h3,h4,h5 を抜き出して、 h1-h2-h3-h4-h5 のようにタイトルとして保存し、 そのなかに含まれる要素のテキストを value として 保存するプログラムを作成して

出力されたコードを若干手直しをして、以下のようなコードで情報を読み込みます

import requests
from bs4 import BeautifulSoup
import json
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
import re
import os

# URL of the your website 
url = 'https://XXX.com'

# Set Chrome options to enable headless mode
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')

# Set the path to chromedriver executable
chromedriver_path = '/usr/local/bin/chromedriver'

# Create a new Chrome instance
driver = webdriver.Chrome(options=chrome_options)

# Load the website
driver.get(url)

# Wait for the page to fully render
time.sleep(5)

# Extract the rendered HTML
html = driver.page_source

# Close the Chrome instance
driver.quit()

# Parse the HTML using BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')

data = {}

# h1〜h4のタグを取得
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5'])
(h1_text,h2_text,h3_text,h4_text,h5_text)=("","","","","")
for heading in headings:
    if heading.name == 'h1':
        h1_text = heading.text
        key = h1_text
    elif heading.name == 'h2':
        h2_text = heading.text
        key = f"{h1_text}-{h2_text}"
    elif heading.name == 'h3':
        h3_text = heading.text
        key = f"{h1_text}-{h2_text}-{h3_text}"
    elif heading.name == 'h4':
        h4_text = heading.text
        key = f"{h1_text}-{h2_text}-{h3_text}-{h4_text}"
    elif heading.name == 'h5':
        h5_text = heading.text
        key = f"{h1_text}-{h2_text}-{h3_text}-{h5_text}"

    # 次の要素のテキストを取得
    sibling = heading.find_next_sibling()
    value = ''
    while sibling and not sibling.name in ['h1', 'h2', 'h3', 'h4', 'h5']:
        value += sibling.text
        sibling = sibling.find_next_sibling()

    data[key] = value.strip()

print(len(data),(data.keys()))

Embeddingする

下記のAPIを呼び出して、自社サービスの知識をVector化します。
openai.Embedding.create(model=EMBEDDING_MODEL, input=batch)
Modelは2023夏時点でbestな"text-embedding-ada-002"を選択します。

https://platform.openai.com/docs/api-reference/embeddings/create

# split sections into chunks
MAX_TOKENS = 8000
webpage_strings = []
for k in data.keys():
    webpage_strings.extend(split_strings_from_subsection(([k],data[k]), max_tokens=MAX_TOKENS))

print(f"{len(data)} page  split into {len(webpage_strings)} strings.")
# calculate embeddings
EMBEDDING_MODEL = "text-embedding-ada-002"  # OpenAI's best embeddings as of Apr 2023
BATCH_SIZE = 1000  # you can submit up to 2048 embedding inputs per request

embeddings = []
for batch_start in range(0, len(webpage_strings), BATCH_SIZE):
    batch_end = batch_start + BATCH_SIZE
    batch = webpage_strings[batch_start:batch_end]
    print(f"Batch {batch_start} to {batch_end-1}")
    response = openai.Embedding.create(model=EMBEDDING_MODEL, input=batch)
    for i, be in enumerate(response["data"]):
        assert i == be["index"]  # double check embeddings are in same order as input
    batch_embeddings = [e["embedding"] for e in response["data"]]
    embeddings.extend(batch_embeddings)

df = pd.DataFrame({"text": webpage_strings, "embedding": embeddings})

このdfにはもとのTextと、1536次元で表現されたvectorが格納されています。

                                                text  \
0                              Console ユーザーマニュアル\n\n   
1  Console ユーザーマニュアル--1. はじめに\n\n本章では、本書の概要、前提につい...   
2  Console ユーザーマニュアル--1. はじめに-1.1. 本書の概要\n\n本書は、A...   
3  Console ユーザーマニュアル--1. はじめに-1.2. 推奨環境と利用時の注意\n\...   
4  Console ユーザーマニュアル--2. Console UIの概要\n\nUIの概要を以...   

                                           embedding  
0  [-0.004639128688722849, -0.02295255847275257, ...  
1  [-0.006827681791037321, -0.011568747460842133,...  
2  [-0.004683870822191238, -0.019036564975976944,...  
3  [-0.00759622547775507, -0.01269635558128357, 0...  
4  [-0.015903225168585777, -0.010579969733953476,...  

split_strings_from_subsection の具体的な実装は下記本家のサンプルを参照してください。
(それぞれの文章がmax_tokenに収まるように分割しています)

pineconeに格納

pineconeというvector dbのSaaSに保存します。 pineconeは、vectorを保存、vector 同士の類似度で検索をすることをサポートしてます。(無料プランだとindexを一つしかつくれないので、 別のものを試したい場合はweb consoleから過去のものを削除する必要があります。またアクセスしていないと7日後に削除されます。Productionで使用する場合は https://www.pinecone.io/pricing/ を参照)

pineconeのwebsiteでアカウントを作成し、APIKeyを発行して取得したKEYと、environmentを .env ファイルに それぞれ "PINECONE_API_KEY","PINECONE_ENVIRONMENT" という名前で保存しておきます。
下記のコードで table, index を作成します。

import pinecone
from dotenv import load_dotenv
from pinecone import QueryResponse

# Configuration
load_dotenv()  # Load default environment variables (.env)
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY", "")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT", "")

# Initialize Pinecone
pinecone.init(api_key=PINECONE_API_KEY, environment=PINECONE_ENVIRONMENT)
# Create Pinecone index
table_name = "XXX"
dimension = 1536
metric = "cosine"
pod_type = "p1"
if table_name not in pinecone.list_indexes():
    print("Creating pinecone index")
    pinecone.create_index(table_name, dimension=dimension, metric=metric, pod_type=pod_type)

# Connect to the index
index = pinecone.Index(table_name)

index が作成できたら、 事前に作成しておいた embeddings vector をupsertします。

def load_vectors():
    print("loadig CSV file")
    embeddings_path = SAVE_PATH

    df = pd.read_csv(embeddings_path)

    # convert embeddings from CSV str type back to list type
    print("converting it to list type")
    df["embedding"] = df["embedding"].apply(ast.literal_eval)
    print(df)

    print("writing vectors")
    vectors = [(f"id_{i}", row["embedding"], {"text": row["text"]}) for i, row in df.iterrows()]
    # print(vectors)
    for vector in vectors:
        index.upsert([vector])
load_vectors()

SlashGPTでAgentをサクッと作成

これで下準備は完了しました。
あとは、ユーザーからの質問を受け取って、 その内容をembeddings して、 このvector_db の中から類似のものを検索して、結果で取得できたTextを再度ChatGPTにわたして質問の回答をえます。

この処理は内容に関わらず汎用的な処理なため、SlashGPTを利用すると manifestを記述するだけで実現できます。

以下のように embeddings に db_type:pinecone name:{table_name}に指定して
promptに 検索結果と一緒にChatGPTにわたす文字列を指定するだけです! 簡単ですね!

manifest例
{
  "title": "XXX support bot",
  "bot": "XXservice",
  "about": "kozayupapa",
  "sample": "XXX サービス は何ができるんですか",
  "model": "gpt-3.5-turbo-16k-0613",
  "embeddings": {
    "db_type": "pinecone",
    "name": "XXX"
  },
  "temperature": 0,
  "prompt": [
    "Use the below articles on the XXX to answer the subsequent question. If the answer cannot be found in the articles, write 'I could not find an answer.'",
    "{articles}"
  ]
}
  • このmanifest ファイルを CloneしたSlashGPT repo 以下 manifest/roles1/ 以下に配置
  • 起動 % python ./SlashGPT.py
  • manifests dirを変更 You(dispatcher): /switch roles1
  • manifest を選択 You(GPT): /XXX
  • あとは 実際の質問プロンプトを入力するだけです。以下のようにサービスに特化した内容も答えてくれます!

例1: 2021年時点ではなかったサービスについて聞いてみる

You(aitrios): aitrios console は何ができるんですか

XXservice: AITRIOS Consoleは、以下の機能を提供しています。
1. エッジAIデバイスの登録と解除: デバイス証明書を使用してエッジAIデバイスをConsoleに登録または解除することができます。
2. エッジAIデバイスの管理: エッジAIデバイスの情報を確認し、推論の開始、再起動、リセットなどの操作を行うことができます。
3. 登録済みAIモデル/ファームウェアのデプロイ: 登録済みのAIモデルやファームウェアをエッジAIデバイスにデプロイすることができます。
4. AIモデルの作成と更新: AIモデルを作成し、Consoleに登録することができます。また、既存のAIモデルを更新することも可能です。
5. エッジAIデバイスからのデータ確認: エッジAIデバイスからクラウドへのデータのアップロード状況や推論結果の確認ができます。
6. マスタ情報の更新: エッジAIデバイスの設定やファームウェア、アプリケーションの更新を行うことができます。

例2:サービスで使われている一般的な用語をサービスの文脈に沿って説明してくれるか確認

You(aitrios): PTQ,QAT について教えてください
Xservice: PTQ(Post-Training Quantization)とQAT(Quantization-Aware Training)は、AIモデルの量子化(Quantization)という手法に関連しています。

1. PTQ(Post-Training Quantization):
   PTQは、学習済みの浮動小数点モデルを量子化する手法です。通常、AIモデルは浮動小数点数を使用してパラメータや活性化値を表現しますが、PTQではこれらの値を整数に近似することでモデルを量子化します。量子化により、モデルのサイズを小さくし、推論の効率を向上させることができます。PTQは学習後に適用されるため、既存の学習済みモデルに対して後処理として適用されます。

2. QAT(Quantization-Aware Training):
   QATは、量子化を考慮した学習(Quantization-Aware Training)を行う手法です。通常の学習では浮動小数点数を使用しますが、QATではモデルの訓練中に整数値に近い形式でパラメータや活性化値を扱います。これにより、モデルが量子化に対してロバストになり、量子化後の性能を向上させることができます。QATは学習中に量子化の影響を考慮するため、モデルの精度を維持しながら量子化を行うことができます。

PTQとQATは、AIモデルの量子化による効率化やデプロイメントの容易化を目的としています。量子化により、モデルのメモリ使用量や計算コストを削減し、エッジデバイスやリソース制約のある環境での推論の高速化や省電力化が可能となります。ただし、量子化による情報の損失が発生するため、適切な量子化手法とパラメータの調整が重要です。

まとめ

以上です。 いかがでしたでしょうか?
後半の質問に対して回答する部分はSlashGPTを利用するとあっけないほど簡単に実現できてしまいます。

前半の事前知識の区切り方、Input量が ユーザー質問への回答の質を決めるので チューニングポイントです。
後半について詳しく実装を見てみたいという方は下記のあたりを参照してください。
https://github.com/snakajima/SlashGPT/blob/main/src/slashgpt/dbs/pinecone.py
https://github.com/snakajima/SlashGPT/blob/main/src/slashgpt/chat_session.py

シンギュラリティ・ソサエティ

Discussion