Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Всем привет!
Сегодня хотим поговорить с вами об участии в чемпионатах, хакатонах, соревнованиях. Меня зовут Максим Межов, и, я работая в отделе предиктивного анализа компании «Цифрум» (Госкорпорация «Росатом»), уже дважды участвовал в чемпионатах, построенных на концепции WorldSkills. Эти соревнования задают стандарты технической подготовки и квалификации специалистов. Кроме оригинального чемпионата, в ряде организаций проводятся ещё внутренние. Например, в Росатоме – это AtomSkills.
Зачем участвовать в чемпионатах?
В первую очередь, для самого себя. Не все чемпионаты подразумевают награду, могут даже просто грамоту выдать. Самое главное – это возможность перезагрузиться, заново взглянуть на привычные процессы, попробовать себя в новом и оценить свои силы. Стрессануть и получить заряд адреналина.
А ещё в соревнованиях такого типа участникам дают решать реальные задачи, настоящие проблемы, с которыми сталкиваются компании на производстве. Бывают также хакатоны, где участники работают с искусственными данными, проверяют подходы, а жюри смотрят, кто лучше сделает модель. Такой опыт тоже полезен, но его сложнее потом применить в жизни.
Как проходят чемпионаты
Марафоны с окончанием «-skills» построены по модели известного чемпионата WorldSkills. Так, DigitalSkills-2021 длился 3 дня. Каждый день мы, участники, садились спиной к спине за компьютеры и в течение определенного времени решали модуль за модулем. По сути, это как сдать 6 экзаменов за 3 дня. К каждому дню надо готовиться, работать 2 модуля по 3 часа. В середине модуля нам давали перерыв около 15 минут и один перерыв на обед между модулями. Мобильные телефоны, прочая собственная вычислительная техника запрещены. Только выданные компьютеры, у всех одинаковые.
Задание участников номинации «Машинное обучение» в 2021 году – создать приложение автоматизированной обработки обращений граждан. Механика такая: люди своими словами пишут обращение на сайте в специальной форме, затем эти сообщения нужно классифицировать по направлениям, по подразделениям администрации, чтобы они попадали в нужный отдел. То есть настроить автоматическую маршрутизацию.
Один модуль = одна задача. На весь чемпионат участникам ставится одно большое задание, которое нужно решить так, чтобы в конце получился законченный продукт. Например, нашей целью было создать web-приложение, которое автоматически обрабатывает обращения граждан, поступающие на сайт «Умного города».
Чтобы выполнить задачу, сначала нужно имеющиеся данные разобрать, понять, как с ними работать. Потом уже почистить, подготовить для обучения моделей машинного обучения. Применить их в готовом приложении. Есть модуль ознакомления с данными, модуль подготовки и моделирования, затем уже создание приложения, и, как финал, презентация.
За 15 минут до старта модуля выдаётся перечень задач, которые нужно решить в рамках одного модуля. Т.е. заранее не известно, что нужно будет делать, но, понимая специфику проекта, можно предугадать, какие задачи логично выполнить дальше для решения кейса в целом. В конце дня я рассуждал над возможными задачами, планировал, что обязательно нужно будет сделать, повторял теорию. В начале нового дня такая подготовка помогала мне сразу включаться в разработку, а не обдумывать что и как, читать справку и т.п. Время на выполнение модуля – ключевой ресурс, его нужно использовать максимально эффективно, чтобы успеть выполнить все задания.
Как оценивают участников
Даже если в конце у тебя появился рабочий итог, это ещё не значит, что победа в кармане. Сам чемпионат и критерии приближены к реальной работе. То есть не просто тебе надо решить задачу и всё. А нужно её пройти от самого начала: поисследовать, набросать разные варианты, сравнить, подумать, как бы они себя повели в жизни. Если ты не успел за три часа выполнить какие-то задачи из модуля, то они не зачтутся, даже если в следующем модуле ты вернёшься и сделаешь их.
Второй критерий – насколько код и работа понятны, есть ли комментарии. Кому-то это может показаться странным, так как задание на чемпионате выполняется в соло. Однако в реальной жизни мы постоянно сотрудничаем с коллегами, над продуктом работают целые команды, а с полуслова друг друга понимают далеко не все. Поэтому надо облегчать жизнь себе и другим – комментировать, объяснять, подсказывать.
Третий критерий – это обоснование. Например, метрик. Почему использовали конкретную метрику качества? Почему она подходит именно в данной задаче? Важны именно такие пояснения: «Вот этот подход, этот алгоритм, этот метод я использую, потому что в данной задаче есть такая особенность, и для такой особенности такая метрика лучше, чем вот такая. Она более адекватно нам дает понимание, как модель работает, с каким качеством решает поставленную задачу» и так далее.
И последний, но не по важности, критерий – презентация. Это ещё один навык, который можно здорово прокачать на соревновании.
Презентовать свой проект нужно так, как будто ты выступаешь перед инвесторами.
В целом, нужно показать, что у тебя получилось хорошее решение, рассказать о всех его плюсах, возможностях.
Какие навыки можно получить (и не только в номинации «Машинное обучение и большие данные»)
1) Конечно, навык разработки. Как правило, конкретных критериев на чем разрабатывать нет. Главное – иметь работающий прототип. Получается, почти что Fullstack. Нужно сделать минимально-жизнеспособный продукт, который покрывает весь требуемый функционал.
2) Web-программирование. Нужно сделать, чтобы приложение работало. Нужно сделать, чтобы оно работало с данными, с разработанной моделью, чтобы выдавало результаты, демонстрировало их пользователю, взаимодействовало с ним.
Никаких супертребований к дизайну приложения нет. Главное, чтобы оно было, работало и выполняло заявленные функции. Я использовал библиотеку для Python Streamlit, она за короткий срок помогает создать веб-приложение, при этом из «коробки» имеет вполне презентабельный, современный вид.
3) Навыки презентации, потому что решение нужно ещё защитить и «продать» жюри. На подготовку презентации, кстати, тоже выделяют целый модуль, 3 часа. Но, правда, ещё за это время нужно написать документацию к проекту: Руководство пользователя и Руководство по установке и запуску приложения.
Документацию не нужно оформлять в соответствии с ГОСТом, достаточно просто создать вордовский файлик с общими объяснениями, как что работает. Потому что кроме формальностей, проверяется ещё то, насколько хорошо участник излагает свои мысли и объясняет другим, что он сделал.
Как я решал задачу – мой кейс
постановка задачи: бизнес и МЛ
С точки зрения бизнес-постановки задачи, необходимо было автоматизировать маршрутизацию обращений и жалоб граждан, поступающих с сайта «умного города». С точки зрения постановки задачи в терминах машинного обучения, – решить задачу классификации текстов.
характеристика данных
Набор данных представлял собой коллекцию файлов в формате json в количестве 59889 штук. Каждый из них представлял собой файл следующей структуры:
{'id': 2947909,
'district_name': 'Приморский',
'watchers_count': 0,
'feed': [{'widget': 'public.status',
'key': 'changestatus_12267237',
'payload': {'id': 12267237,
'dt': '2020-11-13T01:30:54.728196',
'status_name': 'Завершено: Автоматически',
'status': 4},
'meta': {'title': 'Изменение статуса сообщения',
'bg_color': '#EEEEEE',
'icon': 'fa-thumbs-up fa-flip-horizontal',
'icon_bg_color': '#65C381'},
'content_type': 30},
{'widget': 'public.status',
'key': 'changestatus_12063847',
'payload': {'id': 12063847,
'dt': '2020-10-23T14:46:07.242630',
'status_name': 'Получен ответ',
'status': 4},
'meta': {'title': 'Изменение статуса сообщения',
'bg_color': '#EEEEEE',
'icon': 'fa-comments-o',
'icon_bg_color': '#AC89C7'},
'content_type': 30},
...
'comment': '<p>Если Вы обнаружили, что парадная находится в неудовлетворительном состоянии, сообщите об этом на Портал. Данная категория включает в себя сообщения о зданиях...</p>'},
'can_view': True,
'is_authenticated': False,
'last_user_answer_dt': None,
'show_unpublished_message': False}
Для решения задачи показались полезным взять следующие признаки:
- `['feed'][-1]['payload']['body']` (текст обращения гражданина)
- `['district_name']` (название округа), возможно пригодится для группировки или агрегации
- `['sidebar']['full_address']` (адрес, где возникла жалоба)
- `['reason']['category']['name']`, `['reason']['category']['id']` (целевая метка класса, который будем предсказывать)
предварительная обработка данных
import os
import pandas
from tqdm import tqdm
files_list = os.listdir(raw_datadir)
for f in tqdm(files_list):
try:
with open(f'{raw_datadir}/{f}', encoding = 'UTF-8') as fl:
raw = json.load(fl)
data_structure = {}
#parse text
for i, w in enumerate(raw['feed']):
if w['widget'] == 'public.petition':
body_text = raw['feed'][i]['payload']['body']
raw_text = " ".join([t['text'] for t in body_text])
preproc_text = preproc_text.strip()
break # если нашли нужный виджет, то смысла идти по циклу больше нет
data_structure['text'] = preproc_text
data_structure['district_name'] = raw['district_name']
data_structure['target'] = raw['reason']['category']['id']
data_structure['target_name'] = raw['reason']['category']['name']
df = df.append(data_structure, ignore_index=True)
except Exception:
print('Что-то пошло не так :(')
print('file name:', f)
continue # продолжаем цикл для обработки остальных файлов, а с этим будем разбираться отдельно, возможно он какой-то некорректный!?
По итогу парсинга данных, создаём pandas. DataFrame (далее – df) следующего вида (фрагмент):
text | district_name | target | target_name |
Систематическое блокирование парковки крупнога... | Выборгский | 2 | Благоустройство |
Ул. Маршала Захарова, д. 14/4.высокая точка за... | Красносельский | 2 | Благоустройство |
люк выдавило, гранитное мощение вокруг люка вы... | Центральный | 61 | Повреждения или неисправность элементов улично... |
Здравствуйте. Согласно кадастровой карте данны... | Пушкинский | 3 | Благоустройство |
text – текст обращения пользователя,
district_name – наименование округа,
target – идентификатор категории,
target_name – наименование категории.
По выбранным признакам пропусков в данных не было, однако встречались дубликаты. Их пришлось удалить. Также, как можно заметить, в колонке target имеется два различных значения для одной и той же категории. Соответственно, рассчитывать на корректность данного признака не приходится и лучше будет удалить данный столбец, а вместо него добавить новый, который будет содержать числовые значения после кодирования категориального признака target_name.
Итого, финальный датасет представлял собой набор данных, содержащий 50096 записей по четырём признакам.
Перед тем, как приступить к подготовке моделей, необходимо повнимательней посмотреть на имеющиеся данные. В первую очередь, необходимо проверить, какие уникальные значения принимают все признаки за исключением текста обращения. Текст требует отдельной обработки: очистки от стоп-слов, пунктуации, приведению всех слов к начальной форме.
df.district_name.value_counts()
Невский 6681
Выборгский 5033
Центральный 4251
Калининский 3722
Московский 3620
Кировский 3334
Приморский 3278
Красногвардейский 3028
Красносельский 2961
Фрунзенский 2768
Адмиралтейский 2695
Пушкинский 2278
Василеостровский 2226
Петроградский 1714
Колпинский 1006
Петродворцовый 929
Курортный 391
Кронштадтский 181
Name: district_name, dtype: int64
Как видим все наименования округов уникальны, опечаток нет.
df.target_name.value_counts()
Благоустройство 26853
Содержание МКД 14221
Нарушение правил пользования общим имуществом 1753
Фасад 1467
Незаконная информационная и (или) рекламная конструкция 1095
Повреждения или неисправность элементов уличной инфраструктуры 1031
Кровля 819
Водоснабжение 809
Санитарное состояние 433
Состояние рекламных или информационных конструкций 396
Центральное отопление 284
Подвалы 254
Нарушение порядка пользования общим имуществом 243
Водоотведение 238
Незаконная реализация товаров с торгового оборудования (прилавок, ящик, с земли) 200
Name: target_name, dtype: int64
Как видим, самая часто используемая категория обращения связана с благоустройством. Также видим, что в наборе данных имеется дисбаланс классов.
Теперь займёмся текстом.
План предварительной обработки текста:
– подготовить список стоп-слов, пунктуации, спецсимволов и цифр – всё это будем удалять;
– собрать текстовый корпус, в котором не будет стоп-слов, пунктуации, спецсимволов и цифр, а также все тексты привести к нижнему регистру;
– провести лемматизацию для исключения различных форм одного и того же слова;
– выделить токены – слова, встречающиеся в корпусе и приведённые к изначальной форме (лемматизированные слова).
import nltk
import matplotlib.pyplot as plt
import re
from pymystem3 import Mystem
from nltk.corpus import stopwords
from string import punctuation
nltk.download('stopwords')
stop_words_rus = set(stopwords.words('russian'))
stop_words_eng = set(stopwords.words('english')) # бывают также английские слова попадаются на практике, включим стоп-слова для чистоты целевого датасет
# punctuation == '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
numbers = [i for i in range(10)]
numbers = list(map(str, numbers))
# numbers == ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
mysteam = Mystem() # инициализируем для последующего использования при лемматизации
def preprocess_text(raw_text):
"""
Метод для предобработки исходного текста
"""
tokens = mysteam.lemmatize(raw_text.lower()) # лемматизация и приведение к нижнему регистру
"""
удаление стоп-слов и всяких ненужных символов
регулярочка помогает заменить токены типа "1й", которые встречаются в тексте на *, которые потом просто удаляем из списка
"""
text = [re.sub('[0-9][а-я]', '*', token) for token in tokens if (token.strip() not in stop_words_rus) & \
(token.strip() not in stop_words_eng) & \
(token.strip() not in punctuation) & \
(token.strip() not in numbers) & \
(token != ' ')]
if '*' in text:
text.remove('*')
return ' '.join(text)
text = preprocess_text(df.text[0])
text
'подъезд этаж коридор висеть какой-то провод возможно напряжение председатель тсж ленинградский вечер отказываться принимать заявка устранение полгода'
На одном примере работает корректно, соберём далее корпус из очищенных/предобработанных текстов для последующего перевода текста в числовой вид с помощью алгоритма векторизации.
corpus = []
for row in tqdm(df.iterrows()):
text = preprocess_text(row[1]['text'])
corpus.append(text)
Корпус готов, можем далее взять его в работу для векторизации и сборки датасета для дальнейшего использования в обучении моделей.
Все эти шаги: удаление стоп-слов, пунктуации, спецсимволов и цифр; удаление артефактных слов типа «1й», лемматизация слов в текстах нужно для того, чтобы из «сырого» текста выделить слова, которые действительно несут полезную информацию, т.е. избавиться от информационного шума в данных. Тексты, очищенные подобным образом, не вносят в словарь корпуса избыточного количества слов, не несущих информации об исследуемой теме, не помогают в решении поставленной задачи, а лишь мешают моделям более качественно обучаться.
Для векторизации текстов воспользуемся методом IF-IDF. TF показывает, насколько часто слово встречается в каждом конкретном документе, а IDF – насколько редко слово встречается во всей коллекции документов. При данной векторизации учитывается не только частотность слов в конкретном документе, но и частотность слов в рамках всего корпуса. Это даёт возможность отсечь часто встречающиеся слова во всех документах, которые не представляют пользы для разделения классов.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer()
X_tfidf = tfidf.fit_transform(corpus)
К получившейся матрице добавим столбец с целевым признаком, который хотим определять. Фрагмент полученной матрицы приведён ниже:
… | яд | яма | январь | ярковыраженный | ярмарка | ярослав | яхтенный | ящик | target |
… | 0.0 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | Благоустройство |
… | 0.0 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | Благоустройство |
… | 0.0 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | Содержание МКД |
… | 0.0 | 0.282886 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | Содержание МКД |
… | 0.0 | 0.000000 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | Благоустройство |
Данный фрагмент матрицы иллюстрирует тот факт, что TF-IDF матрица является разреженной.
Целевая переменная является категориальной, её необходимо закодировать числовыми значениями. Для этого воспользуемся методом LabelEncoder
из sklearn:
lbe = LabelEncoder()
target = lbe.fit_transform(df['target'])
df['target'] = target
О финальной модели, метрика
Для отбора наилучшей модели, построим несколько моделей, оценим на кросс-валидации метрику качества и выберем модель с наилучшим значением метрики. В качестве метрики качества будем использовать взвешенный вариант F1 меры – гармонического среднего между точностью и полнотой. Данная метрика позволит адекватно оценить качество работы модели при дисбалансе классов.
Model | F1 |
Extra Trees Classifier | 0.7930 |
Gradient Boosting Classifier | 0.7853 |
Ridge Classifier | 0.7850 |
Random Forest Classifier | 0.7793 |
Decision Tree Classifier | 0.7705 |
Light Gradient Boosting Machine | 0.7479 |
Logistic Regression | 0.7253 |
SVM - Linear Kernel | 0.6354 |
Naive Bayes | 0.6135 |
Ada Boost Classifier | 0.5580 |
K Neighbors Classifier | 0.5102 |
Linear Discriminant Analysis | 0.2592 |
Quadratic Discriminant Analysis | 0.1069 |
По итогу анализа различных моделей классификации по метрике `f1` лидирует `Extra Trees Classifier`
На втором и третьем месте: Gradient Boosting Classifier и Ridge Classifier соответственно.
Сделаем небольшой поиск лучших гиперпараметров с помощью поиска по сетке. Лучшим вариантом было бы, конечно, использовать optuna или ray.tune, но в силу ограниченности времени остановимся на более простом варианте.
params = {
'et': {
'n_estimators': [100, 500, 1000]
},
'gbc': {
'learning_rate': [0.1, 0.01]
},
'ridge': {
'alpha': [1.0, 5.0, 10.0]
}
}
models2 = {
'et': ExtraTreesClassifier(n_jobs=-1, random_state=random_state),
'gbc': GradientBoostingClassifier(),
'ridge': RidgeClassifier(random_state=random_state)
}
for name, model in models2.items():
gs = GridSearchCV(model, param_grid=params[name], scoring='f1_weighted', n_jobs=-1, cv=5)
gs.fit(X,y)
models2[name] = gs.best_estimator_
print(name, 'best score =', gs.best_score_)
Результат:
et best score = 0.8276726774886836
gbc best score = 0.8193002246126653
ridge best score = 0.8188670459658077
Как видим, даже при такой простой подборке параметров удалось немного улучшить качество метрики при той же стратегии кросс-валидации.
Сохраняем лучшую модель и векторизатор в сериализованном pickle-формате и в дальнейшем используем их в веб-приложении для классификации обращений пользователей сайта «умного города».
with open('tfidf_vectorizer.pkl', 'wb') as f:
pickle.dump(tfidf, f)
with open('prod_model.pkl', 'wb') as f:
pickle.dump(models2['et'], f)
Лайфхаки от победителя: учимся выигрывать
Если на DigitalSkills я был участником, то в нашем отраслевом чемпионате Росатома – AtomSkills – участвовал в качестве члена жюри. Так что имел представление о том, как оцениваются работы участников и на что нужно обращать особое внимание.
1. Обязательно оставляйте комментарии в коде. Даже если результат хороший, но член жюри не сможет сходу разобраться в коде, это отнимет много баллов. Потому что в реальной жизни код мы пишем не для самих себя, но и для коллег.
2. Оценивается не только итог, но и каждый модуль по отдельности. Причём проверяют их с запаздыванием, так что ты никогда не знаешь, где именно совершил ошибку и в принципе, совершил ли. Поэтому нужно самостоятельно думать наперёд какие задачи могут быть впереди и повторять справочный материал заранее. Это поможет экономить время, даст возможность сфокусироваться на решении задачи, а не на поиске информации. Что в свою очередь снизит вероятность сделать ошибки в спешке и даже даст возможность проверить код и добавить дополнительных комментариев.
3. Настройтесь на марафон и заранее подготовьтесь к стрессовой ситуации. На DigitalSkills участники сидели в одном помещении, часто приходили группы посетителей мероприятия, от напряжения воздух буквально искрит. Я с собой взял наушники и слушал плейлист в стиле Chillout. Сосед рядом слушал DrumStep, наверное, мощные биты резонировали с его биением сердца и помогали не расслабляться. Но тут каждому своё.
4. Высыпайтесь. После окончания каждого дня чемпионата лучше немного развеяться и пораньше лечь спать, чтобы дать мозгу полноценно отдохнуть. А перед сном можно порассуждать, какие задачи могут быть завтра. Как правило, они идут логично. И можно заранее прикинуть, на какие подзадачи разделить следующий модуль и как их решить. А когда появилось понимание, можно почитать теорию. Причём лучше читать краткие справки по конкретным задачам, проблемам, а не лезть в дебри. На чемпионате можно гуглить, а умение правильно задавать вопросы – одно из самых полезных. Я обычно просматривал на каком ресурсе и его разделе можно найти нужную информацию, и на соревновании уже гуглил информацию на конкретных сайтах.
5. Делайте зарядку утром. Она помогает улучшить кровообращение, и вам потребуется меньше времени на раскачку.
6. Читайте научные статьи. Это касается не только подготовки к соревнованиям, но и в целом для повышения профессионализма. Полезно знакомиться с чужими практиками, брать себе что-то на заметку. В научных статьях содержится концентрат знаний по теме. Авторы проводят не один день над формулировками и структурированием материала, чтобы было понятно и можно было воспроизвести полученные в исследовании результаты.
Каким специалистам нужно участвовать в чемпионатах
Всем, кто чувствует необходимость перезагрузки, хочет прокачать навыки или просто проверить свои силы. Чемпионаты делятся по уровню сложности, иногда проходят чемпионаты для юниоров. Номинации известны заранее, но какая именно задача будет, держится в тайне. Она объявляется за 15 минут до старта первого модуля. На AtomSkills я ездил со своим коллегой-участником, специалистом по компьютерному зрению. А задачу дали в области предиктивного анализа с данными акустических датчиков. Нужно было выявить аномальные состояния трубопровода по акустическим и вибрационным сигналам, которые требуют особого подхода. Коллега был в шоке. Но смог справиться в итоге, и на проработке задачи уже смело предлагал несколько вариантов решения, старался комментировать и аргументировать свои решения. Эта задача была сложной, потому что необходимо было понять, как вообще работать с такими сигналами, как преобразовать их в адекватный набор данных, по которому в дальнейшем можно обучать модель.
На DigitalSkills я стрессанул примерно так же. Объявили, что задача будет связана с NLP. А моя специализация – предиктивный анализ, где чаще всего временные ряды. Но задача оказалась довольно творческая, плюс, нечто похожее я делал в рамках одного из своих рабочих проектов, так что имеющийся опыт мне очень помог.
Описание самой задачи выше. Каким бы способом её решали вы? Поделитесь с нами в комментариях!
Так что на самом деле, чемпионаты созданы для IT-специалистов любой сферы. Это возможность перезагрузиться, расширить горизонты. А ещё такие чемпионаты иногда меняют подход к работе. Я выяснил, что для меня сейчас оптимальный режим работы – режим хакатона. 3 часа – оптимальное время для фокусировки на задании, не успеваешь устать. Задачу дробишь на мелкие и стараешься каждый этап за эти 3 часа завершить.
Участвуйте в соревнованиях, чемпионатах и хакатонах. Это развивает, даёт правильный взгляд в будущее, способствует углублению своих знаний, умений и навыков, а также помогает почувствовать уверенность в своих силах!