SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|12|
2025|01|02|03|04|05|06|07|

2025-07-13(Sun) Embeddings APIって、そういうことだったんですかい

  どうもAIを素直に好きになれない自分がいる。スゲェな、と思う場面がある反面、同時に悔しさも感じてしまい、どうしても敵対的に見てしまうのだ。

  なので「AIコーディングツールは生産性を下げる」などという記事を見つけたときは、思わず鼻を鳴らしてしまった。そらそうだろうよ。概ね合ってるコードが一番始末が悪いんだから。なんで人間様がデバッグ係なんだよ。つうても、数行のサンプルコードを書いてもらう限りは、非常に有用。結局は、使い方次第だ。

  つうわけで、$5課金してからというもの、自作した「リナ」をちょいちょい便利に使っているのだが、ハテ、いくら分ぐらい使ったものやら……と、確認するとまだ$2以上残っていた。自分の使い方だと、無限に使える感じだな。

  と、なにげにパネルを見ていたら「Vector stores」という表示を見つけた。ん? ベクトルストア? そんなんあるなら、相手側の資源でRAGを実現できるんじゃないの? そう思って、ChatGPT自身に「どうなの?」と訊いたのだがなんだかハッキリしない。「APIからChatGPTの Vector Store機能を使いたい場合には、OpenAIの Embeddings API + 自前でのベクトルストア管理 が基本になります」だと。なんか言ってることが矛盾してね?

  その延長で、あれこれ調べているうちに「Embeddings API」の意味にようやく気づいた。単語や文章を与えると、それを1536次元のベクトル値に変換して返してくれるAPIなのだということに。Embeddings APIって、ひいては、RAGって、そういうことだったんですかい。

  ここしばらく、RAGを自作してみたいものの、自らGPUを用意するつもりはないので、どうしたものかと思っていたのだが、計算負荷の高いベクトル化を相手側の資源で行えるなら、それでイケるんじゃないか。しかも、変換コストは非常に安いとくる。

  問題はベクトルDBだが……PostgreSQLにベクトル演算を拡張するpgvectorなんてものがあるらしい。なんとも都合のいいことに、コンテナイメージもあり、既存のdocker-compose.ymlの「docker.io/postgres」を「docker.io/pgvector/pgvector:0.8.0-pg17」に書き換えただけで動いてしまった。PostgreSQLなら、割と使い慣れているから助かる。

  役者が揃ったところで、以前に作ったlibngs.rbに、Embeddings APIに対応するコードを追加する。既存のChat Completions APIとの共用部分が多い反面、共用部分を共用化するためには、抽象クラスに取り出す必要があったりして、余計に手間がかかった気がするが、それがまた楽しくもあり。

  結局、以下のようなコードで、ベクトル値のストア、近いベクトル値を持つレコードの検索ができるようになった。要するに、任意の文章を与え、登録してある文章の中から、最も近い意味合いの文章を引き出すことができるようになった、ということだ。

  when('initdb')
    @pgc = setup_pg_conn
    [   "CREATE EXTENSION IF NOT EXISTS vector;",
        "CREATE TABLE embeddings (id SERIAL PRIMARY KEY, text TEXT, embedding VECTOR(1536));",
    ].each {|sql|
        p pgr = @pgc.exec(sql)
    }
 
  when('insertdb')
    response = ngs[0].embedding(text = $stdin.read.chomp)
    @pgc = setup_pg_conn
    [   "INSERT INTO embeddings (text, embedding) VALUES ('%s', '%s');" % [text, response.embedding.inspect]
    ].each {|sql|
        p pgr = @pgc.exec(sql)
    }
 
  when('selectdb')
    response = ngs[0].embedding(text = $stdin.read.chomp)
    @pgc = setup_pg_conn
    [   "SELECT id, text, embedding <=> '%s' AS distance FROM embeddings ORDER BY distance LIMIT 5;" % [response.embedding.inspect]
    ].each {|sql|
        p pgr = @pgc.exec(sql)
        pgr.each {|r|
            p r
        }
    }

  こんな風に使う。まずは、pgvectorコンテナを立ち上げ、初期化する。

$ ./embed initdb
"CREATE EXTENSION IF NOT EXISTS vector;"
#<PG::Result:0x00007f687772dc60 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=0>
"CREATE TABLE embeddings (id SERIAL PRIMARY KEY, text TEXT, embedding VECTOR(1536));"
#<PG::Result:0x00007f687772d878 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=0>

  AIに作ってもらった「架空のニュースヘッドライン」30件をDBにストアする。

