OpenAIのAPIを使う

[2024-11-09] Googleの Gemini API も同じ方法で使えるようになりました。

[2024-06-12] 旧OpenAIのAPIを使うと区別するために「ChatGPTのAPIを使う」という題名にしていましたが、「OpenAIのAPIを使う」に変更しました。

[2024-05-14] gpt-4o (gpt-4o-2024-05-13) が出ました。半額・倍速。128Kコンテクスト、2023年10月の知識。

[2024-04-10] gpt-4-turbo-2024-04-09 が出ました。gpt-4-turbo は現時点ではこれを指します。

[2024-01-26] 2024-01-25の新モデル: 最新のプレビューモデル "gpt-4-0125-preview""gpt-3.5-turbo-0125" が出ました。"gpt-4-turbo-preview""gpt-3.5-turbo" と指定すれば最新のものが選ばれるようです。

[2023-11-07] DevDayで新しいモデル等が発表されました: "gpt-4-1106-preview""gpt-4-vision-preview""gpt-3.5-turbo-1106"

[2023-06-14] 新しいモデル "gpt-3.5-turbo-0613""gpt-3.5-turbo-16k-0613""gpt-4-0613""gpt-4-32k-0613" が来ました。価格も若干改訂され、function calling(関数呼び出し)の機能が付きました。Function calling and other API updates 参照。GPT-3.5の16k版はすぐに使えましたが、GPT-4の32k版はまだ私には降ってきていません。

[2023-03-18] GPT-4のAPIが私のところにも来ました。以下のコードで "gpt-3.5-turbo""gpt-4"(8192トークン版)あるいは "gpt-4-32k"(32768トークン版)にするだけで使えます。値段は1桁以上高くなって、入力は8k版が $0.03/1k、32k版が $0.06/1k、出力はその倍の値段です。

[2023-03-02] [米国時間2023-03-01] OpenAIChatGPTAPI が公開されました(Introducing ChatGPT and Whisper APIs)。費用は従来の text-davinci-003 の1/10の0.0002ドル/1000トークンと、非常にお値打ちです。

はじめに

従来のAPIを使っていた人は何もせずに使えますが、そうでない場合は、まずこちらで登録してAPIキーを発行してもらわなければなりません。

APIの概要はOpenAIの OpenAI developer platform からドキュメンテーション、APIレファレンスなどをご覧ください。APIで送られたデータは学習用に使われることはありません。不正使用の監視のために30日間保持され、特に問題なければ消去されるようです。

APIの料金は、2023-11-06の新モデル gpt-4-1106-preview については、入力1kトークンあたり $0.01、出力1kトークンあたり $0.03 です。初めは一番安い gpt-3.5-turbo-1106(入力1kトークンあたり $0.001、出力1kトークンあたり $0.002)で練習するのがいいでしょう。

使い方の基本

Pythonのパッケージは pip install openai でインストールできます。

APIキーは、プログラムに直接書き込まず、環境変数に設定しておくのが安全・便利です。MacやLinuxでは、ターミナルに

export OPENAI_API_KEY="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

と打ち込めば環境変数が設定されます。.bashrc.zshenv 等に書き込んでおけばシェル起動時に設定されます。APIキーを書き込んだファイルは他人に見られないようにパーミッションを正しく設定しておきましょう。環境変数が使えないときは python-dotenv が便利そうです。

使い方の基本は次の通りです(2023-11-07に大きく変わりました):

from openai import OpenAI

client = OpenAI()

# もし環境変数が使えないなら次のようにする:
# client = OpenAI(api_key="sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")

res = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": "あなたは賢いAIです。"},  # 役割設定(省略可)
        {"role": "user", "content": "1たす1は?"}               # 最初の質問
    ],
    temperature=1  # 温度(0-2, デフォルト1)
)

print(res.choices[0].message.content)  # 答えが返る

ChatGPTのAPIは、質問・応答の履歴を記憶しません。以前の質問・応答を前提としたい場合は、次のように履歴を与えた上で質問をします:

res = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": "あなたは賢いAIです。"},  # 役割設定(省略可)
        {"role": "user", "content": "1たす1は?"},              # 最初の質問
        {"role": "assistant", "content": "2です。"},            # 最初の答え
        {"role": "user", "content": "それを3倍して。"}          # 次の質問
    ],
    temperature=1
)

print(res.choices[0].message.content)  # 答えが返る

履歴と質問、答えを合わせたトークン数には上限(モデルによって違いますが、例えば4096トークン)があります。トークンはほぼ単語に相当するものです(後述)。トークン数を表示するには、次のようにします:

print(res.usage)
CompletionUsage(completion_tokens=8, prompt_tokens=27, total_tokens=35)

トークン上限を超えて会話を続けるには、不要な履歴を削除する必要があります。削除も含めて、簡単な会話を続けるには、例えば次のようにすればいいでしょう:

from openai import OpenAI

client = OpenAI()

msg = [{"role": "system", "content": "あなたは賢いAIです。"}]
while True:
    prompt = input("> ").strip()
    if prompt in ["quit", "exit"]:
        break
    msg.append({"role": "user", "content": prompt})
    res = client.chat.completions.create(model="gpt-3.5-turbo", messages=msg)
    ans = res.choices[0].message.content.strip()
    print(ans)
    msg.append({"role": "assistant", "content": ans})
    if res.usage.total_tokens > 3000:
        msg.pop(1)
        msg.pop(1)

ストリーミング

Web 版 ChatGPT のように文字単位(単語単位)で出力するようにしてみましょう。モデル名として Gemini のモデル("gemini-1.5-pro" など)を与えてもいいようにしました(環境変数 GOOGLE_API_KEY にAPIキーを入れておきます)。

