Навчальний посібник: Тонке налаштування BERT для аналізу настроїв

Навчальний посібник: Тонке налаштування BERT для аналізу настроїв

    

Вперше опубліковано дослідником машинного навчання Skim AI, Крісом Траном.



BERT_для_аналізу_настроїв





A - Вступ

Останніми роками спільнота NLP побачила багато проривних ідей в обробці природної мови, особливо перехід до трансферного навчання. Такі моделі, як ELMo, ULMFiT від fast.ai, Transformer і GPT від OpenAI, дозволили дослідникам досягти найсучасніших результатів у багатьох тестах і надали спільноті великі попередньо навчені моделі з високою продуктивністю. Цей зсув у НЛП розглядається як момент ImageNet, зсув у комп'ютерному зорі, що стався кілька років тому, коли нижчі шари мереж глибокого навчання з мільйонами параметрів, навчених на конкретному завданні, можна повторно використовувати і допрацьовувати для інших завдань, замість того, щоб навчати нові мережі з нуля.

Однією з найважливіших віх в еволюції НЛП за останній час став випуск Google's BERT, який описують як початок нової ери в НЛП. У цьому блокноті я буду використовувати програму HuggingFace трансформатори для точного налаштування попередньо навченої BERT-моделі для задачі класифікації. Потім я порівняю продуктивність BERT з базовою моделлю, в якій я використовую векторизатор TF-IDF і класифікатор Naive Bayes. У цьому розділі я покажу, як працює трансформатори бібліотека допомагає нам швидко та ефективно налаштовувати найсучаснішу BERT-модель та отримувати точність 10% вище, ніж у базовій моделі.

Посилання:

Щоб зрозуміти Трансформатор (архітектура, на якій побудовано BERT) і дізнатися, як реалізувати BERT, я настійно рекомендую прочитати наступні джерела:

B - Налаштування

1. Завантаження основних бібліотек

У [0]:

import os
import re
from tqdm import tqdm
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

2. Набір даних

2.1. Завантажити набір даних

У [0]:

# Завантажити дані
імпорт запитів
request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf")
з open("data.zip", "wb") як файл:
    file.write(request.content)
# Розархівувати дані
імпортуємо zip-файл
with zipfile.ZipFile('data.zip') as zip:
    zip.extractall('data')

2.2. Дані про вантажний потяг

Дані поїздів складаються з 2 файлів, кожен з яких містить 1700 твітів зі скаргами/нескаргами. Кожен твіт у даних містить принаймні хештег авіакомпанії.

Ми завантажимо дані поїздів і позначимо їх. Оскільки ми використовуємо тільки текстові дані для класифікації, ми відкинемо неважливі стовпці і залишимо тільки ідентифікатор, твітнути і етикетка колонки.

У [0]:

 # Завантажити дані та встановити мітки
data_complaint = pd.read_csv('data/complaint1700.csv')
data_complaint['label'] = 0
data_non_complaint = pd.read_csv('data/noncomplaint1700.csv')
data_non_complaint['label'] = 1
# Конкатенація даних зі скаргою та без скарги
data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True)
# Опускаємо 'авіакомпанія' стовпець
data.drop(['airline'], inplace=True, axis=1)
# Відобразити 5 випадкових вибірок
data.sample(5)

Out[0]:

ідентифікатортвітнутиетикетка
198824991Яке чудове повернення. Смішно. Депланін...1
129472380Дуже розчарований @JetBlue сьогодні ввечері. Рейс...0
1090127893@united мої друзі мають пекельний час...0
55358278@united все, що я хочу на Різдво - це загублений мішок, який...0
207530695Так, не буду брехати... мені дуже цікаво спробувати...1

Ми випадковим чином розділимо всі навчальні дані на два набори: навчальний набір з 90% даних та валідаційний набір з 10% даних. Ми виконаємо налаштування гіперпараметрів за допомогою перехресної перевірки на тренувальному наборі та використаємо валідаційний набір для порівняння моделей.

У [0]:

from sklearn.model_selection import train_test_split
X = data.tweet.values
y = data.label.values
X_train, X_val, y_train, y_val =
    train_test_split(X, y, test_size=0.1, random_state=2020)

2.3. Дані навантажувального тесту

Дані тесту містять 4555 прикладів без маркування. Близько 300 прикладів - це твіти, що не відповідають вимогам. Наше завдання - виявити їхні ідентифікатор і перевіряємо вручну, чи правильні наші результати.

У [0]:

# Завантажити дані навантажувального тесту
test_data = pd.read_csv('data/test_data.csv')
# Зберегти важливі стовпці
test_data = test_data[['id', 'tweet']]].
# Вивести 5 вибірок з тестових даних
test_data.sample(5)

Out[0]:

ідентифікатортвітнути
153959336Рейс @AmericanAir затримується більш ніж на 2 години через ...
60724101@SouthwestAir Все ще отримую це повідомлення про помилку...
33313179чекаю на #SeaTac, щоб сісти на свій рейс @JetBlue...
2696102948Ненавиджу, коли я проходжу через попередній вибір місця заздалегідь...
3585135638як вам не соромно @AlaskaAir

3. Налаштуйте графічний процесор для навчання

Google Colab пропонує безкоштовні GPU та TPU. Оскільки ми будемо навчати велику нейронну мережу, найкраще використовувати ці можливості.

Графічний процесор можна додати, перейшовши в меню і вибравши його:

Виконання -> Змінити тип виконання -> Апаратний прискорювач: ГРАФІЧНИЙ ПРОЦЕСОР

Потім нам потрібно запустити наступну комірку, щоб вказати графічний процесор як пристрій.

У [0]:

import torch
if torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'Доступно {torch.cuda.device_count()} графічних процесорів.')
    print('Назва пристрою:', torch.cuda.get_device_name(0))
else:
    print('Графічний процесор відсутній, замість нього використовується центральний процесор.')
    device = torch.device("cpu")
Доступно 1 графічний(і) процесор(и).
Назва пристрою: Tesla T4

C - Базовий рівень: TF-IDF + Наївний класифікатор Байєса¶

У цьому базовому підході спочатку ми використаємо TF-IDF для векторизації наших текстових даних. Потім ми використаємо наївну модель Байєса як наш класифікатор.

Чому саме наївний Байєс? Я випробував різні алгоритми машинного навчання, включаючи Random Forest, Machine of Support Vectors, XGBoost, і помітив, що Naive Bayes дає найкращі результати. У Посібник Scikit-learn Щоб вибрати правильну оцінку, також пропонується використовувати Naive Bayes для текстових даних. Я також спробував використати SVD для зменшення розмірності, але це не дало кращих результатів.

1. Підготовка даних

1.1. Попередня обробка

У моделі "мішок слів" текст представляється як мішок слів, без урахування граматики та порядку слів. Тому нам потрібно буде видалити стоп-слова, розділові знаки та символи, які не роблять значного внеску в зміст речення.

У [0]:

імпортувати nltk
# Видалити коментар для завантаження "стоп-слів"
nltk.download("stopwords")
з nltk.corpus import stopwords
def text_preprocessing(s):
    """
    - Зробіть речення маленькими літерами
    - Замініть "'t" на "not"
    - Видаліть "@name"
    - Ізолювати та видалити знаки пунктуації, крім "?"
    - Видалити інші спеціальні символи
    - Видалити стоп-слова, окрім "not" та "can"
    - Видалення кінцевих пробілів
    """
    s = s.lower()
    # Замінити 't на 'not'
    s = re.sub(r"'t", " not", s)
    # Видалити @name
    s = re.sub(r'(@.*?)[s]', ' ', s)
    # Виділити та видалити розділові знаки, крім '?'
    s = re.sub(r'(['".()!?\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ', s)
    # Видалити деякі спеціальні символи
    s = re.sub(r'([;:|-"n])', ' ', s)
    # Видалити стоп-слова, крім 'not' та 'can';
    s = " ".join([слово за словом в s.split()
                  if word not in stopwords.words('english')
                  або слово в ['not', 'can']])
    # Видалення кінцевих пробілів
    s = re.sub(r's+', ' ', s).strip()
    повернути s
[nltk_data] Завантаження стоп-слів пакунків до /root/nltk_data...
[nltk_data] Стоп-слова пакунків вже оновлено!

1.2. Векторизатор TF-IDF

У пошуку інформації, TF-IDFскорочено від термін частота-обернена частота документаце числова статистика, яка має на меті відобразити, наскільки важливим є слово для документа в колекції або корпусі. Ми використаємо TF-IDF для векторизації наших текстових даних перед тим, як подати їх алгоритмам машинного навчання.

У [0]:

%%time
from sklearn.feature_extraction.text import TfidfVectorizer
# Попередньо обробити текст
X_train_preprocessed = np.array([text_preprocessing(text) for text in X_train])
X_val_preprocessed = np.array([text_preprocessing(text) for text in X_val])
# Обчислити TF-IDF
tf_idf = TfidfVectorizer(ngram_range=(1, 3),
                         binary=True,
                         smooth_idf=False)
X_train_tfidf = tf_idf.fit_transform(X_train_preprocessed)
X_val_tfidf = tf_idf.transform(X_val_preprocessed)
Час роботи процесора: користувач 5.47 с, sys: 519 мс, всього: 5.99 с
Час роботи на стіні: 6 с

2. Навчити наївний класифікатор Байєса

2.1. Налаштування гіперпараметрів

Ми будемо використовувати перехресну перевірку та показник AUC для налаштування гіперпараметрів нашої моделі. Функція get_auc_CV поверне середній показник AUC за результатами перехресної перевірки.

У [0]:

from sklearn.model_selection import StratifiedKFold, cross_val_score
def get_auc_CV(model):
    """
    Повернути середнє значення AUC з перехресної перевірки.
    """
    # Встановити KFold для перемішування даних перед розбиттям
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # Отримання оцінок AUC
    auc = cross_val_score(
        model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf)
    return auc.mean()

У "The MultinominalNB мають лише один гіптерпараметр - альфа. Наведений нижче код допоможе нам знайти значення альфа, яке дасть нам найвищий показник CV AUC.

У [0]:

from sklearn.naive_bayes import MultinomialNB
res = pd.Series([get_auc_CV(MultinomialNB(i))
                 for i in np.arange(1, 10, 0.1)],
                index=np.arange(1, 10, 0.1))
best_alpha = np.round(res.idxmax(), 2)
print('Найкраща альфа: ', best_alpha)
plt.plot(res)
plt.title('AUC проти альфа')
plt.xlabel('Alpha')
plt.ylabel('AUC')
plt.show()
Найкращий альфа:  1.3

2.2. Оцінювання на валідаційному наборі

Щоб оцінити ефективність нашої моделі, ми розрахуємо показник точності та оцінку AUC нашої моделі на валідаційному наборі.

У [0]:

from sklearn.metrics import accuracy_score, roc_curve, auc
def evaluate_roc(probs, y_true):
    """
    - Вивести AUC і точність на тестовому наборі
    - Побудувати графік ROC
    @params probs (np.array): масив прогнозованих ймовірностей з формою (len(y_true), 2)
    @params y_true (np.array): масив істинних значень з формою (len(y_true),)
    """
    preds = probs[:, 1]
    fpr, tpr, threshold = roc_curve(y_true, preds)
    roc_auc = auc(fpr, tpr)
    print(f'AUC: {roc_auc:.4f}')
    # Отримати точність на тестовому наборі
    y_pred = np.where(preds >= 0.5, 1, 0)
    accuracy = accuracy_score(y_true, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # Графік ROC AUC
    plt.title('Робоча характеристика приймача')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = 'нижній правий кут')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('True Positive Rate')
    plt.xlabel('Кількість хибнопозитивних спрацьовувань')
    plt.show()

Поєднуючи TF-IDF і алгоритм Naive Bayes, ми досягаємо точності 72.65% на валідаційному наборі. Це значення є базовою продуктивністю і буде використовуватися для оцінки продуктивності нашої тонко налаштованої BERT-моделі.

У [0]:

# Обчислення прогнозованих ймовірностей
nb_model = MultinomialNB(alpha=1.8)
nb_model.fit(X_train_tfidf, y_train)
probs = nb_model.predict_proba(X_val_tfidf)
# Оцінити класифікатор
evaluate_roc(probs, y_val)
AUC: 0.8451
Точність: 75.59%

D - Точне налаштування BERT

1. Встановіть бібліотеку облич, що обіймаються

Бібліотека трансформаторів Hugging Face містить PyTorch реалізацію найсучасніших моделей NLP, включаючи BERT (від Google), GPT (від OpenAI) ... та попередньо навчені ваги моделей.

В [1]:

#!pip встановлюють трансформатори

2. Токенізація та форматування вхідних даних

Перш ніж токенізувати наш текст, ми виконаємо невелику обробку нашого тексту, включаючи видалення згадок про сутності (наприклад, @united) і деяких спеціальних символів. Рівень обробки тут набагато менший, ніж у попередніх підходах, оскільки BERT навчався на цілих реченнях.

У [0]:

def text_preprocessing(text):
    """
    - Видаліть згадки про сутності (наприклад, '@united')
    - Виправити помилки (наприклад, '&' на '&')
    @param text (str): рядок для обробки.
    @return text (Str): оброблений рядок.
    """
    # Видалити '@name'
    text = re.sub(r'(@.*?)[s]', ' ', text)
    # Замінити '&' на '&'
    text = re.sub(r'&', '&', text)
    # Видалити кінцеві пробіли
    text = re.sub(r's+', ' ', text).strip()
    повернути текст

У [0]:

# Вивести речення 0
print('Оригінал: ', X[0])
print('Оброблено: ', text_preprocessing(X[0]))
Оригінал:  @united У мене проблеми. Вчора я перебронював квиток на 24 години після того, як мав летіти, а тепер не можу зайти на сайт і зареєструватися. Чи можете ви мені допомогти?
Оброблено:  У мене проблеми. Вчора я перебронював квиток на 24 години після того, як мав летіти, а тепер не можу зайти на сайт і зареєструватися. Ви можете мені допомогти?

2.1. BERT Tokenizer

Для того, щоб застосувати попередньо навчений BERT, ми повинні використовувати токенізатор, який надається бібліотекою. Це пов'язано з тим, що (1) модель має певний, фіксований словник і (2) токенізатор BERT має особливий спосіб обробки слів, що не входять до словника.

Крім того, ми повинні додати спеціальні лексеми на початку та в кінці кожного речення, розбити та скоротити всі речення до однієї постійної довжини, а також явно вказати, що таке лексеми розбиття з "маскою уваги".

У "The encode_plus метод BERT токенізатора will:

(1) розбиваємо наш текст на токени,

(2) додайте спеціальний [CLS] і [SEP] токени та

(3) перетворити ці токени в індекси словника токенізатора,

(4) розбийте або скоротіть речення до максимальної довжини, та

(5) Створіть маску уваги.

У [0]:

з трансформаторів імпортувати BertTokenizer
# Завантажити токенізатор BERT
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# Створіть функцію для токенізації набору текстів
def preprocessing_for_bert(data):
    """Виконати необхідні кроки попередньої обробки для попередньо навченого BERT.
    @param data (np.array): Масив текстів для обробки.
    @return input_ids (torch.Tensor): Тензор ідентифікаторів токенів для подачі в модель.
    @return attention_masks (torch.Tensor): Тензор індексів, що визначають, які
                  токени повинні бути розглянуті моделлю.
    """
    # Створити порожні списки для зберігання виходів
    input_ids = []
    attention_masks = []
    # Для кожного речення...
    для надісланих даних:
        # encode_plus буде:
        # (1) Токенізувати речення
        # (2) Додайте [CLS] і [SEP] маркер на початок і кінець
        # (3) Обрізати/вставити речення до максимальної довжини
        # (4) Зіставити токени з їхніми ідентифікаторами
        # (5) Створити маску уваги
        # (6) Повернути словник виходів
        encoded_sent = tokenizer.encode_plus(
            text=text_preprocessing(sent), # Попередньо обробити речення
            add_special_tokens=True, # Додати [CLS] і [SEP]
            max_length=MAX_LEN, # Максимальна довжина для усікання/вставки
            pad_to_max_length=True, # Вставити речення до максимальної довжини
            #return_tensors='pt', # Повернути тензор PyTorch
            return_attention_mask=True # Повернути маску уваги
            )
        # Додати виходи до списків
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # Перетворення списків у тензори
    input_ids = torch.tensor(input_ids)
    attention_masks = torch.tensor(attention_masks)
    повернути input_ids, attention_masks

Перед токенізацією нам потрібно вказати максимальну довжину наших речень.

У [0]:

# Конкатенація даних поїздів та тестових даних
all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values])
# Кодуємо наші об'єднані дані
encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets].
# Знайти максимальну довжину
max_len = max([len(sent) для sent в encoded_tweets])
print('Максимальна довжина: ', max_len)
Максимальна довжина: 68

Тепер давайте токенізуємо наші дані.

У [0]:

# Вказати MAX_LEN
MAX_LEN = 64
# Вивести речення 0 та його закодовані ідентифікатори токенів
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('Original: ', X[0])
print('Ідентифікатори токенів: ', token_ids)
# Функція запуску preprocessing_for_bert на тренувальному наборі та валідаційному наборі
print('Токенізація даних...')
train_inputs, train_masks = preprocessing_for_bert(X_train)
val_inputs, val_masks = preprocessing_for_bert(X_val)
Оригінал:  @united У мене проблеми. Вчора я перебронював квиток на 24 години після того, як мав летіти, а тепер не можу зайти на сайт і зареєструватися. Чи можете ви допомогти?
Ідентифікатори токенів:  [101, 1045, 1005, 1049, 2383, 3314, 1012, 7483, 1045, 2128, 8654, 2098, 2005, 2484, 2847, 2044, 1045, 2001, 4011, 2000, 4875, 1010, 2085, 1045, 2064, 1005, 1056, 8833, 2006, 1004, 4638, 1999, 1012, 2064, 2017, 2393, 1029, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Токенізація даних...

2.2. Створення PyTorch DataLoader

Ми створимо ітератор для нашого набору даних за допомогою класу torch DataLoader. Це допоможе заощадити пам'ять під час навчання та підвищити швидкість навчання.

У [0]:

з torch.utils.data імпортувати TensorDataset, DataLoader, RandomSampler, SequentialSampler
# Перетворення інших типів даних у torch.Tensor
train_labels = torch.tensor(y_train)
val_labels = torch.tensor(y_val)
# Для точного налаштування BERT автори рекомендують розмір партії 16 або 32.
batch_size = 32
# Створюємо DataLoader для нашого навчального набору
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
# Створюємо DataLoader для нашого валідаційного набору
val_data = TensorDataset(val_inputs, val_masks, val_labels)
val_sampler = SequentialSampler(val_data)
val_dataloader = DataLoader(val_data, sampler=val_sampler, batch_size=batch_size)

3. Тренуємо нашу модель

3.1. Створення BertClassifier

BERT-база складається з 12 шарів-трансформаторів, кожен шар-трансформатор отримує список вкладок токенів і на виході видає однакову кількість вкладок з однаковим прихованим розміром (або розмірами). На виході останнього шару-трансформатора [CLS] токен використовується як ознаки послідовності для подачі на класифікатор.

У "The трансформатори бібліотека має BertForSequenceClassification який призначений для задач класифікації. Однак ми створимо новий клас, щоб мати змогу вказати власний вибір класифікаторів.

Нижче ми створимо клас BertClassifier з BERT-моделлю для вилучення останнього прихованого шару [CLS] та одношарову нейронну мережу прямого поширення в якості класифікатора.

У [0]:

%%time
import torch
import torch.nn as nn
from transformers import BertModel
# Створити клас BertClassfier
class BertClassifier(nn.Module):
    """Модель Берта для задач класифікації.
    """
    def __init__(self, freeze_bert=False):
        """
        @param bert: об'єкт BertModel
        @param classifier: класифікатор torch.nn.Module
        @param freeze_bert (bool): Set Неправда. для точного налаштування моделі BERT
        """
        super(BertClassifier, self).__init__()
        # Вказати прихований розмір BERT, прихований розмір нашого класифікатора та кількість міток
        D_in, H, D_out = 768, 50, 2
        # Інстанціюємо модель BERT
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # Створити одношаровий класифікатор прямого поширення
        self.classifier = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Linear(H, D_out)
        )
        # Заморозити BERT модель
        if freeze_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        Передати вхідні дані до BERT та класифікатора для обчислення логів.
        @param input_ids (torch.Tensor): вхідний тензор з формою (batch_size,
                      max_length)
        @param attention_mask (torch.Tensor): тензор, що зберігає маску уваги
                      інформацію з формою (batch_size, max_length)
        @return logits (torch.Tensor): вихідний тензор з формою (batch_size,
                      num_labels)
        """
        # Передати вхідні дані у BERT
        outputs = self.bert(input_ids=input_ids,
                            маска_уваги=маска_уваги)
        # Витягнути останній прихований стан токена [CLS] для задачі класифікації
        last_hidden_state_cls = outputs[0][:, 0, :]
        # Передати вхідні дані класифікатору для обчислення логів
        logits = self.classifier(last_hidden_state_cls)
        повернути logits
Процесорний час: користувач 38 мкс, сист: 0 нс, всього: 38 мкс
Час роботи стіни 40.1 мкс

3.2. Оптимізатор та планувальник швидкості навчання

Щоб точно налаштувати наш класифікатор Берта, нам потрібно створити оптимізатор. Автори рекомендують дотримуватися наступних гіпер-параметрів:

  • Розмір партії: 16 або 32
  • Швидкість навчання (Адам): 5e-5, 3e-5 або 2e-5
  • Кількість епох: 2, 3, 4

Huggingface надав run_glue.py скрипт, приклади реалізації трансформатори бібліотеки. У скрипті використовується оптимізатор AdamW.

У [0]:

from transformers import AdamW, get_linear_schedule_with_warmup
def initialize_model(epochs=4):
    """Ініціалізувати класифікатор Берта, оптимізатор та планувальник швидкості навчання.
    """
    # Створити класифікатор Берта
    bert_classifier = BertClassifier(freeze_bert=False)
    # Вказати PyTorch запустити модель на GPU
    bert_classifier.to(device)
    # Створюємо оптимізатор
    optimizer = AdamW(bert_classifier.parameters(),
                      lr=5e-5, # Швидкість навчання за замовчуванням
                      eps=1e-8 # Значення епсилон за замовчуванням
                      )
    # Загальна кількість кроків навчання
    total_steps = len(train_dataoader) * epochs
    # Налаштування планувальника швидкості навчання
    scheduler = get_linear_schedule_with_warmup(optimizer,
                                                num_warmup_steps=0, # Значення за замовчуванням
                                                num_training_steps=total_steps)
    повернути bert_classifier, оптимізатор, планувальник

3.3. Цикл навчання

Ми будемо навчати наш класифікатор Берта для 4 епох. У кожну епоху ми будемо навчати нашу модель та оцінювати її роботу на валідаційному наборі. Детальніше про це ми розповімо далі:

Тренування:

  • Розпакуйте наші дані з завантажувача даних і завантажте їх на графічний процесор
  • Обнулити градієнти, розраховані на попередньому проході
  • Виконати прямий прохід для обчислення логічних помилок і втрат
  • Виконайте зворотний прохід для обчислення градієнтів (loss.backward())
  • Обмежте норму градієнтів до 1.0, щоб запобігти "вибуховим градієнтам"
  • Оновлення параметрів моделі (optimizer.step())
  • Оновити швидкість навчання (scheduler.step())

Оцінка:

  • Розпакуйте наші дані та завантажте на графічний процесор
  • Пас вперед
  • Обчисліть втрати та рівень точності для валідаційного набору

Сценарій нижче коментується з деталями нашого циклу навчання та оцінювання.

У [0]:

import random
import time
# Specify loss function
loss_fn = nn.CrossEntropyLoss()
def set_seed(seed_value=42):
    """Set seed for reproducibility.
    """
    random.seed(seed_value)
    np.random.seed(seed_value)
    torch.manual_seed(seed_value)
    torch.cuda.manual_seed_all(seed_value)
def train(model, train_dataloader, val_dataloader=None, epochs=4, evaluation=False):
    """Train the BertClassifier model.
    """
    # Start training loop
    print("Start training...n")
    for epoch_i in range(epochs):
        # =======================================
        #               Training
        # =======================================
        # Print the header of the result table
        print(f"{'Epoch':^7} | {'Batch':^7} | {'Train Loss':^12} | {'Val Loss':^10} | {'Val Acc':^9} | {'Elapsed':^9}")
        print("-"*70)
        # Measure the elapsed time of each epoch
        t0_epoch, t0_batch = time.time(), time.time()
        # Reset tracking variables at the beginning of each epoch
        total_loss, batch_loss, batch_counts = 0, 0, 0
        # Put the model into the training mode
        model.train()
        # For each batch of training data...
        for step, batch in enumerate(train_dataloader):
            batch_counts +=1
            # Load batch to GPU
            b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
            # Zero out any previously calculated gradients
            model.zero_grad()
            # Perform a forward pass. This will return logits.
            logits = model(b_input_ids, b_attn_mask)
            # Compute loss and accumulate the loss values
            loss = loss_fn(logits, b_labels)
            batch_loss += loss.item()
            total_loss += loss.item()
            # Perform a backward pass to calculate gradients
            loss.backward()
            # Clip the norm of the gradients to 1.0 to prevent "exploding gradients"
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            # Update parameters and the learning rate
            optimizer.step()
            scheduler.step()
            # Print the loss values and time elapsed for every 20 batches
            if (step % 20 == 0 and step != 0) or (step == len(train_dataloader) - 1):
                # Calculate time elapsed for 20 batches
                time_elapsed = time.time() - t0_batch
                # Print training results
                print(f"{epoch_i + 1:^7} | {step:^7} | {batch_loss / batch_counts:^12.6f} | {'-':^10} | {'-':^9} | {time_elapsed:^9.2f}")
                # Reset batch tracking variables
                batch_loss, batch_counts = 0, 0
                t0_batch = time.time()
        # Calculate the average loss over the entire training data
        avg_train_loss = total_loss / len(train_dataloader)
        print("-"*70)
        # =======================================
        #               Evaluation
        # =======================================
        if evaluation == True:
            # After the completion of each training epoch, measure the model's performance
            # on our validation set.
            val_loss, val_accuracy = evaluate(model, val_dataloader)
            # Print performance over the entire training data
            time_elapsed = time.time() - t0_epoch
            print(f"{epoch_i + 1:^7} | {'-':^7} | {avg_train_loss:^12.6f} | {val_loss:^10.6f} | {val_accuracy:^9.2f} | {time_elapsed:^9.2f}")
            print("-"*70)
        print("n")
    print("Training complete!")
def evaluate(model, val_dataloader):
    """After the completion of each training epoch, measure the model's performance
    on our validation set.
    """
    # Put the model into the evaluation mode. The dropout layers are disabled during
    # the test time.
    model.eval()
    # Tracking variables
    val_accuracy = []
    val_loss = []
    # For each batch in our validation set...
    for batch in val_dataloader:
        # Load batch to GPU
        b_input_ids, b_attn_mask, b_labels = tuple(t.to(device) for t in batch)
        # Compute logits
        with torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        # Compute loss
        loss = loss_fn(logits, b_labels)
        val_loss.append(loss.item())
        # Get the predictions
        preds = torch.argmax(logits, dim=1).flatten()
        # Calculate the accuracy rate
        accuracy = (preds == b_labels).cpu().numpy().mean() * 100
        val_accuracy.append(accuracy)
    # Compute the average accuracy and loss over the validation set.
    val_loss = np.mean(val_loss)
    val_accuracy = np.mean(val_accuracy)
    return val_loss, val_accuracy

А тепер давайте почнемо тренувати наш BertClassifier!

У [0]:

set_seed(42) # Встановити seed для відтворюваності
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)
Починайте тренування...

 Epoch | Batch | Train Loss | Val Loss | Val Acc | Минуло
----------------------------------------------------------------------
   1 | 20 | 0.630467 | - | - | 7.58
   1 | 40 | 0.497330 | - | - | 7.01
   1 | 60 | 0.502320 | - | - | 7.11
   1 | 80 | 0.491438 | - | - | 7.19
   1 | 95 | 0.486125 | - | - | 5.35
----------------------------------------------------------------------
   1 | - | 0.524515 | 0.439601 | 78.81 | 35.54
----------------------------------------------------------------------


 Epoch | Batch | Train Loss | Val Loss | Val Acc | Минуло
----------------------------------------------------------------------
   2 | 20 | 0.287401 | - | - | 7.83
   2 | 40 | 0.260870 | - | - | 7.60
   2 | 60 | 0.287706 | - | - | 7.67
   2 | 80 | 0.283311 | - | - | 7.87
   2 | 95 | 0.280315 | - | - | 5.87
----------------------------------------------------------------------
   2 | - | 0.279978 | 0.454067 | 80.40 | 38.31
----------------------------------------------------------------------


Навчання завершено!

3.4. Оцінювання на валідаційному наборі

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

У [0]:

import torch.nn.functional as F
def bert_predict(model, test_dataoader):
    """Виконати прямий прохід по навченій BERT-моделі для передбачення ймовірностей
    на тестовому наборі.
    """
    # Перевести модель у режим оцінювання. Шари, що випадають, вимкнено на час
    # часу тестування.
    model.eval()
    all_logits = []
    # Для кожної партії у нашому тестовому наборі...
    для batch у test_dataoader:
        # Завантажуємо пакет на GPU
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2].
        # Обчислення логів
        за допомогою torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # Конкатенація логів з кожного пакету
    all_logits = torch.cat(all_logits, dim=0)
    # Застосування softmax для обчислення ймовірностей
    probs = F.softmax(all_logits, dim=1).cpu().numpy()
    повернути probs

У [0]:

# Обчислити прогнозовані ймовірності на тестовому наборі
probs = bert_predict(bert_classifier, val_dataloader)
# Оцінити класифікатор Берта
evaluate_roc(probs, y_val)
AUC: 0.9048
Точність: 80.59%

На валідаційному наборі класифікатор Берта досягає 0,90 AUC і 82,65% точності. Цей результат на 10 пунктів кращий за базовий метод.

3.5. Тренуємо нашу модель на всіх навчальних даних

У [0]:

# Конкатенація набору даних поїздів та набору валідації
full_train_data = torch.utils.data.ConcatDataset([train_data, val_data])
full_train_sampler = RandomSampler(full_train_data)
full_train_dataloader = DataLoader(full_train_data, sampler=full_train_sampler, batch_size=32)
# Навчити класифікатор Берта на всіх навчальних даних
set_seed(42)
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, full_train_dataoader, epochs=2)
Починайте тренування...

 Epoch | Batch | Train Loss | Val Loss | Val Acc | Минуло
----------------------------------------------------------------------
   1 | 20 | 0.664452 | - | - | 8.63
   1 | 40 | 0.587205 | - | - | 8.42
   1 | 60 | 0.522831 | - | - | 8.44
   1 | 80 | 0.476442 | - | - | 8.23
   1 | 100 | 0.467542 | - | - | 8.10
   1 | 106 | 0.483039 | - | - | 2.14
----------------------------------------------------------------------


 Епоха | Партія | Втрати поїздів | Втрати поїздів | Втрати поїздів | Втрати поїздів | Втрати поїздів | Втрати поїздів
----------------------------------------------------------------------
   2 | 20 | 0.338174 | - | - | 8.36
   2 | 40 | 0.296080 | - | - | 7.93
   2 | 60 | 0.295626 | - | - | 7.96
   2 | 80 | 0.277470 | - | - | 7.99
   2 | 100 | 0.314746 | - | - | 8.07
   2 | 106 | 0.293359 | - | - | 2.17
----------------------------------------------------------------------


Навчання завершено!

4. Прогнози на тестовому наборі

4.1. Підготовка даних

Давайте повернемося до нашого тестового набору незабаром.

У [0]:

test_data.sample(5)

Out[0]:

ідентифікатортвітнути
47118654Друзі та родина: Ніколи не літайте @JetBlue. Абсолютно...
197176265@DeltaAssist @rogerioad У мене ніколи не було про.
23672Перший політ за кілька тижнів. Розраховую на вас, американці...
2702103263"USAirways: "Ви знаєте, що ми не можемо залишатися без ...
1355137@southwestair Тут, в аеропорту Південної Африки, ми спостерігаємо за ...

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

У [0]:

# Run preprocessing_for_bert на тестовому наборі
print('Токенізація даних...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Створюємо DataLoader для нашого тестового набору
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
Токенізація даних...

4.2. Прогнози

У нашому тестовому наборі є близько 300 твітів з позитивною оцінкою. Тому ми продовжимо коригувати поріг прийняття рішення, доки не матимемо близько 300 твітів з позитивним значенням.

Поріг, який ми будемо використовувати, становить 0,992, що означає, що твіти з прогнозованою ймовірністю, більшою за 99,2%, будуть вважатися позитивними. Це дуже високе значення порівняно з порогом за замовчуванням 0,5.

Після ручного дослідження тестового набору я виявив, що завдання класифікації настроїв тут навіть складне для людини. Тому високий поріг дасть нам безпечні прогнози.

У [0]:

# Обчислити прогнозовані ймовірності на тестовому наборі
probs = bert_predict(bert_classifier, test_dataoader)
# Отримати передбачення з ймовірностей
threshold = 0.9
preds = np.where(probs[:, 1] > threshold, 1, 0)
# Кількість твітів, передбачених невід'ємними
вивести("Кількість твітів, для яких передбачено невід'ємне значення: ", preds.sum())
Кількість твітів, що не є негативними: 454

Тепер ми розглянемо 20 випадкових твітів з наших прогнозів. 17 з них виявилися правильними, що свідчить про те, що точність класифікатора BERT становить близько 0,85.

У [0]:

output = test_data[preds==1]
list(output.sample(20).tweet)

Out[0]:

["@Delta @DeltaAssist Delta знову завдає удару. Sky lounge в найбільш завантаженому аеропорту країни закритий на вихідних. Тупо і дешево. Сумую за тобою @united",
 @SouthwestAir привезли смажений арахіс в меду. Чи сумно, що це усвідомлення може бути найвищою точкою мого дня? #SmallThingsInLife',
 '@DeltaAssist Я написала електронною поштою kana@delta і contactus.delta@delta, щоб вирішити питання два тижні тому, але не отримала відповіді. Порадьте, до кого звернутися?',
 "Жінка, яку зняли з рейсу @AlaskaAir через рак, планує пожертвувати вартість авіаквитків для своєї сім'ї http://t.co/Uj6rispWLb",
 "@united (2/2) Я не розбивав сумку. Якби мені не довелося платити за її перевірку, я б не так засмутилася. Краще б летіла @AmericanAir, @SouthwestAir і т.д.",
 "Я літав майже всіма авіакомпаніями і ніколи не мав кращого досвіду, ніж з JetBlue. Якість, сервіс, комфорт та доступність. A++",
 "@JetBlue - найкраща авіакомпанія, щоб працювати з вами, міс У багато #keepingitminty ',
 Переконав @firetweet забронювати поїздку в останню хвилину, щоб приєднатися до мене в Остіні завтра! З тих пір співаю пісню про безпеку @VirginAmerica. Бідний Ерік,
 @AmericanAir терпляче чекає, щоб злетіти з #DFW до #ord http://t.co/j1oDSc6fht',
 'О, @JetBlue, сьогодні сумний день для лоялістів B6. Я знаю, що ви рекламуєте свої нові "опції", але ваші послуги/безкоштовне перевезення багажу - це те, що робить вас чудовими',
 Що хорошого в цьому рейсі: @Gogo та чудові враження від @VirginAmerica. Не дуже добре: дитяча блювота/запах тухлого тунця',
 '@USAirways @AmericanAir сумуватиме за USAir :(',
 '@altonbrown @united Час переходити на @VirginAmerica',
 #39;Ніколи не буває поганого часу для Чобані, @AmericanAir Admirals Club! #Був побитий рекорд #Багато хто з'їв під час обіду #39;,
 "Під час польоту я вкрав у людини телефон, щоб випробувати новий потоковий IFE від @alaskaair. Це добре! Шкода, що у неї немає iPad",
 "Не можу дочекатися, коли завершиться злиття @USAirways і @AmericanAir, який клопіт для клієнтів!", "Не можу дочекатися, коли завершиться злиття @USAirways і @AmericanAir!", "Яка морока для клієнтів!",
 "@JetBlue я студент-бідняк, тому $150 - це величезна сума",
 "Не можу дочекатися, коли повернуся до району затоки сьогодні ввечері на рейсі 2256!!!! авіакомпанії @SouthwestAir",
 "Висимо на #SFO в очікуванні туману для наступного рейсу @VirginAmerica на #sxsw!", "Не можу дочекатися наступного рейсу @VirginAmerica на #sxsw!", "Не можу дочекатися наступного рейсу на #sxsw! #SXSW2015 #Austin',
 "@DeltaAssist у будь-якому випадку я можу пересісти з рейсу 1308 на рейс, який не затримується на невизначений термін..... І повернутися до Вашингтона!"].

E - Висновок

Додавши до BERT простий одношаровий нейромережевий класифікатор з прихованим шаром і доопрацювавши BERT, ми можемо досягти майже найсучаснішої продуктивності, яка на 10 пунктів краща за базовий метод, хоча у нас є лише 3400 точок даних.

Крім того, хоча BERT дуже великий, складний і має мільйони параметрів, нам потрібно лише доопрацювати його за 2-4 епохи. Такого результату можна досягти тому, що BERT був навчений на величезному обсязі і вже закодований у великій кількості інформації про нашу мову. Вражаюча продуктивність, досягнута за короткий проміжок часу, з невеликою кількістю даних, показала, чому BERT є однією з найпотужніших моделей НЛП, доступних на даний момент.


Давайте обговоримо вашу ідею

    Пов'язані публікації

    Готові зарядити ваш бізнес на повну потужність

    ДАВАЙТЕ
    ГОВОРИТИ
    ukУкраїнська