$ echo '火星の地下湖に生命の痕跡 探査機が微生物反応検出' | ./embed insertdb
"INSERT INTO embeddings (text, embedding) VALUES ('火星の地下湖に生命の痕跡 探査機が微生物反応検出', '[0.008627256, 0.04008983, 0.022950277, 0.053258..."
#<PG::Result:0x00007fda5cab54a8 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=1>
$ echo '日本政府、空飛ぶ自動車の市販許可を正式発表' | ./embed insertdb
"INSERT INTO embeddings (text, embedding) VALUES ('日本政府、空飛ぶ自動車の市販許可を正式発表', '[0.03220249, 0.011132977, -0.020613244, 0.00199744..."
#<PG::Result:0x00007f40460b18b8 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=1>
$ echo 'AIが初の小説家デビュー 芥川賞ノミネートへ' | ./embed insertdb
"INSERT INTO embeddings (text, embedding) VALUES ('AIが初の小説家デビュー 芥川賞ノミネートへ', '[0.030073598, -0.02538003, -0.042647723, 6.115188..."
#<PG::Result:0x00007fc085637188 status=PGRES_COMMAND_OK ntuples=0 nfields=0 cmd_tuples=1>
  :

  任意の文章を与え、近い意味合いを持つニュースを検索させる。

$ echo 'ドラえもんが実用化' | ./embed selectdb
"SELECT id, text, embedding <=> '[0.023070822, 0.038914356, -0.037266437, 0.0132892635, -0.00953437, -0.023659363, -0.0114589,..."
#<PG::Result:0x00007f8aca269d30 status=PGRES_TUPLES_OK ntuples=5 nfields=3 cmd_tuples=5>
{"id"=>"8", "text"=>"ネコ語翻訳機、精度90%超えで商品化スタート", "distance"=>"0.6139296889305115"}
{"id"=>"28", "text"=>"四次元ポケットの実用化に成功 国際特許を申請", "distance"=>"0.6229722573144583"}
{"id"=>"16", "text"=>"消える道路標識、実は新型AR技術の実験だった", "distance"=>"0.6906551009026104"}
{"id"=>"14", "text"=>"老舗和菓子屋がメタバースに進出 VRで試食体験", "distance"=>"0.7033616127587321"}
{"id"=>"20", "text"=>"鏡の中に映らない猫、科学的解明に一歩前進", "distance"=>"0.7042705416679382"}
 
$ echo '未曾有の大災害' | ./embed selectdb
"SELECT id, text, embedding <=> '[0.023535717, 0.021849712, 0.025312858, 0.046182863, -0.008014219, -0.018546054, 0.0101217255..."
#<PG::Result:0x00007ff3dcf960a0 status=PGRES_TUPLES_OK ntuples=5 nfields=3 cmd_tuples=5>
{"id"=>"6", "text"=>"東京湾に巨大未確認生物出現 各地で目撃情報相次ぐ", "distance"=>"0.6341621497772836"}
{"id"=>"12", "text"=>"富士山に新たな火口 専門家「噴火の兆候なし」", "distance"=>"0.6510091035956672"}
{"id"=>"11", "text"=>"無人島が突如移動 GPS地図が大混乱に", "distance"=>"0.6530257281034317"}
{"id"=>"24", "text"=>"京都の神社で時空のゆがみ報告 観光客パニック", "distance"=>"0.6767845241088242"}
{"id"=>"30", "text"=>"地球に最接近する月、巨大化の理由は未解明", "distance"=>"0.7187142539790712"}
 
$ echo '宇宙人とコンタクト' | ./embed selectdb
"SELECT id, text, embedding <=> '[0.0039180126, -0.007993211, -0.007061737, 0.025079938, -0.023228632, -0.027175754, -0.036723..."
#<PG::Result:0x00007f029cbc4800 status=PGRES_TUPLES_OK ntuples=5 nfields=3 cmd_tuples=5>
{"id"=>"15", "text"=>"宇宙からのラジオ信号にモールス信号の痕跡発見", "distance"=>"0.594331335345663"}
{"id"=>"24", "text"=>"京都の神社で時空のゆがみ報告 観光客パニック", "distance"=>"0.6794118690140118"}
{"id"=>"21", "text"=>"国連、地球外文明との外交専門部署を新設", "distance"=>"0.6888121498841258"}
{"id"=>"30", "text"=>"地球に最接近する月、巨大化の理由は未解明", "distance"=>"0.6926628282860291"}
{"id"=>"26", "text"=>"東京地下に謎の空洞都市 調査隊が内部撮影成功", "distance"=>"0.7025683167658209"}

  完璧とは言えないものの、決して全文検索では引き出せない結果が得られている。よろしいのではないでしょうか!

  例によって、各物件をhttp://itline.jp/git/ngs, http://itline.jp/git/pgvector_dockerhubに置いておく。


