バイナリ分類用。誰もが曲線(AUC)メトリックの下の領域を愛していますが、損失関数で直接標的にする人はいません。代わりに、人々はバイナリクロスエントロピー(BCE)などのプロキシ関数を使用します。
これはほとんどの場合、かなりうまく機能します。しかし、私たちにはしつこい質問が残っています。AUCに自然に近い損失関数を備えたより高いスコアを得ることができますか?
BCEは本当にAUCとはほとんど関係がないので、おそらくそうです。 AUCをより直接的にターゲットにする損失関数を見つけるための多くの試みがありました。 (1つの一般的な戦術は、ヒンジランクロスなどのランクロス機能の形式です。)しかし、実際には、明確な勝者は現れませんでした。 BCEには深刻な挑戦はありません。
パフォーマンス以外の考慮事項もあります。 BCEは基本的にAUCとは異なるため、BCEは最終的なトレーニングに誤解を招く傾向があり、最高のAUCスコアに向かって操縦しようとしています。
多くのAUC最適化が、実際にはハイパーパラメーターのチューニングで発生することになります。モデルがいつでも高得点から急激に分岐する可能性があるため、早期停止は不快な必要性になります。
スコアが高く、トラブルが少ない損失関数が必要です。
ここでそのような機能を提示します。
AUCの私のお気に入りの作業定義は次のとおりです。バイナリクラスラベル「Black」(0)と「White」(1)を呼びましょう。 1つの黒い要素をランダムに選択し、 xを予測値とします。次に、値yのランダムな白い要素を選択します。それから、
AUC =要素が正しい順序である確率。つまり、 x < y 。
それでおしまい。トレーニングセットのような特定のポイントのセットについて、ブルートフォース計算を行うことでこの確率を得ることができます。可能なすべての黒/白のペアのセットをスキャンし、右注文の部分をカウントします。
AUCスコアは微分可能ではないことがわかります(単一のxまたはyに対して滑らかな曲線)。要素(任意の色)を取得し、隣接する要素に触れないように少し動かします。 AUCは同じままです。ポイントが隣人を越えると、x <y比較の1つを反転させる可能性があります。これにより、AUCが変更されます。そのため、AUCはスムーズな移行を行いません。
これは、微分可能な損失関数が必要なニューラルネットの問題です。
そこで、AUCに可能な限り近い微分可能な関数を見つけることに着手しました。
私は既存の文献を掘り下げて、実際には何もうまくいきませんでした。最後に、誰かがTflearnコードベースにチェックインしたという好奇心の強いコードに出会いました。
ファンファーレがなければ、新しい損失関数の形でBCEからの微分可能な配信を約束しました。
(試してはいけません、爆発します。):http://tflearn.org/objectives/#roc-auc-score
def roc_auc_score(y_pred, y_true):
"""Bad code, do not use.
ROC AUC Score.
Approximates the Area Under Curve score, using approximation based on
the Wilcoxon-Mann-Whitney U statistic.
Yan, L., Dodier, R., Mozer, M. C., & Wolniewicz, R. (2003).
Optimizing Classifier Performance via an Approximation to the Wilcoxon-Mann-Whitney Statistic.
Measures overall performance for a full range of threshold levels.
Arguments:
y_pred: `Tensor`. Predicted values.
y_true: `Tensor` . Targets (labels), a probability distribution.
"""
with tf.name_scope("RocAucScore"):
pos = tf.boolean_mask(y_pred, tf.cast(y_true, tf.bool))
neg = tf.boolean_mask(y_pred, ~tf.cast(y_true, tf.bool))
.
.
more bad code)
まったく機能しません。 (爆発は実際にはその問題の中で最も少ない)。しかし、それはそれが基づいていた論文に言及しています。
紙は2003年にさかのぼる古代であるにもかかわらず、私は少しの作業で、数学の延長と慎重なコーディングの延長で、実際に機能することがわかりました。それはUber-Fastで、BCEに匹敵する速度(そしてGPU/MPPのためにベクトル化可能) *。私のテストでは、BCEよりもAUCスコアが高く、学習率に敏感ではなく(テストでスケジューラの必要性を回避します)、早期停止の必要性を完全に排除します。
わかりました、元の論文に目を向けましょう。Wilcoxon -Mann -Whitney Statisticに近似して分類器のパフォーマンスを最適化します。
著者、 Yan et。 AL 、AUCスコアを特定の形式で書くことで議論をやる気にさせます。可能な黒/白のペアのセットに対してブルートフォースカウントを実行して、右注文の部分を見つけることでAUCを計算する例を思い出してください。 bを黒の値のセットとし、 wの白い値のセットとします。考えられるすべてのペアは、デカルト製品B x wによって与えられます。私たちが書く右注文のペアをカウントするには:
これは、「右注文のペアを数える」と言うために、実際に数学的な表記法を緊張させるだけです。この合計をペアの総数で分割する場合、| b | * | w |、正確にAUCメトリックを取得します。 (歴史的に、これは正規化されたWilcoxon-Mann-Whitney(WMW)統計と呼ばれます。)
これから損失関数を作成するために、x <y比較をx> yに反転させて、間違った順序のペアをペナルティに罰することができます。もちろん、問題は、 xがyを交差させるときの不連続なジャンプです。
Yan et。 Al Surveys-そして拒否 - シグモイド曲線などのステップ(重い)関数の連続近似を使用して過去の作業アラウンドを拒否します。それから彼らはこれを帽子から引き出します:
Yannは、WMWに一連の変更を適用することにより、このForumulaを取得しました。
これらを順番に調べます。 1は十分に明確です。目を合わせるよりも2から2つ以上あります。幅広い分離を備えた間違った秩序化されたペアに大きな損失が与えられるべきであることは直感的に理にかなっています。しかし、その分離が0に近づくと、何か面白いことも起こります。損失はステップ機能ではなく、直線的にゼロになります。それで、私たちは不連続性を取り除きました。
実際、Pが1で、γが0の場合、損失は単に私たちの旧友のrelu(XY)になります。しかし、この場合、私たちはしゃっくりに気づき、それが指数pの必要性を明らかにします。 Reluは0で違いはありません。これは、アクティベーション関数としてのReluのより慣れた役割においてそれほど問題ではありませんが、私たちの目的のために、私たちが最も興味を持っているものに直接着地します:白と黒の要素があるポイントお互いを渡します。
幸いなことに、パワーへの関係を高めることでこれが修正されます。 p> 1のrelu^pはどこでも微分可能です。わかりました、だからp> 1。
γに戻る:γは、2つのポイントの間に施行される「パディング」を提供します。間違った秩序化されたペアだけでなく、近すぎる右注文のペアも罰せられます。右注文のペアが近すぎる場合、その要素は、確率的ニューラルネットのランダムな揺れによって将来交換されるリスクがあります。アイデアは、彼らが快適な距離に達するまで彼らをバラバラに保つことです。
そして、それは論文で概説されている基本的なアイデアです。現在、γとpに関するいくつかの改良を整えています。
ここでは、紙を少し壊します。 Yan et。 AlはγとPを選択するトピックについて少しきしむように見えます。P= 2またはP = 3は良好であり、γが0.10〜0.70の間にあることのみを提供します。ヤンは本質的に、これらのパラメーターと弓を使って私たちの運を望んでいます。
まず、自尊心のある損失関数は平方面である必要があるため、 P = 2を永久に修正します。 (この理由の1つは、損失関数が微分可能であるだけでなく、凸状であることを保証することです)
第二に、さらに重要なことは、γを見てみましょう。 「0.10〜0.70」の「どこか」のヒューリスティックは、その表面では奇妙に見えます。予測が0 <x <1であるように正規化されたとしても、このガイダンスは広すぎ、基礎となる分布に無関心であり、奇妙です。
トレーニングセットからγを導き出します。
トレーニングセットとその黒/白のペア、 B x wを考えてみましょう。 |がありますb || w |このセットのペア。これらの、| b | | w | AUCは右注文です。したがって、間違った注文ペアの数は(1-AUC)|ですb | | w |
γがゼロの場合、これらの間違った秩序化ペアのみが動いています(正の損失があります)γは移動ペアのセットを拡張して、右秩序のあるが近すぎるペアを含むいくつかのペアを含みます。 γの数値を心配するのではなく、動き始めたいペアが多すぎるペア数を指定します。
間違った秩序のペアに対してあまりにも閉鎖されたペアの割合を固定する定数δを定義します。
トレーニング中にこのδを修正し、γを更新してそれに準拠します。与えられたΔについて、そのようなγが見つかります
実験では、δは0.5〜2.0の範囲であり、1.0が適切なデフォルトの選択肢であることがわかりました。
したがって、δを1に、pに設定し、γを完全に忘れて、
私たちの損失関数(1)は、計算するのに悲惨な費用がかかるように見えます。個々の予測ごとにトレーニングセット全体をスキャンする必要があります。
この問題をパフォーマンスの調整でバイパスします。
特定のホワイトデータポイントxの損失関数を計算しているとします。 (3)を計算するには、 xを黒い予測のトレーニングセット全体と比較する必要がありますy 。短いカットを撮影し、黒いデータポイントのランダムサブサンプルを使用します。サブサンプルのサイズをたとえば1000に設定すると、真の損失関数に非常に(非常に)密接な近似値が得られます。 [1]
同様の推論は、黒いデータポイントの損失関数に適用されます。すべての白いトレーニング要素のランダムサブサンプルを使用します。
このようにして、白と黒のサブサンプルはGPUメモリに簡単に収まります。特定のバッチ全体で同じサブサンプルを再利用することにより、バッチの操作を並列化できます。 BCEでほぼ速い損失関数になります。
Pytorchのバッチロス関数は次のとおりです。
def roc_star_loss( _y_true, y_pred, gamma, _epoch_true, epoch_pred):
"""
Nearly direct loss function for AUC.
See article,
C. Reiss, "Roc-star : An objective function for ROC-AUC that actually works."
https://github.com/iridiumblue/articles/blob/master/roc_star.md
_y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
gamma : `Float` Gamma, as derived from last epoch.
_epoch_true: `Tensor`. Targets (labels) from last epoch.
epoch_pred : `Tensor`. Predicions from last epoch.
"""
#convert labels to boolean
y_true = (_y_true>=0.50)
epoch_true = (_epoch_true>=0.50)
# if batch is either all true or false return small random stub value.
if torch.sum(y_true)==0 or torch.sum(y_true) == y_true.shape[0]: return torch.sum(y_pred)*1e-8
pos = y_pred[y_true]
neg = y_pred[~y_true]
epoch_pos = epoch_pred[epoch_true]
epoch_neg = epoch_pred[~epoch_true]
# Take random subsamples of the training set, both positive and negative.
max_pos = 1000 # Max number of positive training samples
max_neg = 1000 # Max number of positive training samples
cap_pos = epoch_pos.shape[0]
cap_neg = epoch_neg.shape[0]
epoch_pos = epoch_pos[torch.rand_like(epoch_pos) < max_pos/cap_pos]
epoch_neg = epoch_neg[torch.rand_like(epoch_neg) < max_neg/cap_pos]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
# sum positive batch elements agaionst (subsampled) negative elements
if ln_pos>0 :
pos_expand = pos.view(-1,1).expand(-1,epoch_neg.shape[0]).reshape(-1)
neg_expand = epoch_neg.repeat(ln_pos)
diff2 = neg_expand - pos_expand + gamma
l2 = diff2[diff2>0]
m2 = l2 * l2
len2 = l2.shape[0]
else:
m2 = torch.tensor([0], dtype=torch.float).cuda()
len2 = 0
# Similarly, compare negative batch elements against (subsampled) positive elements
if ln_neg>0 :
pos_expand = epoch_pos.view(-1,1).expand(-1, ln_neg).reshape(-1)
neg_expand = neg.repeat(epoch_pos.shape[0])
diff3 = neg_expand - pos_expand + gamma
l3 = diff3[diff3>0]
m3 = l3*l3
len3 = l3.shape[0]
else:
m3 = torch.tensor([0], dtype=torch.float).cuda()
len3=0
if (torch.sum(m2)+torch.sum(m3))!=0 :
res2 = torch.sum(m2)/max_pos+torch.sum(m3)/max_neg
#code.interact(local=dict(globals(), **locals()))
else:
res2 = torch.sum(m2)+torch.sum(m3)
res2 = torch.where(torch.isnan(res2), torch.zeros_like(res2), res2)
return res2
追加のパラメーターがいくつかあることに注意してください。最後のエポックからトレーニングセットを渡しています。トレーニングセット全体がエポックから次のエポックに大きく変わることはないため、損失関数は各予測をわずかに古いトレーニングセットと再び比較できます。これにより、デバッグが簡素化され、「バックグラウンド」エポックがあるバッチから次のバッチに変化していないため、パフォーマンスに役立つように見えます。
同様に、γは高価な計算です。再びサブサンプリングのトリックを使用しますが、サブサンプルのサイズを約10,000に増やして、正確な推定値を確保します。パフォーマンスの切り抜きを維持するために、この値をエポックごとに1回だけ再計算します。これを行う機能は次のとおりです。
def epoch_update_gamma(y_true,y_pred, epoch=-1,delta=2):
"""
Calculate gamma from last epoch's targets and predictions.
Gamma is updated at the end of each epoch.
y_true: `Tensor`. Targets (labels). Float either 0.0 or 1.0 .
y_pred: `Tensor` . Predictions.
"""
DELTA = delta
SUB_SAMPLE_SIZE = 2000.0
pos = y_pred[y_true==1]
neg = y_pred[y_true==0] # yo pytorch, no boolean tensors or operators? Wassap?
# subsample the training set for performance
cap_pos = pos.shape[0]
cap_neg = neg.shape[0]
pos = pos[torch.rand_like(pos) < SUB_SAMPLE_SIZE/cap_pos]
neg = neg[torch.rand_like(neg) < SUB_SAMPLE_SIZE/cap_neg]
ln_pos = pos.shape[0]
ln_neg = neg.shape[0]
pos_expand = pos.view(-1,1).expand(-1,ln_neg).reshape(-1)
neg_expand = neg.repeat(ln_pos)
diff = neg_expand - pos_expand
ln_All = diff.shape[0]
Lp = diff[diff>0] # because we're taking positive diffs, we got pos and neg flipped.
ln_Lp = Lp.shape[0]-1
diff_neg = -1.0 * diff[diff<0]
diff_neg = diff_neg.sort()[0]
ln_neg = diff_neg.shape[0]-1
ln_neg = max([ln_neg, 0])
left_wing = int(ln_Lp*DELTA)
left_wing = max([0,left_wing])
left_wing = min([ln_neg,left_wing])
default_gamma=torch.tensor(0.2, dtype=torch.float).cuda()
if diff_neg.shape[0] > 0 :
gamma = diff_neg[left_wing]
else:
gamma = default_gamma # default=torch.tensor(0.2, dtype=torch.float).cuda() #zoink
L1 = diff[diff>-1.0*gamma]
ln_L1 = L1.shape[0]
if epoch > -1 :
return gamma
else :
return default_gamma
エポックでループし、次にバッチでループするときに2つの機能を使用する方法を示すヘリコプタービューは次のとおりです。
train_ds = CatDogDataset(train_files, transform)
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE)
#initialize last epoch with random values
last_epoch_y_pred = torch.tensor( 1.0-numpy.random.rand(len(train_ds))/2.0, dtype=torch.float).cuda()
last_epoch_y_t = torch.tensor([o for o in train_tt],dtype=torch.float).cuda()
epoch_gamma = 0.20
for epoch in range(epoches):
epoch_y_pred=[]
epoch_y_t=[]
for X, y in train_dl:
preds = model(X)
# .
# .
loss = roc_star_loss(y,preds,epoch_gamma, last_epoch_y_t, last_epoch_y_pred)
# .
# .
epoch_y_pred.extend(preds)
epoch_y_t.extend(y)
last_epoch_y_pred = torch.tensor(epoch_y_pred).cuda()
last_epoch_y_t = torch.tensor(epoch_y_t).cuda()
epoch_gamma = epoch_update_gamma(last_epoch_y_t, last_epoch_y_pred, epoch)
#...
完全な作業例は、ここにあります。例。
以下に、BCEを使用して同じモデルに対してRoc-Starのパフォーマンスをチャートします。経験によると、ROC-Starは、パフォーマンスが向上する可能性が高く、BCEを使用して任意のモデルに交換できることがよくあります。