Навчальний посібник: Тонке налаштування BERT для аналізу настроїв
- Навчальний посібник: Тонке налаштування BERT для аналізу настроїв
- A - Вступ¶
- B - Налаштування¶
- C - Базовий рівень: TF-IDF + Наївний класифікатор Байєса¶
- D - Точна настройка BERT¶
- E - Висновок¶
Навчальний посібник: Тонке налаштування BERT для аналізу настроїв
Вперше опубліковано дослідником машинного навчання Skim AI, Крісом Траном.
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]:
# Завантажити дані імпорт запитів 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]:
ідентифікатор | твітнути | етикетка | |
---|---|---|---|
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]:
# Завантажити дані навантажувального тесту test_data = pd.read_csv('data/test_data.csv') # Зберегти важливі стовпці test_data = test_data[['id', 'tweet']]]. # Вивести 5 вибірок з тестових даних 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'Доступно {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]:
ідентифікатор | твітнути | |
---|---|---|
471 | 18654 | Друзі та родина: Ніколи не літайте @JetBlue. Абсолютно... |
1971 | 76265 | @DeltaAssist @rogerioad У мене ніколи не було про. |
23 | 672 | Перший політ за кілька тижнів. Розраховую на вас, американці... |
2702 | 103263 | "USAirways: "Ви знаєте, що ми не можемо залишатися без ... |
135 | 5137 | @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 є однією з найпотужніших моделей НЛП, доступних на даний момент.