Перейти к содержимому
Ступень 22.2 Линейные модели с нуля

Логистическая и softmax-регрессия

Как линейная модель превращается в классификатор — сигмоида, бинарная cross-entropy, граница решения и softmax-регрессия, которая в точности совпадает с выходным слоем LLM.

УРОКstage-2/2-2-linear-models/02-logistic-regression.mdx1 ч 40 минУрок 2 из 3
После урока вы сможете
  • Объяснить, как сигмоида превращает линейную модель в вероятность, и читать веса как изменение логарифма шансов
  • Вывести бинарную cross-entropy из правдоподобия и её градиент p−yp - y; понимать, почему для классификации не берут MSE
  • Найти границу решения логистической регрессии и объяснить, почему она всегда линейна
  • Обобщить модель на KK классов через softmax и увидеть в ней выходной слой языковой модели

В мире LLM классификация повсюду. Перед претрейном (предобучением из урока 2.1.1) классификатор решает, математическая ли страница из интернета и стоит ли её брать в корпус. модель вознаграждениямодель вознагражденияEN reward model в RLHF (обучении с подкреплением по оценкам людей) учится предсказывать, какой из двух ответов человек сочтёт лучшим. А сама генерация текста — это классификация на 150 тысяч классов: какой токен будет следующим.

В прошлом уроке линейная модель предсказывала число. Теперь нам нужна вероятность класса. Рецепт простой: берём ту же x⊤w+bx^\top w + b, пропускаем через сигмоиду и получаем логистическую регрессиюлогистическая регрессияEN logistic regressionНесмотря на название, это метод классификации. У Чжоу Чжихуа («арбузная книга») — «регрессия логарифма шансов», у Ли Хана («Методы статистического обучения») — фонетическая запись английского слова logistic.. Через softmax — softmax-регрессию для многих классов. Это однослойная нейросеть, и она в точности совпадает с последним слоем любой языковой модели.

Слово «log» в этом уроке. Здесь и дальше log⁡\log — натуральный логарифм ln⁡\ln с основанием e≈2.718e \approx 2.718 (урок 1.0.2). Так принято в ML-статьях; в Python это np.log.

Можно обучить линейную регрессию на метках 0 и 1 и резать по порогу 0.5. Но выходы вида 1.7 или −0.3-0.3 — не вероятности. Хуже другое: MSE штрафует даже «слишком правильные» ответы. Очевидно математическая страница с прогнозом 3.0 вместо 1 даёт ошибку 22=42^2 = 4 и тянет прямую к себе. Граница решения сдвигается из-за примеров, с которыми и так всё ясно. Нужна функция, которая сжимает любое число в интервал (0,1)(0, 1).

Сигмоидасигмоида (логистическая функция)EN sigmoid (logistic function)σ(z) = 1 / (1 + e^(−z)) — превращает любое число в вероятность от 0 до 1. — именно такая функция. Вы встречали её в уроках 1.0.1 и 1.2.1:

σ(z)=11+e−z\sigma(z) = \frac{1}{1 + e^{-z}}

Почему результат всегда между 0 и 1: e−ze^{-z} — положительное число, поэтому знаменатель больше единицы, а дробь меньше единицы. При большом zz экспонента e−ze^{-z} почти ноль, и σ≈1\sigma \approx 1; при большом отрицательном zz экспонента огромна, и σ≈0\sigma \approx 0. Пример: σ(1)=1/(1+e−1)=1/(1+0.368)≈0.731\sigma(1) = 1 / (1 + e^{-1}) = 1 / (1 + 0.368) \approx 0.731.

zz−4-4−2-2−1-10124
σ(z)\sigma(z)0.0180.1190.2690.50.7310.8810.982

Свойства, которые понадобятся:

  • σ(0)=0.5\sigma(0) = 0.5 и σ(−z)=1−σ(z)\sigma(-z) = 1 - \sigma(z) — кривая симметрична;
  • σ′(z)=σ(z)(1−σ(z))\sigma'(z) = \sigma(z)(1 - \sigma(z)) — производная выражается через само значение (урок 1.2.1). Максимум производной — 0.250.25 в нуле; при ∣z∣>5\lvert z \rvert > 5 она почти ноль: сигмоида насыщается.

Модель логистической регрессии:

p=P(y=1∣x)=σ(x⊤w+b)p = P(y = 1 \mid x) = \sigma(x^\top w + b)

Обратим сигмоиду. Если p=σ(z)p = \sigma(z), то

z=log⁡p1−pz = \log \frac{p}{1 - p}

Отношение p/(1−p)p / (1 - p) — это шансы (odds), как у букмекеров: при p=0.8p = 0.8 шансы 0.8/0.2=40.8 / 0.2 = 4, «4 к 1». Значит, zz — логарифм шансов, или логитлогитEN logitСтрого — логарифм шансов log(p / (1 − p)). В нейросетях логитами называют любые выходы до softmax или сигмоиды.. Проверка на числах: log⁡4≈1.386\log 4 \approx 1.386, и σ(1.386)=1/(1+e−1.386)=1/(1+0.25)=0.8\sigma(1.386) = 1 / (1 + e^{-1.386}) = 1 / (1 + 0.25) = 0.8 — вернулись к исходной вероятности. Логистическая регрессия утверждает простую вещь: логарифм шансов линейно зависит от признаков.

Отсюда понятный смысл весов. Рост признака xjx_j на единицу прибавляет wjw_j к логиту, то есть умножает шансы в ewje^{w_j} раз: прибавить к логарифму wjw_j — значит умножить само число на ewje^{w_j} (урок 1.0.2).

Пример: фильтр математических страниц. Пусть у страницы два признака: x1x_1 — доля цифр и формульных символов, x2x_2 — сколько раз встретились слова «теорема», «доказательство», «уравнение». Допустим, модель обучилась так:

z=−4+8x1+1.5x2z = -4 + 8 x_1 + 1.5 x_2
  • Страница A: x1=0.30x_1 = 0.30, x2=2x_2 = 2. Тогда z=−4+2.4+3=1.4z = -4 + 2.4 + 3 = 1.4 и p=σ(1.4)≈0.80p = \sigma(1.4) \approx 0.80.
  • Страница B: x1=0.05x_1 = 0.05, x2=0x_2 = 0. Тогда z=−3.6z = -3.6 и p≈0.027p \approx 0.027.
  • Каждое ключевое слово умножает шансы в e1.5≈4.5e^{1.5} \approx 4.5 раза. Проверим на странице A: её шансы 0.8/0.2=40.8 / 0.2 = 4. Добавим ещё одно ключевое слово: z=1.4+1.5=2.9z = 1.4 + 1.5 = 2.9, p=σ(2.9)≈0.948p = \sigma(2.9) \approx 0.948, шансы 0.948/0.052≈180.948 / 0.052 \approx 18 — ровно в 4.5 раза больше.

Метка y∈{0,1}y \in \lbrace 0, 1 \rbrace — это бросок монетки с вероятностью pp, то есть распределение Бернуллираспределение БернуллиEN Bernoulli distributionВеличина принимает 1 с вероятностью p и 0 с вероятностью 1 − p. Пример — награда «верно/неверно». (урок 1.3.1). Его записывают одной формулой: P(y)=py(1−p)1−yP(y) = p^{y} (1 - p)^{1 - y}. При y=1y = 1 остаётся pp, при y=0y = 0 — 1−p1 - p. Дальше — метод максимального правдоподобия из урока 1.3.2: берём логарифм, ставим минус, усредняем по примерам и получаем бинарную cross-entropy (BCE)бинарная перекрёстная энтропия (BCE)EN binary cross-entropy (BCE), log loss:

L=−1n∑i=1n[yilog⁡pi+(1−yi)log⁡(1−pi)]L = -\frac{1}{n} \sum_{i=1}^{n} \Big[ y_i \log p_i + (1 - y_i) \log (1 - p_i) \Big]

Это обычная перекрёстная энтропияперекрёстная энтропияEN cross-entropy для двух классов. Для каждого примера работает только одно слагаемое: −log⁡p-\log p, если y=1y = 1, и −log⁡(1−p)-\log(1 - p), если y=0y = 0: второе слагаемое умножается на 1−y=01 - y = 0 и пропадает.

