Навчальний посібник: Тонке налаштування BERT для аналізу настроїв
Навчальний посібник: Тонке налаштування BERT для аналізу настроїв
Originally published by Skim AI's Machine Learning Researcher, Chris Tran.
A - Вступ¶
Останніми роками спільнота NLP побачила багато проривних ідей в обробці природної мови, особливо перехід до трансферного навчання. Такі моделі, як ELMo, ULMFiT від fast.ai, Transformer і GPT від OpenAI, дозволили дослідникам досягти найсучасніших результатів у багатьох тестах і надали спільноті великі попередньо навчені моделі з високою продуктивністю. Цей зсув у НЛП розглядається як момент ImageNet, зсув у комп'ютерному зорі, що стався кілька років тому, коли нижчі шари мереж глибокого навчання з мільйонами параметрів, навчених на конкретному завданні, можна повторно використовувати і допрацьовувати для інших завдань, замість того, щоб навчати нові мережі з нуля.
Однією з найважливіших віх в еволюції НЛП за останній час став випуск Google's BERT, який описують як початок нової ери в НЛП. У цьому блокноті я буду використовувати програму HuggingFace трансформатори
для точного налаштування попередньо навченої BERT-моделі для задачі класифікації. Потім я порівняю продуктивність BERT з базовою моделлю, в якій я використовую векторизатор TF-IDF і класифікатор Naive Bayes. У цьому розділі я покажу, як працює трансформатори
бібліотека допомагає нам швидко та ефективно налаштовувати найсучаснішу BERT-модель та отримувати точність 10% вище, ніж у базовій моделі.
Посилання:
Щоб зрозуміти Трансформатор (архітектура, на якій побудовано BERT) і дізнатися, як реалізувати BERT, я настійно рекомендую прочитати наступні джерела:
- Ілюстрований BERT, ELMo і Ко.: Дуже чіткий і добре написаний посібник для розуміння BERT.
- Документація по проекту
трансформатори
бібліотека - BERT Посібник з тонкого налаштування за допомогою PyTorch до Кріс МакКормік: Дуже детальний підручник, який показує, як використовувати BERT з бібліотекою HuggingFace PyTorch.
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]:
# SpecifyMAX_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 functionpreprocessing_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 є однією з найпотужніших моделей НЛП, доступних на даний момент.