Привет! Меня зовут Марат, и я буду твоим ревьюером. Спешу сообщить что все ключевые этапы в работе выполнены, с задачей тебе удалось справиться. По поводу обращения - в IT сфере принято общаться на «ты» :) Но, если привычней на «вы», дай знать. Как ревьюера моя задача помочь тебе в развитии, дав хорошие советы. Я внимательно посмотрю твой код, ознакомлюсь с твоими выводами и оставлю комментарии. Где то могу предложить небольшие исправление в коде, но ненавязчиво. Где потребуются уточнения, я оставлю много наводящих вопросов. Они помогут тебя с поиском верного решения.
Все мои комментарии размечены по цветам, для лучшего восприятия.
Залог успеха - работа сообща, взаимное уважение и работа в диалоге. Поэтому, помечай свои ответные комментарии на мои реплики заметным цветом или курсивом, так мне будет легче их отслеживать. Пожалуйста, не изменяй и не удаляй мои комментарии. Все это поможет выполнить повторную проверку быстрей.
Пишу это после обработки всех отработанных коментариев.
Большое спасибо за помощь (без сарказмов). От тебя узнал больше чем за 2 недели на платформе. Ты мне дал четкое понимание что знания поверхносты и есть куда идти. Исходя из этого я практически не исправлял желтые коменты, не хочу на скорую руку натворить костылей. Все что ты мне дал- небыло в обучении. А объем информации не хилый, бездумно копи-паст устраивать не хочу. Из уважения к нам обоим)
Сейчас моя задача закрыть проект, время поджало.(на платформе с моделями работать анриал, а дома времени очень
мало). Но опять же большое спасибо, я знаю что мне делать дальше относительно Обучения в текстах.
Успех 👍:
Привет! На самом деле у тебя очень не много ошибок. Если чуть-чуть докрутить pipeline, и хотя бы в самом простом виде воспользоваться Bert, то вообще было бы отлично. Но идею твоего поста я понял)
Проект для «Викишоп»¶
Интернет-магазин «Викишоп» запускает новый сервис. Теперь пользователи могут редактировать и дополнять описания товаров, как в вики-сообществах. То есть клиенты предлагают свои правки и комментируют изменения других. Магазину нужен инструмент, который будет искать токсичные комментарии и отправлять их на модерацию.
Обучите модель классифицировать комментарии на позитивные и негативные. В вашем распоряжении набор данных с разметкой о токсичности правок.
Постройте модель со значением метрики качества F1 не меньше 0.75.
Инструкция по выполнению проекта
- Загрузите и подготовьте данные.
- Обучите разные модели.
- Сделайте выводы.
Для выполнения проекта применять BERT необязательно, но вы можете попробовать.
Описание данных
Данные находятся в файле toxic_comments.csv. Столбец text в нём содержит текст комментария, а toxic — целевой признак.
Подготовка¶
#pip install pymystem3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import nltk
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.dummy import DummyClassifier
from pymystem3 import Mystem
from nltk.corpus import stopwords as nltk_stopwords
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import f1_score
from tqdm import tqdm_notebook
Совет:
у тебя тут есть лишние импорты, то что ты не использовано или использовано не верно - стоит убрать, чтобы поберечь ресурсы
есть рекомендации PEP-8 при написании кода, в том числе и для импортов. Если интересно можешь почитать тут. Есть что поправить
Абсолютно согласен, подчистил
Совет 🤔:
Вот Это лишнее
from pymystem3 import Mystem
Согласна pep8 - пишем не
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import train_test_split
А в одну строку, через запятую
from sklearn.model_selection import GridSearchCV, train_test_split
Подгрузим сразу stopwords
nltk.download('stopwords')
stopwords = set(nltk_stopwords.words('english'))
[nltk_data] Downloading package stopwords to /home/jovyan/nltk_data... [nltk_data] Package stopwords is already up-to-date!
создаем конструкцию для работы на сервисе и локально
try:
df = pd.read_csv(r'C:/Users/norfolk/Documents/python/datasets/toxic_comments.csv', index_col=0)
except:
df = pd.read_csv(r'/datasets/toxic_comments.csv', index_col=0)
display(df.head())
print(f'Размер датасета: {df.shape} \n')
print(f'Наличие пропусков: \n {df.isna().sum()} \n')
df.info()
| text | toxic | |
|---|---|---|
| 0 | Explanation\nWhy the edits made under my usern... | 0 |
| 1 | D'aww! He matches this background colour I'm s... | 0 |
| 2 | Hey man, I'm really not trying to edit war. It... | 0 |
| 3 | "\nMore\nI can't make any real suggestions on ... | 0 |
| 4 | You, sir, are my hero. Any chance you remember... | 0 |
Размер датасета: (159292, 2) Наличие пропусков: text 0 toxic 0 dtype: int64 <class 'pandas.core.frame.DataFrame'> Int64Index: 159292 entries, 0 to 159450 Data columns (total 2 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 text 159292 non-null object 1 toxic 159292 non-null int64 dtypes: int64(1), object(1) memory usage: 3.6+ MB
Совет:
Если не знаешь - чтобы не было столбца Unnamed: 0 при чтении файла можно так:
pd.read_csv(..., index_col=0)
(Unnamed: 0 появляется при не совсем корректном сохранении файла)
Не придал особого значения, но исправляюсь)
Успех 👍:
Это мелочь на самом деле. Просто чтобы ты знал ты не удивлялся если увидешь в чужом коде такое
Произведем "очистку" текста
def cleaning(text):
text = re.sub(r"(?:\n|\r)", " ", text)
text = re.sub(r"[^a-zA-Z ]+", "", text).strip()
text = text.lower()
return text
df['text'] = df['text'].apply(cleaning)
df.head()
| text | toxic | |
|---|---|---|
| 0 | explanation why the edits made under my userna... | 0 |
| 1 | daww he matches this background colour im seem... | 0 |
| 2 | hey man im really not trying to edit war its j... | 0 |
| 3 | more i cant make any real suggestions on impro... | 0 |
| 4 | you sir are my hero any chance you remember wh... | 0 |
Приведем наш текст к леммам
df.reset_index(inplace=True,drop = True)
%%time
import spacy
nlp = spacy.load('en_core_web_sm', disable=['parser', 'ner'])
def lemmatize(text):
doc = nlp(text)
lems = " ".join([token.lemma_ for token in doc])
return lems
for i in tqdm_notebook(range(len(df['text'])),
desc="Processing records"):
df.loc[i,'text'] = lemmatize(df.loc[i,'text'])
# corpus = df['text'].values.astype('U')
# def lemmatize(text):
# m = Mystem()
# lems = m.lemmatize(text)
# lems = "".join(lems)
# return lems
# corpus[0] = lemmatize(corpus[0])
# corpus
<timed exec>:9: TqdmDeprecationWarning: This function will be removed in tqdm==5.0.0 Please use `tqdm.notebook.tqdm` instead of `tqdm.tqdm_notebook`
Processing records: 0%| | 0/159292 [00:00<?, ?it/s]
CPU times: user 55min 55s, sys: 22 s, total: 56min 17s Wall time: 57min 51s
df
| text | toxic | |
|---|---|---|
| 0 | explanation why the edit make under my usernam... | 0 |
| 1 | daww he match this background colour I m seemi... | 0 |
| 2 | hey man I m really not try to edit war its jus... | 0 |
| 3 | more I can not make any real suggestion on imp... | 0 |
| 4 | you sir be my hero any chance you remember wha... | 0 |
| ... | ... | ... |
| 159287 | and for the second time of ask when your view ... | 0 |
| 159288 | you should be ashamed of yourself that be a... | 0 |
| 159289 | spitzer umm there s no actual article for p... | 0 |
| 159290 | and it look like it be actually you who put on... | 0 |
| 159291 | and I really do not think you understand I... | 0 |
159292 rows × 2 columns
Ошибка:
Да, в тренажере был текст на кирилице, там перевод в unicode оправдан (.values.astype('U')). В нашем случае (латиница) это лишь увеличит количество потребляемой памяти и это в лучшем случаи, в худшем он обрушает ядро.
Для нашего случая pymystem бесполезен, он только для текстов на кириллице. Рекомендую WordNetLemmatizer (но не забыть о POS тег, используй функцию
get_wordnet_posиз примера и обрати внимание на то что WordNetLemmatizer на лемматизацию принимает не предложение, а слово) или SpaCy - это ещё лучше, она медленнее, но дружелюбнее, не нужна предварительная токенизация и учет постегов. Почитать как все устроено можно тутСвои функции очистки и лематизации к тексту ты не применил (рекоменду использовать apply, неэффективные циклы нам ни к чему, а еще лучше попробуй .progress_apply, делает что .apply, но еще и показывает на какой итерации находится)
Понял о чем идет речь. Переходим на лучшее "SpaCy". Выше в ячейке что было отправил в коментарии, а новое написал.
Прикрутил цикл перебора. С apply не получается (все время ругается).
Буду признателен за подсказку.
Совет 🤔:
Такое всегда работало:
nlp = spacy.load('en_core_web_sm')
def lemma_clear(text):
lemm = nlp(text)
lemm = " ".join([token.lemma_ for token in lemm])
return " ".join(lemm.split())
corpus = features.apply(lemma_clear)
разницы с тем что у тебя я не вижу
Текст готов для дальнейшей работы. Осталось взглянуть на целевой признак.
plt.title('Баланс классов')
plt.xlabel('Значение')
plt.ylabel('Количество')
df['toxic'].value_counts().plot.bar(figsize=(4,3), grid = True);
Промежуточный вывод:
1) Мы подготовили текст для дальнейшей работы сним. А именно следующий шаг это векторизация.
2) Имеем дисбаланс классов в целевом признаке. Попробуем решить данную задачу используя стандартную возможность
моделей class_weight = 'balanced'
3) Каких либо проблем в данных не выявленно
Успех:
Данные изучены. Небольшой EDA не помешает, так как это аналитический проект.
Плюс за
проверку на сбалансированность
Промежуточный вывод в конце раздела
Совет:
.sample() вместо .head(), ведь если данные каким то образом упорядоченны, то шансы увидеть что то разнообразное через .sample чуть выше чем через .head (или .tail)
после очистки и лемитизации можно провести частотный анализ текста/облако слов - чтобы получить общее представление о тематике и о наиболее часто встерчаемых словах Кроме того графики, рисунки делают проект визуально интересней
Обучение¶
Разделим данные на обучающую и тестовую¶
rs = 12345
target=df['toxic'].values
features = df['text']
(features_train,
features_test,
target_train,
target_test) = train_test_split(features, target, test_size = .2, random_state = rs)
print('Обучающая выборка {:.1%} \n'.format(features_train.shape[0]/features.shape[0]))
print('Тестовая выборка {:.1%}'.format(features_test.shape[0]/features.shape[0]))
Обучающая выборка 80.0% Тестовая выборка 20.0%
Успех:
random_state на месте
правильно разбил на 2 выборки (иногда студенты использующие GS разбивают на 3 датасета)
Совет:
- обрати внимание на аргумент stratify, он позволит сохранить изначальное распределение таргетов во всех новых датасетах. Существующий дисбаланс никуда не денется, но в каждом датасете он будет одинаковым. Почитать можно тут
Векторизация TF-IDF¶
count_tf_idf = TfidfVectorizer(stop_words=stopwords)
tfidf_train = count_tf_idf.fit_transform(features_train)
tfidf_test = count_tf_idf.transform(features_test)
Успех:
Не забыли о стопсловах, они ни к чему и код побежит быстрей
Совет:
Вопросик:
А стопслова важней убирать когда мы используем TF-IDF, или когда используе обычный CountVectorizer?
Совет:
- .fit_transform на train датасете, .transform на test/valid. Вроде все верно, но после ты подаешь tf_idf_train в GridSearchCV или cross_val_score и он внутри себя разбивая его на тренировочный и валидационный датасет, получается подглядывание в будущее (утечка данных). Решение в использовании pipeline, ниже распишу
Модель логистической регрессии¶
pipe = Pipeline([('classifier' , LogisticRegression(random_state=rs, class_weight = 'balanced'))])
param_grid = [
{'classifier__penalty' : ['l1', 'l2'],
'classifier__C' : np.logspace(-4, 4, 20),
'classifier__solver' : ['liblinear']
}]
clf = GridSearchCV(pipe, param_grid = param_grid, scoring='f1', cv = 3, verbose=True, n_jobs=-1)
best_clf = clf.fit(tfidf_train, target_train)
print('Best parameters is:', clf.best_params_)
print('Best score is:', clf.best_score_)
Fitting 3 folds for each of 40 candidates, totalling 120 fits
/opt/conda/lib/python3.9/site-packages/sklearn/svm/_base.py:985: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
warnings.warn("Liblinear failed to converge, increase "
/opt/conda/lib/python3.9/site-packages/sklearn/svm/_base.py:985: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations.
warnings.warn("Liblinear failed to converge, increase "
Best parameters is: {'classifier__C': 4.281332398719396, 'classifier__penalty': 'l1', 'classifier__solver': 'liblinear'}
Best score is: 0.761587895495274
Промежуточный вывод:
Обучив модель логистической регрессии иполучив метрику f1 на обучающей выборке. Можно заключить, что мы получили довольно высокую оценку метрики, а следовательно модель нас устраивает. Её параметры:
{'classifier__C': 4.281332398719396,
'classifier__penalty': 'l1',
'classifier__solver': 'liblinear'}
Успех:
не забыт random_state
class_weight = 'balanced'
scoring = 'f1'
Совет:
Молодец что используешь GridSearch, но еще лучше использовать связку GridSearchCV + pipeline.
У тебя pipeline обьявлен, но не использован по сути. Стоило сделать так (схема):
pipeline = Pipeline([
('tfidf', TfidfVectorizer(stop_words=stopwords)),
('model',LogisticRegression(random_state = 42))])
То есть в pipline нужно добавить векторизатор. Вот это и позволяет избежать утечки данных
О pipeline:
Pipeline, это тема которая сразу затрагивает кроссвалидацию, тюнинг "векторайз", подбор гиперпараметров модели и о том что код стоит делать компактным.
в TfidfVectorizer(stop_words=stopwords) у тебя по умолчанию ngram_range=(1, 1), тут можно подбирать разное число n- грамм (и другие параметры), максимизируя метрику, но как объединить перебор по ngram_range с обучением моделей, чтобы не делать это по отдельности или с использованием цикла?! pipeline! Готовый пример для работы с текстами. Всё что нужно там есть, хотя очень лаконично. Можешь погуглить по:
pipeline nlp gridsearchcv
как избежать ошибки подглядывания в будущее, когда мы предварительно работаем с данными (шкалирование, нормализация, TfidfVectorizer итп итд)? pipeline! особенно это важно, когда мы используем кроссвалидацию. Для TfidfVectorizer делаем .fit (обучаемся) на train, а transform на test, но точно также нужно сделать для валидационной выборки. Но GS делает валидационные внутри себя, спрашивается как добраться до нее и избежать подглядывания в будущее? Казалось бы никак, но нет! Pipeline! )
pipeline позволяет делать наш код компактней и читабельней, это большой плюс, когда код будет раздуваться
Если раньше не использовал pipeline то могу посоветовать видео в котором индус на английском с сильным акцентом, но на пальцах обьясняет самое непонятное (по моему опыту): сопряженность методов fit и transform. Там же есть и код и ссылка на текст. Мне помогло )
В общем если сделать GS+pipeline будет вообще хорошо )
Модель древа классификации¶
dtc = DecisionTreeClassifier(random_state=rs, class_weight = 'balanced')
params = [{'max_depth':[x for x in range(24,30,2)],
}]
clf = GridSearchCV(dtc, params, scoring='f1',cv=3,verbose=True, n_jobs=-1)
clf.fit(tfidf_train, target_train)
print(clf.best_params_)
print(clf.best_score_)
Fitting 3 folds for each of 3 candidates, totalling 9 fits
{'max_depth': 24}
0.6361245967713611
Промежуточный вывод:
Обучив модель получили метрику f1 на обучающей выборке ~0.6. Это противоречит нашему основному условию (F1-метрика >=0.75)
Модель лес классификации¶
params_forest = {
'n_estimators': list(range(30,60,10)),
'max_depth': [x for x in range(24,30,2)],
'max_features' : list(range(1,10, 2))
}
model_forest = RandomForestClassifier(random_state=rs, class_weight = 'balanced')
grid = GridSearchCV(model_forest, param_grid=params_forest, scoring='f1', cv=3, verbose=True, n_jobs=-1)
best_grid = grid.fit(tfidf_train, target_train)
print('Best parameters is:', grid.best_params_)
print('Best score is:', grid.best_score_)
Fitting 3 folds for each of 45 candidates, totalling 135 fits
Best parameters is: {'max_depth': 24, 'max_features': 5, 'n_estimators': 40}
Best score is: 0.36657169745957
Совет 🤔:
Советы по выбору модели для экономии времени
Деревянные модели (RF, DT) медленные, и на данном датасете не показывает хорошие результаты. Лучше используй Логистическую регрессию и LightGBM
Советую сконцентрироваться у Логистической регрессии на переборе "C", а max_iter и solver которые частенько используют, лучше не трогать. У LightGBM Можно попробуй явно указать num_threads
Как уже писал в выводе, на платформе с этими моделями работать ну невозможно( код работает от часа и дольше без вариативности)
Совет 🤔:
Отличный повод попробовать Colab. Как видишь проект не только про НЛП, но и про то как сделать нашу работу эффективнее с точки зрения трат ресурсов и времени. Это имеет очень близкое отношение к реальности в ML
Промежуточный вывод:
Обучив модель получили метрику f1 на обучающей выборке ~0.29. Это противоречит нашему основному условию (F1-метрика >=0.75)
Вывод по моделям:
1) Модель для дальнейшей проверки остается только одна, это LogisticRegression.
2) Модели случайного леса и дерева решения показали не удовлетворительные результаты, следовательно мы их рассматривать не будем.
3) Осталось за кадром изучение двух моделей CatBoostClassifier и LGBM. Время обучени без подбора по гиперпараметрам составляло более часа.
Выводы¶
Проверка на тестовой выборке¶
Т.к. модель логистическая регрессии сохранена в пременной, то мы просто произведем предсказание и посмотрим метрику.
Совет 🤔:
Не линейная а Логистическая регрессия
Исправил. Простите запнулся)
final_predict =best_clf.predict(tfidf_test)
print('F1-мера на тестовой выборке: {:.2}'.format(f1_score(target_test, final_predict)))
F1-мера на тестовой выборке: 0.77
Успех:
Все верно, логика моделирования не нарушена, тут тестируем только лучшую модель отобранную на валидации, или парочку лучших, если на валидации результаты близки
Если студент получил на тесте f1 выше 0,75, это считается приемлемым результатом.
Совет:
Что может помочь добиться лучшего результата (от простого)?
использовать stratify
можно поиграться порогом
полезно настраивать векторайзеры и избегать утечки данных при использовании GS и предобработки данных (тут пригодится pipeline)
сгенерировать новые фичи, например например посчитать число слов в тексте, длину слов итп итд. Или с помощью тематического моделирования
попробовать другие модели. проект своеобразный выбор между вычислительными ограничениями (много примеров, расчеты могут затянуться) и задачей получить хорошую метрику (как это и бывает на практике), поэтому советовать "тяжелые", но мощные модели, чтобы у тебя все окончательно не повисло не буду. Например использование случайного леса часто приводят к получению f1 = 0, потому что для этого датасета нужны деревья бОльше глубины, но это будет слишком долго считать. И тут, градиентный бустинг, который используют деревья небольшой глубины, выигрывает, к тому же некоторые градиентные бустинги быстрые, это видно по их названию (LightGBM).
использование предбученной модели Берта, выбрав соответствующую модель и используя полученные эмбединги, даже на небольшом тренировочном датасете можно обучить модель, которая на test покажет хорошую метрику. В этом случаи можно сразу получить метрику > 0.95 (при правильно выбранной модели)
Дамми модель и ее метрика¶
Создадим DummyClassifier для проверки нашей модели на адекватность. Предсказания строим на самом часто встречающемся значении "0".
dummy_clf = DummyClassifier(strategy="most_frequent", random_state=rs)
dummy_clf.fit(tfidf_train, target_train)
predict_dummy = dummy_clf.predict(tfidf_test)
print('F1-метрика фиктивной модели',f1_score(target_test, predict_dummy))
F1-метрика фиктивной модели 0.0
Метрика F1 оказалась равной 0. Что не удевительно, ведь F1 метрика это метрика комплексной оценки.
Совет:
Плюс за проверку. Хотя на мой взгляд на адекватность стоит применять в разрезе метрики accuracy. А метрика f1 и без того "умная", поэтому константные модели на ней ничего интересного не покажут
Можешь попробовать посчитать f1
model = DummyClassifier(strategy = 'constant', constant = 1)
метрика будет повыше
А нет, не поменялась (я проверил)
Совет 🤔:
С
strategy="most_frequent"
У тебя Метрика f1 всегда будет ноль, потому что всегда будет предсказываться один самый частый класс. И если поставить даже какой-то Рандомный прогноз, метрика F1 перестанет быть нулевой
Вывод :
Мы прошли следующий путь:
1) Загрузили данные, проверелили на ошибки. Оказалось все хорошо.
2) Провели очистку текста и лемматизировали.
3) Произвели деление на обучающую и тестовою выборку.
4) Векторизовали наши признаки
5) Далее построили модели с учетом дисбалнса в целевом признаке.
Итог данной работы : Была разработана модель классифицировать комментарии на позитивные и негативные. Наша модель это "LogisticRegression". Ее параметры: classifier__C: 4.281332398719396, classifier__penalty : l1, classifier__solver : liblinear .
Качесво F1 метрики на тестовом наборе составило 0.77.
У тебя старательно выполненная работа, все четко, осмысленно.
Логика моделирования не нарушена, GS использован корректно. Если хочешь прокачать свой скилл попробуй использовать его с pipeline.
Я оставил небольшие советы и вопросики (если есть время и желание можешь воспользоваться/ответить).
Обязательное к исправлению:
лемматизации не произошло (Mystem не работает с текстом на латинице)
.astype('U') лишнее, стоит экономить ресурсы, иначе может даже ядро обрушиться
Жду исправлений, для принятия проекта. Если какие то вопросы, то сразу спрашивай )
Спасибо за работу! Красное исправлено. Надеюсь мои советы и вопросики были полезны и в копилочку знаний упало что то новое, а проект стал лучше, и симпатичней.
Отличная работа. Желаю успехов в дальнейшей учебе!
Полезные (и просто интересные) материалы:
Для работы с текстами используют и другие подходы. Например, сейчас активно используются RNN (LSTM) и трансформеры (BERT и другие с улицы Сезам, например, ELMO). НО! Они не являются панацеей, не всегда они нужны, так как и TF-IDF или Word2Vec + модели из классического ML тоже могут справляться.
BERT тяжелый, существует много его вариаций для разных задач, есть готовые модели, есть надстройки над библиотекой transformers. Если, обучать BERT на GPU (можно в Google Colab или Kaggle), то должно быть побыстрее.
https://huggingface.co/transformers/model_doc/bert.html
https://t.me/renat_alimbekov
https://colah.github.io/posts/2015-08-Understanding-LSTMs/ - Про LSTM
https://web.stanford.edu/~jurafsky/slp3/10.pdf - про энкодер-декодер модели, этеншены
https://pytorch.org/tutorials/beginner/transformer_tutorial.html - официальный гайд
по трансформеру от создателей pytorch
https://transformer.huggingface.co/ - поболтать с трансформером
Библиотеки: allennlp, fairseq, transformers, tensorflow-text — множествореализованных
методов для трансформеров методов NLP
Word2Vec https://radimrehurek.com/gensim/models/word2vec.html
Если понравилась работа с текстами, то можешь посмотреть очень интересный (но очень-очень сложный) курс лекций: https://github.com/yandexdataschool/nlp_course .
Если нравится смотреть и слушать то есть целый курс на Ютубе https://www.youtube.com/watch?v=qDMwIQRQt-M&list=PLEwK9wdS5g0qksxWxtE5c2KuFkIfUXe3i&index=1
Чек-лист проверки¶
- Jupyter Notebook открыт
- Весь код выполняется без ошибок
- Ячейки с кодом расположены в порядке исполнения
- Данные загружены и подготовлены
- Модели обучены
- Значение метрики F1 не меньше 0.75
- Выводы написаны