BioErrorLog Tech Blog

試行錯誤の記録

GPTのstreamとtimeoutを併用した時の挙動 | OpenAI Python Library

OpenAI APIをPythonで呼び出すとき、GPTのstreamとtimeoutを併用するとどうなるのか、検証します。

はじめに

OpenAI APIをPythonで呼び出すときは、timeoutやstreamを設定できます。

このtimeoutとstreamを併用するとどのタイミングでタイムアウトが発生するのか、パッとわからなかったので検証しました。

GPTのstreamとtimeoutを併用した時の挙動

仮説

2つ仮説があります。

streamとtimeoutを併用した時の挙動の仮説

仮説1:timeoutはstream終了時点に対して実行される

この挙動の場合、streamレスポンスが返って来てても、timeout時間が来たら途中で処理がタイムアウトする、という挙動になります。


仮説2:timeoutはstream開始時点に対して実行される

こちらの挙動の場合、streamレスポンスが開始したら、もうタイムアウトが実行されることはありません。

検証方法

Streamレスポンスがちょうど返ってきている途中でタイムアウトする、という条件でOpenAI APIを呼び出し、挙動を確認します。

具体的には、下記のPythonコードを実行します。

import os
import time
import openai

openai.api_key = os.environ["OPENAI_API_KEY"]


def main() -> None:
    start_time = time.time()

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {'role': 'user', 'content': 'Tell me about the Japanese history.'}
        ],
        stream=True,
        request_timeout=3,
    )

    collected_chunks = []
    collected_messages = []
    for chunk in response:
        chunk_time = time.time() - start_time

        collected_chunks.append(chunk)
        chunk_message = chunk['choices'][0]['delta'].get('content', '')
        collected_messages.append(chunk_message)
        print(f"Message received {chunk_time:.2f} seconds after request: {chunk_message}")

    full_reply_content = ''.join(collected_messages)
    print(f"Full conversation received: {full_reply_content}")


if __name__ == "__main__":
    main()

Ref. python-examples/openai_stream_timeout/main.py at main · bioerrorlog/python-examples · GitHub

ポイントは、

  • 回答の生成に時間がかかる質問を投げる
  • streamレスポンスが開始し、回答が生成しきらないタイミングでtimeoutを設定する

です。

では、結果を見ていきましょう。

検証結果

結果、タイムアウトは発生しませんでした

つまり、先述した仮説2の挙動であり、timeoutはstream開始時点に対して実行されるようです。

# 実行結果
$ python main.py                 
Message received 1.86 seconds after request: 
Message received 1.86 seconds after request: Japanese
Message received 1.86 seconds after request:  history

# 中略

# 設定したtimeout時間(3 sec)でタイムアウトが発生しない
Message received 2.80 seconds after request:  The
Message received 2.82 seconds after request:  earliest
Message received 2.98 seconds after request:  known
Message received 2.99 seconds after request:  human
Message received 3.01 seconds after request:  hab
Message received 3.02 seconds after request: itation
Message received 3.03 seconds after request:  in
Message received 3.04 seconds after request:  Japan

# 以下略


ちなみに、streamが開始される前にtimeoutを設定する(0.5secなど)と、普通にタイムアウトは発生します。

まとめ

まとめると、streamとtimeoutを併用した時の挙動は下記のようになります。

streamとtimeoutを併用したときの挙動

  • stream開始が、設定したtimeoutより遅かった場合、タイムアウトが発生する
  • 設定したtimeoutが、stream開始よりも遅かった場合、タイムアウトは発生しない

おわりに

以上、GPTのstreamとtimeoutを併用した時の挙動を検証しました。

少し気になっていたポイントだったので、スッキリしました。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

openai-cookbook/examples/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub

OpenAI Platform

Using server-sent events - Web APIs | MDN

python-examples/openai_stream_timeout/main.py at main · bioerrorlog/python-examples · GitHub

OpenAI APIでGPTのstreamレスポンス | Python

OpenAI APIで、GPTのstreamレスポンスをPythonで実装する方法のメモです。

はじめに

OpenAI APIでGPTを呼び出すと、デフォルトでは全ての回答生成が終わってからレスポンスが返ってきます。

これを、ブラウザのChatGPTのように順次レスポンスをstreamで返させる方法をメモします。

# 作業環境
# openai version
0.28.0

なお本記事のコードは下記GitHubレポジトリに配置しています。

github.com

OpenAI APIでGPTのstreamレスポンス

下記のように実装します:

import os
import openai

openai.api_key = os.environ["OPENAI_API_KEY"]


def main() -> None:
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=[
            {'role': 'user', 'content': 'Hello?'}
        ],
        stream=True
    )

    collected_chunks = []
    collected_messages = []
    for chunk in response:
        collected_chunks.append(chunk)
        chunk_message = chunk['choices'][0]['delta'].get('content', '')
        collected_messages.append(chunk_message)
        print(f"Message received: {chunk_message}")

    full_reply_content = ''.join(collected_messages)
    print(f"Full conversation received: {full_reply_content}")


if __name__ == "__main__":
    main()

Ref. python-examples/openai_stream/main.py at main · bioerrorlog/python-examples · GitHub

まずはChatCompletion.createに対してstream=Trueのオプションを渡すことで、レスポンスをstreamにすることができます。

その上で、responseに追加されていくchunkをforループで取り出します:

    collected_chunks = []
    collected_messages = []
    for chunk in response:
        collected_chunks.append(chunk)
        chunk_message = chunk['choices'][0]['delta'].get('content', '')
        collected_messages.append(chunk_message)
        print(f"Message received: {chunk_message}")

chunkは下記のフォーマットで返されるので、deltaに含まれるメッセージコンテンツをchunk['choices'][0]['delta'].get('content', '')のようにして取り出しています。

{
  "id": "chatcmpl-123",
  "object": "chat.completion.chunk",
  "created": 1677652288,
  "model": "gpt-3.5-turbo",
  "choices": [{
    "index": 0,
    "delta": {
      "content": "Hello",
    },
    "finish_reason": "stop"
  }]
}

Ref. The chat completion chunk object - OpenAI API Reference

上記のコードの実行結果はこんな感じです:

Message received: 
Message received: Hello
Message received: !
Message received:  How
Message received:  can
Message received:  I
Message received:  assist
Message received:  you
Message received:  today
Message received: ?
Message received: 
Full conversation received: Hello! How can I assist you today?


OpenAIが公式に出しているサンプルコードもあるので、こちらも参照ください: openai-cookbook/examples/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub

おわりに

以上、OpenAI APIでGPTのstreamレスポンスをPythonで実装する方法をまとめました。

レスポンスをstreamにすることで待ち時間が短縮され、多くのケースでユーザー体験が向上します。

上手く使いこなしていきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

openai-cookbook/examples/How_to_stream_completions.ipynb at main · openai/openai-cookbook · GitHub

OpenAI Platform

Using server-sent events - Web APIs | MDN

python-examples/openai_stream/main.py at main · bioerrorlog/python-examples · GitHub

venvでVSCodeのF12(Go to Definition)が機能しない問題の対処法 | Python

Pythonのvenv利用時に、VSCodeのF12(Go to Definition)が機能しない問題への対処法を整理します。

はじめに

VSCodeのF12(Go to Definition)、便利ですよね。

しかし、Pythonでvenvを利用するとこのGo to Definitionがうまく機能しないことがあります。

venvを使うと、F12 (Go to Definision)がうまく機能しない

今回はこの対処法をメモします。

venvでVSCodeのF12(Go to Definition)が機能しない問題の対処法

原因

おそらく原因は、VSCodeで選択されているPythonインタプリタが該当のvenv内のものではないため、でしょう。

VSCodeで選択されているPythonインタプリタは、VSCode右下から確認することができます。

VSCode右下に表示されているPythonバージョンをクリックすると、今どこのPythonインタプリタが参照されているかが確認できる

ここで選択されていたPythonインタプリタが作成したvenv内のものではなく、例えばグローバルにインストールされたものが指定されていた場合、venv内で仮想環境にインストールしたライブラリにはF12ジャンプができません。

