Co pewien czas potrzebuję przypomnieć sobie, co to jest funkcja softmax, dlaczego jej wzór jest akurat taki i jak to jest, że temperatura w tym wzorze działa. Artykuł na wikipedii nie tłumaczy tak, żebym zrozumiał, więc niniejszym spisuję tak, żebym na przyszłość mógł zajrzeć tu, przeczytać i zrozumieć.
Czasem mam listę rzeczy - na przykład smakołyków - i oceniam, który jak bardzo lubię. Na przykład mam cztery smakołyki: smażoną cebulę, budyń, jabłko i kanapkę z czosnkiem. Daję im oceny: 3, 2, 1, 4. Teraz chcę powiedzieć, jak ktoś położy przede mną na stole te cztery rzeczy, to jakie jest prawdopodobieństwo, że wezmę rzecz pierwszą, drugą, trzecią czy czwartą. Prawdopodobieństwa zdarzeń rozłącznych muszą sumować się do 1, a teraz te liczby sumują się do... sprawdźmy... 3 + 2 + 1 + 4 = 10... teraz sumują się do 10. Więc podzielę wszystkie liczby przez 10. Dostaję: 0.3, 0.2, 0.1, 0.4. Zrobione. Wzór, którego użyłem, to:
wzór 1: b[i] = a[i] / sigma(j=1 to n) a[j]
Zauważmy, że przy tym wzorze nic się nie zmieni, jeśli wszystkie oceny dam dziesięć razy (czy ile tam raz chcę) większe. Wiesz, tak jakby: czy oceniam w polskiej skali szkolnej (oceny od 1 do 5), czy w jakiejś innej skali. Seria 3, 2, 1, 4 i seria 30, 20, 10, 40 po przepuszczeniu przez wzór 1 dadzą ten sam wynik - bo liczby są większe, ale i podzielę je przez więcej.
Teraz przypadek trudniejszy: załóżmy, że oceniając smakołyki zdarza mi się wystawiać oceny ujemne. Przykładowo, że wystawiłem takie oceny: 3, 0, -2, -1. Jeśli teraz spróbuję zastosować wzór (1), będę miał problem, bo teraz suma wychodzi... 3 + 0 - 2 - 1 = 0... wychodzi zero. Nie umiem dzielić przez zero. No a nawet gdyby suma nie wyszła zero, to i tak niektóre prawdopodobieństwa wyszłyby ujemne, a ja nie znam sensu fizycznego ujemnych prawdopodobieństw.
Żeby rozwiązać ten problem, przed zastosowaniem wzoru 1 muszę dodać dodatkowy krok - przerobienie liczb tak, żeby wszystkie były większe od zera, ale nadal żeby został zachowany ich porządek (jeśli jedna jest większa od drugiej przed przerobieniem, to niech będzie większa też po przerobieniu). Znam jedną prostą funkcję, która rośnie, ale nigdzie nie jest ujemna: to podnoszenie do potęgi x. Podstawa może być dowolna, byle dodatnia. Na przykład 10^x. Patrz, jeśli tę listę 3, 0, -2, -1 przepuszczę przez funkcję 10^x, to dostanę: 1000, 1, 1/100, 1/10. Już mam normalne, nieujemne liczby, które mogę zapodawać do wzoru 1.
Więc teraz wzór, którego używam do zmiany ocen na prawdopodobieństwa, składa sie z dwu kroków: najpierw dziesięć do potęgi x, a potem dzielenie każdej liczby przez sumę wszystkich.
wzór 2:
b[i] = 10 ^ a[i]
c[i] = b[i] / sigma(j=1 to n) b[j]
Tylko zobaczmy, co się stanie, jeśli tę listę, o której mówiliśmy na początku - 3, 2, 1, 4 - spróbuję przepuścić przez wzór 2. Czy wyjdzie to samo, co z wzoru 1?
Po kroku 1 dostaję: 1000, 100, 10, 10000. Suma to 1000 + 100 + 10 + 10000 = 11110. Po podzieleniu mam: 0.09000900090009, 0.009000900090009, 0.0009000900090009, 0.9000900090009. Dobrze, może nie pajacujmy z tylu mniejscami po przecinku, mamy około: 0.09, 0.009, 0.0009, 0.9. Sumuje się do 1, ale to są inne prawdopodobieństwa niż wyszły z pierwszego wzoru. Wszystkie prawdopodobieństwa wyszły mniejsze niż z pierwszego wzoru, za wyjątkiem kanapki z czosnkiem, która miała najwyższą ocenę, i teraz dostała prawdopodobieństwo większe. No tak, taki jest efekt uboczny tego kroku pierwszego: podbija kontrast między rzeczami ocenianymi nisko a wysoko. Czy to źle czy dobrze to nie wiem, w sumie zależy od konkretnego zastosowania, ale tak jest. Gdybym chciał, żeby nie było tego efektu, żeby mieć taki wzór, który na liście zawierającej liczby ujemne da wyniki rozsądne, a zastosowany na liście niezawierającej liczb ujemnych da wynik taki jak wzór (1), to nie wiem, czy się da, nie mam tego przemyślanego. Może nawet się nie da? Ale jakoś w praktycznych zastosowaniach (w machine learningu) nikomu to nie przeszkadza, a na pewno nie mi. Można wpływać na ten efekt podbijania kontrastu wybierając podstawę potęgowania. Gdybym zamiast robić 10^x zrobił 5^x, podbicie byłoby mniejsze: z listy 3, 2, 1, 4 zrobiłaby się lista 125, 25, 5, 625, która po podzieleniu przez sumę dałaby prawdopodobieństwa 0.160, 0.032, 0.006, 0.801 (widzisz, że teraz kanapka z czosnkiem nie ma już 0.9 tylko 0.8)? A gdybym zrobił 1^x nie byłoby podbicia, a wprost przeciwnie - krok pierwszy wzoru (1) spłaszczyłby tę listę tak, że wszystkie jej elementy stałyby się równe - lista stałaby się 1, 1, 1, 1, więc prawdopodobienstwo każdego smakołyka wyliczyłbym na ¼. A gdybym wziął podstawę ułamkową, to przegiąłbym aż w drugą stronę: niskie oceny stałyby się wysokie. Przykładowo, przy podstawie ½ z listy 3, 2, 1, 4 zrobiłaby się lista 1/8, 1/4, 1/2, 1/16. Widzisz, że teraz kanapka z czosnkiem ma najniższą ocenę? Więc nie dziwne, że po podzieleniu przez sumę dostajemy prawdopodobieństwa, w których kanapka z czosnkiem nadal jest najniżej: 0.133, 0.267, 0.533, 0.067. Myślałem więc kiedyś przez chwilę, że może gdzieś między 1 a 10 istnieje taka podstawa, która będzie dawać takie same prawdopodobieństwa jak wzór (1), ale nie. A dokładniej, taka podstawa istnieje, ale dla każdej listy liczb jest inna. W każdym razie zawodowcy używają podstawy "e" (wiecie, ta stała matematyczna, 2.71828), bo mówią, że to ma jakieś przydatne właściwości.
Ten wzór (2) ma jeszcze jedną ciekawą różnicę w stosunku do wzoru (1): tym razem już jest różnica, czy wystawię oceny 3, 2, 1, 4, czy też oceny 30, 20, 10, 40. No bo zobacz: załóżmy (dla prostoty obliczeń), że są dwie oceny 1 i 2, a podstawa to 10. Po przepuszczeniu przez krok 1 wzoru (2) dostajemy liczby 10 i 100. Ale jeśli oceny bym dał 10 i 20, to po przepuszczeniu przez krok 1 wzoru (2) dostajemy liczby 10000000000 i 100000000000000000000. No to nie jest wszystko jedno, bo przecież dziesiątka to jedna dziesiąta setki, a 10000000000 to tak mała część 100000000000000000000, że aż drobna. Więc zobaczcie: przy wzorze drugim możemy alternatywnie (zamiast zmieniania podstawy potęgowania) podbijać lub spłaszczać kontrast mnożąc lub dzieląc wszystkie liczby przez jakąś liczbę. I tak się w praktyce robi: podstawa to jest zawsze "e", za to przed krokiem pierwszym dodajemy jeszcze jeden krok: podzielenie każdej liczby z listy przez pewien parametr zwany temperaturą. Nazywamy go temperaturą, bo jego efekt jest taki, że kiedy jest większy, to w pewnym sensie rośnie przypadkowość - kontrast między ocenami maleje, więc rosną szanse, że kiedy zastosujemy te prawdopodobieństwa, co wyjdą na końcu, to dostaniemy nie ten element, który jest najbardziej prawdopodobny, tylko jakiś inny.
Łącznie więc wzór na softmax składa się z trzech kroków - wygląda tak:
wzór 3:
b[i] = a[i] / temperatura
c[i] = e ^ b[i]
d[i] = c[i] / sigma(j=1 to n) c[j]
Przykładowo, dla liczb 3, 2, 1, 4 i temperatury wynoszącej 1 tablice a, b, c i d wyglądają tak:
a: [3, 2, 1, 4]
b: [3, 2, 1, 4]
c: [20.09, 7.39, 2.72, 54.60]
d: [0.237, 0.087, 0.032, 0.644]
Przy tym tablica a to dane wejściowe, tablice b i c to obliczenia pośrednie, a tablica d to ostateczny wynik.
A dla temperatury 2 tablice a, b, c i d wyglądają tak:
a: [3, 2, 1, 4]
b: [1.5, 1, 0.5, 2]
c: [4.48, 2.72, 1.65, 7.39]
d: [0.277, 0.168, 0.102, 0.457]
Co można policzyć takim kodem:
import math
a = [3, 2, 1, 4]
temperatura = 1
b = [x / temperatura for x in a]
c = [math.e ** x for x in b]
suma_c = sum(c)
d = [x / suma_c for x in c]
print(f"a: {a}")
print(f"b: {[round(x, 2) for x in b]}")
print(f"c: {[round(x, 2) for x in c]}")
print(f"d: {[round(x, 3) for x in d]}")
Hm, ale gdybym ja wymyślał od zera, jak to zrobić - jak wybierać jedną rzecz z wielu na podstawie punktacji tak, żeby radzić sobie z ujemnymi ocenami i móc płynnie zmieniać kontrast między szansami rzeczy ocenianych wysoko a ocenianych nisko - to zrobiłbym inaczej, prościej. Problem z liczbami ujemnymi rozwiązałbym tak, że na początku wszystkie oceny przesunąłbym w górę o tyle, żeby najniższa ocena wynosiła 0. A potem regulację kontrastu robiłbym przesuwając wszystkie oceny trochę w górę, wszystkie o tyle samo - im więcej przesunę, tym bardziej zmniejszę kontrast. A więc mój wzór wynosiłby:
wzór 4:b[i] = a[i] - min(a) c[i] = b[i] + parametr_wygładzający_kontrast d[i] = c[i] / sigma(j=1 to n) c[j]
Ale ten wzór miałby, wydaje mi się, jedną poważną wadę: zmniejszać kontrast mógłbym łatwo, zwiększając parametr_wygładzający_kontrast. Ale zwiększać kontrast nie byłoby łatwo. Bo co, przyjmę ujemną wartość dla parametr_wygładzający_kontrast? To niektóre oceny zejdą mi poniżej zera. Co prawda potem mógłbym wyzerować te oceny, które schodzą poniżej zera, ale to by wprowadziło brzydką nieliniowość. Więc może jednak to softmax, które jest, jest dobre.