SDAIA Academy · Applied NLP Engineering

From Tokens to Fine-Tuned Language Models

Day 1: Text Representation & Transformer Foundations

AI Engineering Track 3 teaching days + capstone day 12 training hours Hands-on Python
The bootcamp at a glance

Four days, one evolving system

Day 1 · Represent

Tokenization, embeddings, transformers, decision models.

Semantic search + classifier

Day 2 · Generate

LLMs, decoding, prompting, structured outputs, RAG, model selection.

End-to-end ticket assistant

Day 3 · Adapt

Fine-tuning encoders, LoRA for LLMs, evaluation and serving.

Fine-tuned model + demo

Day 4 · Demonstrate

Capstone presentations, live demos and technical Q&A.

Team capstone
Our goal: leave able to build, evaluate and adapt NLP systems, and to choose between embeddings, encoder classifiers, generative LLMs and fine-tuning for a given problem.
Course philosophy

Engineering first, theory on demand

  • ~25% concepts and demos, ~75% coding. Every session ends in a lab.
  • Modern from minute one. Transformers from Day 1. TF-IDF appears only as a baseline to beat.
  • Measure, don't assume. Every model gets evaluated before and after we change it.
  • Right tool for the job. A 100M-parameter classifier often beats a 7B LLM on cost, latency and accuracy.

You should already know

Python, NumPy/pandas, train/test splits, basic scikit-learn.

You don't need

Prior transformer, PyTorch or GPU experience.

You will leave with

12 working notebooks, a capstone repo and a reusable evaluation habit.

One case study, all week

Intelligent Support Ticket Processing

Given an English or Arabic customer message: identify the intent, find similar past cases, assign a category and priority, and draft a grounded reply.

Messageلم يصلني المبلغ المسترد حتى الآن
→
Tokenize + embedDay 1
→
Classifyintent · category · priority
Day 1 → Day 3
→
Retrievesimilar tickets + policy
Day 1 → Day 2
→
Generategrounded JSON reply
Day 2 → Day 3
The data we use

Realistic, bilingual, labelled

textintentcategorypriority
I need help getting a refund for my orderget_refundREFUNDhigh
how do I change the delivery address??change_shipping_addressSHIPPINGmedium
أريد إلغاء اشتراكي في النشرة البريديةnewsletter_subscriptionSUBSCRIPTIONlow
you charged me twice, fix it NOW!!payment_issuePAYMENTurgent
Tickets: about 27k English customer-support messages (Bitext), 27 intents in 11 categories, each with an agent reply.
Arabic test set: hand-written MSA and Gulf-dialect tickets with the same labels.
Help-center KB: short policy articles (refunds, shipping, accounts) used for RAG.
Today

Day 1 agenda

Session 1 · TokenizationWelcome, setup, from text to token IDs, Arabic specifics50 min
Break10 min
Session 2 · Embeddings & semantic searchDense vectors, cosine similarity, pooling, retrieval metrics50 min
Long break20 min
Session 3 · Inside the transformerSelf-attention, positions, encoder vs. decoder, Hugging Face50 min
Break10 min
Session 4 · Decision modelsZero-shot, embedding classifiers, metrics, thresholds50 min
Lab environment

Run everything before you need it

  1. Open the course repo in Google Colab (GPU runtime) or locally with uv.
  2. Run 00_setup_check.ipynb: it installs packages, checks the GPU and pre-downloads every model we use.
  3. Each lab opens with a Setup cell and ends with Checkpoint questions.
  4. Cells marked ✱ Stretch are optional for fast finishers.
git clone https://github.com/SDAIAAcademy/applied-nlp
cd applied-nlp/labs
uv sync                 # creates .venv from pyproject
uv run python prepare_data.py
uv run jupyter lab
No GPU locally? Days 1–2 run on CPU. On Day 3 the fine-tuning labs need a GPU, so use Colab for those.
Session 1

Tokenization: from text to integers

01
Why care?

Models never see text. They see integers.

Text

My refund hasn't arrived

Tokens
My␣refund␣hasn't␣arrived
IDs
515910061263127754705284

