Список работ

Настройка BERT-классификатора

Содержание

Введение

Предобученная нейронная сеть (НС) 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

Возможные результаты обучения:

StepTraining LossValidation LossAccuracy
5000.9294000.5334550.867647
10000.2630000.5800130.886765
15000.0803000.5545510.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))

Список работ

Рейтинг@Mail.ru