Fine-tuning BERT model for Sentiment Analysis

Last Updated : 23 Sep, 2026

BERT (Bidirectional Encoder Representations from Transformers) is a Transformer-based language model developed by Google for natural language processing. It is pre-trained on large amounts of text and can then be fine-tuned for specific tasks using task-specific labeled data. We will fine-tune a pre-trained BERT model for sentiment analysis using the IMDb Dataset of 50K Movie Reviews. The model will learn to classify movie reviews as either positive or negative.

Preparing the Dataset

You can download the dataset from here.

The dataset contains two columns:

  • review: Text of the movie review.
  • sentiment: Sentiment of the review, either positive or negative.

The dataset contains an equal number of positive and negative reviews. To keep the tutorial fast while still demonstrating the complete fine-tuning process, we will use a smaller stratified subset of the dataset.

Loading Dataset

Install the required libraries:

Python
!pip install -q transformers torch scikit-learn pandas

Import the required libraries:

Python
import pandas as pd
import torch

from torch.utils.data import Dataset, DataLoader
from transformers import (
    BertTokenizerFast,
    BertForSequenceClassification,
    DataCollatorWithPadding
)
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

Load the dataset:

Python
df = pd.read_csv(
    '/content/IMDB Dataset.csv',
    engine='python',
    on_bad_lines='skip'
)

print(df.head())
print(df['sentiment'].value_counts())

Convert the sentiment labels into numerical values. Here, 0 represents negative sentiment and 1 represents positive sentiment.

Python
df['sentiment'] = df['sentiment'].map({
    'negative': 0,
    'positive': 1
})

print(df.head())
print(df['sentiment'].value_counts())

Creating a Smaller Dataset

Fine-tuning BERT on all 50,000 reviews can take considerable time and computational resources. For this tutorial, we use 8,000 reviews while keeping the positive and negative classes balanced. First, split the dataset into training and temporary sets. Then split the temporary set into validation and test sets.

Python
train_text, temp_text, train_labels, temp_labels = train_test_split(
    df['review'],
    df['sentiment'],
    test_size=0.20,
    random_state=2021,
    stratify=df['sentiment']
)

val_text, test_text, val_labels, test_labels = train_test_split(
    temp_text,
    temp_labels,
    test_size=0.50,
    random_state=2021,
    stratify=temp_labels
)

print("Training samples:", len(train_text))
print("Validation samples:", len(val_text))
print("Test samples:", len(test_text))

This creates 6,000 training samples, 1,000 validation samples and 1,000 test samples. Using a smaller subset keeps the example practical while preserving the complete workflow of BERT fine-tuning.

Load BERT Tokenizer & Model

Next, load the pre-trained bert-based-uncased tokenizer and model. The tokenizer converts the review text into token IDs that BERT can process. The classification model adds a classification layer on top of BERT for predicting the sentiment class.

Python
tokenizer = BertTokenizerFast.from_pretrained(
    'bert-base-uncased'
)

model = BertForSequenceClassification.from_pretrained(
    'bert-base-uncased',
    num_labels=2
)

Tokenizing the Data

BERT can process a fixed maximum number of tokens for each input. We use a maximum sequence length of 64 tokens to keep training faster. Longer reviews are truncated, while shorter reviews are padded when batches are created.

Python
max_length = 64

Create a dataset class to tokenize the reviews:

Python
class IMDBDataset(Dataset):

    def __init__(self, texts, labels, tokenizer, max_length):
        self.texts = texts.tolist()
        self.labels = labels.tolist()
        self.tokenizer = tokenizer
        self.max_length = max_length

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

    def __getitem__(self, index):
        encoding = self.tokenizer(
            self.texts[index],
            truncation=True,
            max_length=self.max_length
        )

        encoding['labels'] = self.labels[index]

        return encoding

Create datasets for training, validation and testing:

Python
train_dataset = IMDBDataset(
    train_text,
    train_labels,
    tokenizer,
    max_length
)

val_dataset = IMDBDataset(
    val_text,
    val_labels,
    tokenizer,
    max_length
)