Illustrative split. You'll see the exact one per tokenizer in the lab.

Cost: APIs bill per token, so more tokens means a bigger bill.
Latency: generation time grows with output tokens.
Context: limits like 512 or 128k are counted in tokens, not words.
Quality: a word shredded into 8 pieces is harder to model.
The pipeline

What a tokenizer actually does

NormalizeUnicode NFKC, lowercase?, strip accents?
→
Pre-tokenizesplit on spaces / punctuation / bytes
→
ModelBPE · WordPiece · Unigram
→
Post-processadd [CLS] [SEP], chat tokens
→
Encodeinput_ids + attention_mask

BPE

Starts from characters or bytes and merges the most frequent pairs. Used by GPT, Llama, Qwen and most LLMs.

WordPiece

Like BPE, but merges by likelihood. Continuation pieces start with ##. Used by BERT and AraBERT.

Unigram (SentencePiece)

Prunes a large vocabulary probabilistically and treats spaces as ▁. Used by T5, XLM-R and mT5.

Granularity trade-off

Why subwords won

UnitVocabularyUnknown wordsSequence lengthVerdict
WordsHuge (millions with Arabic morphology)Frequent [UNK]ShortBrittle
Characters / bytesTiny (256 bytes)NoneVery longSlow, hard to learn
Subwords30k–250kNone (byte fallback)ModerateThe default

Frequent words stay whole

orderrefundaccount

Rare words decompose into known pieces

unsubscribing
BPE in 60 seconds

Learn merges from a corpus, then apply them in order

StepMost frequent pairCorpus after merge
0–l o w · l o w e r · n e w e s t
1l + olo w · lo w e r · n e w e s t
2lo + wlow · low e r · n e w e s t
3e + slow · low e r · n e w es t
…repeat until vocabulary size is reached
Key insight: the vocabulary reflects the training corpus. A tokenizer trained mostly on English has few Arabic merges.
Byte-level BPE starts from the 256 bytes, so nothing is ever unknown. But an Arabic letter takes 2 bytes in UTF-8, so Arabic can blow up to many tokens per word.
Same text, different tokenizers

Each model family has its own conventions

WordPiece (BERT): continuation pieces start with ##

[CLS]token##izationisfun[SEP]

Byte-level BPE (GPT-2, Qwen): a leading space is part of the token, shown as Ġ

tokenizationĠisĠfun

SentencePiece Unigram (XLM-R): word starts are marked with ▁

<s>▁tokenization▁is▁fun</s>
Rule #1: always use the tokenizer that ships with the model. A model fed IDs from another vocabulary sees garbage.
Arabic is different

Rich morphology and many ways to write a letter

One word, a whole sentence

وسيكتبونها

= و + س + يكتب + ون + ها : "and they will write it"

Dialects and Arabizi

وين طلبي؟ (Gulf) vs. أين طلبي؟ (MSA) vs. "wen 6alabi"

VariationExampleCommon normalization
Alef formsأ إ آ ا→ ا
Taa marbutaة / ه→ ه (search only)
Alef maqsuraى / ي→ ي
Diacriticsطَلَبٌ→ طلب
Tatweelطـــلب→ طلب
Normalize for search and matching, but be careful normalizing text sent to an LLM. Its tokenizer was trained on raw text.
A metric you should track

Fertility: tokens per word

Fertility = n_tokens / n_words. Lower is usually better: cheaper, faster, more text fits in context.

  • English-centric byte-level tokenizers can produce several times more tokens for Arabic than for English.
  • Multilingual (XLM-R, Qwen) and Arabic-specific (AraBERT, Jais, ALLaM) tokenizers close the gap.
  • Same message → different bill and different context budget.

Your lab task: fill in this table

TokenizerEN fertilityAR fertility
gpt2??
google-bert/bert-base-multilingual-cased??
FacebookAI/xlm-roberta-base??
aubmindlab/bert-base-arabertv02??
Qwen/Qwen2.5-0.5B-Instruct??
What the model receives

