{"id":110208,"date":"2026-08-09T20:35:07","date_gmt":"2026-08-09T20:35:07","guid":{"rendered":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/"},"modified":"2026-08-09T20:35:07","modified_gmt":"2026-08-09T20:35:07","slug":"imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning","status":"publish","type":"post","link":"https:\/\/youzum.net\/de\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/","title":{"rendered":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning"},"content":{"rendered":"<p class=\"wp-block-paragraph\">In this tutorial, we develop an end-to-end sentiment analysis workflow using the<a href=\"https:\/\/huggingface.co\/datasets\/stanfordnlp\/imdb\"> <strong>Stanford NLP IMDb<\/strong><\/a> Large Movie Review Dataset and compare classical machine learning with parameter-efficient transformer fine-tuning. We begin by establishing a reproducible environment and auditing the dataset for class ordering, review-length skew, duplicate leakage, and preprocessing artifacts before training a strong TF-IDF and Logistic Regression baseline. We then fine-tune DistilBERT with LoRA through PEFT, evaluate it using accuracy, macro-F1, ROC-AUC, confusion matrices, and ROC curves, and examine threshold selection and probability calibration through Expected Calibration Error and reliability analysis. Beyond headline metrics, we investigate confident errors, performance across review lengths, word-level occlusion saliency, and head-versus-tail truncation to understand how the model reaches its predictions and where long-context limitations affect performance. Finally, we use the unlabeled IMDb split for confidence-based pseudo-labeling, compare the resulting semi-supervised model against our baseline, and save the merged transformer for reusable sentiment inference.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">import importlib.util, subprocess, sys, os, time, random, warnings, inspect, hashlib\nwarnings.filterwarnings(&quot;ignore&quot;)\nos.environ[&quot;TOKENIZERS_PARALLELISM&quot;] = &quot;false&quot;\nos.environ[&quot;WANDB_DISABLED&quot;] = &quot;true&quot;\n_REQUIRED = {\n   &quot;transformers&quot;: &quot;transformers&quot;,\n   &quot;datasets&quot;: &quot;datasets&quot;,\n   &quot;peft&quot;: &quot;peft&quot;,\n   &quot;accelerate&quot;: &quot;accelerate&quot;,\n   &quot;sklearn&quot;: &quot;scikit-learn&quot;,\n}\n_missing = [pkg for mod, pkg in _REQUIRED.items() if importlib.util.find_spec(mod) is None]\nif _missing:\n   print(f&quot;Installing: {&#039;, &#039;.join(_missing)} ...&quot;)\n   subprocess.run([sys.executable, &quot;-m&quot;, &quot;pip&quot;, &quot;install&quot;, &quot;-q&quot;, *_missing], check=True)\n   print(&quot;Done. (If imports fail below, restart the runtime and re-run.)n&quot;)\nimport numpy as np\nimport pandas as pd\nimport torch\nimport matplotlib.pyplot as plt\nfrom datasets import load_dataset\nfrom sklearn.feature_extraction.text import TfidfVectorizer\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.pipeline import make_pipeline\nfrom sklearn.metrics import (accuracy_score, f1_score, roc_auc_score,\n                            classification_report, confusion_matrix, roc_curve)\nfrom transformers import (AutoTokenizer, AutoModelForSequenceClassification,\n                         TrainingArguments, Trainer, DataCollatorWithPadding,\n                         EarlyStoppingCallback, set_seed)\nfrom peft import LoraConfig, get_peft_model, TaskType\ndef _disable_torchao_probe():\n   patched = []\n   try:\n       import peft.import_utils as _piu\n       _piu.is_torchao_available = lambda: False\n       patched.append(&quot;peft.import_utils&quot;)\n   except Exception:\n       pass\n   for _name, _mod in list(sys.modules.items()):\n       if _name.startswith(&quot;peft&quot;) and hasattr(_mod, &quot;is_torchao_available&quot;):\n           _mod.is_torchao_available = lambda: False\n           patched.append(_name)\n   return patched\ntry:\n   import torchao as _tao\n   _v = getattr(_tao, &quot;__version__&quot;, &quot;?&quot;)\n   if tuple(int(x) for x in _v.split(&quot;.&quot;)[:2]) &lt; (0, 16):\n       print(f&quot;[compat] torchao {_v} &lt; 0.16 -&gt; disabling PEFT&#039;s torchao probe: &quot;\n             f&quot;{&#039;, &#039;.join(_disable_torchao_probe())}&quot;)\nexcept Exception:\n   _disable_torchao_probe()\nSEED        = 42\nMODEL_NAME  = &quot;distilbert-base-uncased&quot;\nMAX_LEN     = 256\nN_TRAIN     = 5000\nN_EVAL      = 2000\nN_UNSUP     = 3000\nEPOCHS      = 2\nBATCH       = 16\nLR          = 3e-4\nFULL_RUN    = False\nif FULL_RUN:\n   N_TRAIN, N_EVAL, EPOCHS = 25000, 25000, 3\nset_seed(SEED); random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\nDEVICE = &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;\nprint(&quot;=&quot; * 79)\nprint(f&quot;device={DEVICE} | torch={torch.__version__} | &quot;\n     f&quot;gpu={torch.cuda.get_device_name(0) if DEVICE==&#039;cuda&#039; else &#039;n\/a&#039;}&quot;)\nprint(&quot;=&quot; * 79)\nt0 = time.time()\nraw = load_dataset(&quot;stanfordnlp\/imdb&quot;)\nprint(raw, f&quot;nloaded in {time.time()-t0:.1f}sn&quot;)\nprint(&quot;--- example (truncated) ---&quot;)\nprint(&quot;label:&quot;, raw[&quot;train&quot;][0][&quot;label&quot;], &quot;|&quot;, raw[&quot;train&quot;][0][&quot;text&quot;][:300], &quot;...n&quot;)\nfirst_labels = np.array(raw[&quot;train&quot;][&quot;label&quot;][:5])\nlast_labels  = np.array(raw[&quot;train&quot;][&quot;label&quot;][-5:])\nprint(f&quot;TRAP #1 - split ordering: first 5 labels {first_labels}, &quot;\n     f&quot;last 5 labels {last_labels}  -&gt; ALWAYS shuffle before subsampling.&quot;)\ntrain_full = raw[&quot;train&quot;].shuffle(seed=SEED)\ntest_full  = raw[&quot;test&quot;].shuffle(seed=SEED)\ntrain_ds   = train_full.select(range(min(N_TRAIN, len(train_full))))\neval_ds    = test_full.select(range(min(N_EVAL, len(test_full))))\nprint(f&quot;   after shuffle+subsample: train balance = &quot;\n     f&quot;{np.bincount(train_ds[&#039;label&#039;])}, eval balance = {np.bincount(eval_ds[&#039;label&#039;])}&quot;)\nlens = np.array([len(t.split()) for t in train_full[&quot;text&quot;]])\nq = np.percentile(lens, [50, 75, 90, 95, 99])\nprint(f&quot;nTRAP #2 - length (words): median={q[0]:.0f} p75={q[1]:.0f} p90={q[2]:.0f} &quot;\n     f&quot;p95={q[3]:.0f} p99={q[4]:.0f} max={lens.max()}&quot;)\nprint(f&quot;   ~{(lens &gt; MAX_LEN*0.75).mean()*100:.1f}% of reviews exceed MAX_LEN={MAX_LEN} &quot;\n     f&quot;tokens (rough words-&gt;tokens factor 1.3). Section 9 measures what that costs.&quot;)\nh_tr = {hashlib.md5(t.encode()).hexdigest() for t in raw[&quot;train&quot;][&quot;text&quot;]}\nh_te = {hashlib.md5(t.encode()).hexdigest() for t in raw[&quot;test&quot;][&quot;text&quot;]}\nprint(f&quot;nTRAP #3 - leakage: {len(h_tr &amp; h_te)} exact duplicate reviews across &quot;\n     f&quot;train\/test; {len(raw[&#039;train&#039;])-len(h_tr)} dupes inside train itself.&quot;)\ndef clean(t):\n   return t.replace(&quot;&lt;br \/&gt;&quot;, &quot; &quot;).replace(&quot;&lt;br\/&gt;&quot;, &quot; &quot;).strip()\nplt.figure(figsize=(11, 3.2))\nplt.subplot(1, 2, 1)\nplt.hist(np.clip(lens, 0, 1000), bins=60)\nplt.axvline(MAX_LEN, ls=&quot;--&quot;, color=&quot;k&quot;, label=f&quot;MAX_LEN={MAX_LEN}&quot;)\nplt.title(&quot;Review length (words, clipped at 1000)&quot;); plt.legend()\nplt.subplot(1, 2, 2)\nplt.bar([&quot;neg&quot;, &quot;pos&quot;], np.bincount(raw[&quot;train&quot;][&quot;label&quot;]))\nplt.title(&quot;Train class balance (perfectly balanced)&quot;)\nplt.tight_layout(); plt.show()\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We configure the Colab environment, install the required libraries, apply the PEFT\u2013torchao compatibility fix, and set deterministic seeds for reproducible experiments. We load the Stanford IMDb dataset, shuffle and subsample the train and test splits, and inspect class balance, review-length distributions, duplicate leakage, and HTML artifacts. We also visualize review lengths and label frequencies so we understand the dataset structure before building any models.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">print(\"n\" + \"=\" * 79 + \"n3. TF-IDF BASELINEn\" + \"=\" * 79)\nXtr = [clean(t) for t in train_ds[\"text\"]]; ytr = np.array(train_ds[\"label\"])\nXte = [clean(t) for t in eval_ds[\"text\"]];  yte = np.array(eval_ds[\"label\"])\nt0 = time.time()\ntfidf_clf = make_pipeline(\n   TfidfVectorizer(ngram_range=(1, 2), min_df=2, max_features=300_000,\n                   sublinear_tf=True, strip_accents=\"unicode\"),\n   LogisticRegression(C=8.0, max_iter=2000, n_jobs=-1),\n)\ntfidf_clf.fit(Xtr, ytr)\np_tfidf = tfidf_clf.predict_proba(Xte)[:, 1]\nacc_tfidf = accuracy_score(yte, p_tfidf &gt; 0.5)\nauc_tfidf = roc_auc_score(yte, p_tfidf)\nprint(f\"trained in {time.time()-t0:.1f}s -&gt; acc={acc_tfidf:.4f}  auc={auc_tfidf:.4f}\")\nvec, lr = tfidf_clf.steps[0][1], tfidf_clf.steps[1][1]\nfeats, coefs = np.array(vec.get_feature_names_out()), lr.coef_[0]\norder = np.argsort(coefs)\nprint(\"nmost NEGATIVE n-grams:\", \", \".join(feats[order[:12]]))\nprint(\"most POSITIVE n-grams:\", \", \".join(feats[order[-12:]][::-1]))\nprint(\"n\" + \"=\" * 79 + \"n4. LoRA FINE-TUNINGn\" + \"=\" * 79)\ntok = AutoTokenizer.from_pretrained(MODEL_NAME)\ndef tokenize(batch):\n   return tok([clean(t) for t in batch[\"text\"]], truncation=True, max_length=MAX_LEN)\ntr_tok = (train_ds.map(tokenize, batched=True, remove_columns=[\"text\"])\n                 .rename_column(\"label\", \"labels\"))\nev_tok = (eval_ds.map(tokenize, batched=True, remove_columns=[\"text\"])\n                .rename_column(\"label\", \"labels\"))\nbase = AutoModelForSequenceClassification.from_pretrained(\n   MODEL_NAME, num_labels=2,\n   id2label={0: \"NEGATIVE\", 1: \"POSITIVE\"},\n   label2id={\"NEGATIVE\": 0, \"POSITIVE\": 1},\n)\nlora_cfg = LoraConfig(\n   task_type=TaskType.SEQ_CLS,\n   r=16, lora_alpha=32, lora_dropout=0.05,\n   target_modules=[\"q_lin\", \"v_lin\"],\n   modules_to_save=[\"pre_classifier\", \"classifier\"],\n)\ntry:\n   model = get_peft_model(base, lora_cfg)\nexcept ImportError as e:\n   _disable_torchao_probe()\n   print(f\"[compat] retrying after backend probe failure: {e}\")\n   model = get_peft_model(base, lora_cfg)\nmodel.print_trainable_parameters()\ndef compute_metrics(eval_pred):\n   logits, labels = eval_pred\n   probs = torch.softmax(torch.tensor(logits), dim=-1).numpy()[:, 1]\n   preds = (probs &gt; 0.5).astype(int)\n   return {\"accuracy\": accuracy_score(labels, preds),\n           \"f1_macro\": f1_score(labels, preds, average=\"macro\"),\n           \"roc_auc\": roc_auc_score(labels, probs)}\n_ta = inspect.signature(TrainingArguments.__init__).parameters\n_eval_key = \"eval_strategy\" if \"eval_strategy\" in _ta else \"evaluation_strategy\"\nta_kwargs = dict(\n   output_dir=\".\/imdb_lora\", learning_rate=LR,\n   per_device_train_batch_size=BATCH, per_device_eval_batch_size=BATCH * 2,\n   num_train_epochs=EPOCHS, weight_decay=0.01, warmup_ratio=0.06,\n   logging_steps=50, save_strategy=\"epoch\", save_total_limit=1,\n   load_best_model_at_end=True, metric_for_best_model=\"accuracy\",\n   fp16=(DEVICE == \"cuda\"), report_to=\"none\", seed=SEED,\n)\nta_kwargs[_eval_key] = \"epoch\"\n_tr = inspect.signature(Trainer.__init__).parameters\n_tok_key = \"processing_class\" if \"processing_class\" in _tr else \"tokenizer\"\ntrainer = Trainer(\n   model=model, args=TrainingArguments(**ta_kwargs),\n   train_dataset=tr_tok, eval_dataset=ev_tok,\n   data_collator=DataCollatorWithPadding(tok),\n   compute_metrics=compute_metrics,\n   callbacks=[EarlyStoppingCallback(early_stopping_patience=2)],\n   **{_tok_key: tok},\n)\nt0 = time.time()\ntrainer.train()\nprint(f\"nfine-tuned in {(time.time()-t0)\/60:.1f} min\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We train a strong TF-IDF and Logistic Regression baseline and inspect the most influential positive and negative n-grams to establish an interpretable reference point. We then tokenize the IMDb reviews and configure DistilBERT with LoRA adapters that update only a small subset of model parameters while keeping the backbone largely frozen. We use the Hugging Face Trainer with dynamic padding, early stopping, mixed precision, and multiple evaluation metrics to fine-tune the transformer efficiently.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">print(\"n\" + \"=\" * 79 + \"n5. EVALUATIONn\" + \"=\" * 79)\npred_out = trainer.predict(ev_tok)\np_lora = torch.softmax(torch.tensor(pred_out.predictions), dim=-1).numpy()[:, 1]\ny_true = np.array(pred_out.label_ids)\nyhat = (p_lora &gt; 0.5).astype(int)\nprint(classification_report(y_true, yhat, target_names=[\"neg\", \"pos\"], digits=4))\ncm = confusion_matrix(y_true, yhat)\nfig, ax = plt.subplots(1, 2, figsize=(11, 4))\nax[0].imshow(cm, cmap=\"Blues\")\nfor i in range(2):\n   for j in range(2):\n       ax[0].text(j, i, cm[i, j], ha=\"center\", va=\"center\", fontsize=14)\nax[0].set_xticks([0, 1], [\"pred neg\", \"pred pos\"])\nax[0].set_yticks([0, 1], [\"true neg\", \"true pos\"]); ax[0].set_title(\"Confusion matrix\")\nfor name, p in [(\"TF-IDF\", p_tfidf), (\"DistilBERT+LoRA\", p_lora)]:\n   fpr, tpr, _ = roc_curve(y_true, p)\n   ax[1].plot(fpr, tpr, label=f\"{name} (AUC={roc_auc_score(y_true, p):.4f})\")\nax[1].plot([0, 1], [0, 1], \"k--\", lw=0.8)\nax[1].set_xlabel(\"FPR\"); ax[1].set_ylabel(\"TPR\"); ax[1].set_title(\"ROC\"); ax[1].legend()\nplt.tight_layout(); plt.show()\nprint(\"n\" + \"=\" * 79 + \"n6. THRESHOLD &amp; CALIBRATIONn\" + \"=\" * 79)\nths = np.linspace(0.05, 0.95, 91)\naccs = [(y_true == (p_lora &gt; t)).mean() for t in ths]\nbest_t = ths[int(np.argmax(accs))]\nprint(f\"acc@0.50 = {accs[45]:.4f} | best threshold = {best_t:.2f} -&gt; acc = {max(accs):.4f}\")\ndef expected_calibration_error(probs, labels, n_bins=10):\n   \"\"\"ECE: |confidence - accuracy| averaged over confidence bins.\"\"\"\n   conf = np.maximum(probs, 1 - probs)\n   correct = (probs &gt; 0.5).astype(int) == labels\n   bins = np.linspace(0, 1, n_bins + 1)\n   ece, xs, ys = 0.0, [], []\n   for lo, hi in zip(bins[:-1], bins[1:]):\n       m = (conf &gt; lo) &amp; (conf &lt;= hi)\n       if m.sum() == 0:\n           continue\n       ece += m.mean() * abs(conf[m].mean() - correct[m].mean())\n       xs.append(conf[m].mean()); ys.append(correct[m].mean())\n   return ece, np.array(xs), np.array(ys)\nece, cx, cy = expected_calibration_error(p_lora, y_true)\nprint(f\"Expected Calibration Error = {ece:.4f}  (0 = perfectly calibrated)\")\nplt.figure(figsize=(9, 3.2))\nplt.subplot(1, 2, 1); plt.plot(ths, accs); plt.axvline(best_t, ls=\"--\", color=\"r\")\nplt.xlabel(\"threshold\"); plt.ylabel(\"accuracy\"); plt.title(\"Threshold sweep\")\nplt.subplot(1, 2, 2); plt.plot([0.5, 1], [0.5, 1], \"k--\", lw=0.8)\nplt.plot(cx, cy, \"o-\"); plt.xlabel(\"mean confidence\"); plt.ylabel(\"empirical accuracy\")\nplt.title(f\"Reliability diagram (ECE={ece:.3f})\")\nplt.tight_layout(); plt.show()\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We evaluate the fine-tuned DistilBERT-LoRA model using classification metrics, a confusion matrix, and ROC curves while directly comparing its ROC-AUC performance with the TF-IDF baseline. We sweep classification thresholds to determine whether the default probability cutoff of 0.5 gives the best accuracy on our evaluation set. We also calculate Expected Calibration Error and construct a reliability diagram to measure how closely the model\u2019s predicted confidence corresponds to its actual correctness.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">print(\"n\" + \"=\" * 79 + \"n7. ERROR ANALYSISn\" + \"=\" * 79)\nerr = pd.DataFrame({\n   \"text\": eval_ds[\"text\"], \"y\": y_true, \"p_pos\": p_lora,\n   \"n_words\": [len(t.split()) for t in eval_ds[\"text\"]],\n})\nerr[\"pred\"] = (err.p_pos &gt; 0.5).astype(int)\nerr[\"correct\"] = err.pred == err.y\nerr[\"confidence\"] = np.maximum(err.p_pos, 1 - err.p_pos)\nprint(\"--- 3 most CONFIDENT mistakes (where the model is confidently wrong) ---\")\nfor _, r in err[~err.correct].nlargest(3, \"confidence\").iterrows():\n   print(f\"n[true={'pos' if r.y else 'neg'} pred={'pos' if r.pred else 'neg'} \"\n         f\"conf={r.confidence:.3f} words={r.n_words}]\")\n   print(clean(r.text)[:400].replace(\"n\", \" \"), \"...\")\nerr[\"bucket\"] = pd.qcut(err.n_words, 4, labels=[\"short\", \"med\", \"long\", \"v.long\"])\nby_len = err.groupby(\"bucket\", observed=True).agg(acc=(\"correct\", \"mean\"), n=(\"correct\", \"size\"))\nprint(\"n--- accuracy by review length (truncation hurts long reviews) ---\")\nprint(by_len.to_string())\nprint(\"n\" + \"=\" * 79 + \"n8. OCCLUSION SALIENCYn\" + \"=\" * 79)\ninfer_model = model.merge_and_unload()\ninfer_model.to(DEVICE).eval()\n@torch.no_grad()\ndef predict_proba(texts, bs=64):\n   out = []\n   for i in range(0, len(texts), bs):\n       enc = tok([clean(t) for t in texts[i:i + bs]], truncation=True,\n                 max_length=MAX_LEN, padding=True, return_tensors=\"pt\").to(DEVICE)\n       out.append(torch.softmax(infer_model(**enc).logits, dim=-1)[:, 1].cpu().numpy())\n   return np.concatenate(out)\ndef occlusion(text, max_words=60):\n   words = clean(text).split()[:max_words]\n   base = predict_proba([\" \".join(words)])[0]\n   variants = [\" \".join(words[:i] + words[i + 1:]) for i in range(len(words))]\n   dropped = predict_proba(variants)\n   return words, base - dropped, base\nsample = err[err.correct].nlargest(1, \"confidence\").iloc[0]\nwords, contrib, base_p = occlusion(sample.text)\nprint(f\"P(positive) for the full excerpt = {base_p:.3f} \"\n     f\"(true label = {'pos' if sample.y else 'neg'})n\")\ntop = np.argsort(np.abs(contrib))[-15:]\nplt.figure(figsize=(7, 5))\nplt.barh(range(len(top)), contrib[top],\n        color=[\"tab:green\" if contrib[i] &gt; 0 else \"tab:red\" for i in top])\nplt.yticks(range(len(top)), [words[i] for i in top])\nplt.xlabel(\"\u0394 P(positive) when the word is removed\")\nplt.title(\"Occlusion saliency \u2014 green pushes POSITIVE, red pushes NEGATIVE\")\nplt.tight_layout(); plt.show()\nprint(\"n\" + \"=\" * 79 + \"n9. HEAD vs TAIL TRUNCATIONn\" + \"=\" * 79)\nprobe = err.nlargest(600, \"n_words\")\nW = 180\nhead_txt = [\" \".join(clean(t).split()[:W]) for t in probe.text]\ntail_txt = [\" \".join(clean(t).split()[-W:]) for t in probe.text]\nyp = probe.y.values\nacc_head = ((predict_proba(head_txt) &gt; 0.5).astype(int) == yp).mean()\nacc_tail = ((predict_proba(tail_txt) &gt; 0.5).astype(int) == yp).mean()\nprint(f\"on the {len(probe)} longest reviews, using only {W} words:\")\nprint(f\"  first {W} words -&gt; acc {acc_head:.4f}\")\nprint(f\"  last  {W} words -&gt; acc {acc_tail:.4f}\")\nprint(\"  Practical takeaway: if the tail wins, feed head+tail to the model or \"\n     \"raise MAX_LEN, rather than blindly truncating from the left.\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We examine the model\u2019s most confident incorrect predictions and group reviews by length to identify truncation-related failure patterns and difficult examples. We merge the LoRA adapters into the underlying model and apply leave-one-word-out occlusion to estimate which words push individual predictions toward positive or negative sentiment. We then compare predictions based on the beginning and ending portions of long reviews to determine where the strongest sentiment information resides.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">print(\"n\" + \"=\" * 79 + \"n10. PSEUDO-LABELLINGn\" + \"=\" * 79)\nunsup = raw[\"unsupervised\"].shuffle(seed=SEED).select(range(N_UNSUP))\np_uns = predict_proba(unsup[\"text\"])\nkeep = (p_uns &gt; 0.95) | (p_uns &lt; 0.05)\npl_texts = [clean(t) for t, k in zip(unsup[\"text\"], keep) if k]\npl_labels = (p_uns[keep] &gt; 0.5).astype(int)\nprint(f\"kept {keep.sum()}\/{N_UNSUP} pseudo-labels at conf&gt;0.95 \"\n     f\"(balance: {np.bincount(pl_labels)})\")\naug = make_pipeline(\n   TfidfVectorizer(ngram_range=(1, 2), min_df=2, max_features=300_000,\n                   sublinear_tf=True, strip_accents=\"unicode\"),\n   LogisticRegression(C=8.0, max_iter=2000, n_jobs=-1),\n).fit(Xtr + pl_texts, np.concatenate([ytr, pl_labels]))\nacc_aug = accuracy_score(yte, aug.predict(Xte))\nprint(f\"TF-IDF baseline      : {acc_tfidf:.4f}\")\nprint(f\"TF-IDF + pseudo-labels: {acc_aug:.4f}  (\u0394 {acc_aug-acc_tfidf:+.4f})\")\nprint(\"Caveat: gains are bounded by the teacher. Self-training also amplifies \"\n     \"the teacher's biases \u2014 always validate on clean, held-out data.\")\nprint(\"n\" + \"=\" * 79 + \"n11. SAVE &amp; INFERn\" + \"=\" * 79)\nSAVE_DIR = \".\/imdb-distilbert-lora-merged\"\ninfer_model.save_pretrained(SAVE_DIR); tok.save_pretrained(SAVE_DIR)\nprint(f\"saved merged model to {SAVE_DIR}\/  (load with \"\n     f\"AutoModelForSequenceClassification.from_pretrained('{SAVE_DIR}'))\")\ndemos = [\n   \"A masterclass in tension. The final act left the whole theatre silent.\",\n   \"Two hours I will never get back. Wooden acting, incoherent plot.\",\n   \"It's not the disaster the trailer promised, but it never really lands either.\",\n]\nfor d, p in zip(demos, predict_proba(demos)):\n   print(f\"  P(pos)={p:.3f} -&gt; {'POSITIVE' if p &gt; 0.5 else 'NEGATIVE'} | {d}\")\nprint(\"n\" + \"=\" * 79)\nprint(f\"SUMMARY (n_train={N_TRAIN}, n_eval={N_EVAL}, max_len={MAX_LEN})\")\nprint(\"=\" * 79)\nprint(pd.DataFrame([\n   {\"model\": \"TF-IDF + LogReg\", \"accuracy\": acc_tfidf, \"roc_auc\": auc_tfidf},\n   {\"model\": \"TF-IDF + pseudo-labels\", \"accuracy\": acc_aug, \"roc_auc\": float(\"nan\")},\n   {\"model\": \"DistilBERT + LoRA\", \"accuracy\": accuracy_score(y_true, yhat),\n    \"roc_auc\": roc_auc_score(y_true, p_lora)},\n]).to_string(index=False))\nprint(\"\"\"\nNEXT EXPERIMENTS\n - Set FULL_RUN = True for the real 25k\/25k benchmark (~40 min on a T4).\n - Swap MODEL_NAME to 'roberta-base' (target_modules=['query','value']) or\n   'answerdotai\/ModernBERT-base' for an 8k context window \u2014 no truncation.\n - Head+tail truncation: first 128 + last 128 tokens, motivated by section 9.\n - Ablate LoRA rank r in {4, 8, 16, 64} and plot accuracy vs trainable params.\n - Replace the pseudo-label teacher with an ensemble and iterate self-training.\n - Push to the Hub: huggingface_hub.login() then infer_model.push_to_hub(...).\n\"\"\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We use the fine-tuned transformer to generate high-confidence pseudo-labels for examples from IMDb\u2019s unlabeled split and add these examples to the TF-IDF training corpus. We compare the augmented classifier against the original baseline to measure whether semi-supervised self-training improves predictive accuracy. Finally, we save the merged DistilBERT model and tokenizer, run sentiment inference on custom reviews, and summarize the performance of all models developed throughout the tutorial.<\/p>\n<p class=\"wp-block-paragraph\">In conclusion, we developed a rigorous sentiment classification pipeline that goes well beyond simply fine-tuning a transformer and reporting accuracy. We established a competitive TF-IDF baseline, train DistilBERT efficiently with LoRA, and evaluate both predictive quality and probability reliability while identifying how review length, truncation, and highly confident mistakes influence real-world performance. We also interpreted individual predictions through occlusion-based saliency, tested whether sentiment information is concentrated near the beginning or end of long reviews, and extended supervised learning with high-confidence pseudo-labels from the unlabeled dataset.<\/p>\n<p class=\"wp-block-paragraph\">\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<\/p><p class=\"wp-block-paragraph\">\n<\/p><p class=\"wp-block-paragraph\"><strong>Check out the\u00a0<a href=\"https:\/\/github.com\/MARKTECHPOST-AI-MEDIA-INC\/AI-Agents-Projects-Tutorials\/blob\/main\/Deep%20Learning\/IMDb_Sentiment_Analysis_DistilBERT_LoRA_Tutorial_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">FULL CODES here<\/a><\/strong>.<strong>\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">150k+ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/www.aidevsignals.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p class=\"wp-block-paragraph\">Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.?\u00a0<strong><a href=\"https:\/\/forms.gle\/wbash1wF6efRj8G58\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Connect with us<\/mark><\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/08\/09\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\">IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we develop an end-to-end sentiment analysis workflow using the Stanford NLP IMDb Large Movie Review Dataset and compare classical machine learning with parameter-efficient transformer fine-tuning. We begin by establishing a reproducible environment and auditing the dataset for class ordering, review-length skew, duplicate leakage, and preprocessing artifacts before training a strong TF-IDF and Logistic Regression baseline. We then fine-tune DistilBERT with LoRA through PEFT, evaluate it using accuracy, macro-F1, ROC-AUC, confusion matrices, and ROC curves, and examine threshold selection and probability calibration through Expected Calibration Error and reliability analysis. Beyond headline metrics, we investigate confident errors, performance across review lengths, word-level occlusion saliency, and head-versus-tail truncation to understand how the model reaches its predictions and where long-context limitations affect performance. Finally, we use the unlabeled IMDb split for confidence-based pseudo-labeling, compare the resulting semi-supervised model against our baseline, and save the merged transformer for reusable sentiment inference. Copy CodeCopiedUse a different Browser import importlib.util, subprocess, sys, os, time, random, warnings, inspect, hashlib warnings.filterwarnings(&ldquo;ignore&rdquo;) os.environ[&ldquo;TOKENIZERS_PARALLELISM&rdquo;] = &ldquo;false&rdquo; os.environ[&ldquo;WANDB_DISABLED&rdquo;] = &ldquo;true&rdquo; _REQUIRED = { &ldquo;transformers&rdquo;: &ldquo;transformers&rdquo;, &ldquo;datasets&rdquo;: &ldquo;datasets&rdquo;, &ldquo;peft&rdquo;: &ldquo;peft&rdquo;, &ldquo;accelerate&rdquo;: &ldquo;accelerate&rdquo;, &ldquo;sklearn&rdquo;: &ldquo;scikit-learn&rdquo;, } _missing = [pkg for mod, pkg in _REQUIRED.items() if importlib.util.find_spec(mod) is None] if _missing: print(f&rdquo;Installing: {&lsquo;, &lsquo;.join(_missing)} &hellip;&rdquo;) subprocess.run([sys.executable, &ldquo;-m&rdquo;, &ldquo;pip&rdquo;, &ldquo;install&rdquo;, &ldquo;-q&rdquo;, *_missing], check=True) print(&ldquo;Done. (If imports fail below, restart the runtime and re-run.)n&rdquo;) import numpy as np import pandas as pd import torch import matplotlib.pyplot as plt from datasets import load_dataset from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.metrics import (accuracy_score, f1_score, roc_auc_score, classification_report, confusion_matrix, roc_curve) from transformers import (AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, DataCollatorWithPadding, EarlyStoppingCallback, set_seed) from peft import LoraConfig, get_peft_model, TaskType def _disable_torchao_probe(): patched = [] try: import peft.import_utils as _piu _piu.is_torchao_available = lambda: False patched.append(&ldquo;peft.import_utils&rdquo;) except Exception: pass for _name, _mod in list(sys.modules.items()): if _name.startswith(&ldquo;peft&rdquo;) and hasattr(_mod, &ldquo;is_torchao_available&rdquo;): _mod.is_torchao_available = lambda: False patched.append(_name) return patched try: import torchao as _tao _v = getattr(_tao, &ldquo;__version__&rdquo;, &ldquo;?&rdquo;) if tuple(int(x) for x in _v.split(&ldquo;.&rdquo;)[:2]) &lt; (0, 16): print(f&rdquo;[compat] torchao {_v} &lt; 0.16 -&gt; disabling PEFT&rsquo;s torchao probe: &rdquo; f&rdquo;{&lsquo;, &lsquo;.join(_disable_torchao_probe())}&rdquo;) except Exception: _disable_torchao_probe() SEED = 42 MODEL_NAME = &ldquo;distilbert-base-uncased&rdquo; MAX_LEN = 256 N_TRAIN = 5000 N_EVAL = 2000 N_UNSUP = 3000 EPOCHS = 2 BATCH = 16 LR = 3e-4 FULL_RUN = False if FULL_RUN: N_TRAIN, N_EVAL, EPOCHS = 25000, 25000, 3 set_seed(SEED); random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED) DEVICE = &ldquo;cuda&rdquo; if torch.cuda.is_available() else &ldquo;cpu&rdquo; print(&ldquo;=&rdquo; * 79) print(f&rdquo;device={DEVICE} | torch={torch.__version__} | &rdquo; f&rdquo;gpu={torch.cuda.get_device_name(0) if DEVICE==&rsquo;cuda&rsquo; else &lsquo;n\/a&rsquo;}&rdquo;) print(&ldquo;=&rdquo; * 79) t0 = time.time() raw = load_dataset(&ldquo;stanfordnlp\/imdb&rdquo;) print(raw, f&rdquo;nloaded in {time.time()-t0:.1f}sn&rdquo;) print(&ldquo;&mdash; example (truncated) &mdash;&ldquo;) print(&ldquo;label:&rdquo;, raw[&ldquo;train&rdquo;][0][&ldquo;label&rdquo;], &ldquo;|&rdquo;, raw[&ldquo;train&rdquo;][0][&ldquo;text&rdquo;][:300], &ldquo;&hellip;n&rdquo;) first_labels = np.array(raw[&ldquo;train&rdquo;][&ldquo;label&rdquo;][:5]) last_labels = np.array(raw[&ldquo;train&rdquo;][&ldquo;label&rdquo;][-5:]) print(f&rdquo;TRAP #1 &ndash; split ordering: first 5 labels {first_labels}, &rdquo; f&rdquo;last 5 labels {last_labels} -&gt; ALWAYS shuffle before subsampling.&rdquo;) train_full = raw[&ldquo;train&rdquo;].shuffle(seed=SEED) test_full = raw[&ldquo;test&rdquo;].shuffle(seed=SEED) train_ds = train_full.select(range(min(N_TRAIN, len(train_full)))) eval_ds = test_full.select(range(min(N_EVAL, len(test_full)))) print(f&rdquo; after shuffle+subsample: train balance = &rdquo; f&rdquo;{np.bincount(train_ds[&lsquo;label&rsquo;])}, eval balance = {np.bincount(eval_ds[&lsquo;label&rsquo;])}&rdquo;) lens = np.array([len(t.split()) for t in train_full[&ldquo;text&rdquo;]]) q = np.percentile(lens, [50, 75, 90, 95, 99]) print(f&rdquo;nTRAP #2 &ndash; length (words): median={q[0]:.0f} p75={q[1]:.0f} p90={q[2]:.0f} &rdquo; f&rdquo;p95={q[3]:.0f} p99={q[4]:.0f} max={lens.max()}&rdquo;) print(f&rdquo; ~{(lens &gt; MAX_LEN*0.75).mean()*100:.1f}% of reviews exceed MAX_LEN={MAX_LEN} &rdquo; f&rdquo;tokens (rough words-&gt;tokens factor 1.3). Section 9 measures what that costs.&rdquo;) h_tr = {hashlib.md5(t.encode()).hexdigest() for t in raw[&ldquo;train&rdquo;][&ldquo;text&rdquo;]} h_te = {hashlib.md5(t.encode()).hexdigest() for t in raw[&ldquo;test&rdquo;][&ldquo;text&rdquo;]} print(f&rdquo;nTRAP #3 &ndash; leakage: {len(h_tr &amp; h_te)} exact duplicate reviews across &rdquo; f&rdquo;train\/test; {len(raw[&lsquo;train&rsquo;])-len(h_tr)} dupes inside train itself.&rdquo;) def clean(t): return t.replace(&ldquo;&lt;br \/&gt;&rdquo;, &rdquo; &ldquo;).replace(&ldquo;&lt;br\/&gt;&rdquo;, &rdquo; &ldquo;).strip() plt.figure(figsize=(11, 3.2)) plt.subplot(1, 2, 1) plt.hist(np.clip(lens, 0, 1000), bins=60) plt.axvline(MAX_LEN, ls=&rdquo;&ndash;&ldquo;, color=&rdquo;k&rdquo;, label=f&rdquo;MAX_LEN={MAX_LEN}&rdquo;) plt.title(&ldquo;Review length (words, clipped at 1000)&rdquo;); plt.legend() plt.subplot(1, 2, 2) plt.bar([&ldquo;neg&rdquo;, &ldquo;pos&rdquo;], np.bincount(raw[&ldquo;train&rdquo;][&ldquo;label&rdquo;])) plt.title(&ldquo;Train class balance (perfectly balanced)&rdquo;) plt.tight_layout(); plt.show() We configure the Colab environment, install the required libraries, apply the PEFT&ndash;torchao compatibility fix, and set deterministic seeds for reproducible experiments. We load the Stanford IMDb dataset, shuffle and subsample the train and test splits, and inspect class balance, review-length distributions, duplicate leakage, and HTML artifacts. We also visualize review lengths and label frequencies so we understand the dataset structure before building any models. Copy CodeCopiedUse a different Browser print(&ldquo;n&rdquo; + &ldquo;=&rdquo; * 79 + &ldquo;n3. TF-IDF BASELINEn&rdquo; + &ldquo;=&rdquo; * 79) Xtr = [clean(t) for t in train_ds[&ldquo;text&rdquo;]]; ytr = np.array(train_ds[&ldquo;label&rdquo;]) Xte = [clean(t) for t in eval_ds[&ldquo;text&rdquo;]]; yte = np.array(eval_ds[&ldquo;label&rdquo;]) t0 = time.time() tfidf_clf = make_pipeline( TfidfVectorizer(ngram_range=(1, 2), min_df=2, max_features=300_000, sublinear_tf=True, strip_accents=&rdquo;unicode&rdquo;), LogisticRegression(C=8.0, max_iter=2000, n_jobs=-1), ) tfidf_clf.fit(Xtr, ytr) p_tfidf = tfidf_clf.predict_proba(Xte)[:, 1] acc_tfidf = accuracy_score(yte, p_tfidf &gt; 0.5) auc_tfidf = roc_auc_score(yte, p_tfidf) print(f&rdquo;trained in {time.time()-t0:.1f}s -&gt; acc={acc_tfidf:.4f} auc={auc_tfidf:.4f}&rdquo;) vec, lr = tfidf_clf.steps[0][1], tfidf_clf.steps[1][1] feats, coefs = np.array(vec.get_feature_names_out()), lr.coef_[0] order = np.argsort(coefs) print(&ldquo;nmost NEGATIVE n-grams:&rdquo;, &ldquo;, &ldquo;.join(feats[order[:12]])) print(&ldquo;most POSITIVE n-grams:&rdquo;, &ldquo;, &ldquo;.join(feats[order[-12:]][::-1])) print(&ldquo;n&rdquo; + &ldquo;=&rdquo; * 79 + &ldquo;n4. LoRA FINE-TUNINGn&rdquo; + &ldquo;=&rdquo; * 79) tok = AutoTokenizer.from_pretrained(MODEL_NAME) def tokenize(batch): return tok([clean(t) for t in batch[&ldquo;text&rdquo;]], truncation=True, max_length=MAX_LEN) tr_tok = (train_ds.map(tokenize, batched=True, remove_columns=[&ldquo;text&rdquo;]) .rename_column(&ldquo;label&rdquo;, &ldquo;labels&rdquo;)) ev_tok = (eval_ds.map(tokenize, batched=True, remove_columns=[&ldquo;text&rdquo;]) .rename_column(&ldquo;label&rdquo;, &ldquo;labels&rdquo;)) base = AutoModelForSequenceClassification.from_pretrained( MODEL_NAME, num_labels=2, id2label={0: &ldquo;NEGATIVE&rdquo;, 1: &ldquo;POSITIVE&rdquo;}, label2id={&ldquo;NEGATIVE&rdquo;: 0, &ldquo;POSITIVE&rdquo;: 1}, ) lora_cfg = LoraConfig( task_type=TaskType.SEQ_CLS, r=16, lora_alpha=32, lora_dropout=0.05, target_modules=[&ldquo;q_lin&rdquo;, &ldquo;v_lin&rdquo;], modules_to_save=[&ldquo;pre_classifier&rdquo;, &ldquo;classifier&rdquo;], ) try: model = get_peft_model(base, lora_cfg) except ImportError as e: _disable_torchao_probe() print(f&rdquo;[compat] retrying after backend probe failure: {e}&rdquo;) model = get_peft_model(base, lora_cfg) model.print_trainable_parameters() def compute_metrics(eval_pred): logits, labels = eval_pred probs = torch.softmax(torch.tensor(logits), dim=-1).numpy()[:, 1] preds = (probs &gt; 0.5).astype(int) return {&ldquo;accuracy&rdquo;: accuracy_score(labels, preds), &ldquo;f1_macro&rdquo;: f1_score(labels, preds, average=&rdquo;macro&rdquo;), &ldquo;roc_auc&rdquo;: roc_auc_score(labels, probs)} _ta = inspect.signature(TrainingArguments.__init__).parameters _eval_key = &ldquo;eval_strategy&rdquo; if &ldquo;eval_strategy&rdquo; in _ta else &ldquo;evaluation_strategy&rdquo; ta_kwargs = dict( output_dir=&rdquo;.\/imdb_lora&rdquo;, learning_rate=LR, per_device_train_batch_size=BATCH, per_device_eval_batch_size=BATCH * 2, num_train_epochs=EPOCHS, weight_decay=0.01, warmup_ratio=0.06, logging_steps=50, save_strategy=&rdquo;epoch&rdquo;, save_total_limit=1, load_best_model_at_end=True, metric_for_best_model=&rdquo;accuracy&rdquo;, fp16=(DEVICE == &ldquo;cuda&rdquo;), report_to=&rdquo;none&rdquo;, seed=SEED, ) ta_kwargs[_eval_key] = &ldquo;epoch&rdquo; _tr = inspect.signature(Trainer.__init__).parameters _tok_key = &ldquo;processing_class&rdquo; if &ldquo;processing_class&rdquo; in _tr else &ldquo;tokenizer&rdquo; trainer = Trainer( model=model, args=TrainingArguments(**ta_kwargs), train_dataset=tr_tok, eval_dataset=ev_tok, data_collator=DataCollatorWithPadding(tok), compute_metrics=compute_metrics, callbacks=[EarlyStoppingCallback(early_stopping_patience=2)], **{_tok_key: tok}, ) t0 = time.time() trainer.train() print(f&rdquo;nfine-tuned in {(time.time()-t0)\/60:.1f} min&rdquo;) We train a strong TF-IDF and Logistic Regression baseline and inspect the most influential positive and negative n-grams to establish an interpretable reference point. We then tokenize the IMDb reviews and configure DistilBERT with LoRA adapters that update only a small subset of model parameters while keeping the<\/p>","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pmpro_default_level":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_pvb_checkbox_block_on_post":false,"footnotes":""},"categories":[52,5,7,1],"tags":[],"class_list":["post-110208","post","type-post","status-publish","format-standard","hentry","category-ai-club","category-committee","category-news","category-uncategorized","pmpro-has-access"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v25.3 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum<\/title>\n<meta name=\"description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/youzum.net\/de\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\" \/>\n<meta property=\"og:locale\" content=\"de_DE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum\" \/>\n<meta property=\"og:description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta property=\"og:url\" content=\"https:\/\/youzum.net\/de\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\" \/>\n<meta property=\"og:site_name\" content=\"YouZum\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/DroneAssociationTH\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-09T20:35:07+00:00\" \/>\n<meta name=\"author\" content=\"admin NU\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Verfasst von\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin NU\" \/>\n\t<meta name=\"twitter:label2\" content=\"Gesch\u00e4tzte Lesezeit\" \/>\n\t<meta name=\"twitter:data2\" content=\"15\u00a0Minuten\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\"},\"author\":{\"name\":\"admin NU\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\"},\"headline\":\"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning\",\"datePublished\":\"2026-08-09T20:35:07+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\"},\"wordCount\":716,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"articleSection\":[\"AI\",\"Committee\",\"News\",\"Uncategorized\"],\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\",\"url\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\",\"name\":\"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum\",\"isPartOf\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#website\"},\"datePublished\":\"2026-08-09T20:35:07+00:00\",\"description\":\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\",\"breadcrumb\":{\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#breadcrumb\"},\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/youzum.net\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/yousum.gpucore.co\/#website\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"name\":\"YouSum\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/yousum.gpucore.co\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"de\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\",\"name\":\"Drone Association Thailand\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"width\":300,\"height\":300,\"caption\":\"Drone Association Thailand\"},\"image\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/DroneAssociationTH\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\",\"name\":\"admin NU\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"caption\":\"admin NU\"},\"url\":\"https:\/\/youzum.net\/de\/members\/adminnu\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/youzum.net\/de\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/","og_locale":"de_DE","og_type":"article","og_title":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum","og_description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","og_url":"https:\/\/youzum.net\/de\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/","og_site_name":"YouZum","article_publisher":"https:\/\/www.facebook.com\/DroneAssociationTH\/","article_published_time":"2026-08-09T20:35:07+00:00","author":"admin NU","twitter_card":"summary_large_image","twitter_misc":{"Verfasst von":"admin NU","Gesch\u00e4tzte Lesezeit":"15\u00a0Minuten"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#article","isPartOf":{"@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/"},"author":{"name":"admin NU","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c"},"headline":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning","datePublished":"2026-08-09T20:35:07+00:00","mainEntityOfPage":{"@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/"},"wordCount":716,"commentCount":0,"publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"articleSection":["AI","Committee","News","Uncategorized"],"inLanguage":"de","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/","url":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/","name":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning - YouZum","isPartOf":{"@id":"https:\/\/yousum.gpucore.co\/#website"},"datePublished":"2026-08-09T20:35:07+00:00","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","breadcrumb":{"@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#breadcrumb"},"inLanguage":"de","potentialAction":[{"@type":"ReadAction","target":["https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/youzum.net\/imdb-sentiment-analysis-with-distilbert-lora-tf-idf-baselines-calibration-interpretability-robustness-testing-and-semi-supervised-learning\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/youzum.net\/"},{"@type":"ListItem","position":2,"name":"IMDb Sentiment Analysis with DistilBERT LoRA, TF-IDF Baselines, Calibration, Interpretability, Robustness Testing, and Semi-Supervised Learning"}]},{"@type":"WebSite","@id":"https:\/\/yousum.gpucore.co\/#website","url":"https:\/\/yousum.gpucore.co\/","name":"YouSum","description":"","publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/yousum.gpucore.co\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"de"},{"@type":"Organization","@id":"https:\/\/yousum.gpucore.co\/#organization","name":"Drone Association Thailand","url":"https:\/\/yousum.gpucore.co\/","logo":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","width":300,"height":300,"caption":"Drone Association Thailand"},"image":{"@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/DroneAssociationTH\/"]},{"@type":"Person","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c","name":"admin NU","image":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","caption":"admin NU"},"url":"https:\/\/youzum.net\/de\/members\/adminnu\/"}]}},"rttpg_featured_image_url":null,"rttpg_author":{"display_name":"admin NU","author_link":"https:\/\/youzum.net\/de\/members\/adminnu\/"},"rttpg_comment":0,"rttpg_category":"<a href=\"https:\/\/youzum.net\/de\/category\/ai-club\/\" rel=\"category tag\">AI<\/a> <a href=\"https:\/\/youzum.net\/de\/category\/committee\/\" rel=\"category tag\">Committee<\/a> <a href=\"https:\/\/youzum.net\/de\/category\/news\/\" rel=\"category tag\">News<\/a> <a href=\"https:\/\/youzum.net\/de\/category\/uncategorized\/\" rel=\"category tag\">Uncategorized<\/a>","rttpg_excerpt":"In this tutorial, we develop an end-to-end sentiment analysis workflow using the Stanford NLP IMDb Large Movie Review Dataset and compare classical machine learning with parameter-efficient transformer fine-tuning. We begin by establishing a reproducible environment and auditing the dataset for class ordering, review-length skew, duplicate leakage, and preprocessing artifacts before training a strong TF-IDF and&hellip;","_links":{"self":[{"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/posts\/110208","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/comments?post=110208"}],"version-history":[{"count":0,"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/posts\/110208\/revisions"}],"wp:attachment":[{"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/media?parent=110208"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/categories?post=110208"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/youzum.net\/de\/wp-json\/wp\/v2\/tags?post=110208"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}