2025-07-29(Tue) エンジン音を理詰めする

  次はサウンド関係に手を付けるかなぁ、とかいいながら、なぜか加速の物理挙動を組み込み始めたら止まらなくなってしまい、ほぼ完璧に組み込みを完了してしまったのだが、それはまた後日に扱うことにして、サウンド関係について少し進めることにする。

  F1ゲームでサウンドといえば、重要なのは走行音だ。特に実装したいのは「ドゥ、ドゥ、ドゥ、ドゥーン」という連続シフトダウンの操作の効果音だが、まずは通常のエンジン音を実装するのが先だ。

  エンジン音とは何か。雑に言えば「ブゥーン」という尻上がりのノイズ、なのだが、加速の物理挙動の組み込みがあまりに興味深かったので、効果音も適当に付けようという気にならなくなってきた。ここは理詰めで、可能な限りリアルな効果音を付けてみたい。

  エンジンとは「吸気、圧縮、燃焼、排気」の繰り返しを回転運動に変換する機構で、支配的な音は「燃焼」時の破裂音だ。なので、音色は「鋸歯状波」に近くなる。ノコギリのように素早く立ち上がって、スッと消える。そして、アクセルを踏んでエンジンの回転が高まると、破裂音の発生間隔が狭まる。つまり、周波数が高くなり、低音から高音に遷移していく。それが「ブゥーン」と「尻上がり」な音になる理由だ。

  基本となるパラメータはエンジンの回転数だ。rpm。Round Per Minute。1分間の回転数。3000rpmなら、50回転/秒。では、50Hzかというと、それは違う。4サイクルエンジンは2回転に1回しか燃焼しないからだ。では、25Hzかというと、それも違う。4気筒エンジンなら、基本的に各気筒が順次に燃焼するので、その4倍。100Hzが正解である。

  そこまでわかったところで音を出してみる。効果音の生成には、以前に作ったり、改良したりした、自製のツールを使う。記述は以下。

@length = 2
 
car1 = it = {}
it[:device] = 'generate'
it[:length] = @length
it[:type] = 'sawtooth'
it[:freq] = [100]
 
@connection = [ car1 ]

  こんな音になった。まぁ、ロードスターを含む、小型車のエンジン音、といわれれば、まぁ、そんな風に聴こえなくもない。波形はこう。

  画像の説明

  んが、これ「ノコギリのように素早く立ち上がって、スッと消える」の逆になっている。欲しいのは、こうではないか?

  画像の説明

  つうわけで、ツールに「sawtooth_r」を追加で実装してみる。

#---------------------------------------------------------------
#
#   鋸歯状波
#
wgens['sawtooth'] = Proc.new {|dev, p|
    p += 0.5
    (p - p.to_i) * 2 - 1                        # output: -1.0 - 1.0
}
 
#---------------------------------------------------------------
#
#   鋸歯状波(逆、破裂音?)
#
wgens['sawtooth_r'] = Proc.new {|dev, p|        # p: 波形の周期位置、最初の波形の中点なら 0.5、10 周期目の終点なら 9.9
    1 - (p - p.to_i) * 2
}

  こんな音になった……んが、聴感上では違いがわからないな。鼓膜が感知するのは経時的な圧力変化だから、そういうもんなのかもな。と、ここで、加速させてみる。2秒で3000rpmから6000rpmへと。

@length = 2
 
car1 = it = {}
it[:device] = 'generate'
it[:length] = @length
it[:type] = 'sawtooth_r'
it[:freq] = [100, 200]
 
@connection = [ car1 ]

  こんな音になった。チープだが悪くない。つうか実際、黎明期のレースゲームの走行音って、まさにこんな音だよね。

  とりあえず、ロードスターはコレでいいとして、次はF1マシンだ。F1マシンは12000回転くらいまで回して走る。そんなら倍の400Hzかといえば、これまた違う。6気筒エンジンなので、その1.5倍。600Hzになる……はずなのだが、オンボード映像の音声を解析してみると300Hzが支配的なんだよなぁ。どうやら、バンク角90度のV6という特性が影響しているらしい。ついでなので、10500〜12000回転でシフトアップする音を作ってみる。

@length = 1
 
car1 = it = {}
it[:device] = 'generate'
it[:length] = @length
it[:type] = 'sawtooth_r'
it[:freq] = [262, 300]
 
@connection = [ car1 ]
$ sox 6kitou_f1.wav 6kitou_f1.wav 6kitou_f1.wav 6kitou_f1_shift3.wav

  こんな音になった。3回繰り返しているだけだが、シフトアップしているようにも聴こえなくもない。

  これだけでも十分に高音だが、1990年代のフェラーリは17000回転も回して馬力を稼いでいた。しかも12気筒。フェラーリサウンドってやつだ。こっちも、オンボード映像の音声を解析してみると理論値の半分で850Hz。13000〜17000回転でシフトアップする音を作ってみる。

it[:freq] = [650, 850]

  こんな音になった。カン高いフェラーリサウンドの片鱗を感じられなくもないが、こんなんじゃあない。主な理由は倍音成分がないからだ。つづく。