Универсальный рабочий процесс машинного обучения

Применение универсального рабочего процесса машинного обучения Франсуа Шоле к набору данных UCI Mushroom

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

Прежде чем мы начнем, я хотел бы поблагодарить моего друга-ламбдониста Неда Х за его помощь в написании этой статьи.

Универсальный рабочий процесс машинного обучения

  1. Определите проблему и соберите набор данных
  2. Выберите меру успеха
  3. Определитесь с протоколом оценки
  4. Подготовьте данные
  5. Разработайте модель, которая лучше, чем базовый уровень
  6. Разработайте модель, которая подходит
  7. Регуляризуйте модель и настройте ее гиперпараметры.

1. Определите проблему и соберите набор данных.

Кратко говоря, наша проблема - это бинарная классификация грибов на съедобные и ядовитые. Нам предоставляется набор данных с 23 характеристиками, включая класс гриба (съедобный или ядовитый).

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

column_names = ['class',
                'cap-shape',
                'cap-surface',
                'cap-color',
                'bruises?',
                'odor',
                'gill-attachment',
                'gill-spacing',
                'gill-size',
                'gill-color',
                'stalk-shape',
                'stalk-root',
                'stalk-surface-above-ring',
                'stalk-surface-below-ring',
                'stalk-color-above-ring',
                'stalk-color-below-ring',
                'veil-type',
                'veil-color',
                'ring-number',
                'ring-type',
                'spore-print-color',
                'population',
                'habitat']

Давайте импортируем наш набор данных и создадим Pandas DataFrame из файла .data, используя pd.read_csv()

import pandas as pd
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data'
mushrooms = pd.read_csv(url, header=None, names=column_names)

2. Выберите критерий успеха

Учитывая характер нашей проблемы; классифицируя, является ли гриб ядовитым или нет, мы будем использовать точность как критерий успеха. Точность - это способность классификатора не маркировать ядовитые съедобные грибы. Мы бы предпочли, чтобы люди выбросили съедобные грибы, которые наша модель классифицировала как ядовитые, чем есть ядовитые грибы, которые наш классификатор назвал съедобными.

from sklearn.metrics import precision_score

3. Определитесь с протоколом оценки.

Мы будем использовать 10-кратную перекрестную проверку для оценки нашей модели. Хотя простого набора для проверки удержания, вероятно, будет достаточно, я скептически отношусь к его жизнеспособности, учитывая наши ~ 8000 образцов.

from sklearn.model_selection import train_test_split, cross_validate

Сначала давайте разделим наши данные на матрицу признаков (X) и целевой вектор (y). Мы будем использовать OneHotEncoder для кодирования наших категориальных переменных.

import category_encoders as ce
#Drop target feature
X = mushrooms.drop(columns='class') 
#Encode categorical features
X = ce.OneHotEncoder(use_cat_names=True).fit_transform(X)
 
y = mushrooms['class'].replace({'p':0, 'e':1})
print('Feature matrix size:',X.shape)
print('Target vector size:',len(y))
____________________________________________________________________
Feature matrix size: (8124, 117) Target vector size: 8124

Далее мы разделим наши данные на обучающий набор и тестовый набор.

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42, test_size=.2, stratify=y)
print('Training feature matrix size:',X_train.shape)
print('Training target vector size:',y_train.shape)
print('Test feature matrix size:',X_test.shape)
print('Test target vector size:',y_test.shape)
____________________________________________________________________
Training feature matrix size: (6499, 117) 
Training target vector size: (6499,) 
Test feature matrix size: (1625, 117) 
Test target vector size: (1625,)

4. Подготовьте данные.

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

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

print(mushrooms.info())
____________________________________________________________________
<class 'pandas.core.frame.DataFrame'> 
RangeIndex: 8124 entries, 0 to 8123
Data columns (total 23 columns):
class                       8124 non-null object
cap-shape                   8124 non-null object 
cap-surface                 8124 non-null object 
cap-color                   8124 non-null object 
bruises?                    8124 non-null object 
odor                        8124 non-null object 
gill-attachment             8124 non-null object 
gill-spacing                8124 non-null object 
gill-size                   8124 non-null object 
gill-color                  8124 non-null object 
stalk-shape                 8124 non-null object 
stalk-root                  8124 non-null object 
stalk-surface-above-ring    8124 non-null object 
stalk-surface-below-ring    8124 non-null object 
stalk-color-above-ring      8124 non-null object 
stalk-color-below-ring      8124 non-null object 
veil-type                   8124 non-null object 
veil-color                  8124 non-null object 
ring-number                 8124 non-null object 
ring-type                   8124 non-null object 
spore-print-color           8124 non-null object 
population                  8124 non-null object 
habitat                     8124 non-null object 
dtypes: object(23) memory usage: 1.4+ MB None

Еще один полезный шаг - проверить количество нулевых значений и их местонахождение в DataFrame.

print(mushrooms.isna().sum())
____________________________________________________________________
class                       0 
cap-shape                   0 
cap-surface                 0 
cap-color                   0 
bruises?                    0 
odor                        0 
gill-attachment             0 
gill-spacing                0 
gill-size                   0 
gill-color                  0 
stalk-shape                 0 
stalk-root                  0 
stalk-surface-above-ring    0 
stalk-surface-below-ring    0 
stalk-color-above-ring      0 
stalk-color-below-ring      0 
veil-type                   0 
veil-color                  0 
ring-number                 0 
ring-type                   0 
spore-print-color           0 
population                  0 
habitat                     0 
dtype: int64

Нет… это кажется слишком хорошим, чтобы быть правдой.

Поскольку мы были прилежны и прочитали файл с информацией о наборе данных. Нам известно, что все отсутствующие значения отмечены вопросительным знаком. Как только это станет ясно, мы можем использовать df.replace() для преобразования? в NaN.

import numpy as np
mushrooms = mushrooms.replace({'?':np.NaN})
print(mushrooms.isna().sum())
____________________________________________________________________
class                       0 
cap-shape                   0 
cap-surface                 0 
cap-color                   0 
bruises?                    0 
odor                        0 
gill-attachment             0 
gill-spacing                0 
gill-size                   0 
gill-color                  0 
stalk-shape                 0 
stalk-root               2480
stalk-surface-above-ring    0 
stalk-surface-below-ring    0 
stalk-color-above-ring      0 
stalk-color-below-ring      0 
veil-type                   0 
veil-color                  0 
ring-number                 0 
ring-type                   0 
spore-print-color           0 
population                  0 
habitat                     0 
dtype: int64

Итак, stalk_root имеет 2480 пустых функций, давайте заменим их на m, если они отсутствуют.

mushrooms['stalk-root'] = mushrooms['stalk-root'].replace(np.NaN,'m')
print(mushrooms['stalk-root'].value_counts())
____________________________________________________________________
b    3776 
m    2480 
e    1120 
c     556 
r     192 
Name: stalk-root, dtype: int64

5. Разработайте модель, которая лучше, чем базовый уровень.

Базовая модель

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

Сначала давайте посмотрим, как класс распределяется с помощью df.value_counts()

mushrooms['class'].value_counts(normalize=True)
____________________________________________________________________
e    0.517971 
p    0.482029 
Name: class, dtype: float64

Мы будем использовать режим атрибута class для создания нашего базового прогноза.

majority_class = y_train.mode()[0]
baseline_predictions = [majority_class] * len(y_train)

Давайте посмотрим, насколько точна наша базовая модель.

from sklearn.metrics import accuracy_score
majority_class_accuracy = accuracy_score(baseline_predictions,
                                         y_train)
print(majority_class_accuracy)
____________________________________________________________________

0.5179258347438067

~ 52%… Этого мы и ожидали, учитывая распределение классов в нашем исходном наборе данных.

Дерево решений

Мы попытаемся подогнать дерево решений к нашим обучающим данным и получить показатель точности более 52%.

from sklearn.tree import DecisionTreeClassifier
import graphviz
from sklearn.tree import export_graphviz
tree = DecisionTreeClassifier(max_depth=1)
# Fit the model
tree.fit(X_train, y_train)
# Visualize the tree
dot_data = export_graphviz(tree, out_file=None, feature_names=X_train.columns, class_names=['Poisonous', 'Edible'], filled=True, impurity=False, proportion=True)
graphviz.Source(dot_data)

Теперь, когда мы подогнали дерево решений к нашим данным, мы можем проанализировать нашу модель, посмотрев на распределение вероятностей предсказания для нашего классификатора. Проще говоря, вероятность предсказания показывает, насколько уверена модель в своей классификационной метке.

Помимо вероятности предсказания, мы будем смотреть на оценку точности нашего дерева решений. Sklearn предоставляет нам простой способ увидеть многие из релевантных оценок для моделей классификации с classification_report.

