Messa a punto di un modello BERT sui dati dei social media

Ottenere e preparare i dati

Il set di dati che utilizzeremo proviene da Kaggle, puoi scaricarlo qui: https://www.kaggle.com/datasets/farisdurrani/sentimentsearch (Licenza CC BY 4.0). Nei miei esperimenti, ho scelto solo i set di dati di Facebook e Twitter.

Il seguente frammento prenderà i file CSV e salverà 3 suddivisioni (formazione, convalida e test) dove desideri. Ti consiglio di salvarli in Google Cloud Storage.

Puoi eseguire lo script con:

python make_splits --output-dir gs://your-bucket/
import pandas as pd
import argparse
import numpy as np
from sklearn.model_selection import train_test_split

def make_splits(output_dir):
df=pd.concat((
pd.read_csv("data/farisdurrani/twitter_filtered.csv"),
pd.read_csv("data/farisdurrani/facebook_filtered.csv")
))
df = df.dropna(subset=('sentiment'), axis=0)
df('Target') = df('sentiment').apply(lambda x: 1 if x==0 else np.sign(x)+1).astype(int)

df_train, df_ = train_test_split(df, stratify=df('Target'), test_size=0.2)
df_eval, df_test = train_test_split(df_, stratify=df_('Target'), test_size=0.5)

print(f"Files will be saved in {output_dir}")
df_train.to_csv(output_dir + "/train.csv", index=False)
df_eval.to_csv(output_dir + "/eval.csv", index=False)
df_test.to_csv(output_dir + "/test.csv", index=False)

print(f"Train : ({df_train.shape}) samples")
print(f"Val : ({df_eval.shape}) samples")
print(f"Test : ({df_test.shape}) samples")

if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--output-dir')
args, _ = parser.parse_known_args()
make_splits(args.output_dir)

I dati dovrebbero assomigliare più o meno a questo:

