OpenAI の ChatGPT が非常に知的であることは疑いの余地がありません。弁護士の司法試験に合格し、医師と同等の知識を持ち、一部のテストではIQ 155 を記録しました。しかし、無知を認める代わりに情報を捏造する傾向があります。この傾向は、その知識が 2021 年に終了するという事実と相まって、GPT API を使用して特殊な製品を構築する際に課題を引き起こしています。
どうすればこれらの障害を克服できるでしょうか? GPT-3 のようなモデルに新しい知識を与えるにはどうすればよいでしょうか?私の目標は、Python、OpenAI API、および単語埋め込みを使用して質問応答ボットを構築することで、これらの質問に対処することです。
プロンプトから継続的インテグレーションパイプラインを生成するボットを作成する予定です。ご存知のとおり、このパイプラインは Semaphore CI/CD のYAML でフォーマットされています。
ボットが動作している例を次に示します。
実行中のプログラムのスクリーンショット。画面上で、コマンドpython query.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub"
実行され、プログラムは要求されたアクションを実行する CI パイプラインに対応する YAML を出力します。
DocsGPT 、 My AskAI 、 Librariaなどのプロジェクトの精神に基づいて、セマフォとパイプライン構成ファイルの生成方法について GPT-3 モデルに「教える」予定です。既存のドキュメントを活用してこれを実現します。
ボット構築に関する事前知識は前提としていません。要件に適応できるようにクリーンなコードを維持します。
このチュートリアルを実行するには、ボットのコーディングの経験やニューラル ネットワークの知識は必要ありません。ただし、次のものが必要になります。
ChatGPT、より正確には GPT-3 と GPT-4、それらを支える大規模言語モデル (LLM) は、2021 年 9 月頃のカットオフ日を設定して大規模なデータセットでトレーニングされています。
本質的に、GPT-3 はその日付以降の出来事についてほとんど知りません。これは簡単なプロンプトで確認できます。
ChatGPT は、2022 年のワールドカップで誰が優勝したかを知りません。
一部の OpenAI モデルは微調整できますが、関心のあるモデルなどのより高度なモデルは微調整できません。彼らのトレーニングデータを増強することはできません。
GPT-3 からトレーニング データ以外の答えを得るにはどうすればよいでしょうか? 1 つの方法には、テキスト理解能力を活用することが含まれます。関連するコンテキストを使用してプロンプトを強化することで、正しい答えが得られる可能性があります。
以下の例では、 FIFA の公式サイトからコンテキストを提供していますが、応答は大きく異なります。
提供されたコンテキストを使用して、ChatGPT は正確に応答できます。
関連するコンテキストが十分に与えられていれば、モデルはあらゆるプロンプトに応答できると推測できます。疑問は残ります。任意のプロンプトが与えられた場合、何が関連しているかをどうやって知ることができるのでしょうか?これに対処するには、単語の埋め込みとは何かを調査する必要があります。
言語モデルのコンテキストでは、埋め込みとは、単語、文、またはドキュメント全体をベクトルまたは数値のリストとして表現する方法です。
埋め込みを計算するには、 word2vecやtext-embedding-ada-002などのニューラル ネットワークが必要です。これらのネットワークは大量のテキストでトレーニングされており、トレーニング データに特定のパターンが現れる頻度を分析することで単語間の関係を見つけることができます。
次のような単語があるとします。
これらの埋め込みネットワークの 1 つを使用して各単語のベクトルを計算すると想像してください。例えば:
言葉 | ベクター | コンテクスト |
---|---|---|
猫 | [0.1、0.2、0.3、0.4、0.5] | 動物、物、小物 |
犬 | [0.6、0.7、0.8、0.9、1.0] | 動物、物、大きなもの |
ボール | [0.2、0.4、0.6、0.8、1.0] | オブジェ、おもちゃ、小物 |
家 | [0.3、0.6、0.9、1.2、1.5] | 建物、家、大きなもの |
各単語のベクトルを取得したら、それを使用してテキストの意味を表すことができます。たとえば、「猫はボールを追いかけた」という文は、ベクトル [0.1, 0.2, 0.3, 0.4, 0.5] + [0.2, 0.4, 0.6, 0.8, 1.0] = [0.3, 0.6, 0.9, 1.2, 1.5] として表すことができます。このベクトルは、物体を追いかける動物についての文を表します。
単語の埋め込みは、同様の意味を持つ単語や文が近接している多次元空間として視覚化できます。ベクトル間の「距離」を計算して、入力テキストに対して同様の意味を見つけることができます。
ベクトル空間としてのエンベディングの 3D 表現。実際には、これらの空間は数百または数千の次元を持つ可能性があります。出典: AI のマルチツールの紹介: ベクトル埋め込み
このすべての背後にある実際の数学については、この記事の範囲を超えています。ただし、重要な点は、ベクトル演算により、数学を使用して意味を操作したり決定したりできるということです。 「女王」という単語を表すベクトルを取得し、そこから「女性」ベクトルを減算し、「男性」ベクトルを加算します。結果は「king」の近くのベクトルになるはずです。 「息子」を加えると「王子」に近づくはずです。
これまで、単語を入力、数値を出力として受け取るニューラル ネットワークの埋め込みについて説明してきました。ただし、現代のネットワークの多くは、単語の処理からトークンの処理に移行しています。
トークンは、モデルによって処理できるテキストの最小単位です。トークンは、単語、文字、句読点、記号、または単語の一部です。
OpenAI オンライン トークナイザーを試してみることで、単語がどのようにトークンに変換されるかを確認できます。このトークナイザーは、バイト ペア エンコーディング(BPE) を使用してテキストをトークンに変換し、それぞれを数字で表します。
多くの場合、トークンと単語の間には 1 対 1 の関係があります。ほとんどのトークンには、単語と先頭のスペースが含まれます。ただし、「embed」と「ding」の 2 つのトークンで構成される「embedding」や、4 つのトークンで構成される「capabilities」などの特殊なケースもあります。 [トークン ID] をクリックすると、各トークンのモデルの数値表現が表示されます。
埋め込みとは何かを理解したところで、次の疑問は、埋め込みがよりスマートなボットの構築にどのように役立つのかということです。
まず、GPT-3 API を直接使用すると何が起こるかを考えてみましょう。ユーザーがプロンプトを発行すると、モデルは可能な限りの応答を返します。
ただし、方程式にコンテキストを追加すると、状況は変わります。たとえば、背景を提供した後でワールドカップの勝者について ChatGPT に質問したところ、大きな違いが生じました。
したがって、よりスマートなボットを構築する計画は次のとおりです。
ほとんどのプロジェクトと同様に、データベースを設計することから始めましょう。
私たちのコンテキスト データベースには、元のドキュメントとそれぞれのベクトルが含まれている必要があります。原則として、このタスクには任意のタイプのデータベースを使用できますが、ベクトル データベースがこの作業に最適なツールです。
ベクトル データベースは、高次元ベクトル データを保存および取得するように設計された特殊なデータベースです。検索に SQL などのクエリ言語を使用する代わりに、ベクトルを提供し、N 個の最近傍を要求します。
ベクトルを生成するには、OpenAI のtext-embedding-ada-002を使用します。これは、OpenAI が提供する最も高速でコスト効率の高いモデルだからです。このモデルは入力テキストをトークンに変換し、 Transformerとして知られるアテンション メカニズムを使用してそれらの関係を学習します。このニューラル ネットワークの出力は、テキストの意味を表すベクトルです。
コンテキスト データベースを作成するには、次の手順を実行します。
まず、OpenAI API キーを使用して環境ファイルを初期化する必要があります。 API キーは非公開であり、アカウントに関連付けられているため、このファイルはバージョン管理にコミットしないでください。
export OPENAI_API_KEY=YOUR_API_KEY
次に、Python アプリケーション用の virtualenv を作成します。
$ virtualenv venv $ source venv/bin/activate $ source .env
そして、OpenAI パッケージをインストールします。
```bash $ pip install openai numpy
文字列「Docker Container」の埋め込みを計算してみましょう。これは、Python REPL 上で、または Python スクリプトとして実行できます。
$ python >>> import openai >>> embeddings = openai.Embedding.create(input="Docker Containers", engine="text-embedding-ada-002") >>> embeddings JSON: { "data": [ { "embedding": [ -0.00530336843803525, 0.0013223182177171111, ... 1533 more items ..., -0.015645816922187805 ], "index": 0, "object": "embedding" } ], "model": "text-embedding-ada-002-v2", "object": "list", "usage": { "prompt_tokens": 2, "total_tokens": 2 } }
ご覧のとおり、OpenAI のモデルは 1536 個の項目 (text-embedding-ada-002 のベクトル サイズ) を含むembedding
リストで応答します。
オープンソースのChromaなど、複数のベクター データベース エンジンから選択できますが、 Pineconeを選択したのは、無料利用枠のあるマネージド データベースであり、作業が簡単であるためです。彼らのスターター プランは、私が必要とするすべてのデータを十分に処理できます。
Pinecone アカウントを作成し、API キーと環境を取得した後、両方の値を.env
ファイルに追加します。
これで.env
に Pinecone と OpenAI のシークレットが含まれるはずです。
export OPENAI_API_KEY=YOUR_API_KEY # Pinecone secrets export PINECONE_API_KEY=YOUR_API_KEY export PINECONE_ENVIRONMENT=YOUR_PINECONE_DATACENTER
次に、Python 用の Pinecone クライアントをインストールします。
$ pip install pinecone-client
データベースを初期化する必要があります。これらはdb_create.py
スクリプトの内容です。
# db_create.py import pinecone import openai import os index_name = "semaphore" embed_model = "text-embedding-ada-002" api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, environment=env) embedding = openai.Embedding.create( input=[ "Sample document text goes here", "there will be several phrases in each batch" ], engine=embed_model ) if index_name not in pinecone.list_indexes(): print("Creating pinecone index: " + index_name) pinecone.create_index( index_name, dimension=len(embedding['data'][0]['embedding']), metric='cosine', metadata_config={'indexed': ['source', 'id']} )
スクリプトによるデータベースの作成には数分かかる場合があります。
$ python db_create.py
次に、 tiktokenパッケージをインストールします。これを使用して、ソース ドキュメントに含まれるトークンの数を計算します。埋め込みモデルは最大 8191 個のトークンしか処理できないため、これは重要です。
$ pip install tiktoken
パッケージをインストールするときに、見栄えの良い進行状況バーを作成するためにtqdm
もインストールしましょう。
$ pip install tqdm
次に、ドキュメントをデータベースにアップロードする必要があります。このスクリプトは、 index_docs.py
と呼ばれます。まず、必要なモジュールをインポートし、いくつかの定数を定義します。
# index_docs.py # Pinecone db name and upload batch size index_name = 'semaphore' upsert_batch_size = 20 # OpenAI embedding and tokenizer models embed_model = "text-embedding-ada-002" encoding_model = "cl100k_base" max_tokens_model = 8191
次に、トークンをカウントする関数が必要になります。 OpenAI ページにトークンカウンターの例があります。
import tiktoken def num_tokens_from_string(string: str) -> int: """Returns the number of tokens in a text string.""" encoding = tiktoken.get_encoding(encoding_model) num_tokens = len(encoding.encode(string)) return num_tokens
最後に、元のドキュメントを使用可能なサンプルに変換するために、いくつかのフィルタリング関数が必要になります。ドキュメント内のほとんどの例はコード フェンスの間にあるため、すべてのファイルからすべての YAML コードを抽出します。
import re def extract_yaml(text: str) -> str: """Returns list with all the YAML code blocks found in text.""" matches = [m.group(1) for m in re.finditer("```yaml([\w\W]*?)```", text)] return matches
これで機能は完了です。次に、ファイルをメモリにロードし、サンプルを抽出します。
from tqdm import tqdm import sys import os import pathlib repo_path = sys.argv[1] repo_path = os.path.abspath(repo_path) repo = pathlib.Path(repo_path) markdown_files = list(repo.glob("**/*.md")) + list( repo.glob("**/*.mdx") ) print(f"Extracting YAML from Markdown files in {repo_path}") new_data = [] for i in tqdm(range(0, len(markdown_files))): markdown_file = markdown_files[i] with open(markdown_file, "r") as f: relative_path = markdown_file.relative_to(repo_path) text = str(f.read()) if text == '': continue yamls = extract_yaml(text) j = 0 for y in yamls: j = j+1 new_data.append({ "source": str(relative_path), "text": y, "id": f"github.com/semaphore/docs/{relative_path}[{j}]" })
この時点で、すべての YAML がnew_data
リストに保存されている必要があります。最後のステップは、埋め込みを Pinecone にアップロードすることです。
import pinecone import openai api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, enviroment=env) index = pinecone.Index(index_name) print(f"Creating embeddings and uploading vectors to database") for i in tqdm(range(0, len(new_data), upsert_batch_size)): i_end = min(len(new_data), i+upsert_batch_size) meta_batch = new_data[i:i_end] ids_batch = [x['id'] for x in meta_batch] texts = [x['text'] for x in meta_batch] embedding = openai.Embedding.create(input=texts, engine=embed_model) embeds = [record['embedding'] for record in embedding['data']] # clean metadata before upserting meta_batch = [{ 'id': x['id'], 'text': x['text'], 'source': x['source'] } for x in meta_batch] to_upsert = list(zip(ids_batch, embeds, meta_batch)) index.upsert(vectors=to_upsert)
参考として、完全なindex_docs.pyファイルはデモリポジトリにあります。
インデックス スクリプトを実行して、データベースのセットアップを完了しましょう。
$ git clone https://github.com/semaphoreci/docs.git /tmp/docs $ source .env $ python index_docs.py /tmp/docs
Pinecone ダッシュボードには、データベース内のベクトルが表示されるはずです。
次のコードを使用してデータベースにクエリを実行できます。このコードは、スクリプトとして実行することも、Python REPL で直接実行することもできます。
$ python >>> import os >>> import pinecone >>> import openai # Compute embeddings for string "Docker Container" >>> embeddings = openai.Embedding.create(input="Docker Containers", engine="text-embedding-ada-002") # Connect to database >>> index_name = "semaphore" >>> api_key = os.getenv("PINECONE_API_KEY") >>> env = os.getenv("PINECONE_ENVIRONMENT") >>> pinecone.init(api_key=api_key, environment=env) >>> index = pinecone.Index(index_name) # Query database >>> matches = index.query(embeddings['data'][0]['embedding'], top_k=1, include_metadata=True) >>> matches['matches'][0] {'id': 'github.com/semaphore/docs/docs/ci-cd-environment/docker-authentication.md[3]', 'metadata': {'id': 'github.com/semaphore/docs/docs/ci-cd-environment/docker-authentication.md[3]', 'source': 'docs/ci-cd-environment/docker-authentication.md', 'text': '\n' '# .semaphore/semaphore.yml\n' 'version: v1.0\n' 'name: Using a Docker image\n' 'agent:\n' ' machine:\n' ' type: e1-standard-2\n' ' os_image: ubuntu1804\n' '\n' 'blocks:\n' ' - name: Run container from Docker Hub\n' ' task:\n' ' jobs:\n' ' - name: Authenticate docker pull\n' ' commands:\n' ' - checkout\n' ' - echo $DOCKERHUB_PASSWORD | docker login ' '--username "$DOCKERHUB_USERNAME" --password-stdin\n' ' - docker pull /\n' ' - docker images\n' ' - docker run /\n' ' secrets:\n' ' - name: docker-hub\n'}, 'score': 0.796259582, 'values': []}
ご覧のとおり、最初に一致するのは、Docker イメージをプルして実行するセマフォ パイプラインの YAML です。 「Docker Containers」の検索文字列に関連しているため、これは良いスタートです。
私たちはデータを持っており、それをクエリする方法を知っています。ボットで動作させてみましょう。
プロンプトを処理する手順は次のとおりです。
いつものように、ボットのメイン スクリプトであるcomplete.py
でいくつかの定数を定義することから始めます。
# complete.py # Pinecone database name, number of matched to retrieve # cutoff similarity score, and how much tokens as context index_name = 'semaphore' context_cap_per_query = 30 match_min_score = 0.75 context_tokens_per_query = 3000 # OpenAI LLM model parameters chat_engine_model = "gpt-3.5-turbo" max_tokens_model = 4096 temperature = 0.2 embed_model = "text-embedding-ada-002" encoding_model_messages = "gpt-3.5-turbo-0301" encoding_model_strings = "cl100k_base" import pinecone import os # Connect with Pinecone db and index api_key = os.getenv("PINECONE_API_KEY") env = os.getenv("PINECONE_ENVIRONMENT") pinecone.init(api_key=api_key, environment=env) index = pinecone.Index(index_name)
次に、 OpenAI の例に示すように、トークンをカウントする関数を追加します。最初の関数は文字列内のトークンをカウントし、2 番目の関数はメッセージ内のトークンをカウントします。メッセージについては後ほど詳しく見ていきます。とりあえず、会話の状態を記憶に残しておく構造、とだけ言っておきましょう。
import tiktoken def num_tokens_from_string(string: str) -> int: """Returns the number of tokens in a text string.""" encoding = tiktoken.get_encoding(encoding_model_strings) num_tokens = len(encoding.encode(string)) return num_tokens def num_tokens_from_messages(messages): """Returns the number of tokens used by a list of messages. Compatible with model """ try: encoding = tiktoken.encoding_for_model(encoding_model_messages) except KeyError: encoding = tiktoken.get_encoding(encoding_model_strings) num_tokens = 0 for message in messages: num_tokens += 4 # every message follows {role/name}\n{content}\n for key, value in message.items(): num_tokens += len(encoding.encode(value)) if key == "name": # if there's a name, the role is omitted num_tokens += -1 # role is always required and always 1 token num_tokens += 2 # every reply is primed with assistant return num_tokens
次の関数は、元のプロンプトとコンテキスト文字列を取得して、GPT-3 用に強化されたプロンプトを返します。
def get_prompt(query: str, context: str) -> str: """Return the prompt with query and context.""" return ( f"Create the continuous integration pipeline YAML code to fullfil the requested task.\n" + f"Below you will find some context that may help. Ignore it if it seems irrelevant.\n\n" + f"Context:\n{context}" + f"\n\nTask: {query}\n\nYAML Code:" )
get_message
関数は、API と互換性のある形式でプロンプトをフォーマットします。
def get_message(role: str, content: str) -> dict: """Generate a message for OpenAI API completion.""" return {"role": role, "content": content}
モデルの反応に影響を与える役割には 3 つのタイプがあります。
さて、魅力的な部分です。 get_context
関数は、プロンプトを受け取り、データベースにクエリを実行し、次の条件のいずれかが満たされるまでコンテキスト文字列を生成します。
context_tokens_per_query
を超えています。match_min_score
を下回る類似性スコアを持つ一致は無視されます。 import openai def get_context(query: str, max_tokens: int) -> list: """Generate message for OpenAI model. Add context until hitting `context_token_limit` limit. Returns prompt string.""" embeddings = openai.Embedding.create( input=[query], engine=embed_model ) # search the database vectors = embeddings['data'][0]['embedding'] embeddings = index.query(vectors, top_k=context_cap_per_query, include_metadata=True) matches = embeddings['matches'] # filter and aggregate context usable_context = "" context_count = 0 for i in range(0, len(matches)): source = matches[i]['metadata']['source'] if matches[i]['score'] < match_min_score: # skip context with low similarity score continue context = matches[i]['metadata']['text'] token_count = num_tokens_from_string(usable_context + '\n---\n' + context) if token_count < context_tokens_per_query: usable_context = usable_context + '\n---\n' + context context_count = context_count + 1 print(f"Found {context_count} contexts for your query") return usable_context
次の最後の関数complete
、OpenAI に API リクエストを発行し、モデルの応答を返します。
def complete(messages): """Query the OpenAI model. Returns the first answer. """ res = openai.ChatCompletion.create( model=chat_engine_model, messages=messages, temperature=temperature ) return res.choices[0].message.content.strip()
それで全部です;これで、コマンド ライン引数を処理し、正しい順序で関数を呼び出すだけで済みます。
import sys query = sys.argv[1] context = get_context(query, context_tokens_per_query) prompt = get_prompt(query, context) # initialize messages list to send to OpenAI API messages = [] messages.append(get_message('user', prompt)) messages.append(get_message('system', 'You are a helpful assistant that writes YAML code for Semaphore continuous integration pipelines and explains them. Return YAML code inside code fences.')) if num_tokens_from_messages(messages) >= max_tokens_model: raise Exception('Model token size limit reached') print("Working on your query... ") answer = complete(messages) print("Answer:\n") print(answer) messages.append(get_message('assistant', answer))
スクリプトを実行して、どのように機能するかを確認します。
$ python complete.py "Create a CI pipeline that builds and uploads a Docker image to Docker Hub"
結果は次のとおりです。
version: v1.0 name: Docker Build and Push agent: machine: type: e1-standard-2 os_image: ubuntu1804 blocks: - name: "Build and Push Docker Image" task: jobs: - name: "Docker Build and Push" commands: - checkout - docker build -t /: . - echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin - docker push /: promotions: - name: Deploy to production pipeline_file: deploy-production.yml auto_promote: when: "result = 'passed' and branch = 'master'"
初めての良い結果です。モデルは、提供されたコンテキストの例から構文を推測しました。
私が YAML パイプラインを作成するアシスタントを作成するというささやかな目標から始めたことを思い出してください。ベクター データベースのコンテンツが豊富になると、Semaphore (または任意の製品 - ドキュメントを/tmp
にクローンしたことを覚えていますか?) に関するあらゆる質問に答えるボットを一般化できます。
良い答えを得る鍵となるのは、当然のことながら、質の高いコンテキストです。すべてのドキュメントを単にベクター データベースにアップロードするだけでは、良い結果が得られる可能性は低いです。コンテキスト データベースは厳選され、説明的なメタデータでタグ付けされ、簡潔である必要があります。そうしないと、無関係なコンテキストでプロンプト内のトークン クォータがいっぱいになる危険があります。
したがって、ある意味、ニーズを満たすためにボットを微調整するのには技術が必要であり、多くの試行錯誤が必要です。類似性スコアを調整することで、コンテキストの制限を試したり、低品質のコンテンツを削除したり、要約したり、無関係なコンテキストを除外したりできます。
私のボットでは ChatGPT のような実際の会話ができないことに気づいたかもしれません。 1 つの質問をすると、1 つの答えが得られます。
ボットを本格的なチャットボットに変換することは、原則としてそれほど難しいことではありません。各 API リクエストで以前の応答をモデルに再送信することで、会話を維持できます。以前の GPT-3 回答は、「アシスタント」ロールの下で返送されます。例えば:
messages = [] while True: query = input('Type your prompt:\n') context = get_context(query, context_tokens_per_query) prompt = get_prompt(query, context) messages.append(get_message('user', prompt)) messages.append(get_message('system', 'You are a helpful assistant that writes YAML code for Semaphore continuous integration pipelines and explains them. Return YAML code inside code fences.')) if num_tokens_from_messages(messages) >= max_tokens_model: raise Exception('Model token size limit reached') print("Working on your query... ") answer = complete(messages) print("Answer:\n") print(answer) # remove system message and append model's answer messages.pop() messages.append(get_message('assistant', answer))
残念ながら、この実装はかなり初歩的なものです。インタラクションごとにトークン数が増加するため、長時間の会話はサポートされません。まもなく、GPT-3 のトークン制限 4096 に達し、それ以上の対話ができなくなるでしょう。
したがって、リクエストをトークン制限内に保つ何らかの方法を見つける必要があります。いくつかの戦略が続きます。
ボットの応答を強化するには、単語の埋め込みと適切なコンテキスト データベースを使用します。これを達成するには、高品質のドキュメントが必要です。主題を理解しているように見えるボットの開発には、かなりの量の試行錯誤が伴います。
単語の埋め込みと大規模な言語モデルについてのこの徹底的な調査が、要件に合わせてカスタマイズされた、より強力なボットの構築に役立つことを願っています。
ハッピービルディング!
ここでも公開されています。