連載『手で動かす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枚くらいは外すということでもあります。人間だって走り書きは読み違えるので、そこは似たようなものです。 学習に使ったMNISTのこと さきほど後回しにしたMNISTに触れておきます。手書き数字を集めた画像のセットで、訓練用が6万枚、答え合わせ用が別に1万枚あります。1枚は28×28の白黒画像で、それぞれに「これは3」という正解が添えてあります。元になった数字を書いたのは、アメリカの国勢調査局の職員と高校生で、1990年代に機械学習用として整理されました。 このセットが長く使われてきたのは、みんなが同じ問題を解いているからです。新しい手法を思いついた人がまずこれで試し、「うちの方法は98%当たった」と言えば、他の手法と同じ土俵で比べられる。共通のものさしとして便利なので、機械学習を始める人が最初に触れる題材にもなりました。今回のネットワークが出した95.7%も、この土俵の上での数字です。 なお、このページが持ち歩いているのは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
ピアノの鍵盤の図と、鍵盤を横軸にしたジャンプゲームの画面を並べたアイキャッチ

AIにコードを書かせて、音感トレーニングのジャンプゲームを作った ── Vibe Codingで個人開発した記録

ドミソ、と言われれば何も考えずに出てきます。ところがミソシ、と言われると止まります。頭の中で鍵盤を思い浮かべて、ミがあって、ひとつ飛ばしてソで、もうひとつ飛ばしてシ、と数えてようやくたどり着く。レファラも同じで、いちいち数えないと出てきません。 私は音楽アプリを作っていますが、ピアノをきちんと弾いてきた人間ではありません。子どもの頃から鍵盤で遊んでいた人は、たぶんこれを反射で答えます。指がその形を覚えているからで、考えていないから速い。うらやましいと思いつつ、いまさら音階練習をする気にもならない。 その、埋まらないまま放置していた穴を埋めるゲームを作りました。遊んでいるうちに、和音の形が指ではなく体に入る。そういう仕掛けのジャンプゲームです。 立体にしたら、遊びたくなってしまった きっかけは前回の記事です。音楽アプリのマスコット「えびふりゃ」を3D化した話 を書いて、その最後に、ここからゲームにするといった展望が具体的にあるわけではない、と正直に書きました。そのときは本当にそう思っていました。 ところが、立体になったものが跳ねている動画を眺めているうちに、どうしてもこれで遊びたくなってきた。翌日にはジャンプゲームを作りはじめていました。順番が逆だったら、たぶんこうはなっていません。先に「ジャンプゲームを作ろう」と考えたのではなく、跳ねている立体を見てしまったから作りたくなったわけで、この順番は自分では選べないものでした。 まずは傾けて動かす試作から ジャンプゲームと一口に言っても種類があります。横に走って穴を飛び越えるタイプ、足場を伝って上へ登っていくタイプ、決まったリズムで跳ぶタイプ。えびふりゃは丸くてぽよんと跳ねるので、足場を伝って上へ登っていく形が素直だろう、ということで方向が決まりました。 操作は、スマホを傾けて左右に動かす方式にしました。跳ねるのは自動で、プレイヤーがやることは着地する場所を選ぶことだけです。指でボタンを押すよりも、傾けた角度がそのまま横移動になるほうが、体で狙っている感じが出る。最初に組んだ試作はそこだけを確かめるためのもので、手触りは思っていたよりずっと良いものでした。 ここまでは順調でした。問題はこの先です。 音楽と結びつけようとして、最初は失敗した 作っているのは音楽アプリのマスコットが主役のゲームです。せっかくなら音楽と結びつけたい。そう考えて最初に試したのは、足場を踏むと音が鳴り、うまくつなげると得点が伸びる、というよくある作りでした。 これがまったく駄目でした。踏むたびに鳴る音と、得点が入ったときの効果音と、伴奏のような低音が混ざって、何がどうなって褒められているのか分からない。そもそもプレイヤーは「どこを狙って飛べばいいのか」が分からない。音が鳴っているだけで、音楽が遊びの一部になっていないのです。 このとき一度、音の要素を薄める方向に逃げかけました。音を減らせば手触りは整うけれど、それはもう「マスコットが跳ねるだけのゲーム」で、わざわざ音楽アプリのキャラでやる意味がない。逃げるくらいなら、音の使い方そのものを間違えていると考えるべきでした。 効いた一手 ── 画面の横軸を、そのままピアノの鍵盤にする 行き詰まりを抜けたのは、音を「演出」ではなく「地形」にしてからでした。 画面の横方向を、そのままピアノの鍵盤だと考えます。左端が低い音で、右へ行くほど高い音。マスコットが立つ足場は、その鍵盤の上に生えている板です。そして肝心なところは、足場を置く場所を「いま集めている和音の音の上」だけに限る、というルールです。ドミソを集めている場面なら、板はド・ミ・ソの真上にしか現れない。レやファの上には何もありません。 もうひとつ、真上には次の足場を作らないことにしました。これは実際に遊んでみて気づいたことです。同じ音の上に板が続いてしまうと、傾けなくても勝手に登っていけてしまう。それでは横移動する理由がなくなり、和音の並びを体で覚えるどころか、画面すら見なくなる。だから次の板は必ず別の音の上に置く。プレイヤーは「ド、その次はミ、その次はソ」と、鍵盤の上を横に渡り歩くことになります。 そこまで決めてから、和音に含まれない音の上に置いていた「引っかけ」の板も全部消しました。残っているのは常に正解の音だけです。罠を減らすと難易度が下がって間延びしそうなものですが、実際は逆でした。正解しか置かないからこそ、ド・ミ・ソという三つの位置関係が繰り返し体に入る。踏んだ板には、その音が和音の何番目かも小さく表示しています。根っこの音がR、三番目が3、五番目が5。 そしてステージが上がるごとに、集める和音がひとつずつずれていきます。ドミソの次はレファラ、その次がミソシ、ファラド、ソシレ。まさに私が咄嗟に出てこない並びを、順番に踏まされるわけです。自分で自分に課した練習台のような形になりました。 音は、木琴のような柔らかい音色にしました。板を踏むと、その音が根っこの音と重なって鳴る。和音がそろった瞬間には音がまとめて鳴ります。仕組みとしては単純ですが、ばらばらに踏んだ音が最後に和音として鳴る、という順番になっているのが大事なところで、音が答え合わせの役をしてくれます。 何を競うゲームなのか ── 関所と、その鍵 ここでもうひとつ壁がありました。和音を集めると言っても、ド・ミ・ド・ミと二つだけ踏み続けても登れてしまう。それでは何を競うゲームなのか分かりません。 そこで、ステージの境目に関所を置きました。木の柵で行く手が塞がっていて、その和音の音を全部そろえるまで通れない。二つしか踏んでいなければ弾き返されます。ステージが区切られると背景の色も少しずつ変わっていって、登っている実感が出るようになりました。 さらにもう一段、関所には鍵をかけました。和音がそろっても門は自動では開きません。そろえた瞬間だけ普段より高く跳べるようになり、その高跳びでぎりぎり届く位置に金色の鍵が浮かびます。通常のジャンプでは絶対に届かない高さで、横にもずらしてあるので、まっすぐ上がるだけでは取れない。鍵を取ったら、今度は門の鍵穴に自分で当てにいきます。うまく差し込めると錠前が跳ね上がり、柵が左右に割れて飛んでいく。 「和音をそろえる」「鍵を取る」「鍵穴に差す」という三段構えにしたことで、ステージの切り替わりが行為としてはっきりしました。点数が増えていくよりも、自分の手で開けたという手応えのほうが、区切りとしてずっと強い。 3Dで作ったのに、2Dのゲームに見えない問題 見た目のほうでも、途中で大きく作り直した箇所があります。 最初にBlenderで作った画面のモックはかなり気に入っていたのに、実際に動くゲームはそれと似ても似つかないものになっていました。原因は単純で、ブラウザで動かすために形を簡略化しすぎていたのです。せっかく立体にしたマスコットが、ただの丸い絵に戻ってしまっていました。 そこで作り方を変えて、Blenderで丁寧にレンダリングした絵を透過画像として書き出し、ゲームはその絵を並べるだけにしました。マスコットは三つの姿勢と五つの向き、木の板は音ごと、関所も鍵も巨人も、全部あらかじめレンダリングしておきます。動かす側は軽い2Dの処理しかしていないのに、画面に出ているものは3Dの質感そのままという格好です。昔のゲーム機で、あらかじめレンダリングした絵を使って立体的に見せていたのと同じ発想で、いま同じことをやるとこんなに素直に効くのか、というのは作っていて楽しい発見でした。 カメラの動かし方でも一度失敗しています。3Dらしさを出そうとして、マスコットが動くたびに寄ったり引いたりさせたら、画面が落ち着かずゲームとして成立しなくなりました。いまは基本の画角を固定して、和音がそろった瞬間と落ちはじめた瞬間だけ、ぐっと寄るようにしています。演出は常時かけるものではなく、ここぞという瞬間に取っておくものでした。 だんだん複雑になっていく 同じ和音を延々と繰り返していると、当然ながら飽きます。そこで全体を四つの段階に分けて、集める音が三つ、四つ、五つと増えていくようにしました。途中で調も変わるので、それまで白い鍵盤しか出てこなかったところに黒い鍵盤(♯の音)が混ざりはじめます。 背景は、草地から始まって、空、雲の上、成層圏と上がっていき、最後は宇宙です。星がだんだん見えてきて、最終ステージを抜けると宇宙に到達してクリア。全部で28ステージあります。 上に行くほど手強くなるのは音のほうも同じで、最初の三和音は素直な形をしていますが、四つ五つと重なると鍵盤の上での間隔がぐっと広がります。指が届かない、ではなく、傾けて渡りきれない、という形で難しさが出るのが面白いところでした。 邪魔をしにくる巨人 そこそこ登ると、画面の横から巨人がのっそり出てきます。えびふりゃを食べようとしている人間で、フォークを構えて横から突いてきます。 この巨人がいちばん手こずりました。最初は顔だけが上から降りてくる形にしていたのですが、それだと避けようがなくて理不尽なだけ。体を付けて、横からゆっくり出てくる形に作り直しました。それでも当たり判定がちぐはぐで、体に触れてもすり抜けるのに口に入ると即死する、という納得のいかない状態が続きました。 最終的には、当たり判定を目分量で置くのをやめて、レンダリングした絵そのものから輪郭を読み取って作るようにしました。そのうえでルールを整理して、体と腕は硬い壁として押し返され、危ないのは突き出しているフォークの先と、開いた口の中だけ。頭のてっぺんは踏めるようにして、踏まれた巨人はのけぞって画面の外へ倒れていきます。下から腹を突き上げれば体力ゲージが減って、三発で倒せる。 避けるだけの障害物にせず、こちらから倒せる相手にしたのには理由があって、大きな体が足場の前を塞いでしまうと、どうやっても先に進めない場面が生まれてしまうからです。踏めば消える、突き上げれば削れる、という逃げ道を用意しておけば、その心配がなくなります。 速くなったのは、手を動かす部分だけだった このゲームは、コードのほとんどをAIに書かせています。近ごろ Vibe Coding と呼ばれる作り方で、前回の3D化と同じく、私は「こうしてほしい」「それは違う」と言い続けていただけです。実装の速さでいえば、もう人間の出番はほとんどありません。 ただ、丸ごと頼めば同じものが出てくるかというと、それは違いました。素直に頼んだときにAIが持ってきたのは、踏むと音が鳴って得点が入る、あのつまらない案のほうだったのです。上から顔だけ降ってくる理不尽な巨人も、動くたびに寄って画面を壊すカメラも、最初はそういう案が出てきて、私が違うと言って捨てています。 だから、人間の役割は選別だ、という言い方はかなり当たっていると思います。AIがあれこれ出してきて、人間が通す・通さないを決める。ただ、それだけでもない気がしていて、画面の横軸を鍵盤にするという一手は、出てきた候補から選んだものではありませんでした。あれは選別ではなく持ち込みです。 もう少し正確に言うなら、人間が持ち込んでいるのは判断基準のほうなのだと思います。何を良しとし、何を却下するかの物差し。物差しがあるから捨てられるし、ときにはその物差し自体が新しい案を生む。選別しているように見えるのは、その物差しが外から見えたときの姿にすぎません。 そしてその物差しがどこから来たかというと、経験からです。ミソシが咄嗟に出てこないという体感がなければ、あの一手は思いつかなかった。えびふりゃを長いあいだ描いてきていなければ、主役がいなかった。前回わざわざ立体にしていなければ、遊びたいとすら思わなかった。手を動かす部分が一瞬になった一方で、こちらは相変わらず一瞬になっていません。 自分ひとりの痒みのために作れる時代 このゲームは、お金の話でいえば筋の悪い企画です。ミソシを覚えたい人がどれだけいるのか、私にも分かりません。 大きな予算をかけて作るなら、投資を回収しなければならないので、どうしても広い客層に向けて作ることになります。誰にとっても分かりやすいものが残り、私ひとりの半端な痒みに合わせたゲームは、そもそも企画として成立しない。これは誰が悪いという話ではなく、単に採算の構造がそうなっているというだけのことです。 ...

