Каждый день мы сталкиваемся с новой информацией, которая формирует наше мышление. Важно, чтобы эта информация была непредвзятой и нейтральной, чтобы читатели могли принимать рациональные решения без влияния. В этом посте рассматриваются различные алгоритмы НЛП и модели машинного обучения для выявления политической предвзятости в статьях СМИ, написанных на македонском языке.

Содержание:

  • Изучение набора данных
  • Подготовка данных
  • Использование Doc2Vec для кодирования текста
  • Изучение различных моделей:
    – Наивный байесовский классификатор
    – Классификатор случайного леса
    – Классификатор опорных векторов
    – Классификация глубокого обучения
  • Сравнение и оценка моделей
  • Использование моделей для анализа данных
  • Заключительные заметки

Данные

Набор данных состоит из текстов около 6000 статей, взятых за 2020–2022 годы. Каждая статья помечена как один из трех классов:

  • N - нейтральный (не предвзятый). Эти статьи взяты из средств массовой информации, демонстрирующих небольшую предвзятость или ее полное отсутствие на основе опросов читателей.
  • V - Смещено вправо. Эти статьи из правопартийного пресс-центра.
  • S - Смещено влево. Эти статьи из левого партийного пресс-центра.

Основная цель — отнести текст статьи к одному из этих классов.

Важное примечание.Нейтральные СМИ публикуют не только политические материалы (спорт, мировые новости и т. д.). Набор данных состоит только из статей, взятых из категории местных новостей/политики, поскольку нет смысла выявлять предвзятость в неполитических текстах.

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

Начнем с импорта всего необходимого.

#Deep Learning libraries
from tensorflow import keras
import tensorflow as tf
from keras.models import Sequential
from keras.layers import Dense
from keras import backend as K
#Graphing libraries
import matplotlib.pyplot as plt
import seaborn as sns
#NLP libraries
import nltk
from gensim.models import Doc2Vec
import gensim
from gensim.models.doc2vec import TaggedDocument
#Machine learning libraries
from sklearn import utils
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
#Helper libraries
import multiprocessing
import numpy as np
import pandas as pd
import math
from bs4 import BeautifulSoup
import re


nltk.download('punkt')

Загрузите данные во фрейм данных pandas, удалите ненужные столбцы и перетасуйте строки в наборе данных.

# Load and shuffle the data
dataset = pd.read_csv('/content/dataset.csv', header=0)
dataset = dataset.drop('url',axis=1)
dataset = dataset.iloc[np.random.permutation(len(dataset))]

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

dataset['bias'] = dataset['bias'].replace(['S','N','V'],[0,1,2])

Давайте посмотрим на представление каждого класса в наборе данных.

bias_vals = dataset['bias'].value_counts()
plt.figure()
sns.barplot(x=bias_vals.index, y=bias_vals.values)
plt.show();

Видно, что набор данных несбалансирован, нейтральных выборок гораздо больше, чем остальных. Самая простая наивная модель, которая всегда предсказывает нейтральный класс, будет верна примерно в 44% случаев.

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

def clean(text):
    text = BeautifulSoup(text, "lxml").text
    text = re.sub(r'\|\|\|', r' ', text) 
    text = text.replace('„','')
    text = text.replace('“','')
    text = text.replace('"','')
    text = text.replace('\'','')
    text = text.replace('-','')
    text = text.lower()
    return text
def remove_stopwords(content):
    for word in _stopwords:
        content = content.replace(' '+word+' ',' ')
    return content
dataset['content'] = dataset['content'].apply(clean)
dataset['content'] = dataset['content'].apply(remove_stopwords)

Разделите данные на обучающие и тестовые наборы.

train, test = train_test_split(dataset, test_size=0.2)

Обработка естественного языка — НЛП

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

Doc2Vec — один из последних алгоритмов кодирования текста. Это более продвинутая версия предыдущего алгоритма Word2Vec. Doc2Vec кодирует весь текстовый документ в вектор выбранного размера.

Чтобы использовать Doc2Vec, документ с тегами должен быть создан с использованием токенизированного текста из статей с библиотекой NLTK.

def tokenize_text(text):
    tokens = []
    for sent in nltk.sent_tokenize(text):
        for word in nltk.word_tokenize(sent):
            if len(word) < 3:
                continue
            tokens.append(word.lower())
    return tokens
train_tagged = train.apply(
   lambda r: TaggedDocument(words=tokenize_text(r['content']), tags=  [r.bias]), axis=1)
test_tagged = test.apply(
   lambda r: TaggedDocument(words=tokenize_text(r['content']), tags=[r.bias]), axis=1)

Есть две «вариации» Doc2Vec:

  • Распределенная память (PV-DM) — вдохновлена ​​оригинальным алгоритмом Word2Vec.
  • Распределенный пакет слов (PV-DBOW) — часто лучше всего работает с более короткими текстами.

В этом посте я собираюсь обучить и сравнить оба этих алгоритма.

cores = multiprocessing.cpu_count()
models = [
    # PV-DBOW 
    Doc2Vec(dm=0, vector_size=300, negative=5, hs=0, sample=0, min_count=2, workers=cores),
    # PV-DM
    Doc2Vec(dm=1, vector_size=300, negative=5, hs=0, sample=0,    min_count=2, workers=cores)
]

Пополняйте словарный запас, тренируйтесь и сохраняйте модели.

for model in models:
  model.build_vocab(train_tagged.values)
  model.train(utils.shuffle(train_tagged.values),
    total_examples=len(train_tagged.values),epochs=30)

models[0].save("doc2vec_articles_0.model")
models[1].save("doc2vec_articles_1.model")

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

def vec_for_learning(model, tagged_docs):
    sents = tagged_docs.values
    classes, features = zip(*[(doc.tags[0],
      model.infer_vector(doc.words, steps=20)) for doc in sents])
    return features, classes
# PV_DBOW encoded text
train_x_0, train_y_0 = vec_for_learning(models[0], train_tagged)
test_x_0, test_y_0 = vec_for_learning(models[0], test_tagged)
# PV_DM encoded text
train_x_1, train_y_1 = vec_for_learning(models[1], train_tagged)
test_x_1, test_y_1 = vec_for_learning(models[1], test_tagged)

Классификация ML-моделей

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

Наивный байесовский классификатор

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

Давайте создадим 2 наивных байесовских классификатора для разных моделей Doc2Vec.

bayes_0 = GaussianNB()
bayes_1 = GaussianNB()

bayes_0.fit(train_x_0,train_y_0)
bayes_1.fit(train_x_1,train_y_1)
#Helper function for calculating accuracy on the test set.
def acc(true, pred):
  acc = 0
  for x,y in zip(true,pred):
    if(x == y): acc += 1
  return acc/len(pred)
print(acc(test_y_0,bayes_0.predict(test_x_0)))
print(acc(test_y_1,bayes_1.predict(test_x_1)))
# 0.9197907585004359
# 0.6120313862249346

Для первой модели я получаю точность 0,92, а для второй 0,61. Кодирование DBOW обеспечивает более высокую точность с помощью модели наивного Байеса для этих данных.

Случайный лесной классификатор

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

# Create random forests with 100 decision trees
forest_0 = RandomForestClassifier(n_estimators=100)
forest_1 = RandomForestClassifier(n_estimators=100)

forest_0.fit(train_x_0,train_y_0)
forest_1.fit(train_x_1,train_y_1)
print(acc(test_y_0,forest_0.predict(test_x_0)))
print(acc(test_y_1,forest_1.predict(test_x_1)))
# 0.9197907585004359
# 0.8108108108108109

Для первой модели мне удалось получить точность 0,92, а для второй 0,82. Так же, как и Наивный Байес, случайный лес достигает большей точности с моделью DBOW в этом наборе данных, хотя модель DM не так сильно отстает.

Классификатор опорных векторов

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

svc_0 = SVC()
svc_1 = SVC()

svc_0.fit(train_x_0,train_y_0)
svc_1.fit(train_x_1,train_y_1)
print(acc(test_y_0,svc_0.predict(test_x_0)))
print(acc(test_y_1,svc_1.predict(test_x_1)))
# 0.946817785527463
# 0.8918918918918919

DBOW с таким подходом получил точность 0,95, что на данный момент является лучшим показателем. DM достигает точности 0,89, что также намного лучше, чем у предыдущих моделей с этим кодированием.

Глубокое обучение

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

Перед обучением любых моделей глубокого обучения на этом наборе данных значения смещения должны быть закодированы с помощью кодирования One Hot.

def prepare_data_keras(train_x,train_y,test_x,test_y):
  tx,ty,tex,tey = 
    np.asarray(train_x),
    np.asarray(train_y),
    np.asarray(test_x),
    np.asarray(test_y)
  ty = np.asarray(
    list([np.asarray([0,0,1]) if el == 0 else np.asarray([0,1,0]) 
    if el == 1 else np.asarray([1,0,0]) for el in ty]))
  tey = np.asarray(
    list([np.asarray([0,0,1]) if el == 0 else np.asarray([0,1,0])
    if el == 1 else np.asarray([1,0,0]) for el in tey]))
  return tx,ty,tex,tey

train_x_0, train_y_0, test_x_0, test_y_0 = prepare_data_keras(train_x_0, train_y_0, test_x_0, test_y_0)

