← Back to list

BERT Complete Architecture Guide: From Embeddings to Fine Tuning for Text Classification

BERT = Bidirectional Encoder Representations from Transformers

Prem Vishnoi(cloudvala) in NextGenAI · 2026-06-10 16:34 · 100 claps · 7.4 min read paywalled
#artificial-intelligence #machine-learning #text-classification #bert #ai
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval ML · Machine Learning AI · AI · General GEN · Genomics & Sequencing EDU · Education & Learning 🏛️ · Architecture

BERT Complete Architecture Guide: From Embeddings to Fine Tuning for Text Classification

BERT = Bidirectional Encoder Representations from Transformers

BERT is a language model that reads a full sentence from both left and right side, so it understands the meaning of a word using the full context.

Why BERT exists

Before BERT (2018), NLP models read text in one direction only — left to right (GPT) or right to left. They never saw the full context of a word simultaneously.

The problem:

"I went to the bank to deposit money"
"I sat by the river bank"

A left-to-right model reading “bank” hasn’t seen “deposit” or “river” yet — it guesses context too early.

Bert Model

Bert Model

BERT solved this by reading the entire sentence at once in both directions simultaneously — hence Bidirectional.

This is the main power of BERT.

It does not only look at words. It looks at words + context + position + relationship with other words.

When to use BERT

Use BERT when your data is text.

Product review sentiment Seller complaint classification Delivery issue detection Customer support ticket routing Fake review detection Product quality complaint detection Supplier feedback classification Return reason classification

The full architecture — every component

12 Transformer encoder layers
12 attention heads
768 hidden dimensions
110 million parameters
Maximum input length = 512 tokens

How BERT works — every layer explained

Text
 ↓
Tokenizer
 ↓
Token Embedding + Segment Embedding + Position Embedding
 ↓
Transformer Encoder Layer 1
 ↓
Transformer Encoder Layer 2
 ↓
...
 ↓
Transformer Encoder Layer 12
 ↓
Classification Head
 ↓
Prediction

1. Input layer — three embeddings summed

Every token gets three embeddings added together:

Final input vector = Token embedding + Segment embedding + Position embedding

Token embedding — what is the word (vocabulary of 30,000 wordpieces) Segment embedding — is this sentence A or B (for sentence-pair tasks)

Position embedding — where is this word (position 0, 1, 2… up to 512)

Each embedding is 768 dimensions. Sum them → one 768-dim vector per token.

BERT does not directly understand raw words.
It converts every word/token into numbers.
For every token, BERT creates 3 vectors:
What is the word?
"product"
"good"
"bad"
"delivery"
"refund"

Each token is converted into a vector.

product → [0.12, -0.45, 0.88, ...]
good    → [0.67, 0.21, -0.34, ...]

In BERT-base, each vector has:

768 numbers

so
"product" = 768-dimensional vector

2. Self-attention — the core mechanism

Every token attends to every other token simultaneously. For each attention head:

Q = X · Wq    # Query — "what am I looking for?"
K = X · Wk    # Key   — "what do I contain?"
V = X · Wv    # Value — "what do I return?"

Attention(Q,K,V) = softmax(Q · Kᵀ / √dk) · V

BERT-base uses 12 heads in parallel, each with dk=64 dims. Outputs concatenated → linear projection back to 768.

The √dk scaling prevents dot products from growing too large and saturating softmax.

Every word looks at every other word and decides which words are important for understanding its meaning.

Example

The delivery was not fast.

not

fast = positive
not fast = negative

Self-attention helps BERT understand this.
good ↔ not = strong relationship
good ↔ product = medium relationship
good ↔ the = weak relationship
Sentence A: Customer said product is damaged.
Sentence B: Seller says product was shipped safely.

BERT needs to know which token belongs to sentence A and which token belongs to sentence B.

For simple sentiment classification, usually we only have one sentence, so segment embedding is less important.

3. Feed-forward network — per token, independently

FFN(x) = GELU(x · W1 + b1) · W2 + b2

W1: 768 → 3072   (expand 4×)
W2: 3072 → 768   (compress back)

GELU activation (smoother than ReLU) — empirically better for language.

Where is the word in the sentence?

Because BERT sees all words together, it needs position information.

Dog bites man.
Man bites dog.

Same words, different meaning.

Position matters.

So BERT adds position numbers:

position 0
position 1
position 2
...

4. Pre-training tasks — why BERT is powerful

Masked Language Model (MLM):

  • Randomly mask 15% of tokens with [MASK]
  • Model must predict the original token
  • Forces bidirectional context learning

Next Sentence Prediction (NSP):

  • Feed two sentences, predict if B follows A
  • Trains [CLS] token to capture sentence-level meaning

Final input vector

final vector = token meaning + sentence info + position info
"delivery" vector 
= token embedding 
+ segment embedding 
+ position embedding

Lazada working code product review classifier

This applies BERT to classify Lazada supplier/product feedback from O2O reviews or B2B complaint text:

transformers — Hugging Face library. Contains pre-trained BERT weights, tokenizer, and Trainer API. torch PyTorch. The tensor computation engine BERT runs on.

# Cell 1
%pip install transformers torch

import torch                                        # tensor math engine
import pandas as pd                                 # dataframe handling
import numpy as np                                  # array operations

from transformers import (
    AutoTokenizer,                                  # converts text → token IDs
    AutoModelForSequenceClassification,             # BERT + classification head
    TrainingArguments,                              # hyperparameter config object
    Trainer                                         # training loop engine
)
from torch.utils.data import Dataset                # base class for your dataset
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

#AutoTokenizer — auto-detects the right tokenizer for any model name. For bert-base-multilingual-cased it loads the WordPiece tokenizer with 119,547 vocabulary tokens covering 104 languages including Thai.
#AutoModelForSequenceClassification — loads BERT's 12 transformer layers + adds a classification head on top:

# Cell 3 — sample lazada TH review data
# Replace with your actual review/complaint table from Delta
data = {
    'text': [
        "สินค้าหมดอายุก่อนกำหนด ไม่ควรวางขาย",          # expired product
        "ราคาถูก คุณภาพดี คุ้มค่ามาก",                    # good value
        "จัดส่งช้ามาก รอนานเกิน 2 สัปดาห์",               # slow delivery
        "พนักงานบริการดีมาก ช่วยเหลือดี",                  # good service
        "สินค้าแตกหักมาในกล่อง บรรจุภัณฑ์แย่มาก",         # damaged goods
        "ของสดคุณภาพดี สดมาก ราคาเหมาะสม",                # fresh produce
        "ไม่ตรงกับรูปที่โฆษณา หลอกลวงลูกค้า",              # misrepresentation
        "โปรโมชั่นดีมาก ประหยัดได้เยอะ",                   # good promo
    ],
    'label': [0, 1, 0, 1, 0, 1, 0, 1]  # 0=negative, 1=positive
}
df = pd.DataFrame(data)
print(f"Samples: {len(df)} | Positive: {df['label'].sum()} | Negative: {(df['label']==0).sum()}")

# Cell 4 — load multilingual BERT (handles Thai + English mixed text)
MODEL_NAME = "bert-base-multilingual-cased"   # supports Thai
# Alternative: "airesearch/wangchanberta-base-att-spm-uncased"  ← Thai-specific, better

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

# Test tokenization on Makro text
sample = "สินค้าหมดอายุ lazada branch Bangkok"
tokens = tokenizer(sample, return_tensors="pt")
print(f"Input IDs : {tokens['input_ids']}")
print(f"Token count: {tokens['input_ids'].shape[1]}")

# Cell 5 — dataset class
class LazadaReviewDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len=128):
        self.texts     = texts
        self.labels    = labels
        self.tokenizer = tokenizer
        self.max_len   = max_len

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

    def __getitem__(self, idx):
        enc = self.tokenizer(
            self.texts[idx],
            max_length=self.max_len,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )
        return {
            'input_ids':      enc['input_ids'].squeeze(),
            'attention_mask': enc['attention_mask'].squeeze(),
            'labels':         torch.tensor(self.labels[idx], dtype=torch.long)
        }

# Split
train_texts, val_texts, train_labels, val_labels = train_test_split(
    df['text'].tolist(), df['label'].tolist(),
    test_size=0.2, random_state=42, stratify=df['label']
)

train_dataset = LazadaReviewDataset(train_texts, train_labels, tokenizer)
val_dataset   = LazadaReviewDataset(val_texts,   val_labels,   tokenizer)

print(f"Train: {len(train_dataset)} | Val: {len(val_dataset)}")

# Cell 6 — load BERT with classification head
model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=2,           # negative / positive
    id2label={0:"negative", 1:"positive"},
    label2id={"negative":0, "positive":1}
)

