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

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

    Originally published by Skim AI's Machine Learning Researcher, Chris Tran.



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]:

# Download data
import requests
request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf")
with open("data.zip", "wb") as file:
    file.write(request.content)
# Unzip data
import zipfile
with zipfile.ZipFile('data.zip') as zip:
    zip.extractall('data')

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

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

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

 # Load data and set labels
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
# Concatenate complaining and non-complaining data
data = pd.concat([data_complaint, data_non_complaint], axis=0).reset_index(drop=True)
# Drop 'airline' column
data.drop(['airline'], inplace=True, axis=1)
# Display 5 random samples
data.sample(5)

Out[0]:

ідентифікатор твітнути етикетка
1988 24991 Яке чудове повернення. Смішно. Депланін... 1
1294 72380 Дуже розчарований @JetBlue сьогодні ввечері. Рейс... 0
1090 127893 @united мої друзі мають пекельний час... 0
553 58278 @united все, що я хочу на Різдво - це загублений мішок, який... 0
2075 30695 Так, не буду брехати... мені дуже цікаво спробувати... 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]:

# Load test data
test_data = pd.read_csv('data/test_data.csv')
# Keep important columns
test_data = test_data[['id', 'tweet']]
# Display 5 samples from the test data
test_data.sample(5)

Out[0]:

ідентифікатор твітнути
1539 59336 Рейс @AmericanAir затримується більш ніж на 2 години через ...
607 24101 @SouthwestAir Все ще отримую це повідомлення про помилку...
333 13179 чекаю на #SeaTac, щоб сісти на свій рейс @JetBlue...
2696 102948 Ненавиджу, коли я проходжу через попередній вибір місця заздалегідь...
3585 135638 як вам не соромно @AlaskaAir

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

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

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

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

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

import torch
if torch.cuda.is_available():       
    device = torch.device("cuda")
    print(f'There are {torch.cuda.device_count()} GPU(s) available.')
    print('Device name:', torch.cuda.get_device_name(0))
else:
    print('No GPU available, using the CPU instead.')
    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]:

import nltk
# Uncomment to download "stopwords"
nltk.download("stopwords")
from nltk.corpus import stopwords
def text_preprocessing(s):
    """
    - Lowercase the sentence
    - Change "'t" to "not"
    - Remove "@name"
    - Isolate and remove punctuations except "?"
    - Remove other special characters
    - Remove stop words except "not" and "can"
    - Remove trailing whitespace
    """
    s = s.lower()
    # Change 't to 'not'
    s = re.sub(r"'t", " not", s)
    # Remove @name
    s = re.sub(r'(@.*?)[s]', ' ', s)
    # Isolate and remove punctuations except '?'
    s = re.sub(r'(['".()!?\/,])', r' 1 ', s)
    s = re.sub(r'[^ws?]', ' ', s)
    # Remove some special characters
    s = re.sub(r'([;:|•«n])', ' ', s)
    # Remove stopwords except 'not' and 'can'
    s = " ".join([word for word in s.split()
                  if word not in stopwords.words('english')
                  or word in ['not', 'can']])
    # Remove trailing whitespace
    s = re.sub(r's+', ' ', s).strip()
    return 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
# Preprocess text
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])
# Calculate 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):
    """
    Return the average AUC score from cross-validation.
    """
    # Set KFold to shuffle data before the split
    kf = StratifiedKFold(5, shuffle=True, random_state=1)
    # Get AUC scores
    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: ', best_alpha)
