«Случайный лес» на Python (часть 2)
Nuances of programmingПеревод статьи William Koehrsen: Random Forest in Python
см. Часть 1.

Улучшение модели (при необходимости)
Сегодня процесс машинного обучения может осуществляться с помощью подстройки гиперпараметров. По-сути, это означает «подстройка параметров для повышения производительности» (настройки называются гиперпараметрами, чтобы отличать их от параметров модели, полученных во время обучения). Самый распространенный способ выполнения такой настройки - создать совокупность моделей с разными настройками, оценить их на одном наборе данных и посмотреть, какая из них подходит лучше всего. Конечно, делать это вручную – довольно утомительно, поэтому лучше воспользоваться автоматизированными методами из Skicit-learn. Для настройки гиперпараметров (Hyperparameter) часто нужно скорее проявить изобретательность, а не опираться на теоретические знания, а еще я все-таки рекомендую заглянуть в документацию! Точность 94% удовлетворительна для рассмотренной задачи, но учтите, что обычно первая построенная модель почти никогда не бывает самой эффективной моделью.
Интерпретация модели и отчет о результатах
Сейчас уже известно, что модель качественная, но все же она в значительной степени - черный ящик. Введем некоторые массивы Numpy для обучения, просим сделать прогнозы, оценить прогнозы и убедиться, что они разумны. Возникает вопрос: как эта модель получает результат? Есть два подхода, позволяющих заглянуть под капот «случайного леса»: во-первых, можем посмотреть на одно дерево в лесу, а во-вторых, можем посмотреть на особенности, связанные с нашими объяснительными переменными.
Визуализация отдельного дерева решений
Одна из самых классных реализаций случайного леса в Skicit-learn из общего числа возможных реализаций – можно изучить любые деревья в лесу. Мы выберем одно дерево и сохраним его как образ.
Следующий код берет одно дерево из леса и сохраняет его как изображение.
# Import tools needed for visualization
from sklearn.tree import export_graphviz
import pydot
# Pull out one tree from the forest
tree = rf.estimators_[5]
# Import tools needed for visualization
from sklearn.tree import export_graphviz
import pydot
# Pull out one tree from the forest
tree = rf.estimators_[5]
# Export the image to a dot file
export_graphviz(tree, out_file = 'tree.dot', feature_names = feature_list, rounded = True, precision = 1)
# Use dot file to create a graph
(graph, ) = pydot.graph_from_dot_file('tree.dot')
# Write graph to a png file
graph.write_png('tree.png')
Давайте посмотрим:

Вау! Это выглядит как широкое дерево с 15 слоями (на самом деле это еще достаточно маленькое дерево по сравнению с теми, что доводилось видеть мне). Можете самостоятельно скачать это изображение и изучить его подробнее, но чтобы упростить задачу, я ограничу глубину деревьев в лесу, для создания читаемого изображения.
# Limit depth of tree to 3 levels
rf_small = RandomForestRegressor(n_estimators=10, max_depth = 3)
rf_small.fit(train_features, train_labels)
# Extract the small tree
tree_small = rf_small.estimators_[5]
# Save the tree as a png image
export_graphviz(tree_small, out_file = 'small_tree.dot', feature_names = feature_list, rounded = True, precision = 1)
(graph, ) = pydot.graph_from_dot_file('small_tree.dot')
graph.write_png('small_tree.png');
Вот дерево уменьшенных размеров, аннотированное метками

