Задание 3.1 · часть 3 баллов
Функции потерь и градиенты
Бустингу нужно знать, насколько плох текущий прогноз (потеря) и в какую сторону его подвинуть (градиент). Пишем три потери и их градиенты одной формулой numpy каждую.
- Функция потерь L(y, f)
- Штраф за прогноз f, когда правда y. Чем дальше, тем больше штраф.
- Нормировать
- Делить сумму штрафов на число объектов, то есть брать среднее.
- Производная
- Насколько быстро растёт штраф, если чуть-чуть увеличить прогноз.
- Градиент
- Производные по каждому объекту вместе. Показывает, куда двигать прогноз, чтобы штраф рос; нам нужно в обратную сторону (антиградиент).
- Квантиль
- Число, ниже которого лежит заданная доля значений. Квантиль 0.5 = медиана, 0.8 = значение, ниже которого 80% данных.
Три потери на числах из теста
y = [1, 2, 3, 4, 5], f = [1.2, 2.5, 2.8, 3.9, 5.1], ошибки y - f = [-0.2, -0.5, 0.2, 0.1, -0.1].
Поиграй: потеря и градиент
Правда y = 3. Двигай прогноз f и смотри на три потери и их градиенты. Градиент показывает, куда потеря растёт; бустинг двигает прогноз в обратную сторону.
Код
def mse(y_real, y_predicted):
return np.mean((y_real - y_predicted) ** 2, axis=-1)
def mse_gradient(y_real, y_predicted):
return 2 * (y_predicted - y_real)
def mae(y_real, y_predicted):
return np.mean(np.abs(y_real - y_predicted), axis=-1)
def mae_gradient(y_real, y_predicted):
return np.sign(y_predicted - y_real)
def lq(y_real, y_predicted, q=0.5):
diff = y_real - y_predicted
return np.mean(np.where(diff > 0, q, 1 - q) * np.abs(diff), axis=-1)
def lq_gradient(y_real, y_predicted, q=0.5):
return np.where(y_real - y_predicted > 0, q - 1, q)
np.mean(..., axis=-1)среднее по последней оси. Для обычного вектора это одно число. Для матрицы (каждая строка свой вариант прогноза) получается число на строку. Это пригодится, чтобы проверить 201 вариант ρ одной операцией.2 * (y_predicted - y_real)производная (y - f)² по f. Двойка важна, к ней вернёмся в бустинге.np.signзнак числа: 1, -1 или 0. Производная модуля |y - f| по f.np.where(diff > 0, q, 1 - q)для каждого объекта выбираем вес: q, если прогноз занижен, иначе 1 - q. Вместоifвнутри цикла.q=0.5вlqдобавлено значение по умолчанию, чтобы бустинг мог вызвать её с двумя аргументами.
Проверка градиентов численно
shift = eps * np.eye(n) # строка i: сдвигаем только i-е предсказание
numeric = (loss(y_real, y_predicted + shift) - loss(y_real, y_predicted - shift)) / (2 * eps) * n
np.eye(n)единичная матрица 5 × 5. Строка i сдвигает на eps только прогноз i-го объекта.- Разность потерь, делённая на 2·eps, это производная «по определению».
* nпотому что потеря средняя, а тест ждёт градиент каждого объекта без деления на n. - Благодаря
axis=-1все 5 производных считаются сразу, без цикла.
allclose) требует [0.8, 0.8, -0.2, -0.2, 0.8]. Это производная той же потери, но с q и 1 - q, поменянными местами. Настоящая производная формулы из текста [0.2, 0.2, -0.8, -0.8, 0.2]. Мы сделали градиент как в тесте (иначе ноутбук падает), а значение потерь по формуле (0.08, как ждут). Это честно написано в markdown, и Lq дальше не используется.rtol=1e5.Прогноз f = 1.2, правда y = 1. Каков градиент MSE по тесту?
2(f - y) = 2 · 0.2 = 0.4. Положительный: прогноз завышен, двигать вниз.
Вопросы на защите
Где тут циклы?
Нигде. Всё через numpy над целыми массивами: np.mean, np.abs, np.sign, np.where. Даже численная проверка производной без цикла, через матрицу сдвигов.
Чем MAE лучше MSE?
MSE возводит ошибку в квадрат, один выброс с ошибкой 10 даёт штраф 100 и тянет модель на себя. MAE штрафует линейно, поэтому устойчивее к выбросам. Минус: производная не определена в нуле и не говорит, насколько далеко ошибка, только направление.
Зачем нужна Lq?
Чтобы предсказывать не среднее, а квантиль. Например, q = 0.8: занижение штрафуется вчетверо сильнее завышения, поэтому модель «перестраховывается вверх» и предсказывает значение, ниже которого 80% случаев. Полезно для запасов на складе.
Почему градиент не делится на n?
Так ждёт тест: градиент считается для каждого объекта отдельно. Дерево в бустинге учится на этих числах как на целях, масштаб всё равно поправляет ρ.