公開: 2026年7月23日 · Toshihiko Arai
連載『手で動かすAI』第3回のカバー。入力の数字が、重み付き和z・活性化という部品の鎖を左から右へ流れて予測yになり、途中の値がメモに残る計算グラフのイラスト

ネットワークの中は、数字が流れる鎖 ── 前向き計算

ここまでで、ニューロン1個(第1回 )を重ねてネットワークにすれば、1本の線では割れない模様も分けられる(第2回 )ところまで来ました。そのたびに「自動で学習する」を押して結果を眺めてきましたが、よく考えると、ネットワークの中で数字が実際にどう動いているのかを、まだ一度も手で追っていません。この回では、その中身を一歩ずつ追いかけます。地味に見えますが、ここを飛ばすと次回がまるごと宙に浮くので、土台としてきちんと踏みます。 ネットワークは、単純な部品の鎖 構えるような話ではありません。ニューラルネットワークの計算は、ごく単純な部品をいくつもつないだ鎖に、数字を通していくだけです。部品の種類も3つしかありません。何かを掛ける(× で重みをかける)、足す(+ でバイアスや和をとる)、そして曲げる(活性化関数に通す)。この3つの組み合わせだけで、ネットワーク全体ができています。 使うのは、新しく何かを足した鎖ではなく、第1回 のニューロン1個です。入力を2つ——果物の大きさ $x_1$ と甘さ $x_2$——受け取り、それぞれに重みを掛けて足し、バイアスを加えて重み付き和 $z$ をつくり、最後に第1回で出てきた活性化に通して予測 $y$ を出す。それだけの鎖です。具体的な数を入れてみましょう。大きさ $x_1 = 2$、甘さ $x_2 = 1$、重みを $w_1 = 0.5$、$w_2 = 1$、バイアス $b = -1$ とします。 左から順に追います。まず重み付き和をつくります。大きさ $x_1 = 2$ に $w_1 = 0.5$ を掛けて $1$、甘さ $x_2 = 1$ に $w_2 = 1$ を掛けて $1$、この2つを足してバイアス $b = -1$ を加えると、$z = 1 + 1 - 1 = 1$ です。次にこの $z$ を、第1回 で出てきた活性化に通します。ここでは第1回のデモで使ったシグモイド、どんな数も0から1のあいだへなめらかに押し込む関数を使います。$z = 1$ を通すと、予測は $y = 0.73$。この果物は「0.73くらいりんごらしい」という判定です。 ...

