Время прочтения: 5 мин.

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

Для примера обучим нейросеть отличать «фейковые» новости от реальных на основе соответствующего датасета.

Датасет df_news можно скачать с репозитория github по ссылке, указанной в конце. Он содержит тексты новостей и метки, обозначающие «фейковые» новости (1) и реальные (0). Количество «фейковых» и реальных новостей примерно одинаково. Вид датасета представлен ниже.

После получения текстов, в первую очередь, следует предварительно обработать их: убрать знаки пунктуации, стоп-слова (предлоги, союзы, местоимения) и привести слова в нормальную форму, чтобы разные формы одного и того же слова не считались за различные слова.

Чтобы убрать знаки пунктуации и разделить текст на отдельные слова применим класс RegexpTokenizer из библиотеки nltk. С помощью регулярного выражения в нём задаются символы, которые должны содержать слова. В данном случае в качестве таких символов выступают буквы русского и латинского алфавита.

Далее, из полученного набора слов отбрасываются стоп-слова, множество которых можно получить через nltk, а оставшиеся слова обрабатываются с помощью лемматизатора в классе MorphAnalyzer из библиотеки pymorphy2. Суть лемматизатора заключается в том, что он приводит слово к своей нормальной форме – лемме. Для существительных и прилагательных это будет именительный падеж в единственном числе, для глаголов, причастий и деепричастий – инфинитив.

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import RegexpTokenizer
import pymorphy2

nltk.download('stopwords')
stop_words = set(stopwords.words('russian'))
nltk_tokenizer = RegexpTokenizer(r'[а-яёa-z]+')

morph = pymorphy2.MorphAnalyzer()

def text_preprocessing(text):
  words = nltk_tokenizer.tokenize(text.lower())
  lem_text = [morph.parse(w)[0].normal_form for w in words if w not in stop_words]

  return lem_text

Теперь обработанные тексты нужно привести в форму, понятную для нейронной сети. Для этого возьмём модель Word2vec, где каждому слову ставится в соответствие некоторый вектор. Чтобы лучше понять суть этого подхода приведу простую аналогию.

Возьмём координатную плоскость и в случайном порядке расставим на ней точки. У каждой точки будет значение координат по осям X и Y, с помощью которых можно оценивать взаимное расположение точек, вычислять расстояние между ними и, самое главное, перенести их представление в компьютер.

Такой же подход применяется и в Word2vec, но только по отношению к словам, переводя их в вектор. Обучить такую модель можно и на датасете фейковых новостей, но если будут новости, содержащие новые слова, то всё придётся переобучать заново. С сайта rusvectores можно скачать такую модель для русского языка уже предварительно обученную на большом количестве слов. Подгружается она с помощью библиотеки gensim.

import gensim

if not os.path.isfile('220.zip'):
  !wget http://vectors.nlpl.eu/repository/20/220.zip
  !unzip 220.zip

w2v = gensim.models.KeyedVectors.load_word2vec_format('model.bin', binary=True)

Эта модель принимает на вход слова с указанием части речи, поэтому тексты требуется ещё раз обработать. Часть речи слов можно было бы получить и с помощью pymorphy2, но в pymorphy2 используются тэги частей речи, принятые в OpenCorpora, а в моделях rusvectores они соответствуют формату Universal PoS Tags. Такие теги можно получить через модель UDPipe, предварительно обученную на словах русского языка.

import ufal.udpipe as udp
import corpy.udpipe as crp

# Скачивание модели UDPipe, обученную на русском языке
udp_model_url = r'https://lindat.mff.cuni.cz/repository/xmlui/bitstream/handle/11234/1-3131/russian-syntagrus-ud-2.5-191206.udpipe'
udp_model_filename = udp_model_url.rsplit('/', 1)[-1]
if not os.path.isfile(udp_model_filename):
  wget.download(udp_model_url)

# Загрузка модели в оболочку corpy
corpy_model = crp.Model(udp_model_filename)

# Функция для тегирования слов
def udp_tagging(lem_text):
  sents = [list(corpy_model.process(w)) for w in lem_text]
  tagged_words = [s[0].words[1].form + '_' + s[0].words[1].upostag for s in sents if s]

  return tagged_words

Протестируем Word2vec на слове «учитель», найдем наиболее похожие по смыслу, с указанием части речи.

w2v.most_similar(udp_tagging('учитель'))

Среди слов ещё остались местоимения («это», «который», «оно» и т.д), имена собственные, числительные и просто слова с ошибками. Такие слова модель Word2vec обработать не сможет, так как они отсутствуют в словаре модели, поэтому их нужно отбросить.

X_not_filtered_texts = df_news['text'].apply(lambda x: udp_tagging(text_preprocessing(x))).array

X_filtered_texts = []
for text in X_not_filtered_texts:
  words = []
  for word in text:
    if word in w2v.vocab:
      words.append(word)
    else:
      pass
  X_filtered_texts.append(words)

Наконец, нужно заменить слова на соответствующие индексы словаря Word2vec и разбить на обучающую и тестовую выборки.

from sklearn.model_selection import train_test_split

X = np.array([list(map(lambda x: w2v.vocab[x].index, t)) for t in X_filtered_texts]).astype('float32')

Y = np.array(df_news['label']).astype('float32').reshape((-1,1))

x_train, x_test, y_train, y_test = train_test_split(X, Y)

Теперь тексты полностью обработаны и готовы для обучения. Осталось заключительное действие – обучение нейросети. Первым слоем идёт модель Word2vec, преобразующая индексы слов, полученные ранее, в соответствующие векторы. Далее идут два полносвязных слоя Dense, между которыми проложен слой Dropout для предотвращения переобучения.

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

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout

seq_model = Sequential()

seq_model.add(w2v.wv.get_keras_embedding(train_embeddings=False))
seq_model.add(Dense(50, activation='relu'))
seq_model.add(Dropout(0.6))
seq_model.add(Dense(1, activation='sigmoid'))

seq_model.compile(loss='mean_squared_logarithmic_error',
                  optimizer='adam', metrics=['accuracy'])

seq_model.fit(x_train, y_train, epochs=20,
              validation_data=(x_test, y_test))

За 20 эпох обучения модель добилась точности в 95% на тестовой выборке. Это значит, что полученная модель может распознать «фейковость» новости по ее тексту с большой долей вероятности.

Проверим модель на «фейке», появившимся совсем недавно — про заморозку вкладов у населения.

fake_text = 'Центробанк России готовится заморозить вклады населения…

fake_preproc = udp_tagging(text_preprocessing(fake_text))

fake_words_tagged = []
for word in fake_preproc:
  if word in w2v.vocab:
    fake_words_tagged.append(word)

fake_indexed = np.array(list(map(lambda x: w2v.vocab[x].index, fake_words_tagged))).astype('float32')

seq_model.predict(fake_indexed)
array([[0.94543687]], dtype=float32)

Как видно, с таким заданием нейросеть справилась хорошо и с вероятностью 94% определила «фейк».

Ссылка на репозиторий github