Мы также сгенерируем матрицу путаницы, используя sklearn's confusion_matrix. Матрица неточностей показывает количество истинных и ложных срабатываний и отрицаний.

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

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import classification_report, confusion_matrix
def model_analysis(model, train_X, train_y):
  model_probabilities = model.predict_proba(train_X)
  Model_Prediction_Probability = []
  for _ in range(len(train_X)):
      x = max(model_probabilities[_])
      Model_Prediction_Probability.append(x)
  plt.figure(figsize=(15,10)) 
 
  sns.distplot(Model_Prediction_Probability)
  plt.title('Best Model Prediction Probabilities')
  # Set x and y ticks
  plt.xticks(color='gray')
  #plt.xlim(.5,1)
  plt.yticks(color='gray')
  # Create axes object with plt. get current axes
  ax = plt.gca()
  # Set grid lines
  ax.grid(b=True, which='major', axis='y', color='black', alpha=.2)
  # Set facecolor
  ax.set_facecolor('white')
  # Remove box
  ax.spines['top'].set_visible(False)
  ax.spines['right'].set_visible(False)
  ax.spines['bottom'].set_visible(False)
  ax.spines['left'].set_visible(False)
  ax.tick_params(color='white')
  plt.show();
  
  model_predictions = model.predict(train_X)
  # Classification Report
  print('\n\n', classification_report(train_y, model_predictions, target_names=['0-Poisonous', '1-Edible']))
  # Confusion Matrix
  con_matrix = pd.DataFrame(confusion_matrix(train_y, model_predictions), columns=['Predicted Poison', 'Predicted Edible'], index=['Actual Poison', 'Actual Edible'])
  
  plt.figure(figsize=(15,10))
  sns.heatmap(data=con_matrix, cmap='cool');
  plt.title('Model Confusion Matrix')
  plt.show();
  
  return con_matrix

Теперь применим эту функцию к нашему дереву решений.

model_analysis(tree, X_train, y_train)

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

tree_predictions = tree.predict(X_train)
accuracy_score(y_train, tree_predictions)
____________________________________________________________________
0.8862901984920757

Точность 88% - это неплохо, но давайте перейдем к следующему шагу в нашем рабочем процессе.

6. Разработайте модель, которая перекрывает

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

from sklearn.ensemble import RandomForestClassifier
random_forest = RandomForestClassifier(n_estimators=100, max_depth=5)
cv = cross_validate(estimator = random_forest, X = X_train, y = y_train, scoring='accuracy', n_jobs=-1, cv=10, verbose=10, return_train_score=True)
____________________________________________________________________

[Parallel(n_jobs=-1)]: Using backend LokyBackend with 2 concurrent workers.
[Parallel(n_jobs=-1)]: Done   1 tasks      | elapsed:    2.6s [Parallel(n_jobs=-1)]: Done   4 tasks      | elapsed:    3.2s [Parallel(n_jobs=-1)]: Done  10 out of  10 | elapsed:    4.7s finished

Теперь мы можем увидеть оценку точности нашего случайного леса.

random_forest.fit(X_test, y_test)
test_predictions = random_forest.predict(X_train)
accuracy_score(y_train, test_predictions)
____________________________________________________________________

0.9924603785197723

Мне кажется, что точность 99% слишком высока.

Мы можем использовать нашу функцию model_analysis, которую мы использовали ранее, для анализа нашей модели.

model_analysis(random_forest, X_train, y_train)

7. Регуляризуйте модель и настройте ее гиперпараметры.

Теперь мы настроим гиперпараметры нашего RandomForestClassifier и попытаемся пройти грань между недостаточным и избыточным соответствием. Мы можем использовать RandmoizedSearchCV sklearn для поиска гиперпараметров в нашем param_distributions словаре.

from sklearn.model_selection import RandomizedSearchCV
param_distributions = {
    'max_depth':[1, 2, 3, 4, 5],
    'n_estimators': [10, 25, 50, 100, 150, 200]}
search = RandomizedSearchCV(estimator = RandomForestClassifier(), param_distributions = param_distributions, n_iter=100, scoring='precision', n_jobs=-1, cv=10, verbose=10, return_train_score=True)
 
search.fit(X_train, y_train)

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

best_model = search.best_estimator_
best_model
____________________________________________________________________
RandomForestClassifier
(bootstrap=True, class_weight=None, criterion='gini', max_depth=5, max_features='auto', max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, min_samples_leaf=1, min_samples_split=2,             min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=None,             oob_score=False, random_state=None, verbose=0,             warm_start=False)

Из описания модели мы видим, что RandomForestClassifier с max_depth оценками 5 и 10 является нашей оптимальной моделью. Теперь мы можем запустить нашу функцию анализа.

model_analysis(best_model, X_test, y_test)

3 ложных срабатывания, не идеально, но неплохо.

Заключение

Чтобы переформулировать наш рабочий процесс.

  1. Определите проблему и соберите набор данных
  2. Выберите меру успеха
  3. Определитесь с протоколом оценки
  4. Подготовьте данные
  5. Разработайте модель, которая лучше, чем базовый уровень
  6. Разработайте модель, которая подходит
  7. Регуляризуйте модель и настройте ее гиперпараметры.

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

Я надеюсь, что в этом посте представлено информативное пошаговое руководство по универсальному рабочему процессу машинного обучения Chollet.

Спасибо за прочтение!

Следуйте за мной в T witter, GitHub и LinkedIn

P.S. Вот ссылка на Блокнот Colab, который я использовал для этого поста.