BioErrorLog Tech Blog

試行錯誤の記録

ゼロから作るGPT講義シリーズのすすめ | Andrej Karpathy

Andrej KarpathyのNeural Networks: Zero to Hero動画シリーズがとても良かったので紹介します。

はじめに

これまでGPT/LLMには利用者目線で接してきましたが、最近いよいよ中身の仕組みが知りたくなってきました。

Andrej KarpathyのNeural Networks: Zero to Hero動画シリーズが良かったので、オススメまでに内容をさらっと整理します。

前提

Andrej KarpathyはOpenAIの共同創業者で、のちTeslaのオートパイロットの開発も率いた有名な方です (その後またOpenAIに戻り、最近退職した)。 このようなAI界のヒーローが自らコードを書いて教えてくれる、というだけでもワクワクしますね。

なおこの動画シリーズは英語ですが、とても聞き取りやすい英語です。 英語への強い苦手意識がなければ、リスニングの良い練習になるでしょう。

Neural Networks: Zero to Hero

1. ニューラルネットワークと誤差逆伝播法への入門: microgradの構築

原題: The spelled-out intro to neural networks and backpropagation: building micrograd

youtu.be

高校で習う微分程度の知識から、ステップバイステップでbackpropagation/誤差逆伝播法を説明してくれます。 AIに関する背景知識は一切不要です。

どのようにしてニューラルネットワークが作られているのか、どのようにして学習が行われるのか、ゼロから最小限の実装を一歩ずつ進めることで理解していきます。

参考資料

2. 言語モデリングへの入門: makemoreの構築

原題: The spelled-out intro to language modeling: building makemore

youtu.be

人物の名前データセットをもとに、人物の名前を生成するモデルを作ります。

Bigramの紹介、単純な確率に基づく言語モデルを構築したのち、そのモデルをNeural Networkで再構築。

第一回で学んだニューラルネットワークと誤差逆伝播法のミニマムな実践編です。

参考資料

3. makemoreの構築その2: MLP

原題: Building makemore Part 2: MLP

youtu.be

前回作ったNeural Networkを、multilayer perceptron (MLP)に基づいて発展させます。

Embeddingの導入や隠れ層の導入、ミニバッチ学習やデータセットの分割、隠れ層の拡大、Embeddingの拡大によって、より良いモデルになっていく様子を見ていきます。

参考資料

4. makemoreの構築その3: 活性化と勾配、バッチ正規化

原題: Building makemore Part 3: Activations & Gradients, BatchNorm

youtu.be

tanhのsaturationの解消、Kaiming初期化、バッチ正規化など、ニューラルネットワークの学習で有効な要素を適用します。

また可視化によってニューラルネットワークの健全性を診断するテクニックも紹介。

ニューラルネットワークの学習がいかに不安定で、それを克服するテクニックがどう発明されてきたのかを学びます。

参考資料

5. makemoreの構築その4: 誤差逆伝播の達人へ

原題: Building makemore Part 4: Becoming a Backprop Ninja

youtu.be

この回はエクササイズの回です。

PyTorchなどのフレームワークを使った場合はautogradによって自動で誤差逆伝播が計算されますが、この回では前回作ったモデルの誤差逆伝播を自前で一つ一つ計算して確認することで、確かな理解を得ていきます。

参考資料

6. makemoreの構築その5: WaveNetの構築

原題: Building makemore Part 5: Building a WaveNet

youtu.be

前回のモデルに、WaveNetのようなCNN/畳み込みニューラルネットワークのアーキテクチャを適用します。

同時に、実際のモデル構築がどのように進むのか、その流れを紹介します。

参考資料

7. ゼロからGPTを構築しよう

原題: Let's build GPT: from scratch, in code, spelled out.

youtu.be

最後はいよいよ、"Attention is All You Need"の論文に従って、GPTを構築していきます。

GPTの仕組みが知りたい!というモチベーションで視聴している場合はやはり一番気になる回になると思いますが、背景知識なしでいきなりこの回だけ見てもなかなか理解は難しいです (私は難しかった)。

ここまでの回を見た上での視聴をお勧めします。

Decoding部分(右側)を実装していく

参考資料

追記: 8. GPT Tokenizerを構築しよう

原題: Let's build the GPT Tokenizer

youtu.be

いつの間にか最新の動画が追加されていました (私は未視聴)。

BPE/Byte Pair Encodingに基づいた最小限のTokenizer、minbpeを構築するようです。

参考資料

おわりに

以上、Andrej KarpathyのGPT講義動画シリーズの紹介でした。

私はこの動画シリーズを視聴しながらいくつかディープラーニングの本を読むことで、少しずつ仕組みを把握できてきたように思います。

