前の音が次を選ぶ — マルコフ連鎖
重み付き抽選は、どの音を選ぶ確率も毎回おなじで、いま鳴っている音と次の音につながりがない。実際の旋律は、ある音の次に来やすい音と来にくい音がある。そこで「いまの音」ごとに次の重みを変える。状態(いまの音)から状態への遷移確率を、行が現在の音・列が次の音の表で持つ。行ごとに重みが違うので、同じサイコロを毎回振るのでなく、いまの音に応じてサイコロを持ち替える。
マルコフ連鎖。次の状態が「いまの状態」だけで決まり、それ以前の履歴に依らない確率過程。状態から状態への遷移確率を並べた正方行列が遷移行列で、各行の和は 1 になる。次の音が現在の音だけで決まるのが 1 次のマルコフ連鎖、直前 2 音を見るのが 2 次。本物の曲から「この音の次に来た音」を数えて行列を埋めれば、その曲の癖を真似た旋律を生やせる。Google の PageRank や、テキスト生成の n-gram モデルも同じ枠組み。
行列の一行が、いまの音に対する次の重み。trans[cur] を取り出し、その行の重みで次を抽選する。対角線の近くを重くすると隣の音へ動きやすく、遠い列を重くすると跳ねる。選んだ列がそのまま次の cur になり、また次の行を引く。
// 行 = いまの音、列 = 次の音。各行の重みで次を抽選する
// (近い音へ動きやすく、たまに跳ぶ、という性格を手で入れた例)
const trans = [
[1, 4, 2, 1, 1], // 音0 のあと: 隣の1へ行きやすい
[3, 1, 3, 1, 1],
[1, 3, 1, 3, 1],
[1, 1, 3, 1, 3],
[1, 1, 2, 4, 1], // 音4 のあと: 3へ戻りやすい
]
let cur = 0
const next = () => {
const row = trans[cur]
const total = row.reduce((a, b) => a + b, 0)
let r = Math.random() * total
for (let i = 0; i < row.length; i++) {
if (r < row[i]) return (cur = i)
r -= row[i]
}
return cur
}下の左に遷移行列、右に生えていく旋律を並べる。行列はマスが明るいほど重みが大きい。光っている行がいまの音、その行で次に当たったマスが点滅する。右のピアノロールは抽選した音を一歩ずつ左へ流す。対角線寄りのマスが明るいので、旋律は隣どうしを伝ってなめらかに動き、たまに遠いマスを引いて跳ぶ。
遷移行列の対角線近くを重くすると、いまの音から隣の音へ滑らかに動く旋律になる。遠い列を重くすると跳ねる。純粋なランダムより、前の音を覚えているぶん流れにまとまりが出る。重み付き抽選が「いつも同じサイコロ」なら、マルコフは「いまの音によって持ち替えるサイコロ」。Math.random を seeded な rng() に差し替えれば、行列とその列も再現できる。行列をどう学習・設計するかは未踏の宿題として残る。