Предобученная нейронная сеть (НС) BERT может быть настроена для решения типовых задач обработки естественного языка, в частности, классификации документов.
Для этой цели берется НС BertForSequenceClassification и ее веса инициализируются значениями из 'DeepPavlov/rubert-base-cased':
model_name = 'DeepPavlov/rubert-base-cased'
model = BertForSequenceClassification. \
from_pretrained(model_name, num_labels = num_classes).to(device)
Выбор этой модели обусловлен тем, что классифицируемые документы записаны на русском языке.
Набор данных можно загрузить здесь.
В наборе 3'261 документа 13 классов. Разбивка по классам (класс / документ) : автомобили / 248, здоровье / 155, культура / 349, наука / 227, недвижимость / 98, политика / 452, происшествия / 380, реклама / 94, семья / 101, спорт / 367, страна / 240, техника / 271, экономика / 279.
Каждая строка (каждый абзац) файла - это документ, которому предшествует название класса документа.
Примеры размеченных документа набора данных:
#семья Ваши дети совсем не хотят вылезать из реки. С каждым днем "допустимое" время просмотра телевизора становится все больше и больше. Вы не уверены, что способны вынести хотя бы еще один крик "Его половина больше моей!"
#реклама Для любителей делать покупки в интернете мы сделали наш магазин очень удобным! На сайте очень удобная и простая навигация. В режиме реального времени работает онлайн консультант. Каждое изделие имеет 5 высококачественных фото, которые позволяют рассмотреть его при разных ракурсах. Советы стилиста помогут вам сделать выбор среди большого разнообразия авторских дизайнерских украшений.
Словарь меток (имен классов) задается в программе следующим образом:
cls_dict = {'#семья': 0, '#реклама': 1,'#недвижимость': 2, '#здоровье': 3,
'#политика': 4, '#культура': 5, '#спорт': 6, '#техника': 7,
'#экономика': 8, '#происшествия': 9, '#автомобили': 10,
'#страна': 11, '#наука': 12}
Предварительная обработка набора данных предусматривает выполнение следующих шагов, выполняемых для каждого документа:
Эти действия выполняет следующий код:
import re
fn_c = 'corp0.txt'
cls_dict = {'#семья': 0, '#реклама': 1,'#недвижимость': 2, '#здоровье': 3,
'#политика': 4, '#культура': 5, '#спорт': 6, '#техника': 7,
'#экономика': 8, '#происшествия': 9, '#автомобили': 10,
'#страна': 11, '#наука': 12}
# Обработка строки русского текста
def preprocess_s(s):
s = s.lower()
s = s.replace('ё', 'е')
# Оставляем только русские строчные буквы, остальное заменяем пробелами
s = re.sub('[^а-я]', ' ', s)
# Заменяем одиночные буквы на пробелы
s = re.sub(r'\b[а-я]\b', ' ', s)
s = re.sub(' +', ' ', s) # Заменяем несколько пробелов одним
return s.strip() # Удаляем начальный и конечные пробелы
labels = [] # Список меток
docs = [] # Список документов
with open(fn_c, 'r', encoding = 'utf-8') as f:
for doc in f:
# Разделяем каждую строку по первому пробелу
first_space = doc.find(' ')
cls_name = doc[0:first_space]
labels.append(cls_dict[cls_name])
doc = preprocess_s(doc[first_space + 1:-1])
docs.append(doc)
Далее делим набор на обучающее и проверочные множества:
from sklearn.model_selection import train_test_split
test_size = 0.2
trn_docs, tst_docs, trn_labels, tst_labels = train_test_split(docs, labels, test_size = test_size)
После этого выполняем токенизацию множеств и получаем индексы токенов:
from transformers import BertTokenizerFast as tkn, BertForSequenceClassification
from transformers import Trainer, TrainingArguments
# model_name = 'DeepPavlov/rubert-base-cased-sentence'
model_name = 'DeepPavlov/rubert-base-cased'
max_length = 512 # Максимальное число токенов в последовательности
tokenizer = tkn.from_pretrained(model_name, do_lower_case = True)
trn_encodings = tokenizer(trn_docs, truncation=True, padding=True, max_length=max_length)
tst_encodings = tokenizer(tst_docs, truncation=True, padding=True, max_length=max_length)
# Encoding(num_tokens = 512, attributes=[ids, type_ids, tokens, offsets,
# attention_mask, special_tokens_mask, overflowing])
# Печать кодов первого документа из tst_dataset и токенов документа
doc0_ids = tst_encodings['input_ids'][0] # Индексы
doc0_tkns = tokenizer.decode(doc0_ids) # Токены
print(doc0_ids) # [101, 6186, 4064, 18041, 613, 77933, 10757, 47654, 5153, ...
print(doc0_tkns) # [CLS] минское динамо заключило контракт защитником сборнои беларуси
print(tst_docs[0]) # минское динамо заключило контракт защитником сборной беларуси
for k in trn_encodings.keys(): print(k) # input_ids, token_type_ids, attention_mask
Приводим данные в вид, пригодный для обучения и тестирования:
class TextsDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
item['labels'] = torch.tensor([self.labels[idx]])
return item
def __len__(self):
return len(self.labels)
trn_dataset = TextsDataset(trn_encodings, trn_labels)
tst_dataset = TextsDataset(tst_encodings, tst_labels)
#
# Печать кодов первого документа из tst_dataset и токенов документа
doc0_ids = tst_dataset[0]['input_ids'].data # Индексы
doc0_tkns = tokenizer.decode(doc0_ids) # Токены
print(doc0_ids) # tensor([101, 6186, 4064, 18041, 613, 77933, 10757, 47654, 5153, ...
print(doc0_tkns) # [CLS] минское динамо заключило контракт защитником сборнои беларуси
Обучение и тестирование НС обеспечивает следующий код:
from sklearn.metrics import accuracy_score
num_classes = len(cls_dict) # Число классов
model = BertForSequenceClassification. \
from_pretrained(model_name, num_labels = num_classes).to(device)
# Метрика
def compute_metrics(pred):
labels = pred.label_ids
preds = pred.predictions.argmax(-1)
acc = accuracy_score(labels, preds) # accuracy_score - функция из sklearn.metrics
return {'accuracy': acc}
# Параметры обучения
training_args = TrainingArguments(
output_dir = path + 'results/', # Папка для результатов (весов)
num_train_epochs = 4, # Число эпох
per_device_train_batch_size = 8, # Размеры пакетов обучения и оценки
per_device_eval_batch_size = 8,
warmup_steps = 100, # Шаг выдачи предупреждений
weight_decay = 0.01, # Коэффициент уменьшения весов
logging_dir = path + 'logs/', # Папка для предупреждений
load_best_model_at_end = True, # Флаг загрузки лучшей модели после завершения обучения
logging_steps = 500, # Шаг сохранения весов (checkpoint)
evaluation_strategy = 'steps' # Стратегия обучения
)
trainer = Trainer(
model = model,
args = training_args,
train_dataset = trn_dataset,
eval_dataset = tst_dataset,
compute_metrics = compute_metrics
)
trainer.train() # Обучение
trainer.evaluate() # Оценка
fn_m = path + 'mdl' # Сохраняем модель в папку fn_m
model.save_pretrained(fn_m) # pytorch_model.bin & config.json
Конфигурация trainer:
Model config BertConfig {
"architectures": [
"BertModel"
],
"attention_probs_dropout_prob": 0.1,
"directionality": "bidi",
"gradient_checkpointing": false,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"id2label": {
"0": "LABEL_0",
"1": "LABEL_1",
"2": "LABEL_2",
"3": "LABEL_3",
"4": "LABEL_4",
"5": "LABEL_5",
"6": "LABEL_6",
"7": "LABEL_7",
"8": "LABEL_8",
"9": "LABEL_9",
"10": "LABEL_10",
"11": "LABEL_11",
"12": "LABEL_12"
},
"initializer_range": 0.02,
"intermediate_size": 3072,
"label2id": {
"LABEL_0": 0,
"LABEL_1": 1,
"LABEL_10": 10,
"LABEL_11": 11,
"LABEL_12": 12,
"LABEL_2": 2,
"LABEL_3": 3,
"LABEL_4": 4,
"LABEL_5": 5,
"LABEL_6": 6,
"LABEL_7": 7,
"LABEL_8": 8,
"LABEL_9": 9
},
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"output_past": true,
"pad_token_id": 0,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"position_embedding_type": "absolute",
"transformers_version": "4.8.2",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 119547
}
***** Running training *****
Num examples = 2720
Num Epochs = 5
Instantaneous batch size per device = 8
Total train batch size (w. parallel, distributed & accumulation) = 8
Gradient Accumulation steps = 1
Total optimization steps = 1700
Возможные результаты обучения:
Step | Training Loss | Validation Loss | Accuracy |
---|---|---|---|
500 | 0.929400 | 0.533455 | 0.867647 |
1000 | 0.263000 | 0.580013 | 0.886765 |
1500 | 0.080300 | 0.554551 | 0.905882 |
Модель используется для классификации документов. Документ задается в виде строки, при необходимости подвергается предварительной обработке и затем передается функции get_prediction.
Также с использованием этой функции в примере классифицируются все документы проверочного множества. Код определения токенизатора, задания словаря классов и подготовки проверочного множества в примере отсутствует (он приведен выше).
def get_prediction(text):
inputs = tokenizer(text, padding = True, truncation = True,
max_length = max_length, return_tensors = 'pt').to(device)
outputs = model(**inputs)
probs = outputs[0].softmax(1)
return cls_dict[probs.argmax()]
doc = 'На прошлой неделе в финском городе Куусамо стартовал Кубок мира \
по прыжкам на лыжах с трамплина. Нашу сборную повел в бой \
Вольфганг Штайерт - титулованный немецкий тренер, приглашенный \
в прошлом году в Россию.'
from transformers import BertConfig
config = BertConfig.from_json_file(fn_m + '/config.json')
model = BertForSequenceClassification. \
from_pretrained(fn_m + '/pytorch_model.bin', config = config).to(device)
# checkpoint = torch.load(fn_m + '/pytorch_model.bin')
# for k in checkpoint.keys(): print(k)
cls_dict = list(cls_dict.items())
print(get_prediction(doc)) # ('#спорт', 6)
n_true = 0
for doc, lbl in zip(tst_docs, tst_labels):
res = get_prediction(doc)
if res[1] == lbl:
n_true += 1
acc = n_true / len(tst_labels)
print(100 * round(acc, 4))
С использованием приведенных сведений студентам надлежит настроить один из доступных трансформеров для решения одной из типовых задач обработки естественного языка.
Выбор задачи и поиск подходящего набора данных студенты осуществляют самостоятельно.
# https://www.thepythoncode.com/article/finetuning-bert-using-huggingface-transformers-python
from sys import exit
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
colab = True
# 1 - обучение
# 2 - загрузка весов модели и оценка
step = 2
if colab:
!pip install transformers
from transformers import BertTokenizerFast as tkn, BertForSequenceClassification
from google.colab import drive
drv = '/content/drive/'
drive.mount(drv)
path = drv + 'My Drive/bert_classifier/'
else:
path = ''
fn_c = path + 'corp0.txt'
fn_m = path + 'mdl' # Сохраняем обученную модель в папку mdl
cls_dict = {'#семья': 0, '#реклама': 1,'#недвижимость': 2, '#здоровье': 3,
'#политика': 4, '#культура': 5, '#спорт': 6, '#техника': 7,
'#экономика': 8, '#происшествия': 9, '#автомобили': 10,
'#страна': 11, '#наука': 12}
num_classes = len(cls_dict) # Число классов
model_name = 'DeepPavlov/rubert-base-cased'
max_length = 512 # Максимальное число токенов в последовательности
tokenizer = tkn.from_pretrained(model_name, do_lower_case = True)
# Обработка строки русского текста
import re
def preprocess_s(s):
s = s.lower()
s = s.replace('ё', 'е')
# Оставляем только русские строчные буквы, остальное заменяем пробелами
s = re.sub('[^а-я]', ' ', s)
# Заменяем одиночные буквы на пробелы
s = re.sub(r'\b[а-я]\b', ' ', s)
s = re.sub(' +', ' ', s) # Заменяем несколько пробелов одним
return s.strip() # Удаляем начальный и конечные пробелы
labels = [] # Список меток
docs = [] # Список документов
with open(fn_c, 'r', encoding = 'utf-8') as f:
for doc in f:
# Разделяем каждую строку по первому пробелу
first_space = doc.find(' ')
cls_name = doc[0:first_space]
labels.append(cls_dict[cls_name])
doc = preprocess_s(doc[first_space + 1:-1])
docs.append(doc)
from sklearn.model_selection import train_test_split
test_size = 0.2
trn_docs, tst_docs, trn_labels, tst_labels = train_test_split(docs, labels, test_size = test_size)
if step == 1: # Обучение
from transformers import Trainer, TrainingArguments
# model_name = 'DeepPavlov/rubert-base-cased-sentence'
trn_encodings = tokenizer(trn_docs, truncation=True, padding=True, max_length=max_length)
tst_encodings = tokenizer(tst_docs, truncation=True, padding=True, max_length=max_length)
# Encoding(num_tokens = 512, attributes=[ids, type_ids, tokens, offsets,
# attention_mask, special_tokens_mask, overflowing])
# Печать кодов первого документа из tst_dataset и токенов документа
# doc0_ids = tst_encodings['input_ids'][0] # Индексы
# doc0_tkns = tokenizer.decode(doc0_ids) # Токены
# print(doc0_ids) # [101, 6186, 4064, 18041, 613, 77933, 10757, 47654, 5153, ...
# print(doc0_tkns) # [CLS] минское динамо заключило контракт защитником сборнои беларуси
# print(tst_docs[0]) # минское динамо заключило контракт защитником сборной беларуси
# for k in trn_encodings.keys(): print(k) # input_ids, token_type_ids, attention_mask
#
class TextsDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {k: torch.tensor(v[idx]) for k, v in self.encodings.items()}
item['labels'] = torch.tensor([self.labels[idx]])
return item
def __len__(self):
return len(self.labels)
trn_dataset = TextsDataset(trn_encodings, trn_labels)
tst_dataset = TextsDataset(tst_encodings, tst_labels)
#
# Печать кодов первого документа из tst_dataset и токенов документа
# doc0_ids = tst_dataset[0]['input_ids'].data # Индексы
# doc0_tkns = tokenizer.decode(doc0_ids) # Токены
# print(doc0_ids) # tensor([101, 6186, 4064, 18041, 613, 77933, 10757, 47654, 5153, ...
# print(doc0_tkns) # [CLS] минское динамо заключило контракт защитником сборнои беларуси
#
from sklearn.metrics import accuracy_score
# Метрика
def compute_metrics(pred):
labels = pred.label_ids
preds = pred.predictions.argmax(-1)
acc = accuracy_score(labels, preds) # accuracy_score - функция из sklearn.metrics
return {'accuracy': acc}
# Параметры обучения
training_args = TrainingArguments(
output_dir = path + 'results/', # Папка для результатов (весов)
num_train_epochs = 5, # Число эпох
per_device_train_batch_size = 8, # Размеры пакетов обучения и оценки
per_device_eval_batch_size = 8,
warmup_steps = 100, # Шаг выдачи предупреждений
weight_decay = 0.01, # Коэффициент уменьшения весов
logging_dir = path + 'logs/', # Папка для предупреждений
load_best_model_at_end = True, # Флаг загрузки лучшей модели после завершения обучения
logging_steps = 500, # Шаг сохранения весов (checkpoint)
evaluation_strategy = 'steps' # Стратегия обучения
)
model = BertForSequenceClassification. \
from_pretrained(model_name, num_labels = num_classes).to(device)
trainer = Trainer(
model = model,
args = training_args,
train_dataset = trn_dataset,
eval_dataset = tst_dataset,
compute_metrics = compute_metrics
)
trainer.train() # Обучение
trainer.evaluate() # Оценка
# Сохраняем модель в папку fn_m
model.save_pretrained(fn_m) # pytorch_model.bin & config.json
elif step == 2: # Оценка модели
def get_prediction(text):
inputs = tokenizer(text, padding = True, truncation = True,
max_length = max_length, return_tensors = 'pt').to(device)
outputs = model(**inputs)
probs = outputs[0].softmax(1)
return cls_dict[probs.argmax()]
doc = 'На прошлой неделе в финском городе Куусамо стартовал Кубок мира \
по прыжкам на лыжах с трамплина. Нашу сборную повел в бой \
Вольфганг Штайерт - титулованный немецкий тренер, приглашенный \
в прошлом году в Россию.'
doc = preprocess_s(doc)
from transformers import BertConfig
config = BertConfig.from_json_file(fn_m + '/config.json')
model = BertForSequenceClassification. \
from_pretrained(fn_m + '/pytorch_model.bin', config = config).to(device)
# checkpoint = torch.load(fn_m + '/pytorch_model.bin')
# for k in checkpoint.keys(): print(k)
cls_dict = list(cls_dict.items())
print(get_prediction(doc)) # ('#спорт', 6)
n_true = 0
for doc, lbl in zip(tst_docs, tst_labels):
res = get_prediction(doc)
if res[1] == lbl:
n_true += 1
acc = n_true / len(tst_labels)
print(100 * round(acc, 4))