IDs, masks, padding and truncation

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("FacebookAI/xlm-roberta-base")
batch = tok(
    ["Where is my order?", "أين طلبي؟ لم يصل بعد"],
    padding=True,          # pad to longest in batch
    truncation=True,       # cut at max_length
    max_length=32,
    return_tensors="pt",
)
batch["input_ids"].shape       # torch.Size([2, n])
batch["attention_mask"][0]     # 1 = real token, 0 = pad
tok.convert_ids_to_tokens(batch["input_ids"][1])
tok.model_max_length           # 512
input_ids: integer positions in the vocabulary.
attention_mask: tells the model to ignore padding.
Truncation: silent data loss. Check the length distribution of your data first.
Special tokens: <s> </s> [CLS] or chat markers like <|im_start|> carry meaning. Keep them.
Engineering checklist

Tokenization mistakes that reach production

  • Mixing a model with another model's tokenizer
  • Truncating the important part (e.g., the actual question at the end of a long email)
  • Estimating cost with word counts instead of token counts
  • Over-normalizing input to an LLM (removing diacritics or casing it relies on)
  • Forgetting the chat template for instruction models (Day 2)
  • Losing character offsets needed to highlight extracted spans
  • Assuming the context limit applies only to input. Output tokens count too.
  • Not testing on dialect, typos, emoji and code-switched text
Lab 1.1 · 25 min

Inspect English and Arabic tokenization

labs/D1_1_tokenization.ipynb ↗

You will compare five tokenizers on our support tickets and quantify the "Arabic token tax".

  • Tokenize a ticket, map IDs ↔ tokens, decode back 5'
  • Inspect special tokens, padding, attention masks 5'
  • Compute fertility for EN vs. AR across 5 tokenizers 8'
  • Write an Arabic normalizer; measure its effect 5'
  • Find the token-length percentile for max_length 2'
  • ✱ Train a tiny BPE tokenizer on the tickets
Checkpoint

A ticket is 60 words in English and 60 words in Arabic. Using GPT-2's tokenizer, which is most likely?

Both produce about the same number of tokens
The Arabic version produces many more tokens
Arabic triggers [UNK] tokens and loses information
B. GPT-2 uses byte-level BPE trained mostly on English: no [UNK]s, but few Arabic merges, so Arabic text splits into many short byte pieces.
Break

10-minute break

Next: Session 2 · Embeddings & semantic search

Session 2

Embeddings: meaning as geometry

02
From IDs to vectors

An embedding is a learned coordinate

  • A token ID indexes a row of an embedding matrix (vocab × d, e.g., 250k × 384).
  • Training pushes rows so that things used similarly end up close.
  • Transformers then make vectors contextual: the same token gets different vectors in different sentences.
  • A sentence embedding compresses a whole text into one vector, which is ideal for search and clustering.
refunds delivery account / password "where is my money back?" "وين فلوسي؟"
2-D projection for intuition. Real embeddings have 384 to 1024+ dimensions.
Similarity

Cosine similarity: compare directions, not lengths

cos(a, b) = a·b / (‖a‖‖b‖)

  • Range −1 … 1. In practice most text pairs fall between 0 and 1.
  • Normalize once (‖v‖ = 1) and cosine becomes a plain dot product, so a matrix multiply ranks the whole corpus.
  • Scores are relative. A 0.82 means nothing across models; compare rankings, not raw values.
import numpy as np

def cosine(a, b):
    return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))

# normalized matrices: one matmul ranks everything
E = corpus_emb / np.linalg.norm(corpus_emb, axis=1, keepdims=True)
q = query_emb / np.linalg.norm(query_emb)
scores = E @ q                    # shape: (n_docs,)
top5 = np.argsort(-scores)[:5]
Sentence embeddings

Token vectors → pooling → one vector per text

Ticket"refund not received"
→
Encodertransformer (Session 3)
→
Token vectorsn × 384
→
Poolingmean over real tokens (or [CLS])
→
Normalize1 × 384, ‖v‖=1

Why not just average a raw BERT?

You can, but it's mediocre. Sentence-embedding models are further trained with contrastive pairs (question ↔ answer, paraphrases), so similar meanings are pulled together and the rest pushed apart.