Пример на двух страницах. Первая математическая (y=1y = 1), модель дала p=0.9p = 0.9: потери −log⁡0.9≈0.105-\log 0.9 \approx 0.105. Вторая обычная (y=0y = 0), модель дала p=0.2p = 0.2: потери −log⁡(1−0.2)=−log⁡0.8≈0.223-\log(1 - 0.2) = -\log 0.8 \approx 0.223. Средняя BCE — (0.105+0.223)/2≈0.164(0.105 + 0.223) / 2 \approx 0.164.

Модель дала ppLoss, если y=1y = 1Loss, если y=0y = 0
0.990.014.61
0.90.1052.30
0.50.6930.693
0.12.300.105
0.014.610.01

Уверенная ошибка стоит очень дорого: при p→0p \to 0 и y=1y = 1 потери уходят в бесконечность.

Градиент: та же форма, что у линейной регрессии

Заголовок раздела «Градиент: та же форма, что у линейной регрессии»

Продифференцируем потери одного примера по логиту zz. Цепное правило (урок 1.2.2): производная loss по pp, умноженная на производную pp по zz, а последняя для сигмоиды равна σ′=p(1−p)\sigma' = p(1 - p):

∂L∂z=(−yp+1−y1−p)⋅p(1−p)=p−y\frac{\partial L}{\partial z} = \left( -\frac{y}{p} + \frac{1 - y}{1 - p} \right) \cdot p(1 - p) = p - y

Всё сократилось до «предсказание минус правда». Например, модель дала p=0.9p = 0.9 при y=1y = 1: градиент −0.1-0.1, логит нужно чуть-чуть поднять. При y=0y = 0 градиент был бы 0.90.9: логит надо сильно опустить. Дальше z=x⊤w+bz = x^\top w + b, поэтому

∇wL=1nX⊤(p−y),∂L∂b=1n∑i(pi−yi)\nabla_w L = \frac{1}{n} X^\top (p - y), \qquad \frac{\partial L}{\partial b} = \frac{1}{n} \sum_i (p_i - y_i)

Сравните с линейной регрессией: там было 2nX⊤(y^−y)\frac{2}{n} X^\top (\hat y - y). Та же схема — признаки, взвешенные ошибками. Это не совпадение. (Для любознательных.) В обоих случаях loss — отрицательное лог-правдоподобие «своего» распределения: гауссовского для чисел, Бернулли для меток 0/1. А выходная функция (тождественная или сигмоида) подобрана под это распределение. Такие согласованные пары «выход + loss» дают градиент вида «ошибка × признаки». В статистике всё это семейство называют обобщёнными линейными моделями (GLM).

Возьмём пример с y=1y = 1, где модель уверенно ошиблась: z=−4.6z = -4.6, p≈0.01p \approx 0.01.

  • BCE: ∂L/∂z=p−y≈−0.99\partial L / \partial z = p - y \approx -0.99. Сильный сигнал: «срочно исправляйся».
  • MSE поверх сигмоиды: ∂L/∂z=2(p−y)⋅p(1−p)≈−0.0195\partial L / \partial z = 2(p - y) \cdot p(1 - p) \approx -0.0195. В 50 раз слабее.

Множитель p(1−p)p(1 - p) гасит градиент ровно тогда, когда модель ошибается сильнее всего. Вдобавок MSE поверх сигмоиды невыпукла по ww, а BCE выпуклавыпуклая функцияEN convex function«Чаша» без ложных ям — любой локальный минимум глобальный. Отрезок между двумя точками графика лежит не ниже графика. (урок 1.4.1): ложных локальных минимумов у неё нет.

Наивный код -np.log(sigmoid(z)) ломается при z=−1000z = -1000: сигмоида округляется до 0 (компьютер хранит дробные числа с ограниченной точностью, урок 0.0.2), и log⁡0=−∞\log 0 = -\infty. Как и в уроке 1.3.2, считаем потери сразу по логиту.

