Хабрахабр

Data Science проект от исследования до внедрения на примере Говорящей шляпы

Конкурс сделан неплохо, звучащие по-разному имена определяются на разные факультеты, причем схожие английские и русские имена и фамилии распределяются схожим образом. Месяц назад Лента запустила конкурс, в рамках которого та самая Говорящая Шляпа из Гарри Поттера определяет предоставивших доступ к социальной сети участников на один из четырех факультетов. Не знаю, зависит ли распределение только от имен и фамилий, и учитывается ли как-то количество друзей или другие факторы, но этот конкурс подсказал идею этой статьи: попробовать с нуля обучить классификатор, который позволит распределять пользователей на различные факультеты.

А именно мы: В статье мы сделаем простую ML-модель, которая распределяет людей на факультеты Гарри Поттера в зависимости от их имени и фамилии, пройдя процесс небольшого исследования следуя методологии CRISP.

  • Сформулируем задачу;
  • Исследуем возможные подходы к ее решению и сформулируем требования к данным (Методы решения и данные) ;
  • Соберем необходимые данные (Методы решения и данные);
  • Изучим собранный датасет (Exploratory Research);
  • Извлечем признаки из сырых данных (Feature Engineering);
  • Обучим модель машинного обучения (Model evaluation);
  • Сравним полученные результаты, оценим качество полученных решений и при необходимости повторим пункты 2-6;
  • Упакуем решение в сервис, который можно будет использовать (Продакшн).

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

Более того: качество итоговых моделей — не главная ценность этой статьи. Если вы уже погружены в прекрасный и чудесный мир Data Science и постоянно Кэгглите пока никто не видит, или (упаси бог) любите во время встреч с коллегами померяться длиной своего Хадупа, то скорее всего статья покажется вам простой и неинтересной. Поехали. Мы вас предупредили.

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

Формулируем задачу

Решать задачу, которая не имеет четких критериев решения можно бесконечно долго, поэтому сразу определимся, что мы хотим получить решение, которое позволило бы получить ответ «Гриффиндор», «Когтевран», «Пуффендуй» или «Слизерин» в ответ на введеную строку.

По сути дела, мы хотим получить черный ящик:

"Гарри поттер" => [?] => Griffindor

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

С точки зрения Data Science, мы решаем задачу классификации, а именно назначения объекту (строке, в виде имени и фамилии) некоторого класса (по факту это просто ярлык, или метка, которая может быть цифрой или 4 переменными, которые имеют значение да/нет). Раз уж речь зашла про вероятности, то формализуем задачу в более строгих математических терминах. Мы понимаем, что как минимум в случае Гарри будет корректным давать 2 ответа: Гриффиндор и Слизерин, поэтому лучше будет предсказывать не конкретный факультет, на который определяет шляпа, а вероятность того, что человек будет распределен на этот факультет, поэтому наше решение будет иметь в вид некоторой функции

$ f ( < Имя > < Фамилия > ) = (P_; P_{ravenclaw}; P_{hufflpuff}; P_{slitherin})$

Метрики и оценка качества

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

В жизни все хорошо и просто — мы интуитивно понимаем, что детектор спама должен пропускать минимум спама во входящие, а также пропускать максимум нужных писем и он уж точно не дожен отправлять в спам нужные письма.

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

Это именно то, что мы хотим от метрики в данном случае: чем меньше ложных срабатываний и чем точнее фактическое предсказание, тем больше будет ROC AUC. Бытовое «а давайте выбирать самый лучший» для нас будет ROC AUC.

5. У идеальной модели ROC AUC равен 1, у идеальной случайной модели, которая определяет классы абсолютно случайно — 0.

5;1]$" data-tex="display"/> <img src="https://habrastorage.org/getpro/habr/formulas/0b4/7aa/405/0b47aa4052f3e1bc9c7f1f486b959fee.svg" alt="$ROC\ AUC \in [0.

Алгоритмы

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

нейронные сети, факторизационные машины, линейную регрессию или, например, SVM.

Те, кто не проходил ни одного курса по анализу данных (особенно, субъективно лучшего — от ОДС), или просто читали n новостей про машинное обучение или ИИ, которые сейчас выходят даже в журналах «Рыболов-любитель», наверняка встречали названия общих групп алгоритмов: бэггинг, бустинг, метод опорных векторов (SVM), линейная регрессия. Вопреки популярному мнению, Data Science не ограничивается одними нейросетями, и для популяризации этой мысли, в данной статье нейронные сети оставлены в качестве упражнения любопытному читателю. Именно их мы и будем использовать для решения нашей задачи.

А если быть более точными, мы сравним между собой:

  • Линейную регрессию
  • Бустинг (XGboost, LightGBM)
  • Решаюшие деревья (строго говоря, это тот же бустинг, но вынесем отдельно: Extra Trees)
  • Бэггинг (Random Forest)
  • SVM

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

Данные

Для нашей задачи данные мы возьмем из wikia по миру Гарри Поттера. Поиск корректного датасета для обучения, а что еще более важно — легального для использования в нужных целях — одна из наиболее сложных и трудоёмких задач в Data Science. Важно, что данные в этом случае мы используем в некоммерческих целях, поэтому мы не нарушаем лицензию этого сайта. Например по этой ссылке можно найти всех персонажей, которые учились на факультете Гриффиндора.