(immagine dell'autore)

Utilizzando un piccolo modello preaddestrato BERT

Per il nostro modello utilizzeremo un modello BERT leggero, BERT-Tiny. Questo modello è già stato preaddestrato su grandi quantità di dati, ma non necessariamente con i dati dei social media e non necessariamente con l’obiettivo di eseguire l’analisi del sentiment. Per questo motivo lo perfezioneremo.

Contiene solo 2 livelli con una dimensione di 128 unità, è possibile visualizzare l'elenco completo dei modelli Qui se vuoi prenderne uno più grande.

Creiamo prima un file main.py file, con tutti i moduli necessari:

import pandas as pd
import argparse
import tensorflow as tf
import tensorflow_hub as hub
import tensorflow_text as text
import logging
import os
os.environ("TFHUB_MODEL_LOAD_FORMAT") = "UNCOMPRESSED"

def train_and_evaluate(**params):
pass
# will be updated as we go

Scriviamo anche le nostre esigenze in un apposito requirements.txt

transformers==4.40.1
torch==2.2.2
pandas==2.0.3
scikit-learn==1.3.2
gcsfs

Ora caricheremo 2 parti per addestrare il nostro modello:

  • IL tokenizzatoreche si occuperà di dividere gli input di testo in token con cui BERT è stato addestrato.
  • IL modello si.

Puoi ottenerli entrambi da Huggingface Qui. Puoi anche scaricarli su Cloud Storage. Questo è quello che ho fatto e quindi li caricherò con:


# Load pretrained tokenizers and bert model
tokenizer = BertTokenizer.from_pretrained('models/bert_uncased_L-2_H-128_A-2/vocab.txt')
model = BertModel.from_pretrained('models/bert_uncased_L-2_H-128_A-2')

Aggiungiamo ora il seguente pezzo al nostro file:

class SentimentBERT(nn.Module):
def __init__(self, bert_model):
super().__init__()
self.bert_module = bert_model
self.dropout = nn.Dropout(0.1)
self.final = nn.Linear(in_features=128, out_features=3, bias=True)

# Uncomment the below if you only want to retrain certain layers.
# self.bert_module.requires_grad_(False)
# for param in self.bert_module.encoder.parameters():
# param.requires_grad = True

def forward(self, inputs):
ids, mask, token_type_ids = inputs('ids'), inputs('mask'), inputs('token_type_ids')
# print(ids.size(), mask.size(), token_type_ids.size())
x = self.bert_module(ids, mask, token_type_ids)
x = self.dropout(x('pooler_output'))
out = self.final(x)
return out

Una piccola pausa qui. Abbiamo diverse opzioni quando si tratta di riutilizzare un modello esistente.

  • Trasferire l'apprendimento : congeliamo i pesi del modello e lo utilizziamo come “estrattore di feature”. Possiamo quindi aggiungere ulteriori livelli a valle. Questo viene spesso utilizzato nella visione artificiale in cui modelli come VGG, Xception, ecc. possono essere riutilizzati per addestrare un modello personalizzato su piccoli set di dati
  • Ritocchi : sblocchiamo tutti o parte dei pesi del modello e riqualificamo il modello su un dataset personalizzato. Questo è l'approccio preferito durante la formazione di LLM personalizzati.

Maggiori dettagli sul trasferimento dell'apprendimento e sul perfezionamento Qui:

Nel modello, abbiamo scelto di sbloccare tutto il modello, ma sentitevi liberi di congelare uno o più livelli del modulo BERT preaddestrato e vedere come influenza le prestazioni.

La parte fondamentale qui è aggiungere uno strato completamente connesso dopo il modulo BERT per “collegarlo” al nostro compito di classificazione, da qui lo strato finale con 3 unità. Ciò ci consentirà di riutilizzare i pesi BERT preaddestrati e adattare il nostro modello al nostro compito.

Creazione dei caricatori dati

Per creare i dataloader avremo bisogno del Tokenizer caricato sopra. Il Tokenizer prende una stringa come input e restituisce diversi output tra i quali possiamo trovare i token (“input_ids” nel nostro caso):

Il tokenizzatore BERT è un po' speciale e restituirà diversi output, ma il più importante è il file input_ids: sono i token utilizzati per codificare la nostra frase. Potrebbero essere parole, parti o parole. Ad esempio, la parola “looking” potrebbe essere composta da 2 token, “look” e “##ing”.

Creiamo ora un modulo dataloader che gestirà i nostri set di dati:

class BertDataset(Dataset):
def __init__(self, df, tokenizer, max_length=100):
super(BertDataset, self).__init__()
self.df=df
self.tokenizer=tokenizer
self.target=self.df('Target')
self.max_length=max_length

def __len__(self):
return len(self.df)

def __getitem__(self, idx):

X = self.df('bodyText').values(idx)
y = self.target.values(idx)

inputs = self.tokenizer.encode_plus(
X,
pad_to_max_length=True,
add_special_tokens=True,
return_attention_mask=True,
max_length=self.max_length,
)
ids = inputs("input_ids")
token_type_ids = inputs("token_type_ids")
mask = inputs("attention_mask")

x = {
'ids': torch.tensor(ids, dtype=torch.long).to(DEVICE),
'mask': torch.tensor(mask, dtype=torch.long).to(DEVICE),
'token_type_ids': torch.tensor(token_type_ids, dtype=torch.long).to(DEVICE)
}
y = torch.tensor(y, dtype=torch.long).to(DEVICE)

return x, y

Scrivere lo script principale per addestrare il modello

Definiamo innanzitutto due funzioni per gestire le fasi di formazione e valutazione:

def train(epoch, model, dataloader, loss_fn, optimizer, max_steps=None):
model.train()
total_acc, total_count = 0, 0
log_interval = 50
start_time = time.time()

for idx, (inputs, label) in enumerate(dataloader):
optimizer.zero_grad()
predicted_label = model(inputs)

loss = loss_fn(predicted_label, label)
loss.backward()
optimizer.step()

total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)

if idx % log_interval == 0:
elapsed = time.time() - start_time
print(
"Epoch {:3d} | {:5d}/{:5d} batches "
"| accuracy {:8.3f} | loss {:8.3f} ({:.3f}s)".format(
epoch, idx, len(dataloader), total_acc / total_count, loss.item(), elapsed
)
)
total_acc, total_count = 0, 0
start_time = time.time()

if max_steps is not None:
if idx == max_steps:
return {'loss': loss.item(), 'acc': total_acc / total_count}

return {'loss': loss.item(), 'acc': total_acc / total_count}

def evaluate(model, dataloader, loss_fn):
model.eval()
total_acc, total_count = 0, 0

with torch.no_grad():
for idx, (inputs, label) in enumerate(dataloader):
predicted_label = model(inputs)
loss = loss_fn(predicted_label, label)
total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)

return {'loss': loss.item(), 'acc': total_acc / total_count}

