Введение

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

Без лишних слов, давайте приступим к кодированию (в Colab)!

Настройка гиперпараметров

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

Во-первых, нам нужно написать функцию, которая вычисляет оценку нашей модели как по обучающим данным, так и по набору проверки (мы не будем использовать перекрестную проверку по причинам, обсуждаемым в этой статье):

def rmse(x, y): return math.sqrt(((x-y)**2).mean())
def print_score(m, x_trn, y_trn, x_vld, y_vld):
    r = [rmse(m.predict(x_trn), y_trn), rmse(m.predict(x_vld), y_vld)]
    print(r)

Затем мы собираемся создать случайный лес с 40 оценщиками (деревьями решений), но все остальные гиперпараметры установлены по умолчанию:

# We're using only the last 50000 rows of our training data
# to speed up the process of training. Using set_rf_samples(50000)
# on the entire dataset yielded a slightly worse accuracy.
m = RandomForestRegressor(n_jobs=-1, n_estimators=40)
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)

Output:
[0.08229152597254322, 0.24309588692487768]

Таким образом, базовый уровень нашей оценки на проверочном наборе составляет 0,243. Обратите внимание, что RMSE 0,243 может поставить нас на 24-е место в (частной) таблице лидеров Kaggle, но мы можем добиться большего успеха с помощью всего нескольких простых модификаций. Для этого нам сначала нужно настроить наши гиперпараметры. Типичным методом, который используют люди, является GridSearchCV от Scikit-learn, в котором используется перекрестная проверка, поэтому для нас это неприемлемый вариант, поскольку мы имеем дело с временными данными. Итак, мы собираемся оптимизировать наши гиперпараметры вручную, и, поскольку мы собираемся настраивать только 2 гиперпараметра, это не так утомительно, как может показаться.

Начнем с min_samples_leaf. Значение по умолчанию — 1, а другие возможные значения, которые могут оказаться лучше, — 3, 5, 10, 25 и 100. Мы начнем увеличивать min_samples_leaf, но если наш результат ухудшится, мы остановимся.

m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=3)
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)

Output:
[0.12044783818370393, 0.23799771523795898]
m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5)
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)

Output:
[0.14848213253852324, 0.23774430541765534]
m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=10)
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)
Output:
[0.18479646798842547, 0.23928276460961012]

Вы можете видеть, что 5 — наш лучший выбор для min_samples_leaf, но обратите внимание, что 3 — это второе место. Наша базовая оценка теперь составляет 0,238.

Теперь пришло время настроить max_features. Хорошими значениями являются 0,5, квадратный корень из длины нашего набора данных и log_2 длины нашего набора данных. Мы собираемся следовать той же стратегии, что и ранее (однако мы будем использовать для min_samples_leaf значение 5, а не 1):

m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5, max_features=0.5)
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)

Output:
[0.15294552848666595, 0.23507582565405524]
m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5, max_features='sqrt')
m.fit(X_train.iloc[-50000:], y_train[-50000:])
print_score(m, X_train.iloc[-50000:], y_train[-50000:], X_valid, y_valid)

Output:
[0.19083869962629552, 0.2567332561823771]

При значении min_samples_leaf, равном 5, наилучший результат, которого мы могли достичь, составлял около 0,235 при значении max_features, равном 0,5. Теперь пришло время избавиться от ненужных функций.

Основы выбора функций

Выбор признаков — очень простая, но мощная идея. Как следует из названия, вы выбираете только те функции, которые наиболее полезны для вашей проблемы, что равносильно избавлению от избыточных данных. Обратите внимание, что по определению избыточные функции не очень помогают нашей модели, поэтому избавление от них не должно снижать нашу точность. Есть 3 основные причины для выбора функции:

  1. Чем меньше функций, тем меньше времени требуется нашей модели для обучения. Это может быть очень полезно и изменит правила игры, если вы работаете с десятками миллионов строк данных с тысячами функций.
  2. Модель, которая использует меньше функций, легче понять, что в реальном проекте — это то, что вы обычно пытаетесь сделать.
  3. Иногда выбор функций может повысить производительность модели.

Обратите внимание, что № 2 не имеет отношения к нашей цели, потому что в большинстве соревнований Kaggle все, что вы пытаетесь сделать, — это создать точную модель, но в реальном мире понимание ваших данных в 99% случаев не менее важно (если не более важно). ), чем построение прогностической модели.

