- Je berekent multiclass-kansen stabiel, ook bij zeer grote logits.
- Je leidt het foutsignaal p−y naar de scores terug en bewaakt de batchmiddeling.
Een berichtenfilter kiest nu uit drie klassen: normaal, reclame en phishing. Het netwerk geeft drie ruwe scores; we willen daar een verdeling en één trainingsfout van maken. Een heel hoge score mag de berekening niet laten overlopen.
Je kent de logistische kans voor twee klassen en het logaritmische verlies. Hier breid je die ideeën uit naar meerdere elkaar uitsluitende klassen. Per bericht is precies één label juist; dit is geen taak waarbij meerdere labels tegelijk waar kunnen zijn.
1. Scores worden een verdeling
Een logit is een onbegrensde score, geen kans. Softmax geeft een hogere score een groter aandeel: neem van iedere score de exponent en deel door de som van alle exponenten uit dezelfde rij.
Verander logits of het juiste label. De gradiënt is p−y voor dit ene voorbeeld. Een gemeenschappelijke verschuiving verandert de kansen niet.
Werkelijke logits: [2, 1, 0]
Hier zijn z de scores, p de kansen en C het aantal klassen. De indices j en k beginnen bij nul. Verhoog in de visualisatie één score: de andere kansen dalen mee, ook wanneer hun scores gelijk blijven.
Neem scores [0,ln(2),0]. De exponenten zijn [1,2,1], hun som is 4 en de kansen zijn [¼,½,¼]. De tweede klasse krijgt dus de helft, maar zo'n softmax-uitkomst bewijst nog geen goed gekalibreerde zekerheid.
import numpy as np
z = np.array([[0., np.log(2.), 0.]])
e = np.exp(z)
p = e / e.sum(axis=1, keepdims=True)
print(p.tolist()) # [[0.25, 0.5, 0.25]]
Een batch heeft vorm (N,C): N voorbeelden als rijen, C klassen als kolommen. axis=1 rekent over de klassen binnen iedere rij; keepdims=True houdt de som als kolom (N,1), zodat NumPy iedere rij door haar eigen som deelt.
2. Dezelfde kansen, kleinere getallen
Bij scores rond 1000 kan np.exp(z) oneindig worden. Dezelfde constante van alle scores aftrekken verandert hun verhoudingen niet: de gemeenschappelijke factor in teller en noemer valt weg. Trek daarom per rij haar maximum af; de grootste exponent wordt dan één.
def softmax(logits):
z = np.asarray(logits, dtype=float)
s = z - z.max(axis=1, keepdims=True)
e = np.exp(s)
return e / e.sum(axis=1, keepdims=True)
groot = np.array([[1000., 1001., 1000.]])
print(np.round(softmax(groot), 6))
# [[0.211942 0.576117 0.211942]]
Maak softmax(logits) met een nieuwe NumPy-array als resultaat. De invoer blijft intact; schrijf dus niet met -= in een mogelijk gedeelde invoerarray. We geven hier geldige, niet-lege matrices met minstens twee klassen en eindige scores.
3. Straf voor het juiste label
Cross-entropy neemt min de natuurlijke logaritme van de kans op het juiste label y. In ons handvoorbeeld is y = 1 en het verlies −ln(½) = ln(2), ongeveer 0.693147. Een lagere kans voor het juiste antwoord geeft een grotere straf.
Een uiterst kleine softmax-kans kan naar nul afronden. Bereken daarom het verlies rechtstreeks uit de verschoven scores s, zonder eerst de logaritme van die afgeronde kans te nemen.
Voeg een tweede bericht met gelijke scores toe, met label 0. Dat bericht krijgt verlies ln(3); de gemiddelde batchfout wordt (ln(2)+ln(3))/2, ongeveer 0.895880.
z = np.array([[0., np.log(2.), 0.],
[0., 0., 0.]])
y = np.array([1, 0])
s = z - z.max(axis=1, keepdims=True)
rijen = np.arange(len(z))
juiste = s[rijen, y]
verliezen = np.log(np.exp(s).sum(axis=1))
verliezen = verliezen - juiste
print(round(float(verliezen.mean()), 6))
# 0.89588
rijen bevat [0,1]; samen met y selecteert dit precies de twee juiste posities. Maak cross_entropy(logits,labels) als gemiddelde Python-float, ook bij bijvoorbeeld scores [1000,−1000]. Scores en gehele klasseindices blijven onveranderd.
4. Welke score moet veranderen?
De afgeleide van de log-som-exponent is p; de afgeleide van de afgetrokken juiste score is één op de labelpositie en nul elders. Het verschil geeft het foutsignaal naar de logits: bij het handvoorbeeld [¼,−½,¼]. Gradient descent verhoogt daardoor de juiste score en verlaagt de andere twee.
L is het batchgemiddelde; Y is de one-hot-matrix met één op de juiste klasse en nul elders. Maak die matrix niet verplicht helemaal aan: trek één af op de labelposities en deel door N.
g = softmax(z)
g[np.arange(len(g)), y] -= 1
g = g / len(g)
print(np.round(g, 6))
# [[ 0.125 -0.25 0.125 ]
# [-0.333333 0.166667 0.166667]]
print(np.allclose(g.sum(axis=1), 0))
# True
Maak logit_gradient(logits,labels) met dezelfde vorm als logits. Een veelgemaakte fout is later nogmaals door N delen: dan wordt de stap onbedoeld kleiner. Elke gradiëntrij telt hier tot nul op, passend bij de onveranderde kansen na een gelijke scoreverschuiving.
Wat neem je mee?
- Softmax normaliseert per rij; een maximale score aftrekken voorkomt exponent-overflow.
- Bereken cross-entropy uit logits, zodat een afgeronde nulkans het verlies niet breekt.
- Het teruggaande signaal is kansen min one-hot-labels, met één batchmiddeling.
Hierna stuur je dit signaal door een verborgen laag terug naar alle netwerkgewichten.