Pour la classification binaire. Tout le monde aime la zone sous la métrique de la courbe (AUC), mais personne ne le cible directement dans sa fonction de perte. Au lieu de cela, les gens utilisent une fonction proxy comme l'entropie croisée binaire (BCE).
Cela fonctionne assez bien, la plupart du temps. Mais nous nous retrouvons avec une question lancinante: pourrions-nous obtenir un score plus élevé avec une fonction de perte plus proche de la nature de l'ASC?
Cela semble probable car BCE a vraiment très peu de relation avec l'AUC. Il y a eu de nombreuses tentatives pour trouver une fonction de perte qui cible plus directement l'AUC. (Une tactique commune est une forme de fonction de classement de rang telles que la perte de rang.) Dans la pratique, cependant, aucun gagnant clair n'a jamais émergé. Il n'y a eu aucun défi de BCE.
Il existe également des considérations au-delà de la performance. Étant donné que BCE est essentiellement différent de l'ASC, BCE a tendance à se comporter mal dans le dernier tronçon d'entraînement où nous essayons de le diriger vers le score AUC le plus élevé.
Une grande partie de l'optimisation de l'ASC finit par se produire dans le réglage des hyper-paramètres. L'arrêt précoce devient une nécessité inconfortable car le modèle peut diverger fortement à tout moment à partir de son score élevé.
Nous aimerions une fonction de perte qui nous donne des scores plus élevés et moins de problèmes.
Nous présentons une telle fonction ici.
Ma définition de travail préférée de l'ASC est la suivante: appelons les étiquettes de classe binaire "noire" (0) et "White" (1). Choisissez un élément noir au hasard et soyez x sa valeur prévue. Choisissez maintenant un élément blanc aléatoire avec la valeur y . Alors,
AUC = la probabilité que les éléments soient dans le bon ordre. C'est-à-dire x < y .
C'est ça. Pour tout ensemble de points donné comme l'ensemble de formation, nous pouvons obtenir cette probabilité en effectuant un calcul de force brute. Scannez l'ensemble de toutes les paires noires / blanches possibles et comptez la partie commandée à droite.
Nous pouvons voir que le score AUC n'est pas différenciable (une courbe lisse par rapport à tout x ou y .) Prendre un élément (n'importe quelle couleur) et le déplacer légèrement suffisamment pour qu'il ne frappe pas un élément voisin. L'AUC reste la même. Une fois que le point traverse un voisin, nous avons une chance de retourner l'une des comparaisons x <y - ce qui change l'ASC. Ainsi, l'ASC ne fait aucune transition en douceur.
C'est un problème pour les réseaux neuronaux, où nous avons besoin d'une fonction de perte différenciable.
Nous avons donc décidé de trouver une fonction différenciable qui est proche que possible de l'ASC.
J'ai creusé dans la littérature existante et je n'ai trouvé rien qui fonctionnait dans la pratique. Finalement, je suis tombé sur un curieux morceau de code que quelqu'un avait vérifié dans la base de code Tfrearn.
Sans fanfare, il a promis une délivrance différenciable de BCE sous la forme d'une nouvelle fonction de perte.
( Ne l'essayez pas, ça fait exploser .): Http://tfrearn.org/objectifs/#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)
Cela ne fonctionne pas du tout. (Faire exploser est en fait le moindre de ses problèmes). Mais il mentionne le document sur lequel il était basé.
Même si le papier est ancien , remontant à 2003, j'ai trouvé qu'avec un peu de travail - une extension des mathématiques et un codage soigneux - cela fonctionne réellement. Il est ultra-rapide, avec une vitesse comparable à BCE (et tout aussi vectorisable pour un GPU / MPP) *. Dans mes tests, il donne des scores AUC plus élevés que BCE, est moins sensible au taux d'apprentissage (en évitant la nécessité d'un planificateur dans mes tests) et élimine entièrement la nécessité d'un arrêt précoce.
Ok, tournons-nous vers l'article d'origine: Optimiser les performances du classificateur via une approximation de la statistique Wilcoxon - Mann - Whitney .
Les auteurs, Yan et. Al , motiver la discussion en écrivant le score AUC sous une forme particulière. Rappelez-vous notre exemple où nous calculons l'ASC en faisant un nombre de forces brutes sur l'ensemble des paires noires / blanches possibles pour trouver la partie qui est commandée à droite. Soit B l'ensemble des valeurs noires et W l'ensemble des valeurs blanches. Toutes les paires possibles sont données par le produit cartésien b x w . Pour compter les paires d'ordre droit, nous écrivons:
Il s'agit vraiment de la notation mathématique pour dire «compter les paires d'ordre droit». Si nous divisons cette somme par le nombre total de paires, | B | * | W |, nous obtenons exactement la métrique AUC. (Historiquement, c'est ce qu'on appelle la statistique normalisée de Wilcoxon-Mann-Whitney (WMW).)
Pour faire une fonction de perte à partir de cela, nous pourrions simplement retourner la comparaison x <y avec x> y afin de pénaliser les paires de mauvaises ordonnances. Le problème, bien sûr, est ce saut discontinu lorsque x traverse y.
Yan et. Les enquêtes Al - puis rejettent - des activités de travail passées en utilisant des approximations continues de la fonction de pas (Heaviside), comme une courbe sigmoïde. Ensuite, ils retirent ça d'un chapeau:
Yann a obtenu ce formumula en appliquant une série de modifications à la WMW:
Nous allons les passer par leur tour à tour. 1 est assez clair. Il y a un peu plus à 2 qui rencontre l'œil. Il est intuitif que les paires erronées avec une large séparation devraient avoir une perte plus importante. Mais quelque chose d'intéressant se produit également à mesure que cette séparation approche 0. La perte va à zéro linéairement, plutôt qu'à une fonction étape. Nous nous sommes donc débarrassés de la discontinuité.
En fait, si P était 1 et γ était 0, la perte serait simplement notre vieil ami relu (xy). Mais dans ce cas, nous remarquons un hoquet, qui révèle la nécessité de l'exposant p . RELU n'est pas différenciable à 0. se passer.
Heureusement, l'élever RELU vers une puissance corrige cela. Relu ^ p avec p> 1 est différenciable partout. Ok, donc p> 1.
Maintenant, revenons à γ: γ fournit un «rembourrage» qui est appliqué entre deux points. Nous pénalisons non seulement des paires erronées, mais aussi des paires d'ordre droit qui sont trop proches . Si une paire ordonnée est trop proche, ses éléments risquent d'être échangés à l'avenir par le trempage aléatoire d'un filet neuronal stochastique. L'idée est de les séparer jusqu'à ce qu'ils atteignent une distance confortable.
Et c'est l'idée de base décrite dans le journal. Nous avons maintenant quelques raffinements concernant γ et p.
Ici, nous cassons un peu avec le papier. Yan et. Al semble un peu délicat sur le sujet du choix de γ et P, offrant seulement qu'un p = 2 ou p = 3 semble bon et que γ devrait être entre 0,10 et 0,70. Yan nous souhaite essentiellement de la chance avec ces paramètres et s'incline.
Premièrement, nous fixons en permanence P = 2, car toute fonction de perte auto-respectée devrait être une somme de carrés. (Une des raisons à cela est qu'il garantit que la fonction de perte n'est pas seulement différenciable, mais aussi convexe )
Deuxièmement et plus important encore, jetons un coup d'œil à γ. L'heuristique de «quelque part de 0,10 à 0,70» semble étrange sur le visage; Même si les prédictions étaient normalisées pour être 0 <x <1, ces conseils semblent trop éteints, indifférents aux distributions sous-jacentes, et juste bizarre.
Nous allons dériver γ de l'ensemble de formation.
Considérez l'ensemble de formation et ses paires noires / blanches, b x w . Il y a | B || W | paires dans cet ensemble. Parmi ceux-ci, | B | | W | L'AUC est ordonnée à droite. Ainsi, le nombre de paires ordonnées erronées est (1-Auc) | B | | W |
Lorsque γ est nul, seules ces paires erronées sont en mouvement (ont une perte positive.) Un γ positif élargirait l'ensemble des paires mobiles pour inclure certaines paires qui sont ordonnées à droite, mais trop proches. Au lieu de s'inquiéter de la valeur numérique de γ, nous spécifierons le nombre de paires de serres trop à mettre en mouvement:
Nous définissons une δ constante qui fixe la proportion de paires trop fermes à des paires incorporées.
Nous fixons ce δ tout au long de l'entraînement et mettons à jour γ pour y conformer. Pour Δ donné, nous trouvons γ tels que
Dans nos expériences, nous avons constaté que Δ peut aller de 0,5 à 2,0, et 1,0 est un bon choix par défaut.
Nous avons donc réglé Δ à 1, p à 2, et oublions complètement γ,
Notre fonction de perte (1) semble lamentablement coûteuse à calculer. Il faut que nous scannions l'ensemble de formation entier pour chaque prédiction individuelle.
Nous contournons ce problème avec un ajustement de performances:
Supposons que nous calculons la fonction de perte pour un point de données blanc donné, x . Pour calculer (3), nous devons comparer X à l'ensemble de la formation des prédictions noires, y . Nous prenons un raccourci et utilisons un sous-échantillon aléatoire des points de données noirs. Si nous définissons la taille du sous-échantillon pour être, disons, 1000 - nous obtenons une approximation très (très) étroite de la fonction de perte réelle. [1]
Un raisonnement similaire s'applique à la fonction de perte d'un point de données noir; Nous utilisons un sous-échantillon aléatoire de tous les éléments d'entraînement blancs.
De cette façon, les sous-échantillons blancs et noirs s'intègrent facilement dans la mémoire du GPU. En réutilisant le même sous-échantillon à travers un lot donné, nous pouvons paralléliser l'opération en lots. Nous nous retrouvons avec une fonction de perte aussi rapide à BCE.
Voici la fonction de perte de lots à 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
Notez qu'il existe des paramètres supplémentaires. Nous passons dans l'ensemble de formation de la dernière époque . Étant donné que l'ensemble de formation entier ne change pas beaucoup d'une époque à l'autre, la fonction de perte peut comparer chaque prédiction à nouveau un ensemble de formation légèrement obsolète. Cela simplifie le débogage et semble bénéficier aux performances car l'époque «fond» ne change pas d'un lot à l'autre.
De même, γ est un calcul coûteux. Nous utilisons à nouveau l'astuce de sous-échantillonnage, mais augmentons la taille des sous-échantillons à ~ 10 000 pour assurer une estimation précise. Pour maintenir l'écrasement des performances, nous recomposons cette valeur qu'une seule fois par époque. Voici la fonction pour le faire:
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
Voici la vue d'hélicoptère montrant comment utiliser les deux fonctions pendant que nous bouclent sur les époques, puis sur les lots:
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)
#...
Un exemple de travail complet peut être trouvé ici, exemple.py pour une étoile de saut plus rapide, vous pouvez fourrer ce noyau sur Kaggle: noyau
Ci-dessous, nous tracons les performances de ROC-Star contre le même modèle en utilisant BCE. L'expérience montre que ROC-Star peut souvent être simplement échangé dans n'importe quel modèle utilisant BCE avec de bonnes chances d'une augmentation des performances.