Но как определить, какие функции сохранить? Что ж, не нужно быть гением, чтобы понять, что сохранение важных функций и избавление от второстепенных — это правильный путь. Но как можно измерить важность функции? На самом деле это очень простая концепция. Вот как мы это делаем:

Допустим, мы хотели бы выяснить важность функции F. Первое, что нам нужно, — это точность нашей модели на обучающих данных (метрика, которую мы используем, важна, но пока вы можете просто подумать о R²). Скажем, 0,89. Далее мы случайным образом перемешаем столбец F, ничего больше не меняя. Чтобы лучше понять это, вот наглядное пособие (F — «fiSecondaryDesc»):

+-----------+--------------------------+-----------------+
| SalePrice | MachineHoursCurrentMeter | fiSecondaryDesc |
+-----------+--------------------------+-----------------+
|    66000  | 68.0                     | D               |
|     11000 | 722.0                    | CA              |
|     10500 | 700.0                    | NX              |
+-----------+--------------------------+-----------------+

Превратится в

+-----------+--------------------------+-----------------+
| SalePrice | MachineHoursCurrentMeter | fiSecondaryDesc |
+-----------+--------------------------+-----------------+
|    66000  | 68.0                     | CA              |
|     11000 | 722.0                    | NX              |
|     10500 | 700.0                    | D               |
+-----------+--------------------------+-----------------+

И последнее, но не менее важное: мы рассчитываем точность нашей модели на этом новом наборе данных. Мы могли бы, например, получить 0,87. Затем мы вычитаем эту новую оценку из нашей первоначальной оценки, и результатом будет важность этой функции. Таким образом, в нашем случае важность fiSecondaryDesc будет составлять 0,89–0,87 = 0,02. Мы делаем это для каждого столбца, и столбец с наивысшей важностью будет (сюрприз) самым важным, поскольку чем выше это число, тем ниже будет наша точность после случайного перетасовки, что будет означать, что функция играет решающую роль в определении целевая переменная. Обратите внимание, что мы могли бы для каждой функции обучить новую модель, используя весь набор данных за вычетом этой функции, и сделать то же самое. Потенциально это могло бы быть более точным, но потребовало бы много вычислительного времени и мощности.

P.S. Этот метод НЕ используется в древовидных моделях Scikit-learn, вместо этого они используют преимущества своей структуры для расчета важности функций. Причина, по которой я решил обсудить этот метод, так как он может быть использован для всех моделей ML и, как оказалось, более надежен. Однако чаще всего мы должны получать примерно одинаковые результаты от обоих методов.

Важность функции в Python

Объект Random Forest в Scikit-learn имеет атрибут feature_importances_, который представляет собой массив NumPy, где i-е значение соответствует i-му признаку, но оно не отсортировано, и вам нужно отдельно получать столбцы данных (я знаю, я ленив), но к счастью для нас, в FastAI есть функция rf_feat_importances(), которая построена поверх Scikit-learn feature_importances_, которая делает именно это для нас и возвращает pandas DataFrame с двумя столбцами, один для имени функций, а другой для их важности. .

Но сначала переучимся на оптимальные гиперпараметры:

m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5, max_features=0.5)
m.fit(X_train.iloc[-50000:], y_train[-50000:])

Теперь мы можем вызвать rf_feat_importances():

fi = rf_feat_importance(m, X_train)
fi
Output:
+----+----------------+----------+
|    |      cols      |   imp    |
+----+----------------+----------+
|  5 | YearMade       | 0.141476 |
| 37 | Coupler_System | 0.133767 |
| 13 | ProductSize    | 0.103198 |
| ...| ...            | ...      |
+----+----------------+----------+

Мы видим, что YearMade — самая важная функция в нашем наборе данных, на втором месте — Coupler_System, а на третьем — ProductSize.

Мы также можем построить десять наиболее важных функций, чтобы мы могли лучше понять разницу между важностью каждой функции:

def plot_feat_imp(fi):
    fi[:10].set_index('cols').plot.barh()
plot_feat_imp(fi)
Output:

Теперь нам просто нужно избавиться от некоторых функций. Мы делаем это, сохраняя функции, важность которых больше, чем X. Исходя из моего опыта, значение X различно для каждой проблемы, и не существует значения, которое хорошо работает для всех проблем. Самый простой способ найти хорошее значение для X — это установить его в начальное значение (0,005 — хороший выбор) и создать новый набор данных (с меньшим количеством функций). Если использование этого нового набора данных увеличило вашу оценку, попробуйте также увеличить X и повторить эти шаги, но если это ухудшило вашу оценку, попробуйте уменьшить X. Оказывается, для этой задачи хорошим выбором является 0,001, поэтому мы пойдем с этим:

to_keep = fi[fi.imp >= 0.001].cols
X_keep = X[to_keep].copy()
X_train_keep, X_valid_keep = X_train[to_keep].copy(), X_valid[to_keep].copy()

Давайте обучим случайный лес на наших новых данных с min_samples_leaf=5, но, поскольку у нас немного меньше функций, мы должны немного увеличить значение max_features, поэтому мы установим его равным 0,6:

m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5, max_features=0.6)
m.fit(X_train_keep.iloc[-50000:], y_train[-50000:])
print_score(m, X_train_keep.iloc[-50000:], y_train[-50000:], X_valid_keep, y_valid)

Output:
[0.15138236986252113, 0.23517103849467977]

Мы видим, что наша точность не сильно изменилась, но теперь у нас есть меньший набор данных, который требует меньше времени на обучение.

Ранговая корреляция Спирмена