Основываясь исключительно на этом дереве, можем сделать прогноз для любой новой точки данных. Возьмем, например, прогнозы для среды, 27 декабря 2017 года. Переменными (фактическими) являются: temp_2 = 39, temp_1 = 35, average = 44 и friend = 30. Мы начинаем с корневого узла, и первый ответ True, потому что temp_1 ≤ 59.5. Двигаемся влево и сталкиваемся со вторым вопросом, который также является True как средний ≤ 46.8. Переместитесь вниз налево и на третий и последний вопрос, который также является True, потому что temp_1 ≤ 44.5. Поэтому мы заключаем, что наша оценка максимальной температуры составляет 41,0 градуса, что указано значением в листовом узле. Интересное наблюдение заключается в том, что в корневом узле имеется только 162 выборок, несмотря на 261 точку данных для обучения. Это объясняется тем, что каждое дерево в лесу обучается на случайном подмножестве точек данных с заменой (называемый упаковкой, сокращенной до инициализации программного обеспечения). (Можем отключить выборку с заменой и использовать все точки данных, установив bootstrap = False при создании леса). Случайная выборка точек данных в сочетании со случайной выборкой подмножества признаков в каждом узле дерева объясняет, почему модель называется «случайным» лесом.
Кроме того, обратите внимание, что в нашем дереве только две переменных, которые мы использовали для получения прогноза! Согласно данному конкретному дереву принятия решений, остальные функции не важны для получения прогноза. Месяц года, день месяца и прогноз нашего друга совершенно бесполезны для прогнозирования максимальной температуры на завтра! Единственной важной информацией согласно нашему простому дереву является температура за предыдущий день и среднее значение за имеющийся период. Визуализация дерева увеличила наши знания о задаче, и теперь мы знаем, какие данные нужно искать, если попросят сделать прогноз повторно!
Значение переменных
Чтобы количественно оценить полезность всех переменных случайного леса в целом, можем рассмотреть относительные значения переменных. Значения, возвращаемые в Scikit-learn, показывают, насколько включение определенной переменной может улучшить прогноз. Фактический расчет важности выходит за рамки этой статьи, но мы можем осуществить относительные сравнения между переменными на конкретных численных значениях.
В этом коде используется ряд трюков на языке Python, а именно: полный список, zip, сортировка и распаковка аргументов. Не столь важно, понятно ли вам все перечисленное в данный момент, но если вы хотите стать опытным Python-программистом, это те инструменты, которые вы должны иметь в своем арсенале!
# Get numerical feature importances
importances = list(rf.feature_importances_)
# List of tuples with variable and importance
feature_importances = [(feature, round(importance, 2)) for feature, importance in zip(feature_list, importances)]
# Sort the feature importances by most important first
feature_importances = sorted(feature_importances, key = lambda x: x[1], reverse = True)
# Print out the feature and importances
[print('Variable: {:20} Importance: {}'.format(*pair)) for pair in feature_importances];
Variable: temp_1 Importance: 0.7
Variable: average Importance: 0.19
Variable: day Importance: 0.03
Variable: temp_2 Importance: 0.02
Variable: friend Importance: 0.02
Variable: month Importance: 0.01
Variable: year Importance: 0.0
Variable: week_Fri Importance: 0.0
Variable: week_Mon Importance: 0.0
Variable: week_Sat Importance: 0.0
Variable: week_Sun Importance: 0.0
Variable: week_Thurs Importance: 0.0
Variable: week_Tues Importance: 0.0
Variable: week_Wed Importance: 0.0
В верхней части списка находится temp_1, максимальная температура за предыдущий день. Можно прийти к интуитивному выводу о том, что наилучшим предиктором величины максимальной температуры в течение дня является величина максимальная температуры накануне. Вторым по значимости фактором является среднестатистическая максимальная температура, что также не удивительно. А вот друг оказался не очень полезным, так же, как и день недели, год, месяц и температура за два предшествующих дня. Такой результат вполне оправдывает себя ‑ мы интуитивно и не ожидаем, что день недели станет предиктором максимальной температуры, поскольку он не имеет никакого отношения к погоде. Более того, год одинаков для всех температурных значений и, следовательно, не дает нам информации для прогнозирования максимальной температуры.
В будущих реализациях модели мы сможем удалить те переменные, которые не имеют значения, причем производительность при этом не пострадает. Кроме того, если мы используем другую модель, скажем, машину с поддержкой обработки векторных данных, то сможем использовать функции случайных значений леса как своего рода метод выбора объектов. Давайте быстро создадим случайный лес с двумя наиболее важными переменными: максимальной температурой за предшествующий день и историческим средним значением и посмотрим, как будет различаться производительность.
# New random forest with only the two most important variables
rf_most_important = RandomForestRegressor(n_estimators= 1000, random_state=42)
# Extract the two most important features
important_indices = [feature_list.index('temp_1'), feature_list.index('average')]
train_important = train_features[:, important_indices]
test_important = test_features[:, important_indices]
# Train the random forest
rf_most_important.fit(train_important, train_labels)
# Make predictions and determine the error
predictions = rf_most_important.predict(test_important)
errors = abs(predictions - test_labels)
# Display the performance metrics
print('Mean Absolute Error:', round(np.mean(errors), 2), 'degrees.')
mape = np.mean(100 * (errors / test_labels))
accuracy = 100 - mape
print('Accuracy:', round(accuracy, 2), '%.')
Mean Absolute Error: 3.9 degrees.
Accuracy: 93.8 %.
Это говорит о том, что в действительности не нужны те данные, которые мы собрали, для получения точных прогнозов! Если бы мы использовали эту же модель, то достаточно было бы собрать только две переменные и получить туже производительность. Для решения задачи калибровки потребуется взвешивать соотношение между снижением точности и дополнительным временем, необходимым для получения дополнительной информации. Умение найти верный баланс между производительностью и стоимостью является важным навыком инженера по компьютерному обучению и в конечном итоге будет определяться задачей!
К данному моменту мы уже рассмотрели почти все необходимое для базовой реализации случайного леса при решении задачи контролируемой регрессии. Теперь можем быть уверены, что наша модель может прогнозировать максимальную температуру на следующий день с точностью 94% на основе годовых исторических данных. Поиграйте на этом примере или используйте модель в наборе данных по своему выбору. Завершу этот пост, показав несколько визуализаций. У меня две любимые части науки о данных - это графика и моделирование, поэтому, естественно, я должен построить диаграммы! Кроме того, что на диаграммы просто приятным смотреть, они могут помочь диагностировать используемую модель, потому что они в легко воспринимаемом глазом изображении вмещают огромное количество чисел.
Визуализации
Первая диаграмма, которую я построю, представляет собой простую столбцовую диаграмму характеристик функции, иллюстрирующую различия относительной значимости переменных. Процесс построения графика в Python не является интуитивно понятным, поэтому в конечном счет, приходится просматривать почти все на Stack Overflow, чтобы построить график. Поэтому не стоит беспокоится, если приводимый код покажется не совсем понятным, иногда полное понимание кода не обязательно для получения желаемого конечного результата!
# Import matplotlib for plotting and use magic command for Jupyter Notebooks
import matplotlib.pyplot as plt
%matplotlib inline
# Set the style
plt.style.use('fivethirtyeight')
# list of x locations for plotting
x_values = list(range(len(importances)))
# Make a bar chart
plt.bar(x_values, importances, orientation = 'vertical')
# Tick labels for x axis
plt.xticks(x_values, feature_list, rotation='vertical')
# Axis labels and title
plt.ylabel('Importance'); plt.xlabel('Variable'); plt.title('Variable Importances');

