Универсальный рабочий процесс машинного обучения
Применение универсального рабочего процесса машинного обучения Франсуа Шоле к набору данных UCI Mushroom
Мы будем использовать Набор данных грибов из Репозитория машинного обучения UCI для проведения демонстрации. Эта работа предназначена для читателя, который имеет хотя бы базовое понимание основ Python и некоторый опыт работы с машинным обучением. При этом я предоставлю множество ссылок на вспомогательные источники для непосвященных, чтобы каждый мог использовать представленную информацию.
Прежде чем мы начнем, я хотел бы поблагодарить моего друга-ламбдониста Неда Х за его помощь в написании этой статьи.
Универсальный рабочий процесс машинного обучения
- Определите проблему и соберите набор данных
- Выберите меру успеха
- Определитесь с протоколом оценки
- Подготовьте данные
- Разработайте модель, которая лучше, чем базовый уровень
- Разработайте модель, которая подходит
- Регуляризуйте модель и настройте ее гиперпараметры.
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 ложных срабатывания, не идеально, но неплохо.
Заключение
Чтобы переформулировать наш рабочий процесс.
- Определите проблему и соберите набор данных
- Выберите меру успеха
- Определитесь с протоколом оценки
- Подготовьте данные
- Разработайте модель, которая лучше, чем базовый уровень
- Разработайте модель, которая подходит
- Регуляризуйте модель и настройте ее гиперпараметры.
Хотя Шолле описывает это как универсальный рабочий процесс машинного обучения, существует бесконечное множество вариантов в зависимости от конкретной проблемы, которую мы пытаемся решить. В общем, вы всегда будете начинать с определения вашей проблемы и сбора данных (будь то из готового набора данных или сбор ваших собственных данных).
Я надеюсь, что в этом посте представлено информативное пошаговое руководство по универсальному рабочему процессу машинного обучения Chollet.
Спасибо за прочтение!
Следуйте за мной в T witter, GitHub и LinkedIn
P.S. Вот ссылка на Блокнот Colab, который я использовал для этого поста.