company_banner

Python’ом по машинлернингу

    Сегодня только ленивый не говорит (пишет, думает) про машинное обучение, нейросети и искусственный интеллект в целом. Всего лишь в прошлом году ML сравнили с подростковым сексом — все хотят, но никто не занимается. Сегодня все озабочены тем, что ИИ нас оставит без работы. Хотя, судя по последним исследованиям Gartner, можно успокоиться, так как к 2020 году благодаря ИИ появится больше рабочих мест, чем ликвидируется. Так что, дорогой друг, учи ML, и будет тебе счастье.



    Примечание: мы продолжаем серию публикаций полных версий статей из журнала Хакер. Орфография и пунктуация автора сохранены.


    Изначально статья называлась **Операционализация ML-моделей Python с использованием Azure Functions**, думаю, тебе стоит узнать об этом сейчас, потому что дальше легко не будет. :)

    В этой статье мы хотим показать ML на практическом кейсе — на примере проекта, который мы делали для Актион-пресс (сервис онлайн-подписки). Уверен, описанное в этом примере может пригодиться многим. Почему многим? Да потому, что проблема, которую мы решали, называлась «сортировка и пересылка по адресу огромного количества электронных писем». Проблема гигантской переписки, которую менеджерам приходится сортировать и пересылать в соответствующие отделы, практически универсальная, и проблему эту надо решать современными способами.


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


    Модель машинного обучения


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


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


    Здесь я поделюсь своим опытом и дам некоторые рекомендации тебе, чтобы ты мог сам поэкспериментировать с этим кодом или повторно использовать его.


    Чтобы сохранить конфиденциальность, исходный набор данных был заменен аналогичным общедоступным набором для классификации отзывов о McDonalds. См. файл data/data.csv.


    Сами данные были представлены в файлах CSV с тремя столбцами: Id, Text и Class. И поскольку в NLTK не предусмотрена встроенная поддержка чтения данных из файлов в формате CSV, мы написали собственный модуль, позволяющий читать файлы из папки в виде одного dataframe pandas или извлекать текст в виде списков абзацев, предложений, слов и так далее в формате NLTK.


    А вот код для инициализации этого самописного модуля чтения CsvCorpusReader данными клиента. Реализацию класса можно увидеть в файле lib\corpus.py. Настоятельно рекомендую тебе ознакомиться с содержимым файла Experiments\TrainingExperiment.py.


    #%% create corpus
    corpus = CsvCorpusReader(".\data", ["data.csv"],
                             encoding="utf8",
                             default_text_selector=lambda row: row["Text"])

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


    Ниже мы даем экстрактору команду вернуть документы в виде списка слов, отбрасывая структуру абзацев или предложений (см. keep_levels=Levels.Nothing). Затем переводим каждое слово в нижний регистр, отбрасываем любые стоп-слова и выделяем основы слов. На заключительном этапе удаляем низкочастотные слова, предполагая, что это просто опечатки или что они не оказывают существенного влияния на классификацию.


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


    #%% tokenize the text
    stop_words = ['would', 'like', 'mcdonald']
    text_processor = generate_processor(keep_alpha_only=True,
                                        to_lower=True,
                                        stopwords_langs=['english'],
                                        add_stopwords=stop_words,
                                        stemmer_langs=['english'])
    docs_factory = lambda: corpus.words(keep_levels=Levels.Nothing, **text_processor)
    
    word_frequencies = Counter((word for doc in docs_factory() for word in doc))
    min_word_freq = 3
    docs = [
        [
            word
            for word in doc if word_frequencies[word] >= min_word_freq
         ] for doc in docs_factory()
    ]

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


    Мы протестировали несколько разных подходов (включая BoW, TF-IDF, LSI, RP и w2v), но классическая модель LSI с 500 извлеченными топиками дала наилучшие результаты (AUC = 0,98) в нашем случае. Для начала код проверяет наличие существующей сериализованной модели в общей папке. Если модели нет, код обучает новую модель с использованием предварительно подготовленных данных и сохраняет результат на диск. Если модель обнаружена, она просто загружается в память. Затем код преобразовывает набор данных и повторяет поток со следующим вложением.


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


    Самая очевидная из них связана с тем, что письма тех типов, которые мы искали, имели предсказуемые и повторяющиеся шаблоны слов, как в случае автоответов (например, «Спасибо за ваше письмо… Меня не будет в офисе до… Если вопрос срочный...»). Поэтому для их обработки вполне достаточно чего-то простого, например TF-IDF. LSI поддерживает общую идеологию, и эту модель можно рассматривать как способ добавления синонимов, подходящих для обработки. В то же время алгоритм word2vec, прошедший обучение на Википедии, вероятно, генерирует ненужный шум из-за сложных синонимичных структур, тем самым «размывая» шаблоны в сообщениях и, следовательно, снижая точность классификации.


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


    #%% convert to Bag of Words representation
    dictionary_path = os.path.join(preprocessing_path, 'dictionary.bin')
    if os.path.exists(dictionary_path):
        dictionary = corpora.Dictionary.load(dictionary_path)
    else:
        dictionary = corpora.Dictionary(docs)
        dictionary.save(dictionary_path)
    
    docs_bow = [dictionary.doc2bow(doc) for doc in docs]
    nested_partial_print(docs_bow)
    
    #%% convert to tf-idf representation
    tfidf_path = os.path.join(preprocessing_path, 'tfidf.bin')
    if os.path.exists(tfidf_path):
        model_tfidf = models.TfidfModel.load(tfidf_path)
    else:
        model_tfidf = models.TfidfModel(docs_bow)
        model_tfidf.save(tfidf_path)
    
    docs_tfidf = nested_to_list(model_tfidf[docs_bow])
    
    #%% train and convert to LSI representation
    lsi_path = os.path.join(preprocessing_path, 'lsi.bin')
    lsi_num_topics = 500
    if os.path.exists(lsi_path):
        model_lsi = models.LsiModel.load(lsi_path)
    else:
        model_lsi = models.LsiModel(docs_tfidf, id2word=dictionary, num_topics=lsi_num_topics)
        model_lsi.save(lsi_path)
    
    docs_lsi = model_lsi[docs_tfidf]

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


    Как я уже говорил выше, мы используем несколько бинарных вместо одного многоклассового классификатора. Именно поэтому создаем бинарную цель для одного из классов (в этом образце это SlowService). Ты можешь изменять значение переменной class_to_find и выполнять приведенный ниже код повторно, чтобы обучить каждый одноклассовый классификатор в отдельности. Скрипт оценки сделан так, чтобы работать с несколькими моделями, и автоматически загружает их из выделенной папки. Наконец, формируется обучающий и тестовый набор данных, строки с пропусками при этом полностью исключаются.


    #%% create target
    class_to_find = "SlowService"
    df["Target"] = df.apply(lambda row: 1 if class_to_find in row["Class"] else 0, axis=1)
    df.groupby(by=["Target"]).count()
    
    #%% create features and targets dataset
    features = pd.DataFrame(docs_features, columns=["F" + str(i) for i in range(lsi_num_topics)])
    notnul_idx = features.notnull().all(axis=1)
    features = features[notnul_idx]
    df_notnull = df[notnul_idx]
    target = df_notnull[["Target"]]
    plot_classes_scatter(features.values, target["Target"].values)
    
    #%% split dataset to train and test
    train_idx, test_idx = train_test_split(df_notnull.index.values, test_size=0.3, random_state=56)
    df_train = df_notnull.loc[train_idx]
    features_train = features.loc[train_idx]
    target_train = target.loc[train_idx]
    df_test = df_notnull.loc[test_idx]
    features_test = features.loc[test_idx]
    target_test = target.loc[test_idx]

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


    Как ты мог заметить, в приведенном ниже коде мы придерживаемся специального формата имени модели: class_{0}_thresh_{1}.bin. Это необходимо для определения имени класса и соответствующего порогового значения в ходе дальнейшей оценки.


    И последнее замечание, прежде чем мы продолжим. В качестве инструмента разработки я выбрал Visual Studio Code. Это простой в использовании легковесный редактор, который даже предоставляет базовые возможности IntelliSense (автозавершение кода и подсказки) для такого динамичного языка, как Python. В то же время расширения Jupyter и Python в сочетании с ядром IPython позволяют выполнять код поячеечно и визуализировать результат без повторного запуска скрипта, что всегда удобно для задач ML. Да, это похоже на стандартный Jupyter, но с IntelliSense и ориентацией на код/git. Я рекомендую тебе попробовать, хотя бы пока ты работаешь с образцом, поскольку для продуктивной разработки тут применяется множество других возможностей, связанных с VS Code.


    Что касается кода ниже, строка с plot ROC threshold values — это примеры использования расширения Jupyter. Ты можешь нажать специальную кнопку Run cell (Выполнить ячейку) над ячейкой, чтобы увидеть значения TP и FP и сравнить их с пороговым значением Threshold на панели результатов справа. Мы активно использовали эту диаграмму во время работы, поскольку из-за выраженного дисбаланса в наборе данных оптимальный уровень отсечки всегда был около 0,04 вместо привычных 0,5. Если ты не можешь использовать VS Code для тестирования, можно просто запустить скрипт с помощью стандартных инструментов Python и после просмотра результатов в отдельном окне внести изменения непосредственно в имя файла.


    #%% train logistic classifier
    classifier = LogisticRegression()
    classifier.fit(features_train, target_train)
    
    #%% score on test
    scores_test = classifier.predict_proba(features_test)[:, 1]
    
    #%% plot ROC threshold values
    pd.DataFrame(nested_to_list(zip(tsh, tp_test, fp_test, fp_test-tp_test)), columns=['Threshold', 'True Positive Rate', 'False Positive Rate', 'Difference']).plot(x='Threshold')
    plt.xlim(0, 1)
    plt.ylim([0,1])
    plt.grid()
    plt.show()
    
    #%% save model
    threshold = 0.25
    model_filename = 'class_{0}_thresh_{1}.bin'.format(class_to_find, threshold)
    joblib.dump(classifier, os.path.join(model_path, model_filename))

    Теперь настало время скрипта оценки: Score\run.py. Нового в нем очень мало, большую часть кода взяли из первоначального обучающего эксперимента, рассмотренного ранее. Ознакомься с содержимым этого файла в репозитории GitHub.


    На вход подается файл CSV для оценки, на выходе получаем два разных файла, один содержит оцененные классы, другой — идентификаторы строк, оценить которые не представляется возможным. Я объясню причину использования файла позже, когда будем говорить об операционализации.


    В конце этого раздела хочу пояснить, почему мы используем несколько бинарных вместо одного многоклассового классификатора. Во-первых, так было гораздо проще начать, чтобы работать и оптимизировать производительность на классах по отдельности. Такой подход также позволяет использовать различные математические модели для разных классов, как в случае с автоответами, которые часто имеют довольно жесткую структуру, и их можно обрабатывать с помощью простого bag of words. В то же время, с точки зрения ИТ-специалиста, что-то наподобие кода ниже может упростить развертывание, позволив подключать новые или менять существующие модели, не затрагивая другие.


    model_paths = [path for path in os.listdir(os.path.join('..', 'model')) if path.startswith('class_') ]
    
    for model_path in model_paths:
        model = joblib.load(os.path.join('..', 'model', model_path))
        res = model.predict_proba(features_notnull)[:, 1]
    
        class_name = model_path.split('_')[1]
        threshold = float(model_path.rsplit('.', 1)[0].split('_')[-1])
    
        result.loc[:, "class_" + class_name] = res > threshold
        result.loc[:, "class_" + class_name + "_score"] = res

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


    • клонируй репозиторий, следуй инструкциям по развертыванию локальной среды Anaconda и установи Visual Studio Code с нужным расширением;
    • помести свои данные в поддерживаемом формате в файл data\data.csv и открой файл Experiment\TrainingExperiment.py, чтобы обучить модель на любом классе, который ты хочешь оценить;
    • не забудь предварительно удалить всю папку models, поскольку в противном случае код попытается повторно использовать преобразования и модели из образца;
    • перейди к Score\run.py, замени данные в файле Score\debug\input.csv собственными и построчно выполни скрипт с помощью расширения Jupyter.

    В VS Code ты даже можешь открыть раздел отладки Debug (Ctrl + Alt + D), выбрать Score (Python) в качестве конфигурации и нажать Start Debugging (Выполнить отладку), чтобы провести построчный анализ кода в редакторе. Когда алгоритмы завершат свою работу, результаты можно будет найти в файлах input.scores.csv и input.unscorable.csv в папке Score\debug.


    Операционализация


    Поддержка Python в Azure Functions до сих пор находится в раннем preview, поэтому использование его для mission critical задач нежелательно. Но часто ML к таким не относится, а потому удобство реализации может перевесить сложности с адаптацией предварительной версии.


    Итак, на этом этапе у нас было два скрипта. Скрипт Experiments\TraintExperiment.py обучает модель, затем преобразованную и обученную модель он сохраняет в общий каталог, и, как предполагается, этот обучающий скрипт перезапускается на локальной машине по мере необходимости. Скрипт Score\run.py выполняется ежедневно, он сортирует новые электронные письма по мере поступления.


    В этом разделе мы поговорим об операционализации процесса с помощью Azure Functions. Функции просты в использовании, они позволяют привязать скрипт к множеству различных триггеров (HTTP, очереди, BLOB-объекты хранилища, WebHooks и так далее), предоставляют несколько автоматических привязок вывода и при этом стоят недорого: выбрав план Consumption, ты платишь всего 0,000016 доллара за каждый используемый гигабайт ОЗУ в секунду. Но здесь есть ограничения: твоя функция не может выполняться дольше десяти минут и использовать более 1,5 Гбайт ОЗУ. Если тебя это не устраивает, ты всегда можешь перейти на специальный тарифный план на базе App Service, сохранив при этом доступ к другим преимуществам serverless-подхода. Однако для нашей простой логистической регрессии и пакетов из нескольких сотен писем выбранный план оказался оптимальным.


    С точки зрения программиста, функция — это папка, которая носит имя самой функции (в нашем случае это просто Score) и содержит два разных файла:


    • function.json — это файл, описывающий конфигурацию функции (формат см. здесь);
    • run.py — это Python-скрипт, который запускается при срабатывании триггера.

    Function.json можно создать вручную или сконфигурировать средствами портала Azure. Код, который мы получили в данном случае, представлен ниже. Первая привязка — inputcsv — запускает скрипт каждый раз, когда файл с именем, соответствующим шаблону mail-classify/input/{input_file_name}.csv, появляется в выбранном по умолчанию BLOB-хранилище Azure. Оставшиеся две привязки сохраняют выходные файлы после успешного выполнения функции. В данном случае мы сохраняем их в отдельную папку output, их имена соответствуют имени входного файла с суффиксами scored или unscorable. Таким образом, ты можешь поместить файл с любым именем-идентификатором, например GUID, в папку input, и два новых файла с именем, производным от GUID, через какое-то время появятся в папке output.


    {
      "bindings": [
        {
          "name": "inputcsv",
          "type": "blobTrigger",
          "path": "mail-classify/input/{input_file_name}.csv",
          "connection": "apmlstor",
          "direction": "in"
        },
        {
          "name": "scoredcsv",
          "type": "blob",
          "path": "mail-classify/output/{input_file_name}.scored.csv",
          "connection": "apmlstor",
          "direction": "out"
        },
        {
          "name": "unscorablecsv",
          "type": "blob",
          "path": "mail-classify/output/{input_file_name}.unscorable.csv",
          "connection": "apmlstor",
          "direction": "out"
        }
      ],
      "disabled": false
    }

    Скрипт run.py для функций Azure практически аналогичен нашей первоначальной «неоперационализованной» версии. Единственное изменение касается того, как функции пропускают через себя входящие и исходящие потоки данных. Независимо от выбранного типа входных и выходных данных (HTTP-запрос, сообщение в очереди, BLOB-файл...) содержимое будет храниться во временном файле, и путь к нему будет записан в переменную среды с именем соответствующей привязки. Например, в нашем случае при каждом выполнении функции создастся файл с именем "...\Binding[GUID]\inputcsv" и этот путь будет храниться в переменной среды inputcsv. Аналогичная операция выполнится для каждого исходящего файла. Учитывая эту логику, мы внесли несколько небольших изменений в скрипт.


    # read file
    input_path = os.environ['inputcsv']
    input_dir = os.path.dirname(input_path)
    input_name = os.path.basename(input_path)
    
    corpus = CsvCorpusReader(input_dir, [input_name],
                             encoding="utf8",
                             default_text_selector=lambda row: row["Text"])
    
    [...]
    
    # write unscorables
    unscorable_path = os.environ['unscorablecsv']
    ids_null.to_csv(unscorable_path, index=False) # pandas DataFrame
    
    [...]
    
    # write scored emails
    output_path = os.environ['scoredcsv']
    result.to_csv(output_path) # pandas DataFrame

    Это все изменения, необходимые для запуска службы при появлении файла CSV в BLOB-хранилище и получения в результате файлов, содержащих прогноз.


    Если честно, мы тестировали и другие триггеры, но обнаружили, что самая мощная функция Python — модули — становится ее проклятием в бессерверной системе. Модуль в Python — это не статическая библиотека, которую нужно подключить, как во многих других языках, а код, выполняемый при каждом запуске. Для таких долгосрочных решений, как службы, это почти незаметно, но с точки зрения функций Azure полное выполнение скрипта каждый раз влечет за собой довольно большие расходы. Это осложняет использование триггеров HTTP в Python, но batch-обработка на базе CSV-файлов, популярная во многих ML-сценариях, позволяет снизить эти расходы в расчете на строку данных до разумного минимума.


    Если ты не можешь обойтись без триггеров реального времени с Python, ты можешь попытаться перейти на выделенный тарифный план Azure App Service, поскольку это позволяет значительно увеличить вычислительные ресурсы хоста и ускорить импорт. В нашем случае простота реализации и низкая стоимость плана потребления перевесили преимущества быстрого выполнения.


    Прежде чем продолжить, давай посмотрим, как можно упростить разработку с помощью Visual Studio Code. На момент написания этой статьи интерфейс Functions CLI обеспечивал начальное формирование шаблонов Python, но функций отладки не было. Тем не менее среду выполнения не так сложно имитировать, используя встроенные функции VS Code. Нам поможет файл .vscode\launch.json, позволяющий настраивать параметры отладки. Как видно из JSON ниже, при запуске debug в конфигурации Score (Python) мы просим VS Code выполнить отладку скрипта ${workspaceRoot}/Score/run.py с рабочим каталогом ${workspaceRoot}/Score, кроме того, мы задаем переменные среды для трех файлов-макетов с привязками. Это полностью имитирует то, как эта функция будет выполняться с помощью Azure Functions (не забывай проверить текущий рабочий каталог при разработке скрипта). При наличии этих настроек ты можешь просто открыть раздел отладки Debug (Ctrl + Alt + D) в VS Code, выбрать Score (Python) в качестве конфигурации и нажать Start Debugging, чтобы построчно прогнать код в редакторе.


    [...]
    {
        "name": "Score (Python)",
        "type": "python",
        "request": "launch",
        "stopOnEntry": true,
        "pythonPath": "${config:python.pythonPath}",
        "console": "integratedTerminal",
        "program": "${workspaceRoot}/Score/run.py",
        "cwd": "${workspaceRoot}/Score",
        "env": {
            "inputcsv": "${workspaceRoot}/Score/debug/input.csv",
            "outputcsv": "${workspaceRoot}/Score/debug/output.csv",
            "unscorablecsv": "${workspaceRoot}/Score/debug/unscorable.csv"
        },
        "debugOptions": [
            "RedirectOutput",
            "WaitOnAbnormalExit"
        ]
    }
    [...]

    Если ты хочешь использовать расширение Jupyter для интерактивной поячеечной разработки и выполнения, тебе нужна аналогичная конфигурация в коде функции. Поэкспериментировав, мы остановились на варианте кода ниже. Он выполняется только в среде IPython, а при нормальном выполнении или отладке через Debug игнорируется.


    if "IPython" in sys.modules and 'Score' not in os.getcwd():
        os.environ['inputcsv'] = os.path.join('debug', 'input.csv')    
        os.environ['scoredcsv'] = os.path.join('debug', 'input.scores.csv')
        os.environ['unscorablecsv'] = os.path.join('debug', 'input.unscorable.csv')
        os.chdir('Score')

    Конфигурация среды


    Теперь, когда у нас есть готовая модель и код функции, пришло время настроить нужную инфраструктуру Azure. На момент написания этой статьи поддержка Python в функциях Azure все еще находилась на этапе предварительной версии, поэтому требовались дополнительные шаги по настройке. По умолчанию в среде выполнения установлен Python версии 2.7. Чтобы перейти на более популярную версию 3.6, в соответствии с официальной статьей в wiki тебе нужно получить любой доступный пакет Python (можно использовать подготовленную среду) и поместить его в папку D:\home\site\tools. Все работает достаточно просто. Эта папка предшествует папке с установленным по умолчанию Python 2.7 в переменной PATH в момент поиска python.exe для исполнения.


    Ты можешь сделать это вручную с помощью встроенного пользовательского интерфейса Kudu, как показано в статье, однако я выяснил, что предусмотренная для него специальная функция более удобна. Функция setup показывает, как мы это делали во время работы над проектом. Сначала функция проверяет, установлена ли версия 3.6, если нет, то она загружает предварительно сконфигурированный архив (.zip) с Python и извлекает его в папку D:\home\site\tools.


    tools_path = 'D:\\home\\site\\tools'
    if not sys.version.startswith('3.6'):
    
        # in python 2.7
        import urllib
        print('Installing Python Version 3.6.3')
    
        from zipfile import ZipFile
    
        if not os.path.exists(tools_path):
            os.makedirs(tools_path)
            print("Created [{}]".format(tools_path))
    
        python_url = 'https://apmlstor.blob.core.windows.net/wheels/python361x64.zip'
        python_file = os.path.join(tools_path, 'python.zip')
        urllib.urlretrieve(python_url, python_file)
        print("Downloaded Python 3.6.3")
    
        python_zip = ZipFile(python_file, 'r')
        python_zip.extractall(tools_path)
        python_zip.close()
        print("Extracted Python to [{}]".format(tools_path))
    
        print("Please rerun this function again to install required pip packages")
        sys.exit(0)

    Далее нужно установить требуемые пакеты pip. Pip имеет встроенный API для Python, поэтому работать с ним из Python так же просто, как в обычной командной строке. Из кода ниже видно, что используемые только в Python пакеты (langid, pymorphy) установлены, поэтому от нас не требуется никаких дополнительных действий. Проблема возникнет лишь с пакетами, созданными с помощью C++. В платформе App Service отсутствует компилятор Visual C++, поэтому остается только использовать предварительно скомпилированные пакеты (wheels). Некоторые из них уже присутствуют в репозитории pip (проверить можно здесь), для других специфичных ML-пакетов можно найти необходимый wheel здесь. В данном случае я использовал Azure Blob Storage, чтобы сделать эти пакеты доступными для функции Azure. Ты можешь повторно использовать эти ссылки или перевыложить их в любое публично доступное хранилище.


    def install_package(package_name):
        pip.main(['install', package_name])
    
    install_package('https://apmlstor.blob.core.windows.net/wheels/numpy-1.13.1%2Bmkl-cp36-cp36m-win_amd64.whl')
    install_package('https://apmlstor.blob.core.windows.net/wheels/pandas-0.20.3-cp36-cp36m-win_amd64.whl')
    install_package('https://apmlstor.blob.core.windows.net/wheels/scipy-0.19.1-cp36-cp36m-win_amd64.whl')
    install_package('https://apmlstor.blob.core.windows.net/wheels/scikit_learn-0.18.2-cp36-cp36m-win_amd64.whl')
    
    install_package('https://apmlstor.blob.core.windows.net/wheels/gensim-2.3.0-cp36-cp36m-win_amd64.whl')
    install_package('https://apmlstor.blob.core.windows.net/wheels/nltk-3.2.4-py2.py3-none-any.whl')
    install_package('langid')
    install_package('pymorphy2')

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


    import nltk;
    nltk_path = os.path.abspath(os.path.join('..', 'lib', 'nltk_data'))
    if not os.path.exists(nltk_path):
        os.makedirs(nltk_path)
        print("INFO: Created {0}".format(nltk_path))
    
    nltk.download('punkt', download_dir=os.path.join('..', 'lib', 'nltk_data'))
    nltk.download('stopwords', download_dir=os.path.join('..', 'lib', 'nltk_data'))

    Поскольку весь код в функции Setup идемпотентный, ты можешь без проблем добавить другие этапы настройки или установить дополнительные пакеты. Обрати внимание, что в первый раз эта функция должна запуститься дважды: сначала, чтобы перейти на Python 3.6, а затем, чтобы установить нужные пакеты.


    Заключение


    Несмотря на сложные настройки и ограничения, связанные с предварительным запуском, Azure Functions показали себя как довольно удобный и эффективный инструмент операционализации ML-моделей Python. Наш первоначальный проект был развернут в производственной среде, и модель ML помогла значительно улучшить результаты по сравнению с уже существующими подходами. Код для изучения и повторного использования доступен в репозитории на GitHub.


    Напоминаем, что это полная версия статьи из журнала Хакер.

    • +16
    • 12,7k
    • 1
    Microsoft 493,54
    Microsoft — мировой лидер в области ПО и ИТ-услуг
    Поделиться публикацией
    Комментарии 1
    • –2
      Машинлернинг, твою мать.

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

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