海に並ぶ5種類の島と、表情を変えた3Dマスコット

島の選択マップと、倒せなかったボス ── AIと作るジャンプゲームの開発記録

前回 は、「次は島の選択マップです」と締めました。今回は予告どおり、島を並べます。 この日やったのは、島の選択マップ、輸送機の作り直し、表情の追加、そしてゲーム側のボス調整と配置ツールです。3DCGとゲーム実装はリポジトリを分けてあり、別々のセッションで並行して進めています。片方でモデルを焼いている間に、もう片方でゲームのバグを潰す。AIに指示して作らせ、出てきたものを確かめて突き返す、その繰り返しを一日ぶんまとめました。 作業の中身は違っても、詰まった場所は同じでした。3D側は拡大するまで、ゲーム側は遊ぶまで、できていないことに気づけない。 完成したという報告と、実際にできていることの差を、どう見つけたかの記録です。 まずは、島を置くだけで使えるようにする 島の選択画面に必要なのは、一枚の完成した地形ではありません。「島は1つ1つ分離して、使い回せるように」という作りです。そこで、草の饅頭島、火山島、環礁、岩礁、森の島の5種類を、それぞれ独立したビルダーに分けました。ビルダーというのは、指定すると島を一式組み立てるコードです。 もうひとつ決めたのが、すべての島で水際をローカル座標の z=0 に揃えることです。海面も z=0 にしておけば、別の場面へ持っていっても高さを調整せず、置くだけで水際が合います。前回消した島は、その場限りの背景でした。今回は、どの海にも持っていける部品として作り直したわけです。 5種類を縦画面へ並べると、ようやくステージを選ぶ地図らしくなりました。 ここまでは予告どおりです。問題は、この画面と同じゲームで使う輸送機を、真横向きの画像へ焼き直したところから始まります。 真横から見た輸送機は、飛行船だった 前回の到着ムービーでは、輸送機を4発プロペラ、高翼、T字尾翼の形まで作り直しました。ところが真横にすると、映画のカメラでは隠れていた粗が全部見えます。 機首の上には意味不明な突起があり、胴体は飛行船のように丸い。輸送機らしい跳ね上がった後部がなく、主翼はただの板です。「輸送機ってこんな形でしたっけ。米軍のを想像していた」と駄目出しし、C-130系を目標に機体を全面的に作り直させました。数秒の映像の中で輸送機に見えればよかったものを、単体で見せられる形にするということです。 ところが形を直して拡大すると、もっと妙なものが見えました。窓の影です。 窓が、機体から最大0.5も浮いていた 胴体は楕円体なので、中央は太く、機首と尾へ行くほど細くなります。それなのに窓は、胴体の太さに関係なく、すべて横方向の座標を x=±2.02 に固定していました。中央付近なら表面に見えても、細くなった端では最大0.5も空中へ離れます。真横から見ると、浮いた窓が胴体へくっきり影を落としていました。 直し方は、窓ごとにその位置の楕円体の表面を解き、胴体の太さに合わせて貼り付けることです。全窓に同じ座標を与えるのをやめると、端の窓まで機体へ降りてきました。 前回の動画では、この窓を不自然だと思っていませんでした。流れる映像なら輸送機に見える。止めて、拡大して、初めて窓が別の物体として宙に並んでいると分かります。 半目が、分類の隙間から黙って消えた 同じことが顔でも起きました。アプリ側に増えた6表情を実測し、3Dへ載せる既存の仕組みへ流したところ、眠い顔の片目がありません。 眠い顔の左右の半目は、形が少し違います。塗られている割合を示す充填率は、一方が0.66、もう一方が0.60でした。この差が分類のしきい値をまたぎ、片方だけが「丸い目」と判定されます。もう片方は、ほかのどの分類にも入りませんでした。エラーにも警告にもならず、黙って捨てられていたのです。同じ穴で「聴く」顔の片目も消えていました。 分類を増やすだけでなく、どこにも入らなかったものを無言で捨てない作りにして、両目を戻しました。あわせて、顔から浮いて影を落としていた眉と、飛び出していた目も表面へ埋め、内側まで黒く塗り潰されていた口の縁取りも、穴のあるリングとして張り直しています。窓と同じで、正面からは接して見えるのに、角度を変えると浮いている。影は、AIが置いた部品と本体の隙間を正直に描いていました。 ここまでが「拡大して見つけた嘘」です。ここからは、拡大しても見つからないほうの話になります。 「必ず倒せます」と報告したボスは、絶対に倒せなかった ゲーム側では、門を守る巨人を作っています。倒し方は、巨人の腹の下に潜り、下から突き上げて3発。腹の真下はフォークの歯が届かない安全地帯で、そこを取り合うのが駆け引きです。 このボスが倒せないと指摘すると、AIは配置の条件を直し、「腹の下に足場がある配置を保証しました」と報告してきました。テストも通っています。ところが自分で遊ぶと、やはり倒せません。 抜けていた条件は、その足場に立ったとき、頭が腹にめり込まないことでした。めり込む高さだと、体の押し返し判定が毎フレーム働いて横へ弾かれ、腹の下に居座れません。突き上げる姿勢を作れないので、何度挑んでも0発です。プレイヤーからは「絶対無理」に見えます。 「腹の下に板がある」までは合っていました。足りなかったのは、そこに人が立てるかです。画面を拡大しても、この嘘は映りません。立って、跳んで、初めて分かります。 私と同じ操作で、AIに遊ばせる そこで、実際に遊ぶ検証ボットを書かせました。使える操作は左右キーだけ。ジャンプは着地で自動的に跳ねる仕様なので、私が指で遊ぶのと完全に同じ条件です。座標を直接書き換える瞬間移動は禁止しました。それを許した瞬間、証明になりません。 最初の実行は、和音を集める前に落下死しました。次は、ボスの真下へ潜れず、68秒間ただ跳ね続けて終わりました。倒せないのではなく、そもそも潜れないという事実が、ここではっきりします。 配置の条件を直したあとの走りが、これです。和音を3つ集め、鍵を取り、突き上げ3発で撃破し、門を開けるまで約12秒でした。 このボットは回帰テストとしてリポジトリに常駐させました。攻略できなければ失敗として落ちます。「倒せる配置になっているか」を、私が毎回自分で確かめなくてよくなりました。 倒せるようにしたら、今度は怖くなくなった ところが直ったボスを遊んでみると、今度は拍子抜けしました。腹の下に潜れば絶対に刺されません。何も怖くないのです。 原因は、ボスを門をまたぐ高さに置いたことでした。フォークの高さがプレイヤーの到達上限より上へ出てしまい、安全地帯が広すぎたのです。「倒せない」を「怖くない」に付け替えただけでした。 そこで闘技場を中段へ降ろし、フォークの薙ぎがジャンプの弧と重なるようにしました。さらに突き上げの反動で横にも弾かれるようにして、一発ごとに潜り直す必要を作ります。ボットで測ると、攻略は成立したまま、歯とのニアミスが出るようになりました。倒せて、かつ危ない状態です。 配置は、数値では決まらなかった ここまでの修正はすべて、AIが座標を数値で置いては私が突き返す、の繰り返しでした。何度指示しても適格な場所に置かれず、この件だけで1〜2時間が溶けています。ここで方針を変えました。自分で置けるようにするしかありません。 PCの画面でステージ全体を俯瞰し、巨人をドラッグして置くエディタを作りました。置いた瞬間に、その板に立てるか、跳べば腹に届くか、門より下か、フォークが跳躍弧に重なるか(つまり怖いか)を判定し、OKとNGで出します。倒せない配置も置けます。禁止せず警告に留めました。道具の自由を奪えば、結局また数値の指示に戻るからです。 盤面ごと、左に寄っていた エディタで触ってみると、別の異常に気づきました。右にはいくらでも置けるのに、左には置けない。門も左に寄っている。 調べると、原因はレベルの側にありました。鍵盤は15レーンあり、横位置がそのまま音の高さです。ところがステージ1の和音はドミソで、使うのは15本中の0番、2番、4番。盤面ごと鍵盤の左3分の1に固まっていたのです。門は最上段の板の真上に立つ仕様だったので、門も一緒に左端へ寄っていました。右側の10本は、その島では一度も使わない死に幅です。 そこで、その島が実際に使うレーンの中心が画面中央へ来るよう鍵盤ごとずらし、門の中心は和音のまん中の音(ドミソなら「ミ」)の真上に変えました。左右に同じだけ余白ができ、巨人の入場も回り込みも、どちらの側でも成立します。 確認する道具も、確認しなければ壊れる 3D側では、11表情を横や斜めから見る確認動画を作りました。ところが視点を増やすと、カメラの狙いが外れて背景一色になり、次はキャラの内部へ入り、その次は床の下へ潜ります。表情を確かめる動画そのものが、表情を映していませんでした。目視で済ませず、全フレームにキャラの画素が十分あるかを数値で検査する手順を足しています。 ゲーム側の検証ボットも同じでした。最初のボットは操縦が下手で、板を外して落ちるたびに「ゲームが悪い」ように見えます。ボットの操縦ミスと、ゲームの不具合を切り分けられるようになるまで、何度も書き直しました。成果物を確かめる道具も成果物です。信用する前に、道具のほうを疑う必要があります。 AIが直したと報告した箇所ほど、私が触ると壊れているのも同じ構図でした。「向きを変えられるようにしました」と言うので探すと、ボタンのラベルは「入場: 自動」。これでは何のボタンか読めません。予告マークが出ないと指摘して調べさせると、出現の判定は新しい設定を見るよう直したのに、予告を消す判定だけ古いままで、毎フレーム消されていました。 GPUで速くなった話の訂正 前回の記事で、レンダリングをMacのGPUへ切り替えて速くなったと書きました。今回、新しいスクリプトがCPUで動いていたので調べ直すと、設定を各スクリプトへ手書きしていたため、追加のたびに漏れる作りでした。さらに、Metalを指定した既存スクリプトも、GPUとCPUの両方を有効にしていました。 実測すると、混在は57.9秒で終わる代わりに、CPU時間の合計は400秒を超えます。GPUだけなら87.3秒、CPU時間は68.8秒でした。待ち時間だけなら混ぜたほうが速い。しかしその間、CPUを食い潰します。「GPUにして速くなった」は、半分は本当で、半分は見ていませんでした。設定欄の METAL を確認したところで満足し、その下でCPUも働いているところを拡大していなかったわけです。 ...