test_dataset = IMDBDataset(
    test_text,
    test_labels,
    tokenizer,
    max_length
)

Use a data collator to dynamically pad each batch to the length of its longest sequence.

Python
data_collator = DataCollatorWithPadding(
    tokenizer=tokenizer
)

Create data loaders:

Python
train_dataloader = DataLoader(
    train_dataset,
    batch_size=32,
    shuffle=True,
    collate_fn=data_collator
)

val_dataloader = DataLoader(
    val_dataset,
    batch_size=32,
    collate_fn=data_collator
)

test_dataloader = DataLoader(
    test_dataset,
    batch_size=32,
    collate_fn=data_collator
)

Fine-tuning BERT

Move the model to a GPU if one is available. Otherwise, the model runs on the CPU.

Python
device = torch.device(
    'cuda' if torch.cuda.is_available() else 'cpu'
)

model = model.to(device)

print("Using device:", device)

Create an optimizer for updating the BERT parameters during fine-tuning.

Python
optimizer = torch.optim.AdamW(
    model.parameters(),
    lr=2e-5
)

Training the Model

  • We fine-tune BERT for one epoch. During training, the model predicts the sentiment, calculates the loss, computes gradients and updates its parameters.
  • Mixed precision is used on CUDA devices to reduce memory usage and speed up training.
Python
epochs = 1

scaler = torch.amp.GradScaler(
    'cuda',
    enabled=(device.type == 'cuda')
)

for epoch in range(epochs):

    model.train()
    total_loss = 0

    for batch in train_dataloader:

        batch = {
            key: value.to(device)
            for key, value in batch.items()
        }

        optimizer.zero_grad()

        with torch.amp.autocast(
            device_type=device.type,
            enabled=(device.type == 'cuda')
        ):
            outputs = model(**batch)
            loss = outputs.loss

        scaler.scale(loss).backward()
        scaler.step(optimizer)
        scaler.update()

        total_loss += loss.item()

    avg_loss = total_loss / len(train_dataloader)

    print(
        f"Epoch {epoch + 1}/{epochs} - "
        f"Training Loss: {avg_loss:.3f}"
    )

Because BERT is not frozen, its pre-trained parameters are updated during this process. This allows the model to adapt its language representations to the sentiment analysis task.

Evaluating the Model

After training, evaluate the model on the validation data. The validation data is not used to update the model parameters.

Python
model.eval()

val_preds = []
val_labels_list = []

with torch.no_grad():

    for batch in val_dataloader:

        labels = batch['labels']

        batch = {
            key: value.to(device)
            for key, value in batch.items()
        }

        outputs = model(**batch)

        predictions = torch.argmax(
            outputs.logits,
            dim=1
        )

        val_preds.extend(
            predictions.cpu().numpy()
        )

        val_labels_list.extend(
            labels.numpy()
        )

print(
    classification_report(
        val_labels_list,
        val_preds,
        target_names=[
            'Negative',
            'Positive'
        ]
    )
)

Output:

bert1

The classification report shows the model's precision, recall, F1-score and accuracy on the validation set.

Testing the Fine-tuned BERT Model

Finally, evaluate the model on the test dataset. The test data has not been used to update the model parameters.

Python
model.eval()

test_preds = []
test_labels_list = []

with torch.no_grad():

    for batch in test_dataloader:

        labels = batch['labels']

        batch = {
            key: value.to(device)
            for key, value in batch.items()
        }

        outputs = model(**batch)

        predictions = torch.argmax(
            outputs.logits,
            dim=1
        )

        test_preds.extend(
            predictions.cpu().numpy()
        )

        test_labels_list.extend(
            labels.numpy()
        )

Generate the final classification report:

Python
print(
    classification_report(
        test_labels_list,
        test_preds,
        target_names=[
            'Negative',
            'Positive'
        ]
    )
)

Output:

bert

The resulting report shows how well the fine-tuned BERT model classifies unseen movie reviews as positive or negative.

You can download the complete code from here.

Comment

Explore