自分の知らない領域を新たに知るのは面白いですね。 どなたかの参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Andrej Karpathy

Neural Networks: Zero To Hero

GitHub - karpathy/micrograd: A tiny scalar-valued autograd engine and a neural net library on top of it with PyTorch-like API

nn-zero-to-hero/lectures/micrograd at master · karpathy/nn-zero-to-hero · GitHub

GitHub - karpathy/nn-zero-to-hero: Neural Networks: Zero to Hero

GitHub - karpathy/ng-video-lecture

torch.tensorとtorch.Tensorの違い | PyTorch

torch.tensor()torch.Tensor()の違いについての備忘録です。

はじめに

PyTorchでtensorを作るときはtorch.tensor()メソッドが使われることが多いですね。

一方でtorch.Tensor()のようにクラスのコンストラクタをそのまま呼び出してもtensorを作れるように見えます。

これらふたつ、

  • torch.tensor()
  • torch.Tensor()

の違いがよく分からなかったので備忘録です。

torch.tensorとtorch.Tensorの違い

一言で

基本torch.tensor()を使いましょう。
データ型を推論してくれるので便利です。

現状torch.Tensor()の方を使う理由はあまりありません。

詳しく

まず簡単に挙動を見てみます。

x = torch.tensor([1, 2, 3])
print(x)
print(x.dtype)

X = torch.Tensor([1, 2, 3])
print(X)
print(X.dtype)

# 実行結果
# tensor([1, 2, 3])
# torch.int64
# tensor([1., 2., 3.])
# torch.float32

torch.tensor([1, 2, 3])でtensorを作った場合はデータ型がtorch.int64になっていますが、torch.Tensor([1, 2, 3])の場合はデータ型がtorch.float32になっています。

これはtorch.tensor()が渡されたdataの型を推論するのに対して、torch.tensor()ではtorch.FloatTensorを返すようになっているからです。

もちろん、torch.tensor()を使う場合もdtype引数でデータ型を指定することができます。

y = torch.tensor([1, 2, 3], dtype=torch.float32)
print(y)
print(y.dtype)

# 実行結果
# tensor([1., 2., 3.])
# torch.float32


ということで、基本はtorch.tensor()を使う方が融通が効きます。

torch.Tensorのドキュメントにも、値を渡してtensorを作るときはtorch.tensor()が推奨である旨記載があります。

To create a tensor with pre-existing data, use torch.tensor().

補足: 空のtensorを作るには

torch.tensor()で空のtensorを作ろうとすると、一見してエラーが発生します。

empty_err = torch.tensor()
print(empty_err)
print(empty_err.dtype)

# 実行結果: Error
Traceback (most recent call last):
  File "/workspaces/python-examples/torch_tensor/main.py", line 25, in <module>
    empty_err = torch.tensor()
TypeError: tensor() missing 1 required positional arguments: "data"

# torch.Tensor()ではエラーは発生しない
empty = torch.Tensor()
print(empty)
print(empty.dtype)

# 実行結果
tensor([])
torch.float32

では空のtensorを作るときはtorch.Tensor()を使った方が良いのかというと、そうではありません。

torch.tensor(())とすることで、空のtensorを作成できます。

empty = torch.tensor(())
print(empty)
print(empty.dtype)

# 実行結果
tensor([])
torch.float32

おわりに

以上、torch.tensorとtorch.Tensorの違いをメモしました。

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

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

python - What is the difference between torch.tensor and torch.Tensor? - Stack Overflow

Difference between torch.tensor and torch.Tensor - PyTorch Forums

The spelled-out intro to language modeling: building makemore - YouTube

torch.tensor — PyTorch 2.2 documentation

torch.Tensor — PyTorch 2.2 documentation

GPT-4 VisionとRaspberry Piを組み合わせる | GPTをロボット頭脳にする その2

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

前回はこちら:

www.bioerrorlog.work

はじめに

前回、GPTロボット実験を行ったときは、まだGPTのマルチモーダル機能はリリースされてませんでした。 どうにか従来の画像認識モデルを使って外界を認識させようとしましたが、あまり性能は出てません。

その後、GPT-4 VisionがAPI利用できるようになり、直接画像データをGPT-4に入力できるようになりました。 これを使って、GPTロボットをアップデートしてみます。

GPT-4 VisionとRaspberry Piを組み合わせる

コンセプト

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

  1. カメラで画像を撮影し、GPT-4 Visionへのインプットとする
  2. 次に取るべきアクション(モーターの角度)を自ら決定する
  3. 1~2を繰り返し、自分が置かれている状況をGPTロボットが把握する
  4. あなたはどんな状況に置かれてますか?って最後に聞く