Главное, что нужно унести из этого раздела, — итоговая формула (она стоит отдельной строкой после трёх шагов) и правило в конце раздела. Сам вывод за три шага — для любознательных.

  1. −log⁡σ(z)=log⁡(1+e−z)-\log \sigma(z) = \log(1 + e^{-z}) и −log⁡(1−σ(z))=−log⁡σ(−z)=log⁡(1+ez)-\log(1 - \sigma(z)) = -\log \sigma(-z) = \log(1 + e^{z}).
  2. Подставляем в BCE и пользуемся тем, что log⁡(1+e−z)=log⁡(1+ez)−z\log(1 + e^{-z}) = \log(1 + e^{z}) - z: L=ylog⁡(1+e−z)+(1−y)log⁡(1+ez)=log⁡(1+ez)−yzL = y \log(1 + e^{-z}) + (1 - y) \log(1 + e^{z}) = \log(1 + e^{z}) - y z.
  3. Осталось устойчиво посчитать log⁡(1+ez)\log(1 + e^{z}). При z≤0z \le 0 экспонента и так не больше единицы. При z>0z > 0 выносим eze^{z} за скобку: log⁡(1+ez)=z+log⁡(1+e−z)\log(1 + e^{z}) = z + \log(1 + e^{-z}). Обе ветки записываются одной формулой:
L(z,y)=max⁡(z,0)−yz+log⁡(1+e−∣z∣)L(z, y) = \max(z, 0) - y z + \log\left(1 + e^{-\lvert z \rvert}\right)

Экспонента берётся только от −∣z∣≤0-\lvert z \rvert \le 0, переполнения нет. Проверка: при z=1000z = 1000 и y=0y = 0 loss равен 1000−0+log⁡(1+e−1000)≈10001000 - 0 + \log(1 + e^{-1000}) \approx 1000 — огромный, но конечный штраф за уверенную ошибку. В PyTorch это F.binary_cross_entropy_with_logits (или nn.BCEWithLogitsLoss). Правило то же, что для softmax: в loss передают логиты, а не вероятности.

Класс 1 предсказываем, если p≥0.5p \ge 0.5. Поскольку σ(z)≥0.5  ⟺  z≥0\sigma(z) \ge 0.5 \iff z \ge 0, условие превращается в

x⊤w+b≥0x^\top w + b \ge 0

Множество точек, где x⊤w+b=0x^\top w + b = 0, — граница решенияграница решенияEN decision boundary. В двумерном случае это прямая, в трёхмерном — плоскость, в dd-мерном — гиперплоскость (так называют такую же «ровную перегородку» в пространстве любой размерности). В примере с фильтром граница — прямая −4+8x1+1.5x2=0-4 + 8 x_1 + 1.5 x_2 = 0. Страница совсем без цифр (x1=0x_1 = 0) пройдёт фильтр, только если в ней хотя бы 3 ключевых слова: при двух p=0.27p = 0.27, при трёх p=0.62p = 0.62.

0.00.10.20.30.40.5012345x₁ — доля цифр и формульных символовx₂ — ключевых словp = 0.8p = 0.5 — границаp = 0.2класс «математика»: z ≥ 0(p ≥ 0.5)A: p ≈ 0.80B: p ≈ 0.0272 слова: 0.273 слова: 0.62
Фильтр математических страниц z = −4 + 8x₁ + 1.5x₂. Сплошная прямая — граница решения (z = 0, p = 0.5); выше и правее неё — класс «математика». Пунктиры — линии p = 0.2 и p = 0.8: все линии равной вероятности параллельны границе. Полые кружки — страницы без цифр с двумя и тремя ключевыми словами.

Отсюда три важных следствия.

  1. Логистическая регрессия — линейный классификатор. Если классы нельзя разделить прямой, она бессильна. Классический пример — XOR: класс 1, если ровно один из двух признаков равен 1. Всего четыре точки: (0,0)(0, 0) и (1,1)(1, 1) — класс 0, (0,1)(0, 1) и (1,0)(1, 0) — класс 1. Нарисуйте их на листке: классы стоят «крест-накрест», и никакая прямая не оставит обе единицы с одной стороны, а оба нуля — с другой. Вот почему. Пусть прямая оставила точки (0,1)(0, 1) и (1,0)(1, 0) по одну сторону. Тогда там же и середина отрезка между ними, (0.5,0.5)(0.5, 0.5). Но это и середина отрезка между (0,0)(0, 0) и (1,1)(1, 1), так что хотя бы одна из этих точек тоже окажется на той же стороне. Нужны скрытые слои с нелинейностью — это урок 3.1.1.
  2. Норма весов — это уверенность. Умножьте ww и bb на 10: граница не сдвинется, но сигмоида станет почти ступенькой. На рисунке пунктиры p=0.2p = 0.2 и p=0.8p = 0.8 прижмутся к границе в 10 раз ближе.
  3. На линейно разделимых данных веса растут бесконечно. Loss можно уменьшать вечно, увеличивая ∥w∥\lVert w \rVert. Спасают регуляризациярегуляризацияEN regularization (L2) и ранняя остановка из урока 2.1.2.

Порог 0.5 — не закон природы. Для фильтра, который не должен пропускать мусор, порог поднимают — например, до 0.8. Как выбирать порог и чем мерить результат — в следующем уроке.

Теперь классов KK, например «код», «математика», «проза». Заводим по линейной модели на каждый класс: каждая выдаёт свой логит — «балл» своего класса. Веса всех KK моделей складываем в матрицу W∈RK×dW \in \mathbb{R}^{K \times d} (строка на класс), сдвиги — в вектор b∈RKb \in \mathbb{R}^{K}. Логиты превращаем в вероятности через softmax из урока 1.3.2: softmax(z)k=ezk/∑jezj\mathrm{softmax}(z)_k = e^{z_k} / \sum_j e^{z_j} — каждую экспоненту делим на их сумму, и вероятности в сумме дают 1. Потери — минус логарифм вероятности правильного класса yy:

z=Wx+b,p=softmax(z),L=−log⁡pyz = W x + b, \qquad p = \mathrm{softmax}(z), \qquad L = -\log p_{y}

Это softmax-регрессияsoftmax-регрессия (многоклассовая логистическая регрессия)EN softmax regression (multinomial logistic regression). Градиент по логитам вы уже выводили в уроке 1.3.2. Это снова «предсказание минус правда», только теперь правда — one-hot вектор:

∂L∂z=p−onehot(y),∇WL=(p−onehot(y)) x⊤,∇bL=p−onehot(y)\begin{gathered} \frac{\partial L}{\partial z} = p - \mathrm{onehot}(y), \\ \nabla_W L = (p - \mathrm{onehot}(y)) \, x^\top, \\ \nabla_b L = p - \mathrm{onehot}(y) \end{gathered}

Здесь onehot(y)\mathrm{onehot}(y) — one-hot векторone-hot кодирование (унитарный код)EN one-hot encodingВектор длины V из нулей с единицей в позиции номера токена. Все one-hot векторы попарно ортогональны.: единица на месте правильного класса, нули на остальных местах. Градиент по WW — внешнее произведение (урок 1.1.2): матрица K×dK \times d, такая же, как WW. Для батча из nn примеров градиенты усредняют, как в бинарном случае.

На числах: логиты z=[2.0,1.0,0.1]z = [2.0, 1.0, 0.1], правильный класс — второй. Softmax даёт p=[0.659,0.242,0.099]p = [0.659, 0.242, 0.099], loss =−ln⁡0.242=1.42= -\ln 0.242 = 1.42. Градиент: [0.659,−0.758,0.099][0.659, -0.758, 0.099]. Логит правильного класса тянем вверх, остальные — вниз, и тем сильнее, чем больше вероятность они «украли».

Softmax или сигмоида на каждый класс? Softmax подходит, когда правильный класс ровно один. Вероятности конкурируют: если одна растёт, остальные падают, а в сумме всегда 1. Но страница может быть одновременно и кодом, и математикой. Когда классы не исключают друг друга (multi-label), берут KK независимых сигмоид и BCE на каждом выходе. Получается KK отдельных логистических регрессий. Например, логиты [2.0,1.5,−1.0][2.0, 1.5, -1.0] через сигмоиды дают [0.88,0.82,0.27][0.88, 0.82, 0.27]: «код — да, математика — да, проза — нет». Softmax от тех же логитов дал бы [0.60,0.37,0.03][0.60, 0.37, 0.03] и заставил бы классы делить единицу. Этот выбор вы встретите в китайском углу ниже.

Однослойная нейросеть — и выходной слой LLM

Заголовок раздела «Однослойная нейросеть — и выходной слой LLM»
flowchart LR
X["Признаки x<br/>(d чисел)"] --> L["Linear: z = Wx + b<br/>W: K × d"]
L --> S["softmax"]
S --> P["p₁ … p_K"]
P --> CE["Loss = −log p_y"]
H["Скрытое состояние h<br/>трансформера"] -.->|"в LLM вместо x"| L
Softmax-регрессия — это однослойная нейросеть. В LLM на её вход подают выход трансформера

Softmax-регрессия — это нейронная сетьнейронная сетьEN neural network из одного линейного слоя без скрытых слоёв. Теперь посмотрите на LLM. Трансформер превращает контекст в вектор hh размерности dmodeld_{\text{model}}. Последний слой — LM headLM-голова (языковая голова)EN LM head (language modeling head)Последний линейный слой GPT: превращает вектор ширины d в V логитов, по одному на токен словаря. Часто делит матрицу с эмбеддингами (weight tying). — считает z=Wouthz = W_{\text{out}} h: логиты по всему словарю, а дальше softmax и cross-entropy.

Это softmax-регрессия с VV классами (по классу на каждый токен словаря). Её признаки hh не заданы вручную, а обучаются вместе с ней. Возьмём размеры Qwen3-8B: длина вектора hh — dmodel=4096d_{\text{model}} = 4096 чисел, словарь V=151 936V = 151\,936 токенов. Одна матрица WoutW_{\text{out}} размера V×dmodelV \times d_{\text{model}} содержит 4096⋅151 936≈6224096 \cdot 151\,936 \approx 622 млн параметров.

Ещё два места, где вы встретите эту конструкцию:

  • Биграммная модель из урока 4.1.2 — это softmax-регрессия, у которой признак — one-hot вектор предыдущего токена.
  • Линейный зонд (linear probe). Берут скрытые состояния замороженной LLM и учат на них логистическую регрессию. Если зонд справляется, нужная информация линейно закодирована внутри модели. Это базовый инструмент интерпретируемости (его возможности и ловушки — в уроке 9.3.1).

Реализуйте логистическую регрессию на NumPy: устойчивую сигмоиду, BCE по логитам, градиенты, обучение и предсказание с порогом. Тесты проверят устойчивость на z=±1000z = \pm 1000 и accuracy ≥ 95% на двух «облаках» точек. Структура та же, что в задании прошлого урока: fit_logreg почти дословно повторяет fit_gd, меняются только градиенты и функция потерь.

Логистическая регрессия с нуляnumpy · нужен интернет7 тестов
import numpy as np


def sigmoid(z):
    """Сигмоида σ(z) = 1 / (1 + e^(−z)) поэлементно для массива z.

    Должна работать без переполнения и nan даже для z = ±1000.
    Подсказка: для z ≥ 0 берите 1 / (1 + e^(−z)), для z < 0 — e^z / (1 + e^z).
    Осторожно: np.where вычисляет ОБЕ ветки для всех элементов, поэтому
    np.exp(-z) и np.exp(z) по отдельности всё равно переполнятся. Подумайте,
    как обойтись одной экспонентой от −|z| (она всегда не больше 1).
    """
    # TODO
    ...


def bce_with_logits(z, y):
    """Средняя бинарная cross-entropy, посчитанная прямо по логитам z (без вызова sigmoid).

    Для одного примера: −[y·log σ(z) + (1 − y)·log(1 − σ(z))].
    Устойчивая форма той же величины: max(z, 0) − y·z + log(1 + e^(−|z|)).
    Возвращает число (float) — среднее по всем примерам.
    """
    # TODO
    ...


def gradients(X, y, w, b):
    """Градиенты средней BCE по w и b для логистической регрессии.

    p = σ(X·w + b);  dL/dw = Xᵀ(p − y) / n;  dL/db = mean(p − y).
    Возвращает (dw, db).
    """
    # TODO
    ...


def fit_logreg(X, y, lr=0.5, steps=500):
    """Обучает логистическую регрессию градиентным спуском из нулей.

    Возвращает (w, b, history), где history — список средних BCE после каждого шага.
    """
    # TODO
    ...


def predict(X, w, b, threshold=0.5):
    """Метки классов 0/1: 1, если σ(X·w + b) ≥ threshold. Возвращает массив int."""
    # TODO
    ...


# Проверьте себя кнопкой «Запустить»:
print(sigmoid(np.array([-2.0, 0.0, 2.0])))                    # ≈ [0.1192 0.5 0.8808]
print(bce_with_logits(np.array([0.0, 0.0]), np.array([1.0, 0.0])))  # ln 2 ≈ 0.6931

Классифицируем страницы на три класса по двум признакам. Признаки — доля «кодовых» символов и доля цифр и формул:

import torch
import torch.nn.functional as F
torch.manual_seed(0)
# Синтетика: 2 признака страницы — доля «кодовых» символов ({}();=) и доля цифр/формул
centers = torch.tensor([[0.30, 0.05], # 0: код
[0.05, 0.30], # 1: математика
[0.03, 0.03]]) # 2: обычная проза
# по 200 точек вокруг каждого центра (центр + шум), склеенные в одну таблицу
X = torch.cat([c + 0.07 * torch.randn(200, 2) for c in centers]) # (600, 2)
y = torch.arange(3).repeat_interleave(200) # метки: 200 нулей, 200 единиц, 200 двоек
model = torch.nn.Linear(2, 3) # W: (3, 2), b: (3,) — вот и вся «нейросеть»
opt = torch.optim.SGD(model.parameters(), lr=5.0)
for step in range(300):
logits = model(X) # (600, 3): три логита на каждую страницу
loss = F.cross_entropy(logits, y) # softmax + NLL в одной устойчивой функции
opt.zero_grad() # стандартные три строки шага обучения (урок 1.4.1)
loss.backward()
opt.step()
# argmax(dim=1) — номер класса с наибольшим логитом в каждой строке; сравниваем с правильными метками
acc = (model(X).argmax(dim=1) == y).float().mean()
print(f"loss = {loss.item():.3f}, accuracy = {acc:.3f}")
page = torch.tensor([[0.20, 0.12]]) # немного кода, немного формул
# softmax превращает 3 логита в 3 вероятности; tolist() — в обычный список Python
print("p(код, математика, проза) =", [round(p, 3) for p in F.softmax(model(page), dim=1).squeeze().tolist()])
# Бинарный случай: softmax по двум логитам = сигмоида от их разности
z = torch.tensor([0.3, 1.7])
print(f"{F.softmax(z, dim=0)[1].item():.4f} {torch.sigmoid(z[1] - z[0]).item():.4f}")

Ожидаемый вывод (последние знаки могут отличаться в другой версии PyTorch):

loss = 0.158, accuracy = 0.955
p(код, математика, проза) = [0.648, 0.173, 0.18]
0.8022 0.8022

Обратите внимание: F.cross_entropy принимает логиты, а не вероятности. Если подать в неё результат softmax, softmax применится дважды — классическая ошибка (о ней подробно в уроке 3.3.3). Шаг lr=5.0 большой, потому что признаки маленькие (сотые доли). Попробуйте стандартизировать их, как в прошлом уроке, и обойтись lr=0.5.

Проверь себя6 вопросов
1 Логит примера равен ln⁡3≈1.0986\ln 3 \approx 1.0986. Какую вероятность класса 1 даёт логистическая регрессия?
2 Чему равна производная бинарной cross-entropy по логиту zz для одного примера?
3 Какие утверждения верны?Можно выбрать несколько ответов
4 Почему для классификации берут BCE, а не MSE поверх сигмоиды?
5 LM head модели: dmodel=2048d_{\text{model}} = 2048, словарь V=32 000V = 32\,000, без сдвига. Сколько миллионов параметров в этой матрице? (до одного знака)
6 Как называется величина log⁡p1−p\log \frac{p}{1 - p}? (одно слово)
  • Логистическая регрессия: p=σ(x⊤w+b)p = \sigma(x^\top w + b). Логит — логарифм шансов, и он линеен по признакам.
  • Потери — бинарная cross-entropy (MLE для распределения Бернулли). Градиент по логиту — p−yp - y. Считайте loss по логитам: BCEWithLogitsLoss.
  • Граница решения — гиперплоскость. Поэтому модель линейна и не решает XOR.
  • Softmax-регрессия: p=softmax(Wx+b)p = \mathrm{softmax}(Wx + b), градиент p−onehot(y)p - \mathrm{onehot}(y). Это однослойная нейросеть и в точности LM head любой LLM.

Логистическую регрессию с нуля вы соберёте ещё раз, уже как самостоятельный проект с автотестами, в домашней работе s2-hw01-logreg-from-scratch в конце ступени 2.

Дальше: метрики качества. Accuracy, precision, recall, F1 и ROC-AUC — и почему метрика, которую вы показываете людям, почти никогда не совпадает с loss, который минимизирует модель.