-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexplainer.html
More file actions
687 lines (616 loc) · 47.5 KB
/
Copy pathexplainer.html
File metadata and controls
687 lines (616 loc) · 47.5 KB
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
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
<title>How the ML Works — A Spec Sheet for Engineers</title>
<style>
:root {
--ground:#eef1f5; --surface:#ffffff; --surface-2:#f5f7fa;
--ink:#14202c; --ink-2:#3f5060; --ink-3:#6b7c8c;
--line:#c7d2dd; --line-2:#dde5ec;
--accent:#1f6feb; --accent-ink:#0b4bbf; --accent-wash:rgba(31,111,235,0.07);
--good:#1f9d57; --warn:#c9781f; --bad:#d64550;
--grid:rgba(31,111,235,0.055);
--mono: ui-monospace,"Cascadia Code","SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;
--sans: system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme: dark){
:root{
--ground:#0c131a; --surface:#121c25; --surface-2:#0f1821;
--ink:#e6edf3; --ink-2:#a9b8c4; --ink-3:#78899a;
--line:#283742; --line-2:#1c2833;
--accent:#4d9bff; --accent-ink:#83b8ff; --accent-wash:rgba(77,155,255,0.09);
--good:#3fca7d; --warn:#e0a94a; --bad:#ef6b73;
--grid:rgba(77,155,255,0.05);
}
}
:root[data-theme="light"]{
--ground:#eef1f5; --surface:#ffffff; --surface-2:#f5f7fa;
--ink:#14202c; --ink-2:#3f5060; --ink-3:#6b7c8c;
--line:#c7d2dd; --line-2:#dde5ec;
--accent:#1f6feb; --accent-ink:#0b4bbf; --accent-wash:rgba(31,111,235,0.07);
--good:#1f9d57; --warn:#c9781f; --bad:#d64550; --grid:rgba(31,111,235,0.055);
}
:root[data-theme="dark"]{
--ground:#0c131a; --surface:#121c25; --surface-2:#0f1821;
--ink:#e6edf3; --ink-2:#a9b8c4; --ink-3:#78899a;
--line:#283742; --line-2:#1c2833;
--accent:#4d9bff; --accent-ink:#83b8ff; --accent-wash:rgba(77,155,255,0.09);
--good:#3fca7d; --warn:#e0a94a; --bad:#ef6b73; --grid:rgba(77,155,255,0.05);
}
*{box-sizing:border-box}
body{
margin:0; background:var(--ground); color:var(--ink);
font-family:var(--sans); line-height:1.62; font-size:17px;
-webkit-font-smoothing:antialiased;
background-image:linear-gradient(var(--grid) 1px,transparent 1px),
linear-gradient(90deg,var(--grid) 1px,transparent 1px);
background-size:26px 26px;
}
.wrap{max-width:920px; margin:0 auto; padding:0 24px 120px}
.col{max-width:680px}
/* ---- cover ---- */
header.cover{
border-bottom:2px solid var(--ink); margin:0 auto 56px;
padding:56px 0 22px; max-width:920px;
}
.stamp{
font-family:var(--mono); font-size:12px; letter-spacing:.14em; text-transform:uppercase;
color:var(--accent-ink); display:flex; flex-wrap:wrap; gap:6px 20px; margin-bottom:26px;
}
.stamp b{color:var(--ink-3); font-weight:400}
h1{
font-family:var(--mono); font-weight:700; font-size:clamp(30px,5.4vw,52px);
line-height:1.04; letter-spacing:-.01em; margin:0 0 18px; text-wrap:balance;
}
.lede{font-size:19px; color:var(--ink-2); max-width:640px; margin:0}
.lede b{color:var(--ink); font-weight:600}
/* ---- toc ---- */
nav.toc{
font-family:var(--mono); font-size:13.5px; margin:30px 0 0;
display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:2px 26px;
}
nav.toc a{
color:var(--ink-2); text-decoration:none; display:flex; gap:12px;
padding:5px 0; border-bottom:1px dotted var(--line);
}
nav.toc a:hover{color:var(--accent-ink)}
nav.toc .n{color:var(--accent); min-width:26px}
/* ---- sections ---- */
section{margin:66px auto 0; max-width:920px}
.sechead{display:flex; align-items:baseline; gap:16px; margin-bottom:8px; border-top:1px solid var(--line); padding-top:20px}
.sechead .sec{font-family:var(--mono); color:var(--accent); font-size:15px; font-weight:700; letter-spacing:.02em}
h2{font-family:var(--mono); font-size:clamp(21px,3vw,27px); font-weight:700; margin:0; letter-spacing:-.01em; text-wrap:balance}
h3{font-family:var(--mono); font-size:16px; font-weight:700; margin:34px 0 6px; color:var(--ink)}
h3 .tag{color:var(--ink-3); font-weight:400}
p{margin:14px 0}
p, li{max-width:680px}
strong{color:var(--ink); font-weight:600}
.accent-t{color:var(--accent-ink); font-weight:600}
em{color:var(--ink); font-style:normal; background:var(--accent-wash); padding:0 4px; border-radius:3px}
code{font-family:var(--mono); font-size:.86em; background:var(--surface-2); border:1px solid var(--line-2); border-radius:4px; padding:1px 5px; color:var(--ink)}
ul{padding-left:0; list-style:none; margin:14px 0}
ul li{position:relative; padding-left:22px; margin:8px 0}
ul li::before{content:"›"; position:absolute; left:4px; color:var(--accent); font-family:var(--mono); font-weight:700}
.analogy{
border-left:3px solid var(--accent); background:var(--accent-wash);
padding:12px 18px; margin:16px 0; border-radius:0 6px 6px 0; max-width:680px;
}
.analogy .k{font-family:var(--mono); font-size:11px; letter-spacing:.12em; text-transform:uppercase; color:var(--accent-ink); display:block; margin-bottom:3px}
.analogy p{margin:0; color:var(--ink-2)}
.analogy p b{color:var(--ink)}
/* ---- figure plates ---- */
figure.plate{
margin:30px 0; background:var(--surface); border:1px solid var(--line);
border-radius:4px; padding:26px 20px 12px; position:relative; overflow-x:auto;
}
figure.plate::before,figure.plate::after{
content:""; position:absolute; width:11px; height:11px; border:2px solid var(--accent);
}
figure.plate::before{top:7px; left:7px; border-right:0; border-bottom:0}
figure.plate::after{bottom:7px; right:7px; border-left:0; border-top:0}
figure.plate svg{display:block; width:100%; height:auto; min-width:560px}
figcaption{
font-family:var(--mono); font-size:12px; color:var(--ink-3); letter-spacing:.04em;
margin-top:12px; padding-top:10px; border-top:1px dashed var(--line);
}
figcaption b{color:var(--accent-ink); font-weight:700; letter-spacing:.08em}
/* svg theming */
svg .ln{stroke:var(--ink-3); stroke-width:1.5; fill:none}
svg .ln2{stroke:var(--line); stroke-width:1; fill:none}
svg .bx{fill:var(--surface-2); stroke:var(--ink-3); stroke-width:1.5}
svg .bxa{fill:var(--accent-wash); stroke:var(--accent); stroke-width:1.75}
svg .fa{fill:var(--accent)}
svg .fg{fill:var(--good)}
svg .fb{fill:var(--bad)}
svg .fw{fill:var(--warn)}
svg .sa{stroke:var(--accent); stroke-width:1.75; fill:none}
svg .sg{stroke:var(--good); stroke-width:1.75; fill:none}
svg .sb{stroke:var(--bad); stroke-width:1.75; fill:none}
svg text{fill:var(--ink); font-family:var(--mono); font-size:13px}
svg .t2{fill:var(--ink-2); font-size:12px}
svg .t3{fill:var(--ink-3); font-size:11px}
svg .tw{fill:#fff; font-size:13px}
svg .ttl{fill:var(--ink); font-size:12.5px; font-weight:700}
table.spec{border-collapse:collapse; width:100%; max-width:680px; margin:18px 0; font-size:15px}
table.spec th,table.spec td{border:1px solid var(--line); padding:8px 12px; text-align:left; vertical-align:top}
table.spec th{font-family:var(--mono); font-size:12px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3); background:var(--surface-2); font-weight:600}
table.spec td:first-child{font-family:var(--mono); color:var(--accent-ink); white-space:nowrap; font-size:13.5px}
table.spec tr:nth-child(even) td{background:var(--surface-2)}
.metricrow{display:flex; flex-wrap:wrap; gap:12px; margin:20px 0; max-width:680px}
.metric{flex:1 1 150px; background:var(--surface); border:1px solid var(--line); border-radius:4px; padding:14px 16px}
.metric .v{font-family:var(--mono); font-size:26px; font-weight:700; color:var(--accent-ink); font-variant-numeric:tabular-nums}
.metric .l{font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); margin-top:4px}
.metric .s{font-size:13px; color:var(--ink-2); margin-top:6px}
hr.soft{border:0; border-top:1px solid var(--line); margin:0}
footer{max-width:920px; margin:70px auto 0; padding-top:22px; border-top:2px solid var(--ink); font-family:var(--mono); font-size:13px; color:var(--ink-3)}
footer b{color:var(--ink-2); font-weight:600}
a.inl{color:var(--accent-ink); text-decoration:none; border-bottom:1px solid var(--line)}
@media (max-width:640px){ body{font-size:16px} .wrap{padding:0 16px 80px} }
@media (prefers-reduced-motion:no-preference){ nav.toc a, figure.plate{transition:color .15s} }
</style>
<div class="wrap">
<header class="cover">
<div class="stamp">
<span>DOC / ml-internals</span>
<span><b>REV</b> 2026‑07‑07</span>
<span><b>AUDIENCE</b> engineers, zero ML background</span>
<span><b>SUBJECT</b> tensorflow poc suite</span>
</div>
<h1>How the ML Actually Works</h1>
<p class="lede">A ground-up teardown of the two TensorFlow models here — written for someone who knows <b>functions, loops, and optimization</b>, but has never touched machine learning. Every piece, explained as the engineering it really is.</p>
<nav class="toc">
<a href="#s0"><span class="n">§0</span><span>The problem behind the models</span></a>
<a href="#s1"><span class="n">§1</span><span>The paradigm flip</span></a>
<a href="#s2"><span class="n">§2</span><span>Anatomy of the model</span></a>
<a href="#s3"><span class="n">§3</span><span>The training loop</span></a>
<a href="#s4"><span class="n">§4</span><span>Our two models</span></a>
<a href="#s5"><span class="n">§5</span><span>Doing it honestly</span></a>
<a href="#s6"><span class="n">§6</span><span>Engine & API: TF vs Keras</span></a>
</nav>
</header>
<!-- ============ S0 ============ -->
<section id="s0">
<div class="sechead"><span class="sec">§0</span><h2>The problem behind the models</h2></div>
<div class="col">
<p>These aren't toy models on toy data. They were trained on real operational
data from a live <strong>inside-sales call system</strong> — every call the
sales team places or receives, logged with who dialed, when, and whether it
connected.</p>
<h3>The business problem <span class="tag">// scale breaks triage</span></h3>
<p>The team makes hundreds of calls a day, and <strong>only ~41% connect</strong>.
Once you're dialing at that volume, <em>placing</em> calls isn't the bottleneck
— <strong>knowing when and by whom to dial</strong> is. Rep time is the
scarce resource; spending it on numbers that won't pick up, at hours nobody
answers, is pure waste.</p>
<h3>What we set out to predict</h3>
<ul>
<li><strong>Connect likelihood (model 1).</strong> At the moment of dialing
— before the call is placed — estimate the probability it will be
answered, from dial-time features (rep, line, hour, day). That score
<em>ranks</em> when and by whom to call, so effort flows to the calls most
likely to connect.</li>
<li><strong>Operational volume (model 2).</strong> Forecast call throughput
over time — how many calls to expect next — so staffing and capacity
can be planned instead of guessed.</li>
</ul>
<p>Both are genuine supervised-learning problems with honest difficulty: a
balanced but thin-signal label, and a small sample — which is exactly why
the <a href="#s5">honesty section</a> matters here and isn't decoration.</p>
<h3>Why TensorFlow / Keras <span class="tag">// one engine, two problem shapes</span></h3>
<p>The two problems span two different data shapes — structured columns and
a time series. TensorFlow/Keras was the deliberate choice:</p>
</div>
<table class="spec">
<tr><th>Reason</th><th>What it buys</th></tr>
<tr><td>one framework, all families</td><td>A tabular DNN and a sequence (LSTM) model share one API (<code>layers</code> → <code>Model</code> → <code>compile/fit/predict</code>) — no switching libraries per problem.</td></tr>
<tr><td>in-graph preprocessing</td><td><code>Normalization</code> and <code>StringLookup</code> live <em>inside</em> the model, so the saved artifact ingests a raw feature dict. Kills train/serve skew — a production concern, not a notebook nicety.</td></tr>
<tr><td>autodiff + <code>fit()</code></td><td>The whole training loop — gradients, optimizer, callbacks, early stopping — is handled. Fast iteration on a laptop, CPU-only.</td></tr>
<tr><td>portable SavedModel</td><td>One <code>.keras</code> file per model = a compiled function you load and call later, no retraining, easy to ship behind an endpoint.</td></tr>
<tr><td>the stated goal</td><td>This is also a <strong>learning project</strong> — demonstrating hands-on TensorFlow competence across problem families was itself an objective.</td></tr>
</table>
<div class="col">
<div class="analogy">
<span class="k">The one-liner</span>
<p>A high-volume call operation produces more dials than a human can optimize.
These models turn its history into a <b>ranking signal</b> (which calls will
connect) and a <b>capacity forecast</b> (how much volume is coming) —
built on TensorFlow because one framework, with production-grade in-graph
preprocessing, covers every data shape the problem throws at it.</p>
</div>
</div>
</section>
<!-- ============ S1 ============ -->
<section id="s1">
<div class="sechead"><span class="sec">§1</span><h2>The one flip that makes ML click</h2></div>
<div class="col">
<p>You already know how to write a function: you supply the <strong>rules</strong>, the caller supplies the <strong>input</strong>, out comes the <strong>output</strong>. Machine learning inverts the middle step. You can't write the rules — they're too messy (how <em>do</em> you score a sales call's odds of connecting?) — so instead you hand the machine a pile of <strong>example input→output pairs</strong>, and an algorithm <strong>derives the rules for you</strong>.</p>
<p>A "model" is therefore just <strong>a function whose body you didn't write</strong>. <strong>Training</strong> is the process that writes that body. <strong>Inference</strong> is calling the finished function like any other.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 236" role="img" aria-label="Traditional programming versus machine learning, with the middle term flipped">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" class="fa"/>
</marker>
<marker id="arn" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--ink-3)"/>
</marker>
</defs>
<text x="18" y="30" class="ttl">TRADITIONAL PROGRAMMING</text>
<rect x="18" y="46" width="120" height="46" rx="4" class="bx"/>
<rect x="176" y="46" width="120" height="46" rx="4" class="bx"/>
<rect x="334" y="46" width="120" height="46" rx="4" class="bxa"/>
<text x="78" y="74" text-anchor="middle" class="t2">rules</text>
<text x="236" y="74" text-anchor="middle" class="t2">input</text>
<text x="394" y="74" text-anchor="middle">output</text>
<line x1="138" y1="69" x2="176" y2="69" class="ln2" marker-end="url(#arn)"/>
<line x1="296" y1="69" x2="334" y2="69" class="ln2" marker-end="url(#arn)"/>
<text x="500" y="66" class="t3">you write the</text>
<text x="500" y="82" class="t3" fill="var(--ink)">rules by hand</text>
<line x1="18" y1="118" x2="702" y2="118" class="ln2" stroke-dasharray="4 4"/>
<text x="18" y="150" class="ttl" fill="var(--accent-ink)">MACHINE LEARNING — TRAINING</text>
<rect x="18" y="166" width="120" height="46" rx="4" class="bx"/>
<rect x="176" y="166" width="120" height="46" rx="4" class="bx"/>
<rect x="334" y="166" width="120" height="46" rx="4" class="bxa"/>
<text x="78" y="194" text-anchor="middle" class="t2">input</text>
<text x="236" y="194" text-anchor="middle" class="t2">output</text>
<text x="394" y="194" text-anchor="middle">rules</text>
<line x1="138" y1="189" x2="176" y2="189" class="ln2" marker-end="url(#arn)"/>
<line x1="296" y1="189" x2="334" y2="189" class="sa" marker-end="url(#ar)"/>
<text x="500" y="186" class="t3">the machine</text>
<text x="500" y="202" class="t3" fill="var(--accent-ink)">derives the rules</text>
</svg>
<figcaption><b>FIG.01</b> — Same three terms; the unknown moves from the output to the rules. That single swap is the whole discipline.</figcaption>
</figure>
</section>
<!-- ============ S2 ============ -->
<section id="s2">
<div class="sechead"><span class="sec">§2</span><h2>Anatomy of the model — the function itself</h2></div>
<div class="col">
<p>Before we can train anything, we need the parameterized function whose "rules" are just <strong>numbers we can tune</strong>. Here is every part, bottom-up.</p>
<h3>Tensor <span class="tag">// the only data type</span></h3>
<p>A <strong>tensor</strong> is an n-dimensional array of numbers — that's it. A single number is a 0-D tensor (scalar), a list is 1-D (vector), a spreadsheet is 2-D (matrix), a batch of images is 4-D. Everything flowing through the model — inputs, weights, intermediate results, outputs — is a tensor. The "Tensor" in TensorFlow is literally this: the library is a machine for pushing tensors through math, fast, on a GPU.</p>
<h3>Weights & biases <span class="tag">// the tunable knobs</span></h3>
<p>These are the numbers the training process is allowed to change — the "learned" part of the model. A small model has thousands; a large language model has hundreds of billions. Untrained, they're random, so the function outputs noise. <strong>Learning = finding good values for these knobs.</strong> Nothing else in the model changes.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 220" role="img" aria-label="A single neuron: inputs times weights, summed with a bias, through an activation function">
<circle cx="70" cy="50" r="16" class="bx"/>
<circle cx="70" cy="110" r="16" class="bx"/>
<circle cx="70" cy="170" r="16" class="bx"/>
<text x="70" y="55" text-anchor="middle" class="t2">x₁</text>
<text x="70" y="115" text-anchor="middle" class="t2">x₂</text>
<text x="70" y="175" text-anchor="middle" class="t2">x₃</text>
<text x="70" y="205" text-anchor="middle" class="t3">inputs</text>
<line x1="86" y1="50" x2="300" y2="102" class="ln"/>
<line x1="86" y1="110" x2="300" y2="110" class="ln"/>
<line x1="86" y1="170" x2="300" y2="118" class="ln"/>
<text x="180" y="66" text-anchor="middle" class="t3">× w₁</text>
<text x="180" y="103" text-anchor="middle" class="t3">× w₂</text>
<text x="180" y="160" text-anchor="middle" class="t3">× w₃</text>
<rect x="300" y="86" width="150" height="48" rx="6" class="bxa"/>
<text x="375" y="108" text-anchor="middle" class="ttl">Σ (w·x) + b</text>
<text x="375" y="126" text-anchor="middle" class="t3">weighted sum + bias</text>
<line x1="450" y1="110" x2="516" y2="110" class="ln" marker-end="url(#ar)"/>
<rect x="516" y="86" width="96" height="48" rx="6" class="bx"/>
<text x="564" y="106" text-anchor="middle">f( )</text>
<text x="564" y="122" text-anchor="middle" class="t3">activation</text>
<line x1="612" y1="110" x2="672" y2="110" class="ln" marker-end="url(#ar)"/>
<circle cx="690" cy="110" r="16" class="bxa"/>
<text x="690" y="115" text-anchor="middle">y</text>
</svg>
<figcaption><b>FIG.02</b> — One neuron. <code>y = f(w·x + b)</code>. The <span class="accent-t">w</span>'s and <span class="accent-t">b</span> are the knobs; <code>f</code> is a fixed nonlinear squish. A layer is just many of these side by side; a network is layers stacked.</figcaption>
</figure>
<div class="col">
<h3>The neuron & the activation function <span class="tag">// where nonlinearity comes from</span></h3>
<p>A neuron computes a <strong>weighted sum of its inputs plus a bias</strong> (<code>w·x + b</code> — a dot product, pure linear algebra), then passes it through a fixed <strong>activation function</strong> <code>f</code>. That function is the crucial twist: without it, stacking layers would just collapse into one big linear equation (a line), no matter how many you stack. The nonlinearity — e.g. <strong>ReLU</strong> (<code>max(0,x)</code>, "pass positives, zero out negatives") or <strong>sigmoid</strong> (squash any number into 0–1) — is what lets the network bend and represent complex, curved decision boundaries.</p>
<div class="analogy">
<span class="k">Programmer's model</span>
<p>A layer is one line: <b><code>output = activation(W @ input + b)</code></b> — a matrix multiply, an add, a squish. A "deep" network is just <b>function composition</b>: <code>L3(L2(L1(x)))</code>. Depth buys expressiveness the same way composing small functions buys you complex behavior.</p>
</div>
<h3>Layer, network, forward pass</h3>
<p>A <strong>layer</strong> (Keras <code>Dense</code>) is a vector of neurons sharing the same inputs. A <strong>network</strong> is layers chained so each one's output feeds the next. Running data through, input→output, is the <strong>forward pass</strong> — that's inference, and it's also step one of every training iteration.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 240" role="img" aria-label="A feed-forward network: input layer, two hidden layers, output">
<g class="ln2">
<!-- connections input->h1 -->
<line x1="96" y1="70" x2="250" y2="50"/><line x1="96" y1="70" x2="250" y2="110"/><line x1="96" y1="70" x2="250" y2="170"/><line x1="96" y1="70" x2="250" y2="200"/>
<line x1="96" y1="130" x2="250" y2="50"/><line x1="96" y1="130" x2="250" y2="110"/><line x1="96" y1="130" x2="250" y2="170"/><line x1="96" y1="130" x2="250" y2="200"/>
<line x1="96" y1="190" x2="250" y2="50"/><line x1="96" y1="190" x2="250" y2="110"/><line x1="96" y1="190" x2="250" y2="170"/><line x1="96" y1="190" x2="250" y2="200"/>
<!-- h1->h2 -->
<line x1="270" y1="50" x2="440" y2="90"/><line x1="270" y1="50" x2="440" y2="160"/>
<line x1="270" y1="110" x2="440" y2="90"/><line x1="270" y1="110" x2="440" y2="160"/>
<line x1="270" y1="170" x2="440" y2="90"/><line x1="270" y1="170" x2="440" y2="160"/>
<line x1="270" y1="200" x2="440" y2="90"/><line x1="270" y1="200" x2="440" y2="160"/>
<!-- h2->out -->
<line x1="460" y1="90" x2="620" y2="125"/><line x1="460" y1="160" x2="620" y2="125"/>
</g>
<g>
<circle cx="80" cy="70" r="15" class="bx"/><circle cx="80" cy="130" r="15" class="bx"/><circle cx="80" cy="190" r="15" class="bx"/>
<circle cx="260" cy="50" r="15" class="bxa"/><circle cx="260" cy="110" r="15" class="bxa"/><circle cx="260" cy="170" r="15" class="bxa"/><circle cx="260" cy="200" r="15" class="bxa"/>
<circle cx="450" cy="90" r="15" class="bxa"/><circle cx="450" cy="160" r="15" class="bxa"/>
<circle cx="620" cy="125" r="16" class="bx"/>
</g>
<text x="80" y="222" text-anchor="middle" class="t3">input</text>
<text x="260" y="234" text-anchor="middle" class="t3">hidden layer 1</text>
<text x="450" y="200" text-anchor="middle" class="t3">hidden 2</text>
<text x="620" y="160" text-anchor="middle" class="t3">output</text>
<text x="620" y="129" text-anchor="middle" class="tw" fill="var(--ink)">ŷ</text>
<line x1="60" y1="20" x2="640" y2="20" class="sa" marker-end="url(#ar)"/>
<text x="350" y="14" text-anchor="middle" class="t3" fill="var(--accent-ink)">forward pass — data flows left to right</text>
</svg>
<figcaption><b>FIG.03</b> — Every line is one weight; every node applies <code>f(w·x+b)</code>. Our models are small versions of this: a couple of <code>Dense</code> layers narrowing to a single output neuron.</figcaption>
</figure>
</section>
<!-- ============ S3 ============ -->
<section id="s3">
<div class="sechead"><span class="sec">§3</span><h2>The training loop — how the knobs get tuned</h2></div>
<div class="col">
<p>This is the heart of it, and it's an <strong>optimization loop you could have written yourself</strong>. Start with random knobs; repeatedly measure how wrong the model is and nudge every knob in the direction that reduces the wrongness.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 250" role="img" aria-label="The training loop: forward pass, loss, backpropagation, optimizer update, repeat">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" class="fa"/></marker>
</defs>
<g>
<rect x="22" y="60" width="140" height="66" rx="6" class="bx"/>
<rect x="204" y="60" width="140" height="66" rx="6" class="bx"/>
<rect x="386" y="60" width="140" height="66" rx="6" class="bx"/>
<rect x="568" y="60" width="130" height="66" rx="6" class="bxa"/>
</g>
<text x="92" y="88" text-anchor="middle" class="ttl">1 · FORWARD</text>
<text x="92" y="107" text-anchor="middle" class="t3">predict ŷ</text>
<text x="274" y="88" text-anchor="middle" class="ttl">2 · LOSS</text>
<text x="274" y="107" text-anchor="middle" class="t3">how wrong? → 1 #</text>
<text x="456" y="88" text-anchor="middle" class="ttl">3 · BACKPROP</text>
<text x="456" y="107" text-anchor="middle" class="t3">∂loss/∂knob</text>
<text x="633" y="88" text-anchor="middle" class="ttl" fill="var(--accent-ink)">4 · UPDATE</text>
<text x="633" y="107" text-anchor="middle" class="t3">nudge knobs</text>
<line x1="162" y1="93" x2="204" y2="93" class="ln" marker-end="url(#ar2)"/>
<line x1="344" y1="93" x2="386" y2="93" class="ln" marker-end="url(#ar2)"/>
<line x1="526" y1="93" x2="568" y2="93" class="ln" marker-end="url(#ar2)"/>
<!-- return loop -->
<path d="M633,126 L633,180 L92,180 L92,126" class="sa" marker-end="url(#ar2)"/>
<text x="360" y="200" text-anchor="middle" class="t3" fill="var(--accent-ink)">repeat for every batch, for many epochs, until loss stops falling</text>
<text x="360" y="38" text-anchor="middle" class="t2">one training step</text>
<line x1="22" y1="46" x2="698" y2="46" class="ln2"/>
</svg>
<figcaption><b>FIG.04</b> — The loop <code>model.fit()</code> runs for you. Steps 3–4 are the only "magic": <span class="accent-t">TensorFlow</span> computes the slope of the loss w.r.t. every knob automatically, then the optimizer steps downhill.</figcaption>
</figure>
<div class="col">
<h3>Loss function <span class="tag">// the number you minimize</span></h3>
<p>The <strong>loss</strong> collapses "how wrong were all the predictions" into a single number. Lower = better. The whole loop exists to drive this down. Choice of loss depends on the task: <strong>binary cross-entropy</strong> for yes/no probabilities (our two classifiers), <strong>mean squared error</strong> for predicting a real number (our forecaster). It's your objective function — the thing an optimizer optimizes.</p>
<h3>Gradient <span class="tag">// which way is downhill</span></h3>
<p>The <strong>gradient</strong> is the vector of partial derivatives of the loss with respect to every knob — it answers, for each knob, "if I increase this a hair, does the loss go up or down, and how steeply?" It points in the direction of <em>steepest increase</em>, so we step the opposite way.</p>
<h3>Backpropagation <span class="tag">// computing the gradient efficiently</span></h3>
<p>A network is a deep composition of functions, so its derivative is computed by the <strong>chain rule</strong> — run <em>backward</em> from the loss through each layer, multiplying local derivatives as you go. That's all "backprop" is: the chain rule, applied right-to-left, reusing intermediate results so it's fast. You never do this by hand — <strong>automatic differentiation</strong> in TensorFlow records every operation of the forward pass and replays it in reverse.</p>
<div class="analogy">
<span class="k">Programmer's model</span>
<p>You're on a foggy hillside wanting the lowest point. You can't see the valley, but you can feel the <b>slope under your feet</b> (the gradient) and step downhill. Repeat thousands of times. That's <b>gradient descent</b>; backprop is just the efficient way to feel the slope on a very high-dimensional hill.</p>
</div>
</div>
<figure class="plate">
<svg viewBox="0 0 720 220" role="img" aria-label="Gradient descent stepping down a loss curve toward the minimum">
<line x1="60" y1="180" x2="680" y2="180" class="ln2"/>
<line x1="60" y1="30" x2="60" y2="180" class="ln2"/>
<text x="46" y="40" text-anchor="end" class="t3">loss</text>
<text x="676" y="198" text-anchor="end" class="t3">value of a knob →</text>
<path d="M80,50 Q370,300 660,80" class="sa"/>
<!-- ball steps -->
<circle cx="120" cy="86" r="7" class="fb"/>
<circle cx="180" cy="120" r="7" class="fw"/>
<circle cx="255" cy="150" r="7" class="fw"/>
<circle cx="345" cy="166" r="7" class="fa"/>
<circle cx="370" cy="169" r="8" class="fg"/>
<g class="ln" marker-end="url(#ar)">
<line x1="127" y1="90" x2="172" y2="116"/>
<line x1="188" y1="124" x2="247" y2="147"/>
<line x1="263" y1="152" x2="337" y2="165"/>
</g>
<text x="120" y="72" text-anchor="middle" class="t3" fill="var(--bad)">start (random)</text>
<text x="378" y="192" text-anchor="middle" class="t3" fill="var(--good)">minimum</text>
<text x="230" y="96" class="t3">step = learning rate × slope</text>
</svg>
<figcaption><b>FIG.05</b> — Each step moves a knob opposite its gradient. <span class="accent-t">Learning rate</span> is the step size: too big overshoots the valley, too small crawls. <span class="accent-t">Adam</span> is a smarter stepper that adapts the size per-knob.</figcaption>
</figure>
<div class="col">
<h3>Optimizer, learning rate, epoch, batch</h3>
<table class="spec">
<tr><th>Term</th><th>What it is</th></tr>
<tr><td>optimizer</td><td>The rule for turning gradients into knob updates. <strong>Adam</strong> (what we used) keeps per-knob momentum & adaptive step sizes — robust default.</td></tr>
<tr><td>learning rate</td><td>Global step-size multiplier. The single most important dial. We used <code>1e-3</code>.</td></tr>
<tr><td>batch</td><td>We don't compute the loss on all data at once — we use small chunks (e.g. 16 rows) per step. Faster, and the noise actually helps generalization.</td></tr>
<tr><td>epoch</td><td>One full pass over the entire training set. We trained for tens–hundreds, stopping early when validation stopped improving.</td></tr>
</table>
</div>
</section>
<!-- ============ S4 ============ -->
<section id="s4">
<div class="sechead"><span class="sec">§4</span><h2>Our two models — same recipe, two data shapes</h2></div>
<div class="col">
<p>Both are the identical loop above. The <em>only</em> thing that differs is the shape of the data going in and the layer types that suit it.</p>
<h3>Model 1 — Tabular classifier <span class="tag">// yes/no from columns</span></h3>
<p>The connect-rate model predicts whether a call will be answered, from <strong>structured columns</strong> known at dial time (which rep, inbound/outbound, which line, hour-of-day, day-of-week). A network eats numbers, so numeric columns get <strong>normalized</strong> (rescaled to mean 0, so no single big-numbered feature dominates) and each category — the <code>rep</code>, the line — gets <strong>one-hot encoded</strong> (a 0/1 column per value, because "rep A" isn't numerically greater than "rep B"). Both steps are <strong>Keras layers</strong>, so the saved model ingests a raw feature dict — no external scaler to reproduce. That feeds two <code>Dense</code> layers → one probability. We run it head-to-head against gradient-boosted trees, the honest baseline for tabular data.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 132" role="img" aria-label="Tabular pipeline: feature dict to normalization plus one-hot to dense network to probability">
<rect x="16" y="40" width="196" height="54" rx="5" class="bx"/>
<text x="114" y="63" text-anchor="middle" class="t2">{ rep, hour, line, ... }</text>
<text x="114" y="81" text-anchor="middle" class="t3">raw feature dict</text>
<line x1="212" y1="67" x2="252" y2="67" class="ln" marker-end="url(#ar)"/>
<rect x="252" y="40" width="150" height="54" rx="5" class="bxa"/>
<text x="327" y="61" text-anchor="middle" class="ttl">Normalization</text>
<text x="327" y="79" text-anchor="middle" class="ttl">+ StringLookup</text>
<line x1="402" y1="67" x2="442" y2="67" class="ln" marker-end="url(#ar)"/>
<rect x="442" y="40" width="130" height="54" rx="5" class="bx"/>
<text x="507" y="71" text-anchor="middle">Dense ×2</text>
<line x1="572" y1="67" x2="612" y2="67" class="ln" marker-end="url(#ar)"/>
<rect x="612" y="40" width="92" height="54" rx="5" class="bxa"/>
<text x="658" y="62" text-anchor="middle">0.63</text>
<text x="658" y="80" text-anchor="middle" class="t3">P(answered)</text>
<text x="16" y="26" class="t3" fill="var(--accent-ink)">preprocessing lives INSIDE the model — it ingests a raw feature dict at inference time</text>
</svg>
<figcaption><b>FIG.06</b> — Model 1. Because normalization and one-hot encoding are Keras layers, the saved <code>.keras</code> file takes a raw feature dict in and gives a probability out — no external preprocessing to reproduce.</figcaption>
</figure>
<div class="col">
<h3>Model 2 — LSTM forecaster <span class="tag">// predict the next number in a sequence</span></h3>
<p>An <strong>LSTM</strong> (Long Short-Term Memory) is a layer with an internal <strong>memory cell</strong> and "gates" that decide what to remember, forget, and output as it reads a sequence step by step. That memory is what lets it learn trend and seasonality. We fed it via a <strong>sliding window</strong> — a trick to manufacture supervised examples from one long series — then <strong>forecast recursively</strong> by feeding each prediction back in as the next input.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 210" role="img" aria-label="LSTM sliding window over a series, predicting the next point, then forecasting recursively">
<text x="16" y="26" class="t3">SLIDING WINDOW — manufacture (input → answer) pairs from one series</text>
<g>
<rect x="16" y="40" width="34" height="30" class="bxa"/><rect x="52" y="40" width="34" height="30" class="bxa"/>
<rect x="88" y="40" width="34" height="30" class="bxa"/><rect x="124" y="40" width="34" height="30" class="bxa"/>
<rect x="160" y="40" width="34" height="30" class="bx"/>
<rect x="196" y="40" width="34" height="30" class="bx"/><rect x="232" y="40" width="34" height="30" class="bx"/>
</g>
<text x="105" y="88" text-anchor="middle" class="t3" fill="var(--accent-ink)">window (12 months)</text>
<text x="177" y="88" text-anchor="middle" class="t3">→ predict</text>
<line x1="266" y1="55" x2="330" y2="55" class="ln" marker-end="url(#ar)"/>
<rect x="330" y="34" width="150" height="42" rx="6" class="bxa"/>
<text x="405" y="52" text-anchor="middle" class="ttl">LSTM(32)</text>
<text x="405" y="68" text-anchor="middle" class="t3">carries memory →</text>
<line x1="480" y1="55" x2="540" y2="55" class="ln" marker-end="url(#ar)"/>
<rect x="540" y="34" width="80" height="42" rx="6" class="bx"/>
<text x="580" y="60" text-anchor="middle">ŷ₁₃</text>
<text x="16" y="130" class="t3">RECURSIVE FORECAST — feed predictions back to see the future</text>
<path d="M580,76 C580,110 405,104 405,90" class="sg" stroke-dasharray="5 4" marker-end="url(#ar)"/>
<text x="360" y="128" text-anchor="middle" class="t3" fill="var(--good)">prediction becomes next input</text>
<g transform="translate(0,150)">
<rect x="470" y="6" width="24" height="26" class="bx"/><rect x="496" y="2" width="24" height="30" class="bx"/>
<rect x="522" y="8" width="24" height="24" class="bx"/><rect x="548" y="0" width="24" height="32" class="fg"/>
<rect x="574" y="6" width="24" height="26" class="fg"/><rect x="600" y="12" width="24" height="20" class="fg"/>
<text x="500" y="52" class="t3">actual</text><text x="590" y="52" class="t3" fill="var(--good)">forecast</text>
</g>
</svg>
<figcaption><b>FIG.07</b> — Model 2. Result: test RMSE 42.7 vs a naive last-value baseline of 52.7 — <span class="accent-t">19% better</span> — and a correctly seasonal 12-month forecast.</figcaption>
</figure>
</section>
<!-- ============ S5 ============ -->
<section id="s5">
<div class="sechead"><span class="sec">§5</span><h2>Doing it honestly — the part that's pure engineering discipline</h2></div>
<div class="col">
<p>Every "ML gotcha" below has an exact analogue in ordinary engineering. This section is what separates a real model from a misleading demo — and it's your strongest interview material, because you can reason about it.</p>
<h3>Overfitting <span class="tag">// memorizing instead of generalizing</span></h3>
<p>An early version scored <strong>0.97 on training data but far lower on unseen data</strong> — it had <em>memorized</em> the training rows like a lookup table rather than learning a rule, so it failed on anything new. This is the identical bug to <strong>hardcoding answers to your exact test cases</strong>: the suite passes, the code is broken.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 210" role="img" aria-label="Overfitting: training loss keeps dropping while validation loss turns back up">
<line x1="60" y1="24" x2="60" y2="176" class="ln2"/>
<line x1="60" y1="176" x2="680" y2="176" class="ln2"/>
<text x="46" y="34" text-anchor="end" class="t3">loss</text>
<text x="676" y="196" text-anchor="end" class="t3">epochs →</text>
<path d="M60,40 C200,120 420,158 670,168" class="sg"/>
<path d="M60,52 C200,120 360,150 430,150 C520,150 600,120 670,70" class="sb"/>
<line x1="430" y1="30" x2="430" y2="176" class="ln2" stroke-dasharray="4 4"/>
<text x="430" y="22" text-anchor="middle" class="t3" fill="var(--accent-ink)">early stop here</text>
<text x="660" y="182" text-anchor="end" class="t3" fill="var(--good)">training loss ↓</text>
<text x="640" y="60" text-anchor="end" class="t3" fill="var(--bad)">validation loss ↑ (memorizing)</text>
</svg>
<figcaption><b>FIG.08</b> — The tell-tale divergence. Defenses we used: <b>dropout</b> (randomly disable neurons so no single path is memorized), <b>L2 regularization</b> (penalize large weights → simpler function), <b>early stopping</b> (halt at the dashed line), and small models.</figcaption>
</figure>
<div class="col">
<h3>Train / validation / test split <span class="tag">// never grade on data you trained on</span></h3>
<p>We hold out data the model never sees in training and score only on that — an <strong>integration test on fresh inputs</strong>. Validation = the slice used to decide when to stop; test = the final, untouched exam.</p>
<h3>Data leakage <span class="tag">// accidentally seeing the answer key</span></h3>
<p>We fit the number-scaler and category-encoders on the <strong>training portion only</strong>, and for the time series we never let a future month touch the past. Compute those stats over the whole dataset and information about the test set "leaks" into training, inflating your scores dishonestly — like <strong>benchmarking with the answer sheet open</strong>. And a feature you won't <em>have</em> at prediction time (a call's duration, before it's placed) can't be a feature at all.</p>
<h3>Class imbalance <span class="tag">// the "always return false" trap</span></h3>
<p>When one outcome is rare — say only 30% of examples are positive — a model that <em>always predicts the majority</em> scores 70% accuracy and is <strong>completely useless</strong>. The fix is <strong>class weights</strong> — make errors on the rare class cost more — so the optimizer can't ignore the minority. It's also why we don't report raw accuracy.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 176" role="img" aria-label="Train validation test split, and five-fold cross validation rotating the test fold">
<text x="16" y="24" class="t3">SPLIT — one held-out exam</text>
<rect x="16" y="34" width="430" height="30" class="bxa"/>
<rect x="446" y="34" width="120" height="30" class="bx"/>
<rect x="566" y="34" width="138" height="30" fill="var(--warn)" stroke="var(--warn)"/>
<text x="231" y="54" text-anchor="middle" class="tw" fill="var(--ink)">train (fit the knobs)</text>
<text x="506" y="54" text-anchor="middle" class="t3">validation</text>
<text x="635" y="54" text-anchor="middle" class="tw" fill="#fff">test</text>
<text x="16" y="98" class="t3">5-FOLD CROSS-VALIDATION — rotate the test fold, average the score (stable on small data)</text>
<g font-family="var(--mono)">
<!-- 5 rows -->
<g transform="translate(16,108)">
<rect x="0" y="0" width="132" height="12" class="fw"/><rect x="138" y="0" width="132" height="12" class="bx"/><rect x="276" y="0" width="132" height="12" class="bx"/><rect x="414" y="0" width="132" height="12" class="bx"/><rect x="552" y="0" width="136" height="12" class="bx"/>
</g>
<g transform="translate(16,124)">
<rect x="0" y="0" width="132" height="12" class="bx"/><rect x="138" y="0" width="132" height="12" class="fw"/><rect x="276" y="0" width="132" height="12" class="bx"/><rect x="414" y="0" width="132" height="12" class="bx"/><rect x="552" y="0" width="136" height="12" class="bx"/>
</g>
<g transform="translate(16,140)">
<rect x="0" y="0" width="132" height="12" class="bx"/><rect x="138" y="0" width="132" height="12" class="bx"/><rect x="276" y="0" width="132" height="12" class="fw"/><rect x="414" y="0" width="132" height="12" class="bx"/><rect x="552" y="0" width="136" height="12" class="bx"/>
</g>
<g transform="translate(16,156)">
<rect x="0" y="0" width="132" height="12" class="bx"/><rect x="138" y="0" width="132" height="12" class="bx"/><rect x="276" y="0" width="132" height="12" class="bx"/><rect x="414" y="0" width="132" height="12" class="fw"/><rect x="552" y="0" width="136" height="12" class="bx"/>
</g>
</g>
</svg>
<figcaption><b>FIG.09</b> — With only ~559 rows a single split is noisy, so we rotate the held-out fold five times and average — hence results reported as <code>0.60 ± 0.06</code>, a mean with an error bar rather than one lucky run.</figcaption>
</figure>
<div class="col">
<h3>The metrics — and why not "accuracy"</h3>
</div>
<div class="metricrow">
<div class="metric">
<div class="v">ROC‑AUC</div>
<div class="l">ranking quality</div>
<div class="s">Pick a random answered & missed call: how often does the model rank the answered one higher? 0.5 = coin flip, 1.0 = perfect. Ours ≈ 0.60.</div>
</div>
<div class="metric">
<div class="v">AP</div>
<div class="l">avg precision</div>
<div class="s">Precision-recall summarized; judged against the 0.41 base rate, so beating 0.41 means real signal.</div>
</div>
<div class="metric">
<div class="v">RMSE</div>
<div class="l">forecast error</div>
<div class="s">Average error magnitude in real units. Only meaningful vs a baseline — we beat naive by 19%.</div>
</div>
</div>
<div class="col">
<div class="analogy">
<span class="k">The honest summary</span>
<p>Our connect-rate numbers are <b>modest because the dial-time signal is genuinely thin and the sample small</b> — time-of-day and which rep dials carry most of it. We reported that via cross-validation and baseline comparisons instead of cherry-picking one split. <b>Understanding <i>why</i> a model is mediocre is worth more than a pretty number you can't explain.</b></p>
</div>
</div>
</section>
<!-- ============ S6 ============ -->
<section id="s6">
<div class="sechead"><span class="sec">§6</span><h2>The engine & the API — TensorFlow vs Keras</h2></div>
<div class="col">
<p>Two layers, one install. <strong>TensorFlow</strong> is the engine: it represents your model as a computation graph, runs the tensor math fast on CPU/GPU, and — the killer feature — does <strong>automatic differentiation</strong> so the training loop's gradients come for free. <strong>Keras</strong> is the high-level API you actually write in: <code>Layers</code>, a <code>Model</code>, and <code>compile / fit / predict</code>. You describe <em>what</em> the model is; TF works out <em>how</em> to run and differentiate it.</p>
</div>
<figure class="plate">
<svg viewBox="0 0 720 168" role="img" aria-label="Keras API sits on top of the TensorFlow engine">
<rect x="60" y="24" width="600" height="52" rx="6" class="bxa"/>
<text x="80" y="46" class="ttl" fill="var(--accent-ink)">KERAS — what you write</text>
<text x="80" y="65" class="t3">layers.Dense / LSTM · keras.Model · model.compile() · model.fit() · .predict()</text>
<g class="ln" marker-end="url(#ar)"><line x1="360" y1="76" x2="360" y2="96"/></g>
<rect x="60" y="96" width="600" height="52" rx="6" class="bx"/>
<text x="80" y="118" class="ttl">TENSORFLOW — the engine underneath</text>
<text x="80" y="137" class="t3">computation graph · tensor ops on CPU/GPU · autodiff (gradients) · optimizers</text>
</svg>
<figcaption><b>FIG.10</b> — You drove with the steering wheel (Keras); the engine (TensorFlow) delivered the power. In modern Keras 3 the same API can run on PyTorch or JAX too — ours runs on the TF backend, hence "TF/Keras".</figcaption>
</figure>
<div class="col">
<h3>Two setup facts worth knowing</h3>
<ul>
<li><strong>Python 3.11 in a virtualenv.</strong> TensorFlow doesn't publish builds for the system's Python 3.14 yet — pure dependency-version pinning, exactly like locking a runtime.</li>
<li><strong><code>.keras</code> saved files.</strong> The trained knobs serialized to disk, so you load and call the function later without retraining — a compiled artifact.</li>
</ul>
<div class="analogy">
<span class="k">One line for the interview</span>
<p>"The model is a parameterized function fitted to data by gradient descent. <b>TensorFlow</b> handles the graph, GPU execution, and autodiff; <b>Keras</b> is the API I describe the model in. I was disciplined about train/test separation, leakage, class imbalance, overfitting, and cross-validated baselines — so the numbers are honest, and I can explain exactly why they are what they are."</p>
</div>
</div>
</section>
<footer>
<div>END OF DOC · <b>machine-learning</b> — TensorFlow/Keras models on real operational data</div>
<div style="margin-top:6px">Models: <b>call connect-rate classifier</b> (CV ROC-AUC 0.60, DNN vs gradient-boosted trees) · <b>LSTM forecaster</b> (beats naive 19%)</div>
</footer>
</div>
<script>
// theme toggle already handled by host stamping data-theme; this just keeps
// focus states crisp for keyboard users navigating the TOC.
document.querySelectorAll('nav.toc a').forEach(function(a){
a.addEventListener('click',function(){ /* native anchor jump */ });
});
</script>