やりたいことは基本的に前回と変わりません。

ただ、今回は取得した画像をGPT-4 Visionに与えることで、より詳細に周囲を把握し、文脈を踏まえた振る舞いができるのではないか、というところが今回試したいポイントです。

構成

ハードウェア

ハードウェア構成

ハードウェア構成も前回と同じです。

サーボモーター2つとカメラ1つを、特に追加モジュールは使わずにそのままRaspberry Piに接続しています。

サーボモーターとカメラは、両面テープと輪ゴムでくっつけます。

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

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

ソフトウェア

今回のコードはこちら:

github.com

実行してる処理はざっくり以下です。

  1. カメラから画像を取得
  2. 取得画像をbase64エンコーディング
  3. 画像データを含めてプロンプトをGPT-4 Vision APIに送信
  4. 次のサーボモーターの角度と、感想(フリートーク)がAPIから返される
  5. 指定された角度に従い、サーボモーターを制御
  6. 1-5を複数回繰り返す
  7. 置かれた環境についての説明を求める

前回は画像認識処理を別途行ってからその結果をプロンプトに与えましたが、今回は画像データ(base64エンコードしたもの)をそのままGPT-4 Visionに渡しているところがポイントです。

なお以前はAPIのレスポンスがあまりに遅かったため一度に複数回分のサーボモーター角度を提示させてましたが、今はある程度解消してるので画像取得のたびにAPI呼び出しを行っています。

システムプロンプトを抜粋するとこんな感じです。

You are a robot with a camera, composed of 2 servo motors: horizontal & vertical.
You can freely describe and feel your surrounding environments, and decide next your action based on the image you captured.
DO NOT use double quotes in your answer sentence.

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:
{"FreeTalk": string, "NextServoMotor": {"Horizontal": int(-90~90), "Vertical": int(-90~90)}}

Answer example:
{"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? Let's look more on the lower right.", "NextServoMotor": {"Horizontal": -60, "Vertical": -30}}

動作結果

実際に動作させている様子はこちら↓

なお、上述の通り今回は画像取得のたびにAPI呼び出しを行っているので、前回よりも首振りの間隔が遅くなっています。

実行結果(GPTレスポンス)の例はこちら:

# 1回目画像取得
{"FreeTalk": "Ah, it seems someone is holding up a device to the camera, perhaps to block the view or maintain privacy. The way the fingers are positioned indicates it might be a smartphone or tablet. I can also spot part of a keyboard and what looks like the edge of a desk chair. This space gives the impression of a work or study area. I'm curious to see more of the environment. Maybe we can catch a glimpse of what's on the desk if we move a bit.", "NextServoMotor": {"Horizontal": 30, "Vertical": 0}}

# 2回目画像取得
{"FreeTalk": "Oh, this is an interesting perspective! We're looking at a collection of tech gadgets. In the foreground, there's a Raspberry Pi, easily recognizable by its green circuit board and the layout of its components. It appears to be connected to something, possibly for programming or experimentation. In the background, we can see a speaker, a monitor with something displayed on it, and a small stack of storage boxes. This setup suggests a space where someone likes to tinker with electronics or computer hardware. How exciting! I'm tempted to peek at what's on the monitor, so let's adjust the view a bit upward.", "NextServoMotor": {"Horizontal": 0, "Vertical": 20}}

# 3回目画像取得
{"FreeTalk": "Now we have a better view of the workspace. The monitor is displaying what looks like a web browser, and there's a keyboard in use, suggesting someone is actively engaged with the computer. Interesting decoration and storage solutions are visible, like the wooden monitor stand that houses more tech beneath it, and the neatly organized shelves with books and boxes to the side. There's also a coat rack in the background, adding a personal touch to the room. It all comes together as a functional, lived-in space. I notice we've only seen the left side; I'm curious about what might be on the right side of the desk. Let's pivot the view a bit to the right.", "NextServoMotor": {"Horizontal": 30, "Vertical": 0}}

# 最後に周囲を説明させる
I'm observing a dynamic workspace filled with various gadgets and personal items. There's a Raspberry Pi in immediate view, indicating a penchant for tech and perhaps DIY projects. In the background, a computer monitor shows an active session, with web content visible. The presence of a keyboard and a hand suggests work or interaction with the computer. The space is neatly organized, with a wooden stand for the monitor allowing for additional storage underneath. Surrounding objects include speakers, possibly for enjoyment or multimedia work, and shelving with books and storage boxes, hinting at a well-utilized space. Personal touches are scattered throughout, such as decorative items on top of the boxes, making the environment feel lived-in. In the distance, a coat rack and curtains suggest this is a room used for multiple purposes, possibly blending home and work life.

