Tutorial: Messa a punto del BERT per l'analisi dei sentimenti
- Tutorial: Messa a punto del BERT per l'analisi dei sentimenti
- A - Introduzione
- B - Setup¶
- C - Linea di base: TF-IDF + classificatore Naive Bayes¶
- D - Messa a punto del BERT¶
- E - Conclusione¶
Tutorial: Messa a punto del BERT per l'analisi dei sentimenti
Pubblicato originariamente dal ricercatore di apprendimento automatico di Skim AI, Chris Tran.
A - Introduzione¶
Negli ultimi anni la comunità NLP ha assistito a molte innovazioni nell'elaborazione del linguaggio naturale, in particolare il passaggio all'apprendimento per trasferimento. Modelli come ELMo, ULMFiT di fast.ai, Transformer e GPT di OpenAI hanno permesso ai ricercatori di raggiungere risultati all'avanguardia su diversi benchmark e hanno fornito alla comunità modelli pre-addestrati di grandi dimensioni con prestazioni elevate. Questo cambiamento in NLP è visto come il momento ImageNet di NLP, un cambiamento avvenuto qualche anno fa nella computer vision, quando gli strati inferiori delle reti di deep learning con milioni di parametri addestrati per un compito specifico possono essere riutilizzati e messi a punto per altri compiti, piuttosto che addestrare nuove reti da zero.
Una delle pietre miliari più importanti nell'evoluzione della PNL è il rilascio del BERT di Google, descritto come l'inizio di una nuova era nella PNL. In questo quaderno utilizzerò il sistema HuggingFace. trasformatori
per mettere a punto il modello BERT preaddestrato per un compito di classificazione. Quindi confronterò le prestazioni del BERT con un modello di base, in cui utilizzo un vettorizzatore TF-IDF e un classificatore Naive Bayes. Il trasformatori
ci aiutano a mettere a punto in modo rapido ed efficiente il modello BERT allo stato dell'arte e a ottenere un tasso di accuratezza 10% superiore a quello del modello di base.
Riferimento:
Per capire Trasformatore (l'architettura su cui si basa il BERT) e imparare a implementare il BERT, consiglio vivamente di leggere le seguenti fonti:
- Il BERT illustrato, ELMo e co.: Una guida molto chiara e ben scritta per comprendere il BERT.
- La documentazione del
trasformatori
biblioteca - Esercitazione sulla messa a punto del BERT con PyTorch da Chris McCormick: Un tutorial molto dettagliato che mostra come utilizzare BERT con la libreria HuggingFace di PyTorch.
B - Impostazione¶
1. Caricare le librerie essenziali¶
In [0]:
importare os importare re da tqdm import tqdm importare numpy come np importare pandas come pd importare matplotlib.pyplot come plt %matplotlib inline
2. Set di dati¶
2.1. Scarica il set di dati¶
In [0]:
# Scaricare i dati importare le richieste request = requests.get("https://drive.google.com/uc?export=download&id=1wHt8PsMLsfX5yNSqrt2fSTcb8LEiclcf") con open("data.zip", "wb") as file: file.write(request.content) # Decomprimere i dati importare zipfile con zipfile.ZipFile('dati.zip') come zip: zip.extractall('data')
2.2. Dati del treno di carico¶
I dati del treno sono costituiti da 2 file, ciascuno contenente 1700 tweet di reclamo/non reclamo. Ogni tweet nei dati contiene almeno un hashtag di una compagnia aerea.
Carichiamo i dati del treno e li etichettiamo. Poiché utilizziamo solo i dati di testo per la classificazione, elimineremo le colonne non importanti e manterremo solo le colonne id
, tweet
e etichetta
colonne.
In [0]:
# Caricare i dati e impostare le etichette data_complaint = pd.read_csv('data/complaint1700.csv') data_complaint['label'] = 0 dati_non_reclamo = pd.read_csv('data/noncomplaint1700.csv') dati_non_denuncia['etichetta'] = 1 # Concatenare i dati dei reclami e dei non reclami dati = pd.concat([dati_reclamo, dati_non_reclamo], asse=0).reset_index(drop=True) # Drop 'compagnia aerea' colonna data.drop(['airline'], inplace=True, axis=1) # Visualizzare 5 campioni casuali data.sample(5)
Out[0]:
id | tweet | etichetta | |
---|---|---|---|
1988 | 24991 | Che bel bentornato. Ridicolo. Sbarco... | 1 |
1294 | 72380 | Molto deluso da @JetBlue stasera. Volo... | 0 |
1090 | 127893 | @united i miei amici stanno passando un periodo infernale... | 0 |
553 | 58278 | @united tutto quello che voglio per Natale è una borsa smarrita che... | 0 |
2075 | 30695 | Sì, non voglio mentire... sono super interessato a provare... | 1 |
Divideremo casualmente gli interi dati di addestramento in due set: un set di addestramento con 90% di dati e un set di validazione con 10% di dati. Eseguiremo la regolazione degli iperparametri utilizzando la convalida incrociata sull'insieme di addestramento e useremo l'insieme di convalida per confrontare i modelli.
In [0]:
da sklearn.model_selection import train_test_split X = dati.tweet.valori y = dati.etichetta.valori X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.1, random_state=2020)
2.3. Dati della prova di carico¶
I dati del test contengono 4555 esempi senza etichetta. Circa 300 esempi sono tweets non lamentosi. Il nostro compito è identificare i loro id
ed esaminare manualmente se i nostri risultati sono corretti.
In [0]:
# Caricare i dati del test test_data = pd.read_csv('data/test_data.csv') # Mantenere le colonne importanti test_data = test_data[['id', 'tweet']] # Visualizzare 5 campioni dai dati del test test_data.sample(5)
Out[0]:
id | tweet | |
---|---|---|
1539 | 59336 | Volo @AmericanAir in ritardo di oltre 2 ore per n... |
607 | 24101 | @SouthwestAir Ricevo ancora questo messaggio di errore... |
333 | 13179 | in attesa al #SeaTac per imbarcarmi sul mio volo @JetBlue... |
2696 | 102948 | Odio quando vado a selezionare i sedili in anticipo... |
3585 | 135638 | vergogna @AlaskaAir |
3. Impostazione della GPU per la formazione¶
Google Colab offre GPU e TPU gratuite. Poiché dovremo addestrare una rete neurale di grandi dimensioni, è meglio utilizzare queste funzionalità.
È possibile aggiungere una GPU accedendo al menu e selezionando:
Runtime -> Cambia tipo di runtime -> Acceleratore hardware: GPU
Quindi è necessario eseguire la seguente cella per specificare la GPU come dispositivo.
In [0]:
importare torch se torch.cuda.is_available(): device = torch.device("cuda") print(f'Ci sono {torch.cuda.device_count()} GPU disponibili.') print('Nome dispositivo:', torch.cuda.get_device_name(0)) altrimenti: print('Nessuna GPU disponibile, si utilizza invece la CPU.') device = torch.device("cpu")
Sono disponibili 1 GPU. Nome dispositivo: Tesla T4
C - Linea di base: TF-IDF + classificatore Naive Bayes¶¶
In questo approccio di base, per prima cosa utilizzeremo TF-IDF per vettorizzare i dati di testo. Poi utilizzeremo il modello Naive Bayes come classificatore.
Perché Naive Bayes? Ho sperimentato diversi algoritmi di apprendimento automatico, tra cui Random Forest, Support Vectors Machine, XGBoost e ho osservato che Naive Bayes offre le migliori prestazioni. In Guida di Scikit-learn per scegliere lo stimatore giusto, si suggerisce anche di usare Naive Bayes per i dati di testo. Ho anche provato a utilizzare SVD per ridurre la dimensionalità, ma le prestazioni non sono migliorate.
1. Preparazione dei dati¶
1.1. Preelaborazione¶
Nel modello bag-of-words, un testo è rappresentato come il bagaglio delle sue parole, senza tenere conto della grammatica e dell'ordine delle parole. Per questo motivo, si vogliono rimuovere le stop words, le punteggiature e i caratteri che non contribuiscono molto al significato della frase.
In [0]:
importare nltk # Togliere il commento per scaricare le "stopwords". nltk.download("stopwords") da nltk.corpus import stopwords def text_preprocessing(s): """ - Ridurre la frase in minuscolo - Cambiare "'t" in "not". - Rimuovere "@nome". - Isolare e rimuovere le punteggiature, tranne "?". - Rimuovere gli altri caratteri speciali - Rimuovere le parole di interruzione, tranne "non" e "può". - Rimuovere gli spazi bianchi finali """ s = s.lower() # Cambia 't in 'not' s = re.sub(r"'t", " not", s) # Rimuovere @nome s = re.sub(r'(@.*?)[s]', ' ', s) # Isolare e rimuovere le punteggiature tranne '?' s = re.sub(r'(['".()!?\/,])', r' 1 ', s) s = re.sub(r'[^ws?]', ' ', s) # Rimuovere alcuni caratteri speciali s = re.sub(r'([;:|-"n])', ' ', s) # Rimuovere le stopword tranne 'not' e 'can' s = " ".join([parola per parola in s.split() se parola non è in stopwords.words('english') o parola in ['not', 'can']]) # Rimuovere gli spazi bianchi di coda s = re.sub(r's+', ' ', s).strip() restituire s
[nltk_data] Download del pacchetto stopwords in /root/nltk_data... [nltk_data] Il pacchetto stopwords è già aggiornato!
1.2. Vettorizzatore TF-IDF¶
Nel recupero delle informazioni, TF-IDF, abbreviazione di frequenza dei termini - frequenza inversa dei documentiè una statistica numerica che riflette l'importanza di una parola per un documento in una raccolta o in un corpus. Utilizzeremo TF-IDF per vettorializzare i nostri dati testuali prima di darli in pasto agli algoritmi di apprendimento automatico.
In [0]:
%%tempo da sklearn.feature_extraction.text import TfidfVectorizer # Preelaborazione del testo 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]) # Calcolo della TF-IDF tf_idf = TfidfVectorizer(ngram_range=(1, 3), binary=True, smooth_idf=False) X_train_tfidf = tf_idf.fit_transform(X_train_preprocessato) X_val_tfidf = tf_idf.transform(X_val_preprocessed)
Tempi della CPU: utente 5,47 s, sistema: 519 ms, totale: 5,99 s Tempo di parete: 6 s
2. Addestrare il classificatore Naive Bayes¶
2.1. Regolazione degli iperparametri¶
Utilizzeremo la convalida incrociata e il punteggio AUC per mettere a punto gli iperparametri del nostro modello. La funzione get_auc_CV
restituirà il punteggio medio AUC della convalida incrociata.
In [0]:
da sklearn.model_selection import StratifiedKFold, cross_val_score def get_auc_CV(modello): """ Restituisce il punteggio medio AUC dalla convalida incrociata. """ # Impostare KFold per mescolare i dati prima della divisione kf = StratifiedKFold(5, shuffle=True, random_state=1) # Ottenere i punteggi AUC auc = cross_val_score( model, X_train_tfidf, y_train, scoring="roc_auc", cv=kf) restituire auc.mean()
Il MultinominaleNB
hanno solo un ipterparametro - alfa. Il codice seguente ci aiuterà a trovare il valore alfa che fornisce il punteggio CV AUC più alto.
In [0]:
da sklearn.naive_bayes importa MultinomialNB res = pd.Series([get_auc_CV(MultinomialNB(i)) per i in np.arange(1, 10, 0.1)], indice=np.arange(1, 10, 0.1)) best_alpha = np.round(res.idxmax(), 2) print('Migliore alfa: ', best_alpha) plt.plot(res) plt.title('AUC vs. Alpha') plt.xlabel('Alpha') plt.ylabel('AUC') plt.show()
Miglior alfa: 1.3
2.2. Valutazione sull'insieme di convalida¶
Per valutare le prestazioni del nostro modello, calcoleremo il tasso di precisione e il punteggio AUC del nostro modello sul set di validazione.
In [0]:
da sklearn.metrics import accuracy_score, roc_curve, auc def evaluate_roc(probs, y_true): """ - Stampa AUC e accuratezza sul set di test - Tracciare la ROC @params probs (np.array): un array di probabilità previste con forma (len(y_true), 2) @params y_true (np.array): un array di valori veri con forma (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}') # Ottenere l'accuratezza sul set di test y_pred = np.where(preds >= 0.5, 1, 0) precisione = accuracy_score(y_true, y_pred) print(f'Accuracy: {accuracy*100:.2f}%') # Traccia 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('Tasso di veri positivi') plt.xlabel('Tasso di falsi positivi') plt.show()
Combinando TF-IDF e l'algoritmo Naive Bayes, si ottiene un tasso di accuratezza di 72.65% sul set di validazione. Questo valore rappresenta la prestazione di base e sarà utilizzato per valutare le prestazioni del nostro modello BERT a regolazione fine.
In [0]:
# Calcolo delle probabilità previste nb_model = MultinomialNB(alpha=1,8) nb_model.fit(X_train_tfidf, y_train) probs = nb_model.predict_proba(X_val_tfidf) # Valutare il classificatore evaluate_roc(probs, y_val)
AUC: 0.8451 Precisione: 75,59%
D - Messa a punto del BERT¶
1. Installare la libreria Hugging Face¶
La libreria di trasformatori di Hugging Face contiene l'implementazione in PyTorch di modelli NLP all'avanguardia, tra cui BERT (di Google), GPT (di OpenAI) ... e i pesi dei modelli pre-addestrati.
In [1]:
#!pip installare trasformatori
2. Tokenizzazione e formattazione dell'input¶
Prima di tokenizzare il nostro testo, effettueremo una leggera elaborazione del testo, tra cui la rimozione delle menzioni di entità (ad esempio, @united) e di alcuni caratteri speciali. Il livello di elaborazione è molto inferiore rispetto agli approcci precedenti, perché BERT è stato addestrato con le frasi intere.
In [0]:
def text_preprocessing(testo): """ - Rimuove le menzioni di entità (ad esempio, '@united'). - Correggere gli errori (es. '&' in '&') @param testo (str): una stringa da elaborare. @ritorno testo (Str): la stringa elaborata. """ # Remove '@nome' testo = re.sub(r'(@.*?)[s]', ' ', testo) # Sostituire '&' con '&' testo = re.sub(r'&', '&', testo) # Rimuovere gli spazi bianchi finali testo = re.sub(r's+', ' ', testo).strip() restituire il testo
In [0]:
# Stampa la frase 0 print('Originale: ', X[0]) print('Elaborato: ', text_preprocessing(X[0]))
Originale: @united Sto avendo problemi. Ieri ho rifatto la prenotazione per 24 ore dopo che avrei dovuto volare, ora non riesco ad accedere e fare il check-in. Potete aiutarmi? Elaborato: Ho dei problemi. Ieri ho rifatto la prenotazione per 24 ore dopo il volo previsto, ora non riesco ad accedere e a fare il check-in. Potete aiutarmi?
2.1. Tokenizzatore BERT¶
Per applicare il BERT pre-addestrato, dobbiamo usare il tokenizer fornito dalla libreria. Questo perché (1) il modello ha un vocabolario specifico e fisso e (2) il tokenizer del BERT ha un modo particolare di gestire le parole fuori vocabolario.
Inoltre, dobbiamo aggiungere dei token speciali all'inizio e alla fine di ogni frase, troncare tutte le frasi a una lunghezza costante e specificare esplicitamente quali sono i token di riempimento con la "maschera di attenzione".
Il codificare_in_più
del tokenizer BERT:
(1) dividere il testo in token,
(2) aggiungere lo speciale [CLS]
e [SEP]
gettoni e
(3) convertire questi token in indici del vocabolario del tokenizer,
(4) aggiungere o troncare le frasi alla lunghezza massima e
(5) creare una maschera di attenzione.
In [0]:
da transformers import BertTokenizer # Caricare il tokenizzatore BERT tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True) # Creare una funzione per tokenizzare un insieme di testi def preprocessing_for_bert(data): """Esegue le fasi di pre-elaborazione necessarie per il BERT pre-addestrato. @param dati (np.array): Array di testi da elaborare. @return input_ids (torch.Tensor): Tensore di id di token da fornire al modello. @return attention_masks (torch.Tensor): Tensore di indici che specificano quali token che devono essere presi in considerazione dal modello. """ # Creare elenchi vuoti per memorizzare gli output input_ids = [] maschere_di_attenzione = [] # Per ogni frase... per i dati inviati: #codificare_in_più
volontà: # (1) Tokenizzare la frase # (2) Aggiungere il[CLS]
e[SEP]
token all'inizio e alla fine # (3) Porta la frase alla massima lunghezza # (4) Mappare i token ai loro ID # (5) Crea la maschera di attenzione # (6) Restituisce un dizionario di output encoded_sent = tokenizer.encode_plus( text=text_preprocessing(sent), # Preelaborazione della frase add_special_tokens=True, # Aggiungi[CLS]
e[SEP]
max_length=MAX_LEN, # Lunghezza massima da troncare/padellare pad_to_max_length=True, # Imbottire la frase alla lunghezza massima #return_tensors='pt', # Tensore di ritorno PyTorch return_attention_mask=True # Restituzione della maschera di attenzione ) # Aggiungere gli output agli elenchi input_ids.append(encoded_sent.get('input_ids')) attention_masks.append(encoded_sent.get('attention_mask')) # Convertire gli elenchi in tensori input_ids = torch.tensor(input_ids) attention_masks = torch.tensor(attention_masks) restituire input_ids, attention_masks
Prima di procedere alla tokenizzazione, è necessario specificare la lunghezza massima delle frasi.
In [0]:
# Concatenare i dati del treno e i dati del test all_tweets = np.concatenate([data.tweet.values, test_data.tweet.values]) # Codificare i dati concatenati encoded_tweets = [tokenizer.encode(sent, add_special_tokens=True) for sent in all_tweets] # Trovare la lunghezza massima max_len = max([len(sent) for sent in encoded_tweets]) print('Lunghezza massima: ', max_len)
Lunghezza massima: 68
Ora tokenizziamo i nostri dati.
In [0]:
# SpecificareMAX_LEN
MAX_LEN = 64 # Stampa la frase 0 e i suoi id codificati dei token token_ids = list(preprocessing_for_bert([X[0]])[0].squeeze().numpy()) print('Originale: ', X[0]) print('ID dei token: ', token_ids) Funzione di esecuzione #preelaborazione_per_bert
sull'insieme di addestramento e sull'insieme di validazione print('Tokenizzazione dei dati...') train_inputs, train_masks = preprocessing_for_bert(X_train) val_inputs, val_masks = preprocessing_for_bert(X_val)
Originale: @united Sto avendo problemi. Ieri ho rifatto la prenotazione per 24 ore dopo che avrei dovuto volare, ora non riesco ad accedere e fare il check-in. Potete aiutarmi? ID gettone: [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] Tokenizzazione dei dati...
2.2. Creare il DataLoader di PyTorch¶
Creeremo un iteratore per il nostro set di dati usando la classe torch DataLoader. Questo ci aiuterà a risparmiare memoria durante l'addestramento e ad aumentare la velocità dell'addestramento.
In [0]:
da torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler # Convertire altri tipi di dati in torch.tensor train_labels = torch.tensor(y_train) val_labels = torch.tensor(y_val) # Per la messa a punto di BERT, gli autori consigliano una dimensione del batch di 16 o 32. batch_size = 32 # Creare il DataLoader per il nostro set di addestramento 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) # Creare il DataLoader per il nostro set di validazione 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. Addestrare il nostro modello¶
3.1. Creare BertClassifier¶
BERT-base è composto da 12 strati trasformatori; ogni strato trasformatore riceve un elenco di embeddings di token e produce in uscita lo stesso numero di embeddings con la stessa dimensione nascosta (o dimensioni). L'uscita dello strato trasformatore finale del sistema [CLS]
viene utilizzato come caratteristica della sequenza per alimentare un classificatore.
Il trasformatori
ha la libreria BertForSequenceClassification
che è stata progettata per compiti di classificazione. Tuttavia, creeremo una nuova classe in modo da poter specificare la nostra scelta di classificatori.
Di seguito, creeremo una classe BertClassifier con un modello BERT per estrarre l'ultimo strato nascosto del modello BERT. [CLS]
e una rete neurale feed-forward a singolo strato nascosto come classificatore.
In [0]:
%%tempo importare torch importare torch.nn come nn da transformers importare BertModel # Creare la classe BertClassfier classe BertClassifier(nn.Module): """Modello Bert per compiti di classificazione. """ def __init__(self, freeze_bert=False): """ @param bert: un oggetto BertModel @param classifier: un classificatore torch.nn.Module @param freeze_bert (bool): ImpostaFalso
per mettere a punto il modello BERT """ super(BertClassifier, self).__init__() # Specificare la dimensione nascosta di BERT, la dimensione nascosta del nostro classificatore e il numero di etichette D_in, H, D_out = 768, 50, 2 # Istanziare il modello BERT self.bert = BertModel.from_pretrained('bert-base-uncased') # Istanziare un classificatore feed-forward a un solo livello self.classifier = nn.Sequential( nn.Lineare(D_in, H), nn.ReLU(), #nn.Dropout(0,5), nn.Lineare(H, D_out) ) # Congelare il modello BERT se freeze_bert: per param in self.bert.parameters(): param.requires_grad = False def forward(self, input_ids, attention_mask): """ Fornisce l'input a BERT e al classificatore per calcolare i logit. @param input_ids (torch.Tensor): un tensore di input con forma (batch_size, max_length) @param attention_mask (torch.Tensor): un tensore che contiene le informazioni sulla maschera di attenzione con forma (batch_size, max_length). con forma (batch_size, max_length) @return logits (torch.Tensor): un tensore di output con forma (batch_size, num_etichette) """ # Alimentare l'input al BERT outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask) # Estrarre l'ultimo stato nascosto del token[CLS]
per il compito di classificazione last_hidden_state_cls = outputs[0][:, 0, :] # Alimentare l'input al classificatore per calcolare i logits logits = self.classifier(last_hidden_state_cls) restituire logits
Tempi CPU: utente 38 µs, sistema: 0 ns, totale: 38 µs Tempo di parete: 40,1 µs
3.2. Ottimizzatore e programmatore del tasso di apprendimento¶
Per mettere a punto il nostro classificatore Bert, dobbiamo creare un ottimizzatore. Gli autori raccomandano i seguenti iperparametri:
- Dimensione del lotto: 16 o 32
- Tasso di apprendimento (Adam): 5e-5, 3e-5 o 2e-5
- Numero di epoche: 2, 3, 4
Huggingface ha fornito il run_glue.py un esempio di implementazione dello script trasformatori
libreria. Nello script viene utilizzato l'ottimizzatore AdamW.
In [0]:
da transformers import AdamW, get_linear_schedule_with_warmup def initialize_model(epochs=4): """Inizializza il classificatore Bert, l'ottimizzatore e lo schedulatore del tasso di apprendimento. """ # Istanziare il classificatore di Bert bert_classifier = BertClassifier(freeze_bert=False) # Dire a PyTorch di eseguire il modello su GPU bert_classifier.to(device) # Creare l'ottimizzatore optimizer = AdamW(bert_classifier.parameters(), lr=5e-5, # Tasso di apprendimento predefinito eps=1e-8 # Valore epsilon predefinito ) # Numero totale di passi di addestramento total_steps = len(train_dataloader) * epochs # Impostazione dello scheduler del tasso di apprendimento scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, # Valore di default num_training_steps=totale_steps) restituire bert_classifier, optimizer, scheduler
3.3. Ciclo di formazione¶
Alleneremo il nostro classificatore Bert per 4 epoche. In ogni epoch, alleneremo il nostro modello e valuteremo le sue prestazioni sul set di validazione. Più in dettaglio, ci occuperemo di:
Formazione:
- Disimballare i dati dal dataloader e caricarli sulla GPU
- Azzeramento dei gradienti calcolati nel passaggio precedente
- Eseguire un passaggio in avanti per calcolare i logit e le perdite.
- Eseguire un passaggio all'indietro per calcolare i gradienti (
perdita.indietro()
) - Ridurre la norma dei gradienti a 1,0 per evitare "gradienti esplosivi".
- Aggiornare i parametri del modello (
ottimizzatore.step()
) - Aggiornare il tasso di apprendimento (
scheduler.step()
)
Valutazione:
- Disimballare i dati e caricarli sulla GPU
- Passaggio in avanti
- Calcolo della perdita e del tasso di accuratezza sull'insieme di validazione
Lo script sottostante è commentato con i dettagli del nostro ciclo di formazione e valutazione.
In [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
Ora, iniziamo ad addestrare il nostro BertClassifier!
In [0]:
set_seed(42) # Impostare il seme per la riproducibilità bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, train_dataloader, val_dataloader, epochs=2, evaluation=True)
Iniziare la formazione... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formazione completata!
3.4. Valutazione sull'insieme di convalida¶
La fase di previsione è simile alla fase di valutazione eseguita nel ciclo di addestramento, ma più semplice. Eseguiremo un passaggio in avanti per calcolare i logit e applicheremo softmax per calcolare le probabilità.
In [0]:
importare torch.nn.functional come F def bert_predict(modello, test_dataloader): """Esegue un passaggio in avanti sul modello BERT addestrato per prevedere le probabilità sul set di test. """ # Mettere il modello in modalità di valutazione. I livelli di dropout sono disabilitati durante # il tempo di test. model.eval() all_logits = [] # Per ogni lotto del nostro set di test... per batch in test_dataloader: # Caricare il batch sulla GPU b_input_ids, b_attn_mask = tuple(t.to(device) for t in batch)[:2] # Calcolo dei logit con torch.no_grad(): logits = model(b_input_ids, b_attn_mask) all_logits.append(logits) # Concatenare i logit di ciascun batch all_logits = torch.cat(all_logits, dim=0) # Applicare softmax per calcolare le probabilità probs = F.softmax(all_logits, dim=1).cpu().numpy() restituire probs
In [0]:
# Calcolo delle probabilità previste sull'insieme di test probs = bert_predict(bert_classifier, val_dataloader) # Valutare il classificatore di Bert evaluate_roc(probs, y_val)
AUC: 0.9048 Precisione: 80,59%
Bert Classifer ottiene un punteggio AUC di 0,90 e un tasso di accuratezza di 82,65% sul set di validazione. Questo risultato è migliore di 10 punti rispetto al metodo di base.
3.5. Addestrare il modello su tutti i dati di addestramento¶
In [0]:
# Concatenare l'insieme di addestramento e l'insieme di validazione 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) # Addestrare il classificatore Bert su tutti i dati di addestramento set_seed(42) bert_classifier, optimizer, scheduler = initialize_model(epochs=2) train(bert_classifier, full_train_dataloader, epochs=2)
Iniziare la formazione... Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Epoch | Batch | Train Loss | Val Loss | Val Acc | Elapsed ---------------------------------------------------------------------- 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 ---------------------------------------------------------------------- Formazione completata!
4. Previsioni sul set di prova¶
4.1. Preparazione dei dati¶
Rivediamo a breve il set di test.
In [0]:
test_data.sample(5)
Out[0]:
id | tweet | |
---|---|---|
471 | 18654 | Amici e familiari: Non volate mai con @JetBlue. Assolutamente... |
1971 | 76265 | @DeltaAssist @rogerioad Non ho mai avuto un pro... |
23 | 672 | Primo volo dopo settimane. Contiamo su di voi @Americ... |
2702 | 103263 | "@USAirways: Sapete che non possiamo rimanere senza... |
135 | 5137 | @southwestair Qui all'aeroporto di SA a guardare il ... |
Prima di fare previsioni sull'insieme di test, è necessario ripetere le operazioni di elaborazione e codifica effettuate sui dati di addestramento. Fortunatamente, abbiamo scritto il programma preelaborazione_per_bert
per farlo al posto nostro.
In [0]:
Corsa # preelaborazione_per_bert
sull'insieme di prova
print('Tokenizzazione dei dati...')
test_inputs, test_masks = preprocessing_for_bert(test_data.tweet)
# Creare il DataLoader per il nostro set di test
test_dataset = TensorDataset(test_inputs, test_masks)
test_sampler = SequentialSampler(test_dataset)
test_dataloader = DataLoader(test_dataset, sampler=test_sampler, batch_size=32)
Tokenizzazione dei dati...
4.2. Previsioni¶
Nel nostro set di test ci sono circa 300 tweet non negativi. Pertanto, continueremo a regolare la soglia di decisione finché non avremo circa 300 tweet non negativi.
La soglia che utilizzeremo è 0,992, il che significa che i tweet con una probabilità di previsione superiore a 99,2% saranno considerati positivi. Questo valore è molto elevato rispetto alla soglia predefinita di 0,5.
Dopo aver esaminato manualmente il set di test, ho scoperto che il compito di classificazione del sentiment è difficile anche per gli esseri umani. Pertanto, una soglia elevata ci darà previsioni sicure.
In [0]:
# Calcolo delle probabilità previste sull'insieme di test probs = bert_predict(bert_classifier, test_dataloader) # Ottenere le previsioni dalle probabilità soglia = 0,9 preds = np.where(probs[:, 1] > soglia, 1, 0) # Numero di tweet predetti non negativi print("Numero di tweet previsti non negativi: ", preds.sum())
Numero di tweet previsti non negativi: 454
Ora esamineremo 20 tweet casuali dalle nostre previsioni. 17 di essi sono corretti, il che dimostra che il classificatore BERT acquisisce un tasso di precisione dello 0,85 circa.
In [0]:
output = test_data[preds==1] lista(output.sample(20).tweet)
Out[0]:
[@Delta @DeltaAssist Delta colpisce ancora. La Sky lounge dell'aeroporto più trafficato del paese'chiusa nei fine settimana. Stupido ed economico. ci manchi @united", @SouthwestAir ha riportato le arachidi tostate al miele. È triste che questa realizzazione sia il punto più alto della mia giornata? #SmallThingsInLife', '@DeltaAssist Ho inviato un'e-mail a kana@delta e a contactus.delta@delta per risolvere i problemi due settimane fa senza ottenere risposta. consigli su chi contattare?', "La donna cacciata dal volo da @AlaskaAir perché ha il cancro ha intenzione di donare alla sua famiglia il biglietto aereo http://t.co/Uj6rispWLb", "@united (2/2) Non ho rotto la borsa. Se non avessi dovuto pagare per il check-in, non sarei così arrabbiato. Preferisco volare con @AmericanAir @SouthwestAir ecc", "Ho volato con quasi tutte le compagnie aeree e non ho mai avuto un'esperienza migliore di quella di @JetBlue. Qualità, servizio, comfort e convenienza. A++", '@JetBlue La migliore compagnia aerea per cui lavorare, mi manchi molto #keepingitminty ', 'Ho convinto @firetweet a prenotare un viaggio last minute per raggiungermi ad Austin! Da allora canto la canzone della sicurezza di @VirginAmerica. Povero Eric.', '@AmericanAir aspetta pazientemente di decollare da #DFW a #ord http://t.co/j1oDSc6fht', @JetBlue oggi è un giorno triste per i fedelissimi del B6. So che state pubblicizzando le vostre nuove "opzioni", ma il vostro servizio e le vostre tariffe senza bagaglio sono ciò che vi rende grandi', 'Le cose positive di questo volo: @Gogo e la grande esperienza di @VirginAmerica. Non così buone: l'odore di vomito di bambino/tonno marcio.', '@USAirways @AmericanAir sentirà la mancanza di USAir :(', '@altonbrown @united È ora di passare a @VirginAmerica', Non è mai il momento sbagliato per Chobani, @AmericanAir Admirals Club! #rovato il record #aomanywasabipeas #lunch', "Durante il mio volo, ho rubato il telefono del mio umano'per provare il nuovo IFE in streaming di @alaskaair '. È buono! Peccato che non possieda un iPad", "Non vedo l'ora che la fusione tra @USAirways e @AmericanAir sia completata, che seccatura per i clienti!", "@JetBlue Io'sono un ragazzo universitario al verde, quindi $150 è un affare enorme", "Non vedo l'ora di tornare nella Bay Area stasera con il volo 2256 di @SouthwestAir", 'Appeso al #SFO in attesa che la nebbia si diradi per il prossimo collegamento @VirginAmerica per il #sxsw! #SXSW2015 #Austin', "@DeltaAssist in ogni caso posso cambiare volo dal 1308 a uno che non sia'indefinitamente in ritardo.... E tornare a Washington!"]
E - Conclusione¶
Aggiungendo al BERT un semplice classificatore di rete neurale a uno strato nascosto e regolando con precisione il BERT, possiamo ottenere prestazioni vicine allo stato dell'arte, con un miglioramento di 10 punti rispetto al metodo di base, pur disponendo di soli 3.400 punti dati.
Inoltre, sebbene BERT sia molto grande, complicato e con milioni di parametri, abbiamo bisogno di metterlo a punto in sole 2-4 epoche. Questo risultato può essere ottenuto perché BERT è stato addestrato su una quantità enorme di parametri e codifica già molte informazioni sulla nostra lingua. Una performance impressionante ottenuta in poco tempo, con una piccola quantità di dati, ha dimostrato perché BERT è uno dei modelli NLP più potenti disponibili al momento.