Задание 1 · основа дерева
Как измерить беспорядок
Дереву нужно число, которое говорит, насколько в куче объектов «всё перемешано». Пишем три таких числа: энтропию и индекс Джини для классов и MSE для чисел.
Идея словами
Дерево делит объекты на две кучки. Хорошее деление то, после которого в каждой кучке почти одинаковые ответы. Чтобы сравнивать деления, нужна мера беспорядка: 0, если в кучке всё одинаковое, и больше, чем сильнее перемешано.
- Доля класса p
- Какая часть кучки относится к классу: 5 чёрных из 6 носков, p = 5/6.
- Энтропия
- Мера неопределённости из теории информации. Измеряется в битах: 1 бит = один вопрос «да/нет», который ещё нужно задать.
- Индекс Джини
- Вероятность ошибиться, если взять случайный объект и приписать ему случайную метку из той же кучки.
- MSE (для регрессии)
- Средний квадрат отклонения от среднего, то есть дисперсия: насколько числа в кучке разбросаны.
- log₂
- Логарифм по основанию 2: в какую степень возвести 2, чтобы получить число. log₂ 8 = 3, потому что 2³ = 8.
Поиграй: корзина носков
Добавляй и убирай носки трёх цветов и смотри, как меняются энтропия и Джини. Попробуй: все одного цвета; поровну двух цветов; поровну трёх.
энтропия (от 0 до 1.585)
Джини (от 0 до 0.667)
Формулы с числами
Кучка [0, 0, 0, 0, 0, 1]: пять нулей и одна единица, доли 5/6 и 1/6.
Код (ячейка с функционалами)
def entropy(y): # y - массив значений целевой переменной
_, counts = np.unique(y, return_counts=True)
p = counts / counts.sum() # доли классов p_k
return np.sum(p * np.log2(1 / p)) # то же, что -sum(p * log2(p)), но чистый узел даёт 0.0, а не -0.0
def gini_index(y):
_, counts = np.unique(y, return_counts=True)
p = counts / counts.sum()
return 1 - np.sum(p ** 2)
def mse(y):
y = np.asarray(y, dtype=float)
return np.mean((y - y.mean()) ** 2)
np.unique(y, return_counts=True)находит разные значения и сколько раз каждое встретилось. Для[0,0,0,0,0,1]это значения[0, 1]и счётчики[5, 1]. Значения нам не нужны, поэтому_.p = counts / counts.sum()делим счётчики на общее число:[5/6, 1/6]. Делится сразу весь массив, без цикла.np.sum(p * np.log2(1 / p))формула энтропии. log₂(1/p) = -log₂(p), поэтому минус уходит внутрь. Зачем: для чистой кучки p = 1, и обычная формула даёт «-0.0», а тест сравнивает с 0.0 через==. Так получается ровно 0.0.1 - np.sum(p ** 2)Джини: единица минус сумма квадратов долей.np.asarray(y, dtype=float)превращаем список в массив numpy, чтобы вычитать из него число.np.mean((y - y.mean()) ** 2)среднее квадратов отклонений от среднего.
rtol=1e5, то есть допустимая ошибка в 100 000 раз больше самого числа: пройдёт почти что угодно. Поэтому сразу после тестов добавлена ячейка, которая печатает настоящие значения: 0.6500, 1.5850, 0.2778, 0.6667, 0.6667, 66.6667. В тесте числа просто обрезаны до 0.65, 1.58, 0.27, 0.66.for по объектам тут был бы ошибкой стиля: numpy умеет делать операцию сразу над всем массивом.В узле 10 объектов, все класса 1. Чему равна энтропия?
Все одного класса, доля p = 1, log₂1 = 0. Беспорядка нет, такой узел дальше не делят (purity).
Что показывает MSE в узле регрессии?
MSE от среднего это дисперсия: маленькая, если числа в узле близки друг к другу.
Вопросы на защите
Почему энтропия чистой кучки равна 0?
Там один класс с долей p = 1, а log₂(1) = 0. Значит, 1 · 0 = 0: неопределённости нет.
Какой максимум у энтропии для 2 и 3 классов?
При равных долях: log₂2 = 1 бит для двух классов, log₂3 = 1.585 для трёх. У Джини максимум 0.5 и 0.667.
Чем энтропия отличается от Джини на практике?
Почти ничем: обе нулевые для чистой кучки и максимальные при равных долях. Джини чуть быстрее (нет логарифма). У нас на ирисах они дали почти одинаковую точность.
Почему MSE подходит для регрессии?
В регрессии нет классов, ответы это числа. «Порядок» означает, что числа близки друг к другу, а это и есть маленькая дисперсия (MSE от среднего).