公開: 2026年7月26日 · Toshihiko Arai
電話のキーパッドと、低群4×高群4のDTMF周波数マトリクス。ボタンを押すと2つの周波数の音が同時に鳴り、受信側は8つの周波数のエネルギーを測ってボタンを復元する図

「ピポパ」を解読する ── DTMFと、音でデータを送った時代

スマートフォンの電話アプリで番号を押すと、いまでも「ピ、ポ、パ」と鳴ります。あれは押した感触を出すための効果音ではありません。押したボタンそのものを音に変換した、れっきとしたデータ通信です。だからあの音を録音すれば、あとから何番を押したのか割り出せてしまう。テレビ番組で電話番号を入力する場面の音が伏せられることがあるのは、このためです。 この方式は DTMF(Dual-Tone Multi-Frequency、2つの音を同時に使う多周波信号)と呼ばれ、1963年にアメリカのベル・システムがプッシュホンとともに実用化しました。今回はこの「ピポパ」を材料に、音でデータを送るとはどういうことか、そして受け取った音をどう解読するのかを見ていきます。この連載では音の正体は数の列だ というところから始めて、楽器の音をプログラムで作る話を続けてきましたが、今回は楽器ではなく、音を「文字」として使う回です。 ボタンは音の「座標」でできている プッシュホンの前の世代、ダイヤルを回す黒電話は、まったく別の方法で番号を伝えていました。ダイヤルが戻るあいだに、回した数字の回数だけ回線を切ったりつないだりする。カチカチという断続そのものが合図で、これはパルス式と呼ばれます。機械的で確実ですが、遅く、そして交換機に向けた「番号選び」専用の合図なので、電話がつながった後の相手には何も伝えられません。 黒電話に触ったことがない世代のために補足すると、ダイヤルは指を穴に差し込んで、金具(指止め)まで回して離す道具です。回すときではなく、バネで戻るあいだに回線が断続します。「1」なら1回、「9」なら9回、「0」は10回。数字が大きいほど戻りが長く、ダイヤルするだけで時間がかかりました。市外局番から順に回していると、それだけで十数秒かかることも珍しくありません。 DTMFの発想は逆で、合図をただの「音」にしてしまいました。音でありさえすれば、電話線でも、空気中でも、録音テープの上でも通ります。つながった後の回線にも当然乗るので、通話の途中でボタン操作を送れる。音声ガイダンスの「〜の方は1を押してください」が今日でも成立しているのは、この性質のおかげです。 では、どんな音にするか。ここがDTMFの核心です。 用意するのは8つの音だけです。低めの4音(697・770・852・941ヘルツ)を「低群」、高めの4音(1209・1336・1477・1633ヘルツ)を「高群」とし、低群から1つ、高群から1つを選んで同時に鳴らす。低群がキーパッドの行を、高群が列を決めるので、2音の組はそのままボタンの座標になります。たとえば「5」は770ヘルツと1336ヘルツの同時再生。この組み合わせは「5」だけのもので、ほかのどのボタンとも重なりません。4×4で16通り、電話機に載っている12個のキーに、業務用などで使う拡張のA〜Dを足した16記号がこれで表せます。 なぜわざわざ2音同時なのでしょうか。1音だけでも「697ヘルツが鳴ったら1」と決めれば済みそうなものです。ところが電話回線には人の声が流れています。声にも口笛にもテレビの音にも、697ヘルツ付近の成分くらいいくらでも含まれるので、1音では「たまたま鳴っていた音」と区別がつきません。一方、離れた2つの決められた周波数が、同時に、それだけで鳴るという状況は、偶然にはまず起きません。しかも8つの周波数は、どれも他の音の整数倍(倍音の関係)にならないよう慎重に選ばれています。回線の歪みで倍音が生まれても、別の目標周波数に化けて誤検出を起こさないための用心です。電話回線が通せる帯域はおおむね300〜3400ヘルツと狭いのですが、8音はきちんとその中に収まっています。 この8つの数字は、ITU-T勧告Q.23として国際的に決められた値です。北米で開発されたTouch-Toneがそのまま国際規格になったので、日本のプッシュホンも、海外の電話機も、同じ周波数を使います。日本の携帯から海外の音声ガイダンスを操作できるのは、このおかげです。 ただし、規格どおりの2音をそのまま合成すると、記憶の中のプッシュ音とは少し違って聞こえます。本物には、決まった周波数のほかに2つの作法があるからです。ひとつはツイスト。電話線は高い音ほど減衰するので、送信側で高群を低群より2〜3デシベル強く鳴らしておきます。もうひとつは帯域。実際の音は300〜3400ヘルツしか通らない回線と、小さな受話器のスピーカーを経由して耳に届きます。純粋な正弦波をそのまま鳴らすと、澄みすぎて、あの少し詰まった感じになりません。下のデモは、この2つを模した状態で鳴らしています。 受ける側は、8ヶ所を聴診するだけ 送る側が「2音を足すだけ」なら、受ける側の仕事も驚くほど少なくて済みます。音に含まれるすべての周波数を調べる必要はありません。知りたいのは、決められた8つの周波数のそれぞれが「鳴っているか、いないか」だけです。 受信側は、届いた波形に対して8つの周波数それぞれのエネルギーを測ります。イメージとしては、8本の聴診器をそれぞれ決まった高さの音だけに当てて、どれが強く響いているかを聞き比べる感じです。低群の4本のうち1本だけ、高群の4本のうち1本だけが突出していれば、行と列が確定してボタンが分かる。どちらかの群で1本も立たない、あるいは2本立ってしまったら、それはDTMFではない(ただの声や雑音だ)として捨てる。判定が白黒はっきりしているのが、この設計の気持ちよさです。 この「特定の周波数だけを効率よく聴診する」計算には、ゲルツェル法(Goertzel algorithm、1958年にGerald Goertzelが発表)という定番のアルゴリズムがあります。周波数分析というとFFTのような大掛かりな変換を思い浮かべますが、知りたい周波数が8つと決まっているなら、その8ヶ所だけを漸化式ひとつで安く測れる。電話交換機のように何百回線も同時に監視する装置では、この安さが効きました。 Tip 漸化式という言葉が硬いのですが、要するに「前の答えを使って次の答えを出す式」のことです。プログラムで書けば、1つ前と2つ前の値を変数に持っておいて、ループを1周するたびに更新していくだけ。s[n] = 係数 * s[n-1] - s[n-2] + 入力 のような形になります。再帰と同じ考え方で、実装は素直なループです。 音の波形から必要な情報だけを抜き出すという意味では、マイクの音から音程を当てるピッチ検出 も同じ仲間です。あちらは「知らない高さを探す」問題で、こちらは「決まった高さがあるか確かめる」問題。探し物の種類が違うだけで、どちらも音を数の列として受け取り、計算で答えを取り出します。 触ってみる 言葉より押してみるのが早いので、デモを用意しました。キーパッドを押すと、そのキーの2音が実際に鳴ります。そして右上の解読欄に注目してください。ここに出る文字は、ボタンの信号を横流ししたものではなく、いま鳴った音の波形だけを毎フレーム受け取り、8つの周波数のエネルギーを測って復元したものです。エンコードとデコードが、スピーカーとプログラムのあいだで本当に往復しています。 うまく表示されないときは、デモを別タブで開く 。 「じゃま音を混ぜる」で試せること。 2音の組にした理由は、声や雑音と区別するためでした。それを自分で確かめられるように、回線に人の声や雑音が乗った状態を再現するボタンを付けました。押すと、8つのメーターは絶えず揺れ、スペクトラムにも山が立ちます。それでもキーは1つも確定しません。そのまま数字を押せば、じゃま音の上からでもきちんと解読されます。 実は、この機能を足したときに誤検出が出ました。声の代わりに純音を揺らして流していたのですが、たまたま低群と高群の1本ずつが強く出た瞬間に、デコーダが「キーが押された」と判定してしまったのです。これは実際の電話でも「トークオフ」と呼ばれる有名な誤動作で、通話中の声がボタン操作に化ける現象です。対策も実機と同じで、2本の山が8本ぶんのエネルギーの過半を占めていることを条件に加えました。声や雑音はエネルギーが広く散らばるので、この一行で弾けます。 真ん中の「8つの聴診器」で、押すたびに青(低群)と紫(高群)が1本ずつ立ち上がるのを確かめてください。その下のスペクトラム、つまりどの高さの音がどれだけ鳴っているかのグラフでは、どのキーを押しても山がきっかり2本だけ立ちます。音を周波数の軸で眺める見方は、周波数特性の記事 でも扱いました。キー列は「1234」でも「0000」でも、意味のない適当な並びで十分です。 いちばん下のおまけは、後で触れるカセットテープ方式の再現です。短い文字列を0/1のビット列にして、低い音と高い音の「ピーガガガ」で送り、受信側がそれを聴き取って文字に戻します。 どう実現するか 送る側は本当に足し算だけです。キーに対応する低群・高群の周波数でサイン波を2本作り、足して鳴らします。 // 「5」の音: 770 Hz + 1336 Hz function dtmfSample(n, fs){ const t = n / fs; return 0.5 * (Math.sin(2 * Math.PI * 770 * t) + Math.sin(2 * Math.PI * 1336 * t)); } 受ける側の「聴診」も、正体は素朴な掛け算と足し算です。調べたい周波数 $f$ のコサインとサインを波形に掛けて足し込むと、その周波数の成分だけが積み上がり、無関係な成分は打ち消し合ってゼロに近づきます。積み上がった量の2乗和が、その周波数のエネルギーです。 ...

公開: 2026年7月26日 · Toshihiko Arai
連載『手で動かすAI』第6回のカバー。3つの生スコアの棒が、指数で持ち上げられ、合計で割られて、足すと100%になる確率の棒に変わるイラスト

自信の大きさまで答えさせる ── 出力を確率に整えるソフトマックス

前回、手で描いた数字を自作のネットワークに読ませました。出口には10本の棒が並び、いちばん長い棒の数字が答えでした。ただ、あの棒の高さは、10個の出力ニューロンがそれぞれ勝手に出した反応の強さで、足しても100%にはなりません。だから「答えは3」とは言えても、「どのくらいの自信で3なのか」「2番手の8とどれだけ差があるのか」は、あの棒のままでは読み取れませんでした。 今回は、その生の出力を確率のかたちに整える、小さな仕上げを一つ入れます。名前はソフトマックスといいます。仕組みはあっけないほど単純で、けれどこの先ずっと出てくる部品です。 生の反応は、そのままでは確率にできない やりたいことは、10個(今回は話を追いやすいよう、3・5・8の3個で考えます)の生スコアを、足すと100%になる確率に変えることです。素朴に考えると、全部を合計で割ればよさそうに思えます。3個のスコアを足して、各自をその合計で割れば、たしかに合計は1になります。 ところが、これはうまくいきません。ニューロンの出す生スコアには、マイナスの値がふつうに混じるからです。マイナスのスコアを合計で割ると、マイナスの確率という意味をなさない数字が出てしまう。合計そのものがゼロに近くなれば、割り算も破綻します。確率は0%から100%のあいだの正の数でなければならないのに、生スコアはその約束を守ってくれません。 そこで一手はさむ。合計で割る前に、すべてのスコアを指数関数に通します。指数関数というのは、$e^x$ ── ネイピア数$e$(約2.718)を、スコアの数だけ掛け合わせる関数です。難しく身構える必要はなくて、ここで効く性質はたった二つだけです。ひとつ、どんな入力(負の数でも)を入れても、出てくる値は必ずプラスになる。もうひとつ、入力が大きいほど、出力は急激に大きくなる ── つまりスコアの差を、より際立たせる。 指数で全部プラスに直してしまえば、あとは安心して合計で割れます。これでようやく、足すと100%になる確率が手に入ります。 図の左端、数字5の生スコアはマイナス0.5でした。このままでは確率にできませんが、指数に通すと0.61という小さなプラスの値になります。数字3のスコア2.0は指数で7.39まで持ち上がり、8の1.0は2.72になる。三つを合計すると10.72。あとはそれぞれを10.72で割れば、69%・6%・25%という、足してちょうど100%の確率が出そろいます。いちばんスコアの高かった3が、いちばん高い確率になる。順位は変わらないまま、「どれくらい自信があるか」という情報が新しく読めるようになりました。これがソフトマックスの全部です。 式で書けば、$i$番目の出力の確率は次のように決まります。分子が「そのスコアを指数に通したもの」、分母が「全員を指数に通して足したもの」で、割り算がまさに正規化にあたります。 $$ p_i = \frac{e^{z_i}}{\displaystyle\sum_j e^{z_j}} $$触って確かめる 言葉より、動かすのが早いです。下で3つの数字の生スコアをスライダーで動かすと、ソフトマックスが整えた確率が、その場で棒に組み替わります。 うまく表示されないときは、デモを別タブで開く 。 どんなふうにスコアを動かしても、3本の確率を足すと必ず100%になります。ここがソフトマックスの効き目です。「3と8が僅差」のボタンを押すと、二つがほぼ半々に割れて、ネットワークが迷っている様子がそのまま数字に出ます。「3が断然」なら、3が98%まで跳ね上がる。答えを一つ選ぶだけなら生スコアの大小を見れば済みますが、こうして確率にしておくと、その答えをどれだけ信用していいかまで一緒に読めるわけです。 温度で、自信の尖り方が変わる デモにはもう一つ、温度$T$というスライダーがあります。これは指数に通す前に、スコアをまるごと$T$で割っておく、という小さな細工です。式でいえば分子と分母の肩が$z_i/T$に変わるだけ。 $$ p_i = \frac{e^{z_i/T}}{\displaystyle\sum_j e^{z_j/T}} $$温度を下げると($T$を1より小さく)、スコアの差が拡大されて、確率は一点へ尖ります。勝者がほぼ100%を総取りして、自信満々の出力になる。逆に温度を上げると($T$を1より大きく)、差が薄められて、確率はなだらかに均されます。三つが横並びに近づいて、迷いの多い出力になる。デモの「温度を下げる/上げる」で、この尖りとなだらかさを行き来してみてください。 この温度という言葉は、ずっと先で言語モデルに次の単語を選ばせるときに、また出てきます。文章生成AIの設定項目として実在するもので、開発者向けの画面(GoogleのAI StudioやOpenAIのPlaygroundなど)を開くと、温度のスライダーが並んでいます。上げると突飛で多様な言葉が出やすくなり、下げると無難で堅い言葉に寄る ── あれは、次の単語の候補につけたソフトマックスの確率を、いま見たとおりに尖らせたり均したりしているだけなのです。普段のチャット画面には出てこない設定ですが、裏ではこの計算が動いています。 確率にしておくと、学習も素直になる 自信が読めるようになるのは、人間が結果を眺めるときに便利、というだけではありません。学習そのものにも効きます。この連載では第0回から、学習とは「どれだけ外したか」を表す一本の数字(誤差、専門的には損失とも呼びます)を小さくしていくことだ、とくり返してきました。数字の分類では、正解のラベル(たとえば「これは3」)に対して、ネットワークが3にどれだけ高い確率を割り当てられたかで、この間違いの大きさを測ります。正解の確率が1に近ければ損失は小さく、0に近ければ損失は大きい。この測り方(交差エントロピーと呼びます)は、出力が確率になっていて初めて素直に使えます。ソフトマックスは、答えを読みやすくすると同時に、学習の土台もきれいに整えているわけです。 新しい部品としては、これで打ち止めです。ソフトマックスは、指数で持ち上げて合計で割るだけ。それでネットワークの生の反応が、足して100%の確率に変わり、答えと一緒に自信の大きさまで返せるようになりました。 前回のネットは、これで確率まで返せる一人前の分類器になりました。ただ、テスト正解率95.7%という数字には、まだ一つ問い返す余地があります。このネットは本当に数字の形を理解して読めているのか、それとも訓練で見た6万枚を、うまく丸暗記しているだけなのか。次回は、その「理解」と「暗記」の境目を見にいきます。 連載「手で動かすAI」 ← 前回 手で描いた数字を、自作のネットワークが読む 次回 → 覚えるのと分かるのは違う

公開: 2026年7月26日 · Toshihiko Arai
連載『手で動かすAI』第5回のカバー。手書きの数字3が、入力784ピクセル・出力10個のニューラルネットワークに入り、3の出力がいちばん強く光って答えになるイラスト

手で描いた数字を、自作のネットワークが読む ── ここまでの部品だけで