Bi-encoder vs. cross-encoder

Bi-encoder: embed each text separately. Fast, indexable, used for retrieval.
Cross-encoder: read both texts together. Accurate, slow, used for reranking the top-k (Day 2).

Model selection

Choosing an embedding model

ModelLanguagesDimParamsNotes
sentence-transformers/all-MiniLM-L6-v2English38422MTiny, fast baseline
paraphrase-multilingual-MiniLM-L12-v250+ incl. Arabic384118MSimple, symmetric similarity
intfloat/multilingual-e5-small~100 incl. Arabic384118MNeeds query: / passage: prefixes. Our default.
BAAI/bge-m3100+1024568MStrong, 8k context, heavier
Check MTEB for retrieval tasks in your language, then confirm on your own data.
Read the model card: prefixes, max length, pooling, license.
Never mix vectors from two models in one index.
Architecture

Semantic search = offline indexing + online query

Index time (once / nightly)

Historical tickets + resolutions
↓
encode("passage: …"), normalize
↓
Store vectors + metadata (NumPy, FAISS, pgvector…)

Query time (every request)

New ticket
↓
encode("query: …"), normalize
↓
Top-k by dot product → return cases + their resolutions
Code

Semantic search in ~15 lines

from sentence_transformers import SentenceTransformer
import faiss

model = SentenceTransformer("intfloat/multilingual-e5-small")

docs = history["text"].tolist()
doc_emb = model.encode([f"passage: {d}" for d in docs],
                       normalize_embeddings=True, batch_size=64, show_progress_bar=True)

index = faiss.IndexFlatIP(doc_emb.shape[1])      # inner product == cosine (normalized)
index.add(doc_emb)

def search(query, k=5):
    q = model.encode([f"query: {query}"], normalize_embeddings=True)
    scores, ids = index.search(q, k)
    return history.iloc[ids[0]].assign(score=scores[0])

search("لم يصلني المبلغ المسترد")   # Arabic query, English history
Evaluation

Is the search any good? Measure it.

MetricQuestion it answers
Recall@k / Hit@kIs a relevant result anywhere in the top k?
Precision@kWhat fraction of the top k is relevant?
MRRHow high is the first relevant result? (1/rank)
nDCG@kGraded relevance with position discount
Cheap relevance labels: in our data, a retrieved ticket with the same intent as the query counts as relevant. You get an eval set for free.
Better: 50–100 real queries with hand-judged relevant documents. Small, but it's ground truth.
Leakage: the query must not be in the index. Search with test tickets against train tickets.
Failure modes

Where pure semantic search fails

  • Exact identifiers: order #A-55821, product codes, IBANs. Embeddings blur these, so add keyword search (BM25) and combine → hybrid.
  • Negation: "I don't want a refund" sits close to "I want a refund".
  • Length mismatch: a 5-word query vs. a 2-page document; chunk the documents (Day 2).
  • Domain jargon the model never saw → evaluate, then consider fine-tuning embeddings.
  • Cross-lingual gaps: dialect queries against MSA or English documents. Test them explicitly.
  • Stale index: new policies never re-embedded.
Lab 1.2 · 25 min

Build semantic search over historical tickets

labs/D1_2_embeddings_semantic_search.ipynb ↗

Embed thousands of tickets, search in English and Arabic, and put a number on retrieval quality.

  • Embed sentences; compute a similarity matrix and a heatmap 5'
  • Compare mean pooling by hand with the library output 5'
  • Index historical tickets with NumPy, then FAISS 5'
  • Evaluate Hit@5 and MRR on test tickets (same-intent relevance) 5'
  • Cross-lingual: Arabic queries → English history 5'
  • ✱ Compare two embedding models, or plot a 2-D UMAP of intents
Checkpoint

Users search for "invoice INV-2024-00731". Semantic search returns other invoices but not that one. Best fix?

Switch to a bigger embedding model
Add keyword (BM25) search and merge the results: hybrid search
Lower the similarity threshold
B. Exact identifiers are a lexical problem. Embeddings capture "about invoices", and keyword search catches the exact ID.
Long break