Затем можем построить весь набор данных с выделенными прогнозами. Это требует небольшого манипулирования данными, но в целом это не слишком сложно. Можно использовать этот график, чтобы определить, есть ли какие-либо выбросы в данных или в наших прогнозах.
# Use datetime for creating date objects for plotting
import datetime
# Dates of training values
months = features[:, feature_list.index('month')]
days = features[:, feature_list.index('day')]
years = features[:, feature_list.index('year')]
# List and then convert to datetime object
dates = [str(int(year)) + '-' + str(int(month)) + '-' + str(int(day)) for year, month, day in zip(years, months, days)]
dates = [datetime.datetime.strptime(date, '%Y-%m-%d') for date in dates]
# Dataframe with true values and dates
true_data = pd.DataFrame(data = {'date': dates, 'actual': labels})
# Dates of predictions
months = test_features[:, feature_list.index('month')]
days = test_features[:, feature_list.index('day')]
years = test_features[:, feature_list.index('year')]
# Column of dates
test_dates = [str(int(year)) + '-' + str(int(month)) + '-' + str(int(day)) for year, month, day in zip(years, months, days)]
# Convert to datetime objects
test_dates = [datetime.datetime.strptime(date, '%Y-%m-%d') for date in test_dates]
# Dataframe with predictions and dates
predictions_data = pd.DataFrame(data = {'date': test_dates, 'prediction': predictions})
# Plot the actual values
plt.plot(true_data['date'], true_data['actual'], 'b-', label = 'actual')
# Plot the predicted values
plt.plot(predictions_data['date'], predictions_data['prediction'], 'ro', label = 'prediction')
plt.xticks(rotation = '60');
plt.legend()
# Graph labels
plt.xlabel('Date'); plt.ylabel('Maximum Temperature (F)'); plt.title('Actual and Predicted Values');