公開: 2026年7月23日 · 更新: 2026年7月24日 · Toshihiko Arai
テンポ2倍おきに並んだ刻みの層が釣鐘型の音量の山の上を滑り、速すぎる端で消えて遅すぎる端から生まれ直すことで無限に加速し続けるビートになることを示す図

無限に速くなり続けるビート ── リセ・リズム

前回のシェパードトーン は、無限に上がり続けるのにいつまでも高くならない音階でした。あの記事の終わりに、切れ目なく滑る連続版はフランスの作曲家ジャン=クロード・リセによる拡張だと書きましたが、リセはさらに一歩進んだ人で、同じからくりを音の高さではなくリズムに移植してしまいました。どこまでも加速し続けているのに、いつまで経っても速くならないビート。リセ・リズムと呼ばれる錯覚で、この連載の「耳のバグ」の棚では、差音 ・ミッシング・ファンダメンタル ・シェパードトーンに続く4本目になります。 テンポにも「オクターブ」がある シェパードトーンの仕掛けを思い出すと、鍵は1オクターブ、つまり周波数2倍の関係にありました。周波数が2倍の音は、耳には「同じ音名の高いド」に聞こえる。この「2倍したら同じ仲間」という性質があったから、成分をこっそり入れ替えても気づかれなかったわけです。 実は、テンポにもまったく同じ性質があります。120BPMの曲に手拍子を打つとき、1拍ごとに打つ人もいれば、2拍に1回でゆったり打つ人も、倍の細かさで刻む人もいます。どれも曲に合っている。テンポ60も120も240も、互いに「同じビートの粗い刻み・細かい刻み」として自然に重なれるのです。周波数の2倍がオクターブなら、テンポの2倍は倍テンポ。リセ・リズムは、この対応関係を使ってシェパードトーンの図式を丸ごとリズムに輸入します。 山は動かさず、刻みの層が山を滑っていく 作り方はこうです。同じ刻みを、テンポ2倍おきに並べた何層も用意します。このデモでは30BPMから960BPMまで、2倍ずつの5層です。そして横軸をテンポ(対数)にとった釣鐘型の音量の山を固定しておき、全層を同じ速さで「速くなる方向」へ滑らせ続けます。 ちょうどよいテンポ帯の真ん中にいる層がいちばん大きく鳴り、速くなりすぎた層は山を下って小さくなりながら右端の無音で消える。同時に左端では、うんと遅い層が無音のまま生まれて、山を登りはじめる。全層がちょうどテンポ2倍まで加速しきった瞬間、層の配置は出発時とぴったり同じに戻ります。1層1層は確かに2倍速くなったのに、全体としては振り出しに戻っている。どの瞬間を切り取っても全層が本当に加速しているので、耳は「速くなっている」と正しく感じ続け、それでいて、いつまで経っても速くはならない。シェパードトーン で音の高さに起きていたことが、そっくりそのままテンポに起きます。 音階版にはなかった、ひとつの難所 ただし、リズムならではの落とし穴がひとつあります。音階版では、各成分は伸ばしっぱなしのサイン波でした。ところがリズム版の層は「刻み」なので、層どうしの拍のタイミングが揃っていないと、パラパラとずれた二重打ち(フラム)が聞こえて、その瞬間に層の存在がバレてしまいます。倍テンポの層たちが自然に混ざれるのは、速い層の刻みのちょうど2発に1発が、遅い層の刻みとぴったり重なっているときだけです。 そこで実装では、層ごとにバラバラのメトロノームを持たせるのではなく、全層の刻みをたった1本の主クロックから導きます。主クロックの位相を $\varphi$ とすると、各層の位相はその $2^k$ 倍($k$ は整数)。どの層の刻みも同じ $\varphi$ が整数に届く瞬間の親戚なので、拍の入れ子は数学的に崩れようがありません。層が端で生まれ直すときも、倍率 $2^k$ を掛け替えるだけなので、揃いはそのまま保たれます。 触ってみる デモを用意しました。釣鐘の山の上を5つの層が滑り続け、右端で消えた層が左端から生まれ直します。下のタイムラインでは、実際に鳴った刻みが1本1本の縦線で流れていき、細かい縦線(速い層)が薄れて消え、まばらな縦線(遅い層)が濃く育ってくるのが見えます。 うまく表示されないときは、デモを別タブで開く 。 おすすめは、鳴らしっぱなしにして「積み上げた加速」の数字を眺めることです。×100、×1000と計算上の加速は積み上がっていくのに、聞こえるビートはずっと同じ速さの帯にとどまります。向きを切り替えると、今度は永遠に遅くなり続けるビートになります。こちらは巨大な機械がどこまでも止まりきらずに減速し続けるようで、また違った不気味さがあります。 この手のエンドレスな加速は、クラブ音楽やDJのビルドアップ、つまり盛り上がりへ向けてテンポ感を煽り続ける場面と相性がよく、電子音楽の制作では実際にリセ・リズムを組み込んだトラックやツールが作られています。リセ自身は1960年代に計算機でこの種のリズムを合成しており、シェパードの音階とあわせて「終わりのない変化」の一族を築きました。 どう実現するか デモの中身は、シェパードトーンのコードの周波数をテンポに読み替えたものです。層 $i$ の位置 $u$(0〜$N$)に対して $$\text{tempo} = T_{\min} \cdot 2^{u}, \qquad a(u) = \tfrac{1}{2}\left(1 - \cos\tfrac{2\pi u}{N}\right)$$で、テンポと音量が決まります。$a(u)$ は前回と同じ釣鐘で、両端でちょうどゼロ。ここまでは丸写しです。 リズム版で増えるのは、先ほどの主クロックです。刻みを鳴らすタイミングは、主クロックの位相 $\varphi$ を使って「層 $i$ の位相 $m_i\varphi$ が次の整数に届く時刻」を求めて予約します。加速中の $\varphi$ は指数関数で伸びるので、この時刻は近似ではなく式で厳密に解けます。 const N = 5, TMIN = 30; // 30〜960BPM の5層 const bell = u => 0.5 * (1 - Math.cos(2 * Math.PI * u / N)); // 層 i の刻み予約: 位相 m[i]·φ が整数 n に届く時刻を解いて鳴らす const phiNeeded = n / m[i]; const dt = Math.log2(1 + v * Math.LN2 * (phiNeeded - phi) / rate) / v; scheduleTick(now + dt, bell(u) * 2 / N); // v=倍加/秒, rate=現在の主クロック速度 // 端をまたいだ層は倍率を 2^N だけ掛け替えて反対側へ(音量0の瞬間なので無音) if (u >= N) { m[i] /= 2 ** N; } 実装上の勘どころもひとつ。位置 pos は無限に増え続けるので、$2^{\text{pos}}$ がいつか数値としてあふれます。そこで pos が一周($N$)を超えたら、pos から $N$ を引き、全層の倍率を $2^N$ 倍し、主クロック $\varphi$ を $2^N$ で割って帳尻を合わせます。積 $m_i\varphi$ が変わらなければ音はまったく連続のままです。実はこの $\varphi$ の割り忘れが開発中の実バグで、位相が一気に $2^N$ 倍飛んで刻みが止まりました。数値検証を書いていて拾えたバグです。 ...

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