Ci stiamo avvicinando a rendere operativo il nostro script principale. Cuciamo insieme i pezzi. Abbiamo:

  • UN BertDataset classe per gestire il caricamento dei dati
  • UN SentimentBERT modello che prende il nostro modello Tiny-BERT e aggiunge un ulteriore livello per il nostro caso d'uso personalizzato
  • train() E eval() funzioni per gestire tali passaggi
  • UN train_and_eval() funzioni che raggruppano tutto

Noi useremo argparse per poter lanciare il nostro script con argomenti. Tali argomenti sono in genere i file di training/valutazione/test per eseguire il nostro modello con qualsiasi set di dati, il percorso in cui verrà archiviato il nostro modello e i parametri relativi al training.

import pandas as pd
import time
import torch.nn as nn
import torch
import logging
import numpy as np
import argparse

from torch.utils.data import Dataset, DataLoader
from transformers import BertTokenizer, BertModel

logging.basicConfig(format='%(asctime)s (%(levelname)s): %(message)s', level=logging.DEBUG)
logging.getLogger().setLevel(logging.INFO)

# --- CONSTANTS ---
BERT_MODEL_NAME = 'small_bert/bert_en_uncased_L-2_H-128_A-2'

if torch.cuda.is_available():
logging.info(f"GPU: {torch.cuda.get_device_name(0)} is available.")
DEVICE = torch.device('cuda')
else:
logging.info("No GPU available. Training will run on CPU.")
DEVICE = torch.device('cpu')

# --- Data preparation and tokenization ---
class BertDataset(Dataset):
def __init__(self, df, tokenizer, max_length=100):
super(BertDataset, self).__init__()
self.df=df
self.tokenizer=tokenizer
self.target=self.df('Target')
self.max_length=max_length

def __len__(self):
return len(self.df)

def __getitem__(self, idx):

X = self.df('bodyText').values(idx)
y = self.target.values(idx)

inputs = self.tokenizer.encode_plus(
X,
pad_to_max_length=True,
add_special_tokens=True,
return_attention_mask=True,
max_length=self.max_length,
)
ids = inputs("input_ids")
token_type_ids = inputs("token_type_ids")
mask = inputs("attention_mask")

x = {
'ids': torch.tensor(ids, dtype=torch.long).to(DEVICE),
'mask': torch.tensor(mask, dtype=torch.long).to(DEVICE),
'token_type_ids': torch.tensor(token_type_ids, dtype=torch.long).to(DEVICE)
}
y = torch.tensor(y, dtype=torch.long).to(DEVICE)

return x, y

# --- Model definition ---
class SentimentBERT(nn.Module):
def __init__(self, bert_model):
super().__init__()
self.bert_module = bert_model
self.dropout = nn.Dropout(0.1)
self.final = nn.Linear(in_features=128, out_features=3, bias=True)

def forward(self, inputs):
ids, mask, token_type_ids = inputs('ids'), inputs('mask'), inputs('token_type_ids')
x = self.bert_module(ids, mask, token_type_ids)
x = self.dropout(x('pooler_output'))
out = self.final(x)
return out

# --- Training loop ---
def train(epoch, model, dataloader, loss_fn, optimizer, max_steps=None):
model.train()
total_acc, total_count = 0, 0
log_interval = 50
start_time = time.time()

for idx, (inputs, label) in enumerate(dataloader):
optimizer.zero_grad()
predicted_label = model(inputs)

loss = loss_fn(predicted_label, label)
loss.backward()
optimizer.step()

total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)

if idx % log_interval == 0:
elapsed = time.time() - start_time
print(
"Epoch {:3d} | {:5d}/{:5d} batches "
"| accuracy {:8.3f} | loss {:8.3f} ({:.3f}s)".format(
epoch, idx, len(dataloader), total_acc / total_count, loss.item(), elapsed
)
)
total_acc, total_count = 0, 0
start_time = time.time()

if max_steps is not None:
if idx == max_steps:
return {'loss': loss.item(), 'acc': total_acc / total_count}

return {'loss': loss.item(), 'acc': total_acc / total_count}

# --- Validation loop ---
def evaluate(model, dataloader, loss_fn):
model.eval()
total_acc, total_count = 0, 0

with torch.no_grad():
for idx, (inputs, label) in enumerate(dataloader):
predicted_label = model(inputs)
loss = loss_fn(predicted_label, label)
total_acc += (predicted_label.argmax(1) == label).sum().item()
total_count += label.size(0)

return {'loss': loss.item(), 'acc': total_acc / total_count}

