外れ値フィルタが外れ値にだまされるとき
ピッチ抽出が音節ひとつを丸ごと2倍の高さで拾うと、中央値ベースの外れ値フィルタはすべて正常と判定します。 音節どうしを比べる「折りたたみ」というアプローチの追加により、技術問題を解決した記録です。
発音矯正サービスのフィードバックカードは、学習者の録音からピッチ(声の高低)を抽出して曲線として描きます。抽出にはYINというアルゴリズムを使うのですが、ある日、学生のカードで最初の音節だけが上に飛び、残りの音節がすべて底に潰れていました。
原因はオクターブエラーでした。YIN系のアルゴリズムは、声帯の振動が不安定な区間で実際の音の倍音(2倍の周波数)を基本周波数として拾ってしまうことがあります。発話の最初の音節は声帯がまだ安定していないため、特によく起こります。カードの縦の範囲は全音節の最高値と最低値で決まるので、2倍で拾われた音節がひとつ入ると、残りの正常な音節が縦に圧縮されます。
外れ値フィルタはすでにありました。各音節の中で点の中央値を求め、中央値から±40%外れた点を捨てる方式です。点がひとつ飛ぶ場合はこれで十分です。
問題は、音節の点が丸ごと2倍になった場合です。その音節の中央値そのものが2倍の値になるため、すべての点が中央値の近くに大人しく集まっているように見えます。フィルタから見れば全部正常です。外れ値判定の基準そのものが外れ値に汚染されると、そのフィルタは原理的に何も捕まえられません。
そこで、音節の中ではなく音節どうしを比べる段階を追加しました。
| 段階 | やること |
|---|---|
| 1 | 音節ごとの代表ピッチを求める (有声点の中央値) |
| 2 | 代表値の中央値を全体の基準にする (平均はエラーに引きずられる) |
| 3 | 基準の1.6倍以上または1/1.6以下の音節だけ ÷2 / ×2 を試す |
| 4 | 折りたたんだ値が基準にログ距離で近づくときだけ採用する |
4段階目が核心の安全装置です。「基準よりだいぶ高い」という理由だけで折りたたむと、正常なイントネーションの山(1.5倍まで上がります)を削ってしまいます。折りたたんだときに基準へ実際に近づく場合、つまりオクターブエラーのときだけ折りたたみます。
export function chooseFoldFactor(rep: number, reference: number): number {
const ratio = rep / reference
const candidate = (() => {
if (ratio >= FOLD_HIGH_TRIGGER) return 0.5
if (ratio <= FOLD_LOW_TRIGGER) return 2
return 1
})()
if (candidate === 1) return 1
// 折りたたんだ結果が基準に近づくときだけ採用 (イントネーションの山を削らないための条件)
if (logDistance(rep * candidate, reference) < logDistance(rep, reference)) {
return candidate
}
return 1
}しきい値1.6にも理由があります。2倍エラーは理論上は基準の2.0倍として現れますが、発話のイントネーションが重なると1.6~2.6倍の間で揺れます。1.6未満は最初の音節の上昇や強勢といった正常なイントネーションの範囲と重なるため、触りません。距離をlog2で測るのも同じ文脈です。2倍と半分を対称に扱うには、線形距離ではなくログ距離が必要です。
この折りたたみが捕まえられないケースも、コードのコメントにそのまま書いてあります。
音節の半分だけが倍音として拾われた場合は代表値が曖昧になり、折りたたみがかかりません。それは既存の±40%点単位フィルタの担当です。そして録音の過半数が間違っている場合は基準そのものが汚染されるため、矯正できません。この話者の声が本来どの高さなのかという事前知識(性別、年齢)なしには、原理的に判別不可能です。
フィルタをもうひとつ積む仕事より、各フィルタがどの汚染まで耐えるのか境界を書いておく仕事の方が長く残ります。次にカードがまたおかしくなったら、このコメントから確認すれば済みます。