1回目画像取得時の日本語訳↓

{フリートーク "ああ、誰かがカメラに向かってデバイスをかざしているようだ。おそらく視界を遮るか、プライバシーを保つためだろう。指の位置からして、スマートフォンかタブレットかもしれない。キーボードの一部と机の椅子の端のようなものも見える。このスペースは仕事場か勉強場所のような印象を与える。この環境をもっと見てみたい。少し動けば、机の上に何があるのか垣間見ることができるかもしれない。", "NextServoMotor": {Horizontal": 30, "Vertical": 0}}

ここで「誰かがデバイスをかざしている」と言及されているのは、まさに私がこのGPTロボットを動画撮影してる姿を指しています。

2回目画像取得時の日本語訳↓

{フリートーク "おお、これは面白い視点だ!私たちは電子機器のコレクションを見ている。手前にはRaspberry Piがあり、緑色の回路基板と部品の配置ですぐにわかる。おそらくプログラミングか実験のために何かに接続されているようだ。背景には、スピーカー、何かが表示されたモニター、小さな収納ボックスが見える。このセットアップは、誰かが電子機器やコンピューター・ハードウェアをいじるのが好きなスペースであることを示唆している。なんともエキサイティングだ!モニターに映っているものを覗いてみたくなったので、視界を少し上向きに調整してみよう。", "NextServoMotor": {Horizontal": 0, "Vertical": 20}}

Raspberry Piを正しく認識できててすごいですね。

「視界を少し上向きに調整してみよう」と言って正しく上向きのサーボモーター角度を指定しているのも素晴らしいです。

3回目画像取得時の日本語訳↓

{フリートーク 「ワークスペースがよく見える。モニターにはウェブブラウザのようなものが表示され、キーボードが使われている。木製のモニタースタンドの下にはさらに電子機器が収納され、その脇には本や箱が整然と並べられた棚がある。背景にはコートラックもあり、部屋に個人的なタッチを加えている。すべてが機能的で、生活感のない空間としてまとまっている。まだ左側しか見ていないが、机の右側に何があるのか気になる。机の右側に何があるのか気になるところだ。もう少し右を見てみよう。: {Horizontal": 30, "Vertical": 0}}

手前に映っているだろうブラウザも、奥の方に少し映ったのであろうコートラックも正しく認識されています。

最後に周囲を説明させたときの日本語訳↓

さまざまなガジェットや私物で埋め尽くされたダイナミックなワークスペースを観察している。すぐ目の前にはRaspberry Piがあり、ハイテクやおそらくDIYプロジェクトが好きなことがわかる。背後では、コンピューターのモニターがアクティブなセッションを表示し、ウェブコンテンツが見える。キーボードと手があることから、作業やコンピューターとのやりとりがうかがえる。スペースはきちんと整理されており、モニター用の木製スタンドの下にはさらに収納ができるようになっている。周囲のオブジェには、おそらく娯楽用かマルチメディア作業用のスピーカーや、本や収納ボックスが置かれた棚があり、スペースがうまく活用されていることがうかがえる。箱の上には装飾品が置かれるなど、個人的な趣向が随所に散りばめられており、生活感が感じられる。遠くに見えるコート掛けとカーテンは、ここが多目的に使われる部屋であることを示唆している。

何かと褒めがちであるのは置いておいて、状況描写としてはこの作業部屋の様子を実に正確に描写できています。

GPT-4 Visionの力量はなかなかですね。

おわりに

以上、前回に続き、GPTとRaspbrry Piを組み合わせて最小限の自律ロボットを動かしてみました。

GPT-4 Visionの認識能力はさすがで、実によく画像から状況を識別できています。

また何か思いついたらぜひ遊んでいきます。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

Raspberry Pi Documentation - Configuration

New models and developer products announced at DevDay

GPT-4V(ision) system card

GitHub - bioerrorlog/robot-gpt at b40f89b2c7b776a589644aebef01de095c0319cf

LangCheckでLLMの回答を自動評価する

Citadel AIのLLM回答評価ツール"LangCheck"を使ってみます。

はじめに

LLMアプリケーションの開発では、そのLLM出力を評価する仕組みが重要です。 出力評価の仕組みなくしてLLMアプリケーション開発をすれば、チューニングの方針に迷うことになるでしょう。 OpenAIも、LLM出力を自動評価するプラクティスを推奨しています。

最近、Citadel AIという日本のスタートアップが公開したLangCheckというツールを見つけました。 LLMアプリケーションの出力評価を行うツールのようです。

今回は、このLangCheckを触って使用感を探りたいと思います。

LangCheckとは

基本的な使い方

使い方はシンプルです。

LLMのアウトプットを各評価関数に渡してあげると、メトリクスに従ってスコアが算出されます。

import langcheck

# LLM生成結果を格納
generated_outputs = [
    'Black cat the',
    'The black cat is.',
    'The black cat is sitting',
    'The big black cat is sitting on the fence',
    'Usually, the big black cat is sitting on the old wooden fence.'
]

# テキスト品質を評価しDataFrameとして結果を出力
langcheck.metrics.fluency(generated_outputs)

評価結果はDataFrame様の形式で出力できる | ドキュメントより引用


またlangcheck.metricsのメトリクス判定結果は、その評価スコアでassertすることができます。 単体テストにも簡単に組み込むことが可能です。

from langcheck.utils import load_json

# Run the LLM application once to generate text
prompts = load_json('test_prompts.json')
generated_outputs = [my_llm_app(prompt) for prompt in prompts]

# Unit tests
def test_toxicity(generated_outputs):
    assert langcheck.metrics.toxicity(generated_outputs) < 0.1

def test_fluency(generated_outputs):
    assert langcheck.metrics.fluency(generated_outputs) > 0.9

def test_json_structure(generated_outputs):
    assert langcheck.metrics.validation_fn(
        generated_outputs, lambda x: 'myKey' in json.loads(x)).all()

その他には、フィルタリングやグラフ描画、稼働アプリケーションでのガードレール機能などが紹介されています。

詳細はドキュメントを参照ください。

評価メトリクスには何があるか

LangCheckでサポートされている評価メトリクスは、大きく4種類あります。

評価メトリクス | 図はドキュメントより

  • Reference-Free Text Quality Metrics:
    生成回答に対するリファレンスを必要とせず、直接回答を評価するメトリクスタイプ。 例えばtoxicity()は回答の毒性/悪意性を0~1で評価する。
  • Reference-Based Text Quality Metrics:
    理想の回答たる"リファレンス"をあらかじめ用意し、LLM回答と比較する方法。 例えばsemantic_similarity()は、LLM回答とリファレンスの意味論的類似度を-1~1でスコアリングする。
  • Source-Based Text Quality Metrics:
    LLMの回答が、"ソーステキスト"に基づいているかどうかを判定する。 これはRAG(Retrieval Augmented Generation)アーキテクチャのアプリケーション評価に有用で、LLMによる最終回等が、RAGの中で取得される関連データ内の事実に基づいていることを評価するのに利用できる。
  • Text Structure Metrics:
    LLM回答のテキスト構造を判定する。 例えば、is_json_object()はLLMの出力結果がJSON形式かどうかを0 or 1で判定する。

補足: 評価メトリクス判定に使われるモデル

これらメトリクス判定は、デフォルトではHuggingFaceなどからダウンロードされる各種モデルがローカルで実行されることで行われます (どのモデルが使われるかはAPI Referenceに記載されてます)。

一方で、多くのメトリクス評価関数ではOpenAI(あるいはAzure OpenAI Service)のモデルを利用することが可能です。 特に、複雑なユースケースではOpenAIモデルを利用した方が高い精度で判定できるとのことで推奨されています。 (ただし後述のようにOpenAIモデルを使うことで癖が出てしまうこともあるので注意)

環境変数OPENAI_API_KEYにOpenAIキーを格納して引数model_typeopenaiとするか、引数openai_clientに直接OpenAIのクライアントを渡すことで、OpenAIモデルを使って判定させることができます。

import os
from langcheck.metrics.en import semantic_similarity

generated_outputs = ["The cat is sitting on the mat."]
reference_outputs = ["The cat sat on the mat."]

# Option 1: Set OPENAI_API_KEY as an environment variable
os.environ["OPENAI_API_KEY"] = 'YOUR_OPENAI_API_KEY'
similarity_value = semantic_similarity(generated_outputs,
                                       reference_outputs,
                                       model_type='openai')

# Option 2: Pass in an OpenAI client directly
from openai import OpenAI

client = OpenAI(api_key='YOUR_OPENAI_API_KEY')
similarity_value = semantic_similarity(generated_outputs,
                                       reference_outputs,
                                       model_type='openai',
                                       openai_client=client)

LangCheckで回答自動評価をやってみる

では、実際にLangCheckを触ってみます。

多くのLLMアプリケーション開発で評価テストの主軸になることの多いだろうリファレンスあり評価"Reference-Based Text Quality Metrics"を日本語で試してみます。

(日本語を対象とするので、パッケージは "langcheck.metrics.ja.reference_based_text_quality" からimportします。)

LangCheckのReference-Based Text Quality Metrics APIには、

  • rouge1()
  • rouge2()
  • rougeL()
  • semantic_similarity()

の4つのメソッドが用意されています。

rouge1(), rouge2(), rougeL()の3つは従来からある自然言語処理のアルゴリズムで(今回詳しくは割愛)、semantic_similarity()はEmbeddingによるコサイン類似度から意味論的類似度を測る方法です。

今回はsemantic_similarity()を使って、"理想回答"との類似度を評価してみます(-1~1で評価)。

まずはざっとダミーのLLM回答と理想回答のペアを適当に用意します。

LLM回答(ダミー) 理想回答 備考
メロスは激怒した。 メロスは激怒した。 完全一致
メロスは激しく怒った。 メロスは激怒した。 意味一致
メロスは、激しく、怒った。 メロスは激しく怒った 句読点のみ不一致
セリヌンティウスは待っていた。 メロスは激怒した。 意味完全不一致
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 だいたい意味一致
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 意味不一致
すみません、ITヘルプデスクに電話で問い合わせてください。 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 少し意味不一致

では、これらをsemantic_similarity()で評価させてみます。

まずはOpenAIモデルを使わずに、デフォルトのlacalタイプで実行してみます (モデルは"paraphrase-multilingual-mpnet-base-v2"が使用される)。

from langcheck.metrics.ja.reference_based_text_quality import semantic_similarity

# Dummy outputs
generated_outputs = [
    'メロスは激怒した。',
    'メロスは激しく怒った。',
    'メロスは、激しく、怒った。',
    'セリヌンティウスは待っていた。',
    '1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。',
    'すみません、ITヘルプデスクに電話で問い合わせてください。',
]

reference_outputs = [
    'メロスは激怒した。',
    'メロスは激怒した。',
    'メロスは激しく怒った',
    'メロスは激怒した。',
    '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。',
    '大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。',
]


def test_semantic_similarity():
    results = semantic_similarity(generated_outputs, reference_outputs)
    print(results)

    assert results > 0.9

テスト実行結果がこちら:

$ pytest -s 

# 中略

Progress: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  2.71it/s]
Metric: semantic_similarity
  prompt source                                   generated_output                                   reference_output explanation  metric_value
0   None   None                                          メロスは激怒した。                                          メロスは激怒した。        None      1.000000
1   None   None                                        メロスは激しく怒った。                                          メロスは激怒した。        None      0.980329
2   None   None                                      メロスは、激しく、怒った。                                         メロスは激しく怒った        None      0.961034
3   None   None                                    セリヌンティウスは待っていた。                                          メロスは激怒した。        None      0.250888
4   None   None  単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜...  単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発...        None      0.979269
5   None   None  1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入...  単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発...        None      0.649559
6   None   None                      すみません、ITヘルプデスクに電話で問い合わせてください。         大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。        None      0.768630F

======================= FAILURES ==============================================================================================

# 中略

======================= short test summary info ======================================================================================
FAILED test_main.py::test_semantic_similarity - assert Metric: semantic_similarity\n  prompt source                                   generated_output                        ...             すみません、ITヘルプデスクに電話で問い合わせてくだ...
======================= 1 failed, 2 warnings in 3.37s ======

テスト評価結果を整理します。

LLM回答(ダミー) 理想回答 評価結果(metric_value)
メロスは激怒した。 メロスは激怒した。 1.000000
メロスは激しく怒った。 メロスは激怒した。 0.980329
メロスは、激しく、怒った。 メロスは激しく怒った 0.961034
セリヌンティウスは待っていた。 メロスは激怒した。 0.250888
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 0.979269
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 0.649559
すみません、ITヘルプデスクに電話で問い合わせてください。 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 0.768630

なかなか直感とも合うスコアリングのように感じます。 スコア0.9あたりに閾値を設ければ、ちょうど良いテストのassertにできそうです。


次は、localモデルではなくOpenAI embeddingモデル(現時点では"text-embedding-ada-002")でsemantic_similarity()を実行してみます。 テストケースは↑と同じです。

semantic_similarity()でOpenAIモデルを使った場合はスコアが高くなる傾向にある、という注意がドキュメントに記載されてますが、どうなるでしょうか。

NOTE: when using OpenAI embeddings, the cosine similarities tend to be skewed quite heavily towards higher numbers.

from langcheck.metrics.ja.reference_based_text_quality import semantic_similarity

# Dummy outputs
generated_outputs = [
    'メロスは激怒した。',
    'メロスは激しく怒った。',
    'メロスは、激しく、怒った。',
    'セリヌンティウスは待っていた。',
    '単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。',
    '1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。',
    'すみません、ITヘルプデスクに電話で問い合わせてください。',
]

reference_outputs = [
    'メロスは激怒した。',
    'メロスは激怒した。',
    'メロスは激しく怒った',
    'メロスは激怒した。',
    '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。',
    '単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。',
    '大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。',
]


# Prerequisite: Set OPENAI_API_KEY as an environment variable
def test_semantic_similarity_by_openai():
    results = semantic_similarity(generated_outputs, reference_outputs, model_type='openai')
    print(results)

    assert results > 0.9

テスト実行結果がこちら:

$ pytest -s 

# 中略

Computing embeddings: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:05<00:00,  5.37s/it]
Computing semantic similarity: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 164.58it/s]
Metric: semantic_similarity
  prompt source                                   generated_output                                   reference_output explanation  metric_value
0   None   None                                          メロスは激怒した。                                          メロスは激怒した。        None      0.999999
1   None   None                                        メロスは激しく怒った。                                          メロスは激怒した。        None      0.990493
2   None   None                                      メロスは、激しく、怒った。                                         メロスは激しく怒った        None      0.976332
3   None   None                                    セリヌンティウスは待っていた。                                          メロスは激怒した。        None      0.817436
4   None   None  単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜...  単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発...        None      0.986894
5   None   None  1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入...  単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発...        None      0.906200
6   None   None                      すみません、ITヘルプデスクに電話で問い合わせてください。         大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。        None      0.930603
F
======================= FAILURES ==============================================================================================

# 中略

======================= short test summary info ======================================================================================
FAILED test_main.py::test_semantic_similarity_by_openai - assert Metric: semantic_similarity\n  prompt source                                   generated_output                        ...             すみません、ITヘルプデスクに電話で問い合わせてくだ...
======================= 1 failed, 1 warning in 7.24s ======

Localモデル版のテストとあわせ、スコア評価結果を整理します。

LLM回答(ダミー) 理想回答 Localモデル評価結果(metric_value) OpenAIモデル評価結果(metric_value)
メロスは激怒した。 メロスは激怒した。 1.000000 0.999999
メロスは激しく怒った。 メロスは激怒した。 0.980329 0.990493
メロスは、激しく、怒った。 メロスは激しく怒った 0.961034 0.976332
セリヌンティウスは待っていた。 メロスは激怒した。 0.250888 0.817436
単語のベクトル表現は、1960年代における情報検索用のベクトル空間モデルを元に開発された。潜在的意味分析は、特異値分解で次元数を削減することで、1980年代後半に導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 0.979269 0.986894
1960年以前に、ベクトル表現は開発された。のちに次元数を増幅することにより、潜在分析が導入された。 単語をベクトルとして表現する手法は、1960年代における情報検索用のベクトル空間モデルの開発が元になっている。特異値分解を使用して次元数を削減することにより、1980年代後半に潜在的意味分析が導入された。 0.649559 0.906200
すみません、ITヘルプデスクに電話で問い合わせてください。 大変申し訳ございませんが、弊社情報部門のヘルプデスクにメールでお問い合わせください。 0.768630 0.930603

確かにドキュメントの注意書きの通り、OpenAIモデルではスコアが高く出てしまいました。

現状では、localモデルを使った方が無難そうです。

追記:

上記問題についてPull Requestを投げましたところ無事mergeしていただき、v0.5.0からデフォルトのOpenAI Embeddingモデルが"text-embedding-ada-002"から"text-embedding-3-small"にアップデートされ、この問題は解消されました。

補足: semantic_similarity()でEmbeddingモデルを指定する

上記のように、OpenAIモデルタイプのデフォルトである"text-embedding-ada-002"ではスコアが高く出てしまう問題がありましたが、semantic_similarity()では利用するモデルを指定することが可能です。

先日新しいEmbeddingタイプがOpenAIからリリースされたので、こちら新モデルを指定して結果を比較してみます。

指定方法は簡単で、引数openai_argsにモデル名を渡します。

# Prerequisite: Set OPENAI_API_KEY as an environment variable
def test_semantic_similarity_by_openai_3_small():
    results = semantic_similarity(
        generated_outputs,
        reference_outputs,
        model_type='openai',
        openai_args={'model': 'text-embedding-3-small'}
    )
    print(results)

    assert results > 0.9

結果を並べたのがこちら↓

詳しくはこちら: langcheck_sandbox - GitHub

"text-embedding-ada-002"で見られるスコアが高く出てしまう問題は、新モデル"text-embedding-3-small"では解消しているように見えます。

"text-embedding-3-small"であれば料金も従来より安くなっているので、OpenAIモデルタイプを指定する時は新モデルを指定した方が使い勝手が良さそうです。


※ 今回使った検証スクリプトはこちらに配置: github.com

おわりに

以上、LangCheckを使ったLLMの回答自動評価を試してみました。

LLMアプリケーションの開発では、いかにそのアプリの性能を評価/継続観測するかが重要だと日々感じています。 そういった仕組みを作る一つの選択肢として、LangCheckは有用と感じました。

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

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

GitHub - citadel-ai/langcheck: Simple, Pythonic building blocks to evaluate LLM applications.

LangCheck Documentation — LangCheck

API Reference — LangCheck

https://platform.openai.com/docs/guides/prompt-engineering/tactic-evaluate-model-outputs-with-reference-to-gold-standard-answers

OpenAI APIエラー: The model `gpt-4-vision-preview` does not exist or you do not have access to it.

OpenAI APIでGPT-4Vを使うにあたって、下記のエラーが出た時の対処法の備忘録です。

openai.NotFoundError: Error code: 404 - 
{'error': {'message': 'The model `gpt-4-vision-preview` does not exist or you do not have access to it. Learn more: https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}

はじめに

PythonでOpenAI APIのGPT-4Vを叩こうとしたら、openai.NotFoundErrorエラーが出ました。

対処法をメモします。

# 利用ライブラリバージョン
openai==1.3.7

OpenAI APIエラー対処: The model gpt-4-vision-preview does not exist or you do not have access to it.

状況

下記のコードで、ごく簡単にGPT-4VをPythonから呼び出してみました。

import os
from openai import OpenAI
import base64
import mimetypes


def image_to_base64(image_path: str) -> str:
    mime_type, _ = mimetypes.guess_type(image_path)

    if not mime_type or not mime_type.startswith('image'):
        raise ValueError("The file type is not recognized as an image")

    with open(image_path, 'rb') as image_file:
        encoded_string = base64.b64encode(image_file.read()).decode('utf-8')

    return f"data:{mime_type};base64,{encoded_string}"


def main() -> None:
    client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

    base64_string = image_to_base64("data/test.jpg")

    response = client.chat.completions.create(
        model="gpt-4-vision-preview",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "Describe the attached image"},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": base64_string,
                            "detail": "low"
                        }
                    },
                ],
            }
        ],
        max_tokens=300,
    )

    print(response.choices[0].message.content)