plt.plot(res)
plt.title('AUC vs. Alpha')
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):
    """
    - Print AUC and accuracy on the test set
    - Plot ROC
    @params    probs (np.array): an array of predicted probabilities with shape (len(y_true), 2)
    @params    y_true (np.array): an array of the true values with shape (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}')
    # Get accuracy over the test set
    y_pred = np.where(preds >= 0.5, 1, 0)
    accuracy = accuracy_score(y_true, y_pred)
    print(f'Accuracy: {accuracy*100:.2f}%')
    # Plot ROC AUC
    plt.title('Receiver Operating Characteristic')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = 'lower right')
    plt.plot([0, 1], [0, 1],'r--')
    plt.xlim([0, 1])
    plt.ylim([0, 1])
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')
    plt.show()

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

# Compute predicted probabilities
nb_model = MultinomialNB(alpha=1.8)
nb_model.fit(X_train_tfidf, y_train)
probs = nb_model.predict_proba(X_val_tfidf)
# Evaluate the classifier
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):
    """
    - Remove entity mentions (eg. '@united')
    - Correct errors (eg. '&' to '&')
    @param    text (str): a string to be processed.
    @return   text (Str): the processed string.
    """
    # Remove '@name'
    text = re.sub(r'(@.*?)[s]', ' ', text)
    # Replace '&' with '&'
    text = re.sub(r'&', '&', text)
    # Remove trailing whitespace
    text = re.sub(r's+', ' ', text).strip()
    return text

У [0]:

# Print sentence 0
print('Original: ', X[0])
print('Processed: ', 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]:

from transformers import BertTokenizer
# Load the BERT tokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
# Create a function to tokenize a set of texts
def preprocessing_for_bert(data):
    """Perform required preprocessing steps for pretrained BERT.
    @param    data (np.array): Array of texts to be processed.
    @return   input_ids (torch.Tensor): Tensor of token ids to be fed to a model.
    @return   attention_masks (torch.Tensor): Tensor of indices specifying which
                  tokens should be attended to by the model.
    """
    # Create empty lists to store outputs
    input_ids = []
    attention_masks = []
    # For every sentence...
    for sent in data:
        # encode_plus will:
        #    (1) Tokenize the sentence
        #    (2) Add the [CLS] і [SEP] token to the start and end
        #    (3) Truncate/Pad sentence to max length
        #    (4) Map tokens to their IDs
        #    (5) Create attention mask
        #    (6) Return a dictionary of outputs
        encoded_sent = tokenizer.encode_plus(
            text=text_preprocessing(sent),  # Preprocess sentence
            add_special_tokens=True,        # Add [CLS] і [SEP]
            max_length=MAX_LEN,                  # Max length to truncate/pad
            pad_to_max_length=True,         # Pad sentence to max length
            #return_tensors='pt',           # Return PyTorch tensor
            return_attention_mask=True      # Return attention mask
            )
        # Add the outputs to the lists
        input_ids.append(encoded_sent.get('input_ids'))
        attention_masks.append(encoded_sent.get('attention_mask'))
    # Convert lists to tensors
    input_ids = torch.tensor(input_ids)
    attention_masks = torch.tensor(attention_masks)
    return input_ids, attention_masks

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

# Concatenate train data and test data
all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values])
# Encode our concatenated data
encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets]
# Find the maximum length
max_len = max([len(sent) for sent in encoded_tweets])
print('Max length: ', max_len)
Максимальна довжина: 68

Тепер давайте токенізуємо наші дані.
У [0]:

# Specify MAX_LEN
MAX_LEN = 64
# Print sentence 0 and its encoded token ids
token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy())
print('Original: ', X[0])
print('Token IDs: ', token_ids)
# Run function preprocessing_for_bert on the train set and the validation set
print('Tokenizing data...')
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]:

from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
# Convert other data types to torch.Tensor
train_labels = torch.tensor(y_train)
val_labels = torch.tensor(y_val)
# For fine-tuning BERT, the authors recommend a batch size of 16 or 32.
batch_size = 32
# Create the DataLoader for our training set
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)
# Create the DataLoader for our validation set
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
# Create the BertClassfier class
class BertClassifier(nn.Module):
    """Bert Model for Classification Tasks.
    """
    def __init__(self, freeze_bert=False):
        """
        @param    bert: a BertModel object
        @param    classifier: a torch.nn.Module classifier
        @param    freeze_bert (bool): Set Неправда. to fine-tune the BERT model
        """
        super(BertClassifier, self).__init__()
        # Specify hidden size of BERT, hidden size of our classifier, and number of labels
        D_in, H, D_out = 768, 50, 2
        # Instantiate BERT model
        self.bert = BertModel.from_pretrained('bert-base-uncased')
        # Instantiate an one-layer feed-forward classifier
        self.classifier = nn.Sequential(
            nn.Linear(D_in, H),
            nn.ReLU(),
            #nn.Dropout(0.5),
            nn.Linear(H, D_out)
        )
        # Freeze the BERT model
        if freeze_bert:
            for param in self.bert.parameters():
                param.requires_grad = False
    def forward(self, input_ids, attention_mask):
        """
        Feed input to BERT and the classifier to compute logits.
        @param    input_ids (torch.Tensor): an input tensor with shape (batch_size,
                      max_length)
        @param    attention_mask (torch.Tensor): a tensor that hold attention mask
                      information with shape (batch_size, max_length)
        @return   logits (torch.Tensor): an output tensor with shape (batch_size,
                      num_labels)
        """
        # Feed input to BERT
        outputs = self.bert(input_ids=input_ids,
                            attention_mask=attention_mask)
        # Extract the last hidden state of the token [CLS] for classification task
        last_hidden_state_cls = outputs[0][:, 0, :]
        # Feed input to classifier to compute logits
        logits = self.classifier(last_hidden_state_cls)
        return 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):
    """Initialize the Bert Classifier, the optimizer and the learning rate scheduler.
    """
    # Instantiate Bert Classifier
    bert_classifier = BertClassifier(freeze_bert=False)
    # Tell PyTorch to run the model on GPU
    bert_classifier.to(device)
    # Create the optimizer
    optimizer = AdamW(bert_classifier.parameters(),
                      lr=5e-5,    # Default learning rate
                      eps=1e-8    # Default epsilon value
                      )
    # Total number of training steps
    total_steps = len(train_dataloader) * epochs
    # Set up the learning rate scheduler
    scheduler = get_linear_schedule_with_warmup(optimizer,
                                                num_warmup_steps=0, # Default value
                                                num_training_steps=total_steps)
    return bert_classifier, optimizer, scheduler

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)    # Set seed for reproducibility
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_dataloader):
    """Perform a forward pass on the trained BERT model to predict probabilities
    on the test set.
    """
    # Put the model into the evaluation mode. The dropout layers are disabled during
    # the test time.
    model.eval()
    all_logits = []
    # For each batch in our test set...
    for batch in test_dataloader:
        # Load batch to GPU
        b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2]
        # Compute logits
        with torch.no_grad():
            logits = model(b_input_ids, b_attn_mask)
        all_logits.append(logits)
    # Concatenate logits from each batch
    all_logits = torch.cat(all_logits, dim=0)
    # Apply softmax to calculate probabilities
    probs = F.softmax(all_logits, dim=1).cpu().numpy()
    return probs

У [0]:

# Compute predicted probabilities on the test set
probs = bert_predict(bert_classifier, val_dataloader)
# Evaluate the Bert classifier
evaluate_roc(probs, y_val)
AUC: 0.9048
Точність: 80.59%


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

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

У [0]:

# Concatenate the train set and the validation set
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)
# Train the Bert Classifier on the entire training data
set_seed(42)
bert_classifier, optimizer, scheduler = initialize_model(epochs=2)
train(bert_classifier, full_train_dataloader, 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]:

ідентифікатор твітнути
471 18654 Друзі та родина: Ніколи не літайте @JetBlue. Абсолютно...
1971 76265 @DeltaAssist @rogerioad У мене ніколи не було про.
23 672 Перший політ за кілька тижнів. Розраховую на тебе, американцю...
2702 103263 "USAirways: "Ви знаєте, що ми не можемо залишатися без ...
135 5137 @southwestair Тут, в аеропорту Південної Африки, ми спостерігаємо за ...

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

# Run preprocessing_for_bert on the test set
print('Tokenizing data...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Create the DataLoader for our test set
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]:

# Compute predicted probabilities on the test set
probs = bert_predict(bert_classifier, test_dataloader)
# Get predictions from the probabilities
threshold = 0.9
preds = np.where(probs[:, 1] > threshold, 1, 0)
# Number of tweets predicted non-negative
print("Number of tweets predicted non-negative: ", 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 є однією з найпотужніших моделей НЛП, доступних на даний момент.

Let’s Discuss Your Idea

    Related Posts

    Ready To Supercharge Your Business

    LET’S
    TALK
    ukУкраїнська