Время прочтения: 7 мин.
Сегодня я рассмотрю процесс получения эмбеддингов текстов с помощью BERT для их дальнейшего тематического моделирования.
Обработка естественного языка одно из востребованных направлений машинного обучения, которое постоянно развивается. В 2018 году компания Google представила новую модель — BERT, сделавшую прорыв в области обработки естественного языка. Несмотря на то, что сейчас у BERT много конкурентов, включая модификации классической модели (RoBERTa, DistilBERT и др.) так и совершенно новые (например, XLNet), BERT всё ещё остается в топе nlp-моделей.
Данная модель применяется во многих задачах: классификация текстов, генерация текста, суммаризация текста и т.д. Но ее также можно использовать для получения векторных представлений текста — эмбеддингов. Они могут использоваться, например, для последующей кластеризации или в качестве исходных данных для других моделей.
Сейчас я попытаюсь выделить подтемы внутри большого набора статей про науку и технику в новостном сайте. В начале я получу эмбеддинги с помощью предобученной модели BERT. Для этого буду использовать python-библиотеки transformers и pytorch. Затем полученные эмбеддинги буду использовать в качестве признаков в кластеризации. После того как получу метку принадлежности к определенному кластеру, сожму эмбеддинги до двух признаков, чтобы визуализировать результат.
В качестве источника данных буду использовать архив с новостями Lenta.ru. Весь код будет выполняться в облачном сервисе Google Colaboratory.
Скачаем и распакуем архив с новостями и предобученную на русских текстах модель BERT с проекта deeppavlov. Весь код будет выполняться в облачном сервисе Google Colaboratory.
!gdown https://github.com/yutkin/Lenta.Ru-News-Dataset/releases/download/v1.1/lenta-ru-news.csv.bz2
!gdown http://files.deeppavlov.ai/deeppavlov_data/bert/rubert_cased_L-12_H-768_A-12_pt.tar.gz
!bzip2 -d lenta-ru-news.csv.bz2
!tar -xzf /content/rubert_cased_L-12_H-768_A-12_pt.tar.gz
Импортируем нужные библиотеки
!pip install transformers
!pip install pyyaml==5.4.1
import numpy as np
import pandas as pd
import torch
import os
import json
from tqdm import tqdm
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.decomposition import PCA
from sklearn.cluster import AgglomerativeClustering
from torch.utils.data import Dataset, DataLoader
from transformers import BertModel, BertTokenizer
В распакованном архиве хранится csv-файл с информацией о статьях. Почти для каждой статьи указан заголовок, тематика, тег и дата публикации.
Посмотрим список тем:
df = pd.read_csv('/content/lenta-ru-news.csv', low_memory=False)
df['topic'].unique()
# 'Библиотека', 'Россия', 'Мир', 'Экономика', 'Интернет и СМИ',
# 'Спорт', 'Культура', 'Из жизни', 'Силовые структуры',
# 'Наука и техника', 'Бывший СССР', nan, 'Дом', 'Сочи', 'ЧМ-2014',
# 'Путешествия', 'Ценности', 'Легпром', 'Бизнес', 'МедНовости',
# 'Оружие', '69-я параллель', 'Культпросвет ', 'Крым'
Выберем десять тысяч статей из темы «Наука и техника».
corpus = df.loc[df['topic']=='Наука и техника', 'text'].to_numpy()
corpus = corpus[:10000]
Посмотрим пример текста:
corpus[0]
# Американские ученые в ближайшее время отправят на орбиту спутник,
# который проверит два фундаментальных предположения, выдвинутых Альбертом Эйнштейном...
В распакованной папке с моделью есть файл bert_config.json. Но метод from_pretrained ожидает наличие в папке файла config.json, а не bert_config.json, добавим недостающий файл. Если бы мы использовали обобщенные классы AutoModel и AutoTokenize, то пришлось бы в файл config.json добавлять строчку «model_type»: «bert», т.к изначально эти классы ничего не знают о типе используемой модели.
with open("/content/rubert_cased_L-12_H-768_A-12_pt/bert_config.json", "r") as read_file, open("/content/rubert_cased_L-12_H-768_A-12_pt/config.json", "w") as conf:
file = json.load(read_file)
conf.write(json.dumps(file))
!rm /content/rubert_cased_L-12_H-768_A-12_pt/bert_config.json
В BERT подается на вход не сам текст, а токены. Поэтому вместе со скачанной моделью в папке идет готовый токенизатор.
Подгрузим модель и токенизатор из распакованной папки с помощью метода from_pretrained. Для этого нужно просто указать путь к папке. При инициализации модели укажем, чтобы выводились промежуточные состояния.
На самом деле модель можно подгрузить с сайта Hugging Face, просто указав ее корректное название в методе from_pretrained. Но вариант с предварительным скачиванием пригодится, например, когда отсутствует доступ к интернету.
tokenizer = BertTokenizer.from_pretrained('rubert_cased_L-12_H-768_A-12_pt')
model = BertModel.from_pretrained('rubert_cased_L-12_H-768_A-12_pt', output_hidden_states = True)
Далее воспользуюсь вспомогательными классами Dataset и Dataloader из torch.utils. Создам класс CustomDataset на основе импортированного класса Dataset. Для корректной работы переопределю в нем методы __len__ и __getitem__.
Добавим метод tokenize, внутри которого к тексту будет применяться инициализированный ранее tokenizer. Формат выходных данных будет torch.tensor. Поставим максимальную длину токенизированного текста на 150 токенов.
class CustomDataset(Dataset):
def __init__(self, X):
self.text = X
def tokenize(self, text):
return tokenizer(text, return_tensors='pt', padding='max_length', truncation=True, max_length=150)
def __len__(self):
return self.text.shape[0]
def __getitem__(self, index):
output = self.text[index]
output = self.tokenize(output)
return {k: v.reshape(-1) for k, v in output.items()}
eval_ds = CustomDataset(corpus)
eval_dataloader = DataLoader(eval_ds, batch_size=10)
Для получения эмбеддингов, воспользуемся функцией из этой статьи. В ней берется выход из последнего слоя и усредняется по каждому значению. Про значения, получаемые на выходе из BERT, можно почитать здесь.
def mean_pooling(model_output, attention_mask):
token_embeddings = model_output['last_hidden_state']
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
sum_embeddings = torch.sum(token_embeddings * input_mask_expanded, 1)
sum_mask = torch.clamp(input_mask_expanded.sum(1), min=1e-9)
return sum_embeddings / sum_mask
Переводим модель в состояние валидации и отключаем подсчет градиента, а также переносим на графический ускоритель, если он доступен:
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
model.eval()
embeddings = torch.Tensor().to(device)
with torch.no_grad():
for n_batch, batch in enumerate(tqdm(eval_dataloader)):
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
embeddings = torch.cat([embeddings, mean_pooling(outputs, batch['attention_mask'])])
embeddings = embeddings.cpu().numpy()
Алгоритмы кластеризации времязатратны, поэтому, чтобы сократить длительность расчетов, уменьшим размерность эмбеддингов с 768 до 15. Для этого применим классический метод уменьшения размерности — «Метод главных компонент», реализованный в sklearn.
pca = PCA(n_components=15, random_state=42)
emb_15d = pca.fit_transform(embeddings)
Далее запускаем алгоритм кластеризации. Данный алгоритм позволяет определить автоматически число кластеров. Для этого надо указать в параметрах n_clusters=None, но тогда нужно обязательно указать параметр distance_threshold. Он отвечает за пороговое расстояние. Если расстояние между наблюдением и кластером меньше порога, то это наблюдение причисляется к этому кластеру. Параметр linkage отвечает за то, как считается координата кластера. В моём случае координата считается, как среднее координат всех наблюдений, принадлежащих к этому кластеру. Параметр affinity отвечает за метрику, которая будет использоваться для вычисления расстояния между наблюдениями. Мы выберем косинусное расстояние.
clustering = AgglomerativeClustering(n_clusters=None, distance_threshold=0.6, affinity='cosine', linkage='average').fit(emb_15d)
Чтобы визуализировать результат на графике, уменьшу размерность до двух.
pca = PCA(n_components=2, random_state=42)
emb_2d = pd.DataFrame(pca.fit_transform(embeddings), columns=['x1', 'x2'])
emb_2d['label'] = clustering.labels_
emb_2d['label'].nunique() # 40
У меня получилось 40 разных кластеров. Количество кластеров может быть другим. Всё зависит от выбранных гиперпараметров при кластеризации, о которых я говорил ранее.
Построю интерактивный график с помощью библиотеки plotly для быстрой навигации по получившимся кластерам.
import plotly.express as px
fig = px.scatter(emb_2d, x='x1', y='x2', color='label', width=800, height=600)
fig.show()
Данный график иллюстрирует распределение кластеров на двумерном пространстве. Для интерпретации получившихся результатов необходимо более детально рассмотреть каждый кластер. Это можно осуществить, рассчитав TF-IDF для каждого слова из текста. С помощью этого метода можно получить ключевые слова каждой темы. Важность слов в этом методе оценивается по частоте встречаемости их в конкретном тексте и редкости в других текстах. Более подробно о реализации этого можно почитать здесь.
Я лишь посмотрим несколько первых предложений внутри кластеров и оценим их схожесть. Рассмотрим кластер слева под номером 0:
def show_examples(cluster, n):
for i in range(n):
print(i, corpus[emb_2d['label'] == cluster][i].split('.')[0])
show_examples(cluster=0, n=3)
# 0 Доплеровский радар в армии США будет использоваться не только для составления метеорологических карт,
# но и для раннего предупреждения о биологической или химической атаке с воздуха
# 1 Военно-космические силы США приняли на вооружение новую систему,
# предназначенную для глушения космических спутников, сообщает агентство Reuters
# 2 На авиабазе ВВС США "Эдвардс" в Калифорнии проведены первые успешные испытания боевого лазера воздушного базирования
Вероятнее всего, в этом кластере говорится о военных достижениях США.
Для сравнения посмотрим кластер 6 сверху:
show_examples(cluster=6, n=3)
# 0 Используя преимущества технологии Blu-ray (синий лазер, в отличие от красного, применяемого в CD и DVD-приводах),
# позволяющей создавать сверхтонкие носители информации, корпорация Sony и компания Toppan Printing разработали "бумажный" диск,
# на который можно записать 25 гигабайт видео
# 1 Американская компания Microvision создала лазерную технологию,
# которая позволит человеку видеть дополнительное изображение - помимо той картинки, которую он получает при помощи обычного зрения
# 2 Автор операционной системы Linux Линус Торвальдс (Linus Torvalds) предлагает создать новую систему # регистрации изменений, вносимых в операционную системы, с целью предотвращения любых обвинений в нарушении авторских прав, сообщает Siliconvalley
В этом кластере, вероятнее всего, говорится о средствах передачи информации.
Таким образом, я рассмотрел все основные этапы тематического моделирования, кроме подробного анализа получившихся кластеров. Не исключено, что в наших кластерах найдутся темы, которые дублируют друг друга. Есть несколько способов это исправить. Первый – это поэкспериментировать с параметрами кластеризации или указать вручную число кластеров, а также можно вовсе использовать другой метод кластеризации. Второй – уменьшить количество тем с помощью поиска похожих кластеров и объединения их в один. Данный подход можно осуществить с помощью TF-IDF и подобных методов.