Für binäre Klassifizierung. Jeder liebt den Bereich unter der Kurve (AUC), aber niemand richtet sich direkt an ihn in seiner Verlustfunktion. Stattdessen verwenden Leute eine Proxyfunktion wie binäre Kreuzentropie (BCE).
Dies funktioniert die meiste Zeit ziemlich gut. Aber wir haben eine quälende Frage: Könnten wir mit einer Verlustfunktion in der Natur zu AUC eine höhere Punktzahl erzielen?
Es scheint wahrscheinlich, dass BCE wirklich nur sehr wenig mit AUC in Beziehung steht. Es gab viele Versuche, eine Verlustfunktion zu finden, die direkter auf AUC abzielt. (Eine gemeinsame Taktik ist eine Form der Rangverlustfunktion wie dem Scharnier-Rang-Verlust.) In der Praxis ist jedoch noch nie ein klarer Gewinner aufgetaucht. BCE gab es keine ernsthafte Herausforderung.
Es gibt auch Überlegungen, die über die Leistung hinausgehen. Da sich BCE im Wesentlichen von AUC unterscheidet, benimmt sich BCE im letzten Trainingsabschnitt, wo wir versuchen, es in Richtung des höchsten AUC -Scores zu lenken.
Ein Großteil der AUC-Optimierung tritt tatsächlich bei der Abstimmung von Hyperparametern auf. Das frühzeitige Stoppen wird zu einer unangenehmen Notwendigkeit, da das Modell jederzeit stark von seiner hohen Punktzahl abweichen kann.
Wir möchten eine Verlustfunktion, die uns höhere Punktzahlen und weniger Probleme gibt.
Wir präsentieren hier eine solche Funktion.
Meine Lieblingsarbeitsdefinition von AUC ist: Wählen Sie ein schwarzes Element zufällig und lassen Sie X sein vorhergesagter Wert sein. Wählen Sie nun ein zufälliges weißes Element mit Wert y . Dann,
AUC = die Wahrscheinlichkeit, dass die Elemente in der richtigen Reihenfolge sind. Das heißt x < y .
Das war's. Für alle Punkte wie das Trainingssatz können wir diese Wahrscheinlichkeit durch eine Brute-Force-Berechnung erhalten. Scannen Sie den Satz aller möglichen schwarzen/weißen Paare und zählen Sie den Teil, der rechts geordnet ist.
Wir können sehen, dass der AUC -Score nicht differenzierbar ist (eine glatte Kurve in Bezug auf ein einzelnes x oder y . Die AUC bleibt gleich. Sobald der Punkt einen Nachbarn überquert, haben wir die Chance, einen der X <Y -Vergleiche zu drehen - was die AUC verändert. Die AUC macht also keine reibungslosen Übergänge.
Das ist ein Problem für neuronale Netze, bei denen wir eine differenzierbare Verlustfunktion benötigen.
Deshalb haben wir uns vorgenommen, eine differenzierbare Funktion zu finden, die AUC wie möglich ist.
Ich habe mich durch die vorhandene Literatur zurückgegraben und in der Praxis nichts gefunden. Schließlich stieß ich auf einen neugierigen Code, den jemand in die TFLARN -Codebasis eingecheckt hatte.
Ohne Fanfare versprach es eine differenzierbare Befreiung von BCE in Form einer neuen Verlustfunktion.
( Versuchen Sie es nicht, es bläst es auf .): 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)
Es funktioniert überhaupt nicht. (Das Blasen ist tatsächlich das geringste seiner Probleme). Aber es erwähnt das Papier, auf dem es basiert.
Obwohl das Papier uralt ist und aus dem Jahr 2003 zurückgeht, fand ich, dass es mit ein wenig Arbeit - eine Erweiterung der Mathematik und sorgfältige Codierung - tatsächlich funktioniert. Es ist überflüssig, mit der Geschwindigkeit vergleichbar mit BCE (und für eine GPU/MPP genauso vektorisierbar) *. In meinen Tests gibt es höhere AUC -Scores als BCE, ist weniger empfindlich gegenüber Lernrate (die die Notwendigkeit eines Zeitplaners in meinen Tests vermeiden) und beseitigt die Notwendigkeit eines frühzeitigen Stopps.
OK, wenden Sie sich an das Originalpapier: Optimierung der Klassifiziererleistung über eine Annäherung an die Statistik von Wilcoxon - Mann - Whitney .
Die Autoren Yan et. Al motiviert die Diskussion, indem er den AUC -Score in einer bestimmten Form schreibt. Erinnern Sie sich an unser Beispiel, bei dem wir AUC berechnen, indem wir eine Brute-Force-Anzahl über den Satz möglicher Schwarz/Weiß-Paare durchführen, um den Teil, der rechts geordnet ist, zu finden. Sei B die Menge der schwarzen Werte und w der Satz weißer Werte. Alle möglichen Paare werden durch das kartesische Produkt B x W gegeben. Um die richtig ordnungsgemäßen Paare zu zählen, die wir schreiben:
Dies ist wirklich nur die mathematische Notation, um zu sagen: "Zählen Sie die richtig geordneten Paare". Wenn wir diese Summe durch die Gesamtzahl der Paare teilen, | B | * | W |, wir bekommen genau die AUC -Metrik. (Historisch gesehen wird dies als normalisierte Wilcoxon-Mann-Whitney (WMW) -Statistik bezeichnet.)
Um eine Verlustfunktion daraus zu machen, konnten wir den X <y-Vergleich mit X> y einfach umdrehen, um die falsch angeordneten Paare zu bestrafen. Das Problem ist natürlich der diskontinuierliche Sprung, wenn x y kreuzt.
Yan et. AL -Umfragen - und lehnt dann - frühere Arbeiten aus, die kontinuierliche Annäherungen an die Stufenfunktion (heftig) wie eine Sigmoid -Kurve verwenden. Dann ziehen sie das aus einem Hut:
Yann bekam dieses Forumula, indem er eine Reihe von Änderungen an der WMW anwendet:
Wir werden diese wiederum durchgehen. 1 ist klar genug. Es gibt etwas mehr zu 2, als auf das Auge zu treffen. Es ist intuitiv, dass falsch angeordnete Paare mit breiter Trennung einen größeren Verlust verleihen sollten. Aber auch etwas Interessantes geschieht, wenn sich diese Trennung 0 nähert. Der Verlust geht eher linear als auf eine Schrittfunktion. Also haben wir die Diskontinuität losgeworden.
Wenn P 1 und γ 0 wäre, wäre der Verlust einfach unser alter Freund Relu (XY). In diesem Fall bemerken wir jedoch einen Schluckauf, der die Notwendigkeit des Exponenten p . Relu ist bei 0 nicht differenzierbar. Das ist kein groß sich gegenseitig übergeben.
Glücklicherweise repariert das Anziehen von Relu dies. Relu^p mit p> 1 ist überall differenzierbar. OK, also p> 1.
Jetzt zurück zu γ: γ liefert eine "Polsterung", die zwischen zwei Punkten erzwungen wird. Wir bestrafen nicht nur Paare, die zu falsch geordneten Paaren, sondern auch rechtsgerichtete Paare sind, die zu nahe sind. Wenn ein recht angeordnetes Paar zu eng ist, besteht die Gefahr, dass seine Elemente in Zukunft durch das zufällige Wackeln eines stochastischen neuronalen Netzes ausgetauscht werden. Die Idee ist, sie auseinander zu halten, bis sie eine bequeme Entfernung erreichen.
Und das ist die Grundidee, wie in der Zeitung beschrieben. Wir geben jetzt einige Verfeinerungen in Bezug auf γ und p.
Hier brechen wir ein bisschen mit dem Papier. Yan et. Al scheint ein wenig zimperlich für das Thema der Auswahl von γ und P zu sein, und bietet nur, dass A p = 2 oder p = 3 gut erscheint und dass γ irgendwo zwischen 0,10 und 0,70 liegen sollte. Yan wünscht uns im Wesentlichen Glück mit diesen Parametern und bohrt uns.
Zunächst reparieren wir P = 2 dauerhaft, da jede Selbstachtungsverlustfunktion eine Summe der Quadrate sein sollte. (Ein Grund dafür ist, dass die Verlustfunktion nicht nur differenzierbar, sondern auch konvex ist)
Zweitens und vor allem, werfen wir einen Blick auf γ. Die Heuristik von 'irgendwo von 0,10 bis 0,70' sieht auf dem Gesicht seltsam aus; Auch wenn die Vorhersagen auf 0 <x <1 normalisiert wurden, scheint diese Anleitung überschritten, gleichgültig gegenüber den zugrunde liegenden Verteilungen und nur seltsam.
Wir werden γ aus dem Trainingssatz ableiten.
Betrachten Sie das Trainingssatz und seine schwarzen/weißen Paare b x w . Es gibt | B || W | Paare in diesem Satz. Von diesen, | B | | W | AUC sind rechts angeordnet. Die Anzahl der falsch angeordneten Paare ist also (1-auc) | B | | W |
Wenn γ Null ist, sind nur diese falsch angeordneten Paare in Bewegung (haben positive Verlust). Ein positives γ würde den Satz bewegender Paare um einige Paare ausdehnen, die rechts geordnet sind, aber zu nahe sind. Anstatt sich um den numerischen Wert von γ zu sorgen, geben wir an, wie viele zu schließende Paare wir in Bewegung setzen möchten:
Wir definieren eine Konstante & Dgr;, die den Anteil der zu klammenden Paare zu falsch angeordneten Paaren fixiert.
Wir reparieren dieses δ während des Trainings und aktualisieren γ, um sich daran anzupassen. Für gegebene δ finden wir γ so, dass
In unseren Experimenten fanden wir, dass δ zwischen 0,5 und 2,0 reichen kann und 1,0 eine gute Ausfallauswahl ist.
Also setzen wir δ auf 1, p auf 2 und vergessen γ ganz,
Unsere Verlustfunktion (1) sieht düstere Berechnung aus. Es erfordert, dass wir den gesamten Trainingssatz für jede einzelne Vorhersage scannen.
Wir umgehen dieses Problem mit einer Leistungsverbot:
Angenommen, wir berechnen die Verlustfunktion für einen bestimmten weißen Datenpunkt x . Um (3) zu berechnen, müssen wir X mit dem gesamten Trainingssatz von schwarzen Vorhersagen vergleichen. Wir nehmen eine Kurzzeit und verwenden eine zufällige Unterprobe der schwarzen Datenpunkte. Wenn wir die Größe der Unterprobe so einstellen, dass sie beispielsweise 1000 sind, erhalten wir eine sehr (sehr) nähere Annäherung an die wahre Verlustfunktion. [1]
Eine ähnliche Argumentation gilt für die Verlustfunktion eines schwarzen Datenpunkts. Wir verwenden eine zufällige Unterprobe aller weißen Trainingselemente.
Auf diese Weise passen weiße und schwarze Teilproben problemlos in den GPU -Speicher. Indem wir die gleiche Unterprobe in einem bestimmten Charge wiederverwenden, können wir den Betrieb in Chargen parallelisieren. Wir haben eine Verlustfunktion, die bei BCE so schnell ist.
Hier ist die Batch-Verlust-Funktion in 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
Beachten Sie, dass es einige zusätzliche Parameter gibt. Wir gehen im Trainingssatz aus der letzten Epoche vorbei. Da sich das gesamte Trainingssatz von einer Epoche zum nächsten nicht viel ändert, kann die Verlustfunktion jede Vorhersage erneut mit einem etwas veralteten Trainingssatz vergleichen. Dies vereinfacht das Debuggen und scheint die Leistung zugute, da sich die "Hintergrund -Epoche" nicht von einer Stapel zum nächsten ändert.
In ähnlicher Weise ist γ eine teure Berechnung. Wir verwenden erneut den Unterabtasttrick, erhöhen jedoch die Größe der Unterproben auf ~ 10.000, um eine genaue Schätzung zu gewährleisten. Um die Leistungsausschnitte beizubehalten, werden wir diesen Wert nur einmal pro Epoche neu berechnen. Hier ist die Funktion dazu:
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
Hier ist die Hubschrauberansicht, die zeigt, wie die beiden Funktionen verwendet werden, während wir uns auf Epochen und dann auf Chargen schleifen:
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)
#...
Ein vollständiges Arbeitsbeispiel finden Sie hier, Beispiel.
Im Folgenden starten wir die Leistung von ROC-Star gegen dasselbe Modell mit BCE. Die Erfahrung zeigt, dass ROC-Star oft einfach in ein Modell ausgetauscht werden kann, indem BCE mit einer guten Chance auf eine Leistungssteigerung verwendet werden kann.