# --- Main function ---
def train_and_evaluate(**params):

logging.info("running with the following params :")
logging.info(params)

# Load pretrained tokenizers and bert model
# update the paths to whichever you are using
tokenizer = BertTokenizer.from_pretrained('models/bert_uncased_L-2_H-128_A-2/vocab.txt')
model = BertModel.from_pretrained('models/bert_uncased_L-2_H-128_A-2')

# Training parameters
epochs = int(params.get('epochs'))
batch_size = int(params.get('batch_size'))
learning_rate = float(params.get('learning_rate'))

# Load the data
df_train = pd.read_csv(params.get('training_file'))
df_eval = pd.read_csv(params.get('validation_file'))
df_test = pd.read_csv(params.get('testing_file'))

# Create dataloaders
train_ds = BertDataset(df_train, tokenizer, max_length=100)
train_loader = DataLoader(dataset=train_ds,batch_size=batch_size, shuffle=True)
eval_ds = BertDataset(df_eval, tokenizer, max_length=100)
eval_loader = DataLoader(dataset=eval_ds,batch_size=batch_size)
test_ds = BertDataset(df_test, tokenizer, max_length=100)
test_loader = DataLoader(dataset=test_ds,batch_size=batch_size)

# Create the model
classifier = SentimentBERT(bert_model=model).to(DEVICE)
total_parameters = sum((np.prod(p.size()) for p in classifier.parameters()))
model_parameters = filter(lambda p: p.requires_grad, classifier.parameters())
params = sum((np.prod(p.size()) for p in model_parameters))
logging.info(f"Total params : {total_parameters} - Trainable : {params} ({params/total_parameters*100}% of total)")

# Optimizer and loss functions
optimizer = torch.optim.Adam((p for p in classifier.parameters() if p.requires_grad), learning_rate)
loss_fn = nn.CrossEntropyLoss()

# If dry run we only
logging.info(f'Training model with {BERT_MODEL_NAME}')
if args.dry_run:
logging.info("Dry run mode")
epochs = 1
steps_per_epoch = 1
else:
steps_per_epoch = None

# Action !
for epoch in range(1, epochs + 1):
epoch_start_time = time.time()
train_metrics = train(epoch, classifier, train_loader, loss_fn=loss_fn, optimizer=optimizer, max_steps=steps_per_epoch)
eval_metrics = evaluate(classifier, eval_loader, loss_fn=loss_fn)

print("-" * 59)
print(
"End of epoch {:3d} - time: {:5.2f}s - loss: {:.4f} - accuracy: {:.4f} - valid_loss: {:.4f} - valid accuracy {:.4f} ".format(
epoch, time.time() - epoch_start_time, train_metrics('loss'), train_metrics('acc'), eval_metrics('loss'), eval_metrics('acc')
)
)
print("-" * 59)

if args.dry_run:
# If dry run, we do not run the evaluation
return None

test_metrics = evaluate(classifier, test_loader, loss_fn=loss_fn)

metrics = {
'train': train_metrics,
'val': eval_metrics,
'test': test_metrics,
}
logging.info(metrics)

# save model and architecture to single file
if params.get('job_dir') is None:
logging.warning("No job dir provided, model will not be saved")
else:
logging.info("Saving model to {} ".format(params.get('job_dir')))
torch.save(classifier.state_dict(), params.get('job_dir'))
logging.info("Bye bye")

if __name__ == '__main__':
# Create arguments here
parser = argparse.ArgumentParser()
parser.add_argument('--training-file', required=True, type=str)
parser.add_argument('--validation-file', required=True, type=str)
parser.add_argument('--testing-file', type=str)
parser.add_argument('--job-dir', type=str)
parser.add_argument('--epochs', type=float, default=2)
parser.add_argument('--batch-size', type=float, default=1024)
parser.add_argument('--learning-rate', type=float, default=0.01)
parser.add_argument('--dry-run', action="store_true")

# Parse them
args, _ = parser.parse_known_args()

# Execute training
train_and_evaluate(**vars(args))

È fantastico, ma sfortunatamente questo modello richiederà molto tempo per addestrarsi. Infatti, con circa 4,7 milioni di parametri da addestrare, un passaggio richiederà circa 3 secondi su un MacBook Pro da 16 Gb con chip Intel.

3 secondi per passo possono essere piuttosto lunghi quando hai 1238 passi da fare e 10 epoche da completare…

Nessuna GPU, nessuna festa.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *