File size: 29,446 Bytes
1ea7ba6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 | """Generates: outputs/Ramim_Contribution_Guide.pdf"""
from fpdf import FPDF, XPos, YPos
from pathlib import Path
W = 190 # usable page width
class PDF(FPDF):
def header(self):
self.set_font("Helvetica", "B", 9)
self.set_fill_color(26, 107, 138)
self.set_text_color(255, 255, 255)
self.cell(0, 8, "SpiceFusionNet | Ramim's Contribution Guide | CSE 414",
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
self.set_text_color(0)
self.ln(2)
def footer(self):
self.set_y(-12)
self.set_font("Helvetica", "I", 8)
self.set_text_color(150)
self.cell(0, 8, f"Page {self.page_no()} | Md. Noushad Jahan Ramim | 22201257", align="C")
def chapter_title(self, num, title):
self.set_fill_color(26, 107, 138)
self.set_text_color(255, 255, 255)
self.set_font("Helvetica", "B", 13)
self.cell(0, 10, f" {num}. {title}", fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT)
self.set_text_color(0)
self.ln(3)
def section(self, title):
self.set_fill_color(210, 230, 240)
self.set_text_color(20, 60, 90)
self.set_font("Helvetica", "B", 11)
self.cell(0, 8, f" {title}", fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT)
self.set_text_color(0)
self.ln(1)
def _w(self):
return self.w - self.l_margin - self.r_margin
def body(self, text):
self.set_x(self.l_margin)
self.set_font("Helvetica", "", 10)
self.set_text_color(30, 30, 30)
self.multi_cell(self._w(), 6, text)
self.ln(2)
def highlight(self, text):
self.set_x(self.l_margin)
self.set_fill_color(255, 243, 205)
self.set_font("Helvetica", "B", 10)
self.set_text_color(120, 60, 0)
self.multi_cell(self._w(), 7, f" KEY POINT: {text}", fill=True)
self.set_text_color(0)
self.ln(2)
def code_block(self, lines):
self.set_fill_color(40, 40, 40)
self.set_text_color(180, 230, 180)
self.set_font("Courier", "", 8.5)
self.ln(1)
for line in lines:
self.set_x(self.l_margin)
self.cell(self._w(), 5.2, f" {line}", fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT)
self.set_text_color(0)
self.ln(3)
def kv(self, key, val):
self.set_x(self.l_margin)
self.set_font("Helvetica", "B", 10)
self.set_text_color(26, 107, 138)
self.multi_cell(self._w(), 6, f" {key}:")
self.set_x(self.l_margin)
self.set_font("Helvetica", "", 10)
self.set_text_color(30, 30, 30)
self.multi_cell(self._w(), 6, f" {val}")
self.ln(1)
def analogy(self, title, text):
self.set_x(self.l_margin)
self.set_fill_color(232, 245, 233)
self.set_font("Helvetica", "B", 10)
self.set_text_color(30, 100, 30)
self.cell(0, 7, f" Analogy -- {title}", fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT)
self.set_x(self.l_margin)
self.set_font("Helvetica", "I", 9.5)
self.set_text_color(40, 80, 40)
self.multi_cell(self._w(), 6, f" {text}", fill=True)
self.set_text_color(0)
self.ln(3)
def qa(self, q, a):
self.set_x(self.l_margin)
self.set_font("Helvetica", "B", 10)
self.set_text_color(100, 0, 100)
self.multi_cell(self._w(), 6, f" Q: {q}")
self.set_x(self.l_margin)
self.set_font("Helvetica", "", 10)
self.set_text_color(30, 30, 30)
self.multi_cell(self._w(), 6, f" A: {a}")
self.ln(2)
pdf = PDF()
pdf.set_margins(10, 15, 10)
pdf.set_auto_page_break(auto=True, margin=15)
# ββ COVER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.set_fill_color(26, 107, 138)
pdf.rect(0, 25, 210, 58, "F")
pdf.set_y(33)
pdf.set_font("Helvetica", "B", 22)
pdf.set_text_color(255, 255, 255)
pdf.cell(0, 12, "SpiceFusionNet", new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_font("Helvetica", "B", 14)
pdf.cell(0, 8, "Ramim's Contribution -- Full Study Guide",
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_font("Helvetica", "", 11)
pdf.cell(0, 7, "Model Architecture | EfficientNet-B4 | Phase 1 & Phase 2 Training",
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_y(93)
pdf.set_text_color(0)
pdf.set_font("Helvetica", "B", 11)
pdf.cell(0, 7, "Prepared for: Md. Noushad Jahan Ramim (ID: 22201257)",
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.set_font("Helvetica", "", 10)
pdf.cell(0, 6, "CSE 414 - Machine Learning and Deep Learning Lab",
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.cell(0, 6, "University of Asia Pacific | May 2026",
new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
pdf.ln(8)
pdf.set_fill_color(240, 248, 255)
pdf.set_font("Helvetica", "B", 10)
pdf.set_text_color(26, 107, 138)
pdf.cell(0, 8, " What this guide covers:", fill=True,
new_x=XPos.LMARGIN, new_y=YPos.NEXT)
pdf.set_font("Helvetica", "", 10)
pdf.set_text_color(30)
for item in [
"1. Model Architecture -- why 3 branches, AttentionFusion, fusion_head",
"2. EfficientNet-B4 -- what it is, why B4, transfer learning, 1792-d",
"3. Phase 1 Training -- CE loss, label smoothing, AdamW, LR schedule, early stopping",
"4. Phase 2 Training -- SupCon loss, what freezes, projection head, tau",
"5. Q&A Prep -- every question sir might ask YOU, with ready answers",
]:
pdf.cell(W, 6.5, f" {item}", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
# ββ CH 1: MODEL ARCHITECTURE ββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.chapter_title("01", "Model Architecture -- The Big Picture")
pdf.section("What is SpiceFusionNet?")
pdf.body(
"SpiceFusionNet is the deep learning model YOU designed. It is called a "
"multi-modal fusion network because it combines THREE different types of information "
"from the same spice image -- not just one. Most classifiers look at pixels only. "
"SpiceFusionNet looks at: visual appearance (CNN), surface texture (LBP+GLCM), "
"and color signature (HSV histogram). All three together."
)
pdf.analogy("The Expert Chef",
"A chef identifying a spice doesn't just look at it. They feel the texture "
"(rough or smooth?) and notice the color (bright yellow or dark orange?). "
"SpiceFusionNet does the same -- three information sources, one smart decision.")
pdf.section("The Three Branches -- What Each One Does")
pdf.kv("Branch 1 -- CNN (Image)",
"Input: 224x224 pixel image. Architecture: EfficientNet-B4 (pretrained on ImageNet). "
"Output: 1792 numbers describing the visual appearance -- shapes, edges, patterns.")
pdf.kv("Branch 2 -- Texture",
"Input: LBP (10-d) + GLCM (48-d) = 58 numbers from surface analysis. "
"Architecture: MLP (58->128->256) with BatchNorm and ReLU. "
"Output: 256 numbers describing surface texture -- rough, smooth, granular.")
pdf.kv("Branch 3 -- Color",
"Input: HSV Histogram -- H:36 bins, S:32 bins, V:32 bins = 100 numbers. "
"Architecture: MLP (100->64->128) with BatchNorm and ReLU. "
"Output: 128 numbers describing exact color signature.")
pdf.section("Why THREE branches? Why not just the CNN?")
pdf.body(
"Because cumin and coriander look almost IDENTICAL in photos. The CNN alone gets "
"confused. But their TEXTURE is different -- cumin has fine ridges, coriander smoother. "
"Their COLOR is slightly different too. By combining all three, the model has much more "
"information. This is why full fusion gets 99.68% while image-only gets 99.59% -- "
"the extra branches fill the gaps the CNN cannot cover."
)
pdf.section("AttentionFusion -- The Smart Combination")
pdf.body(
"After three branches output (1792 + 256 + 128 = 2176 numbers total), we combine them. "
"Simple concatenation treats all three equally. AttentionFusion is smarter -- it LEARNS "
"which branch matters most for each spice type."
)
pdf.body(
"How: A tiny neural network takes the 2176-d vector and outputs 3 numbers "
"(alpha_img, alpha_tex, alpha_col). Softmax makes them sum to 1.0 (like percentages). "
"Each branch output is multiplied by its weight. Result: 2176-d intelligently weighted vector."
)
pdf.highlight(
"For turmeric (bright yellow powder) -- color branch gets higher weight. "
"For coriander (textured seed) -- texture branch gets higher weight. "
"The model learns these weights automatically during Phase 3 training. No manual tuning."
)
pdf.code_block([
"# AttentionFusion logic (src/model.py)",
"cat = concat(f_cnn=1792, f_tex=256, f_col=128) # -> 2176-d",
"gates = softmax( Linear(2176, 3) ) # [a_img, a_tex, a_col] sum=1.0",
"fused = a_img*f_cnn + a_tex*f_tex + a_col*f_col # weighted 2176-d",
"out = fusion_head(fused) # MLP(2176->512->11) -> class scores",
])
pdf.section("Model Size")
pdf.code_block([
"EfficientNet-B4 backbone : ~19.3 Million parameters",
"Texture + Color MLPs : ~56 Thousand parameters",
"Fusion head + Attention : ~2.2 Million parameters",
"TOTAL : ~21.6 Million parameters",
])
# ββ CH 2: EFFICIENTNET-B4 ββββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.chapter_title("02", "EfficientNet-B4 -- The Backbone YOU Integrated")
pdf.section("What is EfficientNet?")
pdf.body(
"EfficientNet is a CNN family created by Google Brain (Tan & Le, ICML 2019). "
"The key innovation: COMPOUND SCALING. Old models scaled in only one direction -- "
"deeper (more layers) OR wider (more filters) OR higher resolution. "
"EfficientNet scales all three dimensions TOGETHER in a balanced ratio. "
"Result: best accuracy per parameter count in its era."
)
pdf.analogy("Balanced Diet vs One Food",
"If you only eat protein (make network deeper only), you miss vitamins and carbs. "
"EfficientNet eats a balanced diet -- deeper + wider + higher resolution in the "
"right proportions. Most efficient use of every parameter.")
pdf.section("Why B4 Specifically? (YOUR Ablation A4 Proved This)")
pdf.code_block([
"Backbone Accuracy Inference Verdict",
"MobileNetV3-Large 93.5% 0.8ms Too weak for our task",
"EfficientNet-B0 95.0% 1.0ms Still not enough",
"EfficientNet-B2 96.8% 1.4ms Good but not best",
"ResNet-50 94.2% 1.85ms Older architecture, lower acc",
"EfficientNet-B4 99.68% 2.10ms BEST accuracy-speed tradeoff",
])
pdf.body(
"B5, B6, B7 would be slightly more accurate but significantly slower and need "
"more GPU memory. For real-world deployment, B4 is the sweet spot. "
"This is a design decision YOU justified through experimentation."
)
pdf.section("How YOU Loaded It (The Exact Code You Wrote)")
pdf.code_block([
"import timm # PyTorch Image Models library",
"",
"self.backbone = timm.create_model(",
" 'efficientnet_b4', # model name in timm",
" pretrained=True, # load ImageNet-1k pretrained weights",
" num_classes=0, # REMOVE original 1000-class head",
" global_pool='avg', # Global Average Pooling -> 1792-d vector",
" drop_rate=0.4, # dropout for regularization",
")",
"",
"# Output: (batch_size, 1792) feature vector per image",
])
pdf.section("What Does Each Parameter Mean?")
pdf.kv("pretrained=True",
"Model starts with weights trained on 1.2 million ImageNet images. It already knows "
"how to detect edges, shapes, textures from general images. We START from this "
"knowledge instead of random weights. This is Transfer Learning.")
pdf.kv("num_classes=0",
"Removes EfficientNet's original 1000-class ImageNet classification head. "
"We only want the raw feature extractor (1792-d output). "
"We add our own img_head (Phase 1) and fusion_head (Phase 3) on top.")
pdf.kv("global_pool='avg'",
"After convolution layers, feature maps are (7x7x1792). Global Average Pooling "
"takes the spatial average of each feature map. 7x7 -> 1 number per map. "
"1792 maps -> 1792-dimensional vector. Translation invariant.")
pdf.kv("drop_rate=0.4",
"40% of neurons randomly set to zero during training. "
"Prevents overfitting. Model cannot memorize -- must learn generalizable patterns.")
pdf.section("What is Transfer Learning? (You Will Definitely Be Asked)")
pdf.body(
"Transfer learning: use a model trained on Task A to help with Task B. "
"EfficientNet-B4 was trained on ImageNet (1.2M diverse images -- animals, cars, food). "
"It learned GENERAL visual features: how to detect edges, gradients, textures, shapes. "
"We TRANSFER this general knowledge to our spice-specific task."
)
pdf.highlight(
"Without transfer learning: we have 11,000 spice images -- too few to train "
"a 19M parameter model from scratch. With transfer learning: start from strong "
"general knowledge, fine-tune for spices. Result: 99.68% accuracy."
)
pdf.section("What is the 1792-d Vector?")
pdf.body(
"EfficientNet-B4's last convolutional stage has 1792 feature maps. "
"Each feature map detects a specific visual pattern (one detects horizontal edges, "
"another detects orange colors, another detects granular texture, etc.). "
"After Global Average Pooling, each map collapses to a single number. "
"1792 feature maps -> 1792 numbers. This 1792-d vector is a COMPRESSED ENCODING "
"of the entire image -- all visual information in 1792 floating point numbers."
)
# ββ CH 3: PHASE 1 TRAINING ββββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.chapter_title("03", "Phase 1 Training -- Backbone Pre-training (30 Epochs)")
pdf.section("What Happens in Phase 1?")
pdf.body(
"Phase 1 is Step 1 of our 3-phase curriculum training. Goal: teach the model "
"to classify spices from IMAGES ALONE before adding texture and color. "
"Only the CNN branch (image branch) is active. Texture and color branches exist "
"but produce zero outputs (data loaded with multimodal=False). "
"Forward pass: image -> EfficientNet-B4 -> img_head -> 11 class scores."
)
pdf.section("Loss Function: Cross-Entropy with Label Smoothing (0.1)")
pdf.body(
"Cross-Entropy Loss is the standard loss for classification. For each image, "
"it compares predicted probabilities with the true label. The model learns to "
"maximize probability for the correct class."
)
pdf.body(
"Label Smoothing (0.1): Instead of hard targets (correct=1.0, others=0.0), "
"use soft targets: correct class = 0.9, others = 0.01 each (shared evenly). "
"This prevents overconfidence -- the model learns to say '90% sure' not '100% sure'."
)
pdf.highlight(
"Why label smoothing? An overconfident model memorizes training data and fails on "
"new images. Label smoothing 0.1 reduces overconfidence and improves test accuracy. "
"It is like a student who is confident but not arrogant."
)
pdf.section("Optimizer: AdamW (Adam with Weight Decay)")
pdf.kv("Learning Rate (lr=1e-4)",
"Controls update step size. 0.0001 is standard for fine-tuning. "
"Too large: model diverges. Too small: training takes forever.")
pdf.kv("Weight Decay (1e-4)",
"L2 regularization -- penalizes large weights. Keeps all weights small and distributed. "
"Prevents any single weight from dominating. Reduces overfitting.")
pdf.section("Learning Rate Scheduler: Linear Warmup + Cosine Annealing")
pdf.code_block([
"Stage 1 -- Linear Warmup (Epochs 1-3):",
" LR starts at 0.0001 * 0.001 = 0.0000001 (very tiny)",
" Increases linearly to full LR 0.0001 over 3 epochs",
" WHY: At start, weights are randomly initialized or from ImageNet.",
" Large updates immediately can destroy pretrained knowledge.",
" Warmup lets the model stabilize before full-speed training.",
"",
"Stage 2 -- Cosine Annealing (Epochs 4-30):",
" LR decreases following cosine curve: 0.0001 -> 0.000001",
" WHY: As model improves, smaller steps = more precise fine-tuning.",
" Smooth decay avoids overshooting the optimal solution.",
])
pdf.analogy("Landing a Plane",
"Warmup = slow taxi on runway. Full LR = cruise altitude. "
"Cosine annealing = smooth descent to land. You never land at full speed.")
pdf.section("Early Stopping (patience=8)")
pdf.body(
"After each epoch, validation accuracy is measured on the held-out val set. "
"If val accuracy does NOT improve for 8 consecutive epochs, training stops "
"automatically -- even if 30 epochs haven't been reached yet."
)
pdf.kv("p1_best.pth", "Saved whenever a NEW highest val accuracy is achieved during training")
pdf.kv("p1_last.pth", "Saved at the very final epoch (whether stopped early or not)")
pdf.body(
"Why patience=8? Generous enough to allow the model to escape flat regions "
"in the loss landscape. Not so large that we waste compute on a stuck model."
)
pdf.section("What is the Output of Phase 1?")
pdf.body(
"A model (p1_best.pth) that classifies spices from images alone with high accuracy. "
"EfficientNet-B4 has now been fine-tuned from general ImageNet knowledge to "
"spice-specific knowledge. This strong foundation is handed to Phase 2."
)
pdf.highlight(
"Phrase to use: 'Phase 1 establishes a strong image-based feature extractor. "
"The backbone adapts from general ImageNet knowledge to spice-specific visual "
"patterns through fine-tuning. This checkpoint is the foundation for Phase 2.'"
)
# ββ CH 4: PHASE 2 TRAINING ββββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.chapter_title("04", "Phase 2 Training -- Supervised Contrastive Learning (10 Epochs)")
pdf.section("Why Do We Need Phase 2?")
pdf.body(
"After Phase 1, the model classifies spices well overall -- but still struggles "
"with HARD cases. Coriander vs cumin look nearly identical in photos. "
"In the model's internal representation (embedding space), coriander and cumin "
"images are clustered very CLOSE to each other -- like neighbors on a map. "
"When they are close, small errors flip the prediction."
)
pdf.body(
"Phase 2 uses Supervised Contrastive Loss (SupCon) to fix this. "
"Goal: same-class images cluster TIGHTLY together. Different-class images "
"(especially the hard pairs) are pushed FAR APART in embedding space."
)
pdf.analogy("Reorganizing a Library",
"Phase 1: books are findable but coriander and cumin books are mixed on the same shelf. "
"Phase 2: reorganize so all coriander books are in one corner, all cumin books "
"in a completely different corner, far away. Finding the right book becomes easy.")
pdf.section("SupCon Loss -- Simple Explanation")
pdf.body(
"In each training batch of 32 images, there are multiple images per class. "
"For example: 3 cumin images, 2 coriander images, 4 turmeric images, etc."
)
pdf.code_block([
"For each anchor image (say: cumin_photo_01):",
"",
" POSITIVE pairs = other images of the SAME class in the batch",
" -> cumin_photo_02, cumin_photo_03",
" -> PULL these closer: make their embeddings similar",
"",
" NEGATIVE pairs = images of DIFFERENT classes",
" -> all coriander, turmeric, paprika, etc. images",
" -> PUSH these apart: make embeddings different",
"",
"Loss = -log( similarity_with_positives / similarity_with_all_others )",
"Minimize loss = maximize positive similarity relative to negatives",
])
pdf.kv("Temperature tau=0.07",
"Controls sharpness of contrast. Lower = more aggressive pushing apart. "
"0.07 is from the original SupCon paper (Khosla et al., NeurIPS 2020). "
"Standard value used in research worldwide.")
pdf.section("What Gets FROZEN and What Gets TRAINED?")
pdf.code_block([
"TRAINABLE in Phase 2:",
" - backbone (EfficientNet-B4) <- reshaped to separate hard pairs",
" - proj_head (1792->512->128) <- creates SupCon embedding space",
"",
"FROZEN in Phase 2 (DO NOT UPDATE):",
" - tex_branch <- not involved in SupCon",
" - col_branch <- not involved in SupCon",
" - img_head <- Phase 1 classification weights, keep intact",
" - fusion_head <- Phase 3 will train this",
])
pdf.body(
"WHY freeze so much? Phase 2 is ONLY about reshaping the CNN embedding space. "
"We don't want to destroy the Phase 1 classification ability. Only the backbone "
"and projection head need to change. Everything else stays safe."
)
pdf.section("The Projection Head -- What and Why?")
pdf.body(
"proj_head architecture: Linear(1792->512) -> ReLU -> Linear(512->128) -> L2-Normalize."
)
pdf.body(
"It projects the 1792-d backbone output to a 128-d embedding. "
"L2 normalization means every embedding vector has length exactly 1.0 -- "
"all embeddings live on a 128-dimensional unit sphere (like points on a globe). "
"Cosine similarity measures the angle between two points on this sphere. "
"SupCon loss minimizes angles between same-class points."
)
pdf.highlight(
"After Phase 2: all coriander images cluster in one area of the 128-d sphere. "
"All cumin images cluster in a completely different, distant area. "
"This clean geometric separation makes Phase 3 classification far more confident."
)
pdf.section("Why No Validation Accuracy in Phase 2?")
pdf.body(
"SupCon loss does not produce class predictions -- it only shapes the embedding space. "
"There is no accuracy to monitor. Phase 2 always runs all 10 epochs, no early stopping. "
"The effectiveness of Phase 2 is proven indirectly by Phase 3's final accuracy."
)
pdf.section("Ablation A2 -- YOUR Proof That Phase 2 Works")
pdf.code_block([
"Experiment: What if we SKIP Phase 2 entirely?",
"",
"Phase 1 -> Phase 3 directly (no SupCon): ~97.8% accuracy",
"Phase 1 -> Phase 2 -> Phase 3 (full): 99.68% accuracy",
"",
"Improvement from Phase 2: +1.88 percentage points",
"Most impact on: coriander/cumin, paprika/turmeric (the hardest pairs)",
])
pdf.body(
"This ablation study proves Phase 2 is not optional -- it is essential. "
"The model performs well overall without it, but the hard cases suffer most. "
"And in food safety applications, getting the hard cases RIGHT is critical."
)
# ββ CH 5: Q&A PREP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
pdf.add_page()
pdf.chapter_title("05", "Q&A Prep -- Questions Sir Will Ask YOU")
pdf.section("Architecture Questions")
pdf.qa("Why multi-modal? Why not just CNN?",
"Cumin and coriander look almost identical in photos -- CNN alone gets confused. "
"Texture (LBP+GLCM) and color (HSV) give extra information the image cannot provide. "
"Ablation A1 confirmed: removing any branch drops accuracy. Full fusion is always best.")
pdf.qa("What is AttentionFusion and why not simple concatenation?",
"Simple concatenation weights all branches equally. AttentionFusion uses a learned "
"softmax gate (3 weights summing to 1.0). For yellow powder spices, color branch "
"gets higher weight. For textured seeds, texture gets more weight. The model learns "
"these weights automatically during Phase 3 training -- more intelligent than fixed weights.")
pdf.qa("What does num_classes=0 do?",
"Removes EfficientNet-B4's original 1000-class ImageNet head. We only want the feature "
"extractor part -- 1792-d output. We then add our own custom classification heads.")
pdf.section("EfficientNet Questions")
pdf.qa("Why EfficientNet-B4 and not ResNet or ViT?",
"We tested 5 backbones (Ablation A4). EfficientNet-B4 gave the best accuracy (99.68%) "
"with fast inference (2.70ms). ResNet-50 only got 94.2%. ViT got 99.73% but is 4.30ms -- "
"37% slower. For real-world deployment, B4 gives the best accuracy-speed tradeoff.")
pdf.qa("What is Transfer Learning?",
"Using a model pre-trained on a large dataset (ImageNet, 1.2M images) as the starting "
"point for our task. EfficientNet-B4 already knows how to detect edges, shapes, textures. "
"We only have 11,000 spice images -- training from scratch would give poor results. "
"Transfer learning gives us a 99.68% result with limited data.")
pdf.qa("What is Global Average Pooling? Why does it give 1792-d?",
"After convolutional layers, we have 1792 feature maps each of spatial size 7x7. "
"Global Average Pooling takes the mean of all 49 values in each feature map. "
"1792 maps x 1 number each = 1792-d vector. This is translation invariant -- "
"the spice can be anywhere in the image and we still get a consistent representation.")
pdf.section("Phase 1 Questions")
pdf.qa("What is label smoothing and why use it?",
"Instead of hard targets (1.0 correct, 0.0 others), use 0.9 for correct and 0.01 for "
"others. Prevents overconfidence. An overconfident model memorizes training data but "
"fails on new images. Label smoothing 0.1 improves generalization on the test set.")
pdf.qa("Why warmup in the learning rate schedule?",
"At training start, a large LR causes chaotic gradient updates that can destroy "
"pretrained ImageNet weights. Warmup starts with a tiny LR and gradually increases "
"over 3 epochs, letting the model stabilize before full-speed training begins.")
pdf.qa("What is early stopping? Why patience=8?",
"If validation accuracy does not improve for 8 consecutive epochs, training stops. "
"Prevents overfitting and saves compute. Patience=8 is generous enough to escape "
"flat regions in the loss landscape before deciding the model is stuck.")
pdf.section("Phase 2 Questions")
pdf.qa("Explain Supervised Contrastive Learning simply.",
"In each batch, same-class images are positive pairs, different-class are negatives. "
"SupCon Loss pulls positive pairs close together and pushes negative pairs far apart "
"in the 128-d embedding space. After Phase 2, all cumin images are in one cluster, "
"all coriander in another distant cluster -- they are easy to distinguish.")
pdf.qa("What is temperature tau=0.07 in SupCon?",
"Controls how sharp the contrast is. Lower = more aggressive separation. "
"0.07 is the standard value from Khosla et al. (NeurIPS 2020). "
"Too high: weak learning signal. Too low: training becomes unstable.")
pdf.qa("Why freeze most layers in Phase 2?",
"Phase 2 ONLY reshapes the CNN embedding space for hard-negative pairs. "
"We freeze tex_branch, col_branch, img_head, fusion_head to preserve Phase 1 "
"classification weights. Only backbone and proj_head are updated.")
pdf.section("The Numbers You Must Know By Heart")
pdf.code_block([
"1792-d = EfficientNet-B4 Global Average Pool output",
"58-d = LBP(10) + GLCM(48) texture input to texture branch",
"100-d = HSV Histogram (H:36 + S:32 + V:32) color input",
"256-d = Texture branch MLP output",
"128-d = Color branch MLP output OR proj_head SupCon embedding",
"2176-d = 1792 + 256 + 128 (fused representation after AttentionFusion)",
"21.6M = Total model parameters",
"Phase 1 = 30 epochs | CE + LabelSmooth(0.1) | ALL params train",
"Phase 2 = 10 epochs | SupCon (tau=0.07) | backbone + proj_head only",
"Phase 3 = 10 epochs | 0.5*CE + 0.5*SupCon | ALL params train",
"99.68% = SpiceFusionNet Top-1 Accuracy on test set",
"2.70ms = Inference time per image",
])
pdf.ln(4)
pdf.set_fill_color(26, 107, 138)
pdf.set_text_color(255, 255, 255)
pdf.set_font("Helvetica", "B", 11)
pdf.cell(0, 11, " You built this. You understand it. Walk in with confidence.",
fill=True, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align="C")
out_path = Path("D:/SpiceNet/outputs/Ramim_Contribution_Guide.pdf")
out_path.parent.mkdir(parents=True, exist_ok=True)
pdf.output(str(out_path))
print(f"PDF saved -> {out_path}")
|