Линейная регрессия
Первая модель, которую вы обучите целиком своими руками, — от формулы y = Xw + b и MSE до аналитического решения, градиентного спуска и нормализации признаков.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Линейная регрессиялинейная регрессияEN linear regression — первая модель, которую вы обучите целиком: сформулируете модель, выберете функцию потерь, найдёте веса и подготовите данные. Ровно тот же цикл потом повторится для LLMбольшая языковая модель (БЯМ, LLM)EN large language model (LLM)В китайском «большая модель» («да мосин») — разговорное общее название для LLM и вообще foundation models. на миллиардах параметров. Разница в масштабе, а не в идее.
Вторая причина: формула — это атом нейросети. Когда выходов несколько, вектор весов становится
матрицей, и получается линейный слой из урока 1.1.2:
(транспонирование — потому что nn.Linear хранит веса в форме «выходы × входы»). Внутри LLM
таких слоёв сотни: проекции Q, K, V во внимании, слои FFN, выходной слой, который выдаёт логиты по всему словарю.
Эти названия — детали трансформера со ступени 4, запоминать их сейчас не нужно. Важно другое: большая часть
вычислений в LLM — матричные умножения, и в основном именно в таких слоях. Разберёте одну формулу досконально —
поймёте, что происходит в каждом из них.
Третья причина практическая. Линейная регрессия — рабочий инструмент инженера: оценить скорость генерации модели по замерам, подогнать закон масштабирования, найти тренд в метриках. Сквозной пример урока — как раз такая задача.
Сквозной пример: сколько ждать ответа модели
Заголовок раздела «Сквозной пример: сколько ждать ответа модели»Вы подняли локальную модель и замерили время ответа на пяти запросах с разной длиной ответа (числа синтетические, но правдоподобные):
| Токенов в ответе | 100 | 200 | 300 | 400 | 500 |
|---|---|---|---|---|---|
| Время, с | 2.4 | 4.5 | 6.3 | 8.6 | 10.2 |
Гипотеза: время растёт линейно, то есть точки лежат почти на прямой (линейная функция и наклон — урок 1.0.1). У обоих параметров есть смысл.
- — наклон, секунды на один токен. Обратная величина — скорость генерации в токенах в секунду (её ещё называют скоростью декодирования).
- — сдвиг, накладные расходы: передача по сети, обработка промпта (текста запроса), время до первого токена.
Задача — найти и по данным. Это и есть обучение.
Модель: ŷ = Xw + b
Заголовок раздела «Модель: ŷ = Xw + b»Обычно признаковпризнакEN featureОдно измеримое свойство объекта, вход модели. «Feature engineering» — конструирование признаков. больше одного. Время ответа зависит и от длины ответа, и от длины промпта, и от размера батча. Для признаков модель — это взвешенная сумма:
Каждый признак умножаем на свой вес, всё складываем и прибавляем сдвиг. Запись — это то же скалярное произведение из урока 1.1.1: перемножить попарно и сложить (значок — транспонирование, урок 1.1.2). Пример с двумя признаками: в ответе токенов, в промпте токенов, веса , . Тогда с.
Для примеров сразу складываем признаки в матрицу , по строке на пример:
Справа — формы (урок 1.0.3): — таблица из строк (примеров) и столбцов (признаков), — весов, — одно число, — предсказаний, по одному на пример. Умножение матрицы на вектор считает сразу для всех строк.
Сдвиг прибавляется к каждому элементу — это broadcastingbroadcasting (растягивание форм)EN broadcastingПравило, по которому NumPy и PyTorch выполняют операции над массивами разных форм без копирования данных. из урока 0.3.2. Полезный трюк: допишите к столбец из единиц, а — последним элементом в . Тогда модель становится чистым , и формулы короче. Например, строка превращается в , веса — в , и — ровно прежнее предсказание.
import numpy as np
X = np.array([[100.0], [200.0], [300.0], [400.0], [500.0]]) # (5, 1): 5 примеров, 1 признакy = np.array([2.4, 4.5, 6.3, 8.6, 10.2]) # (5,): правильные ответыw, b = np.array([0.02]), 0.0 # веса «на глаз»: 0.02 с на токенy_hat = X @ w + b # (5,) — все предсказания разомprint(y_hat)[ 2. 4. 6. 8. 10.]Функция потерь: MSE
Заголовок раздела «Функция потерь: MSE»Ошибка на примере — остаток . Чтобы свести остатки в одно число, возводим их в квадрат и усредняем. Получаем среднеквадратичную ошибку (MSE)среднеквадратичная ошибка (MSE)EN mean squared error (MSE)Корень из MSE — RMSE (root mean squared error), измеряется в тех же единицах, что и целевая переменная.:
Почему именно квадрат:
- он гладкий, у него везде есть производная (у модуля в нуле её нет);
- большие ошибки штрафуются сильнее: ошибка 2 с стоит вчетверо дороже ошибки 1 с;
- для линейной модели MSE — выпуклаявыпуклая функцияEN convex function«Чаша» без ложных ям — любой локальный минимум глобальный. Отрезок между двумя точками графика лежит не ниже графика. квадратичная чаша (урок 1.4.1): ложных минимумов нет, а если признаки не дублируют друг друга, минимум единственный.
Проверим на числах. Возьмём «на глаз» , . Предсказания: 2, 4, 6, 8, 10 секунд. Остатки: . Тогда . Лучшая прямая, которую мы сейчас найдём, даёт — в 10 раз меньше. Корень из MSE (RMSE) измеряется в секундах: с. Это типичная ошибка прогноза.
Для любознательных: откуда берётся квадрат
Заголовок раздела «Для любознательных: откуда берётся квадрат»Этот раздел отвечает на вопрос «почему именно квадрат, а не что-то другое?». При первом чтении достаточно вывода в конце раздела (он выделен жирным), а сам вывод можно пробежать глазами.
В уроке 1.3.2 вы видели, что cross-entropy — это отрицательное лог-правдоподобие. С MSE та же история. Предположим, что данные порождены так: , где шум имеет нормальное распределениенормальное (гауссовское) распределениеEN normal (Gaussian) distribution«Колокол» N(μ, σ²). Правило 68–95–99.7 — доля значений в пределах 1, 2 и 3 σ от среднего. — «колокол» с центром 0 и шириной из урока 1.3.1. Тогда при известном — это колокол с центром в предсказании модели. Запись читается «плотность вероятности при условии »:
Берём минус логарифм. Экспонента и логарифм взаимно уничтожаются:
Примеры независимы, поэтому правдоподобие всей выборки — произведение вероятностей, а его логарифм — сумма: . Положительный множитель и константа не меняют того, где находится минимум. Значит, максимизировать правдоподобие данных — то же самое, что минимизировать сумму квадратов ошибок. MSE — это MLE при гауссовском шуме. Если шум другой, например с редкими огромными выбросами, лучше подойдут другие потери (MAE — в уроке 2.2.3).
Способ 1: аналитическое решение
Заголовок раздела «Способ 1: аналитическое решение»Идея: минимум гладкой чаши — там, где она плоская, то есть где все производные равны нулю (как у параболы в уроке 1.2.1). Спрячем в трюком со столбцом единиц. Градиент MSE — вектор частных производных по каждому весу (урок 1.2.3, там же выводилась формула ):
В минимуме градиент равен нулю. Получаем нормальное уравнениенормальное уравнениеEN normal equationXᵀXw = Xᵀy — условие минимума MSE. Его решение называют аналитическим решением (closed-form solution).:
Здесь — обратная матрица из урока 1.1.3: она «отменяет» умножение на , как деление отменяет умножение на число.
У равенства есть красивый смысл. Вектор остатков ортогонален каждому признаку (ортогональныортогональные (перпендикулярные) векторыEN orthogonal vectorsВекторы, скалярное произведение которых равно нулю. Ортогональная матрица — та, у которой столбцы единичные и попарно ортогональны. — значит, скалярное произведение равно нулю, урок 1.1.1). В остатках не осталось ничего, что признаки могли бы объяснить линейно: мы «выжали» из данных всё, что доступно прямой.
Для одного признака нормальное уравнение — это система из двух уравнений с двумя неизвестными. Её решение записывается совсем просто: и (пошаговый вывод — в «Тыквенной книге», глава 3). Что здесь написано:
- и («икс с чертой», «игрек с чертой») — средние значения;
- — дисперсия : среднее от , разброс признака;
- — ковариация: среднее от . Она положительна, если и растут вместе, и отрицательна, если один растёт, когда другой падает.
В отношении общий множитель сокращается, поэтому дальше считаем просто суммы. Считаем по таблице.
- Средние: , .
- Отклонения : . Отклонения : .
- и .
- с/токен, с.
Итог: модель генерирует примерно токен в секунду и тратит около полсекунды на старт. Ответ на 1000 токенов займёт с.
Проверим ортогональность на этих числах. Остатки лучшей прямой равны . Их сумма — ноль: остатки ортогональны столбцу единиц, то есть прямая в среднем не завышает и не занижает. Сумма — они ортогональны и признаку .
Почему на практике не пишут inv
Заголовок раздела «Почему на практике не пишут inv»В NumPy формулу не переписывают буква в букву, а зовут готовую функцию np.linalg.lstsq (least squares —
«наименьшие квадраты», МНК). Продолжение кода выше:
A = np.c_[X, np.ones(len(X))] # np.c_ склеивает столбцы: справа приписали столбец единиц для bw_b, *_ = np.linalg.lstsq(A, y, rcond=None) # МНК: устойчиво и точно# lstsq возвращает 4 значения; нам нужно первое (веса), а «*_» собирает остальные в ненужную переменнуюprint(w_b) # [w, b][0.0197 0.49 ]Тот же ответ, что мы получили вручную: , .
np.linalg.lstsq решает ту же задачу, но не обращает матрицу явно. Явное inv(X.T @ X) теряет точность,
когда признаки почти линейно зависимы (скажем, длина ответа в токенах и она же в символах). А если один признак
точно выражается через другие (длина в токенах и в тысячах токенов) или признаков больше, чем примеров (),
матрица вырождена: обратной у неё нет, а минимумов MSE бесконечно много. lstsq и тогда вернёт ответ —
решение с наименьшей нормой. Другой выход — L2-регуляризациярегуляризацияEN regularization, то есть
гребневая регрессия (ridge)гребневая регрессия (ridge), L2-регуляризацияEN ridge regressionШтраф λ‖w‖² к функции потерь. В нейросетях та же идея называется weight decay. из урока 2.1.2:
. Добавка делает матрицу обратимой. (Для любознательных: собственные
числа из урока 1.1.3 неотрицательны, а сдвигает каждое из них на , так что нулевых
не остаётся.)
Главное ограничение — цена. Её записывают через «О большое» (сложность алгоритмасложность алгоритма (O-нотация)EN time complexity, big-O notationКак растёт время работы с размером данных n. O(n) — пропорционально n (поиск в list), O(1) — не зависит от n (поиск в dict и set).): значит «число операций растёт как » — при вдвое большем работы в раз больше. Построить стоит , решить систему — . Для это доли секунды. Для нейросети с параметров операций — невозможно. И у нейросети вообще нет готовой формулы для лучших весов, как нормальное уравнение: модель нелинейна. Поэтому нужен второй способ.
Способ 2: градиентный спуск
Заголовок раздела «Способ 2: градиентный спуск»Градиентный спускградиентный спускEN gradient descent из урока 1.4.1 работает для любой дифференцируемой модели. Именно им учат все LLM. Для линейной регрессии один шаг выглядит так:
- Посчитать предсказания и остатки .
- Посчитать градиенты: , .
- Шагнуть против градиента: , .
Здесь («эта») — скорость обученияскорость обученияEN learning rate, размер шага из урока 1.4.1, а стрелка значит
«заменить старое значение новым», как w = w - lr * dw в Python. Посмотрите на градиент по : каждый признак
взвешен остатком. Где модель сильно ошиблась, там вклад примера в обновление больше.
Один шаг на наших данных. Старт: , , . Все предсказания равны 0, остатки : . Тогда , и . Шаг против градиента: , . Оба параметра двинулись в нужную сторону — вверх. Но заметьте: градиент по в 360 раз меньше, чем по , и сдвинулся в 360 раз меньше. Это не случайность, а главная беда этого раздела — о ней ниже.
Запустим спуск на наших данных «как есть», с в токенах, из точки , . И сразу упрёмся в проблему.
| Learning rate | Шагов | Что получилось |
|---|---|---|
| 100 | Расходится: MSE ≈ и растёт дальше | |
| 1000 | , , MSE = 0.062 — в 3.4 раза хуже оптимума | |
| , , MSE = 0.0183 — почти дошли |
Learning rate уже слишком велик, а при сдвиг еле ползёт: за тысячу шагов он сдвинулся на полторы тысячных из нужных 0.49. Почему так?
Нормализация признаков
Заголовок раздела «Нормализация признаков»Диагноз: овраг
Заголовок раздела «Диагноз: овраг»Градиент по содержит множитель , а градиент по — множитель 1. Поверхность потерь круто обрывается вдоль и почти плоская вдоль . Это узкий длинный овраг — такой же, как в уроке 1.4.1, только намного уже.
Чем это плохо, видно без всяких формул. Шаг один на оба параметра. Если сделать его большим, спуск «перелетит» крутой склон по и разойдётся — первая строка таблицы. Поэтому шаг приходится брать крошечным, под самое крутое направление. Но с таким шагом по пологому направлению модель ползёт еле-еле — вторая строка. Вывод: признаки в разных масштабах (сотни и единицы) превращают чашу в овраг, и спуск становится очень медленным.
Для любознательных: точный расчёт. Следующие абзацы показывают, откуда берутся числа «605 000» и «миллион шагов». При первом чтении их можно пропустить и перейти к «Лечению». Кривизна — это крутизна стенок чаши: насколько быстро растёт loss, когда отходим от минимума. В уроке 1.4.1 было правило: для чаши с кривизной спуск устойчив только при . Когда параметров несколько, кривизну описывает матрица вторых производных — гессиан . У MSE он не зависит от весов: (здесь уже со столбцом единиц). Для наших данных и , поэтому
Собственные числасобственное числоEN eigenvalueЧисло λ, для которого Av = λv при некотором ненулевом v. Показывает, во сколько раз матрица растягивает «свои» направления. гессиана — это кривизны вдоль главных осей оврага (урок 1.1.3). У нас и . Их отношение — число обусловленностичисло обусловленностиEN condition numberОтношение самой большой кривизны к самой маленькой. Большое число обусловленности — вытянутый «овраг», где градиентный спуск зигзагует. — около 605 000. Для сравнения: у оврага из урока 1.4.1 оно было 20.
Шаг ограничен самым крутым направлением: . При допустимом шаге отклонение от минимума вдоль пологого направления за шаг умножается всего на . Чтобы это отклонение уменьшилось хотя бы в раза, нужно около шагов. Отсюда и миллион.
Кстати, это же число объясняет, почему inv(X.T @ X) проигрывает lstsq. Число обусловленности
равно квадрату числа обусловленности самой : у нас . Грубо говоря, при решении системы теряется
столько десятичных знаков, каков порядок этого числа. Через это почти 6 знаков из примерно 16, которые
хранит float64, а lstsq работает с самой и теряет около 3.
Лечение: стандартизация
Заголовок раздела «Лечение: стандартизация»Стандартизация признаковстандартизация признаковEN feature standardization (z-score)(x − μ)/σ. Не путайте с нормализацией (normalization) — обычно это min-max масштабирование в [0, 1]; в китайских текстах эти термины иногда смешивают.: из каждого признака вычитаем среднее и делим на стандартное отклонение (оба понятия — из урока 0.3.2, там вы уже стандартизовали столбцы матрицы):
Здесь («мю») — среднее признака, («сигма») — его стандартное отклонение. После такого преобразования у каждого признака среднее 0 и разброс 1, в каких бы единицах он ни был раньше. У нас , , и признак превращается в : например, . Теперь оба признака — и столбец единиц для — одного масштаба, и овраг исчезает.
(Для любознательных: и , поэтому в формуле гессиана выше . Оба собственных числа равны 2, число обусловленности — 1.) Овраг превратился в круглую чашу: градиент указывает прямо в минимум.
| Сырые признаки | Стандартизированные | |
|---|---|---|
| Число обусловленности | ≈ 605 000 | 1 |
| Граница устойчивости | ≈ | 1.0 |
| Шагов до MSE = 0.0182 (4 знака) | ≈ при | 31 при |
Проверьте таблицу сами — хватит десятка строк на NumPy:
import numpy as np
x = np.array([100.0, 200.0, 300.0, 400.0, 500.0])y = np.array([2.4, 4.5, 6.3, 8.6, 10.2])
def gd(x, y, lr, steps): """Полный градиентный спуск для ŷ = w·x + b из точки w = b = 0.""" w, b = 0.0, 0.0 for _ in range(steps): # «_» — номер шага нам не нужен r = w * x + b - y # остатки для всех 5 точек сразу w -= lr * 2 * np.mean(r * x) # dL/dw = (2/n)·Σ rᵢ·xᵢ; «w -= a» значит «w = w - a» b -= lr * 2 * np.mean(r) # dL/db = (2/n)·Σ rᵢ return w, b, np.mean((w * x + b - y) ** 2) # итоговые веса и MSE
# Для любознательных: гессиан для сырых признаков и его собственные числаA = np.c_[x, np.ones(5)] # столбец единиц для bH = 2 / 5 * A.T @ Alam = np.linalg.eigvalsh(H) # собственные числа по возрастанию (урок 1.1.3)print(f"λmin = {lam[0]:.2f}, λmax = {lam[1]:.0f}, число обусловленности ≈ {lam[1] / lam[0]:.0f}")
# Строка % (a, b, c) — старый способ форматирования: каждое %.4f заменяется числом с 4 знакамиprint("сырые, lr=8e-6, 1000 шагов: w = %.4f, b = %.4f, MSE = %.4f" % gd(x, y, 8e-6, 1000))xs = (x - x.mean()) / x.std() # стандартизация: среднее 0, std 1print("станд., lr=0.1, 31 шаг: w' = %.4f, b' = %.4f, MSE = %.4f" % gd(xs, y, 0.1, 31))λmin = 0.36, λmax = 220002, число обусловленности ≈ 605009сырые, lr=8e-6, 1000 шагов: w = 0.0210, b = 0.0015, MSE = 0.0616станд., lr=0.1, 31 шаг: w' = 2.7832, b' = 6.3937, MSE = 0.0182На сырых данных за тысячу шагов почти не сдвинулся. На стандартизированных за 31 шаг и подошли к оптимуму и (их смысл — чуть ниже). Замените 1000 на 1 000 000 — и примерно через 10 секунд получите третью строку первой таблицы.
При спуск вообще приходит в минимум за один шаг: это идеальный шаг из урока 1.4.1. После стандартизации кривизна чаши по всем направлениям (отсюда и граница в таблице), так что , и в круглой чаше этот шаг один на все направления. При модель прыгает между двумя стенками чаши, при — расходится.
Посмотрите на это своими глазами в песочнице из урока 1.4.1. Ниже уже выбрана функция «Овраг». Поставьте скорость обучения около 0.45 (чуть ниже границы ) и 10 шагов. Красная траектория SGD скачет поперёк оврага и остаётся далеко от звёздочки. Теперь переключитесь на «Чашу». При смене функции песочница сбрасывает скорость обучения, поэтому снова поставьте 0.45: путь идёт почти прямо в минимум. Синюю и жёлтую траектории (Momentum и Adam) можно выключить галочками. Овраг в песочнице — уменьшенная копия наших сырых признаков: отношение кривизн там 20, у нас 605 000. Чаша — то, что даёт стандартизация.
Обученные на веса легко вернуть в исходный масштаб. Модель на стандартизированных данных — это . Раскроем скобки и соберём слагаемые с и без него:
У нас , , откуда и . Ответ совпал с аналитическим.
flowchart LR A["Данные"] --> B["train / val / test"] B --> C["μ, σ — только по train"] C --> D["Стандартизация train, val и test<br/>одними и теми же μ, σ"] D --> E["Градиентный спуск<br/>на train"] E --> G["Оценка на val / test"] E -.-> F["Веса → исходный масштаб<br/>(чтобы читать их смысл)"]
Та же идея живёт внутри каждой LLM. LayerNormнормализация слояEN layer normalization и RMSNorm нормируют уже не входные признаки, а активации на каждом слое. Иначе глубокая сеть превращается в каскад оврагов. Подробно — в уроках 3.4.1 и 4.5.3.
Практика
Заголовок раздела «Практика»Соберите линейную регрессию целиком: предсказание, MSE, градиенты, стандартизацию, спуск и возврат весов в исходный масштаб. В заготовке шесть функций — делайте их сверху вниз, каждая опирается на предыдущие. Все формулы уже есть в докстрингах и в разделах урока выше, задача — перевести их в NumPy. Последний тест — пример из урока: должны получиться и .
import numpy as np
def predict(X, w, b):
"""Предсказания линейной модели: ŷ = X·w + b.
X — матрица признаков формы (n, d), w — вектор весов формы (d,), b — число.
Возвращает вектор формы (n,).
"""
# TODO: одна строка
...
def mse(y_pred, y):
"""Среднеквадратичная ошибка: mean((ŷ − y)²). Возвращает число (float)."""
# TODO
...
def gradients(X, y, w, b):
"""Градиенты MSE по w и по b.
dL/dw = (2/n) · Xᵀ(ŷ − y)
dL/db = (2/n) · Σ(ŷ − y)
Возвращает кортеж (dw, db): dw — вектор формы (d,), db — число.
"""
# TODO
...
def standardize(X, mean=None, std=None):
"""Стандартизация признаков по столбцам: (X − mean) / std.
Если mean и std не переданы — посчитайте их по X (это обучающая выборка).
Если переданы — используйте их (так преобразуют валидацию и тест).
Столбец-константу (std == 0) делите не на 0, а на 1.
Возвращает кортеж (X_std, mean, std).
"""
# TODO
...
def fit_gd(X, y, lr=0.1, steps=500):
"""Обучает линейную регрессию полным градиентным спуском.
Старт: w — нули формы (d,), b = 0.0.
На каждом шаге: градиенты → шаг против градиента → записать MSE в history.
Возвращает (w, b, history), где history — список из steps чисел.
"""
# TODO
...
def to_original_scale(w_std, b_std, mean, std):
"""Пересчитывает веса, обученные на стандартизированных признаках, в исходный масштаб.
На стандартизированных данных: ŷ = Σ_j w_std[j] · (x[j] − mean[j]) / std[j] + b_std.
Раскройте скобки и верните (w, b), для которых ŷ = X·w + b на ИСХОДНЫХ X.
"""
# TODO
...
# Проверьте себя кнопкой «Запустить»:
X_demo = np.array([[100.0], [200.0], [300.0]])
print(predict(X_demo, np.array([0.02]), 0.5)) # ожидается [2.5 4.5 6.5]
print(mse(np.array([2.5, 4.5]), np.array([2.4, 4.5]))) # ожидается ≈ 0.005
То же на PyTorch (локально)
Заголовок раздела «То же на PyTorch (локально)»Теперь два признака: длина промпта и длина ответа. Обработка промпта (prefill) идёт параллельно и стоит дёшево на токен, генерация (decode) — последовательно и дорого. Решим задачу обоими способами. Запустите у себя.
PyTorch вы видели в уроках 1.1.2 (nn.Linear) и
1.4.1 (цикл zero_grad → backward → step). Тензор
PyTorch устроен как массив NumPy, и многие функции называются похоже; незнакомые объяснены в комментариях.
import torch
torch.manual_seed(0)n = 256n_in = torch.randint(100, 8000, (n,)).float() # токены промпта: 256 случайных целых от 100 до 7999n_out = torch.randint(10, 500, (n,)).float() # токены ответа; .float() — перевести в дробные числа# «Истина» (синтетика): 0.2 мс на токен промпта, 20 мс на токен ответа, 0.3 с накладныхt = 0.0002 * n_in + 0.02 * n_out + 0.3 + 0.05 * torch.randn(n) # + шум со std 0.05 с
X = torch.stack([n_in, n_out], dim=1) # два вектора → таблица (256, 2): по столбцу на признакy = t.unsqueeze(1) # (256,) → (256, 1): так ждёт nn.Linear
# Способ 1. Аналитическое решение: МНК через lstsq (столбец единиц отвечает за b)A = torch.cat([X, torch.ones(n, 1)], dim=1) # cat — склеить по столбцам, как np.c_w_in, w_out, b = torch.linalg.lstsq(A, y).solution.squeeze().tolist() # три числа в обычный список Pythonprint(f"lstsq: w_in = {w_in:.6f}, w_out = {w_out:.5f}, b = {b:.3f}")
# Способ 2. Градиентный спуск: стандартизация + nn.Linear + SGDmean, std = X.mean(0), X.std(0) # torch.std делит на n − 1, NumPy — на n; для стандартизации это не важноXs = (X - mean) / stdmodel = torch.nn.Linear(2, 1) # ŷ = Xw + b с двумя весами и одним сдвигомopt = torch.optim.SGD(model.parameters(), lr=0.1) # SGD здесь на полном батче: все 256 примеров на каждом шагеfor step in range(200): loss = torch.nn.functional.mse_loss(model(Xs), y) # MSE предсказаний opt.zero_grad() # стереть старые градиенты loss.backward() # посчитать градиенты (PyTorch делает это сам) opt.step() # шаг: w ← w − η·∇L if step in (0, 9, 199): print(f"шаг {step + 1:3d}: MSE = {loss.item():.4f}")
# Веса обратно в исходный масштаб: w = w'/σ, b = b' − Σ w'·μ/σ# .detach() — «просто числа» без истории для градиентов; .squeeze() убирает лишние оси размера 1w_std, b_std = model.weight.detach().squeeze(), model.bias.detach()w = w_std / stdb = b_std - (w_std * mean / std).sum()print(f"SGD: w_in = {w[0]:.6f}, w_out = {w[1]:.5f}, b = {b.item():.3f}")Ожидаемый вывод (в другой версии PyTorch последние знаки могут отличаться):
lstsq: w_in = 0.000200, w_out = 0.01999, b = 0.304шаг 1: MSE = 45.7267шаг 10: MSE = 0.8553шаг 200: MSE = 0.0028SGD: w_in = 0.000200, w_out = 0.01999, b = 0.304Оба способа нашли «истину»: 0.2 мс на токен промпта и 20 мс на токен ответа. Финальная MSE ≈ 0.0028 почти равна
дисперсии шума — лучше не бывает. nn.Linear(2, 1) — та самая . В трансформере таких
слоёв сотни, только вместо там и больше.
Бонус: закон масштабирования — тоже линейная регрессия
Заголовок раздела «Бонус: закон масштабирования — тоже линейная регрессия»Многие зависимости в LLM степенные: . Логарифмируем и пользуемся свойствами логарифма из урока 1.0.2 (логарифм произведения — сумма логарифмов, логарифм степени — показатель, умноженный на логарифм): . Если обозначить и , получится — обычная прямая с наклоном и сдвигом . Её коэффициенты находит линейная регрессия.
Здесь — вычислительный бюджет прогона в FLOPs: сколько арифметических операций с дробными числами потрачено
на обучение. Числа вида 1e17 — научная запись из урока 1.0.2: .
import numpy as np
# Синтетика: «лучший» learning rate в 6 маленьких прогонах с разным бюджетом C (FLOPs)C = np.array([1e17, 3e17, 1e18, 3e18, 1e19, 3e19])rng = np.random.default_rng(0)lr_best = 2.0 * C ** -0.15 * np.exp(rng.normal(0, 0.05, C.size)) # закон + шум
# Степенной закон lr = a·C^k ⇔ log lr = log a + k·log C — это прямая!k, log_a = np.polyfit(np.log(C), np.log(lr_best), deg=1) # линейная регрессияprint(f"k = {k:.3f}, a = {np.exp(log_a):.2f}")print(f"прогноз для C = 1e22: lr ≈ {np.exp(log_a) * 1e22 ** k:.2e}")k = -0.151, a = 2.07прогноз для C = 1e22: lr ≈ 1.00e-03Числа здесь выдуманы, но приём настоящий: так по дешёвым маленьким прогонам предсказывают гиперпараметры для дорогого большого. Подробно законы масштабированиязаконы масштабированияEN scaling laws разберём на ступени 5.
Проверь себя
Заголовок раздела «Проверь себя»- Линейная регрессия: , потери — MSE. MSE — это MLE при гауссовском шуме.
- Аналитическое решение — нормальное уравнение . Решайте через
lstsq. Оно точное, но стоит и существует только для линейной модели. - Градиентный спуск: . Работает для любой модели, включая LLM.
- Стандартизация превращает овраг в круглую чашу: в примере урока больше миллиона шагов превратились в 31. Статистики — только по train.
nn.Linear— это , атом любой нейросети.
Дальше: логистическая и softmax-регрессия — та же линейная модель, но для классификации. Добавим сигмоиду и cross-entropy и получим однослойную нейросеть — в точности выходной слой LLM.