Время прочтения: 6 мин.
Идея данного проекта состоит в том, чтобы создать модель машинного обучения, которая могла бы определять, являются ли заголовки новостей, представленные в интернете, правдой или нет. Для обучения модели в данной статье будем использовать данные из файла train.tsv, который содержит новостные заголовки взятые с https://panorama.pub и https://lenta.ru. В файле находится таблица, состоящая из двух колонок. В колонке title записаны заголовки новостей. В колонке is_fake содержатся метки: 0 – новость реальная, 1 – новость выдуманная (рисунок 1).
Рисунок 1 – Таблица с заголовками и метками из train.tsv
Для демонстрации работы модели используются данные тестового набора из файла test.tsv. В нем также есть колонка title, данные которой являются входными для модели.
Однако изначально колонка с метками заполнена значением 0. Прежде чем приступать к обучению модели, необходимо провести оценку или анализ данных. Так в первую очередь оценим соотношение реальных и фальшивых заголовков в исследуемом наборе данных (рисунок 2):
fig, ax = plt.subplots(figsize=(22,12))
sns.countplot(x=df[“is_fake”], palette=”Set2”)
ax.set(title=»Соотношение фейковых и реальных заголовков»)
plt.ylabel(«Кол-во заголовков»)
plt.xlabel(«Заголовок»)
plt.show()
Рисунок 2 – Соотношение меток фальшивых и реальных заголовков
Из рисунка 2 видно, что соотношение меток составляет 50% на 50%. Даже если это явно не заметно, проверим следующим способом:
#количество значений разными метками
df['is_fake'].value_counts()
Output:
1 2879
0 2879
Name: is_fake, dtype: int64
Видно, что количество реальных и фейковых заголовков в обучающем наборе у нас одинаковое.
Теперь приступим непосредственно к созданию модели. Прежде чем создавать «мешок слов» удалим стоп-слова с помощью набора stopwords от nltk для русского языка, а также проведем лемматизацию каждого слова в начальную форму.
Кратко о том, что такое «мешок слов» и лемматизация:
«Мешок слов» — наиболее распространенный метод преобразование текста в признаки. Данные методы выводят признак для каждого уникального слова в текстовых данных, но при этом каждый признак содержит количество вхождений в наблюдениях.
Например, в предложении «Бразилия — моя любовь. Бразилия!» имеет значение 2 в признаке «Бразилия», потому что слово Бразилия появляется два раза.
С помощью pymorphy2 приведем слова к начальной (нормальной) форме:
люди -> человек,
гулял -> гулять,
И т.д.
Начальную форму слова можно получить через атрибуты Parse.normal_form и Parse.normalized. Чтобы получить объект Parse, нужно первым делом разобрать слово и выбрать правильный вариант разбора из предложенных. Рассмотрим реализацию:
#импорт библиотек и загрузка модулей
from nltk.corpus import stopwords #импорт набора стоп-слов
import pymorphy2
import re #регулярные выражения
from string import punctuation #сборник символов пунктуации
stopWords = stopwords.words("russian")
morph = pymorphy2.MorphAnalyzer()
Проведем очистку текстовых данных от символов пунктуации, также приведем весь текст к нижнему регистру и выберем начальную форму каждого слова в заголовке:
def lemmatize(content):
review = re.sub('(?i)[^А-ЯЁA-Z]',' ',content)
review = review.lower()
review = review.split()
review = [morph.parse(word)[0].normal_form for word in review if not word in stopwords.words('russian')]
review = ' '.join(review)
return review
Далее применим функцию для очистки текста, это делается довольно просто:
df['title_normalize'] = df['title'].apply(lemmatize)
На выходе получаем следующий результат (рисунок 3):
Рисунок 3 – Очистка и лемматизация
Далее уже идет классический сценарий для обучения любой модели, а именно разделим набор данных на тренировочный и тестовый наборы:
from sklearn.model_selection import train_test_split
x_train,x_test,y_train,y_test=train_test_split(df['title_normalize'],
df['is_fake'],
test_size=0.2,
random_state=7)
print('Тренировочный набор:', x_train.shape)
print('Тестовой набор:', x_test.shape)
На выходе можем посмотреть сколько данных ушли для тренировки и для теста соответственно:
Импортируем необходимые библиотеки для обучения модели:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import PassiveAggressiveClassifier,LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.svm import LinearSVC
from sklearn.metrics import accuracy_score, confusion_matrix
Для «мешка слов» будут отбираться слова, взвешенные по их важности для наблюдения. То есть необходимо сравнить частоту слова в заголовке с частотой слова во всех других заголовках, используя статистическую меру словарной частоты — обратной документной частоты (tf-idf). Библиотека scikit-learn позволяет легко это делать с помощью класса TfidfVectorizer:
tfidf_vectorizer=TfidfVectorizer(stop_words=stopWords, max_df=0.7)
Обучение и преобразование переменных x_train и x_test:
tfidf_train=tfidf_vectorizer.fit_transform(x_train)
tfidf_test=tfidf_vectorizer.transform(x_test)
Теперь перейдем к самому интересному и интригующему пункту — выбор классификатора и его оценка на тестовой выборке.
Мною были выбраны и рассмотрены, на мой взгляд, самые популярные классификаторы для NLP задач, а именно:
- PassiveAggressiveClassifier;
- MultinomialNB (Naive Bayes);
- Logistic Regression;
- Linear SVC.
Посмотрим на PassiveAggressiveClassifier и его точность по результатам обучения:
pac=PassiveAggressiveClassifier(max_iter=50)
pac.fit(tfidf_train, y_train)
y_pred=pac.predict(tfidf_test)
score_pac=accuracy_score(y_test, y_pred)
print(f'Точность PassiveAggressiveClassifier: {round(score_pac*100,2)}%')
На выходе получаем следующее:
Точность PassiveAggressiveClassifier: 82.29%
В качестве метрики была выбрана Accuracy, так как данный показатель дает представление об общей точности предсказании моделей по всем классам. Поэтому знать это в данном проекте особенно полезно, так как каждый из классов важен. Данная метрика рассчитывается как отношение суммы правильных предсказаний к их общему количеству.
Однако при использовании данной метрики есть особенность, про которую необходимо помнить. Если распределение заголовков в обучающем наборе сильно смещено в сторону какого-то одного из классов, то при таком раскладе у классификатора есть больше информации по одному из классов и соответственно в рамках этого класса он будет принимать более верные решения. При такой ситуации, даже учитывая то, что accuracy может составлять, скажем, 80%, классификатор будет работать плохо и не определяя даже трети заголовков. Ниже показан отчет о классификации и матрица ошибок:
confusion_matrix(y_test,y_pred)
Получаем:
array([[473, 101],
[103, 475]])
Что можно заметить по матрице ошибок? То, что 101 заголовок имеет ошибку 1-го рода, то есть модель предсказала положительный результат, а на самом деле он отрицательный. И 103 заголовка, которые имеют ошибку 2-го рода – модель предсказала отрицательный результат, но на самом деле он положительный. Теперь взглянем на результаты каждого классификатора и его точности:
data_result = pd.DataFrame()
data_result['classifier'] = ['PassiveAggressiveClassifier',
'MultinomialNB (Naive Bayes)',
'Logistic Regression', 'Linear SVC']
data_result['results'] = score_pac, score_NB, score_LR, score_L_SVC
data_result.sort_values(by='results')
Ниже будет приведен датафрейм, в котором указаны результаты каждого классификатора и из него можно заметить, что Наивный Байес показал себя чуть лучше остальных (рисунок 4):
Рисунок 4 – Результаты классификаторов
Из рисунка 4 видно, что лучшим из 4-х классификаторов оказался MultinominalNB, поэтому прогоним через эту модель тестовый датасет (test.tsv) и определим фейковый заголовок или нет.
#импортируем тестовый набор
df_new = pd.read_csv("../nlp_test_task_2022/dataset/test.tsv", sep='\t')
#приводим слова в заголовках к начальной форме
df_new['title_normalize'] = df_new['title'].apply(lemmatize)
tfidf_test=tfidf_vectorizer.transform(df_new['title_normalize'])
#определяем метку для заголовка
new_label_test = []
for doc, category in zip(df_new['title'], NB.predict(tfidf_test)):
print('%r => %s' % (doc, category))
new_label_test.append(category)
На выходе получаем:
В данной статье были рассмотрены классические подходы решения конкретной NLP задачи, для которой была получена достаточно хорошая точность, но все равно возникает вопрос: «возможно ли еще улучшить модель?». Ответ на этот вопрос «очевидно, да», так как данный проект имеет множество возможностей для улучшений и модификаций, которые хотелось бы рассмотреть в следующей статье.