import os
from openai import OpenAI

class Chatbot:
    def __init__(self, model="gpt-4-turbo", messages=None, temperature=0, stream=True):
        if "gemini" in model:
            self.client = OpenAI(
                api_key=os.getenv("GOOGLE_API_KEY"),
                base_url="https://generativelanguage.googleapis.com/v1beta/"
            )
        else:
            self.client = OpenAI()
        self.model = model
        self.messages = messages or []
        self.temperature = temperature
        self.stream = stream

    def chat(self, prompt, temperature=None, stream=None, verbose=0, **kwargs):
        prompt = prompt.strip()
        if len(self.messages) > 0 and self.messages[-1]["role"] == "user":
            self.messages.pop()
        self.messages.append({"role": "user", "content": prompt})
        stream = stream if stream is not None else self.stream
        temperature = temperature if temperature is not None else self.temperature
        try:
            res = self.client.chat.completions.create(model=self.model,
                                                      messages=self.messages,
                                                      temperature=temperature,
                                                      stream=stream, **kwargs)
        except Exception as e:
            print("Error", e)
            return
        if stream:
            ans = ""
            for chunk in res:
                content = chunk.choices[0].delta.content or ""
                print(content, end="")
                ans += content
        else:
            ans = res.choices[0].message.content.strip()
            print(ans)
            if verbose:
                u = res.usage
                print(u.prompt_tokens, u.completion_tokens, u.total_tokens)
        if verbose > 1:
            print(res)
        self.messages.append({"role": "assistant", "content": ans})

    def get_messages(self):
        return self.messages

これを使うには例えば

chatbot = Chatbot()
chatbot.chat("こんにちは!")

のようにします。会話を続けるには

chatbot.chat("""
○○について説明してください。
""")

のようにします。コンテクストが溢れた場合は、とりあえず

chatbot = Chatbot(messages=chatbot.get_messages()[2:])

のようにして新しいチャットのインスタンスを作ってください。

応用:長いページの要約

上で作った Chatbot() クラスの応用として、URLを与えてページを要約するアプリを作ってみましょう。ここでは Trafilatura というライブラリを使ってWebページのテキストを取り出しています。

import trafilatura

URL = "https://....."
text = trafilatura.extract(trafilatura.fetch_url(URL))

chatbot = Chatbot(model="gpt-3.5-turbo-16k")
chatbot.chat(prompt="Provide a long and detailed summary of what follows:\n\n" + text)

トークン

トークンはほぼ単語に相当する概念で、gpt-3.5-turbo や gpt-4 では tiktoken の cl100k_base というエンコーディングが使われています(→ OpenAI 言語モデルごとのエンコーディング一覧)。頻出単語は1トークン、そうでない単語は2トークン以上に分割されます。日本語の場合は、2〜3文字が1トークンになることも、逆に1文字が2〜3トークンに分割されることもあります。平均してトークンあたり日本語0.9170文字程度です(OpenAI 言語モデルで日本語を扱う際のトークン数推定指標)。

pip install tiktoken して試してみましょう:

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

次の例で試してみましょう(山本義隆『熱学思想の史的展開』(現代数学社,1987年)より):

s = "「何人ものニュートンがいた(There were several Newtons)」と言ったのは,科学史家ハイルブロンである.同様にコーヘンは「ニュートンはつねに二つの貌を持っていた(Newton was always ambivalent)」と語っている."
e = enc.encode(s)
for i in e:
    c = enc.decode([i])
    if len(c) == 1 and ord(c) == 65533:  # 65533は「�」
        print(i, end="|")
    else:
        print(c, end="|")
print()

次のように88トークンに分割されていることがわかります:

「|何|人|も|の|ニ|ュ|ート|ン|が|い|た|(|There| were| several| Newton|s|)|」|と|言|っ|た|の|は|,|科|学|5877|110|家|2845|237|イ|ル|ブ|ロ|ン|で|あ|る|.|同|162|100|246|に|コ|ー�|246|ン|は|「|ニ|ュ|ート|ン|は|つ|2243|255|に|二|つ|の|80631|234|を|持|って|い|た|(|Newton| was| always| amb|ivalent|)|」|と|45918|252|って|い|る|.|

英語はだいたい1語1トークンですが、直前のスペースも含めてトークンになっていることがけっこうあります。日本語はだいたい1文字1トークンですが、「様」のようにUTF-8の3バイトがそれぞれトークンになっている場合や、「ーヘ」の「ヘ」の最初の2バイトが「ー」とくっついて1トークンになっているような場合もあります。平均して10トークンが日本語9文字少々という感じです。

ちなみに、同じ文字列をOpenAIの Tokenizer に入れると、GPT-3では120トークンになります。

[2024-05-14追記] GPT-4o では新しいトークナイザで日本語のトークン数も減りました。tiktoken 0.7.0 以降で試せます:

enc = tiktoken.get_encoding("o200k_base")
# または enc = tiktoken.encoding_for_model("gpt-4o")

上の例でトークン数は67です:

「|何|人|もの|ニュ|ート|ン|が|いた|(|There| were| several| Newton|s|)」|と言|った|の|は|,|科学|史|家|ハ|イル|ブ|ロン|で|ある|.|同|様|に|コ|ー�|246|ン|は|「|ニュ|ート|ン|は|つ|ね|に|二|つ|の|貌|を|持|って|いた|(|Newton| was| always| amb|ivalent|)」|と|語|って|いる|.|

「名無しさん」「転載は禁止」「VIPがお送りします」みたいなのがそれぞれ1トークンになるので、何を学習させたかの想像がついてしまいます。