対処法

venv内のPythonインタプリタを、VSCodeのPythonインタプリタとして指定します。

やり方は、先ほどと同様にVSCode右下からPythonインタプリタを選択し、そこで利用したいvenv内のPythonインタプリタpathを追加するだけです。

1. VSCode右下に表示されているPythonバージョンをクリック

2. "Enter interpreter path..." をクリック

3. venv内のPythonインタプリタpathを入力

venv内のPythonインタプリタpathは、絶対パスでも相対パス(VSCodeを開いているrootからの相対パス)からでもどちらでも大丈夫です。

venv内のPythonインタプリタは、<仮想環境名>/bin/pythonに配置されています。
※ Windowsであれば<仮想環境名>/Scripts/pythonです

# 例
.venv/bin/python

# Windowsの場合
.venv/Scripts/python

おわりに

以上、Pythonのvenv利用時にVSCodeのF12が機能しない問題への対処をまとめました。

コードを書いてる時にライブラリのソースコードをパッと読めるのは、便利ですし何より楽しいですね。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

OpenAI Python Libraryでtimeoutを設定する

OpenAI Python LibraryでOpenAI APIを呼び出すときに、timeoutを設定する方法のメモです。

はじめに

OpenAI Python Libraryを使ってChatCompletion APIを叩いてるときに、timeoutを設定したくなりました。

Referenceを見てもパッとやり方がわからなかったので、備忘録を残します。

# 作業環境
# openai version
0.27.8

OpenAI Python Libraryでtimeoutを設定する

やり方:request_timeout パラメータ

request_timeoutパラメータを設定することで、timeoutを設定することができます。

# コード例
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "user", "content": "Hello world"},
    ],
    request_timeout=20,  # タイムアウトを設定 (秒)
)

このrequest_timeoutパラメータについては、よく見るとREADMEに記載があります。

All endpoints have a .create method that supports a request_timeout param. This param takes a Union[float, Tuple[float, float]] and will raise an openai.error.Timeout error if the request exceeds that time in seconds

Ref. GitHub - openai/openai-python: The OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language.

ChatCompletion APIだけでなく、.createメソッドをもつものには全てrequest_timeoutパラメータを利用できるようです。

Timeoutが発生した時は、openai.error.Timeoutエラーが発生します。

落とし穴:timeout パラメータ

openaiのソースコードを眺めてみると、ChatCompletionではtimeoutパラメータも機能しているようにも見えます。

class ChatCompletion(EngineAPIResource):
    engine_required = False
    OBJECT_NAME = "chat.completions"

    @classmethod
    def create(cls, *args, **kwargs):
        """
        Creates a new chat completion for the provided messages and parameters.

        See https://platform.openai.com/docs/api-reference/chat/create
        for a list of valid parameters.
        """
        start = time.time()
        timeout = kwargs.pop("timeout", None)

        while True:
            try:
                return super().create(*args, **kwargs)
            except TryAgain as e:
                if timeout is not None and time.time() > start + timeout:
                    raise

                util.log_info("Waiting for model to warm up", error=e)

Ref. openai-python/openai/api_resources/chat_completion.py at b82a3f7e4c462a8a10fa445193301a3cefef9a4a · openai/openai-python · GitHub

が、執筆時点(openai version0.27.8)ではこのtimeoutパラメータは機能しませんでした。

Issueにもこの問題が提起されています。

timeout paramter is not respected in openai.ChatCompletion.create method · Issue #549 · openai/openai-python · GitHub

しばらくはtimeoutパラメータではなく、ドキュメントに明示されているrequest_timeoutパラメータを使った方が良さそうです。

おわりに

以上、OpenAI Python Libraryでtimeoutを設定する方法を整理しました。

OpenAI APIを本番運用するには、このあたりのハンドリングも重要になりますね。

参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - openai/openai-python: The OpenAI Python library provides convenient access to the OpenAI API from applications written in the Python language.

timeout paramter is not respected in openai.ChatCompletion.create method · Issue #549 · openai/openai-python · GitHub

OpenAI Platform

Code interpreter/Advanced Data Analysisの仕組みを理解する | ChatGPT

ChatGPTのCode interpreterの仕組みを、自分の理解で整理します。

※ 追記:「Code interpreter」は、現在「Advanced Data Analysis」に改名されました。 中身の機能に変更はありません。

はじめに

先日、ChatGPTのCode interpreterがbetaリリースされました。

非常に有用な機能で、世間的にもお祭り状態ですね。
私も色々試して遊んでいます。

一方で、「どう活用できるのか」ではなく、「どういう仕組みなのか」という観点は、Code interpreterを実際に触ってみるまで私も良く分かっていませんでした。

今回は、Code interpreterの仕組みに焦点を当てて、自分の理解を整理します。

Code interpreterの仕組みを理解する

Code interpreterの仕組み

Code interpreterは、Pythonを実行できるサンドボックス環境がChatGPTに割り当てられる機能です。

Code interpreterの仕組み概要

三人の登場人物を意識すると、この仕組みを理解しやすいでしょう。

  • ChatGPT:
    ユーザーからの入力に対して実行計画を立て、Pythonコードを生成する。
  • Python実行環境:
    ChatGPTが生成したPythonコードを実行できる。
  • ディスクスペース:
    実行されるPythonコードは、この領域にアクセスできる。
    ユーザーはここにファイルをアップロード/ダウンロードできる。
    このディスクスペースは一時的なもので、一定時間経過するとリセットされる(ephemeral/一時的な領域である)。

つまり、ユーザーの指示に応えるためのPythonコードが生成&実行される、その実行対象データたるファイルもアップロードできる、という訳ですね。

ChatGPTに出す指示はPythonが対応できる範囲(Pythonからコマンドも実行できるので範囲は非常に広いです)であればなんでも対応してくれますし、アップロードするファイルもPythonで扱えるファイルであればなんでも対応できます。

これまではChatGPTにコードを生成させることはできても、それをコピペして実行するのはあくまでユーザーの責務でした。 それが、ChatGPT側でコード実行までできるようになったのは嬉しいアップデートですね。

ケーススタディ:Code interpreter実行時の流れ

では例として、「ある動画ファイルをTwitterアップロード用に変換する」というケースを取り上げ、どのような処理がどこで起きているのかを追いかけてみます。

1. ユーザーからのファイルアップロードと指示

まず、動画ファイルをアップロードして指示を出します。

この段階では、ファイルが一時ディスクスペースにアップロードされ、ChatGPTに指示が飛んでいる状態です。

ファイルがアップロードされ、promptが送信された

2. ChatGPTによる計画の立案

この指示対して、ChatGPTは「こういう処理を実行しようと思います。よろしいですか?」と計画を立案してきます。

今回は、下記のようにして実行計画を立ててきました。

  1. Twitterアップロード用の最適なフォーマットの整理
  2. アップロードされた動画ファイルのフォーマットを確認 (Python実行)
  3. Twitterアップロード用の最適なフォーマットと、現状の動画ファイルのフォーマットの差分から、どんな変換を行えば良いかを立案

ChatGPTからのレスポンス

3. 処理の実行と実行結果のダウンロード

提案された計画に対してユーザーがGOサインを出すと、ChatGPTはPythonコードを生成して実行します。

Python実行結果がエラーになったときは、そのエラーを解析して勝手に試行錯誤してくれます。 面白いですね。

処理結果のファイルは、ダウンロードリンクが提示されてユーザーがダウンロードできます。

ファイル処理のPythonコードが実行される様子


以上、ユーザーからの指示を実現する処理(Python)を、ChatGPTがCode interpreterサンドボックス環境上で実行していく、という流れを追いました。

Code interpreterサンドボックス環境の詳細

最後に、Code interpreterで割り当てられるサンドボックス環境の詳細を調べます。
(全て執筆時2023/07時点の調査結果です)

カレントディレクトリ

カレントディレクトリは、作業開始時点で /home/sandbox です。

Disk容量

  • 全容量:約 133 GB
  • 使用中:約 75 GB
  • 利用可能:約 57 GB

思った以上のdisk容量が割り当てられていました。