20-minute break

Next: Session 3 · Inside the transformer

Session 3

Inside the transformer

03
The problem attention solves

Meaning depends on context

"You charged me twice for one order"

→ billing, PAYMENT

"My phone won't charge since the update"

→ technical issue

  • A static embedding gives "charge" one vector, a blend of both meanings.
  • A transformer lets each token look at every other token and update its vector, so "charge" near "twice" and "order" becomes billing-flavoured.
In the lab you'll extract the contextual vector of the same word in two sentences and watch their cosine similarity drop.
Self-attention

Every token asks, advertises and shares

Query (Q): "what am I looking for?"
Key (K): "what do I contain?"
Value (V): "what I pass on if you attend to me"

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

  1. Project each token vector into Q, K, V (learned matrices).
  2. Score every pair: q_i · k_j, scaled by √d.
  3. Softmax each row into weights that sum to 1.
  4. New vector = weighted sum of values.
Cost grows with n² in sequence length, which is why context windows are limited and long contexts are expensive.
Seeing it

An attention map (one head, one layer)

youchargedmetwicefororder youchargedmetwicefororder rows = token attending (query) · columns = attended token (key)
  • "charged" puts weight on "order" and "twice": evidence for the billing sense.
  • Multi-head: 12+ heads per layer each learn different patterns (syntax, coreference, position…).
  • Many layers (6–80+) stack these updates.
Caution: attention weights are a diagnostic, not an explanation of the decision.
The block

A transformer is the same block, stacked

Token embeddings + positional information
↓
× N layers
Multi-head self-attention → add & normalize
↓
Feed-forward network (per token) → add & normalize
↓
Head (task-specific): classify · predict mask · next token
Positions: attention is order-blind, so position must be injected. Older models use learned absolute positions; modern LLMs use RoPE (rotary), which extends better to long contexts.
Residual + norm: lets very deep stacks train stably.
The head is small. Pretraining learns the body, and your task usually only needs a new head (Day 3).
Three families

Encoder, decoder, encoder–decoder

Encoder-onlyDecoder-onlyEncoder–decoder
AttentionBidirectional: sees left and rightCausal: sees only the pastBidirectional encoder + causal decoder
PretrainingMasked language modelingNext-token predictionSpan corruption / denoising
Best atUnderstanding: classification, NER, embeddingsGeneration, chat, reasoning, toolsTranslation, summarization
ExamplesBERT, XLM-R, AraBERT, CAMeLBERT, ModernBERTGPT, Llama, Qwen, Jais, ALLaMT5, mT5, BART, AraT5
In this courseDay 1 classifiers, Day 3 fine-tuningDay 2 LLM apps, Day 3 LoRAMentioned only
Rule of thumb: if the output is a label, span or vector, start with an encoder. If the output is free text, you need a decoder.
Pretraining objective

Masked LM vs. next-token prediction

Encoder: fill the blank

My<mask>hasnotarrivedyet,Iordereditlastweek
Predictionp
order0.40
package0.22
refund0.08

Illustrative probabilities

Decoder: continue the text

Myorderhasnot…
Next tokenp
arrived0.52
been0.31
shipped0.07

Tomorrow: sampling from this distribution

Hugging Face mental model

Tokenizer + body + head

from transformers import AutoTokenizer, AutoModel
import torch

name = "google-bert/bert-base-multilingual-cased"
tok = AutoTokenizer.from_pretrained(name)
model = AutoModel.from_pretrained(name, output_attentions=True)

enc = tok("You charged me twice for one order", return_tensors="pt")
with torch.no_grad():
    out = model(**enc)

out.last_hidden_state.shape  # (batch, seq_len, hidden) = (1, n, 768)
len(out.attentions)          # 12 layers
out.attentions[0].shape      # (1, heads=12, n, n)
ClassHead
AutoModelnone: hidden states
…ForSequenceClassificationlabel per text
…ForTokenClassificationlabel per token (NER)
…ForMaskedLMfill-mask
…ForCausalLMnext token (LLMs)
pipeline("task", model=…) wraps tokenize → model → post-process for quick experiments.
Practical model choice

