Untuk klasifikasi biner. Semua orang menyukai area di bawah metrik Curve (AUC), tetapi tidak ada yang secara langsung menargetkannya dalam fungsi kerugian mereka. Sebaliknya orang menggunakan fungsi proxy seperti Biner Cross Entropy (BCE).
Ini bekerja dengan cukup baik, sebagian besar waktu. Tapi kita pergi dengan pertanyaan yang mengganggu: bisakah kita mendapatkan skor yang lebih tinggi dengan fungsi kerugian lebih dekat dengan AUC?
Tampaknya karena BCE benar -benar memiliki hubungan yang sangat sedikit dengan AUC. Ada banyak upaya untuk menemukan fungsi kerugian yang lebih langsung menargetkan AUC. ; Tidak ada tantangan serius untuk SM.
Ada juga pertimbangan di luar kinerja. Karena BCE pada dasarnya berbeda dari AUC, BCE cenderung berperilaku buruk di hamparan pelatihan terakhir di mana kami mencoba mengarahkannya ke skor AUC tertinggi.
Banyak optimasi AUC sebenarnya terjadi dalam penyetelan hiper-parameter. Perhentian awal menjadi kebutuhan yang tidak nyaman karena model dapat menyimpang dengan tajam kapan saja dari skor tinggi.
Kami ingin fungsi kerugian yang memberi kami skor yang lebih tinggi dan lebih sedikit masalah.
Kami menyajikan fungsi seperti itu di sini.
Definisi kerja favorit saya tentang AUC adalah ini: Mari kita sebut label kelas biner "hitam" (0) dan "putih" (1). Pilih satu elemen hitam secara acak dan biarkan x menjadi nilai yang diprediksi. Sekarang pilih elemen putih acak dengan nilai y . Kemudian,
AUC = probabilitas bahwa elemen berada dalam urutan yang benar. Yaitu, x < y .
Itu saja. Untuk setiap set poin yang diberikan seperti set pelatihan, kami bisa mendapatkan probabilitas ini dengan melakukan perhitungan brute-force. Pindai set semua pasangan hitam/putih yang mungkin, dan hitung porsi yang dipesan kanan.
Kita dapat melihat bahwa skor AUC tidak dapat dibedakan (kurva halus sehubungan dengan x atau y tunggal.) Ambil elemen (warna apa pun) dan gerakkan sedikit sehingga tidak mengenai elemen tetangga. AUC tetap sama. Setelah titik itu melintasi tetangga, kami memiliki kesempatan untuk membalik salah satu perbandingan x <y - yang mengubah AUC. Jadi AUC tidak membuat transisi yang mulus.
Itu masalah untuk jaring saraf, di mana kita membutuhkan fungsi kerugian yang dapat dibedakan.
Jadi kami berangkat untuk menemukan fungsi yang dapat dibedakan yang mungkin sedekat mungkin dengan AUC.
Saya menggali literatur yang ada dan tidak menemukan apa pun yang berhasil dalam praktik. Akhirnya saya menemukan sepotong kode penasaran yang telah diperiksa seseorang ke basis kode TFLEARN.
Tanpa keriuhan, itu menjanjikan pembebasan yang dapat dibedakan dari SM dalam bentuk fungsi kerugian baru.
( Jangan mencobanya, itu meledak .): 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)
Itu tidak berhasil sama sekali. (Meledakkan sebenarnya adalah yang paling sedikit dari masalahnya). Tapi itu menyebutkan makalah itu berdasarkan.
Meskipun makalahnya kuno , sejak tahun 2003, saya menemukan bahwa dengan sedikit pekerjaan - beberapa perpanjangan matematika dan pengkodean yang cermat - itu benar -benar berfungsi. Ini sangat cepat, dengan kecepatan yang sebanding dengan BCE (dan sama seperti yang dapat di vektor untuk GPU/MPP) *. Dalam tes saya, ini memberikan skor AUC yang lebih tinggi daripada BCE, kurang sensitif terhadap tingkat pembelajaran (menghindari kebutuhan akan penjadwal dalam tes saya), dan menghilangkan sepenuhnya kebutuhan untuk berhenti lebih awal.
OK, mari kita beralih ke kertas asli: Mengoptimalkan kinerja classifier melalui perkiraan ke Wilcoxon - Mann - Whitney Statistic .
Penulis, Yan et. Al , memotivasi diskusi dengan menulis skor AUC dalam bentuk tertentu. Ingatlah contoh kami di mana kami menghitung AUC dengan melakukan jumlah brute-force atas seperangkat kemungkinan pasangan hitam/putih untuk menemukan porsi yang dipesan kanan. Misalkan b menjadi himpunan nilai hitam dan set nilai putih. Semua pasangan yang mungkin diberikan oleh produk Cartesian b x w . Untuk menghitung pasangan yang dipesan dengan benar yang kami tulis:
Ini benar-benar hanya membuat notasi matematika untuk mengatakan 'Hitung pasangan yang dipesan kanan.' Jika kita membagi jumlah ini dengan jumlah total pasangan, | B | * | W |, kami mendapatkan metrik AUC dengan tepat. (Secara historis, ini disebut statistik Wilcoxon-Mann-Whitney (WMW) yang dinormalisasi.)
Untuk membuat fungsi kerugian dari ini, kita bisa membalikkan perbandingan x <y> y untuk menghukum pasangan yang dipesan salah. Masalahnya, tentu saja, adalah lompatan yang terputus -putus ketika x melintasi y.
Yan ET. Al survei - dan kemudian menolak - di masa lalu kerja menggunakan perkiraan kontinu untuk fungsi langkah (Heaviside), seperti kurva sigmoid. Kemudian mereka mengeluarkan ini dari topi:
Yann mendapatkan forumula ini dengan menerapkan serangkaian perubahan pada WMW:
Kami akan melalui ini secara bergantian. 1 cukup jelas. Ada sedikit lebih dari 2 daripada memenuhi mata. Masuk akal secara intuitif bahwa pasangan yang dipesan salah dengan pemisahan luas harus diberikan kerugian yang lebih besar. Tetapi sesuatu yang menarik juga terjadi ketika pemisahan itu mendekat 0. Kehilangannya nol secara linier, daripada fungsi langkah. Jadi kami sudah menyingkirkan diskontinuitas.
Faktanya, jika P adalah 1 dan γ adalah 0, kerugian itu hanya akan menjadi teman lama kami Relu (XY). Tetapi dalam hal ini kami melihat cegukan, yang mengungkapkan kebutuhan akan eksponen p . Relu tidak dapat dibedakan pada 0. Itu bukan masalah dalam peran Relu yang lebih terbiasa sebagai fungsi aktivasi, tetapi untuk tujuan kita singularitas di 0 mendarat langsung pada hal yang paling kita minati: titik -titik di mana elemen putih dan hitam melewati satu sama lain.
Untungnya, menaikkan Relu ke daya memperbaiki ini. Relu^p dengan p> 1 dapat dibedakan di mana -mana. Oke, jadi p> 1.
Sekarang kembali ke γ: γ menyediakan 'padding' yang ditegakkan di antara dua titik. Kami menghukum tidak hanya pasangan yang dipesan salah, tetapi juga pasangan yang dipesan kanan yang terlalu dekat . Jika pasangan yang memesan benar terlalu dekat, elemen-elemennya berisiko ditukar di masa depan dengan goncangan acak dari jaring saraf stokastik. Idenya adalah untuk membuat mereka tetap terpisah sampai mencapai jarak yang nyaman.
Dan itulah ide dasar yang diuraikan dalam makalah ini. Kami sekarang mengumpulkan beberapa penyempurnaan tentang γ dan p.
Di sini kami sedikit istirahat dengan kertas. Yan ET. Al tampaknya sedikit mual pada topik memilih γ dan P, hanya menawarkan bahwa p = 2 atau p = 3 tampaknya baik dan bahwa γ harus berada di suatu tempat antara 0,10 dan 0,70. Yan pada dasarnya berharap kami beruntung dengan parameter dan busur ini.
Pertama, kami secara permanen memperbaiki P = 2, karena fungsi kerugian yang menghargai diri sendiri harus menjadi jumlah persegi. (Salah satu alasan untuk ini adalah bahwa ia memastikan fungsi kerugian tidak hanya dapat dibedakan, tetapi juga cembung )
Kedua dan yang lebih penting, mari kita lihat γ. Heuristik 'di suatu tempat dari 0,10 hingga 0,70' terlihat aneh di wajahnya; Bahkan jika prediksi dinormalisasi menjadi 0 <x <1, panduan ini tampaknya overbroad, acuh tak acuh terhadap distribusi yang mendasarinya, dan hanya aneh.
Kami akan mendapatkan γ dari set pelatihan.
Pertimbangkan set pelatihan dan pasangan hitam/putihnya, b x w . Ada | B || W | berpasangan di set ini. Dari ini, | B | | W | AUC memesan kanan. Jadi, jumlah pasangan yang dipesan salah adalah (1-AUC) | B | | W |
Ketika γ adalah nol, hanya pasangan yang salah yang bergerak ini (memiliki kerugian positif.) Sebuah γ positif akan memperluas himpunan pasangan bergerak untuk memasukkan beberapa pasangan yang dipesan kanan, tetapi terlalu dekat. Alih-alih mengkhawatirkan nilai numerik γ, kami akan menentukan berapa banyak pasangan yang terlalu dekat yang ingin kami lakukan:
Kami mendefinisikan δ konstan yang memperbaiki proporsi pasangan yang terlalu dekat untuk pasangan yang salah.
Kami memperbaiki δ ini selama pelatihan dan memperbarui γ agar sesuai dengannya. Untuk δ yang diberikan, kami menemukan γ sedemikian rupa
Dalam percobaan kami, kami menemukan bahwa Δ dapat berkisar dari 0,5 hingga 2.0, dan 1.0 adalah pilihan default yang baik.
Jadi kami mengatur Δ ke 1, p ke 2, dan melupakan γ sama sekali,
Fungsi kerugian kami (1) terlihat sangat mahal untuk dihitung. Ini mengharuskan kami memindai seluruh pelatihan yang ditetapkan untuk setiap prediksi individu.
Kami melewati masalah ini dengan tweak kinerja:
Misalkan kita menghitung fungsi kerugian untuk titik data putih yang diberikan, x . Untuk menghitung (3), kita perlu membandingkan x dengan seluruh rangkaian pelatihan prediksi hitam, y . Kami mengambil jalan pintas dan menggunakan sub-sampel acak dari titik data hitam. Jika kita mengatur ukuran sub -sampel menjadi, katakanlah, 1000 - kita mendapatkan perkiraan yang sangat (sangat) dengan fungsi kerugian yang sebenarnya. [1]
Alasan serupa berlaku untuk fungsi kerugian dari titik data hitam; Kami menggunakan sub-sampel acak dari semua elemen pelatihan putih.
Dengan cara ini, subsampel putih dan hitam mudah masuk ke dalam memori GPU. Dengan menggunakan kembali sub-sampel yang sama di seluruh batch yang diberikan, kita dapat memparalelkan operasi dalam batch. Kami berakhir dengan fungsi kerugian yang secepat di SM.
Inilah fungsi kalah batch di 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
Perhatikan bahwa ada beberapa parameter tambahan. Kami lulus dalam set pelatihan dari zaman terakhir . Karena seluruh set pelatihan tidak banyak berubah dari satu zaman ke zaman berikutnya, fungsi kerugian dapat membandingkan setiap prediksi lagi set pelatihan yang sedikit ketinggalan zaman. Ini menyederhanakan debugging, dan tampaknya menguntungkan kinerja karena zaman 'latar belakang' tidak berubah dari satu batch ke yang berikutnya.
Demikian pula, γ adalah perhitungan yang mahal. Kami kembali menggunakan trik sub-sampling, tetapi meningkatkan ukuran sub-sampel menjadi ~ 10.000 untuk memastikan perkiraan yang akurat. Untuk menjaga kliping kinerja, kami mengkomputasi kembali nilai ini hanya sekali per zaman. Inilah fungsi untuk melakukan itu:
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
Inilah tampilan helikopter yang menunjukkan cara menggunakan dua fungsi saat kita melingkar pada zaman, lalu pada batch:
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)
#...
Contoh kerja lengkap dapat ditemukan di sini, example.py untuk jump-star yang lebih cepat Anda dapat membayar kernel ini di kaggle: kernel
Di bawah ini kami memetakan kinerja ROC-Star terhadap model yang sama menggunakan BCE. Pengalaman menunjukkan bahwa ROC-Star seringkali dapat ditukar dengan model apa pun menggunakan BCE dengan peluang bagus pada peningkatan kinerja.