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


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


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


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


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


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


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


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


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


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


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

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


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


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


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


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


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


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


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


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


    $ROC\ AUC \in [0.5;1]$


    Алгоритмы


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


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


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


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


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

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


    Данные


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



    Для тех, кто думает, что 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)


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


    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

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


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


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



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


    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;
    • Модель должна быть готова к использованию в любой среде без длительной настройки.

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


    Первый вариант
    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, которое объединяет большое количество русскоязычных специалистов в области анализа данных.

    • +65
    • 12,2k
    • 1

    Open Data Science

    225,00

    Крупнейшее русскоязычное Data Science сообщество

    Поделиться публикацией
    Комментарии 1
      +1
      О, наконец-то я увидел реальный пример, как обученную модель вывести в прод! Ни в одной прочитанной мной книжке по ML этого почему-то не было. Большое человеческое спасибо!

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

      Самое читаемое