Бинарная классификация: как машина отличает кота от не-кота

Продолжение статьи «Как работает LLM: путь одного токена» — там мы прошли путь токена через уже обученную сеть. Теперь откатываемся к самому началу: как вообще устроена задача «посмотреть на данные и ответить да/нет» и что нужно, чтобы сеть могла этому научиться. Все формулы разобраны до числа: каждую можно пересчитать в столбик.

Задача: кот или не кот

Бинарная классификация — это задача с двумя возможными ответами. На вход даётся картинка, на выходе нужно одно из двух: 1 — кот на картинке есть, 0 — кота нет. Никаких промежуточных ответов задача не допускает: либо «да», либо «нет».

Алгоритм, который решает такую задачу, называется классификатор. Роль классификатора здесь играет логистическая регрессия (что у неё внутри — разберем в разделе «Предсказание») — простейшая модель, которую собирают из тех же кирпичей, что и полноценную нейросеть. Это не метафора: все шаги, которые мы здесь пройдём (предсказание, ошибка, градиент), потом переедут в большие сети без изменений.

Прежде чем учить модель, надо понять, что она вообще видит. Компьютер не видит кота — он видит числа.

Картинка превращается в строку чисел

Возьмём картинку 64 на 64 пикселя, цветную. Цветной пиксель — это три числа: сколько красного, сколько зелёного, сколько синего (RGB). Считаем размер:

64×64×3=12288

Двенадцать тысяч двести восемьдесят восемь чисел — вот и вся «картинка» для компьютера. Все эти числа выстраивают в одну длинную строку — вектор признаков и обозначают x. Слово «признак» здесь точное: каждое число — один признак картинки, яркость одного канала одного пикселя. Размер этого вектора называют nx (x — потому что это вход; просто n — общий жаргон): число входных признаков. У нас nx=12288.

Если можешь пересказать: картинка — это не изображение, а вектор из 12288 чисел, — двигаемся дальше.

Нотация: как записываются данные

Эти обозначения будут с нами до самого конца, поэтому зафиксируем их один раз и внимательно.

Один обучающий пример — это пара (x, y):

Верхний индекс в скобках — номер примера: (x(1),y(1)) — первый пример, (x(2),y(2)) — второй, и так до (x(m),y(m)). Это не возведение в степень! Скобки в индексе выбраны ровно для того, чтобы отличать номер примера от степени: x(2) — «второй пример», а x2 было бы «икс в квадрате».

Пара (x, y) — это один «фотоальбом»: вход и подписанный к нему правильный ответ (y). Обучающий набор — стопка из m таких пар. Осталось собрать стопку в одну таблицу.

Если можешь пересказать: один пример = пара (вход, правильный ответ), x — вход, y — метка 1/0, m — сколько примеров, — листай дальше.

Матрица примеров X и матрица ответов Y

Чтобы обучать модель не по одному примеру, а сразу по всем, примеры складывают в матрицы. И здесь есть выбор, который потом экономит массу отладки — выбор неожиданный.

Матрица X: каждый обучающий пример x(i) кладётся в матрицу столбцом, слева направо. Первый пример — первый столбец, второй — второй, последний m-й — последний столбец. Получается матрица высотой nx (число признаков) и шириной m (число примеров). Мини-пример: два примера по три признака, первый — кот (y=1), второй — не кот (y=0):

X=[0.51.00.30.20.90.4]

Первый столбец [0.5,0.3,0.9] — это пример №1 целиком, второй столбец [1.0,0.2,0.4] — пример №2:

X=[x(1)|x(2)|…|x(m)] — размер nx×m.

В Python: X.shape=(nx,m). У нас: 12288 строк, m столбцов.

Неожиданность в том, что интуитивно хочется положить примеры в строки — как в таблице Excel, один пример на строку. Но примеры кладут именно столбцами: с примерами-столбцами операции нейросети записываются одной матричной операцией без транспонирований и циклов. Это соглашение — columns are examples, «примеры — это столбцы» — основа всей реализации нейросетей; запомни его сейчас, чтобы не путаться дальше.

Матрица Y: правильные ответы складываются в одну строку — матрицу 1×m:

Y=[y(1)y(2)…y(m)] — размер 1×m.

В Python: Y.shape == (1, m). Позиция в Y отвечает позиции столбца в X: столбец №7 матрицы X и седьмой элемент Y — вход и ответ одного и того же примера.

Путь данных: картинка 64×64×3 → вектор 12288 чисел → m столбцов собираются в матрицу X (12288 × m); ответы ложатся в строку Y (1 × m)

Если можешь пересказать: примеры лежат в матрице X столбцами, ответы — в строке Y, столбец №X и элемент №Y — один и тот же пример, — листай дальше.

Логистическая регрессия: предсказание

Дано: вектор x из 12288 чисел. Нужно: вероятность того, что на картинке кот, — число от 0 до 1. Обозначается yˆ («y с крышечкой») — предсказание, в отличие от y — правильного ответа.

yˆ=P(y=1|x) — «вероятность того, что y равен 1, при данном входе x». Если модель выдала yˆ=0.7, она говорит: «на 70% уверен, что это кот». То есть 0.7 — это уверенность на шкале от 0 («точно не кот») до 1 («точно кот»), просто записанная долей, а не процентами: 0.7 = 70 из 100.

Внутри предсказания два шага.

Шаг 1 — посчитать число. Каждому признаку назначается свой вес (w) (это не «сколько килограммов» — так называют важность признака) — число, показывающее, насколько этот признак важен для ответа «кот» (яркие усы важны — вес большой; цвет неба не важен — вес около нуля). Веса собираются в вектор w — по одному на каждый из 12288 признаков.

Отдельно живёт одно число b (bias, сдвиг) — добавка, которая двигает черту между «скорее не кот» и «скорее кот»: чем больше b, тем легче модели сказать «кот» при тех же весах (w). Считаем взвешенную сумму (z):

z=wTx+b

Расшифровка до числа: wTx — это w1x1+w2x2+⋯+wnxxnx. Каждый признак умножили на свой вес (w) и всё сложили. T («транспонирование») — техническая поворачивалка. w хранится столбцом, x тоже столбцом. Столбец на столбец умножать нельзя. Поворачиваем w в строку (записываем wT) — и произведение «строка × столбец» даёт одно число. На смысл формулы поворот не влияет — это просто правила записи.

Пример на трёх признаках (не на 12288 — механика та же): x=[0.5,0.3,0.9], w=[1.2,−0.4,2.0], b=−0.9. Тогда:

z=1.2⋅0.5+(−0.4)⋅0.3+2.0⋅0.9+(−0.9)=0.6−0.12+1.8−0.9=1.38

Шаг 2 — сжать число в вероятность. Проблема: z — любое число, от −∞ до +∞, а вероятность должна жить от 0 до 1. Ответ −3.7 не может быть «вероятностью кота». Решение — сигмоида (σ), функция, которая принимает любое число и выдает число от 0 до 1. В формуле встретится число e≈2.718 — математическая константа роста, второй после π знаменитости; вручную считать e−z не нужно — вся сигмоида уже посчитана в таблице ниже:

σ(z)=11+e−z

Таблица значений, чтобы почувствовать поведение:

zσ(z)
−40.018
−20.119
00.500
+20.881
+40.982

Сильный минус — почти 0 («точно не кот»), ноль — ровно 0.5 («не знаю»), сильный плюс — почти 1 («точно кот»). По краям сигмоида (σ) выходит на плато: сколько ни добавляй к z, ответ уже почти не меняется.

Итоговая формула предсказания (ŷ) — оба шага в одном:

yˆ=σ(wTx+b)=σ(z)

Для нашего примера: yˆ=σ(1.38)=0.799 — модель на 80% уверена, что это кот. Если правильный ответ y=1, модель молодец; если y=0 — ошиблась, и сейчас научимся измерять насколько.

Если можешь пересказать: предсказание (ŷ) — это σ от взвешенной суммы (z): веса (w) задают важность признаков, сигмоида (σ) сжимает сумму в вероятность 0..1, — листай дальше.

Функция ошибки L: штраф за один пример

Функция ошибки (loss function) L(yˆ,y) берёт предсказание (ŷ) и правильный ответ (y) и возвращает одно число — насколько велика ошибка. Больше число — хуже ответ. Ноль — идеально.

Нужна функция, которая: при правильном ответе (y) даёт почти ноль, при уверенно-неправильном — огромное число, и при этом плавно растёт между ними.

Кросс-энтропия — стандартная функция ошибки (L) для логистической регрессии:

L(yˆ,y)=−[ylog⁡(yˆ)+(1−y)log⁡(1−yˆ)]

Формула выглядит страшно, но внутри всего два случая. Подставь y=1 (кот) — второе слагаемое умножается на ноль и исчезает:

L=−log⁡(yˆ)

Подставь y=0 (не кот) — исчезает первое:

L=−log⁡(1−yˆ)

Минутка о самом логарифме: log отвечает на вопрос «в какую степень возвести число e, чтобы получить то, что в скобках». Чем меньше число в скобках, тем больше по модулю ответ: ln⁡(0.01)≈−4.6, ln⁡(0.7)≈−0.357, ln⁡(0.98)≈−0.02. Числа для обоих случаев (хочешь проверить — жми на калькуляторе кнопку ln, натуральный логарифм, не log — обычный log считает по основанию 10 и даст другое число):

Обрати внимание на последний пункт: одна и та же уверенность «на 70% кот» для настоящего кота стоит 0.357, а для настоящей не-кота — 1.204. Функция ошибки (L) смотрит не на уверенность, а на уверенность в правильную сторону.

Почему внутри логарифм? Он превращает «штраф за уверенную ошибку» в обрыв: при ŷ → 1 и y = 1 штраф стремится к нулю плавно, а при ŷ → 0 и y = 1 — улетает в бесконечность. Модели дорого ошибаться уверенно и неправильно — именно этого мы и хотим от штрафа (L).

Если можешь пересказать: штраф (L) за один пример — кросс-энтропия; уверенно правильный ответ почти бесплатен, уверенная ошибка почти бесконечно дорога, — листай дальше.

Функция стоимости J: штраф на всём наборе

Loss (L) измеряет ошибку одного примера. Но обучать модель нужно по всем m примерам сразу. Для этого усредняем loss по всему набору — получаем функцию стоимости (J) (cost function):

J(w,b)=1m∑i=1mL(yˆ(i),y(i))

Читается: «сумма ошибок всех примеров, делённая на их количество» — средний штраф (J) на пример. Сумма Σ — просто компактная запись сложения: возьми loss первого, прибавь loss второго, … до m-го, раздели на m.

Разница терминов: loss (L) — про один пример, cost (J) — про весь набор. Дальше ищем не «где меньше ошибка на одной картинке», а «где меньше средняя ошибка на всех картинках».

Числовой пример. Три примера, модель всюду дала yˆ=0.7, правильные ответы — y=1,0,1:

L(1,0.7)=0.357, L(0,0.7)=1.204, L(1,0.7)=0.357

J=0.357+1.204+0.3573=1.9183=0.639

Средний штраф (J) 0.639. Обучение — попытка сделать это число меньше.

Если можешь пересказать: J — среднее арифметическое всех loss'ов (L), один пример на все m не влияет сильно, — листай дальше.

Почему не квадрат разницы

Самый естественный кандидат в ошибки — квадрат разницы: L=(yˆ−y)2/2 (деление на 2 — просто чтобы производная вышла без лишнего множителя; на форму кривой не влияет). Простая, знакомая по школьной алгебре. Но для логистической регрессии она не годится — и почему, одно из самых важных мест всей темы.

Проблема — форма поверхности ошибки. Если нарисовать средний штраф J как функцию весов w и b, для кросс-энтропии получается чаша: любая точка на стенке чаши имеет наклон, ведущий вниз, к единственному дну. Такая форма называется выпуклой (convex) — и это не красота ради красоты: вся идея градиентного спуска из следующего раздела держится на том, что чаша имеет одно дно. Спуск прост: куда ни поставь шарик, скатится в то же дно.

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

Можно убедиться и числом: кривизна кросс-энтропии по z равна yˆ(1−yˆ)... умножь два соседних значения из таблицы выше: 0.119×(1−0.119)=0.105, положительно. У квадрата разницы на сигмоиде (σ) при z=−2 выходит −0.059 — знак сменился, там горб. Пока просто запомни: у кросс-энтропии чаша, у квадрата — горб (что такое производная и откуда эти формулы — следующий раздел). Именно горбы и ловят спуск.

Градиентный спуск: как учатся веса (w)

Итак, есть число J(w, b) — средний штраф. Обучение — поиск весов (w), при которых J минимальна. Перебрать все значения весов (w) невозможно (их 12289). Но у чаши есть свойство, которое всё упрощает: в каждой точке известен наклон — направление наискорейшего спуска.

Наклон в точке — это производная. Обозначения: dJ/dw — «насколько изменится J при малом изменении w». В коде её называют коротко dw. Отдельно — db для сдвига b.

Алгоритм градиентного спуска (gradient descent):

w:=w−αdw,b:=b−αdb

Двоеточие со знаком равно (:=) читается «присвоить новое значение»: посчитай правую часть и запиши результат в w. Это не уравнение — поэтому w и стоит по обе стороны.

α (альфа) — скорость обучения (learning rate): размер шага. Маленькая α — ползём медленно, но точно. Большая — шагами перепрыгиваем через дно туда-сюда. Типичные значения — от 0.001 до 0.1.

Почему «минус»: производная (dJ/dw) указывает, где J растёт. Чтобы штраф уменьшать, идём в обратную сторону.

Числовой пример одного шага. Продолжаем трёхпризнаковый пример: x=[0.5,0.3,0.9], правильный ответ y=1, модель дала yˆ=0.799 (из раздела про предсказание). Ошибка есть — значит, веса (w) стоит поправить.

  1. Сигнал ошибки: dz=yˆ−y=0.799−1=−0.201 (модель недобрала уверенности — сигнал отрицательный, веса (w) должны подрасти; откуда формула — выведем в разделе про backprop, пока принимай как готовую).
  2. Градиенты — тот же наклон из чаши, собранный по всем весам сразу (в коде так и называют: dw, db) — по весам: dw=x⋅dz=[0.5⋅(−0.201),0.3⋅(−0.201),0.9⋅(−0.201)]=[−0.10,−0.06,−0.18], и db=−0.201.
  3. Шаг при α=0.1: w:=w−0.1⋅dw=[1.2,−0.4,2.0]−0.1⋅[−0.10,−0.06,−0.18]=[1.21,−0.394,2.018], b:=−0.9−0.1⋅(−0.201)=−0.88.

Что произошло: признак №1 («важный для кота») получил вес (w) 1.2 → 1.21 — подрос, следующий предсказание будет увереннее. Ровно это, миллионы раз, на 12288 признаках и m примерах, и есть обучение.

Если можешь пересказать: спуск поправляет веса (w) против наклона чаши маленькими шагами α, — листай дальше.

Backprop: откуда берутся производные

Остался последний вопрос: откуда алгоритм берёт dw и db для любой пары (x, y)? Вычислять их честным определением («пошевелил вес (w), посмотрел, как сдвинулся J») — миллионы прогонов на каждый шаг. Выход — цепное правило из матанализа: производная сложной функции = произведение производных по цепочке.

Предсказание — цепочка из трёх шагов: z=wTx+b, затем a=σ(z) (активация; a и есть yˆ), затем L(a, y). Ошибка течёт по этой цепочке назад: от L к a, от a к z, от z к каждому весу (w).

Механику удобно видеть на вычислительном графе (computation graph): кружки-операции, стрелки-зависимости. В качестве игрушечного примера — функция J=3(a+bc). Внимание: буквы здесь ничего не значат — это не «активация» и не bias из предыдущих разделов, просто три входных числа. Возьмём a=2,b=1,c=5:

Для логистической регрессии вся обратная цепочка сворачивается в одну изящную формулу:

dz=a−y

Производная (dz) ошибки по z — это просто «предсказание минус ответ». Проверим честно на числах: z=0.8, значит a=σ(0.8)=0.69, ответ y=1. Численная производная (посчитать L при z=0.800001 и z=0.799999, взять разницу) даёт −0.31; формула даёт a−y=0.69−1=−0.31. Совпало до четвёртого знака — вместо честного перемножения всей цепочки производных по графу достаточно одного вычитания.

Дальше по цепочке от z к весам — производные линейного шага: чуть меняешь вес wi — вся взвешенная сумма z сдвигается ровно на xi (сам признак), поэтому производная dwi=xi⋅dz — признак, умноженный на сигнал ошибки и db=dz. Все три производные нашего числового примера выше посчитаны именно этими формулами.

На m примерах всё то же самое, усреднённое: dw=1m∑ix(i)dz(i), db=1m∑idz(i). Как аккуратно вывести и запрограммировать это усреднение, а затем убрать оставшиеся циклы через матричные операции (векторизацию) — тема следующих разборов.

Если можешь пересказать: производная — это наклон в точке, а backprop собирает её дёшево, от ответа назад по графу, — листай дальше.

Что дальше

Мы прошли весь путь: задача → данные → предсказание → ошибка → оптимизация. Получился полный цикл обучения простейшей нейросети:

картинка → вектор 12288 → z=wTx+b → yˆ=σ(z) → L(yˆ,y) → J по всем m → градиенты dz=a−y → шаг спуска w:=w−αdw → повторить

Каждый элемент этой цепочки останется без изменений в большой нейросети: сигмоида (σ) станет одной из функций активации, dz=a−y — частным случаем общего backprop, один вес (w) и сдвиг (b) — миллионами весов в слоях. Нейросеть — это та же логистическая регрессия, повторённая и соединённая много раз.

Если можешь пересказать: картинка — числа; предсказание (ŷ) — сигмоида (σ) от взвешенной суммы (z); ошибка (L) — кросс-энтропия; обучение — спуск против наклона чаши; производные — одним вычитанием, — значит, тема усвоена.