Pythonバージョン

Pythonのバージョンは 3.8.10 です。

少し古くも感じますが、GPTが2021年までの情報で学習されていることを考えると妥当ですね。

OS情報

Linuxで、アーキテクチャはx86_64でした。

ちなみにplatform.linux_distribution()がPython3.8から使えなくなっていることもあり、何のディストリビューションなのかまで辿り着けませんでした。 (わかったら是非教えてください)

おわりに

以上、Code interpreterの仕組みを整理しました。

止まる気配のないLLM界隈のアップデートを見ていると、まさに時代の変わり目といった機運で面白いですね。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

ChatGPT — Release Notes | OpenAI Help Center

ChatGPT plugins

LangChain 入門

LLMによる開発パターンを容易に実装できると噂のLangChainに入門します。

はじめに

LangChainは、LLMを活用した開発を容易に実現するフレームワークです。

We believe that the most powerful and differentiated applications will not only call out to a language model via an API, but will also:
- Be data-aware: connect a language model to other sources of data
- Be agentic: allow a language model to interact with its environment

(ドキュメントより)

LamgChainは単にLLMのAPIをラップするのではなく、

  • 外部データと繋げる
  • エージェントとして相互作用する

など、LLMの活用パターンを容易に実現できます。

今回は下記のLangChain Python公式ガイドを参考にしながら、このLangChainの基本的な使い方を見ていきます。

https://python.langchain.com/en/latest/getting_started/getting_started.html

LangChainに入門する

事前準備

今回はLLMモデルとしてOpenAI APIを使うので、langchainopenaiをインストールします。

pip install langchain openai

OpenAIのAPIキーも環境変数に設定しておきます。

export OPENAI_API_KEY="..."


以降、OpenAI APIを使ったことがある方向けの内容として書いていきます。 まだOpenAI API、特にChat completionを触ったことのない方は、先にOpenAI APIのドキュメントに軽く目を通しておくことをおすすめします:

OpenAI Platform

Chat Model

まずは、LangChainを用いてgpt-3.5-turboのようなチャット形式でやり取りできるモデルを使ってみます。

from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
)

chat = ChatOpenAI(temperature=0)

response = chat([HumanMessage(content="Translate this sentence from English to French. I love programming.")])
# -> AIMessage(content="J'aime programmer.", additional_kwargs={})

print(response)  # content="J'aime programmer." additional_kwargs={}
print(type(response))  # <class 'langchain.schema.AIMessage'>

OpenAI APIのgpt-3.5-turboではメッセージをsystem/user/assistantの3つのroleとして与えますが、LangChainのChatOpenAI (こちらもデフォルトでgpt-3.5-turboが使われます)ではそれぞれ、

  • HumanMessage (user role)
  • SystemMessage (system role)
  • AIMessage (assistant role)

として与えます。

シンプルに一問一答の形で良ければ、上記のようにHumanMessageとしてテキストを与えます。


Chat ModelにHumanMessage以外の文脈を与えるには、下記のようにします。

from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage,
)

chat = ChatOpenAI(temperature=0)

messages = [
    SystemMessage(content="You are a helpful assistant that translates English to French."),
    HumanMessage(content="Translate this sentence from English to French. I love programming.")
]
response = chat(messages)
# -> AIMessage(content="J'aime programmer.", additional_kwargs={})

print(response)  # content="J'aime programmer." additional_kwargs={}
print(type(response))  # <class 'langchain.schema.AIMessage'>

SystemプロンプトはSystemMessageとして格納し、Chat Modelからの返答はAIMessageとして返されます。 もしそのままやり取りを続ける場合は、Chat Modelからの返答AIMessageをそのままメッセージに追記する、という使い方ができます。


ここまでは単純にOpenAI APIをラップしただけの機能と言えます。 以下、LangChainならではの便利な機能を見ていきます。

Prompt Templates

API経由でのLLMモデル利用パターンの一つに、Prompt Templatesという使い方があります。

Prompt Templatesは、あらかじめ用意されたテンプレートにユーザーからの入力を挿入してプロンプトを構築する方法です。 複雑なプロンプトが必要になるような用途でも、ユーザーからの入力をシンプルにしつつ、LLMに想定の振る舞いをさせることができます。

from langchain.chat_models import ChatOpenAI
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)

chat = ChatOpenAI(temperature=0)

template = "You are a helpful assistant that translates {input_language} to {output_language}."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)

chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])

format_prompt = chat_prompt.format_prompt(input_language="English",
                                          output_language="French", text="I love programming.").to_messages()
print(format_prompt)
# -> [SystemMessage(content='You are a helpful assistant that translates English to French.', additional_kwargs={}), HumanMessage(content='I love programming.', additional_kwargs={})]

SystemMessageのプロンプトテンプレートはSystemMessagePromptTemplateで、HumanMessageのプロンプトテンプレートはHumanMessagePromptTemplateで用意します。

後から挿入する部分を鉤括弧{}で用意し、format_prompt関数で挿入箇所を補完することで、最終的なプロンプトが生成されています。

Chains

ここまで挙げたようなModelやPrompt Templatesは、Chainsによってシンプルに繋げることができます。

from langchain.chat_models import ChatOpenAI
from langchain import LLMChain
from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)

chat = ChatOpenAI(temperature=0)

template = "You are a helpful assistant that translates {input_language} to {output_language}."
system_message_prompt = SystemMessagePromptTemplate.from_template(template)
human_template = "{text}"
human_message_prompt = HumanMessagePromptTemplate.from_template(human_template)
chat_prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt])

chain = LLMChain(llm=chat, prompt=chat_prompt)
response = chain.run(input_language="English", output_language="French", text="I love programming.")

print(response)
# -> "J'aime programmer."

LLMChainによって、LLMモデルとPrompt Templateを連結しています。

Chainsに連結したら、あとはrun関数の引数にPrompt Templateの補完を渡して、LLMへのリクエストを投げることができます。


ここまでのおさらい:

  • Chat Model: LLMモデルのAPIラッパー
  • Prompt Templates: プロンプトを後から補完できるテンプレート
  • Chains: これらを連結してシンプルなインターフェースを提供

以下、より応用的な使い方を見ていきます。

Agents

ここまでは、LLMに行わせる振る舞いをあくまで人間が指定してきました。 Agentsでは、何のアクションを行うべきなのか、をLLMに決めさせることができます。

Agentsは大きく3つの概念から成り立っています。

  • Tool: 特定タスクを実行するための外部機能
  • LLM: Agentの振る舞いを決定するLLM
  • Agent: Agentのタイプ。今回はChat Modelを使う際の最も標準的なタイプAgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTIONを使います。
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI


chat = ChatOpenAI(temperature=0)
tools = load_tools(["terminal"])