Скачанные данные нужно вручную отмодерировать, чтобы удалить, например, «Седьмого Префекта Гриффиндора» и полуавтоматически удалить «Неизвестной девушки из Гриффиндора». Для тех, кто думает, что Data Scientists вот такие классные ребята, пойду в Data Scientists, пусть меня научат, мы напомним, что существует такой шаг, как очистка и подготовка данных. В реальной работе пропорционально большая часть задачи всегда связана с подготовкой, очисткой и восстановлением пропущенных значений в датасете.

Немного ctrl+c & ctrl+v и на выходе мы получим 4 текстовых файла, в которых находятся имена персонажей на 2 языках: английском и русском.

Изучаем собранные данные ( EDA, Exploratory Data Analysis)

К этому этапу у нас есть 4 файла, содержащие имена учеников факультетов, посмотрим более детально:

$ ls ../input
griffindor.txt hufflpuff.txt ravenclaw.txt slitherin.txt

Каждый файл содержит по 1 имени и фамилии (если она имеется) ученика на строчку:

$ wc -l ../input/*.txt 250 ../input/griffindor.txt 167 ../input/hufflpuff.txt 180 ../input/ravenclaw.txt 254 ../input/slitherin.txt 851 total

Собранные данные имеют вид:

$ cat ../input/griffindor.txt | head -3 && cat ../input/griffindor.txt | tail -3
Юан Аберкромби
Кэти Белл
Бем
Charlie Stainforth
Melanie Stanmore
Stewart

Вся наша задумка строится на предположении, что в именах и фамилиях есть что-то схожее, что наша черная коробка (или чёрная шляпа ) научиться различать.

Алгоритму можно скормить строки как есть, но результат не будет хорошим, потому что базовые модели не смогут самостоятельно понять, чем “Драко” отличается от “Гарри”, поэтому из наших имен и фамилий нужно будет извлечь признаки.

Подготовка данных (Feature Engineering)

Количество раз, которое человек менял работу за последний год, число пальцев на левой руке, объем двигателя автомобиля, превосходит ли пробег машины 100 000 км или нет. Признаки (или фичи, от англ. feature — свойство) — это отличительные свойства объекта. Всевозможных классификаций признаков придумано очень большое количество, какой-то единой системы в этом плане нет и быть не может, поэтому приведем примеры, какими могут быть признаки:

  1. Рациональным числом
  2. Категорией (до 12, 12-18 или 18+)
  3. Бинарным значением (Вернул первый кредит или нет)
  4. Датой, цветом, долей, итд.

По факту в самом процессе помогает здравый смысл, опыт и проверка гипотез в деле. Поиск (или формирование) признаков (на англ Feature Engineering) очень часто выделяется в отдельный этап исследования или работы специалиста по анализу данных. Иногда в этом есть шаманство, но общий подход очень простой: нужно делать то, что приходит в голову, а потом проверять, получилось ли улучшить решение за счет добавления нового признака. Угадать нужные признаки сразу — вопрос комбинации набитой руки, фундаментальных знаний и везения. Например, в качестве признака для нашей задачи мы можем брать количество шипящих в имени.

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

  1. 1 и последняя буквы слова — гласная или согласная
  2. Количество удвоенных гласных и согласных
  3. Количество гласных, согласных, глухих, звонких
  4. Длина имени, длина фамилии
  5. ...

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

>> from Phonetic import RussianLetter, EnglishLetter
>> RussianLetter('р').classify()
{'consonant': True, 'deaf': False, 'hard': False, 'mark': False, 'paired': False, 'shock': False, 'soft': False, 'sonorus': True, 'vowel': False}
>> EnglishLetter('d').classify()
{'consonant': True, 'deaf': False, 'hard': True, 'mark': False, 'paired': False, 'shock': False, 'soft': False, 'sonorus': True, 'vowel': False}

Теперь мы можем определить простые функции для подсчета статистик, например:

def starts_with_letter(word, letter_type='vowel'): """ Проверяет тип буквы, с которой начинается слово. :param word: слово :param letter_type: 'vowel' или 'consonant'. Гласная или согласная. :return: Boolean """ if len(word) == 0: return False return Letter(word[0]).classify()[letter_type] def count_letter_type(word): """ Подсчитывает число букв разного типа в слове. :param word: слово :param debug: флаг для дебага :return: :obj:`dict` of :obj:`str` => :int:count """ count = { 'consonant': 0, 'deaf': 0, 'hard': 0, 'mark': 0, 'paired': 0, 'shock': 0, 'soft': 0, 'sonorus': 0, 'vowel': 0 } for letter in word: classes = Letter(letter).classify() for key in count.keys(): if classes[key]: count[key] += 1 return count

С помощью этих функций мы можем получить уже первые признаки:

from feature_engineering import * >> print("Длина имени («Гарри»): ", len("Гарри"))
Длина имени («Гарри»): 5
>> print("Имя («Гарри») начинается с гласной: ", starts_with_letter('Аптека', 'vowel'))
Имя («Гарри») начинается с гласной: True
>> print("Фамилия («Поттер») начинается с согласной: ", starts_with_letter('Гарри', 'consonant'))
Фамилия («Поттер») начинается с согласной: True
>> count_Harry = count_letter_type("Гарри")
>> print ("Количество удвоенных согласных в имени («Гарри»): ", count_Harry['paired'])
Количество удвоенных согласных в имени («Гарри»): 1

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

$f ( < Имя > < Фамилия > ) => (длина_{имени}, длина_{фамилии}, ..., количество\_гласных_{фамилии})$

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

>> from data_loaders import load_processed_data >> hogwarts_df = load_processed_data() >> hogwarts_df.head()

При этом в результате мы получаем следующие признаки для каждого студента:

>> hogwarts_df[hogwarts_df.columns].dtypes

Полученные признаки

name object
surname object
is_english bool
name_starts_with_vowel bool
name_starts_with_consonant bool
name_ends_with_vowel bool
name_ends_with_consonant bool
name_length int64
name_vowels_count int64
name_double_vowels_count int64
name_consonant_count int64
name_double_consonant_count int64
name_paired_count int64
name_deaf_count int64
name_sonorus_count int64
surname_starts_with_vowel bool
surname_starts_with_consonant bool
surname_ends_with_vowel bool
surname_ends_with_consonant bool
surname_length int64
surname_vowels_count int64
surname_double_vowels_count int64
surname_consonant_count int64
surname_double_consonant_count int64
surname_paired_count int64
surname_deaf_count int64
surname_sonorus_count int64
is_griffindor int64
is_hufflpuff int64
is_ravenclaw int64
is_slitherin int64
dtype: object

Последние 4 колонки являются целевыми — они содержат информацию, на какой факультет зачислен студент.

Обучение алгоритмов

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

Как правило, процесс обучения очень прост и он состоит из нескольких шагов:

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

    Например, есть явление переобучения (англ overfitting) — алгоритм может буквально запомнить, какие признаки соотвествуют ответу и таким образом, ухудшить результат для объектов, которые не похожи на те, на которых он обучался. На практике, конечно же, все немного сложнее. Чтобы этого избежать есть различные методики и хаки.

Поэтому подготовим данные для Слизерина: Как уже было сказано выше, мы будем решать 4 задачи: по одной на каждый факультет.

# Копируем данные, чтобы случайно не потерять что-нибудь нужное:
>> data_full = hogwarts_df.drop( [ 'name', 'surname', 'is_griffindor', 'is_hufflpuff', 'is_ravenclaw' ], axis=1).copy()
# Берем данные для обучения, сбросив целевую колонку:
>> X_data = data_full.drop('is_slitherin', axis=1)
# В качестве целевой будет колонка, которая содержит 1 для учеников Слизерина
>> y = data_full.is_slitherin

Правилом хорошего тона считается также оценивать результату работы алгоритма на отдельных данных, которые алгоритм вообще не видел. Обучаясь, алгоритм постоянно сравнивает свои результаты с настоящими данными, для этого часть датасета выделяется под валидацию. Поэтому сейчас мы разделим выборку в пропорции 70/30 и обучим первый алгоритм:

from sklearn.cross_validation import train_test_split
from sklearn.ensemble import RandomForestClassifier # Фиксируем сид для воспроизводимоси результата
>> seed = 7
# Пропорции разделения датасета
>> test_size = 0.3
>> X_train, X_test, y_train, y_test = train_test_split(X_data, y, test_size=test_size, random_state=seed) >> rfc = RandomForestClassifier()
>> rfc_model = rfc.fit(X_train, y_train)

Теперь, если подать данные на вход этой модели, она выдаст результат. Готово. Для этого сначала подготовим функции для того, чтобы получить предсказание алгоритма: Это – весело, поэтому в первую очередь мы проверим, на признает ли модель в Гарри слизеринца.

Посмотреть код

from data_loaders import parse_line_to_hogwarts_df
import pandas as pd def get_single_student_features (name): """ Возвращает признаки для переданного имени :param name: string для имени и фамилии :return: pd.DataFrame объект с готовыми признаками """ featurized_person_df = parse_line_to_hogwarts_df(name) person_df = pd.DataFrame(featurized_person_df, columns=[ 'name', 'surname', 'is_english', 'name_starts_with_vowel', 'name_starts_with_consonant', 'name_ends_with_vowel', 'name_ends_with_consonant', 'name_length', 'name_vowels_count', 'name_double_vowels_count', 'name_consonant_count', 'name_double_consonant_count', 'name_paired_count', 'name_deaf_count', 'name_sonorus_count', 'surname_starts_with_vowel', 'surname_starts_with_consonant', 'surname_ends_with_vowel', 'surname_ends_with_consonant', 'surname_length', 'surname_vowels_count', 'surname_double_vowels_count', 'surname_consonant_count', 'surname_double_consonant_count', 'surname_paired_count', 'surname_deaf_count', 'surname_sonorus_count', ], index=[0] ) featurized_person = person_df.drop( ['name', 'surname'], axis = 1 ) return featurized_person def get_predictions_vector (model, person): """ Предсказывает вероятности классов :param model: обученная модель :param person: string полного имени :return: list вероятностей принадлежности классу """ encoded_person = get_single_student_features(person) return model.predict_proba(encoded_person)[0]

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

def score_testing_dataset (model): """ Предсказывает результат на искусственном наборе данных. :param model: обученная модель """ testing_dataset = [ "Кирилл Малев", "Kirill Malev", "Гарри Поттер", "Harry Potter", "Северус Снейп", "Северус Снегг","Severus Snape", "Том Реддл", "Tom Riddle", "Салазар Слизерин", "Salazar Slytherin"] for name in testing_dataset: print ("{} — {}".format(name, get_predictions_vector(model, name)[1])) score_testing_dataset(rfc_model)

Кирилл Малев — 0.5
Kirill Malev — 0.5
Гарри Поттер — 0.0
Harry Potter — 0.0
Северус Снейп — 0.75
Северус Снегг — 0.9
Severus Snape — 0.5
Том Реддл — 0.2
Tom Riddle — 0.5
Салазар Слизерин — 0.2
Salazar Slytherin — 0.3

Даже основатель факультета не попал бы на свой факультет, согласно мнению этой модели. Результаты получились сомнительные. Поэтому нужно оценить строгое качество: посмотреть на метрики, которые мы задали в начале:

from sklearn.metrics import accuracy_score, roc_auc_score, classification_report
predictions = rfc_model.predict(X_test)
print("Classification report: ")
print(classification_report(y_test, predictions))
print("Accuracy for Random Forest Model: %.2f" % (accuracy_score(y_test, predictions) * 100))
print("ROC AUC from first Random Forest Model: %.2f" % (roc_auc_score(y_test, predictions)))

Classification report: precision recall f1-score support 0 0.66 0.88 0.75 168 1 0.38 0.15 0.21 89 avg / total 0.56 0.62 0.56 257 Accuracy for Random Forest Model: 62.26
ROC AUC from first Random Forest Model: 0.51

51 говорит о том, что модель предсказывает незначительно лучше, чем бросок монеты. Неудивительно, что результаты получились такими сомнительными — ROC AUC около 0.

Тестирование полученных результатов. Метрики качества

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

Этот этап называется Model Tuning или Hyperparameter Optimization и его суть очень простая: выбирается тот набор настроек, который дает наилучший результат. В этом нет ничего сложного, для каждого алгоритмы мы обучаем 1 со стандартаными настройками, а также обучаем целый набор, перебирая различные варианты опций, которые влияют на качество работы алгоритма.

from model_training import train_classifiers
from data_loaders import load_processed_data
import warnings
warnings.filterwarnings('ignore') # Загружаем данные
hogwarts_df = load_processed_data() # Оставляем только нужные колонки
data_full = hogwarts_df.drop( [ 'name', 'surname', 'is_griffindor', 'is_hufflpuff', 'is_ravenclaw' ], axis=1).copy()
X_data = data_full.drop('is_slitherin', axis=1)
y = data_full.is_slitherin # Проводим исследование моделей
slitherin_models = train_classifiers(data_full, X_data, y)
score_testing_dataset(slitherin_models[5])

Кирилл Малев — 0.09437856871661066
Kirill Malev — 0.20820536334902712
Гарри Поттер — 0.07550095601699099
Harry Potter — 0.07683794773639624
Северус Снейп — 0.9414529336862744
Северус Снегг — 0.9293671807790949
Severus Snape — 0.6576783576162999
Том Реддл — 0.18577792617672767
Tom Riddle — 0.8351835484058869
Салазар Слизерин — 0.25930925139546795
Salazar Slytherin — 0.24008788903854789

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

>> from model_training import train_all_models # Обучаем модели для каждого факультета
>> slitherin_models, griffindor_models, ravenclaw_models, hufflpuff_models = \ train_all_models()

Длинный вывод результатов и результаты мультиномиальной регрессии

SVM Default Report
Accuracy for SVM Default: 73.93
ROC AUC for SVM Default: 0.53 Tuned SVM Report
Accuracy for Tuned SVM: 72.37
ROC AUC for Tuned SVM: 0.50 KNN Default Report
Accuracy for KNN Default: 70.04
ROC AUC for KNN Default: 0.58 Tuned KNN Report
Accuracy for Tuned KNN: 69.65
ROC AUC for Tuned KNN: 0.58 XGBoost Default Report
Accuracy for XGBoost Default: 70.43
ROC AUC for XGBoost Default: 0.54 Tuned XGBoost Report
Accuracy for Tuned XGBoost: 68.09
ROC AUC for Tuned XGBoost: 0.56 Random Forest Default Report
Accuracy for Random Forest Default: 73.93
ROC AUC for Random Forest Default: 0.62 Tuned Random Forest Report
Accuracy for Tuned Random Forest: 74.32
ROC AUC for Tuned Random Forest: 0.54 Extra Trees Default Report
Accuracy for Extra Trees Default: 69.26
ROC AUC for Extra Trees Default: 0.57 Tuned Extra Trees Report
Accuracy for Tuned Extra Trees: 73.54
ROC AUC for Tuned Extra Trees: 0.55 LGBM Default Report
Accuracy for LGBM Default: 70.82
ROC AUC for LGBM Default: 0.62 Tuned LGBM Report
Accuracy for Tuned LGBM: 74.71
ROC AUC for Tuned LGBM: 0.53 RGF Default Report
Accuracy for RGF Default: 70.43
ROC AUC for RGF Default: 0.58 Tuned RGF Report
Accuracy for Tuned RGF: 71.60
ROC AUC for Tuned RGF: 0.60 FRGF Default Report
Accuracy for FRGF Default: 68.87
ROC AUC for FRGF Default: 0.59 Tuned FRGF Report
Accuracy for Tuned FRGF: 69.26
ROC AUC for Tuned FRGF: 0.59 SVM Default Report
Accuracy for SVM Default: 70.43
ROC AUC for SVM Default: 0.50 Tuned SVM Report
Accuracy for Tuned SVM: 71.60
ROC AUC for Tuned SVM: 0.50 KNN Default Report
Accuracy for KNN Default: 63.04
ROC AUC for KNN Default: 0.49 Tuned KNN Report
Accuracy for Tuned KNN: 65.76
ROC AUC for Tuned KNN: 0.50 XGBoost Default Report
Accuracy for XGBoost Default: 69.65
ROC AUC for XGBoost Default: 0.54 Tuned XGBoost Report
Accuracy for Tuned XGBoost: 68.09
ROC AUC for Tuned XGBoost: 0.50 Random Forest Default Report
Accuracy for Random Forest Default: 66.15
ROC AUC for Random Forest Default: 0.51 Tuned Random Forest Report
Accuracy for Tuned Random Forest: 70.43
ROC AUC for Tuned Random Forest: 0.50 Extra Trees Default Report
Accuracy for Extra Trees Default: 64.20
ROC AUC for Extra Trees Default: 0.49 Tuned Extra Trees Report
Accuracy for Tuned Extra Trees: 70.82
ROC AUC for Tuned Extra Trees: 0.51 LGBM Default Report
Accuracy for LGBM Default: 67.70
ROC AUC for LGBM Default: 0.56 Tuned LGBM Report
Accuracy for Tuned LGBM: 70.82
ROC AUC for Tuned LGBM: 0.50 RGF Default Report
Accuracy for RGF Default: 66.54
ROC AUC for RGF Default: 0.52 Tuned RGF Report
Accuracy for Tuned RGF: 65.76
ROC AUC for Tuned RGF: 0.53 FRGF Default Report
Accuracy for FRGF Default: 65.76
ROC AUC for FRGF Default: 0.53 Tuned FRGF Report
Accuracy for Tuned FRGF: 69.65
ROC AUC for Tuned FRGF: 0.52 SVM Default Report
Accuracy for SVM Default: 74.32
ROC AUC for SVM Default: 0.50 Tuned SVM Report
Accuracy for Tuned SVM: 74.71
ROC AUC for Tuned SVM: 0.51 KNN Default Report
Accuracy for KNN Default: 69.26
ROC AUC for KNN Default: 0.48 Tuned KNN Report
Accuracy for Tuned KNN: 73.15
ROC AUC for Tuned KNN: 0.49 XGBoost Default Report
Accuracy for XGBoost Default: 72.76
ROC AUC for XGBoost Default: 0.49 Tuned XGBoost Report
Accuracy for Tuned XGBoost: 74.32
ROC AUC for Tuned XGBoost: 0.50 Random Forest Default Report
Accuracy for Random Forest Default: 73.93
ROC AUC for Random Forest Default: 0.52 Tuned Random Forest Report
Accuracy for Tuned Random Forest: 74.32
ROC AUC for Tuned Random Forest: 0.50 Extra Trees Default Report
Accuracy for Extra Trees Default: 73.93
ROC AUC for Extra Trees Default: 0.52 Tuned Extra Trees Report
Accuracy for Tuned Extra Trees: 73.93
ROC AUC for Tuned Extra Trees: 0.50 LGBM Default Report
Accuracy for LGBM Default: 73.54
ROC AUC for LGBM Default: 0.52 Tuned LGBM Report
Accuracy for Tuned LGBM: 74.32
ROC AUC for Tuned LGBM: 0.50 RGF Default Report
Accuracy for RGF Default: 73.54
ROC AUC for RGF Default: 0.51 Tuned RGF Report
Accuracy for Tuned RGF: 73.93
ROC AUC for Tuned RGF: 0.50 FRGF Default Report
Accuracy for FRGF Default: 73.93
ROC AUC for FRGF Default: 0.53 Tuned FRGF Report
Accuracy for Tuned FRGF: 73.93
ROC AUC for Tuned FRGF: 0.50 SVM Default Report
Accuracy for SVM Default: 80.54
ROC AUC for SVM Default: 0.50 Tuned SVM Report
Accuracy for Tuned SVM: 80.93
ROC AUC for Tuned SVM: 0.52 KNN Default Report
Accuracy for KNN Default: 78.60
ROC AUC for KNN Default: 0.50 Tuned KNN Report
Accuracy for Tuned KNN: 80.16
ROC AUC for Tuned KNN: 0.51 XGBoost Default Report
Accuracy for XGBoost Default: 80.54
ROC AUC for XGBoost Default: 0.50 Tuned XGBoost Report
Accuracy for Tuned XGBoost: 77.04
ROC AUC for Tuned XGBoost: 0.52 Random Forest Default Report
Accuracy for Random Forest Default: 77.43
ROC AUC for Random Forest Default: 0.49 Tuned Random Forest Report
Accuracy for Tuned Random Forest: 80.54
ROC AUC for Tuned Random Forest: 0.50 Extra Trees Default Report
Accuracy for Extra Trees Default: 76.26
ROC AUC for Extra Trees Default: 0.48 Tuned Extra Trees Report
Accuracy for Tuned Extra Trees: 78.60
ROC AUC for Tuned Extra Trees: 0.50 LGBM Default Report
Accuracy for LGBM Default: 75.49
ROC AUC for LGBM Default: 0.51 Tuned LGBM Report
Accuracy for Tuned LGBM: 80.54
ROC AUC for Tuned LGBM: 0.50 RGF Default Report
Accuracy for RGF Default: 78.99
ROC AUC for RGF Default: 0.52 Tuned RGF Report
Accuracy for Tuned RGF: 75.88
ROC AUC for Tuned RGF: 0.55 FRGF Default Report
Accuracy for FRGF Default: 76.65
ROC AUC for FRGF Default: 0.50 # Респект тем читателям, которые открывают кат и смотрят на результаты

from sklearn.linear_model import LogisticRegression clf = LogisticRegression(random_state=0, solver='lbfgs', multi_class='multinomial') hogwarts_df = load_processed_data_multi() # Оставляем только нужные колонки
data_full = hogwarts_df.drop( [ 'name', 'surname', ], axis=1).copy()
X_data = data_full.drop('faculty', axis=1)
y = data_full.faculty clf.fit(X_data, y)
score_testing_dataset(clf)

Кирилл Малев — [0.3602361 0.16166944 0.16771712 0.31037733]
Kirill Malev — [0.47473072 0.16051924 0.13511385 0.22963619]
Гарри Поттер — [0.38697926 0.19330242 0.17451052 0.2452078 ]
Harry Potter — [0.40245098 0.16410043 0.16023278 0.27321581]
Северус Снейп — [0.13197025 0.16438855 0.17739254 0.52624866]
Северус Снегг — [0.17170203 0.1205678 0.14341742 0.56431275]
Severus Snape — [0.15558044 0.21589378 0.17370406 0.45482172]
Том Реддл — [0.39301231 0.07397324 0.1212741 0.41174035]
Tom Riddle — [0.26623969 0.14194379 0.1728505 0.41896601]
Салазар Слизерин — [0.24843037 0.21632736 0.21532696 0.3199153 ]
Salazar Slytherin — [0.09359144 0.26735897 0.2742305 0.36481909]

И confusion_matrix:

confusion_matrix(clf.predict(X_data), y)

array([[144, 68, 64, 78], [ 8, 9, 8, 6], [ 22, 18, 31, 20], [ 77, 73, 78, 151]])

def get_predctions_vector (models, person): predictions = [get_predictions_vector (model, person)[1] for model in models] return { 'slitherin': predictions[0], 'griffindor': predictions[1], 'ravenclaw': predictions[2], 'hufflpuff': predictions[3] } def score_testing_dataset (models): testing_dataset = [ "Кирилл Малев", "Kirill Malev", "Гарри Поттер", "Harry Potter", "Северус Снейп", "Северус Снегг","Severus Snape", "Том Реддл", "Tom Riddle", "Салазар Слизерин", "Salazar Slytherin"] data = [] for name in testing_dataset: predictions = get_predctions_vector(models, name) predictions['name'] = name data.append(predictions) scoring_df = pd.DataFrame(data, columns=['name', 'slitherin', 'griffindor', 'hufflpuff', 'ravenclaw']) return scoring_df # Data Science — лучший выбор для тех, кто хочет работать с топ моделями
top_models = [ slitherin_models[3], griffindor_models[3], ravenclaw_models[3], hufflpuff_models[3]
] score_testing_dataset(top_models)

name slitherin griffindor hufflpuff ravenclaw
0 Кирилл Малев 0.349084 0.266909 0.110311 0.091045
1 Kirill Malev 0.289914 0.376122 0.384986 0.103056
2 Гарри Поттер 0.338258 0.400841 0.016668 0.124825
3 Harry Potter 0.245377 0.357934 0.026287 0.154592
4 Северус Снейп 0.917423 0.126997 0.176640 0.096570
5 Северус Снегг 0.969693 0.106384 0.150146 0.082195
6 Severus Snape 0.663732 0.259189 0.290252 0.074148
7 Том Реддл 0.268466 0.579401 0.007900 0.083195
8 Tom Riddle 0.639731 0.541184 0.084395 0.156245
9 Салазар Слизерин 0.653595 0.147506 0.172940 0.137134
10 Salazar Slytherin 0.647399 0.169964 0.095450 0.26126

При этом средний ROC AUC чуть лучше, чем 0. Как видно из тестового датасета, не все слизеринцы злы шляпа иногда ошибается. Чтобы избежать нежелательных ошибок, остается несколько вариантов: 5.

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

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

Модели, рассмотренные выше были обучены только на 70% данных. Важно! Поэтому для использования в продакшне, мы заново обучим 4 модели с использованием всего набора данных и снова оценим результаты.

from model_training import train_production_models
from xgboost import XGBClassifier best_models = []
for i in range (0,4): best_models.append(XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1, colsample_bytree=0.7, gamma=0, learning_rate=0.05, max_delta_step=0, max_depth=6, min_child_weight=11, missing=-999, n_estimators=1000, n_jobs=1, nthread=4, objective='binary:logistic', random_state=0, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, seed=1337, silent=1, subsample=0.8)) slitherin_model, griffindor_model, ravenclaw_model, hufflpuff_model = \ train_production_models(best_models) top_models = slitherin_model, griffindor_model, ravenclaw_model, hufflpuff_model
score_testing_dataset(top_models)

name slitherin griffindor hufflpuff ravenclaw
0 Кирилл Малев 0.273713 0.372337 0.065923 0.279577
1 Kirill Malev 0.401603 0.761467 0.111068 0.023902
2 Гарри Поттер 0.031540 0.616535 0.196342 0.217829
3 Harry Potter 0.183760 0.422733 0.119393 0.173184
4 Северус Снейп 0.945895 0.021788 0.209820 0.019449
5 Северус Снегг 0.950932 0.088979 0.084131 0.012575
6 Severus Snape 0.634035 0.088230 0.249871 0.036682
7 Том Реддл 0.426440 0.431351 0.028444 0.083636
8 Tom Riddle 0.816804 0.136530 0.069564 0.035500
9 Салазар Слизерин 0.409634 0.213925 0.028631 0.252723
10 Salazar Slytherin 0.824590 0.067910 0.111147 0.085710

Если внимательно присмотреться в эту таблицу, то видно, что результаты улучшились.

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

import pickle pickle.dump(slitherin_model, open("../output/slitherin.xgbm", "wb"))
pickle.dump(griffindor_model, open("../output/griffindor.xgbm", "wb"))
pickle.dump(ravenclaw_model, open("../output/ravenclaw.xgbm", "wb"))
pickle.dump(hufflpuff_model, open("../output/hufflpuff.xgbm", "wb"))

Продакшн

А для того, чтобы ее вообще увидел свет, нужно, чтобы пользоваться ей было удобно конечному пользователю. Даже самая крутая и сложная модель будет приносить пользу только тогда, когда ей смогут пользоваться люди.

Даже если эту роль приходится взять на себя, то модель нужно упаковать в удобный интерфейс. Конечным пользователем любой МЛ модели часто является не конечный пользователь сервиса, а разработчик, который будет это интегрировать в продукт. Поэтому вспоминаем, что каждый Data Scientist — это немного бекендер и погружаемся в бекенд-разработку.

Основные требования к задаче интеграции:

  • Модель должна работать отдельным сервисом;
  • Принимать данные в виде json-запроса и также отдавать ответ в виде json;
  • Модель должна быть готова к использованию в любой среде без длительной настройки.

Само решение, которое будет принимать данные и возвращать ответ мы сделаем на flask. Конечно, решение будет упаковано в docker-контейнер, чтобы избавить разработчика от необходимости установки лишних пакетов и настройки python-окружения.

Первый вариант

from __future__ import print_function # In python 2.7
import os
import subprocess
import json
import re
from flask import Flask, request, jsonify
from inspect import getmembers, ismethod
import numpy as npb
import pandas as pd
import math
import os
import pickle
import xgboost as xgb
import sys
from letter import Letter
from talking_hat import *
from sklearn.ensemble import RandomForestClassifier
import warnings def prod_predict_classes_for_name (full_name): featurized_person = parse_line_to_hogwarts_df(full_name) person_df = pd.DataFrame(featurized_person, columns=[ 'name', 'surname', 'is_english', 'name_starts_with_vowel', 'name_starts_with_consonant', 'name_ends_with_vowel', 'name_ends_with_consonant', 'name_length', 'name_vowels_count', 'name_double_vowels_count', 'name_consonant_count', 'name_double_consonant_count', 'name_paired_count', 'name_deaf_count', 'name_sonorus_count', 'surname_starts_with_vowel', 'surname_starts_with_consonant', 'surname_ends_with_vowel', 'surname_ends_with_consonant', 'surname_length', 'surname_vowels_count', 'surname_double_vowels_count', 'surname_consonant_count', 'surname_double_consonant_count', 'surname_paired_count', 'surname_deaf_count', 'surname_sonorus_count', ], index=[0] ) slitherin_model = pickle.load(open("models/slitherin.xgbm", "rb")) griffindor_model = pickle.load(open("models/griffindor.xgbm", "rb")) ravenclaw_model = pickle.load(open("models/ravenclaw.xgbm", "rb")) hufflpuff_model = pickle.load(open("models/hufflpuff.xgbm", "rb")) predictions = get_predctions_vector([ slitherin_model, griffindor_model, ravenclaw_model, hufflpuff_model ], person_df.drop(['name', 'surname'], axis=1)) return { 'slitherin': float(predictions[0][1]), 'griffindor': float(predictions[1][1]), 'ravenclaw': float(predictions[2][1]), 'hufflpuff': float(predictions[3][1]) } def predict(params): fullname = params['fullname'] print(params) return prod_predict_classes_for_name(fullname) def create_app(): app = Flask(__name__) functions_list = [predict] @app.route('/<func_name>', methods=['POST']) def api_root(func_name): for function in functions_list: if function.__name__ == func_name: try: json_req_data = request.get_json() if json_req_data: res = function(json_req_data) else: return jsonify({"error": "error in receiving the json input"}) except Exception as e: data = { "error": "error while running the function" } if hasattr(e, 'message'): data['message'] = e.message elif len(e.args) >= 1: data['message'] = e.args[0] return jsonify(data) return jsonify({"success": True, "result": res}) output_string = 'function: %s not found' % func_name return jsonify({"error": output_string}) return app if __name__ == '__main__': app = create_app() app.run(host='0.0.0.0')

Dockerfile:

FROM datmo/python-base:cpu-py35 # Используем python3-wheel, чтобы не тратить время на сборку пакетов
RUN apt-get update; apt-get install -y python3-pip python3-numpy python3-scipy python3-wheel
ADD requirements.txt /
RUN pip3 install -r /requirements.txt RUN mkdir /code;mkdir /code/models
COPY ./python_api.py ./talking_hat.py ./letter.py ./request.py /code/
COPY ./models/* /code/models/ WORKDIR /code CMD python3 /code/python_api.py

Модель собирается очень просто:

docker build -t talking_hat . && docker rm talking_hat && docker run --name talking_hat -p 5000:5000 talking_hat

Тестирование продакшн модели

Исправим это, но сначала замерим производительность данного решения при помощи Apache Benchmark. У решения есть недостаток — скрипт каждый раз тратит время на выгрузку и загрузку модели из памяти. Тестирование — наше все. Действительно будет недальновидно провести тестирование модели, но не провести тестирование конечного решения.

$ ab -p data.json -T application/json -c 50 -n 10000 http://0.0.0.0:5000/predict

Вывод ab

This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 0.0.0.0 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests Server Software: Werkzeug/0.14.1
Server Hostname: 0.0.0.0
Server Port: 5000 Document Path: /predict
Document Length: 141 bytes Concurrency Level: 50
Time taken for tests: 238.552 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 2880000 bytes
Total body sent: 1800000
HTML transferred: 1410000 bytes
Requests per second: 41.92 [#/sec] (mean)
Time per request: 1192.758 [ms] (mean)
Time per request: 23.855 [ms] (mean, across all concurrent requests)
Transfer rate: 11.79 [Kbytes/sec] received 7.37 kb/s sent 19.16 kb/s total Connection Times (ms) min mean[+/-sd] median max
Connect: 0 0 0.1 0 3
Processing: 199 1191 352.5 1128 3352
Waiting: 198 1190 352.5 1127 3351
Total: 202 1191 352.5 1128 3352 Percentage of the requests served within a certain time (ms) 50% 1128 66% 1277 75% 1378 80% 1451 90% 1668 95% 1860 98% 2096 99% 2260 100% 3352 (longest request)

Теперь приведем решение к варианту, когда модель постоянно загружена в память:

def prod_predict_classes_for_name (full_name): <...> predictions = get_predctions_vector([ app.slitherin_model, app.griffindor_model, app.ravenclaw_model, app.hufflpuff_model ], person_df.drop(['name', 'surname'], axis=1)) return { 'slitherin': float(predictions[0][1]), 'griffindor': float(predictions[1][1]), 'ravenclaw': float(predictions[2][1]), 'hufflpuff': float(predictions[3][1]) } def create_app(): <...> with app.app_context(): app.slitherin_model = pickle.load(open("models/slitherin.xgbm", "rb")) app.griffindor_model = pickle.load(open("models/griffindor.xgbm", "rb")) app.ravenclaw_model = pickle.load(open("models/ravenclaw.xgbm", "rb")) app.hufflpuff_model = pickle.load(open("models/hufflpuff.xgbm", "rb")) return app

И замерим результаты тестов:

$ docker build -t talking_hat . && docker rm talking_hat && docker run --name talking_hat -p 5000:5000 talking_hat
$ ab -p data.json -T application/json -c 50 -n 10000 http://0.0.0.0:5000/predict

Вывод ab

This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 0.0.0.0 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests Server Software: Werkzeug/0.14.1
Server Hostname: 0.0.0.0
Server Port: 5000 Document Path: /predict
Document Length: 141 bytes Concurrency Level: 50
Time taken for tests: 219.812 seconds
Complete requests: 10000
Failed requests: 3 (Connect: 0, Receive: 0, Length: 3, Exceptions: 0)
Total transferred: 2879997 bytes
Total body sent: 1800000
HTML transferred: 1409997 bytes
Requests per second: 45.49 [#/sec] (mean)
Time per request: 1099.062 [ms] (mean)
Time per request: 21.981 [ms] (mean, across all concurrent requests)
Transfer rate: 12.79 [Kbytes/sec] received 8.00 kb/s sent 20.79 kb/s total Connection Times (ms) min mean[+/-sd] median max
Connect: 0 0 0.1 0 2
Processing: 235 1098 335.2 1035 3464
Waiting: 235 1097 335.2 1034 3462
Total: 238 1098 335.2 1035 3464 Percentage of the requests served within a certain time (ms) 50% 1035 66% 1176 75% 1278 80% 1349 90% 1541 95% 1736 98% 1967 99% 2141 100% 3464 (longest request)

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

Заключение

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

Конечно, решение можно улучшить и далее:

  • С точки зрения feature engineering-а можно использовать фонетический поиск (вводная статья на Хабре), в частности, Soundex алгоритм для определения звучания имени.
  • Можно воспользоваться замечательной статьёй в блоге PyTorch и применить рекуррентную нейронную сеть для классификации имен. Эта статья почти готова для наших задач, тк в ней рассматривается определение страны происхождения имени, то есть решается та же самая задач классификации имени.
  • Можно перейти от синхронного flask к асинхронному Quart, который теоретически выглядит пригодным для решения нашей задачи, что сделает решение еще более уже устойчивым к высоким нагрузкам.
  • Добавить в репозиторий телегам-бота или демо-страничку, чтобы решение было удобнее тестировать.

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

Эта статья не была бы опубликована без сообщества Open Data Science, которое объединяет большое количество русскоязычных специалистов в области анализа данных.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть