Вступление

Одно из моих профессиональных устремлений в течение некоторого времени на самом деле было грязным и грязным, чтобы написать немного кода ML и, наконец, получить достойное представление о DNN (глубоких нейронных сетях) и Tensorflow.. несколько курсов по Coursera и Udemy, но ничто не сравнится с работой над реальным проектом. Попутно я работал над проектом, который назвал Octopus (должно быть, это были все шарики Tako-pachi или японского осьминога, которые я ел). Octopus - это проект по классификации заявок об ошибках Jira для инженерных групп, использующих традиционный ML, и я хотел посмотреть, можно ли применить DNN к аналогичному проекту: присвоение приоритета заявкам об ошибках Jira на основе описания.

У меня появился шанс сделать это, когда я записался на онлайн-курс AICamp Deep Learning for Developers - интенсивный 4-недельный курс, который познакомит вас с основами DNN, но в очень практичном виде. манера. В рамках курса я приступил к работе над проектом Capstone, и вуаля, у меня появился шанс по-настоящему покопаться!

Было всего 2 проблемы:

  1. Поскольку в этом проекте будет контролируемое обучение, мне понадобятся данные для обучения с пометкой. Я не мог использовать данные Octopus, поскольку они внутренние. Также набор данных был слишком мал (сотни против десятков тысяч, необходимых для обучения DNN).
  2. Все примеры в курсе AICamp были сосредоточены в основном на классификации изображений, например. MNIST, CIFAR. Ничего о классификации текста.

Я также хотел получить практический опыт по следующим вопросам:

  1. Обучите модель с помощью графических процессоров, чтобы почувствовать ускорение обучения.
  2. Упакуйте модель в какую-нибудь развертываемую форму (возможно, используя pickle).
  3. Упакуйте предсказатель как HTTP POST API с помощью Apache Flask.
  4. Создайте Docker-образ пакета предиктора.
  5. Создайте конвейер CI для сборки пакета и развертывания в реестре Docker.

Я смог выполнить это, и вы можете найти Jupyter Notebooks, data, Dockerfile, Gitlab YAML здесь:

Https://gitlab.com/foohm71/octopus2

Поскольку одна из целей, которую я поставил перед собой, заключалась в обучении модели с использованием графических процессоров, использование Google Colab стало естественным (Colab - это инструмент Google для ноутбуков, эквивалентный Jupyter). Кроме того, в Google Colab встроен Tensorflow 2 и есть несколько отличных улучшений для Jupyter Notebook, которые упрощают работу с Python и наукой о данных. Большинство записных книжек в репо предназначены для работы в Google Colab.

Данные

Оказывается, кто-то ранее открывал исходные данные заявок Jira из нескольких проектов с открытым исходным кодом и импортировал их в базу данных PostgresSQL. Вы можете найти его здесь: [1]. Команда, которая работала над извлечением этого набора данных, также написала об этом статью [2].

Следующим шагом была установка PostgresSQL и выполнение импорта данных.

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

Я выбрал для этого сообщения об ошибках проекта Zookeeper:

select * from jira_issue_report 
   where status = ‘Closed’ 
      and type = ‘Bug’ 
      and project = ‘ZOOKEEPER’ 
      not (description is null or description = ‘’) 
      and priority is not null

Вы можете найти CSV-файл в репозитории с именем: JIRA_OPEN_DATA_ZOOKEEPER.csv. У этого было ~ 400 строк.

Я также извлек ВСЕ билеты в CSV: JIRA_OPEN_DATA_ALL.csv. У этого было около 200 тысяч строк.

Однако вскоре я понял, что этот набор данных слишком велик для обработки в Google Colab (что было жаль), и поэтому я создал подмножество примерно из 40 тысяч строк: JIRA_OPEN_DATA_LARGESET.csv с помощью SQL-запроса:

select * from jira_issue_report 
  where status = ‘Closed’ 
    and type = ‘Bug’ 
    and (project = ‘FLEX’ 
    or project = ‘JBIDE’ 
    or project = ‘RF’ 
    or project = ‘SPR’ 
    or project = ‘HBASE’) 
    and not (description is null or description = ‘’) 
    and priority is not null

Классификация текста с использованием традиционного машинного обучения

Примечание. Я лишь кратко рассмотрю этот раздел, так как он не является «сутью» данной статьи. Хорошее введение в тему см. В [4].

В ExploratoryDataAnalysis.ipynb был проведен очень простой исследовательский анализ данных. Также для определения основы классификации использовался очень простой классификатор наивный байесовский объект. Это следовало примеру, найденному в [3].

Несколько замечаний:

  1. Точность была ~ 49% на наивном байесовском классификаторе текстовых блобов.
  2. Цели классификации очень предвзяты. Вместо того, чтобы балансировать, мы хотели посмотреть, насколько это будет плохо / хорошо.
  3. Чтобы получить TF-IDF для использования в качестве функций для традиционной классификации машинного обучения, я провел анализ слов, чтобы извлечь избыточные данные, например. Идентификаторы, адреса электронной почты, URL-адреса. Также была проведена токенизация в слова и биграммы.

Все это было сделано с использованием библиотек Numpy, Pandas, Sci-kit Learn и Matplotlib.

Руководство о том, как это сделать, можно найти в [4] и [5].

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

Все это можно найти в ModelAnalysis.ipynb репозитория. В результате точность составила всего ~ 33%. (В то время я не пытался использовать XGBoost. Возможно, позже!)

Классификация текста с использованием DNN и Tensorflow

Если вы посмотрите на литературу, применение DNN к классификации текста обычно можно разделить на следующие подходы:

  1. Использование рекуррентной нейронной сети (RNN), в частности LSTM (долгосрочная, краткосрочная память).
  2. Использование сверточной нейронной сети (CNN).
  3. Использование встраивания слов, например word2vec, GloVe.
  4. Использование предварительно обученной модели в качестве базовой модели и замораживания / размораживания слоев, например. БЕРТ.

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

  1. Использование слоя внедрения для сопоставления каждого слова с векторным пространством
  2. Использование 1D слоев свертки вместо 2D

Остальная часть сети представляет собой довольно стандартный CNN для классификации изображений.

Чтобы получить хорошее представление о том, как CNN могут применяться к классификации текста, я рекомендую либо посмотреть видео Лукаса Бевальда на YouTube «Классификация текста с использованием сверточных нейронных сетей (2019)» - см. [7], либо пройти курс ленивого программиста по Udemy » Tensorflow 2.0: глубокое обучение и искусственный интеллект »[8]. Есть раздел о применении LSTM и CNN для двоичной классификации текста.

Ключевая идея, лежащая в основе слоя «Встраивание», заключается в следующем: если мы сопоставим каждое слово с N-мерным векторным пространством, то «расстояние» между словами (то есть, как каждое слово соотносится друг с другом) можно «узнать», настроив расположение каждого слова в векторном пространстве.

Что касается слоев 1D Convolution - идея та же, что и слоев 2D Convolution, но вместо работы с n x n ядром мы работаем с 1 x n ядрами, которые являются числовыми представлениями слов. Ключевая интуиция, лежащая в основе этого, заключается в том, что слова имеют отношения со словами рядом с ними (та же идея, что и использование н-граммов в традиционном НЛП).

Подготовка данных

Подобно традиционному ML, где мы конвертируем необработанный текст в матрицу TF-IDF, здесь мы должны сделать нечто подобное. В основном создайте серию последовательностей слов и дополните ее нулями в конце.

# Convert sentences to sequences
MAX_VOCAB_SIZE = 20000
tokenizer = Tokenizer(num_words=MAX_VOCAB_SIZE)
tokenizer.fit_on_texts(df_train)
sequences_train = tokenizer.texts_to_sequences(df_train)
sequences_test = tokenizer.texts_to_sequences(df_test)
# get word -> integer mapping
word2idx = tokenizer.word_index
V = len(word2idx)
# pad sequences so that we get a N x T matrix
data_train = pad_sequences(sequences_train)
# get sequence length
T = data_train.shape[1]
data_test = pad_sequences(sequences_test, maxlen=T)

Исследование / настройка модели

В DNNModelAnalysis.ipynb вы найдете исследование модели:

  1. Я начал с базовой модели CNN, предложенной для бинарной классификации, и модифицировал ее для обработки классификации по нескольким категориям.
  2. Постепенно добавляйте больше сверточных и плотных слоев, чтобы модель лучше училась.
  3. Добавление пакетной нормализации и исключения для уменьшения чрезмерной подгонки.
  4. Наконец добавляем раннюю остановку.

В финальной модели было следующее:

  • Встраиваемый слой
  • 6 одномерных сверточных слоев с размером ядра 3
  • Слой MaxPooling
  • 4 одномерных сверточных слоя с размером ядра 3
  • Слой MaxPooling
  • 2 сверточных слоя ID с размером ядра 3
  • Глобальный максимальный пул
  • 4 плотных слоя с выпадением
  • Все слои с пакетной нормализацией
  • Ранняя остановка

Код:

D = 20
early_stopper = EarlyStopping(monitor=’val_loss’, patience=5, restore_best_weights=True)
model = Sequential()
model.add(Input(shape=(T,)))
model.add(Embedding(V + 1, D))
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(32, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(MaxPooling1D(3))
model.add(Conv1D(64, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(64, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(64, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(64, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(MaxPooling1D(3))
model.add(Conv1D(128, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(Conv1D(128, 3, activation=’relu’))
model.add(BatchNormalization())
model.add(GlobalMaxPooling1D())
model.add(Dense(units=800, activation=’relu’))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(units=400, activation=’relu’))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(units=200, activation=’relu’))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(units=100, activation=’relu’))
model.add(BatchNormalization())
model.add(Dropout(0.2))
model.add(Dense(units=num_classes, activation=’softmax’))
model.compile(optimizer=’adam’, loss=’categorical_crossentropy’, metrics=[‘accuracy’])
history = model.fit(data_train1, training_labels1, epochs=40, batch_size=8, verbose=True, validation_split=.2, callbacks=[early_stopper])
plot_training_history(history, model, data_test1, test_labels1)

Достигнутая точность составляет ~ 54%, хотя это в некотором роде сравнение яблок с апельсинами, если сравнивать его с традиционными моделями машинного обучения, но набор данных LARGESET был бы слишком большим для моделей машинного обучения, а набор данных ZOOKEEPER был бы слишком мал для DNN.

Построение развертываемой модели

Для этого есть 3 компонента:

  1. Обучение и сохранение модели
  2. Создание простой оболочки HTTP POST API
  3. Создание простого конвейера CI

Примечание. Существует несколько способов создания развертываемой модели для развертывания облака: у Google Cloud (GCP) есть несколько способов сделать это, которые хорошо документированы, как и Amazon (AWS), например. Sagemaker и Microsoft Azure.

Обучение и сохранение модели

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

Модель такая же, как окончательная модель, описанная в последнем разделе. Единственная разница в том, что на этот раз он обучается со всем набором данных LARGESET (без разделения на поезд / тест).

После обучения он сохраняется:

model.save(“/content/gdrive/My Drive/Colab Notebooks/Octopus2/jira_open_data_classifier.h5”, save_format=’tf’)

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

test_sentance = [‘The hard coded host of the client can only let it run on the same host as the thrift server.’]
test_seq = tokenizer.texts_to_sequences(test_sentance)
test_padded = pad_sequences(test_seq, maxlen=T)
p = model.predict_classes(test_padded)

Создание простой оболочки HTTP POST API

По сути, это app.py в репо.

Это просто приложение Apache Flask, которое:

  1. Загружает модель из jira_open_data_classifier.h5
  2. Извлекает «заголовок» и «описание» из полезной нагрузки JSON POST.
  3. Выполняет прогноз и выводит прогноз. Этот код похож на тестовый код в предыдущем разделе.

Для запуска app.py, просто «python3 app.py»

test.sh содержит команду curl для выполнения POST:

curl -X POST -H “Content-Type: application/json” \
  -d ‘{“title”: “TestSizeBasedThrottler fails occasionally”, “description”: “Every now and then TestSizeBasedThrottler fails reaching the test internal timeouts.I think the timeouts (200ms) are too short for the Jenkins machines.On my (reasonably fast) machine I get timeout reliably when I set the timeout to 50ms, and occasionally at 100ms.”}’ \
  “http://localhost:5000/api_predict"

Создание простого конвейера CI

Одна приятная вещь, которую я обнаружил в Gitlab, заключается в том, что они предоставляют шаблоны для .gitlab-ci.yml (для CI) и Dockerfile. Чтобы создать конвейер CI, вам просто нужно на главной странице проекта нажать кнопку «Настроить CI / CD», и будет создан базовый бланк .gitlab-ci.yml и появится раскрывающийся список с надписью «Применить шаблон», который позволит вам выберите из множества типов построения конвейеров, например. Android, C ++ и т. Д. Для этого я выбрал «Docker», так как хотел создать контейнер Docker.

Примечание: если вы никогда не создавали контейнер Docker и не публиковали контейнер Hello World в реестре Docker, я предлагаю перейти на docker.com и следовать руководству Quickstart, чтобы создать простой контейнер Hello World и опубликовать его. в реестре Docker.

Вернувшись к .gitlab-ci.yml, вы заметите следующие переменные среды:

  • $CI_REGISTRY_USER - это идентификатор пользователя, который вы используете для входа в реестр (в данном случае docker.com)
  • $CI_REGISTRY_PASSWORD - это пароль, который вы используете для входа в реестр (в данном случае docker.com)
  • $CI_REGISTRY - это можно удалить, поскольку мы используем реестр Docker по умолчанию. Он понадобится, если у вас есть отдельный реестр Docker, например. AWS

Эти переменные среды необходимо настроить в разделе «Настройки -› CI / CD - ›Переменные». Оказавшись там, вы можете добавить эти переменные как пары ключ-значение.

Если вы все настроили правильно (фактически, когда в репозитории есть .gitlab-ci.yml), Gitlab запустит сборку. Перейдите в «CI / CD -› Pipelines », вы должны увидеть, что некоторые из них работают.

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

К сожалению, конвейер, скорее всего, выйдет из строя, поскольку файл Dockerfile еще не создан. Для этого перейдите в «Обзор проекта» и нажмите кнопку «Новый файл» (если вы не видите кнопку «Новый файл», вы также можете нажать кнопку «Журнал изменений»).

Затем вам будет разрешено выбрать шаблон. В качестве основы я выбрал Python.

Созданный мной Dockerfile был:

FROM python:3.6

WORKDIR /usr/src/app

COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r requirements.txt

COPY . /usr/src/app

# Inform Docker that the container is listening on the specified port at runtime.
EXPOSE 5000

# Run command
CMD ["python", "app.py"]

Как только это будет сделано, сборка должна завершиться успешно, т. Е. образ докера создается и публикуется в реестре.

использованная литература

[1] Социальный репозиторий Jira, Марко Орту и др. Https://github.com/marcoortu/jira-social-repository

[2] Набор данных репозитория JIRA, Марко Орту и др., Сентябрь 2015 г. https://www.researchgate.net/publication/301370380_The_JIRA_Repository_Dataset

[3] Учебное пособие: создание системы классификации текста, TextBlob https://textblob.readthedocs.io/en/dev/classifiers.html

[4] Текстовая аналитика для начинающих с использованием NLTK, Авинаш Навлани, декабрь 2019 г. https://www.datacamp.com/community/tutorials/text-analytics-beginners-nltk

[5] Понимание классификаторов случайных лесов в Python, Авинаш Навлани, май 2019 г. https://www.datacamp.com/community/tutorials/random-forests-classifier-python

[6] Настройка гиперпараметров случайного леса в Python, Уилл Кёрсен, январь 2018 г. https://towardsdatascience.com/hyperparameter-tuning-the-random-forest-in-python-using-scikit-learn-28d2aa77dd74

[7] Классификация текстов с использованием сверточных нейронных сетей (2019), Лукас Бевальд https://youtu.be/8YsZXTpFRO0

[8] Tensorflow 2.0: глубокое обучение и искусственный интеллект, ленивый программист https://www.udemy.com/course/deep-learning-tensorflow-2/