Мы удалили функции, которые не сильно повлияли на точность нашей модели, но все еще могут быть функции, которые можно удалить из нашего набора данных не потому, что они не имеют большого значения, а потому, что в нашем наборе данных уже есть столбец, который представляет то же самое. То есть, если две или более функции коррелируют, мы можем удалить все эти функции, кроме одной. Для этого мы можем использовать ранговую корреляцию Спирмена (сначала я собирался объяснить это подробно, но я хотел, чтобы эта серия была где-то между легкой и средней, поэтому я не буду этого делать, но если вы' если интересно, ссылка на статью - отличное место для начала). По сути, ранговая корреляция Спирмена говорит нам о том, насколько связаны две функции. Например, если одна функция измеряет высоту бульдозера в метрах, а другая измеряет высоту того же бульдозера в футах, эти функции коррелируют, поскольку между ними существует линейная связь, кроме цвета бульдозера и наличия у него переменного тока. или нет, вероятно, не связаны.

Вот как мы строим корреляцию ранжирования набора данных с помощью SciPy:

from scipy.cluster import hierarchy as hc
corr = np.round(scipy.stats.spearmanr(X_keep).correlation, 4)
corr_condensed = hc.distance.squareform(1-corr)
z = hc.linkage(corr_condensed, method='average')
fig = plt.figure(figsize=(16,10))
dendrogram = hc.dendrogram(z, labels=X_keep.columns, orientation='left', leaf_font_size=16)
plt.show()

Output:

Обратите внимание, что чем раньше две функции соединятся, тем больше они коррелируют. Например, корреляции между «SalesID» и «состоянием» почти не существует, так как они не подключаются до самого конца, тогда как «Hydraulics_Flow» и «Grouser_Tracks» очень коррелированы, поскольку они подключаются сразу.

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

correlated = ['Hydraulics_Flow', 'Grouser_Tracks', 'Coupler_System',
'ProductGroupDesc', 'ProductGroup', 'fiBaseModel', 'fiModelDesc',
'Pushblock', 'Blade_Width', 'Scarifier'
]
for i in correlated:
    m = RandomForestRegressor(n_jobs=-1, n_estimators=40,       min_samples_leaf=5, max_features=0.6)
    m.fit(X_train_keep.iloc[-50000:].drop(i, axis=1), y_train[-50000:])
    print(i)
    print_score(m, X_train_keep.iloc[-50000:].drop(i, axis=1), y_train[-50000:], X_valid_keep.drop(i, axis=1), y_valid)

Output:
Hydraulics_Flow
[0.15181820054177672, 0.23557909682371558]
Grouser_Tracks
[0.1516332704723034, 0.23505840642822629]
Coupler_System
[0.15174695077765227, 0.2353865983254473]
ProductGroupDesc
[0.15101461320758594, 0.23481050128663936]
ProductGroup
[0.15154077648778166, 0.23569670990327843]
fiBaseModel
[0.15227384397504082, 0.23600675665843016]
fiModelDesc
[0.15249203156766614, 0.23688018253401885]
Pushblock
[0.1508151506261641, 0.234740857411555]
Blade_Width
[0.15146129006866857, 0.23542536610239492]
Scarifier
[0.15194900028475958, 0.23442403568051656]

Мы можем видеть, например, что удаление «fiModelDesc» сделало нашу модель хуже, тогда как удаление «Scarifier» сделало ее лучше. Мы собираемся удалить те, удаление которых сделало нашу модель лучше (например, «Скарификатор»), но обратите внимание, что если две функции коррелируют и удаление одной из них помогло нашей модели, мы не можем удалить их обе, поскольку причина, по которой мы можем уйти удаление одного из них связано с тем, что другой в основном фиксирует одно и то же значение.

to_drop = ['Grouser_Tracks', 'ProductGroupDesc', 'Scarifier', 'Pushblock']
m = RandomForestRegressor(n_jobs=-1, n_estimators=40, min_samples_leaf=5, max_features=0.6)
m.fit(X_train_keep.iloc[-50000:].drop(to_drop, axis=1), y_train[-50000:])
print_score(m, X_train_keep.iloc[-50000:].drop(to_drop, axis=1), y_train[-50000:], X_valid_keep.drop(to_drop, axis=1), y_valid)
Output:
[0.15089659799045144, 0.23458822510514812]

Мы видим, что точность нашей модели практически такая же, но мы еще раз уменьшили сложность наших данных и модели. Итак, давайте сделаем эти изменения постоянными:

X_keep.drop(to_drop, axis=1, inplace=True)
X_train_keep.drop(to_drop, axis=1, inplace=True)
X_valid_keep.drop(to_drop, axis=1, inplace=True)

Вывод

В этой части мы создали очень простой случайный лес и с помощью нашей обученной модели смогли рассчитать важность каждой функции и избавиться от ненужных. Мы также кратко рассмотрели ранговую корреляцию Спирмена, которая может сказать нам, насколько взаимосвязаны наши функции. Используя эти две простые концепции, мы смогли уменьшить наш набор данных до меньшего, что требует меньше времени и места для вычислений. В следующей части мы более подробно рассмотрим более важные функции и проведем некоторые разработки.

Пожалуйста, если у вас есть какие-либо вопросы или отзывы, не стесняйтесь оставлять их в комментариях ниже и, как всегда, спасибо за чтение!

Часть 1: https://medium.com/@borna.ahz/blue-book-for-bulldozers-competition-part-1-basic-data-pre-processing-1248cd5d4214

Часть 2: https://medium.com/@borna.ahz/blue-book-for-bulldozers-competition-part-2-finding-the-right-validation-set-115175cde02c

Часть 4: https://medium.com/python-in-plain-english/blue-book-for-bulldozers-competition-part-4-thorough-analysis-of-important-features-i-804861dc82bb

Часть 4 (продолжение): https://medium.com/@borna.ahz/blue-book-for-bulldozers-competition-part-4-continued-thorough-analysis-of-important-features-ii-db135b959d6a

Часть 5 (заключительная часть): https://medium.com/python-in-plain-english/blue-book-for-bulldozers-competition-part-5-final-part-extrapolating-with-random-forests- б56564204975

Часть 6 (необязательно): https://medium.com/@borna.ahz/blue-book-for-bulldozers-competition-part-6-Optional-xgboost-extra-trees-and-ensemble-2e99a910f667

Часть 7 (необязательно): https://medium.com/python-in-plain-english/blue-book-for-bulldozers-competition-part-7-Optional-deep-learning-for-tabular-data-i -eeda7e12c107

Часть 7 (продолжение): https://medium.com/python-in-plain-english/blue-book-for-bulldozers-competition-part-7-continued-deep-learning-for-tabular-data-ii -dbe267aca33b

Часть 7 (заключительная часть): https://medium.com/python-in-plain-english/blue-book-for-bulldozers-competition-part-7-continued-deep-learning-for-tabular-data- iii-e439aa1368cc

Твиттер: https://twitter.com/bobmcdear

GitHub: https://github.com/bobmcdear

Примечание от Python In Plain English

Мы всегда заинтересованы в помощи в продвижении качественного контента. Если у вас есть статья, которую вы хотели бы отправить в какое-либо из наших изданий, отправьте нам электронное письмо по адресу [email protected], указав свое имя пользователя на Medium, и мы добавим вас в качестве автора.