Reading a model card like an engineer

Fit

  • Languages and dialects covered
  • Training data domain
  • Max sequence length

Cost

  • Parameters → memory, latency
  • CPU-viable? Quantized versions?
  • Throughput at your batch size

Risk

  • License (commercial use?)
  • Known biases and limitations
  • Maintained? Reproducible revision?
Pin the revision in production: from_pretrained(name, revision="<commit-sha>"), because models on the Hub can change under you.
Lab 1.3 · 25 min

Open the black box

labs/D1_3_transformers_inside.ipynb ↗

Look at hidden states and attention, implement attention in NumPy, and watch context change a word's vector.

  • Implement scaled dot-product attention in 10 lines of NumPy 6'
  • Inspect hidden-state and attention shapes for a ticket 4'
  • Plot an attention heatmap for chosen layer/head 5'
  • Contextual embeddings: "charge" in two contexts 5'
  • Fill-mask in English and Arabic with XLM-R 5'
  • ✱ Causal mask: compare encoder vs. decoder attention patterns
Checkpoint

You need to route 2 million tickets per day into 27 intents with low latency. Which family do you start with?

An encoder (e.g., XLM-R or a multilingual MiniLM) with a classification head
A 70B decoder LLM with a prompt listing all intents
An encoder–decoder translation model
A. The output is a label, the volume is high and latency matters. Small encoders are cheap, fast and accurate after fine-tuning. Keep the LLM for cases where you need generated text.
Break

10-minute break

Next: Session 4 · Decision models

Session 4

Decision models: when the answer is a label

04
Why this matters

Most production NLP is classification

Text"you charged me twice, fix it NOW"
↓
Decision modellabel + confidence
↓
Actionroute to Billing queue · priority = urgent · SLA 2h
  • Routing, triage, prioritization
  • Sentiment and complaint detection
  • Content moderation and spam
  • Compliance flags (PII present?)
  • Guardrails on LLM inputs and outputs
Decision models are measurable, fast, cheap and auditable. They are often the backbone that LLM features sit on.
The ladder

Six ways to classify a ticket

ApproachLabelled dataLatency / costTypical qualityWhen
1 · Keyword rulesnoneµs / freebrittleHard constraints, bootstrapping
2 · TF-IDF + logistic regressionhundreds+µs / freedecent baselineAlways run it as a baseline
3 · Zero-shot (NLI / label embeddings)nonems / lowfairCold start, changing labels
4 · Frozen embeddings + linear headtens–hundredsms / lowgoodFast, strong default
5 · Fine-tuned encoderhundreds–thousandsms / lowbest per $Day 3
6 · LLM promptingfew examples100s ms–s / $$good, variableDay 2: low volume, nuance
Zero-shot classification

Reuse a model trained for "does A imply B?"

An NLI model scores whether a premise entails a hypothesis. Turn each label into a hypothesis:

Premise: "I was billed twice for the same order"
Hypothesis: "This customer message is about payment."
  • No training data needed; labels can change at runtime.
  • Label wording matters a lot: "billing problem" ≠ "PAYMENT".
  • Cost scales with the number of labels (one pass per label).
import torch
from transformers import pipeline

clf = pipeline(
    "zero-shot-classification",
    model="MoritzLaurer/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7",
    dtype=torch.float32,   # checkpoint is fp16: slow on CPU
)
labels = ["refund", "delivery", "payment problem",
          "account access", "order changes", "invoice"]
clf("لقد تم خصم المبلغ مرتين من بطاقتي",
    candidate_labels=labels,
    hypothesis_template="This customer message is about {}.")
# {'labels': [...], 'scores': [...]}  label wording changes the ranking
Embeddings + a linear head

The best value-for-effort classifier

from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

enc = SentenceTransformer("intfloat/multilingual-e5-small")
X_tr = enc.encode(["query: " + t for t in train.text], normalize_embeddings=True)
X_te = enc.encode(["query: " + t for t in test.text],  normalize_embeddings=True)

clf = LogisticRegression(max_iter=2000, C=4.0)
clf.fit(X_tr, train.intent)

pred = clf.predict(X_te)
proba = clf.predict_proba(X_te).max(axis=1)   # confidence
print(classification_report(test.intent, pred, digits=3))
Encoder is frozen: encode once, then train in seconds on CPU. Swapping classifiers is cheap.
Bonus: the same vectors power search. One embedding pass, two features.
Compare against TF-IDF + LR. If the gain is small, ask why.
Evaluation

Accuracy is not enough

MetricMeaning for "complaint" class
PrecisionOf tickets we flagged as complaint, how many were?
RecallOf real complaints, how many did we catch?
F1Harmonic mean of the two
Macro-F1Average over classes: rare classes count equally
Weighted-F1Average weighted by class frequency
Imbalance trap: if 95% of tickets are "other", a model that always says "other" has 95% accuracy and zero use.
Confusion matrix shows which classes get mixed up (e.g., get_refund vs track_refund). That's where you focus labelling and fixes.
Slice it: report English vs. Arabic, short vs. long, dialect vs. MSA.
Thresholds and abstention

Let the model say "I'm not sure"

confidence threshold → 100%0% chosen τ accuracy on accepted coverage (auto-handled)
Raising the threshold improves accuracy on what you keep, but more goes to humans.
def route(text, tau=0.6):
    p = clf.predict_proba(embed([text]))[0]
    i = p.argmax()
    if p[i] < tau:
        return {"intent": None, "action": "human_review"}
    return {"intent": clf.classes_[i], "conf": float(p[i])}
  • Pick τ from validation data, not test.
  • Thresholds can be per class: stricter for costly errors.
  • Raw probabilities are often over-confident. Calibrate if the score drives decisions.
Think like the business

Not all errors cost the same

Missed urgent complaint

Customer churns, social-media escalation. High cost, so optimize recall for complaint and payment_issue.

Wrong queue

An agent re-routes it, costing minutes. Medium cost.

Newsletter mislabeled

Barely noticed. Low cost, so automate aggressively.

Write the cost table first, then pick metrics and thresholds that minimise expected cost. That's what you defend in your capstone.
Lab 1.4 · 30 min

Ticket classification + Day 1 integration

labs/D1_4_decision_models.ipynb ↗

Climb the ladder (TF-IDF → zero-shot → embeddings + LR), evaluate properly, add abstention, and combine it with this morning's search.

  • TF-IDF + logistic regression baseline 5'
  • Zero-shot NLI on categories; try two label wordings 7'
  • Embeddings + logistic regression on intents 5'
  • Report, confusion matrix, EN vs. AR slice 5'
  • Coverage–accuracy curve; choose τ on validation 4'
  • Integrate: process_ticket() = classify + top-3 similar 4'
  • ✱ kNN-vote classifier from the search index; compare errors
Day 1 deliverable

What you built today

TicketEN or AR
→
Embedmultilingual-e5-small
→
Searchtop-k similar historical cases
→
Classifyintent + confidence → abstain
→
EvaluateHit@5, MRR, macro-F1, by language

Remember

Tokens drive cost and context. Measure fertility for Arabic.

Remember

Embeddings + a linear head is a strong default. Always compare with a baseline.

Remember

Pick thresholds from error costs, on validation data.

Capstone: start thinking

Form teams of 2–3 by the end of today

1 · Ticket Assistant

Classify tickets, retrieve similar cases, draft grounded replies.

2 · Arabic Document Intelligence

Extract and categorize information from Arabic documents; answer questions with evidence.

3 · Voice of Customer

Classify feedback, cluster issues with embeddings, generate insight summaries.

Tonight (optional, 30 min): pick a use case and find or describe your data. Bring one sentence: "Our system takes ___ and produces ___, measured by ___."
Tomorrow · Day 2

LLMs & Applied NLP Systems

From predicting labels to generating grounded, structured answers.

Generative LLMs

Next-token prediction, chat templates, decoding

Structured outputs

Prompting, few-shot, validated JSON

RAG

Chunking, retrieval, reranking, grounding

Model selection

Encoder vs. LLM: accuracy, latency, cost