agent = initialize_agent(
    tools,
    chat,
    agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

agent.run("Who is the current terminal user?I would like to know the list of directories under that user's home directory.")

使い方はシンプルです。 initialize_agentに先ほど挙げた3つの概念Tool/LLM/Agentを渡して初期化し、run関数で実行します。

上の例では、Toolとしてterminalを、LLMとしてChatOpenAIを、AgentとしてAgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTIONを渡しています。

サポートされているToolの一覧はこちら、Agentの一覧はこちらを参照ください。


この例では、Toolとしてterminalを利用し、現在のユーザーとそのホームディレクトリ配下のディレクトリ構成を質問しています。

Who is the current terminal user? I would like to know the list of directories under that user's home directory.

実行してみましょう。

> Entering new AgentExecutor chain...
Thought: We can use the Terminal tool to execute the `whoami` command to get the current user and then use the `ls` command to list the directories under the user's home directory.

Action:
{
  "action": "Terminal",
  "action_input": "whoami"
}


Observation: bioerrorlog

Thought:Now that we have the current user, we can use the `ls` command to list the directories under the user's home directory.

Action:
{
  "action": "Terminal",
  "action_input": "ls /home/bioerrorlog"
}


Observation: Command 'ls /home/bioerrorlog' returned non-zero exit status 1.
Thought:The `ls` command returned a non-zero exit status, which means that there might be an issue with the path we provided. We can try using the `~` symbol to represent the home directory of the current user.

Action:
{
  "action": "Terminal",
  "action_input": "ls ~"
}


Observation: AWSCLIV2.pkg
Applications
Desktop
Documents
Downloads
Dropbox
Library
Movies
Music
Pictures
Public
dev
dotfiles

Thought:We have successfully listed the directories under the current user's home directory using the `ls` command with the `~` symbol. 

Final Answer: The list of directories under the current user's home directory are: AWSCLIV2.pkg, Applications, Desktop, Documents, Downloads, Dropbox, Library, Movies, Music, Pictures, Public, dev, and dotfiles.

> Finished chain.

与えられた質問に回答するためのアクションが何なのかを推察し、最終回答が得られるまで繰り返されます。

Thought: We can use the Terminal tool to execute the whoami command to get the current user and then use the ls command to list the directories under the user's home directory.


あるアクションがエラーになったとしても、またすぐ別の方法を試行してくれるのも面白いところです。

Observation: Command 'ls /home/bioerrorlog' returned non-zero exit status 1.

このようにホームディレクトリをlsしようとしてエラーになった場合、また別の方法で試行してくれます:

Thought:The ls command returned a non-zero exit status, which means that there might be an issue with the path we provided. We can try using the ~ symbol to represent the home directory of the current user.

Toolを利用したアクションが繰り返された後、最終回答が得られます:

Final Answer: The list of directories under the current user's home directory are: AWSCLIV2.pkg, Applications, Desktop, Documents, Downloads, Dropbox, Library, Movies, Music, Pictures, Public, dev, and dotfiles.

Memory

LLMは基本的にステートレスなAPIのため、過去に行ったやりとりの記憶は何か工夫しない限り保持されません。

そこで最後に、ChainやAgentに記憶を保持させることができる、Memoryを紹介します。

from langchain.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate
)
from langchain.chains import ConversationChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory

prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template("The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know."),
    MessagesPlaceholder(variable_name="history"),
    HumanMessagePromptTemplate.from_template("{input}")
])

llm = ChatOpenAI(temperature=0)
memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm)

response = conversation.predict(input="Hi I'm BioErrorLog, living in Tokyo.")
print(response)
# -> "Hello BioErrorLog, it's nice to meet you! I'm an AI language model. How can I assist you today?"

response = conversation.predict(input="Today is a holiday and the weather is nice.")
print(response)
# -> "That sounds lovely! Do you have any plans for the day?"

response = conversation.predict(input="Summarize our conversation so far.")
print(response)
# -> "Sure! You introduced yourself as BioErrorLog and mentioned that you're currently living in Tokyo. You also mentioned that today is a holiday and the weather is nice."

ConversationBufferMemoryをChainに渡すことで、そのChainで行ったやりとりの記憶を簡単に保持させることができます。

上の例では3回のやり取りを行なっていますが、3回目の質問の時点でも1, 2回目のやり取りの記憶が保持されていることがわかります。

Q:
Summarize our conversation so far.
A:
Sure! You introduced yourself as BioErrorLog and mentioned that you're currently living in Tokyo. You also mentioned that today is a holiday and the weather is nice.

この他にも、Vector Storeを使ったMemoryやナレッジグラフをベースにしたMemoryなど、様々なタイプが提供されています。 色々遊んでみると面白そうですね: https://python.langchain.com/en/latest/modules/memory/how_to_guides.html

おわりに

以上、LangChainの基本的な使い方をざっとまとめました。

LLMのフレームワークは、LangChainの他にもMicrosoftのSemantic Kernelなど色々と選択肢があると思います。

現時点ではその中でもLangChainが一番広く使われているように見受けられます。

ぜひ遊び倒していきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - hwchase17/langchain: ⚡ Building applications with LLMs through composability ⚡

🦜️🔗 Langchain

https://python.langchain.com/en/latest/getting_started/getting_started.html

OpenAI Platform

GitHub - bioerrorlog/langchain-sandbox: LangChain sandbox projects

GitHub - microsoft/semantic-kernel: Integrate cutting-edge LLM technology quickly and easily into your apps

flake8で特定ルールのみ指定してチェックする | Python

--selectオプションで指定できます。

はじめに

flake8を使っていて、特定ルール/エラーコードのみ指定してチェックを実行したくなりました。

--helpマニュアルをぱっと眺めて見逃してしまったので、備忘録を残します。

flake8で特定ルールのみ指定する

やり方

# 例
flake8 --select=F811,E202

--selectオプションに対して、コンマ区切りでエラーコードを渡すことで指定できます。

エラーコード指定は前方一致で適用されるので、例えば--select=Fとした場合はFから始まるエラー全てが、--select=F8として場合はF8から始まるエラー全てが指定されます。

# マニュアル抜粋
$ flake8 --help                                    
usage: flake8 [options] file file ...

positional arguments:
  filename

options:
<中略>
  --select errors       Comma-separated list of error codes to enable. For example, ``--select=E4,E51,W234``. (Default: E,F,W,C90)

実行例

# エラー単体指定
$ flake8 --select=F811     
./app/backend/approaches/readretrieveread.py:9:1: F811 redefinition of unused 'AzureOpenAI' from line 5

# エラー複数指定
$ flake8 --select=F811,E202
./app/backend/approaches/readdecomposeask.py:39:126: E202 whitespace before ']'
./app/backend/approaches/readretrieveread.py:9:1: F811 redefinition of unused 'AzureOpenAI' from line 5
./scripts/prepdocs.py:289:70: E202 whitespace before '}'

# エラー前方一致指定
$ flake8 --select=F5 
./app/backend/langchainadapters.py:21:22: F541 f-string is missing placeholders
./app/backend/langchainadapters.py:39:22: F541 f-string is missing placeholders
./scripts/prepdocs.py:301:11: F541 f-string is missing placeholders

おわりに

以上、ちょっとした備忘録でした。

前方一致で指定できる、というのは少し面白いですね。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

flake8 · PyPI

Full Listing of Options and Their Descriptions — flake8 6.0.0 documentation

Error / Violation Codes — flake8 6.0.0 documentation

ChatGPTをロボットの頭脳にする その1: カメラ/サーボモーターとChatGPTを組み合わせる

Raspberry Pi上で、カメラ/サーボモーターとChatGPTを組み合わせて簡単な実験を行います。

はじめに

ChatGPTを初めて触った時、これはロボットの頭脳として使えるのでは、とピンと来ました。

最小限の世界観を作ってみたので、備忘録を残します。

ChatGPTとRaspberry Pi/カメラ/サーボモーターを組み合わせる

プロトタイプコンセプト

今回の実験コンセプトはこちら:

  1. ChatGPTが現実世界の情報を認識する
  2. ChatGPTが現実世界の次のアクションを自ら決定する
  3. 1-2を繰り返し、自分が置かれている現実世界の状況をChatGPTが把握する
  4. 「ねえ今どんな気持ち?」ってChatGPTに聞く

現実世界の次のアクションを"ChatGPT自身が"決定する、というところがこだわりです。

将来的には、ChatGPTを頭脳としてロボットが自由に動き回り、環境を自律的に学習していく、みたいなことが出来たら面白いですね。

構成

ハードウェア

ハードウェア構成

サーボモーター2つとカメラ1つをRaspberry Piに接続しています。

サーボモーターの制御方法は、以前書いたこちらの記事と同様です。

www.bioerrorlog.work

サーボモーターとカメラは、両面テープと輪ゴムでくっつける、という小学生の工作みたいなことをしています。

サーボモーターとカメラを両面テープと輪ゴムでくくり付けただけの"首振りロボット"

これで水平方向および垂直方向の首振りと、カメラによる画像取得が可能です。

ソフトウェア

ソースコードはこちら:

github.com

ざっくり下記の処理を行っています。

  1. カメラから画像を取得
  2. 取得画像から物体認識
  3. 現状のサーボモーターの角度と、画像に映った物体の名前をChatGPT APIに送信
  4. 次のサーボモーターの角度と、今の気持ち(フリートーク)がChatGPT APIから返される
  5. ChatGPTが指定した角度に従い、サーボモーターを制御
  6. 1-5を複数回繰り返す
  7. 置かれた環境についての説明を求める


一つのポイントは、"次のサーボモーターの角度"をChatGPTに指定させることです。 返答に含まれる値をPython側で解釈し、実際にサーボモーターを制御しています。 画面のあちら側だけの存在だったChatGPTに、現実世界に作用できる手段を与えたみたいで興奮しました。

これは、ChatGPTからの返答をJSON形式に固定させることで実現しています。 (JSONならそのままPythonで容易に解釈できる)

ChatGPTからの返答をJSON形式に固定させるためには、ChatGPT APIのtemperature(回答のランダム性パラメータのようなもの)を低めに設定する必要があります。 今回は0.2に設定して上手く機能しましたが、高め(例えばデフォルトの1.0)に設定すると、指定したJSON形式とは異なる返答がくる場合が出てきます。


もう一つの注意点は、ChatGPT APIからのレスポンスは現状かなり遅い(10-20sec, ひどい時はもっと遅い)、という点です。 これはForumで調べてみても皆同じように言っているのですが、現状は改善を待つことしかできなそうです。

Search results for 'API response too slow' - OpenAI API Community Forum

スムーズにこの首振りロボットを動かすには、なるべくAPIを呼び出す回数を減らす工夫が必要になります。 今回のケースでは、一回の応答の中に"次のサーボモーター角度"の指定を複数回分入れ込ませることで、API応答の待ち時間が毎回発生するのを回避しました。

最終的には、こちらのsystemプロンプトに落ち着きました:

You are a robot with a camera, composed of 2 servo motors: horizontal & vertical.
Horizontal: min -90 right, max 90 left.
Vertical: min -90 down, max 90 up.
Your behavior principles: [curiosity, inquisitiveness, playfulness].
Your answer MUST be in this JSON format: {"NextServoMotor": [{"Horizontal": int(-90~90), "Vertical": int(-90~90)}], "FreeTalk": string}
Constraint: len(your_answer["NextServoMotor"]) == 5
Answer example: {"NextServoMotor": [{"Horizontal": -60, "Vertical": -30},{"Horizontal": 0, "Vertical": 0},{"Horizontal": 90, "Vertical": -45},{"Horizontal": 0, "Vertical": 60},{"Horizontal": -30, "Vertical": -60}],"FreeTalk": "Based on what I've seen, I'm curious about the PC and mouse. I wonder what you use them for and what kind of work or play they are involved in?"}

robot-gpt/robot.py at 85c256e3366f57532e74ee5c1294b69717647df9 · bioerrorlog/robot-gpt · GitHub

動作結果

実際にこの"首振りロボット"が動作している様子はこちらです:

周囲の探索を複数回行った後、置かれている状況を尋ねると下記のような返答が返ってきます。

Based on what I have seen, there are a few objects in the room, including a bottle and a laptop. However, I have not seen much else yet. I am curious to explore more and see what other interesting things I can find!

「これまでに見たものに基づくと、部屋にはボトルとラップトップ等の物がいくつかあります。 しかし、まだそれ以外の物はあまり見てません。 もっと探索して、他にどんな面白いものが見つけられるか楽しみです!」

想定した最小限の世界観を実現できました。

夢が広がります。

課題

最小限の世界観は実現できましたが、もちろん課題がたくさんあります。

物体認識

現状の実装では、物体認識の精度は高くありません。

今回はカメラで取得した画像をYOLOv3で物体認識させています。 実際は周囲の物体を認識できることは稀で、たまに何かを認識する(ほとんど何も認識しない)くらいの挙動です。

画像認識関連に詳しくなかったので、ChatGPTに聞いたりググって出てきたやり方で簡単に実装しましたが、改めて調べてみるとYOLOv3はかなり古いモデルのようです。 新しいモデルを使えば、精度も処理速度も向上するかもしれません。

そもそも、GPTでマルチモーダルが利用可能になれば、取得画像ごとGPTに送って認識させる、という形にできるでしょう。 マルチモーダルの課金体系が良心的であることを祈っています。

Token消費量

現在の実装では、ChatGPT APIから帰ってきたレスポンスは全てassistantプロンプトとして追加しています。 動作を続ければ続けるほど、蓄積されたレスポンス全てをプロンプトに入れ込むので、tokenの消費量は増加していく仕様です。

ちょっとした実験くらいなら無視できるような課金量ですが、今後長く起動し続けるような使い方をしてくならば、今のやり方は現実的ではありません。

レスポンス全てをプロンプトに入れ込むのではなく、重要情報を要約してcontextに入れ込むようにすれば、情報量の喪失を抑えたままtoken消費を軽減できるでしょう。

記憶保持

現在は一連の処理が終われば記憶は残りません。 起動する度にまたゼロから周囲を探索する形になっています。

例えばデータベースを使って外部記憶の仕組みを導入すれば、いわゆる長期記憶的な感じで情報を保持できるようになるでしょう。 長期記憶があれば、遊びの幅が広がります。

外界の認識

現在の実装では、

  • サーボモーターの角度 (水平方向および垂直方向)
  • 認識された物体名のリスト

の組み合わせを蓄積することで、外界の状況を認識させています。

このやり方でも、結構上手く外界を認識できています。 例えば「周囲にある物を左側から順に説明して」みたいに聞いても、ある程度正しい位置関係で説明が返ってきます。

ただ、もっとChatGPT/LLMならではのやり方が、もっと機械っぽくない良いやり方があると思っています。 物体リストと角度の組み合わせではなく、例えば「周囲の状況を詳細に描写した文章」を記憶の拠り所にする、とかも面白そうですね。 あるいはマルチモーダルが使えるようになったら、もっと自由な手段が取れそうです。

「機械/プログラムを扱っている」という人間側の思い入れが、遊びの幅を狭めてしまうことのないよう気をつけたいところです。

おわりに

以上、ChatGPTをロボットの頭脳にする取り組みの第一弾として、試作したものをまとめました。

昨今のLLMの盛り上がりから、何か面白い物が作れるんじゃないか、という興奮が止みません。

色々実験して遊び倒していきたいところです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

OpenAI API

Search results for 'API response too slow' - OpenAI API Community Forum

GitHub - bioerrorlog/robot-gpt at 85c256e3366f57532e74ee5c1294b69717647df9

Parquetファイルのダミーデータを生成する | Python

Pythonでparquetファイルのダミーデータを生成する方法の備忘録です。

はじめに

ちょっとした検証のために、ダミーデータのparquetファイルを用意する機会がありました。

Pythonスクリプトを書いたので、備忘録にメモします。

※ソースコードはこちらに置いています: github.com

Parquetファイルのダミーデータを生成する

Parquetファイルのダミーデータを生成するPythonスクリプト例です。 圧縮形式はここではgzipとしています。

from typing import Dict, List
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import boto3


def create_dummy_data(rows: int = 1000) -> pd.DataFrame:
    data: Dict[str, List] = {
        'id': range(1, rows + 1),
        'name': [f'name_{i}' for i in range(1, rows + 1)],
        'value': [i * 100 for i in range(1, rows + 1)]
    }
    return pd.DataFrame(data)


def save_gz_parquet(df: pd.DataFrame, file_path: str) -> None:
    table: pa.Table = pa.Table.from_pandas(df)
    pq.write_table(table, file_path, compression='gzip')


def main() -> None:
    dummy_data: pd.DataFrame = create_dummy_data()

    file_path: str = 'dummy_data.parquet.gz'
    save_gz_parquet(dummy_data, file_path)


if __name__ == '__main__':
    main()

pyarrow.parquetを使うことで、pandas DataFrameから簡単にparquetファイルを書き出すことができます。

上記のコード例では、create_dummy_data関数でダミーのpandas DataFrameを作成し、save_gz_parquet関数でそのデータをparquetファイルで保存しています。

補足:S3に配置する

作成したparquetファイルをそのままS3に配置するには、下記のような形になります。

from typing import Dict, List
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import boto3


def create_dummy_data(rows: int = 1000) -> pd.DataFrame:
    data: Dict[str, List] = {
        'id': range(1, rows + 1),
        'name': [f'name_{i}' for i in range(1, rows + 1)],
        'value': [i * 100 for i in range(1, rows + 1)]
    }
    return pd.DataFrame(data)


def save_gz_parquet(df: pd.DataFrame, file_path: str) -> None:
    table: pa.Table = pa.Table.from_pandas(df)
    pq.write_table(table, file_path, compression='gzip')


def upload_to_s3(bucket: str, s3_key: str, file_path: str) -> None:
    s3 = boto3.client('s3')
    s3.upload_file(file_path, bucket, s3_key)


def main() -> None:
    dummy_data: pd.DataFrame = create_dummy_data()

    file_path: str = 'dummy_data.parquet.gz'
    save_gz_parquet(dummy_data, file_path)

    bucket_name: str = 'your-bucket-name'
    # ".parquet.gz" doesn't work in S3 select.
    s3_key: str = 'path/to/dummy_data.gz.parquet'
    upload_to_s3(bucket_name, s3_key, file_path)


if __name__ == '__main__':
    main()

boto3のupload_fileでシンプルにファイルをS3にアップロードしています。

ちなみに、ファイルの拡張子を.parquet.gzとしてしまうと、S3 Selectがうまく機能しないので注意が必要です。 (ファイル全体をgzipされたものとしてS3に判定されてしまい、S3 Selectできない)

おわりに

以上、Pythonでparquetファイルのダミーデータを生成する方法の簡単な備忘録でした。

拡張子を.parquet.gzにするとS3 Selectdできない、というのは盲点でした。

参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

raspberry-pi-examples/put_parquet_gz_to_s3 at main · bioerrorlog/raspberry-pi-examples · GitHub

GPTのtoken数を確認する | Python

GPTにて、テキストのToken数を確認する方法をまとめます。

はじめに

ChatGPTでも使われているGPTシリーズは、その入力テキストを'Token'という単位で処理しています。

OpenAI APIの課金はこのToken単位で行われるので、いかに情報をToken効率良く記載するか、も重要な観点になります。

例:GPT-4の課金体系

今回は、このGPTシリーズの実際のToken分割を確認する方法をまとめます。

GPTのToken数を確認する

GUIで確認する | Tokenizer

手軽かつ分かりやすくToken分割を確認できる方法に、OpenAIが提供しているTokenizerというサイトがあります。

Tokenizer - OpenAI API

テキストを入力するだけで、どの単語がどのようにToken分割されているのかが可視化できます。

TokenizerでToken分割を可視化する例

余談ですが、日本語が英語の倍近いToken数を消費していることが分かります。

Pythonで確認する | tiktoken

tiktokenを使うことで、PythonでToken分割を確認することができます。
(先述のTokenizerも内部ではこのtiktokenが使われているようです)

# インストール
pip install tiktoken


テキストのToken数は、例えば下記のようにして取得できます。

import tiktoken


def num_tokens_from_string(string: str, model_name: str) -> int:
    """Returns the number of tokens in a text string."""
    encoding = tiktoken.encoding_for_model(model_name)
    num_tokens = len(encoding.encode(string))
    return num_tokens

(コードはOpenAI Cookbookを元に改変)

実行例:

text = "This is an example of using a Tokenizer."

# GPT-3でToken数を確認
num_tokens_from_string(text, "gpt-3.5-turbo")
# 10

# GPT-4でToken数を確認
num_tokens_from_string(text, "gpt-4")
# 10

選択できるモデルと、そこで使われるエンコーディングは以下の通りです。

OpenAI Cookbookより

(gpt-3.5-turbogpt-4は同じエンコーディングcl100k_baseが利用されているので、先の例でも両者の結果に差は出ていません)

終わりに

以上、GPTのToken分割を確認する方法をまとめました。

いかに少ない文字数(Token数)でこちらの意図を明示するか、というのは、シンプルで綺麗なコードを書く、という行為に似ていて面白いですね。

どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

OpenAI Platform

Pricing

GitHub - openai/tiktoken: tiktoken is a fast BPE tokeniser for use with OpenAI's models.

openai-cookbook/examples/How_to_count_tokens_with_tiktoken.ipynb at main · openai/openai-cookbook · GitHub

Raspberry PiでPyTorchがIllegal instructionエラーを起こす事象の対処法

Raspberry Pi 4でPyTorch2.0が下記エラーを出す事象の解決策をメモします。

Illegal instruction

はじめに

最近、Raspberry PiとChatGPTを組み合わせて何か面白いことができないだろうか、とあれこれ実験しています。

そんな中、Raspberry PiでPyTorch2.0.0(記事執筆時の最新バージョン)をインストールしたところ、import torchしただけでIllegal instructionのエラーが出る事象に遭遇しました。

状況と取り急ぎの対処をメモします。

Raspberry PiでPyTorchがIllegal instruction

起きた事象

PyTorch2.0.0をpip installしてimport torchすると、Illegal instructionのエラーが出る。

pip install torch==2.0.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
python -c "import torch;print(torch.__version__)"
# Illegal instruction


環境: Raspberry Pi 4 Model B 4GB RAM / 64bit Raspberry Pi OS(bullseye)

$ hostnamectl
   Static hostname: raspberrypi
         Icon name: computer
        Machine ID: 4f8b19cb3280458c99d89xxxxxx
           Boot ID: 275f81g1c1fd4249a49bbxxxxxx
  Operating System: Debian GNU/Linux 11 (bullseye)
            Kernel: Linux 6.1.19-v8+
      Architecture: arm64

対処法

根本解決ではありませんが、PyTorchバージョンを1.13.1に戻せば解消します。

pip install torch==1.13.1 torchvision==0.14.1 torchaudio==0.13.1 --index-url https://download.pytorch.org/whl/cpu
python -c "import torch;print(torch.__version__)"
# 1.13.1

原因はPyTorch最新バージョン2.0.0とRaspberry PiのCPUアーキテクチャの相性の問題だと思いますが、下記のissueでも未だ議論は決着していません。

Illegal instruction (core dumped) : PyTorch 2.0 on Raspberry Pi 4.0 8gb · Issue #97226 · pytorch/pytorch · GitHub

バージョンを戻せば解消する、という元も子もない話ですが、逆にその他もろもろの足掻きでは解消できなかった事例、と受け取ってもらえれば幸いです。

おわりに

バージョンを戻さないと解決しなかった、というただの懺悔のような話になってしまいました。

今後のversionで解決されるのを祈っています。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Illegal instruction (core dumped) : PyTorch 2.0 on Raspberry Pi 4.0 8gb · Issue #97226 · pytorch/pytorch · GitHub

Illegal instruction (core dumped) on Raspberry Pi 4B · Issue #8 · KumaTea/pytorch-aarch64 · GitHub

SageMakerでlocal training jobが実行できない時の対処法 | Unable to locate credentials

Unable to locate credentials

のエラーで、SageMaker instanceのlocal training jobが実行できないときの対処法をまとめます。

はじめに

SageMakerでは、SageMaker Python SDKを利用して簡単にtraining job/学習ジョブを実行することができます。

SageMaker SDKのtraining jobには、

  • オンデマンドに別インスタンスが立ち上がってジョブを実行する通常ジョブ
  • 作業中のNotebookインスタンス上でジョブを実行するlocalジョブ

の2種類があります。

この後者、local training jobを実行する際に、Unable to locate credentialsエラーが発生してジョブが実行できない現象に遭遇しました。

対処法を見つけたので、備忘録としてまとめておきます。

SageMakerでlocal training jobが実行できない時の対処法

事象

  • SageMaker instanceでlocal training jobを実行した際に、ジョブコンテナでUnable to locate credentialsエラーが発生してジョブが異常終了する

ただし、私はまだ確実なエラー再現手順を特定できてません。
同様の現象は下記のようにいくつか報告があがっています:

原因

  • ジョブ実行コンテナからインスタンスメタデータエンドポイント(169.254.169.254)にアクセスできていないこと

インスタンスメタデータエンドポイント(169.254.169.254)は、インスタンスのメタデータを取得するためのリンクローカルアドレスです。

Localのジョブ実行コンテナがインスタンスに付与されたIAM権限を参照/Token取得する際に、このエンドポイントにアクセスする必要があります。

しかし、このエンドポイントにアクセスできていないことで、ジョブ実行コンテナがIAMのTokenを取得できない = 権限を取得できない = Unable to locate credentials、という状況です。

対処法

  • 下記のようなスクリプトを実行し、ジョブ実行コンテナから169.254.169.254へアクセスできるように設定変更する
#!/bin/bash

# check if we need to configure our docker interface
SAGEMAKER_NETWORK=`docker network ls | grep -c sagemaker-local`
if [ $SAGEMAKER_NETWORK -eq 0 ]; then
  docker network create --driver bridge sagemaker-local
fi

# Get the Docker Network CIDR and IP for the sagemaker-local docker interface.
SAGEMAKER_INTERFACE=br-`docker network ls | grep sagemaker-local | cut -d' ' -f1`
DOCKER_NET=`ip route | grep $SAGEMAKER_INTERFACE | cut -d" " -f1`
DOCKER_IP=`ip route | grep $SAGEMAKER_INTERFACE | cut -d" " -f9`

# check if both IPTables and the Route Table are OK.
IPTABLES_PATCHED=`sudo iptables -S PREROUTING -t nat | grep -c $SAGEMAKER_INTERFACE`
ROUTE_TABLE_PATCHED=`sudo ip route show table agent | grep -c $SAGEMAKER_INTERFACE`

if [ $ROUTE_TABLE_PATCHED -eq 0 ]; then
  # fix routing
  sudo ip route add $DOCKER_NET via $DOCKER_IP dev $SAGEMAKER_INTERFACE table agent
  echo "route tables for Docker setup done"
else
  echo "SageMaker instance route table setup is ok. We are good to go."
fi

if [ $IPTABLES_PATCHED -eq 0 ]; then
  # fix ip table
  sudo iptables -t nat -A PREROUTING  -i $SAGEMAKER_INTERFACE -d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 169.254.0.2:9081
  echo "iptables for Docker setup done"
else
  echo "SageMaker instance routing for Docker is ok. We are good to go!"
fi

重要なのは下記2つの設定変更部分です。

# fix routing
sudo ip route add $DOCKER_NET via $DOCKER_IP dev $SAGEMAKER_INTERFACE table agent

# fix ip table
sudo iptables -t nat -A PREROUTING  -i $SAGEMAKER_INTERFACE -d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT --to-destination 169.254.0.2:9081

SageMaker SDKのlocal training jobでは、sagemaker-localという名前のdocker networkが利用されます (存在しない場合は作成する処理をスクリプト冒頭で行っています)。

このsagemaker-localを変更(ip routeの追加とiptablesのdestination変更)することによって、ジョブ実行コンテナから169.254.169.254へアクセスできるようになる -> インスタンスメタデータ取得できるようになる -> IAM権限Tokenを取得できるようになる、で、エラーを解消できました。

※上記スクリプトは、AWS公式が提供しているSageMaker examples内で見つけたセットアップスクリプトから一部を抽出/改変したものです。

おわりに

以上、Unable to locate credentialsエラーでSageMakerのlocal training jobが実行できないときの対処法をまとめました。

再現方法が特定できていないなどモヤモヤが残る部分はありますが、取り急ぎ解決にたどり着くことができました。

思わぬハマりポイントだったので、この記事が同じ目に遭ったどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

amazon web services - Assume Sagemaker Notebook instance role from Docker container with default network mode - Stack Overflow

Local mode: role chaining/assumed role on notebook instances does not forward correct credentials · Issue #3464 · aws/sagemaker-python-sdk · GitHub

amazon-sagemaker-examples/setup.sh at main · aws/amazon-sagemaker-examples · GitHub

GitHub - aws/sagemaker-python-sdk: A library for training and deploying machine learning models on Amazon SageMaker

Retrieve instance metadata - Amazon Elastic Compute Cloud

Link-local address - Wikipedia

boto3とbotoの違い | AWS SDK for Pythonの歴史を調べる

boto3とbotoの違いや、そもそもの"boto"の由来など、AWS SDK for Pythonの歴史を調べてまとめます。

はじめに

AWS SDK for Pythonとしてboto3をいつも使っています。

ただ、私がAWSを触り始めたときには既に"boto3"になっていたので、"3"というからには無印botoやboto2もあるんだよね..?というモヤっとした疑問を抱えていました。 以前からAWSを知る人にとっては自明かもしれませんか、自分はまだ明確な答えを知りません。

そこで今回はboto3の歴史的背景を調べ、

  • boto3とbotoの違い
  • boto2はどこに行ったのか
  • そもそも"boto"の由来は?

をまとめます。

AWS SDK for Pythonの歴史

boto3とbotoの違い

少し調べると、boto3とは別にbotoがライブラリとして存在していたことが分かります。

このbotoとboto3の違いを調べてみると、stackoverflowにbotoの作者自身が回答を寄せていました:
python - What is the difference between the AWS boto and boto3 - Stack Overflow

ざっくり意訳抜粋すると、

  • botoは2006年から使われていたAWS公式のAWS Python SDK
  • しかしその設計から、AWSサービスが増えるにしたがってメンテが困難になった
  • そこで設計を刷新したboto3が開発された
    • botocoreをベースとする設計
    • AWSとの低レベルなインターフェースは自動生成される
    • これによりクライアント層の設計を綺麗に保てるようになった

ということのようです。
なるほど私が知らなかっただけで、botoは2006年から(boto3がGAする)2015年まで10年近くも活躍していたんですね。 時間の経過とともに設計刷新の必要性が出たのも納得できます。

各GitHub starsの推移からも、この背景が透けて見えてきます:

  • botoは元々人気のあったライブラリ
  • 2015頃からboto3/botocoreが登場
  • 以後boto3の人気は直線状に上昇/botoは役目を終える

boto, boto3, botocoreのGitHub star数推移

boto2はどこに行ったのか

botoとboto3登場の歴史は納得しました。
が、"boto2はどこに行ったのか"という疑問が残ります。

これは"boto2"という言葉がどの文脈で使われているのか、を過去の情報から調べることで推し量ろうと思います。

いろいろ調べて得た結論はこちらです:

「boto2 はbotoのversion2のことを指すらしい」


ドキュメント類の記述からいくつか例示します:

botoのドキュメントには、"これはbotoの古いバージョン(boto2)のドキュメントだから、boto3を見ることをお勧めします"という記述があります:

You are viewing the documentation for an older version of boto (boto2).
boto: A Python interface to Amazon Web Services — boto v2.49.0


boto3のドキュメントにも、"boto2からの移行"という文脈でbotoのversion2のことを"boto2"と呼んでいる様子が見られます:

The rest of this document will describe specific common usage scenarios of Boto 2 code and how to accomplish the same tasks with Boto3.
Migrating from Boto 2.x - Boto3 1.26.127 documentation


以上、歴史的推移をまとめると、

  • 2006年よりbotoが利用されてきた
  • botoのversion2のことをboto2と呼んでいた
  • 2015年、設計刷新したboto3がリリースされた

ということになりそうです。 スッキリしました。

"boto"の由来

ちなみに"boto"という言葉の由来については、boto3のREADMEに記載があります:

Boto (pronounced boh-toh) was named after the fresh water dolphin native to the Amazon river.
GitHub - boto/boto3: AWS SDK for Python

アマゾンカワイルカ/Amazon river dolphin/Inia geoffrensis/通称boto、が由来のようです。

また下記Issueでは、botoの作者自身がその由来をコメントされています:

It was named after the fresh water dolphin native to the Amazon river. I wanted something short, unusual, and with at least some kind of connection to Amazon. Boto seemed to fit the bill 8^)
Why is the project named boto? · Issue #1023 · boto/boto3 · GitHub

(意訳)
アマゾン川に生息する川イルカから名前を付けました。 短くて、特徴的で、何かしらAmazonに関係する名を付けたかったので、Botoはぴったりの名前でした。

Amazon river dolphin (boto) の写真 | 画像はWikipediaより

おわりに

以上、boto3とbotoの違いから、AWS SDK for Pythonの歴史を調べてまとめました。

ふと思い立って発作的に始めた調べものでしたが、なかなか面白いことが知れて満足です。

同じように不思議に思ったどなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

python - What is the difference between the AWS boto and boto3 - Stack Overflow

Elastician

What is the difference between the AWS boto and boto3? - Quora

boto: A Python interface to Amazon Web Services — boto v2.49.0

Migrating from Boto 2.x - Boto3 1.26.127 documentation

GitHub - boto/boto3: AWS SDK for Python

GitHub - boto/boto: For the latest version of boto, see https://github.com/boto/boto3 -- Python interface to Amazon Web Services

GitHub - boto/botocore: The low-level, core functionality of boto3 and the AWS CLI.

Why is the project named boto? · Issue #1023 · boto/boto3 · GitHub

Amazon river dolphin - Wikipedia

Raspberry Piで複数のサーボモーターを制御する

PWM/サーボ制御モジュールを使わずに、複数のサーボモーターを制御する方法の備忘録です。

はじめに

最近またRaspberry Piを触って遊んでいます。

複数のサーボモーターを制御したい、となったとき、世の事例を検索してみると、多くの場合はPWM/サーボ制御モジュールを取り付けることが多そうです。 ただ今回は外部モジュールを使わずに、とりあえず素のGPIOで複数サーボを制御してみました (パーツの用意やはんだ付けが面倒だったので...)。

やり方の備忘録を残します。


[関連記事] www.bioerrorlog.work

Raspberry Piで複数のサーボモータを制御する

OS環境の準備

モデルはRaspberry Pi 4 Model B (4GB RAM)を使いました。

Raspberry PiにはUbuntu 22.04.1 LTS (64-bit)を入れています (Raspberry Pi OSなど他の環境でも特に動作に違いはないはず)。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:    22.04
Codename:   jammy

Raspberry PiにUbuntuを入れる方法は別途記事にしているので、こちらを参照ください: www.bioerrorlog.work

回路を組む

回路は↓のように組みました。

  • サーボモーターにはSG90を使用
  • サーボモーターに供給する外部電源は単4電池4本パック/6Vを使用
    (データシートによるとギリギリSG90の動作範囲)
  • 外部電源とサーボモーターのマイナス端子はRaspberry PiのGNDに接続
  • サーボモーターのPWM制御線とRaspberry PiのGPIOを1kΩ抵抗を挟んで接続
    (今回は17, 18, 27のpinを使用)

サーボモータを動かすスクリプトの用意

では、サーボモーターの動作チェックのための簡単なスクリプトを書きます。 今回はPythonで書きました。

ソースコード: github.com

import math
import time
from gpiozero import AngularServo


def main():
    servo_17 = AngularServo(17)
    servo_18 = AngularServo(18)
    servo_27 = AngularServo(27)

    t = 0
    max_angle = 60
    gap_angle = 30

    try:
        while True:
            servo_17.angle = math.sin(math.radians(t % 360)) * max_angle
            servo_18.angle = math.sin(math.radians((t % 360) + gap_angle)) * max_angle
            servo_27.angle = math.sin(math.radians((t % 360) + (gap_angle * 2))) * max_angle

            t += 2
            time.sleep(0.01)
    except KeyboardInterrupt:
        servo_17.angle = 0
        servo_18.angle = 0
        servo_27.angle = 0


if __name__ == '__main__':
    main()

gpiozeroAngularServoを使って3つのサーボを動かす、動作確認用のちょっとしたスクリプトです。 回転を滑らかにするため、角度の計算にはsin関数を挟んでいます。

サーボモータを動かす

では用意したスクリプトをRaspberry Piで実行します。

スクリプトをcloneして、必要なライブラリをRaspberry Piにインストールします:

# Clone
git clone https://github.com/bioerrorlog/raspberry-pi-examples.git
cd raspberry-pi-examples/multi_servo_gpio/

# Install required packages
sudo apt update
sudo apt upgrade
sudo apt install python3-pip python3-dev gcc git
sudo pip install -r requirements.txt

そしたら、Pythonスクリプトを実行します:

sudo python3 main.py

# sudoで実行するのは、下記エラー回避のため:
# RuntimeError: No access to /dev/mem. Try running as root!

上手く実行できれば、↓みたいな感じでサーボモーターが動きます。

(サーボモーター同士をダンボールで固定しています)

おわりに

Raspberry Piで複数のサーボモーターを制御する方法の備忘録でした。

PWM制御モジュールも使ってないし、ソフト面での工夫もしていないので結構ジッターが入ってしまいますが、とりあえず動く、というところは見ることができます。

以上、備忘録でした。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

O'Reilly Japan - Raspberry Piクックブック 第3版

マイクロサーボ9g SG-90 | 秋月電子通商

GitHub - gpiozero/gpiozero: A simple interface to GPIO devices with Raspberry Pi

GitHub - bioerrorlog/raspberry-pi-examples: Example projects for Raspberry Pi.

Athena経由でpandas DataFrameを作成する

Amazon Athena経由でpandas DataFrameを作成するやり方をまとめます。

はじめに

こんにちは、@bioerrorlogです。

Amazon Athenaクエリ経由でpandas DataFrameを作成したい、としたらどのようなやり方があるでしょうか。 

例えばいちばん愚直にやろうとすれば、boto3でAthenaクエリを発行し、クエリ結果からデータを取得して、上手いことpandasに読み込ませる...とかでしょうか。 そんな複雑なことやりたくないなぁと思って調べてみても、stackoverflowの上位answerでも似たような解決策が示されています。

しかしもう少し調べると、awswranglerを使えばかなりスッキリ書けることを知りました。

今回は、awswranglerを使ってAthena経由でpandas DataFrameを作成するやり方をメモします。


※追記:awswranglerはAWS SDK for pandasに改名されました(使い方は特に変わってない模様です)。

awswrangler (AWS SDK for pandas)とは

awswranglerは、AWSサービスとpandasを連携するツールです。

AWS公式(awslabs)が開発しており、Athenaだけでなく各AWSのDB系サービスとの連携もサポートしています。

Pandas on AWS. Easy integration with Athena, Glue, Redshift, Timestream, OpenSearch, Neptune, QuickSight, Chime, CloudWatchLogs, DynamoDB, EMR, SecretManager, PostgreSQL, MySQL, SQLServer and S3 (Parquet, CSV, JSON and EXCEL).

github.com

Athena経由でpandas DataFrameを作成する

このawswranglerを使えば、次のように簡単にAthenaのクエリ結果をそのままpandas DataFrameに読み込ませることができます。

import awswrangler as wr

df = wr.athena.read_sql_query(sql='SELECT * FROM "<table_name>"', database='<database_name>')

ちなみに上記のコードでは、Athena query resultの出力先を指定していません。 その場合はデフォルトでs3://aws-athena-query-results-ACCOUNT-REGION/にquery resultが出力されますが、任意のS3を指定したければs3_outputから指定することができます。

その他athena.read_sql_queryの各オプションはドキュメントをご覧ください

おわりに

今回は、awswranglerを使ってAthena経由でpandas DataFrameを作成するやり方をメモしました。

素のboto3で書くよりもずっとシンプルに書くことができるので、なかなか使い勝手が良いのではないでしょうか。 awslabs公式のツールであることも嬉しいポイントです。

以上、どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - aws/aws-sdk-pandas: Pandas on AWS - Easy integration with Athena, Glue, Redshift, Timestream, Neptune, OpenSearch, QuickSight, Chime, CloudWatchLogs, DynamoDB, EMR, SecretManager, PostgreSQL, MySQL, SQLServer and S3 (Parquet, CSV, JSON and EXCEL).

Quick Start — AWS SDK for pandas 2.17.0 documentation

awswrangler.athena.read_sql_query — AWS SDK for pandas 2.17.0 documentation

python - How to Create Dataframe from AWS Athena using Boto3 get_query_results method - Stack Overflow