ここまで4回かけて、部品を一つずつ組み上げてきました。ニューロン1個(第1回 )、それを重ねたネットワーク(第2回 )、数字が鎖を流れる前向き計算(第3回 )、そして全つまみを一度に回す逆伝播(第4回 )。ただ、動かしてきたのはいつも、平面に散らばった点や、たった4つのXORといったおもちゃでした。今回はいよいよ、この同じ部品だけで、実物を動かします。あなたが手で描いた数字を、自作のネットワークに読ませてみましょう。約束しておくと、新しい仕組みは一つも足しません。 数字も、結局は数の列 サウンドプログラミングの連載では、音は数の列 だという話から始めました。機械にとって数なのは、音にかぎりません。手で描いた数字も例外ではありません。28×28のマス目に描いた絵は、それぞれのマスがどれだけ黒いかという、784個の数(0から1)の並びにすぎません。ネットワークからすれば、これは入力が784個あるというだけのことです。 前回まで、入力は2つ(大きさと甘さ、あるいは点のx1とx2)でした。それを784個に増やす。出口のほうも、これまでは1つでしたが、今回は0から9までの10個用意します。10個の出力ニューロンが、それぞれ「この絵は0っぽいか」「1っぽいか」……と反応し、いちばん強く反応した出口を答えとする。仕組みとして新しいのは、この「入口を増やし、出口を10個にしていちばん強いものを選ぶ」ところだけで、部品そのものは第1回のニューロンのままです。 学習のやり方も、新しくありません。第4回の逆伝播を、そのまま使います。手書き数字を大量に集めたMNISTという定番の練習台(何者かはデモのあとで触れます)を用意し、その6万枚を1枚ずつネットワークに通して、出た答えと正解のズレから全部のつまみの勾配を求め、少しだけ回す。これを何万回とくり返すだけです。つまり、この回で本当に新しいものは、何一つありません。ただ入口と出口を広げて、たくさんの例で今までどおり学習させた。それだけで、次のデモが動きます。 描いて、読ませる 言葉より、試すのが早いです。下の黒い欄に、0から9の数字を1つ、大きめに描いてみてください。指でもマウスでもかまいません。 うまく表示されないときは、デモを別タブで開く 。 描き終えると、右の10本の棒が動きます。これがまさに10個の出力ニューロンの反応の強さで、いちばん長い棒の数字が、ネットワークの答えです。中身は、たった今説明したとおりのネットワーク(入力784・隠れ層48・出力10)で、MNISTの6万枚から前回の逆伝播で学習させたもの。ブラウザの中で動いているのは、その学習済みのつまみを使った前向き計算だけです。ひとつだけ舞台裏を明かすと、描いた絵はそのまま渡すのではなく、中央に寄せて大きさをそろえてから28×28にしています(小さな28×28の表示が、実際にネットワークへ渡している絵です)。学習に使ったMNISTの数字が中央ぞろえだったので、それに合わせているだけです。 太く・大きく・なるべく真ん中に描くと、よく当たります。逆に、隅に小さく描いたり、他の数字と紛らわしい崩し方をすると間違えます。95.7%というのは裏を返せば、20枚に1枚くらいは外すということでもあります。人間だって走り書きは読み違えるので、そこは似たようなものです。 ひとつだけ先に断っておくと、日本で習うとおりに旗をつけて「1」を書くと、このデモはほぼ確実に7と答えます。 崩し方の問題ではなく、学習に使った数字の書き手が原因です。理由は後の節 で書きます。 学習に使ったMNISTのこと さきほど後回しにしたMNISTに触れておきます。手書き数字を集めた画像のセットで、訓練用が6万枚、答え合わせ用が別に1万枚あります。1枚は28×28の白黒画像で、それぞれに「これは3」という正解が添えてあります。元になった数字を書いたのは、アメリカの国勢調査局の職員と高校生で、1990年代に機械学習用として整理されました。 このセットが長く使われてきたのは、みんなが同じ問題を解いているからです。新しい手法を思いついた人がまずこれで試し、「うちの方法は98%当たった」と言えば、他の手法と同じ土俵で比べられる。共通のものさしとして便利なので、機械学習を始める人が最初に触れる題材にもなりました。今回のネットワークが出した95.7%も、この土俵の上での数字です。 「1」がどうしても7になる、という報告 ここに、この連載でいちばん大事かもしれない落とし穴があります。日本で習うとおりに「1」を書くと、このデモはまず外します。 左上から斜めに入る、旗のついた1のことです。 手元で同じデモに描いて測りました。旗つきの1は7が64%、9が21%で、正解の1はわずか1%。同じ位置に旗のない素の縦棒を引くと、1が98%になります。ついでに本物の7を描くと7が94%。つまりネットワークから見て、旗つきの1と7はほとんど同じ形なのです。上に斜めの線があって、そこから下へ伸びる ── 言われてみれば、そのとおりです。 原因は仕組みではなく、学習に使った数字を書いた人にあります。MNISTを書いたのはアメリカの国勢調査局の職員と高校生で、アメリカ式の「1」はただの縦棒です。旗つきの1は、6万枚の中にほとんど入っていません。見たことのない形を出されたネットワークは、手持ちの知識でいちばん近いもの、つまり7と答えます。 95.7%という数字は、あくまでMNISTと同じ書き方の数字に対しての成績です。書き手が変わり、国が変わり、時代が変わると、同じネットワークがあっさり外す。学習データに何が入っていて、何が入っていないか。それが、仕組みの良し悪しと同じくらい結果を左右します。この連載でこの先どんな仕組みを足しても、入っていないものは読めないという事実だけは変わりません。 (デモで試すときは、旗をつけずに縦棒で「1」を書いてみてください。素直に当たります。) なお、このページが持ち歩いているのはMNISTそのものではありません。6万枚から学び終えたあとの重み、つまり数値が4万個弱の入ったファイルだけです。画像の山は学習が済んだ時点で役目を終えていて、あなたが数字を描いたとき動いているのは、その数値を使った掛け算と足し算だけです。 これが、画像認識の芯 ここで一度、立ち止まる価値があります。いま動いたものは、写真から猫を見つけたり、スキャンした文字を読み取ったりする画像認識の、まぎれもない芯です。使った道具は、この連載で手を動かして作ってきた三つ ── ニューロン、それを重ねること、そして勾配降下 ── だけ。それを入力784個・出力10個に広げ、6万枚で回しただけで、手書き数字が95%以上読めてしまう。世の中の巨大な画像認識も、つまみの数が何億に増え、層がずっと深くなっただけで、芯の考え方はいま触ったこれと同じです。 もちろん、このネットワークにはまだ素朴なところがあります。たとえば784個の入力を、ネットワークは順番のないただの数の袋として扱っていて、それが本当は縦横に並んだ一枚の絵だということを知りません。隣り合ったピクセルが関係しているという当たり前の事実を、まだ活かせていないのです。ここを作り込むと、認識はもう一段強くなります。 また、10本の棒の高さは、いまはそれぞれのニューロンが勝手に出した反応の強さで、合計しても100%になりません。「この絵は80%の自信で3」といった、確率らしい出力に整える一手も、まだ入れていません。 次回はまず、この出力を確率のかたちに整えるソフトマックスという小さな仕上げを見て、そのあとで、ピクセルが一枚の絵だという事実を活かす方法へ進んでいきます。土台の上に、実物のための工夫を一つずつ足していく段階に入ります。 連載「手で動かすAI」 ← 前回 100万個のつまみを、どうやって一度に回すのか 次回 → 自信の大きさまで答えさせる

公開: 2026年7月25日 · Toshihiko Arai
途切れたトーンの切れ目を無音のままにすると途切れて聞こえ、ノイズで塞ぐと破線のように音が続いて聞こえることを示した図

鳴っていない音が聞こえ続ける ── 連続性の錯覚

