Softmax и температура
Вы узнаете
Как softmax превращает оценки в вероятности с суммой один и что с ними делает температура.
Классификатор заканчивается одной оценкой на класс, и softmax превращает оценки в вероятности: exp каждой оценки, делённая на сумму всех. Оценки 1, 2 и 3 становятся 0.090, 0.245 и 0.665. Если сначала разделить оценки на температуру, меняется уверенность: при 0.5 верхняя оценка забирает 0.867, при 5 все три равны 0.27, 0.33 и 0.40. Языковая модель выбирает следующий токен именно отсюда, и настройка температуры чат-модели это и есть T. Спека softmax.t27 сначала вычитает наибольшую оценку, поэтому exp никогда не переполняется.
Попробовать
Охладите температуру до 0.1 и прочитайте три вероятности; затем сделайте ничью и нагрейте её.

Move three scores and the temperature: softmax turns them into probabilities that sum to one. Cool it and the top score takes almost everything; heat it and the choice evens out.
specs/ml/activation/softmax.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Control sharpness of distribution | φ² + 1/φ² = 3 | TRINITY
// #8980: written with `range` and `math::exp`, which no spec declares, so no test
// ever ran. backward was also wrong: it used only the Jacobian's diagonal,
// p_i (1 - p_i), and dropped the -p_i p_j terms. It is now the full product
// grad_input_i = p_i (g_i - sum_j g_j p_j) / temperature.
module Softmax;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const ZERO : f32 = 0.0;
const ONE : f32 = 1.0;
const MIN_TEMP : f32 = 0.001;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const SoftmaxConfig = struct {
temperature : f32,
axis : u32,
stable : bool,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// output_i = exp((x_i - max) / T) / sum_j exp((x_j - max) / T); subtracting the max keeps exp finite
fn forward(logits: []const f32, output: []f32, config: SoftmaxConfig) -> void {
var max_val : f32 = logits[0];
var i : usize = 0;
while (i < logits.len) {
if (logits[i] > max_val) {
max_val = logits[i];
}
i = i + 1;
}
var sum : f32 = ZERO;
i = 0;
while (i < logits.len) {
output[i] = exp((logits[i] - max_val) / config.temperature);
sum = sum + output[i];
i = i + 1;
}
i = 0;
while (i < logits.len) {
output[i] = output[i] / sum;
i = i + 1;
}
}
// Vector-Jacobian product: grad_input_i = p_i (g_i - sum_j g_j p_j) / T
fn backward(grad_output: []const f32, probabilities: []const f32, grad_input: []f32, config: SoftmaxConfig) -> void {
var dot : f32 = ZERO;
var i : usize = 0;
while (i < grad_output.len) {
dot = dot + grad_output[i] * probabilities[i];
i = i + 1;
}
i = 0;
while (i < grad_output.len) {
grad_input[i] = probabilities[i] * (grad_output[i] - dot) / config.temperature;
i = i + 1;
}
}
fn cfg(temperature: f32) -> SoftmaxConfig {
return SoftmaxConfig{ .temperature = temperature, .axis = 0, .stable = true };
}
// output[index] of softmax over (a, b, c)
fn softmax_at(a: f32, b: f32, c: f32, temperature: f32, index: usize) -> f32 {
const logits : [3]f32 = [a, b, c];
var output : [3]f32 = [0.0] ** 3;
forward(&logits, &output, cfg(temperature));
return output[index];
}
// grad_input[index] for grad_output (0.1, 0.2, 0.3) at probabilities (0.2, 0.3, 0.5)
fn backward_at(index: usize) -> f32 {
const grad_output : [3]f32 = [0.1, 0.2, 0.3];
const probabilities : [3]f32 = [0.2, 0.3, 0.5];
var grad_input : [3]f32 = [0.0] ** 3;
backward(&grad_output, &probabilities, &grad_input, cfg(1.0));
return grad_input[index];
}
// L = sum_i w_i softmax(x)_i with w = (0.1, 0.2, 0.3): the loss whose gradient backward computes
fn weighted(a: f32, b: f32, c: f32) -> f32 {
return 0.1 * softmax_at(a, b, c, 1.0, 0) + 0.2 * softmax_at(a, b, c, 1.0, 1) + 0.3 * softmax_at(a, b, c, 1.0, 2);
}
fn grad_at(a: f32, b: f32, c: f32, index: usize) -> f32 {
const grad_output : [3]f32 = [0.1, 0.2, 0.3];
var probabilities : [3]f32 = [0.0] ** 3;
const logits : [3]f32 = [a, b, c];
forward(&logits, &probabilities, cfg(1.0));
var grad_input : [3]f32 = [0.0] ** 3;
backward(&grad_output, &probabilities, &grad_input, cfg(1.0));
return grad_input[index];
}
fn valid_input() -> f32 {
return 3.0;
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
// softmax(1, 2, 3) = (0.0900306, 0.2447285, 0.6652410)
test "forward_basic_case"
given p0 = softmax_at(1.0, 2.0, 3.0, 1.0, 0)
when p2 = softmax_at(1.0, 2.0, 3.0, 1.0, 2)
then p0 > 0.0 and softmax_at(1.0, 2.0, 3.0, 1.0, 1) > 0.0 and p2 > 0.0 and @abs(p0 + softmax_at(1.0, 2.0, 3.0, 1.0, 1) + p2 - 1.0) < 0.0001 and @abs(p0 - 0.0900306) < 0.000001 and @abs(p2 - 0.6652410) < 0.000001
// Subtracting the max: logits of 1000 do not overflow exp
test "forward_large_logits_stay_finite"
given p = softmax_at(1000.0, 1000.0, 1000.0, 1.0, 1)
then @abs(p - 1.0 / 3.0) < 0.000001
// A higher temperature flattens the distribution toward uniform
test "forward_temperature_flattens"
given sharp = softmax_at(1.0, 2.0, 3.0, 1.0, 2)
when flat = softmax_at(1.0, 2.0, 3.0, 10.0, 2)
then flat < sharp and flat > 1.0 / 3.0
// dot = 0.23: grad = (0.2 (0.1 - 0.23), 0.3 (0.2 - 0.23), 0.5 (0.3 - 0.23)) = (-0.026, -0.009, 0.035)
test "backward_basic_case"
given g0 = backward_at(0)
when g2 = backward_at(2)
then g0 != 0.0 and backward_at(1) != 0.0 and g2 != 0.0 and @abs(g0 - (-0.026)) < 0.000001 and @abs(backward_at(1) - (-0.009)) < 0.000001 and @abs(g2 - 0.035) < 0.000001
// The gradient of a function of probabilities that sum to 1 sums to 0
test "backward_sums_to_zero"
given total = backward_at(0) + backward_at(1) + backward_at(2)
then @abs(total) < 0.0000001
test "backward_matches_finite_difference"
given slope = (weighted(1.0, 2.0 + 0.001, 3.0) - weighted(1.0, 2.0 - 0.001, 3.0)) / 0.002
when result = grad_at(1.0, 2.0, 3.0, 1)
then @abs(result - slope) < 0.0005
// ═══════════════════════════════════════════════════════════
// TDD: Invariants (from .tri constraints)
// ═══════════════════════════════════════════════════════════
// logits.size() == output.size(): the first and last element are both probabilities
invariant softmax_constraint_0
assert softmax_at(1.0, 2.0, valid_input(), 1.0, 0) >= 0.0 and softmax_at(1.0, 2.0, valid_input(), 1.0, 2) >= 0.0
// config.temperature >= MIN_TEMP
invariant softmax_constraint_1
assert cfg(1.0).temperature >= MIN_TEMP
// sum(output) = 1.0 within floating precision
invariant softmax_constraint_2
assert @abs(softmax_at(1.0, 2.0, valid_input(), 1.0, 0) + softmax_at(1.0, 2.0, valid_input(), 1.0, 1) + softmax_at(1.0, 2.0, valid_input(), 1.0, 2) - 1.0) < 0.0001
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.