Точность является наиболее распространенной метрикой для оценки моделей. Кроме того, для этих моделей глубокого обучения я буду использовать показатели F1 Score, точность и полнота.

def recall_m(y_true, y_pred):
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
    recall = true_positives / (possible_positives + K.epsilon())
    return recall

def precision_m(y_true, y_pred):
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
    precision = true_positives / (predicted_positives + K.epsilon())
    return precision

def f1_m(y_true, y_pred):
    precision = precision_m(y_true, y_pred)
    recall = recall_m(y_true, y_pred)
    return 2*((precision*recall)/(precision+recall+K.epsilon()))

Теперь создайте модели с тремя скрытыми слоями, активацией softmax на выходном слое и категориальной кросс-энтропийной потерей. 90 эпох обучения для каждого.

deep_models = [Sequential(),Sequential()]
for model in deep_models:
  model.add(Dense(512, activation='relu', input_shape=(300,)))
  model.add(Dense(256, activation='relu'))
  model.add(Dense(64, activation='relu'))
  model.add(Dense(3,activation='softmax'))
model.compile(loss='categorical_crossentropy',
 optimizer=tf.keras.optimizers.Adam(learning_rate=0.000001),
 metrics=['acc',recall_m,precision_m,f1_m])
# fit with 90 epochs
history_0 = deep_models[0].fit(train_x_0,train_y_0,epochs=90,validation_data=(test_x_0,test_y_0), verbose=1)
history_1 = deep_models[1].fit(train_x_0,train_y_0,epochs=90,validation_data=(test_x_0,test_y_0), verbose=1)
# evaluate the models
# fit with 90 epochs
for model in deep_models:
  model.evaluate(test_x_0, test_y_0, batch_size=128)
for model in deep_models:
  model.evaluate(test_x_0, test_y_0, batch_size=128)
# Output
# loss: 0.1751 acc: 0.9416 recall_m: 0.9283 precision_m: 0.9498 f1_m: 0.9388
# loss: 0.1721 acc: 0.9372 recall_m: 0.9300 precision_m: 0.9482 f1_m: 0.9390

При глубоком обучении точность почти одинакова для моделей DBOW и DM и составляет примерно 0,94.

# graph values
import seaborn as sns
import matplotlib.pyplot as plt


def plot_graph_loss(history):
    plt.plot(history.history['acc'])
    plt.plot(history.history['val_acc'])
    plt.title('model acc')
    plt.ylabel('acc')
    plt.xlabel('epoch')
    plt.legend(['train', 'test'], loc='upper left')
    plt.show()
    # summarize history for loss
    plt.plot(history.history['loss'])
    plt.plot(history.history['val_loss'])
    plt.title('model loss')
    plt.ylabel('loss')
    plt.xlabel('epoch')
    plt.legend(['train', 'test'], loc='upper left')
    plt.show()
# graph error and loss
plot_graph_loss(history_0)
plot_graph_loss(history_1)

Сравнение моделей

Все модели получили очень высокие оценки по показателю точности. Машины опорных векторов справились лучше всего с точностью 0,95, но глубокое обучение отстает с точностью 0,94. Важно отметить, что с дополнительной настройкой гиперпараметров алгоритмов глубокого обучения он, вероятно, может получить еще более высокие оценки. Однако для производственных сред модели глубокого обучения могут быть не лучшим выбором, поскольку они обычно требуют гораздо больше памяти и вычислительной мощности.

Использование моделей

С помощью классификатора опорных векторов я проанализировал 300 000 новостных статей из почти 40 различных источников и ранжировал их по степени предвзятости. Все статьи были опубликованы в течение года (с июля 2021 г. по июль 2022 г.) и относились к категории политических/местных новостей. С результатами можно ознакомиться по этой ссылке. Полный процесс извлечения статей и расчета смещения занял 89 часов на одной машине. Текст статьи не сохранился из соображений авторского права.

Важно отметить, что эти результаты не являются точным представлением правды, но дают приблизительное представление о предвзятости каждого источника СМИ.

Кроме того, я создал веб-сайт, на котором пользователи могут вводить URL-адрес статьи или текст, и с помощью модели SVC он вычисляет смещение. Он доступен по этой ссылке.

Заключительные заметки

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

Область, в которой эти модели могут быть улучшены, связана с более разнообразными предвзятыми данными из разных источников. Это еще больше ускорит обобщение алгоритмов и даст лучшие результаты. Например, один пресс-центр может публиковать одно и то же предложение или использовать слова в общем порядке в нескольких статьях. Модель изучит эти случаи и классифицирует новые статьи, содержащие похожий порядок, как необъективные, независимо от реального уклона.

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