電話の向こうで工事の音がして、相手の声がところどころかき消される。それでも会話は途切れて聞こえず、声はずっと続いていたように感じる。誰でも身に覚えのあるこの日常は、実は耳の錯覚です。かき消された瞬間、声の証拠は本当に耳へ届いていません。それでも聞こえた気がするのは、脳がその区間を作って埋めているからです。 連続性の錯覚と呼ばれるこの現象は、この連載の「耳のバグ」の棚では差音 ・ミッシング・ファンダメンタル ・シェパードトーン ・リセ・リズム に続く5本目です。これまでの4本が音の高さやテンポの錯覚だったのに対し、今回は「鳴っていない音がまるごと聞こえる」という、いちばん大胆な捏造を耳がやってのけます。 穴は穴に聞こえる。では塞ぐと? まず錯覚が起きない側から見ます。サイン波のような単純な音を、鳴らして、止めて、また鳴らして、と繰り返すと、当然ながらプツプツと途切れた音に聞こえます。切れ目は100ミリ秒。短いようで、耳には隠しようのない長さです。 ここで、切れ目の区間だけに大きめのノイズをかぶせます。音そのものは1ミリ秒たりとも足していません。トーンは切れ目で完全に止まったまま、穴をノイズで塞いだだけです。ところが聞いてみると、トーンはノイズの向こうで鳴り続けている1本の音に化けます。物理的には存在しない区間の音を、耳がはっきり聞かせてくるのです。 種を明かすと、これは脳が行っている推論の副産物です。ポイントは、かぶせたノイズが「もしトーンが鳴り続けていたとしても、かき消してしまう」だけの大きさを持っていることです。このとき脳の立場からすると、ノイズの区間にトーンの証拠が無いのは当然で、「途切れた」証拠にはなりません。鳴っていても鳴っていなくても、届く音は同じだからです。そこで脳は、切れ目の前後がきれいにつながるという状況証拠から「鳴っていたはず」と判定し、その区間の音を自前で補って聞かせます。逆に言えば、ノイズが小さくて「鳴っていれば聞こえたはず」の状況では、脳は正しく「途切れた」と判定します。錯覚が起きるかどうかの分かれ目は、ノイズの音量が握っているわけです。 触ってみる 切り替えながら確かめられるデモを用意しました。音の高さがゆっくり上下する「うねる音」で、まず「無音のまま」のプツプツ途切れた状態を確認してから、「ノイズで埋める」に切り替えてみてください。それだけで、うねりがノイズの向こうで1本につながります。切れ目の間も音程が動き続けて聞こえたら、それは完全に脳の作った音です。イヤホンで聴くとよく効きます。 うまく表示されないときは、デモを別タブで開く 。 いちばんの見どころはノイズの音量スライダーです。つながって聞こえている状態から少しずつ下げていくと、どこかで錯覚が壊れて、突然「途切れた音+小さいノイズ」に聞こえ出します。先ほどの理屈どおり、「鳴っていれば聞こえたはずなのに聞こえない」と脳が判定できる音量まで下がった瞬間です。「一定の音」モードも用意しましたが、こちらは効き方の個人差がかなり大きい上級編です。動き続ける軌道という強い手がかりを持つうねる音に比べて、一定の音の補完は地味で、同じ音量でも途切れて聞こえる人がいます。錯覚は万人に同じ強さでは働かない、という実感も含めて試してみてください。 この錯覚は、単純な音だけでなく言葉でも起きます。文章の一部の音を咳の音で置き換えて聞かせると、聞き手は欠けた音素まで含めて文章を完全に聞き取り、どこが欠けていたかを言い当てられない。1970年に心理学者リチャード・ウォーレンが報告した音素修復と呼ばれる実験で、連続性の錯覚が日常の聞き取りをずっと下支えしていることを示しました。雑踏や電話越しで会話が成立するのは、耳が物理的な音をそのまま報告する装置ではなく、欠けを埋めながら聞かせる推論装置だからです。 どう実現するか デモの実装は、この連載でやってきたことの組み合わせで足ります。トーンは鳴らしっぱなしのオシレーターを、ゲイン(音量)の自動化で周期的にオン・オフするだけ。ノイズは2秒ぶんの乱数を詰めたバッファをループ再生して、切れ目の区間だけゲインを上げます。 const PERIOD = 0.53, TONE_ON = 0.43; // 0.43秒鳴らして0.1秒の切れ目 const OVERLAP = 0.04, RAMP = 0.012; // ノイズは切れ目より少し広く覆う // サイクル k のトーンとノイズの窓 toneGain.gain.setValueAtTime(0, on); toneGain.gain.linearRampToValueAtTime(TONE_GAIN, on + RAMP); toneGain.gain.setValueAtTime(TONE_GAIN, off - RAMP); toneGain.gain.linearRampToValueAtTime(0, off); noiseGain.gain.setValueAtTime(0, off - OVERLAP); // 切れ目の少し前から noiseGain.gain.linearRampToValueAtTime(g, off - OVERLAP + RAMP); ただし、この錯覚には成立条件があって、雑に作ると全く働きません。実は開発中の最初の版では、真っ白なノイズをそのまま被せていて、錯覚が起きませんでした。ノイズは十分うるさく聞こえるのに、トーンは途切れたまま。計算してみると原因は明快で、白色ノイズはエネルギーが全周波数に薄く広がるため、トーン周辺の帯域だけを見るとトーンのほうが18dBも大きく、まるでかき消せていなかったのです。かき消せていない以上、脳は正しく「途切れた」と聞き取ります。そこでノイズをバンドパスフィルターでトーンの帯域へ絞り込み、ゲインを掛けて、帯域内でノイズがトーンを6dB以上上回るよう設計し直しました。帯域はかなり細く(Q=7)絞っています。マスキングに効くのは帯域内の音圧だけで、帯域の外に広がるエネルギーは錯覚に寄与せず、ただうるさいだけだからです。絞るほど、同じマスキング力を小さな総音量で実現できます。錯覚を起こすのは「うるさいノイズ」ではなく「その音の居場所を静かに占拠するノイズ」です。 うねる音では、このバンドパスの中心周波数を音程の軌道に追従させています。ここにも一つ罠があって、バンドパスの帯域幅は中心周波数に比例して広がるため、そのままだと高い音程のときほどノイズの総量が増えて、ノイズ自体の音量がうねって聞こえてしまいます。中心周波数に応じてゲインを補正し、総音量を音程によらず一定に保っています。 細かい数字もふたつ効いています。ひとつは12ミリ秒のランプで、ゲインを瞬間的に切り替えるとプチッというクリックノイズが出て、そこが「切れ目の証拠」として耳に残ってしまいます。もうひとつはノイズの窓を切れ目の前後に40ミリ秒ずつ、合わせて80ミリ秒だけ広く取っている点です。トーンの端とノイズの端がぴったり同時だと、切り替わりの縁が聞き取れてしまう。ノイズが先に立ち上がってからトーンが消えるようにして、縁を完全に覆い隠します。錯覚の実験装置は、こういう縁の処理が生命線です。 ちなみに、この錯覚は音声技術の側でも実用されています。インターネット通話で音声データの一部が届かなかったとき、欠けた区間を前後から予測した音で埋めるパケットロス隠蔽という処理は、脳が勝手にやっている補完を機械側で肩代わりする発想です。埋め方が多少雑でも、聞き手の脳がさらに上から補正してくれるので、思いのほか気づかれません。 参考文献 R. M. Warren, “Perceptual Restoration of Missing Speech Sounds,” Science, vol. 167 (1970), pp. 392–393. DOI: 10.1126/science.167.3917.392 。咳で置き換えた音素が補完されて聞こえる音素修復の原論文。 連続性の錯覚の全体像は Wikipedia: Continuity illusion がまとまっています。 デモの実装には Web Audio API を使用。GainNode のオートメーションでトーンとノイズの窓を予約しています。 音が消えている100ミリ秒のあいだ、耳に聞こえているものの正体を、ぜひデモで確かめてみてください。 ...

公開: 2026年7月25日 · Toshihiko Arai
みかん色のパラシュートで空を降下するえびふりゃ。遠くに飛行機が小さく去っていく

輸送機から降ってくるマスコット ── ゲームの開幕ムービーを、AIとBlenderで作った

前回 、音感トレーニングのジャンプゲームをAIと作っている話を書きました。画面の横軸がピアノの鍵盤になっていて、和音の音の上だけに足場が出るあれです。あれからリリースに向けてあれこれ手を入れ続けているのですが、その試行錯誤の途中で思いついたのが、今回の到着ムービーです。ステージが島になっていて、新しい島に挑むたびに「飛行機で島へ運ばれて、パラシュートで降りて、砂浜に着く」というシーンが流れたら格好いいのではないか。 思いついたプロットはこうです。上空を俯瞰で飛ぶ飛行機。腹のドアが開いて、マスコットのえびふりゃが落ちる。落下の途中でパラシュートがバサッと開いて、ふわりと浮く。砂浜に着地して、勢い余って砂に埋まる。鉢巻きと尻尾だけ覗かせてブルブル震え、ポンッと飛び出して、さあ冒険だ。 先に、完成したものをお見せします。25秒・全8ショット・614フレーム。雲海の巡航からジャンプまでの通しです。 ここから先は、この25秒がすんなりできなかった、という話です。作り方は3D化のとき と同じで、Blenderを私は一切触らず、コードは全部AIに書かせます。舞台になる海辺は釣りの世界 で一度作ってあるので、今回は流用すればすぐ終わる──と思っていました。終わりませんでした。この記事は、その顛末の記録です。 また、海に負けかけた まず舞台のビーチです。私は最初に釘を刺しておきました。海を作るのはすごく大変だったのだから、釣りゲームで作った海を活用してほしい、と。あの海は、サイン波を全部捨ててノイズにたどり着くまで三度負けた、いわくつきの海です。 AIは「海を移植しました」と、それらしいビーチを出してきました。ところが見ると、全然違う。うねりがゴロゴロした、彩度の高い、どこかで見たようなCGの海。しかも波打ち際には泡の玉が置物のように並んでいる。置物の泡は、釣りの海で「チートがバレましたね」と白状させた、まさにその却下案です。同じAIが、同じ人間相手に、同じ手を二度打ってきたわけです。 問い詰めると、原因はあっけないものでした。AIは釣りの記事の「文章」は読んでいたのに、記事に載っている海の「画像」を一度も見ていなかった。文章から仕組みだけ拾って、見た目は自分の癖で作っていたのです。画像を並べて比較させてようやく、波はほぼ平らでいい、色は淡い岸から水平線の濃い帯への3段だ、主役は細かいきらめきの粒だ、と伝わりました。 ついでに技術的な収穫もひとつ。色がどうしても濃く沈む問題の犯人は、色空間の変換漏れでした。人間が指定する色(sRGB)とレンダラーが計算に使う色(リニア)は別物で、変換せずに渡すと「深い紺」のつもりが「中途半端な青」になる。分かってしまえば一行の変換関数の話ですが、これを踏むまで、私は何度も「記事の海と全然違う」と言い続けることになりました。 波打ち際は釣りの海の結論をそのまま使っています。水際線を描くのではなく、海へ下る砂の斜面と水面の交わる線から「生まれさせる」方式。おかげで泡の白い線は、置物ではなく水面の高さの等高線として、勝手に有機的な曲線になります。 頼んでいない島が、生えてきた 事件はビーチの沖で起きました。あるとき水平線の手前に、緑色の饅頭が2個浮かんでいたのです。言っておきますが、私は島を頼んでいません。このゲームはステージを島単位で渡っていく作りなので、海の向こうに次の島が見えたら気が利いているだろう、とAIが自分で判断して置いたようです。 気持ちは分かります。でも饅頭にしか見えない。100メートルほど先にぽっかり浮かんでいるようにしか見えないのです。遠くの島というのはもっと霞んでいるものなので、置くならもっと遠くにリアルに霞ませろ、と直しを頼みました。AIは霞の色に寄せる、稜線をギザギザにする、切り絵のシルエットにする、と手を替え品を替えてきましたが、多少ましになるだけで、どうやっても「近くの何か」から抜けない。距離感というのは物体の描き込みではなく空気の描き方で出るものらしく、それを本気でやるのは大工事です。 そこで判断しました。ステージ到着の数秒のムービーのために、遠景の空気遠近を作り込むのは割に合わない。「島を表現するな。消せ」。消してみると、水平線だけの海で何も困りませんでした。AIが気を利かせて足した背景ほど、リアリティの請求書は高くつくという話です。 輸送機を、デザインし直す キャラを運ぶ飛行機は、最初AIが小ぶりな単発のプロペラ機を出してきました。これが原案です。 可愛いのですが、これでは遊覧飛行です。私のイメージは、パラシュート部隊が飛び降りる、プロペラがいくつも付いた大型の輸送機。ただしミリタリー色に塗れという意味ではなく、色はパラシュートと同じ「みかん×生成り」のままで、シルエットだけ輸送機にしてほしい。 そう伝えて出てきたのが、この機体です。4発のプロペラ、太い胴体、高い位置の一枚翼、T字の尾翼。そして投下シーンの主役になる、腹の観音開きのドア。 作り直しの途中には珍事もありました。プロペラの羽根を回転軸の中心に置いた箱で作ったせいで、3枚羽根のつもりが中心から6本の腕が生えた風車になっていたのです。羽根は軸から一方向に伸びるものだと教えて、ようやくプロペラになりました。 世界の縁は、四角い 次の敵は世界の形でした。飛行機のシーンを作らせると、まず高度が低い。海面スレスレを飛ぶ輸送機を見せられて、これでは輸送機というより飛行艇です。 高度を上げさせると、今度はもっと根本的な問題が露出しました。カメラが高く上がった途端、海の四角い縁、つまりワールドの終端が画面に写り込んだのです。 3Dの世界は、実は板の上に作られています。地上すれすれのカメラなら板の縁は水平線の彼方ですが、上空からの俯瞰では板がただの板として見えてしまう。対策は身も蓋もなくて、海の板を巨大にするだけです。ただし芸のない巨大化はレンダリングが重くなるので、波の立つ詳細な海は岸の近くだけにして、その外側は起伏のない一枚板を敷く二層構えにしました。近くは波、遠くは色。人間の目は遠くの波など見ていないので、これで十分です。 高度のほうは、実際の高さの数値よりも記号が効きました。雲です。雲海を下に見て飛べば、それだけで「はるか上空」になる。クレイ調の平たい雲をいくつか浮かべ、輸送機はその上を飛ばすことにしました。 開傘は、カメラが落ち続ける 今回いちばん作りたかったのは、パラシュートが開く瞬間です。私の注文は「映画のスカイダイビング映像のリアル感」でした。落下するカメラがキャラと並走する。パラシュートが開いた瞬間、キャラだけが急減速する。カメラは速度を一切変えずに落ち続け、上を仰ぎながらキャラを見送る。するとキャラは、画面の中でぐんぐん上へ遠ざかって小さくなっていく。 実際に浮かび上がっているのではありません。減速しただけです。でも落ち続けるカメラから見れば、それは「急に浮いた」ように見える。相対運動だけで無重力感を作る、実写のスカイダイビング撮影がやっているのと同じ理屈です。 パラシュート自体も、最初から開いた状態を出すわけにはいきません。畳まれた束がまず細長いストリーマーになってはためき、それからバサッとドームに開く。開く瞬間には弾性の揺り戻しを入れて、布が空気を掴んだ感じを出します。 ここで表情の駄目出しをひとつ。開傘の喜びで口を開けた顔にさせたのはいいのですが、AIはその顔のまま延々と降下を続けさせました。いつまでも口を開けていたら不格好です。感情の表現は一瞬でいい。「すぐ元に戻せ」という指示で、口開きは開傘の0.5秒だけになりました。 緊張感がないんだよ 着地のシーンには、注文を重ねました。最初にAIが出してきた着地は、ふわふわとスローに降りてきて、そっと砂に埋まるものでした。可愛い。可愛いが、緊張感がない。勢い余って埋まるのだから、埋もれるくらい速くないと意味がない。 もうひとつ、細かいけれど許せなかったのが砂の土手です。突入でめくれた砂がクレーターの縁に盛り上がるのですが、AIはこの土手を着地する前から置いていました。埋まってから現れるなら分かる。まだ何も起きていない砂浜に、結果だけが先にある。ホラーです。 直した後の着地はこうなりました。0.5秒の急降下で砂に突入。衝撃でカメラが揺れ、土手はその瞬間に盛り上がって現れる。役目を終えたパラシュートは消えたりせず、空気が抜けてフニャーと萎み、横に流れて砂の上に横たわる。以降のシーンでも、萎んだ傘はずっと背景に残っています。 総計何時間かかったと思ってんの 制作の裏で、私はAIにこう聞きました。今回のレンダリング、総計何時間かかったと思ってるのか。AIは「1本25分」のような顔をしていましたが、実際に積み上げさせると約3時間。しかもその大半は、レンダラーが遅いからではなく、直すたびに全フレームを焼き直していたからでした。 ここから運用を変えさせました。まず、動画をショット単位の部品に分割する。巡航、投下、落下、開傘、降下、着地、飛び出し、歩き。修正が入ったら、そのショットのフレームだけレンダリングし直して、部品を差し替えて連結する。連結は再エンコードすらしないので一瞬です。確定したシーンは、二度とレンダリングしない。 もうひとつはGPUです。調べさせたら、スクリプトはGPU指定をしておらず、ずっとCPUだけで回っていました。Metal GPUを有効にし、さらにフレーム間でシーンの下準備を使い回す設定(persistent data)を入れて、1フレーム3.5秒が1.8秒に。仕上げの高解像度に至っては3倍速になりました。この2つで、「1ショット直すのに25分」が「2〜4分」になっています。 歩き出して、ジャンプで終わる 最後のシーンは、ゲームへの接続です。砂から飛び出したえびふりゃが、体を左右にくねらせながら、正面のカメラへ向かってペタペタ歩いてくる。目の前まで来たら、ぐっとためて、ジャンプ。画面いっぱいの笑顔で飛び上がったところでカットです。このジャンプがそのままゲームの1跳ね目につながる、という算段です。 ...

公開: 2026年7月24日 · Toshihiko Arai
サウンドプログラミング連載の目次。音の波形が数字の列に分解され、合成・エフェクト・分析へ枝分かれしていく図

サウンドプログラミング入門 ── ブラウザで鳴らして学ぶ、音の作り方と仕組み

音をプログラムで扱うと聞くと、専門の道具や難しい理論が要りそうに思えます。ところが中身は驚くほど単純で、音とは1秒間に数万個並んだ数字の列でしかありません。スピーカーはその数字のとおりに膜を前後させているだけです。だから音を作るのも、加工するのも、分析するのも、突き詰めればすべて数列の計算になります。 このページは、araisun.com で書いているサウンドプログラミング連載の目次です。オシレータやエンベロープといった音響合成の基本から、ディレイ・リバーブなどのエフェクト、声を作るフォルマント合成、FFTによる分析、そして耳が引っかかる錯覚まで。どの回にも、ブラウザで実際に音が鳴るデモを置いてあります。 Web Audio API で書いた素のJavaScriptなので、何もインストールせずにその場で聴けます。読んで分からなくても、つまみを動かせば分かる、という作りを目指しました。 目次のうち「準備中」と書いてあるものは、書き上がってはいるものの、まだ公開していない回です。それぞれの分野の中に混ぜてあるので、この連載がどこまでを扱うつもりなのかは、そこから読み取ってください。公開したらリンクになります。 数式は隠しません。ただし出しっぱなしにもせず、必ず「その式で何が鳴るのか」まで着地させます。C言語で音を書いていた時代の教科書的なアルゴリズムを土台にしているので、Web Audio に限らず、どの言語でも同じ考え方が使えます。 どこから読むか はじめてなら、音は数の列 から順に読むのが素直です。サンプル・倍音・エンベロープという語彙が最初の3本でそろい、以降がぐっと読みやすくなります。 作りたい音が決まっているなら、目次から直接どうぞ。ウォブルベースが作りたい、リバーブの中身が知りたい、といった動機で1本だけ読んでも成立するように書いています。 理屈より不思議な話が読みたいなら、耳の錯覚 の章から。無限に上がり続ける音階や、鳴っていないのに聞こえる低音など、聴いて驚く回を集めてあります。 音の正体 音・音色・音程が、それぞれ数の何にあたるのか。連載の土台になる4本と、音楽の側から見た1本です。 音は数の列 ── サイン波を数式から鳴らす 音色の正体は倍音の配合 ── 足し算だけでノコギリ波を作る 同じサイン波が「ピアノらしく」も「オルガンらしく」もなる ── ADSRエンベロープ 音色を変えるデジタルフィルター入門 ── ローパスとハイパスのカットオフ きれいな和音はなぜ「うなり」が消えるのか ── 平均律と純正律 楽器の音を合成する サンプリング音源を使わず、計算だけで楽器の音を作る回です。 sin 2つで金属もベルもエレピも ── FM合成 ノイズをひと吹き、輪に閉じ込めて回すだけで弦が鳴る ── カープラス・ストロング ノイズと sin だけでドラムセットを作る ── 打楽器の手続き合成 トロンボーンの音を「実測」で当てにいく ── 音色工房① 声を作る 母音、子音、そして機械が喋るようになるまでの歴史。 ノコギリ波が「あー」と歌う ── フォルマント合成 「あ」は作れたのに「さ」が作れない ── 子音の正体(前編・摩擦音) 「た」は音ではなく出来事 ── 子音の正体(後編・破裂音) 機械はどうやって喋ってきたか ── ボコーダーからAI音声まで 「ゆっくり」も初音ミクも同じ木の枝 ── 音声合成の系統樹 シンセサイザーの音づくり クラブミュージックでおなじみの音を、部品から組み立てます。 ...

公開: 2026年7月24日 · Toshihiko Arai
連載『手で動かすAI』第4回のカバー。誤差がネットワークを逆向きに流れ、各つまみへ責任として配られていくイラスト

100万個のつまみを、どうやって一度に回すのか ── 誤差逆伝播

この連載では毎回、「自動で学習する」というボタンを押してきました。直線が点に吸い付き、境界線が青と赤を分け、曲がった塗り分けがXORを解く。そのたびに「誤差が減る向きへ、つまみを少しずつ回している」と説明してきましたが、肝心のことをまだ話していません。誤差が減る向き、つまり勾配を、機械はどうやって知るのか。今回はその中身をついに開けます。ここは現代のAI訓練の心臓部で、この連載でいちばん大事な回です。 測り方その1: 少し動かして、差を見る じつは、勾配を測るだけなら誰にでもできます。知りたいのは「このつまみを少し増やしたら、誤差は増えるのか減るのか、どれくらい変わるのか」でした。なら、実際に少しだけ動かして、誤差を測り直せばいい。 ここで言葉を一つ、高校数学と接続しておきます。勾配とは傾きのことで、これは高校で習った微分の「傾き」と同じ意味です。ただし横軸が違います。時間や x の代わりに、横軸につまみの値、縦軸に誤差をとったグラフを思い浮かべてください。つまみを回すと誤差が上下する一本の曲線が描けます。その曲線の、いまのつまみ位置での傾きが勾配です。 図のとおり、左右でやっていることは同じで、軸の名前が x と y から「つまみの値」と「誤差」に変わっただけです。傾きがプラスなら、つまみを右に回すと誤差が増える(だから左へ回すべき)。マイナスなら右へ。学習に傾きが必要なのは、それが下り坂の向きを教えてくれるからでした。 式で書くと、つまみ $w$ の勾配はこう近似できます。$h$ はごく小さい数(たとえば0.0001)です。 $$\text{勾配} \approx \frac{L(w+h) - L(w)}{h} \qquad (L\text{ は誤差})$$見覚えのある形のはずです。高校で習う微分係数の定義 $$f^{\prime}(x) = \lim_{h \to 0} \frac{f(x+h) - f(x)}{h}$$の、$h$ を「限りなく0に近づける」代わりに小さい数で止めたものがこれです。つまりこの測り方は、微分の定義を素朴にそのまま実行しているだけ。ただの割り算です。これを数値微分と呼びます。コードにしても3行で書けます。 function numericGrad(k) { // k番目のつまみの勾配 const h = 1e-4, base = loss(P); // いまの誤差 P[k] += h; // 少しだけ動かして const g = (loss(P) - base) / h; // 誤差の変化 ÷ 動かした量 P[k] -= h; // 元に戻す return g; } これは正真正銘、ちゃんと学習できる方法です。全つまみをこの方法で測って回せば、これまでのデモと同じようにネットワークは賢くなります。素朴で、確実で、そして重大な欠点が1つあります。 問題は、遅すぎること 数値微分は、つまみ1個の勾配を測るために誤差の計算(=データ全部に対する予測)を1回余分に必要とします。つまみが N 個なら、基準の1回とあわせて N+1 回。第2回 のネットワークはつまみ13個だったので14回、これはまだ笑って済みます。 しかし本物のAIは笑えません。いまの大規模言語モデルはつまみ(パラメータ)を数十億から数兆個持っていて、学習ではそれを何百万ステップも回します。1ステップごとに数十億回の予測計算をやり直すのは、どんな計算機でも不可能です。つまみを増やせば賢くなると分かっていても、この測り方のままでは、そもそも訓練が終わらない。ディープラーニングが長いあいだ離陸できなかった理由の一つが、まさにここでした。 測り方その2: 誤差への「責任」を後ろへ配る そこで登場するのが誤差逆伝播(バックプロパゲーション)です。発想を変えます。つまみを1個ずつつついて外から反応を見るのではなく、誤差がどの経路を通って生まれたかを、計算の流れを逆にたどって配りなおすのです。 出力の誤差は、まず出力の直前にいたつまみたちのせいです。では隠れ層のニューロンに責任はないかというと、あります。ただし全員が同罪ではなく、出力に強くつながっていた(重みの絶対値が大きかった)ニューロンほど、誤差への影響も大きかったはずです。だから、責任はそのつながりの重みを掛けて渡す。重みが大きいほど強く伝わり、重みがマイナスのつながりなら、伝わる責任の向き(符号)も反転します。責任を受け取った隠れニューロンは、こんどは自分の入力側のつまみたちへ、同じルールでまた責任を配る。 これを出力から入力まで一巡させると、すべてのつまみに「あなたはこの誤差にこれだけ加担した」という数が行き渡ります。この数こそ勾配です。かかる手間は、前向きの計算を1回と、責任配りを1回。前向きの計算とは、前回 で数字を鎖に流したあの計算、入力を左から右へ通して予測と誤差を出すところまでのことです。責任配りの「逆流」と区別するために、この普段どおりの向きを前向きと呼ぶのでした。前向き1回と逆流1回、たった一往復で、つまみが13個だろうと数十億個だろうと、全部の勾配が同時にそろいます。 ここで大事なことを言っておくと、逆伝播は数値微分と別の何かを求めているのではありません。求めているものは同じ、さきほどの「つまみに対する誤差のグラフの傾き」です。違うのは求め方だけ。誤差は「入力 → 重み付き和 → 活性化 → … → 誤差」と関数が入れ子になった合成関数なので、その傾きは、高校数学の合成関数の微分 ...

公開: 2026年7月24日 · Toshihiko Arai
ふらつく歌声の音程が、いちばん近い正しい音へ吸い付いて階段状になるオートチューンの図

オートチューンの仕組み ── 音程を検出して、吸い付かせる

ラジオから流れるポップスで、人間の声なのに電子楽器のように音程がカクカクと切り替わる歌声を聴いたことがあると思います。テクノポップやヒップホップでおなじみの、いわゆる「ケロケロ声」です。あれはボコーダーのような特殊な機材の音ではなく、正体は音程補正ツール、いわゆるオートチューンです。本来は歌の音程のずれをこっそり直すための道具が、設定を極端にすると、あの声になります。 前回のピッチ検出の記事 では、マイクに入った音の高さを言い当てるチューナーを作りました。今回はその直接の続編です。チューナーは「ラより47セント高いですよ」と教えてくれるだけで、直すのは人間の仕事でした。オートチューンは、そこから一歩踏み込んで、機械が音程そのものを直してしまいます。 中身は3段のくり返し オートチューンのやることは、突き詰めると3段です。いまの音程を検出する。いちばん近い「正しい音」を選ぶ。そこへ向けて音程をずらす。これを1秒に何十回もくり返すだけです。 1段目の検出は、前回やった自己相関そのものです。波形を自分自身と少しずつずらして重ね、ぴたり一致するずれ幅から基本周期を割り出す、あの方法がここでも心臓部として動いています。チューナーとオートチューンは、前半がまるごと同じ機械なのです。 2段目が今回の新顔で、「正しい音」を決める段です。といっても大げさなものではなく、あらかじめ「この曲で使ってよい音の集まり」を決めておくだけです。この集まりをスケール(音階)と呼びます。たとえばハ長調ならドレミファソラシの7音で、平均律の回 で見たとおり、それぞれの音の周波数は決まっています。検出した音程が452Hzだったら、スケールの音の中でいちばん近いものを探す。ラ(440Hz)までは47セント、シ(494Hz)までは153セント。だから目標はラだ、という多数決ならぬ最短距離の判定です。 3段目は、選んだ目標へ向けて音程をずらす段です。今回のデモのように音を合成で作っている場合は簡単で、位相を回す速さ を補正後の周波数に差し替えるだけで済みます。マイクで録った本物の歌声を加工する場合はもう少し大変で、波形を壊さずに音程だけ動かす処理が要るのですが、やりたいことは同じ「452Hzの声を440Hzの声にして出す」です。 「吸い付く速さ」がケロケロの正体 ここまでで音程は直せるのですが、実はまだ大事なつまみが1つ残っています。目標へ向けて、どれくらいの速さでずらすか。オートチューンの音色を決定づけるのは、検出でも量子化でもなく、この補正スピードです。 人間の歌声は、正確な歌手でも音程がぴたりと一直線ではありません。ビブラートで揺れ、音の変わり目では下や上からしゃくり上げ、音の中心もわずかに漂います。この揺れこそが人間らしさで、全部消すと不自然になります。そこで補正を0.1〜0.2秒くらいかけてゆっくり効かせると、速い揺れ(ビブラートやしゃくり)は補正が追いつけずに生き残り、ゆっくりしたずれ(音の中心の外れ)だけが直る。これが「直したことに気づかれない」自然な補正で、オートチューンの本来の使い方です。 では補正時間を0にしたら何が起きるか。検出した瞬間に音程が目標へ張り付くので、出てくる音程はスケールの音の上をぴたりと這い、音から音へは垂直にジャンプします。図の右側のとおり、音程の軌跡が階段になるのです。ビブラートもしゃくりも消え、階段の踏み面は電子楽器のように平坦、段差の瞬間はポルタメントなしの瞬間移動。人間の声帯では物理的に出せないこの動きが、耳には「人の声なのに機械」という独特の質感、つまりケロケロ声として聞こえます。 面白いのはここからで、この設定はもともと「やり過ぎ」、いわばエフェクトの副作用でした。オートチューンの元祖である Auto-Tune は、石油探査の信号処理技術者だったアンディ・ヒルデブランドが1997年に発売したソフトウェアで、狙いはあくまで自然な音程補正でした。ところが翌1998年、シェールの「Believe」が補正スピード最速の「不自然な」設定を堂々とサビで使ってヒットし、以来この副作用は、T-Pain がそれを自分の看板に据え、カニエ・ウェストがアルバム1枚をまるごとこの声で通したあたりで、ひとつの声の音色として完全に定着します。歪みすぎたギターアンプの音がロックの音色になったのと同じ道筋を、歌声の世界でたどったわけです。 この声を曲で聴く 言葉で説明するより、実際にその声が入った曲を聴いたほうが早い部分でもあります。エフェクトの副作用が音色として定着し、やがて感情を運ぶ道具にまで変わっていく道筋を、年代順に4枚だけ挙げておきます。 Cher / 1998 Believe この声が世に出た起点。サビの「I can't break through」で音程が階段状に跳ぶのが、まさに補正スピードを最速にしたときの挙動です。発表当時は効果の正体が伏せられていて、何の機材を使ったのかがしばらく話題になりました。 YouTubeで探す Amazonで探す T-Pain / 2005 Rappa Ternt Sanga こっそり直すための道具を、隠すどころか自分の声の看板にしたデビュー作。「ばれないように使う」という前提をひっくり返した時点から、これはエフェクトではなく歌い方のひとつになりました。 YouTubeで探す Amazonで探す Kanye West / 2008 808s & Heartbreak ラップをやめてアルバム1枚を歌で通し、その歌をまるごとこの声にした作品。うまく歌うためではなく、感情を平坦に均して寒々しさを出すために使っています。補正の副作用が、表現のための選択肢として扱われた例です。 YouTubeで探す Amazonで探す Bon Iver / 2009 Blood Bank 収録曲「Woods」は伴奏がなく、この声だけを重ねて作られています。ダンスミュージックともヒップホップとも縁のなかったフォークの歌い手が、機械の声を震えや寂しさの側に引き寄せた1曲で、翌年にはカニエ・ウェストがこれをサンプリングしています。 YouTubeで探す Amazonで探す ジャケット画像はMusicBrainz / Cover Art Archive由来の画像を記事内に同梱して配信しています。 触ってみる 聴き比べるのがいちばん早いので、デモを用意しました。再生ボタンを押すと、わざと音程をふらつかせた歌声風のメロディ(ドレミソミレド)が鳴ります。各音は最大で半音の半分近く外れていて、ビブラートもかかっています。この同じメロディを、補正オフ・なめらか補正・即時補正の3つで切り替えて聴いてみてください。 うまく表示されないときは、デモを別タブで開く 。 ...

公開: 2026年7月24日 · Toshihiko Arai
ヒープ・Metaspace・スレッド数・開いたままのファイル数という4つの上限を、それぞれ別の壊れ方で突き破る様子を示した図

GCがあるのに、なぜTomcatは落ちるのか ── Dockerで4通りに壊して確かめた

iOSアプリを作っていると、メモリのことを嫌でも意識します。画像を何枚か抱えたまま画面を行き来していたら、ある日突然アプリが消える。クラッシュログを見ても理由がはっきりせず、循環参照を探して回る。そういう時間を過ごした人は多いはずです。 ところが同じ人がJavaでWebアプリを書くと、メモリのことをほとんど考えなくなります。Tomcatに載せて、何ヶ月も動かして、それで困らない。ガベージコレクション(以下GC)がメモリを自動で片付けてくれるからだ、と説明されればそのまま納得してしまいます。 この記事は、その納得が半分しか正しくないという話です。JavaでもメモリリークはしますしTomcatは落ちます。ただしiOSとは落ち方がまるで違い、しかも「落ちた」という言葉の意味さえ違う。最後にDockerでTomcatを立てて、実際に4通りに壊して確かめます。 何を「ゴミ」と見なすかが、そもそも違う まず前提から。iOSとJavaでは、そもそも何をゴミと判定するかの基準が違います。 iOSのARCは、その物を指している矢印が何本あるかを数えます。矢印がゼロ本になった瞬間に解放する、という単純明快な仕組みです。ところがAとBが互いを指し合うと、どちらも1本のまま永久にゼロになりません。誰も使っていないのに、いつまでも居座り続ける。これが循環参照です。しかもiOSはアプリのメモリをディスクに逃がさないので、上限を超えるとOSがアプリごと終了させます。開発中に強く意識させられるのは、こういう作りだからです。 JavaのGCは数えません。見ているのは「出発点から参照を辿って行き着けるか」だけです。出発点になるのは、static フィールドや、いま動いているスレッドが握っている変数など。そこから辿り着けないものは、互いに指し合っていようが構わず、まとめてゴミと見なされます。 だからJavaでは「解放し忘れ」型のリークは原理的に起きません。解放を書く場所そのものが無いし、循環参照も怖くない。ここまでは、GCのありがたさとして語られるとおりです。 GCが捨てるのは「使わないもの」ではない 問題は、この判定基準がもうひとつのことを意味している点です。GCは「もう使わない」を判定していません。判定しているのは辿り着けるかどうかだけです。 この2つは普段ほとんど一致するので混同しがちですが、ずれた瞬間にリークになります。「もう使わない」のに「辿り着ける」ものは、GCから見れば生きているデータです。何度回収しに来ても、大事に守って帰っていく。 代表例が static なコレクションです。 private static final Map<String, byte[]> CACHE = new HashMap<>(); 同じサムネイルを何度も作り直すのは無駄だから、と書いてしまうこの1行。static フィールドはクラスが読み込まれている限り生き続ける出発点そのものなので、上限も期限も付けなければ、入れたものは全部、アプリが動いている間ずっと守られ続けます。 同じ形のバグは他にもあります。Tomcatのワーカースレッドは使い回されるので、ThreadLocal に入れたものは明示的に消さない限り、リクエストが終わってもスレッドに貼り付いたまま残ります。HttpSession に巨大なオブジェクトを詰めれば、セッションが切れるまで消えません。どれも「参照が残っている」という一点で同じ型です。 それでも普段は問題にならない理由 では、なぜ日常的には困らないのか。理由は2つあって、どちらも「GCが優秀だから」とは少し違います。 ひとつは、Webアプリが扱うオブジェクトの大半がリクエストと一緒に生まれてレスポンスと一緒に死ぬからです。世代別GCはそういう短命なものを非常に安く回収できるように作られていて、生き残りが少ないほど仕事が楽になります。普通に書いていれば、勝手に片付いていく。 もうひとつは、サーバーが案外よく再起動されることです。デプロイのたび、設定変更のたびにプロセスが入れ替わる。じわじわ増えるタイプのリークは、顕在化する前にリセットされている。つまり気にせずにいられるのは、リークが育つ前に片付いているからでもあります。 裏を返すと、長期間動き続けるサーバーほど、そして再デプロイを繰り返すサーバーほど、この手のバグが表に出ます。 本当に落ちるのか、Dockerで確かめる ここからは実測です。公式イメージのTomcat 10.1(JDK 17)を立てて、わざとリークするサーブレットを置きます。 docker run -d --name leak-heap -p 18080:8080 \ -e CATALINA_OPTS="-Xmx64m -Xms64m -Xlog:gc" \ -v "$PWD/webapp:/usr/local/tomcat/webapps/leak" \ tomcat:10.1-jdk17-temurin ヒープを64MBに絞っているのは、実サーバーで何時間もかかることを数十秒に縮めるためです。仕組みは変わりません。 置いたサーブレットは、先ほどの上限なしキャッシュそのものです。 @WebServlet("/thumb") public class ThumbnailServlet extends HttpServlet { private static final Map<String, byte[]> CACHE = new HashMap<>(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { String id = req.getParameter("id"); byte[] thumb = CACHE.get(id); if (thumb == null) { thumb = renderThumbnail(id); // 1枚 1MB のつもり CACHE.put(id, thumb); } Runtime rt = Runtime.getRuntime(); long usedMb = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024); res.getWriter().printf("cached=%d used=%dMB max=%dMB%n", CACHE.size(), usedMb, rt.maxMemory() / (1024 * 1024)); } } 異なるIDで順に叩いていくと、27リクエスト目で応答が返らなくなりました。ログには java.lang.OutOfMemoryError: Java heap space が出ています。 ...

公開: 2026年7月23日 · Toshihiko Arai