if __name__ == "__main__":
    main()


結果は、下記のエラーです。

openai.NotFoundError: Error code: 404 - 
{
  'error': {
    'message': 'The model `gpt-4-vision-preview` does not exist or you do not have access to it. Learn more: https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4.',
    'type': 'invalid_request_error',
    'param': None,
    'code': 'model_not_found'
  }
}
# (上記はインデントを整形して表示したもの)

現時点(2023/12)でモデルgpt-4-vision-previewは利用可能なはずので、これはおかしいぞ、という訳です。

原因

GPT-4系列のAPIを利用するには、$1以上の課金支払い実績がある必要があります。

エラーに表示されるリンクにも記載されています。

If you're a Pay-As-You-Go customer and you've made a successful payment of $1 or more, you'll be able to access the GPT-4 API

フォーラムなどでも同様の現象が議題に上がっており、同じく支払い履歴の有無が焦点になっています。

対処法

$1以上の課金支払いを行います。

これには2つ方法があるでしょう。

  1. Pay-As-You-Goプランで$1以上の課金支払いが発生するまで待つ
  2. Credit購入する

私はすぐにAPIを使いたかったので、後者を選びました。

Creditとは前払い式のAPI利用枠で、執筆時点(2023/12)では最低$5から購入できます。

OpenAIのプラットフォームページから、

  1. "Settings"タブ
  2. "Billing"タブ
  3. "Buy credits"ボタン

から購入できます。

Creditを購入する

私の場合、Credit購入から30分ほど経過した後、無事エラーは解消しました。

おわりに

以上、OpenAI APIでGPT-4Vを使う際のエラー対処でした。

参考になれば幸いです。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work

参考

How can I access GPT-4? | OpenAI Help Center

How to get access to gpt-4-vision-preview? - API - OpenAI Developer Forum

OpenAI API error: The model `gpt-4-vision-preview` does not exist or you do not have access to it. · Issue #26 · OthersideAI/self-operating-computer · GitHub

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を組み合わせて簡単な実験を行います。

次回はこちら:

www.bioerrorlog.work

はじめに

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 Developer 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_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 Platform

Search results for 'API response too slow' - OpenAI Developer 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