Потребуется совсем немного работы для получения красивого графика! Он не выглядит так, как если бы у нас имелись заметные выбросы, которые необходимо исправить. Чтобы дополнительно диагностировать модель, можем построить остатки (ошибки), чтобы убедиться, имеет ли наша модель тенденцию к прогнозу с преувеличением или, занижением прогнозируемых величин, можем увидеть, нормально ли распределены остатки. Однако я просто приведу заключительную диаграмму, показывающую фактические значения, температуру за предшествующий день, исторические средние и прогноз нашего друга. Это позволит нам увидеть разницу между полезными переменными и теми, которые менее полезны.
# Make the data accessible for plotting
true_data['temp_1'] = features[:, feature_list.index('temp_1')]
true_data['average'] = features[:, feature_list.index('average')]
true_data['friend'] = features[:, feature_list.index('friend')]
# Plot all the data as lines
plt.plot(true_data['date'], true_data['actual'], 'b-', label = 'actual', alpha = 1.0)
plt.plot(true_data['date'], true_data['temp_1'], 'y-', label = 'temp_1', alpha = 1.0)
plt.plot(true_data['date'], true_data['average'], 'k-', label = 'average', alpha = 0.8)
plt.plot(true_data['date'], true_data['friend'], 'r-', label = 'friend', alpha = 0.3)
# Formatting plot
plt.legend(); plt.xticks(rotation = '60');
# Lables and title
plt.xlabel('Date'); plt.ylabel('Maximum Temperature (F)'); plt.title('Actual Max Temp and Variables');

Несколько затруднительно различить все линии, но мы можем понять, почему максимальная температура за предыдущий день и максимальная историческая температура полезны для прогнозирования максимальной температуры, тогда как наш друг – не является полезным (не отказывайтесь от советов друга, просто не придавайте его прогнозам слишком большой вес!). Такие графики часто оказываются полезными, поэтому стоит сделать их заблаговременно, таким образом мы можем выбрать переменные, которые могут быть использованы и для диагностики. Как и в случае квартета Anscombe, графики часто позволяют понять гораздо больше, чем просто изучение числовых значений, и должны быть частью любого рабочего процесса машинного обучения.
Выводы
На этих графиках мы завершили процесс машинного обучения! Теперь, если хотим улучшить нашу модель, сможем попробовать разные гиперпараметры (настройки), попробовать другой алгоритм или более хороший подход для более детального сбора данных! Производительность любой модели прямо пропорциональна количеству действительных данных, которые она может извлечь, а мы использовали очень ограниченный объем информации для обучения. Я бы рекомендовал, вам попытаться улучшить эту модель и поделиться результатами. Так вы сможете глубже разобраться в теорию случайного леса и его приложениях, используя многочисленные открытые онлайн-ресурсы (причем, бесплатно). Для тех, кто ищет одну книгу как с теорией, так и с примерами реализаций моделей машинного обучения на Python, настоятельно рекомендую Hands-On Machine Learning with Scikit-Learn and Tensorflow. Более того, надеюсь, что все, кто повторил все шаги вместе с нами убедились, насколько стало доступно машинное обучение, и теперь готовы присоединиться к гостеприимному и полезному сообществу машинного обучения.
Как всегда, приветствую обратную связь и конструктивную критику! Мой адрес: wjk68@case.edu.
Статью перевел Владислав Семёнов.