pythonでf値を計算したい!
でもOnset Detectionでは少し勝手が違うんでしょ?
今回は,Onset Detection用のF値をpythonで算出する方法をお伝えしていこうと思います。また,本記事はpython実践講座シリーズの内容になります。その他の記事は,こちらの「Python入門講座/実践講座まとめ」をご覧ください。
お題
pythonでOnset Detection用のF値を算出してみよう。
流れ
F値の基本的な説明は,こちらの記事(「【超初心者向け】F値のくどい解説とPythonでの実装例。」)をご覧ください。Onset Detectionというのは,音楽音響信号などから所望の信号がなっている時刻を推定するタスクです。例えば,市販のCD楽曲からスネアドラムが鳴っている時刻を抽出するようなタスクです。
結論から言うと,Onset DetectionではTNは定義されないことが多いです。なぜなら,信号のOnsetはフレーム長に対してスパースであるからです。STFT(短時間フーリエ変換)を施した特徴量で,例えば1フレームが10msになるようにサンプリングレートとホップ長さを調整したとすれば,3分の楽曲に対して特徴量は18000フレームという長さになります。
ですので,推定結果はほどんど「0」という状況になります。これが,Onset DetectionでTNが定義されないことが多い所以です。
図解
\begin{eqnarray}
\rm{R} &=& \frac{\rm{TP}}{\rm{TP} + \rm{FN}}\\
\rm{P} &=& \frac{\rm{TP}}{\rm{TP} + \rm{FP}}\\
\rm{F} &=& \frac{2\cdot \rm{TP}}{2\cdot \rm{TP} + \rm{FP} + \rm{FN}}
\end{eqnarray}
もしくは,F値は以下のようにしても算出できます。
\begin{eqnarray}
\rm{F} &=& \frac{2\cdot\rm{P}\cdot\rm{R}}{\rm{P} + \rm{R}}
\end{eqnarray}
ソースコード
関数部分だけを抜粋しています。入力はndarrayのバイナリとします。windowは許容誤差です。許容誤差はInputの単位に合わせる必要があります。たとえば,1フレームが10msのInputを与えたとすれば,window=5で50msの許容誤差となります。
import numpy as np
import copy
def fmeasure_drum(pred, truth, window):
'''
Input: ndarray
Output: precision, recall, f-measure
pred: detected onset binary
truth: ground truth binary
window: torelance window
'''
TP = 0
FP = 0
FN = 0
# truthを書き換えるためdeepcopyしておく
truth_copy = copy.deepcopy(truth)
# 推定結果からonsetのインデックスを抽出
pred_index = np.where(pred==1)[0]
# onsetの数だけ繰り返します
for i in range(pred_index.shape[0]):
# 端っこは例外処理
if pred_index[i] < window:
# 推定されたonsetの前後windowフレーム分を見て推定結果が正しいかどうかを判断する
scope_index = np.where(truth_copy[0:pred_index[i]+window+1]==1)[0]
if scope_index.shape[0] != 0:
TP += 1
truth_copy[0:pred_index[i]+window+1][scope_index[0]] = 0
else:
FP += 1
# 端っこは例外処理
elif pred_index[i] > pred.shape[0] - 1 - window:
scope_index = np.where(truth_copy[pred_index[i]-window:]==1)[0]
if scope_index.shape[0] != 0:
TP += 1
truth_copy[pred_index[i]-window:][scope_index[0]] = 0
else:
FP += 1
# 端っこ以外の処理
else:
scope_index = np.where(truth_copy[pred_index[i]-window:pred_index[i]+window+1]==1)[0]
if scope_index.shape[0] != 0:
TP += 1
truth_copy[pred_index[i]-window:pred_index[i]+window+1][scope_index[0]] = 0
else:
FP += 1
# ここまででTPとFPの数え上げは終了。あとはTruthを見てFNを数えるだけ。
truth_remain = np.where(truth_copy==1)[0]
if truth_remain.shape[0] != 0:
FN += truth_remain.shape[0]
# 一応TPがゼロの場合に備える。多分必要ない。
if TP==0:
TP += 1e-5
print("ゼロ除算を防止するために微小量を加えました")
precision = (TP / (TP + FP))
recall = (TP / (TP + FN))
fmeasure = ((2 * precision * recall) / (precision + recall))
return precision, recall, fmeasure, TP, FP, FN