# BERT has 110M params — freeze base, fine-tune only classifier
# Unfreeze last 2 layers + classifier for faster training
for name, param in model.named_parameters():
    if 'encoder.layer.10' in name or \
       'encoder.layer.11' in name or \
       'classifier' in name:
        param.requires_grad = True
    else:
        param.requires_grad = False

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total     = sum(p.numel() for p in model.parameters())
print(f"Trainable params: {trainable:,} / {total:,}  ({trainable/total:.1%})")

# Cell 7 — training
def compute_metrics(eval_pred):
    logits, labels = eval_pred
    preds = np.argmax(logits, axis=1)
    report = classification_report(labels, preds,
                                   target_names=['negative','positive'],
                                   output_dict=True)
    return {
        'accuracy':  report['accuracy'],
        'f1_neg':    report['negative']['f1-score'],
        'f1_pos':    report['positive']['f1-score'],
        'f1_macro':  report['macro avg']['f1-score'],
    }

training_args = TrainingArguments(
    output_dir            = '/tmp/makro_bert_reviews',
    num_train_epochs      = 5,
    per_device_train_batch_size = 8,
    per_device_eval_batch_size  = 8,
    learning_rate         = 2e-5,        # standard BERT fine-tune LR
    warmup_steps          = 10,
    weight_decay          = 0.01,
    evaluation_strategy   = 'epoch',
    save_strategy         = 'epoch',
    load_best_model_at_end= True,
    metric_for_best_model = 'f1_macro',
    logging_steps         = 5,
    report_to             = 'none'       # set to 'mlflow' to log to your experiment
)

trainer = Trainer(
    model           = model,
    args            = training_args,
    train_dataset   = train_dataset,
    eval_dataset    = val_dataset,
    compute_metrics = compute_metrics
)

trainer.train()

# Cell 8 — inference on new Makro text
def predict_sentiment(texts, model, tokenizer, threshold=0.5):
    model.eval()
    results = []

    for text in texts:
        enc = tokenizer(
            text,
            max_length=128,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )
        with torch.no_grad():
            logits = model(**enc).logits
            probs  = torch.softmax(logits, dim=1).squeeze()

        results.append({
            'text':           text,
            'sentiment':      'positive' if probs[1] > threshold else 'negative',
            'confidence':     float(probs.max()),
            'prob_positive':  float(probs[1]),
            'prob_negative':  float(probs[0])
        })

    return pd.DataFrame(results)

# Test on new Makro product/supplier feedback
new_reviews = [
    "สินค้า Nestle คุณภาพดีมาก ส่งตรงเวลา",
    "supplier ส่งสินค้าไม่ครบ ขาดไป 3 รายการ",
    "ราคา Makro แพงกว่า Big C มาก",
    "Fresh food zone ดีขึ้นมากเลย สดสะอาด",
    "แอปใช้งานยาก สั่งของแล้วระบบค้าง"
]

results = predict_sentiment(new_reviews, model, tokenizer)
print(results[['text','sentiment','confidence']].to_string(index=False))

# Cell 9 — save model to MLflow + DBFS
import mlflow

mlflow.set_experiment("/Users/pvishnoi@lazada.com/ML_USE_CASE/bert_reviews")

with mlflow.start_run(run_name="bert_multilingual_makro_v1"):
    mlflow.log_params({
        'model_name':  MODEL_NAME,
        'epochs':      5,
        'lr':          2e-5,
        'max_len':     128,
        'num_labels':  2
    })

    # Log final metrics
    final_metrics = trainer.evaluate()
    mlflow.log_metrics({
        'val_f1_macro': final_metrics['eval_f1_macro'],
        'val_accuracy': final_metrics['eval_accuracy']
    })

    # Save model
    trainer.save_model('/dbfs/tmp/prem/bert_makro_reviews')
    mlflow.log_artifacts('/dbfs/tmp/prem/bert_makro_reviews', 'model')
    print(" Model saved to DBFS and MLflow")

Why BERT improves Makro sales — specifically

The key advantage over LightGBM for these tasks: BERT understands “สินค้าดี” and “good product” as the same sentiment your tabular model cannot do this.




메타데이터
post_id
7d4b398756d2
slug
bert-complete-architecture-guide-from-embeddings-to-fine-tuning-for-text-classification-7d4b398756d2
url
https://medium.com/nextgenllm/bert-complete-architecture-guide-from-embeddings-to-fine-tuning-for-text-classification-7d4b398756d2
canonical_url
https://medium.com/nextgenllm/bert-complete-architecture-guide-from-embeddings-to-fine-tuning-for-text-classification-7d4b398756d2
author_url
https://medium.com/@premvishnoi
status
ok
fetched_at
2026-06-17 08:20:12