Lecture5 (deep_learning_part2_2.pdf, 38 หน้า) · 2603520 Deep Learning

Vanishing/Exploding Gradients, Weight Initialization, Normalization และ Keras ในภาคปฏิบัติ

บทนี้ตอบคำถามที่ Lesson 3–4 ทิ้งไว้ว่า ทำไม backprop ที่คูณหลายเทอมต่อกันถึง "พัง" เมื่อเครือข่ายลึก และอาจารย์แก้ด้วยอะไรบ้าง (activation ที่เลือกถูก, weight initialization, batch normalization, layer normalization) ก่อนจะจบด้วยการเขียนทุกอย่างเป็นโค้ด Keras จริงทีละบรรทัดตั้งแต่โหลดข้อมูล MNIST จนถึง predict. ครึ่งหลังของบทนี้คือส่วนที่ข้อสอบ "เติมโค้ด" ดึงมาใช้มากที่สุด ดังนั้นอย่าอ่านผ่าน — ให้ปิดหน้าจอแล้วเขียนโค้ดตามให้ได้เอง.

อ่านเพิ่มก่อนเริ่ม (แนะนำ)
Glorot & Bengio (2010), Understanding the difficulty of training deep feedforward neural networks — ต้นตำรับของ Xavier Initialization; Ioffe & Szegedy (2015), Batch Normalization — ต้นตำรับของ BN ที่สไลด์หน้า 12 อ้างถึงโดยตรง.
ต้องรู้ก่อน — Mean, Variance, Standard Deviation (ใช้ตลอดทั้งบท)

ทั้งบทนี้พูดถึง "ค่าเฉลี่ย" กับ "การกระจาย" ตลอด ถ้าสองคำนี้ยังไม่แน่น อ่านย่อหน้านี้ก่อนแล้วจะอ่านบทนี้รู้เรื่องทั้งบท. สมมติเรามีตัวเลข 4 ตัว \(z = [2,\ 4,\ 6,\ 8]\) (คิดว่าเป็นค่า \(z\) ของ neuron ตัวหนึ่งจากตัวอย่าง 4 ตัวอย่าง)

variance ใหญ่ = ข้อมูลกระจายกว้าง, variance เล็ก = ข้อมูลเกาะกลุ่มใกล้ mean. ทั้ง weight initialization (§2) และ batch normalization (§3) คือการ "จัดการ variance ของ \(z\)" ทั้งคู่

ต้องรู้ก่อน — Normal Distribution และ "variance ของผลรวม"

Normal distribution \(N(\mu,\sigma^2)\) คือการแจกแจงรูประฆังคว่ำ: สุ่มเลขออกมาแล้วส่วนใหญ่ตกใกล้ \(\mu\) ยิ่งห่างยิ่งเจอน้อย. เขียน \(\theta\sim N(0,1)\) แปลว่า "สุ่ม \(\theta\) จากระฆังที่ mean = 0 variance = 1" — ค่าที่ได้ส่วนใหญ่อยู่ในช่วง \(-2\) ถึง \(+2\).

กฎที่ใช้ตลอด §2: ถ้า \(A\) กับ \(B\) เป็นตัวแปรสุ่มที่อิสระต่อกัน แล้ว \(\operatorname{Var}(A+B)=\operatorname{Var}(A)+\operatorname{Var}(B)\) — variance บวกกันตรงๆ (ไม่ใช่ \(\sigma\) บวกกัน). ตัวอย่างเล็กๆ: ทอยลูกเต๋า 1 ลูก variance ≈ 2.92; ทอย 2 ลูกแล้วบวกแต้มกัน variance ≈ 5.83 = 2 เท่า ไม่ใช่เท่าเดิม. และถ้าคูณด้วยค่าคงที่ \(c\): \(\operatorname{Var}(cA)=c^2\operatorname{Var}(A)\) (ยกกำลังสองเพราะ variance เป็นหน่วยกำลังสอง).

สองกฎนี้คือทั้งหมดที่ต้องใช้พิสูจน์สูตร He/Xavier ในหัวข้อ 2 — ไม่มีอะไรยากกว่านี้

1. Vanishing/Exploding Gradients: ผลสืบเนื่องจาก Chain Rule ที่ยาวเกินไป

เริ่มจากปัญหาที่ผู้อ่านจะเจอจริง: คุณต่อเครือข่ายให้ลึกขึ้นเพราะเชื่อว่า "ลึกกว่า = เก่งกว่า" แต่พอกดเทรน cost กลับไม่ลดเลย (หรือพุ่งเป็น NaN ภายในไม่กี่ step). สาเหตุไม่ได้อยู่ที่ข้อมูลหรือ learning rate แต่อยู่ที่วิธีที่ gradient ถูกคำนวณ — และอาจารย์เปิดเลกเชอร์นี้ด้วยเรื่องนั้นพอดี.

The backpropagation chain rule formula for a deep network, showing that computing the gradient at an early layer requires multiplying through many terms (delta, theta, g-prime, a) across all the layers in between, which is the mechanical source of the vanishing and exploding gradient problems
Slide: Vanishing and Exploding Gradients — ที่มาจากโซ่ chain rule ที่ยาว (Lecture5.pdf, p.1)

สไลด์หน้า 1 กางสูตรเดียวกับที่เราหามาแล้วใน Lesson 3 §4 ให้ดูอีกครั้ง: gradient ของ weight ตัวหนึ่งใน layer \(L-1\) เท่ากับผลคูณของอนุพันธ์ 4 ตัวที่ต่อกันเป็นโซ่

$$\frac{\partial J(\theta)}{\partial\theta_{1,1}^{(L-1)}} = \frac{\partial J}{\partial z_1^{(L)}}\cdot\frac{\partial z_1^{(L)}}{\partial a_1^{(L-1)}}\cdot\frac{\partial a_1^{(L-1)}}{\partial z_1^{(L-1)}}\cdot\frac{\partial z_1^{(L-1)}}{\partial\theta_{1,1}^{(L-1)}} = \delta^{(L)}\,\theta_{1,1}^{(L)}\,g'(z_1^{[L-1]})\,a_1^{(L-2)}$$

อ่านสูตรนี้เป็นภาษาคน: "ความผิดพลาดที่ปลายทาง \(\delta^{(L)}\) เดินทางย้อนกลับมาหา weight ตัวนี้ โดยระหว่างทางถูกคูณด้วย weight ของ layer ที่ผ่าน (\(\theta^{(L)}_{1,1}\)) และคูณด้วยความชันของ activation (\(g'\)) ของ layer นั้น"

คำว่า "ระหว่างทาง" คือหัวใจ. ถ้า weight ตัวที่เราสนใจอยู่ลึกลงไปอีก 10 ชั้น โซ่ก็ยาวขึ้นอีก 10 คู่ — ต้องคูณ \(\theta\) กับ \(g'\) เพิ่มอีกชั้นละคู่. อาจารย์สรุปว่าเพราะการหา gradient โดยเฉพาะที่ layer ต้นๆ ต้อง "incurs multiple steps of calculation" ผลลัพธ์คือ gradient โตlarger or smaller จนทำให้ training difficult — สังเกตว่าอาจารย์ทำสีทั้งคำว่า "ใหญ่ขึ้นหรือเล็กลง" และ "เทรนยาก" ไว้คู่กัน เพราะสองเคสนี้คือสองปัญหาที่มีชื่อเรียกเฉพาะ:

ย้ำจุดที่นักเรียนพลาดบ่อย: ทั้งสองปัญหาเกิดกับ layer ต้นๆ (earlier layers / lower layers) รุนแรงที่สุด เพราะโซ่จาก output มาถึง layer ต้นยาวที่สุด ส่วน layer สุดท้ายมีโซ่สั้นจึงแทบไม่โดนผลกระทบ. ในทางปฏิบัติคุณจะเห็นอาการ "layer ท้ายๆ เรียนได้ แต่ layer แรกๆ เหมือนถูกแช่แข็ง".

คำนวณ: ตัวคูณเล็กกว่า 1 หรือมากกว่า 1 นิดเดียว พอคูณซ้ำก็ต่างกันมหาศาล
ให้โซ่ chain rule ยาว \(k\) ชั้น และตัวคูณเฉลี่ยต่อชั้น (คือ \(\theta\cdot g'\)) เท่ากับ \(r\) ⇒ gradient ของ layer แรก \(\propto r^{k}\)
  1. ขั้นที่ 1: sigmoid ต่อกัน 10 layer — ตัวคูณจาก \(g'\) สูงสุดแค่ 0.25 (ดู Lesson 3 §5)
    $$0.25^{10} = 9.54\times10^{-7}$$

    ทำไมต้องสนใจ: gradient ของ layer แรกเล็กลงเกือบ ล้านเท่า เทียบกับ layer สุดท้าย ถ้า learning rate ใช้ค่าเดียวกันทั้งเครือข่าย layer แรกก็แทบไม่ขยับ

  2. ขั้นที่ 2: ลองตัวคูณ 0.9 ต่อ layer (ดูเผินๆ เหมือนเกือบ 1 ไม่น่าเป็นปัญหา)
    $$0.9^{10} = 0.349,\qquad 0.9^{50} = 0.00515$$

    ลึก 10 ชั้นหายไป 65%; ลึก 50 ชั้นหายไป 99.5% — ความลึกต่างหากที่ขยายปัญหา ไม่ใช่ตัวเลข 0.9

  3. ขั้นที่ 3: ฝั่งตรงข้าม ตัวคูณ 1.1 ต่อ layer
    $$1.1^{10} = 2.59,\qquad 1.1^{50} = 117.4$$

    gradient ใหญ่ขึ้น 117 เท่า ⇒ ก้าวเดียวกระโดดข้ามจุดต่ำสุดไปไกล นี่คือ exploding

ตัวคูณเบี่ยงจาก 1 แค่ \(\pm10\%\) แต่พอลึก 50 layer ผลลัพธ์ต่างกันหลายหมื่นเท่า — ปัญหานี้เป็น ผลคูณสะสม (product) จึงโตแบบเลขชี้กำลัง ต่างจากผลบวกที่โตแบบเชิงเส้น. เป้าหมายของทั้งบทนี้คือ "ทำให้ตัวคูณต่อ layer อยู่ใกล้ 1"

1.1 สาเหตุแรก: Activation ที่ Saturate (สไลด์หน้า 2)

Sigmoid function curve rising from 0 to 1 with flat saturated regions at both extremes, shown together with its derivative curve which peaks at 0.25 when z is 0 and falls to nearly zero beyond z of plus or minus 4
Slide: Activation Function — Sigmoid กับกราฟอนุพันธ์ที่สูงสุดเพียง 0.25 (Lecture5.pdf, p.2)

กราฟคู่บนสไลด์หน้า 2 บอกทุกอย่าง: sigmoid ไต่จาก 0 ขึ้นไป 1 และแบนราบเมื่อ \(z\) ออกนอกช่วงประมาณ \(-4\) ถึง \(+4\); ส่วนกราฟอนุพันธ์ข้างล่างมีค่าสูงสุดเพียง 0.25 ที่ \(z=0\) แล้วดิ่งลงชิด 0 ทั้งสองข้าง. ตัวเลขจริงที่เราคำนวณไว้ตั้งแต่ Lesson 3 §5: \(g'(\pm4)=0.0177\) และ \(g'(\pm8)=0.000335\). อาจารย์จึงเขียนเป็นข้อกำหนดว่า activation ใน hidden layer should not saturate เพราะการ saturate นำไปสู่ derivative extremely close to 0.

คำว่า saturate (อิ่มตัว) หมายถึงสถานะที่ input เปลี่ยนไปเท่าไรก็ไม่ทำให้ output ขยับ — เหมือนฟองน้ำที่อมน้ำเต็มแล้ว เทน้ำเพิ่มก็ไม่ซับอีก. ถ้าเราใช้ sigmoid เป็น activation แล้วผลรวมถ่วงน้ำหนักของ input (คือ \(z\)) มีค่ามากหรือน้อยเกินไป อนุพันธ์จะประมาณ 0. ผลที่ตามมาตามสไลด์: ตอนคำนวณ gradient เพื่ออัปเดต weight การเปลี่ยนแปลงจะ "infinitesimally small" จนโมเดลไม่ดีขึ้น โดยเฉพาะที่ layer ล่าง — นี่คือ vanishing gradient problem.

สังเกตว่ามีสองเหตุผลซ้อนกันที่ทำให้ sigmoid เสี่ยง ซึ่งข้อสอบชอบเอามาสลับกัน: (ก) ต่อให้ \(z\) อยู่ตรงกลางดีที่สุด ตัวคูณก็ยังได้แค่ 0.25 (น้อยกว่า 1 อยู่ดี ⇒ หดทุกชั้น) และ (ข) ถ้า \(z\) หลุดออกไปไกล ตัวคูณแทบเป็น 0 (หดทันทีในชั้นเดียว). ข้อ (ก) แก้ไม่ได้ด้วยการ normalize แต่ข้อ (ข) แก้ได้ — และนั่นคือเหตุผลทั้งหมดของ §2 กับ §3.

1.2 ทางแก้ชุดแรก: ReLU และ Gradient Clipping (สไลด์หน้า 3)

Slide Activation Function (2): ReLU graph rising linearly for positive z and flat at zero for negative z, with its derivative shown as exactly 1 for positive z and 0 for negative z; text explains ReLU is preferred in hidden layers, some nodes may die, ReLU can blow activations up and explode gradients, and gradient clipping thresholds gradient values before a gradient descent step
Slide: Activation Function (2) — ReLU, node ตาย, และ Gradient Clipping (Lecture5.pdf, p.3)

อาจารย์ประกาศตรงๆ ว่า ReLU เป็น preferred choice สำหรับ hidden layers เหตุผลอยู่ที่กราฟอนุพันธ์ทางขวาของสไลด์ซึ่งเป็นเส้นตรงคงที่ที่ระดับ 1: เมื่อผลรวมถ่วงน้ำหนักเป็นบวก differentiation is 1 พอดี จึง "not increase or decrease the gradient too much". ลองแทนในสูตรจากขั้นที่ 1 ข้างบน: ตัวคูณ \(r=1\) ⇒ \(1^{50}=1\) — gradient เดินทางผ่าน 50 ชั้นแล้วยังเท่าเดิม นี่คือเหตุผลเชิงตัวเลขว่าทำไม ReLU ทำให้เครือข่ายลึกเทรนได้ ขณะที่ sigmoid ทำไม่ได้.

แต่สไลด์ก็เตือนข้อเสียสองข้อทันที ซึ่งต้องจำคู่กันเสมอ:

คำนวณ: gradient clipping 2 แบบ กับ gradient \(g = [3,\ -7,\ 0.5]\)
สมมติโมเดลมี 3 พารามิเตอร์ และรอบนี้ backprop คืน gradient ออกมาใหญ่ผิดปกติคือ \(-7\) ในตัวที่สอง
  1. ขั้นที่ 1: Clip by value (threshold \(c=1\)) — บังคับให้ทุกสมาชิกอยู่ใน \([-1,1]\)
    $$g' = [\min(3,1),\ \max(-7,-1),\ 0.5] = [1,\ -1,\ 0.5]$$

    ทำไมทำแบบนี้: ง่ายและกันก้าวใหญ่ได้แน่นอน แต่สังเกตว่าสัดส่วนเดิม \(3:-7\) กลายเป็น \(1:-1\) ⇒ ทิศทางของก้าวเปลี่ยนไป

  2. ขั้นที่ 2: Clip by norm (threshold \(c=5\)) — วัดความยาวเวกเตอร์ก่อน
    $$\|g\| = \sqrt{3^2+(-7)^2+0.5^2} = \sqrt{58.25} = 7.63$$
  3. ขั้นที่ 3: ความยาว 7.63 > 5 จึงย่อทั้งเวกเตอร์ด้วยอัตราส่วนเดียวกัน
    $$g' = g\cdot\frac{5}{7.63} = [1.97,\ -4.59,\ 0.33]$$

    ตรวจ: \(\|g'\|=5\) พอดี และอัตราส่วนระหว่างสมาชิกยังเป็น \(3:-7:0.5\) เหมือนเดิม ⇒ ทิศทางเดิม แค่ก้าวสั้นลง

ทั้งสองแบบกันไม่ให้ก้าวใหญ่จนระเบิด: by value ง่ายแต่บิดทิศทาง, by norm รักษาทิศทาง. ใน Keras ตั้งที่ optimizer ได้เลย เช่น keras.optimizers.SGD(clipvalue=1.0) หรือ SGD(clipnorm=5.0).

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
เพราะ backprop สร้าง gradient ด้วยการคูณสะสม ไม่ใช่บวกสะสม. ตัวคูณที่เบี่ยงจาก 1 เพียงเล็กน้อยจึงถูกยกกำลังตามจำนวนชั้น ทำให้ผลลัพธ์เบี่ยงแบบเลขชี้กำลัง — ถ้าเป็นการบวก ความลึก 50 ชั้นจะเพิ่มแค่ 50 เท่า ไม่ใช่หลายหมื่นเท่า.
เปลี่ยนแล้วเกิดอะไร
เปลี่ยน activation จาก sigmoid (\(g'\le0.25\)) เป็น ReLU (\(g'=1\) ฝั่งบวก) ตัวคูณต่อชั้นเปลี่ยนจาก \(\le0.25\) เป็น 1: ลึก 10 ชั้นต่างกัน \(1/0.25^{10}\approx 1{,}048{,}576\) เท่า. แต่ก็ย้ายปัญหาไปอีกฝั่ง (ไม่มีเพดานบน ⇒ เสี่ยง explode) จึงต้องมี clipping/normalization ตามมา.
เชื่อมกับ
การคูณเลขใกล้ 1 ซ้ำหลายรอบเป็นกลไกเดียวกับ Exponentially Weighted Average ใน Lesson 4 §4 และตารางเปรียบเทียบ activation ใน Lesson 3 §5.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 1Vanishing Gradient
ทำไมเครือข่ายที่ใช้ sigmoid ทุก layer จึงเสี่ยงเกิด Vanishing Gradient มากกว่าเครือข่ายที่ใช้ ReLU?
คำถาม 2Gradient Clipping
ตามสไลด์หน้า 3 Gradient Clipping ทำงานอย่างไรและแก้ปัญหาใด?

2. Weight Initialization: ทำไม "สุ่มเฉยๆ" ถึงยังไม่พอ

ก่อนรอบเทรนรอบแรกจะเริ่ม เราต้องใส่ตัวเลขอะไรสักอย่างลงในทุก \(\theta\). คำถามคือ "ใส่อะไร?" — ฟังดูเป็นเรื่องเล็ก แต่สไลด์หน้า 4–8 ใช้ถึง 5 หน้าอธิบายว่าการเลือกตรงนี้ตัดสินว่าโมเดลจะเรียนได้หรือไม่เลย.

Slide Weight Initialization: weights are sampled from a uniform or Gaussian distribution as small random numbers; they should be random and not the same to avoid redundant nodes; empirical evidence says the choice of uniform versus Gaussian does not matter much but the scale of the initial weights does matter for both optimization progress and generalization
Slide: Weight Initialization — ข้อสรุปจากหลักฐานเชิงประจักษ์ (Lecture5.pdf, p.4)

สไลด์หน้า 4 วางกติกาไว้ 2 ข้อ. ข้อแรก: เริ่มจากการสุ่มค่าเล็กๆ (จาก uniform หรือ Gaussian ก็ได้) แต่ weight ต้อง random คือ "not the same" เพื่อเลี่ยง redundant nodes — node ที่ทำงานซ้ำซ้อนกันจนเหลือค่าเท่ากับมี node เดียว (จะพิสูจน์ด้วยตัวเลขในหัวข้อ 2.1). ข้อสอง ซึ่งเป็นใจความที่อาจารย์ทำสีไว้: จากหลักฐานเชิงประจักษ์ (from empirical evidence) การเลือก uniform หรือ Gaussian แทบไม่ต่างกัน แต่ scale of the initial weights matters — "ขนาด" ของ weight เริ่มต้นต่างหากที่มีผลทั้งต่อความคืบหน้าของการ optimize และความสามารถในการ generalize กับข้อมูลที่ไม่เคยเห็น. สไลด์ปิดท้ายว่า "It turns out that some choices are better than others in this respect!!!" — คือมีสูตรที่ดีกว่าการเดา ซึ่งจะมาในหน้า 6.

2.1 กติกาข้อแรก: weight ต้องไม่เท่ากัน — symmetry breaking

ทำไมการตั้ง weight ให้เท่ากันหมด (กรณีสุดโต่งคือตั้งเป็น 0 ทุกตัว) ถึงพัง? อย่าท่องจำ ให้ลองไล่เลขดูจริงกับเครือข่ายเล็กที่สุดที่ยังมี hidden layer.

คำนวณ: zero initialization ในเครือข่าย 2 → 2 → 1 (sigmoid ทุกชั้น)
\(x=(1,2)\), \(y=1\), และ weight/bias ทุกตัวเริ่มต้นเป็น 0
  1. ขั้นที่ 1: Forward — คำนวณ \(z\) และ \(a\) ของ hidden layer
    $$z^{[1]}_1 = 0(1)+0(2)+0 = 0,\quad z^{[1]}_2 = 0 \Rightarrow a^{[1]}_1 = a^{[1]}_2 = g(0) = 0.5$$

    เพราะ weight เป็น 0 หมด input จึงไม่มีผลใดๆ ทั้งสอง node ได้ค่าเท่ากันเป๊ะ

  2. ขั้นที่ 2: Forward ต่อถึง output และหา cost
    $$z^{[2]} = 0 \Rightarrow h = g(0) = 0.5,\qquad J = -\ln(0.5) = 0.6931$$

    ตัวเลข 0.6931 นี้จำไว้ให้ดี — เดี๋ยวจะเจอมันอีกครั้งในกราฟการทดลองหน้า 7

  3. ขั้นที่ 3: Backward — \(\delta\) ที่ output layer (สูตร \(a-y\) จาก Lesson 3 §6)
    $$\delta^{[2]} = 0.5 - 1 = -0.5$$
  4. ขั้นที่ 4: \(\delta\) ของ hidden ต้องคูณย้อนผ่าน \(\theta^{[2]}\) ซึ่งเป็น 0
    $$\delta^{[1]}_1 = \theta^{[2]}_{11}\,\delta^{[2]}\,g'(z^{[1]}_1) = 0\times(-0.5)\times0.25 = 0,\qquad \delta^{[1]}_2 = 0$$

    ⇒ gradient ของ \(\theta^{[1]}\) ทุกตัวเป็น 0 ⇒ weight ชั้นแรกไม่ขยับเลย

  5. ขั้นที่ 5: gradient ของ \(\theta^{[2]}\) ยังไม่เป็น 0 แต่เท่ากันทั้งคู่
    $$\frac{\partial J}{\partial\theta^{[2]}_{11}} = \delta^{[2]}a^{[1]}_1 = -0.25,\qquad \frac{\partial J}{\partial\theta^{[2]}_{12}} = \delta^{[2]}a^{[1]}_2 = -0.25$$

    หลังอัปเดต \(\theta^{[2]}_{11}=\theta^{[2]}_{12}\) (ไม่เป็น 0 แล้ว แต่ยังเท่ากัน)

  6. ขั้นที่ 6: รอบถัดไป hidden ทั้งสองยังได้ \(\delta\) เท่ากันอีก แล้ววนซ้ำแบบนี้ตลอด

    ความสมมาตรไม่มีอะไรมาทำลาย จึงคงอยู่ตลอดการเทรน

hidden 2 node ทำงานเหมือนตัวเดียวกันตลอดกาล — ต่อให้ใส่ hidden 100 node ก็ได้กำลังการเรียนรู้เท่ากับ 1 node (นี่แหละคำว่า redundant nodes บนสไลด์หน้า 4). การสุ่มให้ \(\theta\) ต่างกันเพื่อทำลายความสมมาตรนี้เรียกว่า symmetry breaking ซึ่งเป็นคำที่อาจารย์ใช้อีกครั้งในหน้า 8.

2.2 กติกาข้อสอง: "ขนาด" ของ weight — โจทย์ที่สไลด์หน้า 5 เว้นช่องว่างไว้

Slide The Problem of Random Initialization: 100 inputs x1 through x100 all equal to 1 feed a layer whose weights are generated from a normal distribution with mean 0 and variance 1; the slide asks the student to fill in blank boxes for the mean and variance of z1[1] = theta10 + theta11*x1 + ... + theta1100*x100, and states the problem that variance of inputs grows with the number of inputs and the solution var = 1/n where n is the size of the previous layer
Slide: The Problem of Random Initialization — โจทย์เติมคำตอบ mean และ variance (Lecture5.pdf, p.5)

สไลด์หน้า 5 ตั้งโจทย์ให้เราเติมเอง และนี่เป็นโจทย์ที่ออกสอบง่ายมากเพราะมีตัวเลขชัด: เครือข่ายมี input 100 ตัว \(x_1,\dots,x_{100}\) และกำหนดให้ทุกตัวมีค่าเท่ากับ 1; weight ถูกสร้างจาก normal distribution ที่ mean = 0, variance = 1; แล้วถามว่า \(z_1^{[1]} = \theta_{10}^{[1]} + \theta_{11}^{[1]}x_1 + \dots + \theta_{1\,100}^{[1]}x_{100}\) มี mean และ variance เท่าไร (สองช่องบนสไลด์ถูกเว้นว่างไว้). อาจารย์ยังหมายเหตุไว้ด้วยว่า "it is fine to set the weight of bias unit = 0" ⇒ เราตั้ง \(\theta_{10}=0\) ได้เลย.

คำนวณ: เติมช่องว่างของสไลด์หน้า 5 — mean และ variance ของ \(z_1^{[1]}\)
\(x_1=\dots=x_{100}=1\); \(\theta_i\sim N(0,1)\) เป็นอิสระต่อกัน; bias \(\theta_{10}=0\)
  1. ขั้นที่ 1: mean — ใช้กฎ "ค่าคาดหวังของผลรวม = ผลรวมของค่าคาดหวัง"
    $$E[z] = \sum_{i=1}^{100} E[\theta_i]\,x_i = 100\times 0\times 1 = 0$$

    ทำไม 0: เพราะเราสุ่ม weight จากระฆังที่สมมาตรรอบ 0 ค่าบวกกับค่าลบหักล้างกันพอดี

  2. ขั้นที่ 2: variance — ใช้กฎ "variance ของผลรวมตัวแปรอิสระ = ผลรวม variance" (ดูกล่องต้องรู้ก่อนด้านบน)
    $$\operatorname{Var}(z) = \sum_{i=1}^{100}\operatorname{Var}(\theta_i x_i) = \sum_{i=1}^{100} x_i^2\operatorname{Var}(\theta_i) = 100\times 1^2\times 1 = 100$$

    ทำไมไม่หักล้างเหมือน mean: เพราะ variance เป็นระยะห่างยกกำลังสอง จึงเป็นบวกเสมอ มีแต่บวกสะสมขึ้นเรื่อยๆ

  3. ขั้นที่ 3: แปลงเป็นส่วนเบี่ยงเบนมาตรฐานเพื่อให้เห็นภาพ
    $$\sigma_z = \sqrt{100} = 10$$
  4. ขั้นที่ 4: ประเมินผลกระทบ — ค่า \(z\) ที่กระจาย \(\pm10\) ตกตรงไหนของ sigmoid?
    $$g'(10) = 4.54\times10^{-5}$$

    แทบเป็นศูนย์ ⇒ ตัวอย่างส่วนใหญ่ทำให้ node นี้ saturate ตั้งแต่ layer แรกก่อนเทรนด้วยซ้ำ

คำตอบของสไลด์: mean = 0, variance = 100. นี่คือสิ่งที่อาจารย์เรียกว่า Problem — "Variance of inputs grows with the number of inputs" (variance ของ \(z\) โตตามจำนวน input ที่ต่อเข้ามา). และ Solution คือ "weights should be chosen randomly but in such a way that the sigmoid is primarily activated in its linear region" คือสุ่มแบบที่ทำให้ sigmoid ทำงานอยู่ในช่วงเชิงเส้นตรงกลาง (หลีกเลี่ยงการลด/ขยายขนาดของ \(z\)) โดยใช้ \(var = \frac{1}{n}\) เมื่อ \(n\) = จำนวน neuron ของ layer ก่อนหน้า.

ลองตรวจว่าสูตร \(var=1/n\) แก้ปัญหาจริงไหม: ถ้า \(\operatorname{Var}(\theta)=1/100=0.01\) แทนที่จะเป็น 1 จะได้ \(\operatorname{Var}(z)=100\times0.01\times1=1\) ⇒ \(\sigma_z=1\) ⇒ ค่า \(z\) ส่วนใหญ่อยู่ในช่วง \(-2\) ถึง \(+2\) ซึ่งเป็นช่วงที่ sigmoid ยังชันอยู่. สังเกตความสวยงาม: \(n\times\frac{1}{n}=1\) เสมอ ไม่ว่าชั้นนั้นจะกว้าง 10 หรือ 10,000 — สูตรนี้จึงทำให้ variance คงที่ได้ทุกความกว้าง.

2.3 สามสูตรมาตรฐาน: LeCun, Xavier/Glorot, He (สไลด์หน้า 6)

Slide Weight initialization Techniques listing LeCun Initialization 1998 with var = 1/fan_in recommended for SELU, Glorot or Xavier Initialization 2010 with var = 2/(fan_in+fan_out) and a uniform limit formula, said to take into account both forward and backward propagation and to work better for sigmoid tanh softmax, and He or Kaiming Initialization 2015 with var = 2/fan_in recommended for ReLU; a small diagram defines fan_in = 2 and fan_out = 1
Slide: Weight initialization Techniques — สูตร variance ของแต่ละวิธี (Lecture5.pdf, p.6)

ก่อนอื่นต้องเข้าใจสองคำที่สไลด์นิยามไว้มุมขวาบนด้วยภาพ node 3 ตัว: fan_in = จำนวน neuron ที่ส่งเข้ามา (ในภาพเล็กนั้น = 2) และ fan_out = จำนวน neuron ที่รับออกไป (= 1). สำหรับ layer ของ MNIST ที่ต่อ 784 → 16 จะได้ fan_in = 784 และ fan_out = 16. ทั้งสามวิธีต่างกันแค่ "หารด้วยอะไร" เท่านั้น:

วิธี (ปี)\(\operatorname{Var}(\theta)\)สไลด์แนะนำให้ใช้กับเหตุผลที่สไลด์ให้
LeCun (1998)\(\dfrac{1}{\text{fan\_in}}\)SELUดูจากเครือข่าย feedforward ธรรมดา คุมเฉพาะทิศ forward
Glorot / Xavier (2010)\(\dfrac{2}{\text{fan\_in}+\text{fan\_out}}\)sigmoid / tanh / softmaxคิดทั้ง forward และ backward propagation ของสัญญาณ
He / Kaiming (2015)\(\dfrac{2}{\text{fan\_in}}\)ReLU และตระกูลของมันReLU มีจุดที่หาอนุพันธ์ไม่ได้ (ที่ \(z=0\)) และตัดฝั่งลบทิ้ง

ทั้งสามวิธี "draws samples from a truncated normal distribution centered on 0" — สุ่มจากระฆังที่ mean 0 แบบ truncated คือตัดหางที่ไกลเกิน 2 เท่าของส่วนเบี่ยงเบนมาตรฐานทิ้ง เพื่อไม่ให้มี weight ตัวไหนใหญ่ผิดปกติหลุดมา. เฉพาะ Glorot สไลด์ให้ทางเลือกแบบ uniform ด้วย คือสุ่มเท่าๆ กันในช่วง \([-limit, +limit]\) โดยกล่องสูตรบนสไลด์เขียนว่า \(limit = \frac{6}{\text{fan\_in}+\text{fan\_out}}\).

จุดที่สไลด์น่าจะพิมพ์ตกราก (อ่านให้ปลอดภัยตอนสอบ)

ในเปเปอร์ต้นฉบับของ Glorot สูตรของแบบ uniform คือ \(limit=\sqrt{\dfrac{6}{\text{fan\_in}+\text{fan\_out}}}\) (มีรากที่สอง) และคำว่า limit บนสไลด์ก็ถูกตัดเหลือ "limi" ด้วย จึงน่าจะเป็นความคลาดเคลื่อนตอนทำสไลด์. ตรวจได้เองว่าต้องมีราก: การแจกแจงแบบ uniform บน \([-l,l]\) มี \(\operatorname{Var}=l^2/3\) ถ้าอยากให้ variance เท่ากับ \(\frac{2}{\text{fan\_in}+\text{fan\_out}}\) ก็ต้องได้ \(l^2/3=\frac{2}{n_{in}+n_{out}}\) ⇒ \(l=\sqrt{\frac{6}{n_{in}+n_{out}}}\) พอดี. ตอนสอบ: ถ้าโจทย์ให้เลือกสูตร ให้ตอบตามที่เขียนบนสไลด์ แต่ถ้าโจทย์ให้คำนวณตัวเลข ให้ใส่รากที่สอง (เช่น layer 784→16: \(\sqrt{6/800}=0.0866\) ไม่ใช่ \(6/800=0.0075\)).

พิสูจน์ที่มา: ทำไม He ถึงใช้ \(\operatorname{Var}(\theta) = 2/\text{fan\_in}\) สำหรับ ReLU
layer หนึ่งมี \(n=\)fan_in ตัว, input \(a_i\) อิสระกัน, weight \(\theta_i\) มี mean 0 และ variance \(\sigma_\theta^2\) — เป้าหมายคือให้ variance ของ \(z\) เท่ากันทุกชั้น
  1. ขั้นที่ 1: variance ของ \(z\) ในชั้นถัดไป (ใช้กฎผลรวมเหมือนหน้า 5)
    $$\operatorname{Var}(z') = n\,\sigma_\theta^2\,E[a^2]$$

    \(E[a^2]\) คือ "ขนาดกำลังสองเฉลี่ยของ activation ที่ป้อนเข้ามา"

  2. ขั้นที่ 2: ใส่ผลของ ReLU — มันตัดค่าลบครึ่งหนึ่งทิ้งเป็น 0
    $$E[a^2] = \tfrac12\operatorname{Var}(z)\ \Rightarrow\ \operatorname{Var}(z') = n\,\sigma_\theta^2\cdot\tfrac12\operatorname{Var}(z)$$

    ทำไมครึ่ง: \(z\) สมมาตรรอบ 0 ⇒ ครึ่งหนึ่งเป็นลบและถูก ReLU ทำให้เป็น 0 พลังงานจึงเหลือครึ่งเดียว

  3. ขั้นที่ 3: บังคับให้ variance คงที่ คือ \(\operatorname{Var}(z')=\operatorname{Var}(z)\)
    $$n\,\sigma_\theta^2\cdot\tfrac12 = 1\ \Rightarrow\ \sigma_\theta^2 = \frac{2}{n} = \frac{2}{\text{fan\_in}}$$

เลข 2 ในตัวเศษของ He คือค่าชดเชยที่ ReLU ตัดครึ่งลบทิ้งพอดี. ถ้าใช้ LeCun \(1/n\) กับ ReLU ตัวคูณต่อชั้นจะเหลือ \(\tfrac12\) ⇒ variance หดครึ่งทุกชั้น (vanishing). ส่วน Xavier ใช้ค่าเฉลี่ยแบบฮาร์มอนิกของเงื่อนไข forward (\(1/\text{fan\_in}\)) กับ backward (\(1/\text{fan\_out}\)) จึงออกมาเป็น \(2/(\text{fan\_in}+\text{fan\_out})\).

คำนวณ: จำลองจริง — variance ของ \(z\) เมื่อผ่าน 10 layer ReLU ที่กว้าง 100 node
จำลองด้วย numpy 4,000 ตัวอย่าง เฉลี่ยจาก 20 seed (ตัวเลขด้านล่างคือค่าที่รันได้จริง)
  1. ขั้นที่ 1: weight \(N(0,1)\) ธรรมดา — ตัวคูณต่อชั้น \(= n\sigma_\theta^2/2 = 100/2 = 50\)
    $$\operatorname{Var}(z):\ 99.6\ \to\ 4{,}890\ \to\ 2.47\times10^{5}\ \to\ \dots\ \to\ 1.8\times10^{17}$$

    โตขึ้น 50 เท่าทุกชั้น ⇒ exploding แบบชัดเจน

  2. ขั้นที่ 2: He — \(\sigma_\theta^2 = 2/100 = 0.02\) (\(\sigma_\theta=0.141\)) ⇒ ตัวคูณ \(= 100(0.02)/2 = 1\)
    $$\operatorname{Var}(z):\ 1.99\ \to\ 1.96\ \to\ 1.97\ \to\ \dots\ \to\ 1.87$$

    คงที่ราว 2 ตลอด 10 ชั้น ตรงตามที่พิสูจน์ไว้ (แกว่งเล็กน้อยเพราะเป็นการสุ่ม)

  3. ขั้นที่ 3: LeCun \(1/n\) จับคู่กับ ReLU (ผิดคู่) ⇒ ตัวคูณ \(=\tfrac12\)
    $$\operatorname{Var}(z):\ 0.996\ \to\ 0.489\ \to\ 0.247\ \to\ \dots\ \to\ 0.0018$$

    หายไป 99.8% ใน 10 ชั้น ⇒ vanishing

สามแถวนี้คือเหตุผลทั้งหมดว่าทำไมต้อง "จับคู่ initializer กับ activation ให้ถูก". ใน Keras เขียนที่อาร์กิวเมนต์ kernel_initializer ของ Dense: 'he_normal' (He แบบ normal), 'he_uniform', 'glorot_uniform' (ค่า default ของ Dense), 'lecun_normal'.

2.4 หลักฐานจากการทดลองบน Circle Dataset (สไลด์หน้า 7)

Experiments on the Circle Dataset comparing three initializations: Zero Initialization whose cost is flat at about 0.69306 with no decision boundary drawn, Random Initialization whose cost hovers near 0.70 for the first iterations then drops to about 0.30 with Training Set Accuracy 84.142857 and a rough boundary, and He Initialization whose cost falls from about 1.05 to about 0.28 within the first ten iteration blocks with a clean circular boundary
Slide: Experiments — Zero vs Random vs He Initialization บน Circle Dataset (Lecture5.pdf, p.7)

อ่านตัวเลขบนกราฟทั้งสามช่องแล้วเทียบกับที่เราคำนวณมา. ช่อง Zero Initialization: cost ค้างนิ่งที่ประมาณ 0.69306 ตลอด 100 บล็อกการวนซ้ำ และภาพล่างไม่มีเส้นแบ่งกลุ่มเกิดขึ้นเลย — ตรงกับที่เราคำนวณในหัวข้อ 2.1 ว่า \(-\ln 0.5 = 0.6931\) ซึ่งคือ cost ของโมเดลที่ทำนาย 0.5 ให้ทุกตัวอย่าง (เดาสุ่ม 50/50). สังเกตแกน y ของช่องนั้นด้วย: มันไล่จาก 0.69306 ถึง 0.69313 เท่านั้น แปลว่าความเปลี่ยนแปลงทั้งหมดที่เกิดขึ้นมีค่าน้อยกว่า 0.0001 — โมเดลไม่ได้เรียนอะไรเลยจริงๆ.

ช่อง Random Initialization: cost เริ่มที่ราว 0.70 แล้วแกว่งอยู่ตรงนั้นไปสิบกว่าบล็อกก่อนจะดิ่งลงมาที่ราว 0.30 และจบด้วย Training Set Accuracy: 84.142857 พร้อมเส้นแบ่งที่พอใช้ได้. ส่วนช่อง He Initialization: cost เริ่มสูงกว่าเพื่อน (ราว 1.05) แต่ดิ่งลงถึง ~0.30 ภายในไม่กี่บล็อกแรก แล้วนิ่งอยู่แถว 0.28. บทเรียนคือ ช่วงออกตัว: สุ่มธรรมดาต้องเสียเวลาหลายรอบกว่าจะหลุดจากบริเวณที่ gradient เล็ก ส่วน He ออกตัวได้ทันทีเพราะ variance ของ \(z\) ถูกคุมให้พอดีตั้งแต่ก้าวแรก. ทั้งสามช่องใช้ข้อมูลชุดเดียวกัน (Circle Dataset) และโครงสร้างเดียวกัน — ต่างกันแค่ค่าเริ่มต้นของ \(\theta\) เท่านั้น.

2.5 ข้อจำกัดที่ต้องพูดให้ได้ (สไลด์หน้า 8)

Slide Issues about Weight Initialization: initialization methods can break the symmetry and address vanishing and exploding gradient problems; use He initialization for ReLU and Leaky ReLU and Xavier initialization for tanh and sigmoid; tanh/sigmoid vanishing gradients solved with Xavier; ReLU/Leaky ReLU exploding gradients solved with He; however weight initialization can not totally eliminate the vanishing or exploding gradient problems because it only controls the variance of the outputs of each layer during the first iteration of gradient descent and the weights will change in the next iterations
Slide: Issues about Weight Initialization — สรุปคู่ที่ถูกต้องและข้อจำกัด (Lecture5.pdf, p.8)

หน้านี้คือหน้าที่ข้อสอบดึงไปทำตัวเลือกได้ง่ายที่สุด เพราะมีทั้ง "คู่ที่ถูก" และ "ข้อจำกัด". ประโยคแรก: วิธี initialization ช่วย break the symmetry (ทำลายความสมมาตรตามที่เราพิสูจน์ในหัวข้อ 2.1) และจัดการปัญหา vanishing/exploding ได้. จากนั้นให้คู่จับที่ต้องท่องให้ขึ้นใจ: ใช้ He initialization สำหรับ ReLU และ Leaky ReLU, ใช้ Xavier initialization สำหรับ tanh/sigmoid. อาจารย์ขยายความต่อเป็นสองบรรทัดย่อยที่ระบุปัญหาเฉพาะของแต่ละคู่ด้วย: "Tanh/Sigmoid vanishing gradients can be solved with Xavier" และ "ReLU/Leaky ReLU exploding gradients can be solved with He" — เห็นความสมเหตุสมผลไหม: sigmoid มีปัญหาฝั่งหด ส่วน ReLU มีปัญหาฝั่งระเบิด ตรงกับที่วิเคราะห์ไว้ใน §1.

แล้วปิดท้ายด้วยข้อจำกัดที่ต้องพูดได้: weight initialization not totally eliminate ปัญหา vanishing or exploding gradient problems ได้. เหตุผลอยู่ที่สองบรรทัดย่อย — มันควบคุมได้แค่ "variance of the outputs of each layer" และเฉพาะ "during the first iteration of gradient descent" เท่านั้น; พอ "the weights will change in the next iterations" ค่าเหล่านั้นก็เล็กหรือใหญ่เกินได้อีกในภายหลัง. คิดง่ายๆ ว่า initialization คือการตั้งต้นรถให้ตรงเลนตอนออกตัว ไม่ใช่ระบบพวงมาลัยที่คอยแก้เลนให้ตลอดทาง — ระบบที่คอยแก้ตลอดทางคือ Batch Normalization ในหัวข้อ 3.

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
ทำไมต้องคุม variance ไม่ใช่ mean? เพราะ mean เป็น 0 อยู่แล้วจากการสุ่มแบบสมมาตร (บวก-ลบหักล้างกัน) แต่ variance ของทุกเทอมที่บวกกันเป็นบวกเสมอจึงสะสมตามจำนวน input — ปล่อยไว้แล้ว \(z\) จะกระจายกว้างขึ้นเรื่อยๆ ทุกชั้นที่ลึกลงไป.
เปลี่ยนแล้วเกิดอะไร
สลับสูตร He ไปใช้กับ sigmoid: \(\operatorname{Var}(z)=n\cdot\frac{2}{n}=2\) ⇒ \(\sigma_z=1.41\) ซึ่งกว้างเกินช่วงเชิงเส้นของ sigmoid ไปเล็กน้อย (Xavier ให้ \(\operatorname{Var}\) ต่ำกว่า จึงพอดีกว่า); กลับกัน ใช้ Xavier กับ ReLU ที่ fan_in \(\gg\) fan_out เช่น 784→16 จะได้ \(\operatorname{Var}(\theta)=2/800=0.0025\) ⇒ ตัวคูณต่อชั้น \(=784(0.0025)/2=0.98\) ยังพอใช้ได้ แต่ถ้าเป็น 16→16 จะได้ \(2/32=0.0625\) ⇒ ตัวคูณ \(=16(0.0625)/2=0.5\) ⇒ หดครึ่งต่อชั้น.
เชื่อมกับ
ตอบคำถามของ §1 โดยตรง: คุม variance ให้คงที่ทุกชั้น ⇒ \(z\) ไม่หลุดเข้าเขต saturation ⇒ ตัวคูณ \(g'\) ไม่หด; และเชื่อมกับการทดลองใน Lesson 6 §7 ที่ initializer แบบสุ่มผิดช่วงทำให้เทรนไม่ขยับ.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 3Weight Init Variance
ตามโจทย์สไลด์หน้า 5: input 100 ตัวมีค่าเท่ากับ 1 ทุกตัว weight สุ่มจาก N(0,1) อิสระกัน variance ของ z₁⁽¹⁾ เท่ากับเท่าไร?
คำถาม 4He vs Xavier
ตามสไลด์หน้า 8 ควรจับคู่ initializer กับ activation อย่างไร และเพราะอะไร?
คำถาม 5ข้อจำกัดของ Initialization
ข้อความใดตรงกับสิ่งที่สไลด์หน้า 8 บอกเกี่ยวกับข้อจำกัดของ weight initialization?

3. Batch Normalization: จัดระเบียบ z ใหม่ทุก Layer ทุก Mini-batch

หัวข้อ 2 จบด้วยข้อจำกัดว่า initialization ช่วยได้แค่รอบแรก. คำถามต่อไปจึงเป็นคำถามที่อาจารย์ตั้งไว้เองว่า "ถ้าอย่างนั้น เราคุม distribution ของ \(z\) ให้ดีทุกรอบเลยได้ไหม?" — คำตอบคือ Batch Normalization. แต่ก่อนจะถึงสูตร สไลด์หน้า 9 ย้อนกลับไปถามคำถามที่พื้นฐานกว่านั้นก่อน: ทำไมการ normalize ถึงช่วยการ optimize ตั้งแต่แรก.

3.0 ทำไม Normalization ช่วยให้ Optimize ง่ายขึ้น (สไลด์หน้า 9)

Slide Why Normalization helps Optimization: for logistic regression, unnormalized data spanning 0 to 100 on one axis and about minus 40 to 40 on the other gives elongated elliptical cost contours; shifting toward zero mean and then rescaling to a similar spread turns the contours circular; for a deep network with layers 1 to L, the caption says one should normalize z (pre-activation) or a (post-activation); the z-score formula X_new = (X - mean)/StdDev is shown with the definitions of mean and standard deviation
Slide: Why Normalization helps Optimization — จาก logistic regression ถึง deep network (Lecture5.pdf, p.9)

ตัวอย่างที่อาจารย์ยกคือ logistic regression ที่มี 2 feature: feature หนึ่งมีค่าตั้งแต่ 0 ถึงประมาณ 100 อีก feature หนึ่งอยู่ราว \(-40\) ถึง \(+40\). ผลคือ contour ของ cost (เส้นระดับความสูงของ \(J\)) ยืดเป็นวงรีแคบยาว และ gradient descent ต้องเดินซิกแซ็กไปมาข้ามหุบเขาแคบนั้นแทนที่จะเดินตรงลงก้นหุบ. เมื่อทำสองขั้นที่สไลด์กำกับไว้ — shift toward zero mean (ลบค่าเฉลี่ยออก) แล้ว rescale to similar spread (หารด้วยส่วนเบี่ยงเบนมาตรฐาน) — contour กลายเป็นวงกลม gradient ชี้ตรงเข้าหาจุดต่ำสุดตั้งแต่ก้าวแรก. สูตรที่สไลด์กำกับคือ z-score เดียวกับ feature scaling ใน Lesson 1 §4:

$$X_{new} = \frac{X-\mu}{\sigma},\qquad \mu = \frac{1}{n}\sum_{i=1}^{n}x_i,\qquad \sigma = \sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_i-\mu)^2}$$

อ่านว่า "เอาทุกค่ามาลบจุดกึ่งกลาง แล้วหารด้วยความกว้างของการกระจาย" — ผลคือชุดตัวเลขใหม่ที่ mean = 0 และ variance = 1 เสมอ ไม่ว่าข้อมูลเดิมจะมีหน่วยอะไร

แล้วอาจารย์ก็ขยายความคิดนี้ไปยัง deep neural network ครึ่งล่างของหน้า: ใน logistic regression เรา normalize ได้แค่ input ชั้นเดียวเพราะมีชั้นเดียว แต่ในเครือข่ายลึก ทุก hidden layer ก็รับ input จาก layer ก่อนหน้าเหมือนกัน ดังนั้นตามคำที่สไลด์เน้นไว้ใต้ภาพเครือข่าย เรา "should normalize z (pre-activation) or \(a\) (post-activation)" ของชั้นในด้วย ไม่ใช่แค่ข้อมูลดิบที่ป้อนเข้าไป. ประเด็นนี้คือแกนกลางของทั้งหัวข้อ 3 — และจะกลายเป็นคำถาม "วาง BN ก่อนหรือหลัง activation" ในหน้า 12–14.

3.0B Implementation: normalize เพื่อดึง z กลับเข้าช่วงเชิงเส้น (สไลด์หน้า 10–11)

Slide Batch Normalization Implementation: a deep network from Layer 1 to Layer L above, and below a sigmoid activation curve annotated with Saturating at both tails and Linear in the middle around z = 0, sourced from the Hands on ML book
Slide: Batch Normalization — Implementation กับช่วง Linear/Saturating ของ sigmoid (Lecture5.pdf, p.10)

กราฟ sigmoid บนหน้า 10 ถูกแบ่งเป็นสามโซนและติดป้ายไว้: ตรงกลางราว \(z\in[-2,2]\) คือ Linear ซึ่งเส้นโค้งเกือบเป็นเส้นตรงเอียงและมีความชันดี ส่วนสองหางคือ Saturating ที่เส้นแบนราบ. นี่คือเป้าหมายของ BN อย่างเป็นรูปธรรม: ดึงค่า \(z\) ที่กระจัดกระจาย (อาจมี \(\sigma_z=10\) อย่างที่คำนวณในหน้า 5) กลับมาให้ mean 0 variance 1 ซึ่งแปลว่าตัวอย่างประมาณ 95% จะตกอยู่ในช่วง \([-2,+2]\) — พอดีกับโซน Linear. เมื่อ \(z\) อยู่โซนนี้ \(g'\) มีค่าใกล้ 0.25 (ค่าสูงสุดที่เป็นไปได้) แทนที่จะเป็น 0.00005 ⇒ gradient ไหลผ่านได้.

Slide Batch Normalization (2) showing a compact feedforward network with two inputs x1 and x2, two hidden layers of two nodes each, and one output node h_theta(x), used as the canvas for discussing where a normalization step is inserted between layers
Slide: Batch Normalization (2) — เครือข่ายตัวอย่างที่ใช้อธิบายตำแหน่งของ BN (Lecture5.pdf, p.11)

สไลด์หน้า 11 ย่อเครือข่ายให้เล็กที่สุดที่ยังอธิบายเรื่องได้ คือ 2 input → 2 hidden → 2 hidden → 1 output เพื่อใช้เป็นฉากสำหรับสูตรในหน้าถัดไป. ประโยชน์ของการย่อขนาดคือทำให้เห็นว่า BN ไม่ใช่ layer ที่เพิ่ม neuron ใหม่ — มันคือการแทรกขั้นตอนคำนวณเข้าไประหว่าง \(z\) กับ \(a\) ของ node ที่มีอยู่แล้ว. ในเครือข่ายเล็กนี้ ถ้าใส่ BN ที่ hidden layer แรก เราจะมี \(\gamma,\beta\) อย่างละ 2 ตัว (หนึ่งคู่ต่อ node) ไม่ใช่ต่อทั้ง layer — จุดนี้จะใช้ตอนนับพารามิเตอร์ในหัวข้อ 3.2.

3.1 สูตรของ Batch Normalization (สไลด์หน้า 12)

Slide Batch Normalization (BN) with the four formulas: mu_B is the mean over the mini-batch, sigma_B squared is the variance over the mini-batch, z_norm^(i) = (z^(i) - mu_B) divided by the square root of sigma_B squared plus epsilon, and z_tilde^(i) = gamma times z_norm^(i) plus beta, with notes that gamma is the scale parameter vector, beta the shift parameter vector, epsilon a tiny number typically 10^-7, and B denotes the mini-batch
Slide: Batch Normalization (BN) — สูตรเต็มทั้ง 4 บรรทัด (Lecture5.pdf, p.12)

อาจารย์นิยาม BN (Ioffe & Szegedy, 2015) ว่าคือการเพิ่ม normalized operation เข้าไปในโมเดล โดยวางได้สองตำแหน่ง คือ just before (normalize \(z\) หรือ pre-activation) หรือ after (normalize \(a\) หรือ post-activation) ของ activation function ของ each hidden layer. — ขอให้สังเกตคำว่า "แต่ละ hidden layer" ให้ดี เพราะหมายความว่าไม่ได้ทำครั้งเดียวที่ input แต่ทำซ้ำทุกชั้น. ตัวปฏิบัติการนี้มีสองขั้น:

$$\mu_B = \frac{1}{m_B}\sum_{i=1}^{m_B} \mathbf{z}^{(i)},\qquad \sigma_B^2 = \frac{1}{m_B}\sum_{i=1}^{m_B}\left(\mathbf{z}^{(i)}-\mu_B\right)^2$$ $$\mathbf{z}_{\text{norm}}^{(i)} = \frac{\mathbf{z}^{(i)}-\mu_B}{\sqrt{\sigma_B^2+\epsilon}},\qquad \tilde{\mathbf{z}}^{(i)} = \gamma\,\mathbf{z}_{\text{norm}}^{(i)} + \beta$$

ขั้นที่ 1 "simply zero-centering and normalizing each input": ลบ mean ของ mini-batch แล้วหารด้วยส่วนเบี่ยงเบนมาตรฐานของ mini-batch ⇒ ได้ mean 0 variance 1. ขั้นที่ 2 "scaling and shifting the result using two new parameter vectors per layer": คูณ \(\gamma\) แล้วบวก \(\beta\)

ความหมายของสัญลักษณ์ตามที่สไลด์กำกับไว้ทุกตัว — ต้องบอกได้หมดเพราะข้อสอบชอบถามว่าตัวไหนคืออะไร: \(m_B\) คือจำนวนตัวอย่างใน mini-batch; ตัวห้อย \(B\) หมายถึง mini-batch ("B−subscripted is denoted as mini-batch"); \(\mathbf{z}_{\text{norm}}^{(i)}\) คือเวกเตอร์ของ input ที่ถูกทำให้ศูนย์กลางเป็น 0 และ normalize แล้วสำหรับตัวอย่างที่ \(i\); \(\epsilon\) คือเลขจิ๋วกันการหารด้วยศูนย์ (สไลด์ระบุว่าปกติ \(10^{-7}\)); \(\gamma\) คือเวกเตอร์พารามิเตอร์ scale ของ layer นั้น; \(\beta\) คือเวกเตอร์พารามิเตอร์ shift/offset; และ \(\tilde{\mathbf{z}}^{(i)}\) คือ output ของ BN ซึ่งเป็น "a rescaled and shifted version of the inputs". สำคัญมาก: \(\gamma\) กับ \(\beta\) เป็นพารามิเตอร์ที่เรียนรู้ได้ด้วย gradient descent เหมือน \(\theta\) ไม่ใช่ hyperparameter ที่เราตั้งเอง.

คำนวณ: BN ของ neuron หนึ่งตัวใน mini-batch ขนาด 4
\(z = [2,\ 4,\ 6,\ 8]\) (ค่า \(z\) ของ neuron เดียวกันจาก 4 ตัวอย่างใน batch), \(\gamma=1.5\), \(\beta=0.5\), ละ \(\epsilon\) เพราะเล็กมาก
  1. ขั้นที่ 1: หา \(\mu_B\) — เฉลี่ยข้ามตัวอย่างใน batch นี้
    $$\mu_B = \frac{2+4+6+8}{4} = 5$$
  2. ขั้นที่ 2: หา \(\sigma_B^2\) จากระยะห่างยกกำลังสอง
    $$\sigma_B^2 = \frac{(-3)^2+(-1)^2+(1)^2+(3)^2}{4} = \frac{20}{4} = 5,\qquad \sqrt{\sigma_B^2} = 2.236$$
  3. ขั้นที่ 3: normalize ทีละตัว
    $$z_{\text{norm}} = \left[\frac{-3}{2.236},\ \frac{-1}{2.236},\ \frac{1}{2.236},\ \frac{3}{2.236}\right] = [-1.342,\ -0.447,\ 0.447,\ 1.342]$$

    ตรวจสอบ: ผลรวมเป็น 0 ⇒ mean = 0 ✓ และ variance = 1 ✓ — ทุกค่าตอนนี้อยู่ในช่วง \(\pm1.35\) ซึ่งเป็นโซน Linear ของ sigmoid พอดี

  4. ขั้นที่ 4: scale ด้วย \(\gamma\) แล้ว shift ด้วย \(\beta\)
    $$\tilde z = 1.5\,z_{\text{norm}} + 0.5 = [-1.512,\ -0.171,\ 1.171,\ 2.512]$$

ทำไมต้องมีขั้นที่ 4 ทั้งที่ขั้นที่ 3 ได้ mean 0 variance 1 สวยแล้ว? เพราะถ้าหยุดแค่ขั้น 3 เท่ากับเราบังคับให้ทุก layer มี distribution เดียวกันตายตัว ซึ่งอาจไม่ใช่สิ่งที่ layer นั้นต้องการ. ขั้นที่ 4 คืนอิสระให้เครือข่าย: ถ้ามันเรียนได้ว่า \(\gamma=\sqrt{\sigma_B^2}=2.236\) และ \(\beta=\mu_B=5\) ก็จะได้ \(\tilde z = z\) กลับมาเหมือนเดิมเป๊ะ (เท่ากับปิด BN เอง) — เครือข่ายเลือกเองได้ว่าจะ normalize แรงแค่ไหน.

3.2 วาง BN ก่อนหรือหลัง Activation — พร้อมโค้ดและการนับพารามิเตอร์ (สไลด์หน้า 13–14)

Slide Normalize z (pre-activation) As in paper: Keras code adding Flatten(input_shape=(28,28)), an optional commented-out BatchNormalization for 784x4, then Dense(16), BatchNormalization 16x4, Activation relu, Dense(16), BatchNormalization 16x4, Activation relu, Dense(10), BatchNormalization 10x4, Activation softmax, and model.summary; the summary table lists flatten_2 0 params, dense_4 12,560, batch_normalization_6 64, activation_3 0, dense_5 272, batch_normalization_7 64, activation_4 0, dense_6 170, batch_normalization_8 40, activation_5 0, with Total params 13,170, Trainable params 13,086 and Non-trainable params 84
Slide: Normalize z (pre-activation) — โค้ดแบบในเปเปอร์ พร้อม model.summary (Lecture5.pdf, p.13)

แบบแรกคือแบบที่เปเปอร์ต้นฉบับทำ (สไลด์เขียนกำกับว่า "As in paper" และ "BN before Activation Function"): แยก Dense กับ Activation ออกจากกัน แล้วเสียบ BatchNormalization() ไว้ตรงกลาง ⇒ ลำดับคือ Dense → BatchNormalization → Activation. ต้องแยกแบบนี้เพราะถ้าเขียน Dense(16, activation='relu') ไปเลย Keras จะทำ activation ให้ทันทีภายใน layer เดียว เราจะไม่มีที่แทรก BN ก่อน activation ได้.

# normalize before
model = models.Sequential()
model.add(Flatten(input_shape=(28,28)))
#model.add(BatchNormalization())  # 784 x 4 (optional)
model.add(Dense(16))
model.add(BatchNormalization())   # 16x4
model.add(Activation('relu'))
model.add(Dense(16))
model.add(BatchNormalization())   # 16x4
model.add(Activation('relu'))
model.add(Dense(10))
model.add(BatchNormalization())   # 10x4
model.add(Activation('softmax'))
model.summary()
โค้ดทำอะไร
model = models.Sequential()สร้างโมเดลเปล่าแบบ "ต่อกันเป็นแถวเดียว" แล้วค่อยใช้ .add() ต่อ layer ทีละชั้นตามลำดับที่เขียน
model.add(Flatten(input_shape=(28,28)))แปลงภาพ 28×28 เป็นเวกเตอร์ยาว 784 ให้อัตโนมัติ; input_shape คือ shape ของหนึ่งตัวอย่าง ไม่รวมมิติ batch — output shape ที่ summary แสดงจึงเป็น (None, 784) โดย None = จำนวนตัวอย่างที่ยังไม่กำหนด
#model.add(BatchNormalization()) # 784 x 4บรรทัดนี้ถูกคอมเมนต์ไว้ (optional) — ถ้าเปิดใช้จะ normalize พิกเซล 784 ตัวโดยตรง ให้พารามิเตอร์ \(784\times4=3{,}136\) ตัว (ดูหน้า 14 ที่เปิดใช้จริง)
model.add(Dense(16))ชั้น fully-connected 16 node ไม่ใส่ activation จึงได้ \(z\) ดิบออกมา ซึ่งเป็นสิ่งที่เราต้องการ normalize; พารามิเตอร์ \(16\times(784+1)=12{,}560\)
model.add(BatchNormalization()) # 16x4normalize \(z\) ทั้ง 16 ค่าตามสูตรหน้า 12; คอมเมนต์ 16x4 บอกจำนวนพารามิเตอร์ = \(4\times16=64\) (γ, β, moving mean, moving variance อย่างละ 16)
model.add(Activation('relu'))ค่อยใส่ ReLU ทีหลัง เท่ากับ \(a=g(\tilde z)\) — นี่คือจุดที่ทำให้เป็นแบบ "pre-activation"
model.add(Dense(10))
model.add(BatchNormalization())
model.add(Activation('softmax'))
ชั้น output 10 node (เท่ากับจำนวนคลาสของ MNIST) ตามด้วย BN (\(4\times10=40\)) แล้วปิดท้ายด้วย softmax เพื่อให้ได้ความน่าจะเป็น 10 ค่าที่รวมกันได้ 1
model.summary()พิมพ์ตารางชื่อ layer, output shape และจำนวนพารามิเตอร์ต่อชั้น พร้อมยอดรวม — ใช้ตรวจว่าโครงสร้างตรงกับที่ตั้งใจหรือไม่
ผลลัพธ์ที่คาด (ตัวเลขจริงจาก model.summary ในสไลด์หน้า 13)

flatten_2 (None, 784) → 0 · dense_4 (None, 16) → 12,560 · batch_normalization_6 (None, 16) → 64 · activation_3 (None, 16) → 0 · dense_5 (None, 16) → 272 · batch_normalization_7 → 64 · dense_6 (None, 10) → 170 · batch_normalization_8 (None, 10) → 40 · Total params: 13,170 · Trainable params: 13,086 · Non-trainable params: 84

คำนวณ: ทำไม BN ถึงมี \(4n\) พารามิเตอร์ และตัวเลข 13,170 / 13,086 / 84 มาจากไหน
โมเดล 784 → 16 → 16 → 10 ใส่ BN ก่อน activation ทุกชั้น (ไม่เปิด BN ที่ input)
  1. ขั้นที่ 1: BN หนึ่งชั้นที่มี \(n\) node เก็บค่า 4 ชุด ชุดละ \(n\) ตัว

    \(\gamma\) (scale) และ \(\beta\) (shift) — เรียนรู้ได้ จึงเป็น trainable; moving mean และ moving variance — ใช้ตอน test เท่านั้น ไม่ได้มาจาก gradient descent จึงเป็น non-trainable

    $$\text{พารามิเตอร์ของ BN หนึ่งชั้น} = 4n\quad(\text{trainable } 2n,\ \text{non-trainable } 2n)$$
  2. ขั้นที่ 2: แทน \(n\) ของแต่ละชั้น
    $$4(16)=64,\qquad 4(16)=64,\qquad 4(10)=40$$

    ตรงกับตัวเลข 64, 64, 40 ในตาราง summary ของสไลด์ทุกตัว

  3. ขั้นที่ 3: บวกกับพารามิเตอร์ของ Dense (12,560 + 272 + 170 = 13,002 — คำนวณเต็มในหัวข้อ 5.8)
    $$\text{Total} = 13{,}002 + 64 + 64 + 40 = 13{,}170$$
  4. ขั้นที่ 4: แยก trainable กับ non-trainable
    $$\text{Non-trainable} = 2(16+16+10) = 84,\qquad \text{Trainable} = 13{,}170-84 = 13{,}086$$

ตรงกับสไลด์ทั้งสามบรรทัด: Total 13,170 / Trainable 13,086 / Non-trainable 84. จุดที่ข้อสอบชอบหลอก: ตอบ \(2n\) (ลืม moving mean/variance) หรือตอบว่าทั้ง \(4n\) เป็น trainable.

Slide Normalize a (post-activation): Keras code with Flatten(input_shape=(28,28)), BatchNormalization 784x4 optional now enabled, Dense(16, activation relu), BatchNormalization 16x4, Dense(16, activation relu), BatchNormalization 16x4, Dense(10, activation softmax), model.summary; the summary shows batch_normalization_9 with 3,136 params on 784 units, dense_7 12,560, 64, dense_8 272, 64, dense_9 170, Total params 16,266, Trainable params 14,634, Non-trainable params 1,632; a highlighted note says adding a BN layer as the very first layer means you do not need to standardize your training set
Slide: Normalize a (post-activation) — BN หลัง Activation พร้อม model.summary (Lecture5.pdf, p.14)

แบบที่สองคือ "BN after Activation Function": ปล่อยให้ Dense ทำ activation เองแล้วค่อย normalize ผลลัพธ์ \(a\) ⇒ ลำดับคือ Dense(activation) → BatchNormalization ซึ่งเขียนสั้นกว่าแบบแรก. หน้านี้ยังเปิดใช้ BN ที่ชั้นแรกสุดจริง (ไม่คอมเมนต์ทิ้ง) เพื่อประกอบข้อความที่อาจารย์ทำสีไว้ว่า ในหลายกรณีถ้าเรา add a BN layer เป็น first layer ของเครือข่าย เราจะ not need to standardize your training set — คือไม่ต้องไปทำ z-score ให้ข้อมูลเทรนเองก่อน เพราะ BN ชั้นแรกทำหน้าที่นั้นแทน (และทำให้อัตโนมัติกับข้อมูล test ด้วย).

# normalize after
model = models.Sequential()
model.add(Flatten(input_shape=(28,28)))
model.add(BatchNormalization())   # 784x4 (optional)
model.add(Dense(16, activation='relu'))
model.add(BatchNormalization())   # 16x4
model.add(Dense(16, activation='relu'))
model.add(BatchNormalization())   # 16x4
model.add(Dense(10, activation='softmax'))
model.summary()
โค้ดทำอะไร
model.add(BatchNormalization()) # 784x4BN ชั้นแรกสุด ทำงานกับพิกเซล 784 ตัวหลัง Flatten ⇒ \(4\times784=3{,}136\) พารามิเตอร์ (trainable 1,568 / non-trainable 1,568). นี่คือชั้นที่ทำให้ไม่ต้อง standardize ข้อมูลเอง
model.add(Dense(16, activation='relu'))เขียน activation ไว้ในอาร์กิวเมนต์เลย ⇒ layer นี้คืน \(a=\mathrm{ReLU}(z)\) ออกมาโดยตรง ไม่มีช่องให้แทรก BN ก่อน activation
model.add(BatchNormalization()) # 16x4normalize ค่า \(a\) (post-activation) ที่ออกจาก ReLU ⇒ ได้ \(4\times16=64\) พารามิเตอร์เท่าเดิม เพราะจำนวน node เท่าเดิม
model.add(Dense(10, activation='softmax'))ชั้น output ไม่ตาม BN ในแบบนี้ — softmax ต้องเป็นตัวสุดท้ายจริงๆ เพราะผลรวมต้องเป็น 1 ถ้าเอา BN ไปต่อท้ายจะทำลายคุณสมบัตินั้น
ผลลัพธ์ที่คาด (ตัวเลขจริงจาก model.summary ในสไลด์หน้า 14)

flatten_3 (None, 784) → 0 · batch_normalization_9 (None, 784) → 3,136 · dense_7 (None, 16) → 12,560 · batch_normalization_10 → 64 · dense_8 (None, 16) → 272 · batch_normalization_11 → 64 · dense_9 (None, 10) → 170 · Total params: 16,266 · Trainable params: 14,634 · Non-trainable params: 1,632

ลองตรวจตัวเลขของหน้า 14 ด้วยกฎ \(4n\) เดียวกัน: BN ชั้นแรก \(4\times784=3{,}136\) ✓; ยอดรวม \(13{,}002+3{,}136+64+64=16{,}266\) ✓; non-trainable \(=2(784+16+16)=1{,}632\) ✓; trainable \(=16{,}266-1{,}632=14{,}634\) ✓. ครบทุกตัวเลขบนสไลด์ — ถ้าสอบถามว่า "เพิ่ม BN ที่ input แล้วพารามิเตอร์เพิ่มเท่าไร" คำตอบคือ 3,136 ซึ่งมากกว่า BN ชั้นในถึง 49 เท่า เพราะ 784 node มากกว่า 16 node.

ออกสอบสูง — ข้อความที่อาจารย์ทำสี

3.3 ตอน Train ต่างจากตอน Test อย่างไร (สไลด์หน้า 15)

ต้องรู้ก่อน — Moving Average (ค่าเฉลี่ยเคลื่อนที่)

สมมติคุณอยากรู้ "อุณหภูมิเฉลี่ยของเมืองนี้" แต่ได้ข้อมูลมาวันละค่า และไม่อยากเก็บค่าทุกวันไว้ในหน่วยความจำ. วิธีที่ใช้คือเก็บตัวเลขตัวเดียว แล้วอัปเดตทีละนิดทุกวันด้วยสูตร

$$v_{\text{new}} = \text{momentum}\cdot v_{\text{old}} + (1-\text{momentum})\cdot x_{\text{today}}$$

ถ้า momentum = 0.9 แปลว่า "เชื่อของเก่า 90% เชื่อค่าวันนี้ 10%" — ค่าที่ได้จึงนิ่ง ไม่กระโดดตามค่าผิดปกติของวันเดียว. ตัวอย่าง: \(v_{old}=20\), วันนี้วัดได้ 30 ⇒ \(v_{new}=0.9(20)+0.1(30)=21\) ไม่ใช่ 30. ยิ่ง momentum ใกล้ 1 ยิ่งนิ่งแต่ยิ่งตามช้า. นี่คือ Exponentially Weighted Average ตัวเดียวกับที่ใช้ใน Momentum optimizer (Lesson 4 §4) — ใน BN เราใช้มันเก็บ \(\mu\) และ \(\sigma^2\) ของทุก mini-batch ไว้ใช้ตอน test

Slide Issues about Batch Normalization: at training time the algorithm estimates each input's mean and standard deviation by evaluating them over the current mini-batch; at test time the whole test instance is used instead of a mini-batch and one solution is to use the moving average of means and standard deviations of all mini-batches to normalize the test instance; a Momentum hyperparameter is used by the BN layer when it updates the exponential moving averages
Slide: Issues about Batch Normalization — สถิติที่ใช้ตอน train กับตอน test (Lecture5.pdf, p.15)

ปัญหาที่หน้านี้แก้คือปัญหาที่ผู้อ่านน่าจะนึกออกเองแล้วตั้งแต่เห็นสูตร: สูตร BN ต้องใช้ \(\mu_B,\sigma_B^2\) ของ mini-batch — แล้วตอนใช้งานจริงที่มีรูปให้ทำนายแค่รูปเดียวล่ะ เอา mean จากไหน? อาจารย์แยกตอบเป็นสองสถานะ. ที่ training time อัลกอริทึมต้องประมาณ mean และ standard deviation ของแต่ละ input โดย evaluating the mean and standard deviation of each input over the current mini-batch คือคำนวณสดจาก batch ที่กำลังวิ่งอยู่. ส่วนที่ test time "the whole test instance is used (not mini-batch)" ทางแก้หนึ่งคือหา moving average of means and standard deviations of all mini-batch ที่สะสมไว้ระหว่างเทรน แล้วใช้ค่านั้นมา normalize ตัวอย่าง test. และ Momentum คือ hyperparameter ที่ BN layer ใช้ตอนอัปเดต exponential moving average เหล่านั้น.

เหตุผลที่ต้องทำแบบนี้ไม่ใช่แค่ "ไม่มี batch": ต่อให้มี test set ทั้งก้อน เราก็ไม่ควรให้ผลทำนายของรูปหนึ่งขึ้นกับว่ามันถูกจัดอยู่ในกลุ่มกับรูปไหน — โมเดลที่ดีต้องให้คำตอบเดิมเสมอสำหรับ input เดิม. การใช้ moving average ที่คงที่จึงรับประกันข้อนี้ และนี่คือพารามิเตอร์ non-trainable \(2n\) ตัวที่เรานับไว้ในหัวข้อ 3.2.

คำนวณ: อัปเดต moving mean ระหว่างเทรน (momentum = 0.99 ค่า default ของ Keras)
moving mean ที่สะสมมาถึงตอนนี้ = 4.8; mini-batch ล่าสุดคำนวณได้ \(\mu_B = 5\)
  1. ขั้นที่ 1: แทนค่าในสูตร moving average
    $$\mu_{\text{moving}} = 0.99(4.8) + 0.01(5) = 4.752 + 0.05 = 4.802$$
  2. ขั้นที่ 2: ประเมินว่าขยับเร็วแค่ไหน
    $$\Delta = 4.802-4.8 = 0.002 = 1\% \text{ ของส่วนต่าง } (5-4.8)$$

    ถ้า momentum เป็น 0.9 แทน จะได้ \(0.9(4.8)+0.1(5)=4.82\) ⇒ ขยับเร็วกว่า 10 เท่า

momentum สูง (0.99) ⇒ ค่านิ่งมาก ทนต่อ batch ที่ผิดปกติ แต่ถ้าเทรนไม่กี่ epoch ค่าอาจยังตามไม่ทันสถิติจริง; momentum ต่ำ ⇒ ตามไว แต่แกว่ง. Lesson 6 แนะนำให้ลอง 0.9 / 0.99 / 0.999 เป็นตัวเลือกในการจูน.

3.4 ข้อดีทั้งชุดของ BN (สไลด์หน้า 16)

Slide Issues about Batch Normalization (2): BN increases both speed of training and accuracy; it helps prevent pre or post activations becoming too small (vanishing gradients) or too large (exploding gradients); although BN introduces an extra computation at each layer that takes more time per epoch, convergence may be faster since fewer epochs are needed; BN allows much higher learning rates and being less careful about initialization; it weakens the coupling between earlier and later layers; BN allows each layer to learn more independently, making gradients more stable between layers
Slide: Issues about Batch Normalization (2) — ข้อดีที่ต้องท่องให้ครบ (Lecture5.pdf, p.16)

หน้านี้คือรายการข้อดีที่ออกสอบแบบ "ข้อใดไม่ใช่ข้อดีของ BN" ได้ทันที จึงต้องไล่ให้ครบทีละข้อพร้อมเหตุผล. ข้อแรก BN ช่วย prevent pre or post activations ไม่ให้กลายเป็น too small (vanishing gradients) หรือ large (exploding gradients) — สังเกตว่าอาจารย์พูดถึงทั้ง pre (\(z\)) และ post (\(a\)) สอดคล้องกับสองตำแหน่งวางในหน้า 13–14.

ข้อสองเป็นข้อที่ดูขัดแย้งในตัวเองแต่ต้องอธิบายให้ถูก: BN ทำให้เกิด extra computation ในทุกชั้น ซึ่งกินเวลาต่อ epoch มากขึ้น แต่ convergence may be faster เพราะใช้จำนวน epoch น้อยลงกว่าจะลู่เข้า. พูดเป็นตัวเลขสมมติให้เห็นภาพ: ถ้าไม่มี BN ใช้ 100 epoch × 10 วินาที = 1,000 วินาที; มี BN ใช้ 30 epoch × 13 วินาที = 390 วินาที — ช้าลงต่อ epoch แต่เร็วขึ้นโดยรวม. อย่าตอบว่า BN ทำให้แต่ละ epoch เร็วขึ้น เพราะสไลด์บอกตรงข้าม.

ข้อสาม BN ทำให้ใช้ higher learning rates ได้มาก (เหตุผลในวงเล็บของสไลด์: "no extreme values in the normalized activations" — เมื่อไม่มีค่าสุดโต่ง ก้าวใหญ่ก็ไม่พาให้ระเบิด) และทำให้เรา less careful about initialization คือไม่ต้องพิถีพิถันเรื่องค่าเริ่มต้นมากเท่าเดิม (เพราะต่อให้เริ่มด้วย variance ผิด BN ก็ดึงกลับให้ทุกรอบ — นี่คือคำตอบของข้อจำกัดในหน้า 8 พอดี). ข้อสี่ BN "weakens the coupling of the learning process between the earlier and later layers" คือคลายความผูกพันระหว่าง layer ต้นกับ layer ปลาย ⇒ แต่ละ layer เรียนได้ independently มากขึ้น ⇒ ทำให้ gradient stable ขึ้นระหว่างชั้น. เหตุผลเบื้องหลัง — เสริมความเข้าใจ (นอกสไลด์): ถ้าไม่มี BN เมื่อ layer 1 ขยับ distribution ของ input ที่ layer 5 เห็นก็เปลี่ยนตาม layer 5 ต้องเรียนใหม่ไปเรื่อยๆ; BN ตรึง mean/variance ที่ layer 5 เห็นไว้ให้คงที่ layer 5 จึงไม่ต้องไล่ตาม.

3.5 BN เป็น Regularization และการใช้คู่กับ Dropout (สไลด์หน้า 17)

Slide Issues about Batch Normalization (3): BN offers some regularization; if we work on a small batch of instances the statistics computed over the batch would be unreliable due to stochastic properties; BN can be added with dropout, preferably add dropout after BN, because applying dropout before normalization changes the mean and variance of the activations and negates the benefits; the code box shows Dense(16), BatchNormalization, Activation relu, Dropout(0.2) repeated twice, then Dense(10), BatchNormalization, Activation softmax
Slide: Issues about Batch Normalization (3) — BN กับ Dropout (Lecture5.pdf, p.17)

อาจารย์บอกว่า BN ให้ regularization. มาด้วยในตัว — เหตุผลอยู่ในบรรทัดถัดมา: ถ้าเราทำงานกับ a small batch of instances การคำนวณสถิติจากตัวอย่างใน batch นั้นจะ "unreliable (stochastic properties)" คือไม่น่าเชื่อถือเพราะสุ่มแกว่ง. ความแกว่งนี้ทำหน้าที่เหมือน noise ที่ใส่เข้าไปตอนเทรน ซึ่งกันไม่ให้โมเดลจำข้อมูลเทรนได้แม่นเกินไป — กลไกเดียวกับที่ dropout ทำงาน. แต่นี่เป็นดาบสองคม: ถ้า batch เล็กเกินไป สถิติก็เพี้ยนจน BN ทำงานได้แย่ลงจริงๆ.

จากนั้นสไลด์ระบุว่า BN ใช้ร่วมกับ dropout. ได้ แต่ควร add dropout after BN — วาง dropout ไว้หลัง BN. เหตุผลที่อาจารย์ให้: "If you apply dropout before normalization, the mean and variance of the activations could change, negating the benefits of batch normalization" คือ dropout ปิด node แบบสุ่ม ทำให้ mean/variance ของ activation เปลี่ยนไป ถ้า BN มาคำนวณสถิติทีหลังก็จะได้สถิติของข้อมูลที่ถูกเจาะรูแล้ว ผลดีของ BN จึงถูกล้างทิ้ง. ลองคิดเป็นตัวเลข: activation \([1,2,3,4]\) มี mean 2.5; ถ้า dropout ปิดสองตัวหลังแล้วคูณชดเชย \(1/0.5\) ได้ \([2,4,0,0]\) mean กลายเป็น 1.5 — BN ที่ตามมาจะ normalize ด้วย mean ที่ผิดจากความจริง.

model = models.Sequential()
model.add(Flatten(input_shape=(28,28)))
#model.add(BatchNormalization())  # 784 x 4 (optional)
model.add(Dense(16))
model.add(BatchNormalization())   # 16x4
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(16))
model.add(BatchNormalization())   # 16x4
model.add(Activation('relu'))
model.add(Dropout(0.2))
model.add(Dense(10))
model.add(BatchNormalization())   # 10x4
model.add(Activation('softmax'))
โค้ดทำอะไร
model.add(Dense(16))
model.add(BatchNormalization())
โครงเดิมจากหน้า 13 (แบบ pre-activation) — สไลด์กำกับใต้ภาพว่า "an example of adding BN before activation function"
model.add(Activation('relu'))activation มาหลัง BN ⇒ ReLU ได้รับค่าที่ normalize แล้ว จึงไม่มีค่าสุดโต่งเข้ามา
model.add(Dropout(0.2))บรรทัดที่เป็นหัวใจของหน้านี้: dropout อยู่หลังสุด ของบล็อก ตามคำแนะนำ "add dropout after BN"; 0.2 คือ rate = สัดส่วน node ที่ถูกปิด (20%) ไม่ใช่สัดส่วนที่เก็บไว้
(บล็อกที่ 2 ซ้ำแบบเดียวกัน)Dense → BN → Activation → Dropout อีกชุดสำหรับ hidden layer ที่สอง — รูปแบบนี้คือ "บล็อกมาตรฐาน" ที่ควรจำให้ขึ้นใจสำหรับข้อสอบเติมโค้ด
model.add(Dense(10))
model.add(BatchNormalization())
model.add(Activation('softmax'))
ชั้น output ไม่มี Dropout — ถ้าปิด node ของชั้น output แบบสุ่ม ความน่าจะเป็นของคลาสจะหายไปดื้อๆ
ผลลัพธ์ที่คาด

โค้ดชุดนี้เพียงประกาศโครงสร้าง จึงยังไม่พิมพ์อะไรออกมา; ถ้าเรียก model.summary() ต่อ จะได้ตารางเหมือนหน้า 13 ทุกประการบวกแถว dropout สองแถวที่มี Param # = 0 — เพราะ dropout ไม่มีพารามิเตอร์ที่ต้องเรียนรู้เลย มันแค่สุ่มปิด node ตอน forward เท่านั้น. ยอดรวมจึงยังเป็น Total params 13,170 เท่าเดิม

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
ทำไมต้อง normalize "ต่อ mini-batch" ทุกรอบ ไม่ใช่คำนวณสถิติทั้ง dataset ครั้งเดียวตอนเริ่ม? เพราะ distribution ของ \(z\) ที่ hidden layer ไม่คงที่ — มันขึ้นกับ \(\theta\) ของทุก layer ก่อนหน้า ซึ่งเปลี่ยนทุก step. สถิติที่คำนวณครั้งเดียวจะล้าสมัยทันทีหลังอัปเดตรอบแรก.
เปลี่ยนแล้วเกิดอะไร
ถ้า batch size = 2 และ \(z=[1,2]\) จะได้ \(\mu_B=1.5,\sigma_B^2=0.25\) ⇒ \(z_{\text{norm}}=[-1,+1]\) เสมอ ไม่ว่าค่าจริงจะเป็น \([1,2]\) หรือ \([100,200]\) — ข้อมูลเรื่องขนาดหายหมด; และถ้า batch size = 1 จะได้ \(z-\mu_B=0\) ⇒ output เป็น \(\beta\) ล้วน ข้อมูลหายสนิท. นี่คือเหตุผลเชิงตัวเลขของคำว่า "unreliable" ในสไลด์หน้า 17.
เชื่อมกับ
batch size ตัวเดียวกับที่ชั่งน้ำหนักความเร็ว/ความนิ่งใน Lesson 4 §3 — ตอนนี้มันมีผลเพิ่มอีกด้านคือคุณภาพของสถิติ BN; และ dropout จาก Lesson 4 §6 ที่ตอนนี้ต้องคิดเรื่องลำดับด้วย.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 6Batch Normalization
เพราะเหตุใด Batch Normalization จึงต้องมีขั้น scale-and-shift (γ·z_norm + β) ต่อจากขั้น normalize?
คำถาม 7BN Train vs Test
ทำไม Batch Normalization จึงใช้ moving average ของ μ และ σ² ที่สะสมตอนเทรน แทนการคำนวณสดตอน test?
คำถาม 8BN + Dropout (เติมโค้ด)
ตามคำแนะนำในสไลด์หน้า 17 ลำดับ layer ชุดใดถูกต้องสำหรับหนึ่งบล็อกของ hidden layer?

4. Layer Normalization: เมื่อไม่อยากพึ่ง Mini-batch

หัวข้อ 3 จบด้วยจุดอ่อนที่ชัดเจนของ BN คือมันพึ่งพา batch. อาจารย์จึงตั้งคำถามต่อในหน้า 18 ว่า "BatchNorm normalizes each neuron across examples. What if we instead normalize the neurons within each individual example?" — แทนที่จะเฉลี่ยข้ามตัวอย่าง ทำไมไม่เฉลี่ยข้าม neuron ภายในตัวอย่างเดียวไปเลย?

Slide From Batch Normalization to Layer Normalization with a worked example: a layer has 3 neurons and batch size 4, z is the matrix with rows (3,1000,0), (2,800,1), (4,1200,1), (3,900,0); BN normalizes down each column giving mu_1B = (3+2+4+3)/4 = 3, mu_2B = (1000+800+1200+900)/4 = 975, mu_3B = (0+1+1+0)/4 = 0.5; LayerNorm by Ba, Kiros and Hinton 2016 normalizes within each example, with mu^(1) = (3+1000+0)/3 = 334.33 and mu^(2) and mu^(m) left blank; LN also uses scaling and shifting with learnable gamma and beta
Slide: From Batch Normalization to Layer Normalization — ตัวอย่างเมทริกซ์ 4×3 พร้อมโจทย์ที่เว้นว่าง (Lecture5.pdf, p.18)

ตัวอย่างบนสไลด์ตั้งใจเลือกตัวเลขให้เห็นประเด็นชัด: layer มี 3 neuron และ batch size = 4 ได้เมทริกซ์

$$z = \begin{bmatrix}3 & 1000 & 0\\ 2 & 800 & 1\\ 4 & 1200 & 1\\ 3 & 900 & 0\end{bmatrix}\quad\begin{array}{l}\text{แถว} = \text{ตัวอย่างที่ }1..4\\ \text{คอลัมน์} = \text{neuron ที่ }1..3\end{array}$$

สังเกตว่า neuron ที่ 2 มีค่าอยู่ในหลักพัน ส่วน neuron ที่ 1 และ 3 อยู่ในหลักหน่วย — เหมือน feature ที่มีหน่วยต่างกันมากในตัวอย่าง logistic regression หน้า 9. อาจารย์บอกว่า "BN conceptually normalizes down each column" คือไล่ลงตามคอลัมน์ แล้วคำนวณให้ดูครบทั้งสาม: \(\mu_{1B}=\frac{3+2+4+3}{4}=3\), \(\mu_{2B}=\frac{1000+800+1200+900}{4}=975\), \(\mu_{3B}=\frac{0+1+1+0}{4}=0.5\). แต่ละ neuron ได้ mean ของตัวเอง ⇒ หลัง normalize ทั้งสาม neuron จะมี mean 0 variance 1 เท่ากันหมด ความต่างเรื่องสเกลถูกลบทิ้งพอดี.

ส่วน LayerNorm (Ba, Kiros & Hinton, 2016) normalize "within each example" คือไล่ตามแถว. สไลด์ทำตัวอย่างแรกให้ดูแล้วเว้นที่เหลือไว้เป็นโจทย์: \(\mu^{(1)}=\frac{3+1000+0}{3}=334.33\) ส่วน \(\mu^{(2)}=\dots\) และ \(\mu^{(m)}=\dots\) ถูกทิ้งว่าง เรามาเติมให้ครบ.

คำนวณ: เติมช่องว่างของสไลด์หน้า 18 — \(\mu^{(2)}\) ถึง \(\mu^{(m)}\) ของ LayerNorm
เมทริกซ์ \(z\) ข้างบน; LayerNorm เฉลี่ยข้าม 3 neuron ภายในแต่ละแถว (\(m=4\) ตัวอย่าง)
  1. ขั้นที่ 1: ตัวอย่างที่ 2 คือแถว \([2,\ 800,\ 1]\)
    $$\mu^{(2)} = \frac{2+800+1}{3} = \frac{803}{3} = 267.67$$
  2. ขั้นที่ 2: ตัวอย่างที่ 3 คือแถว \([4,\ 1200,\ 1]\)
    $$\mu^{(3)} = \frac{4+1200+1}{3} = \frac{1205}{3} = 401.67$$
  3. ขั้นที่ 3: ตัวอย่างสุดท้าย \(m=4\) คือแถว \([3,\ 900,\ 0]\)
    $$\mu^{(m)} = \mu^{(4)} = \frac{3+900+0}{3} = \frac{903}{3} = 301$$
  4. ขั้นที่ 4: ลอง normalize แถวแรกให้ครบเพื่อดูว่าเกิดอะไรขึ้น
    $$\sigma^{2(1)} = \frac{(3-334.33)^2+(1000-334.33)^2+(0-334.33)^2}{3} = 221{,}558 \Rightarrow \sigma^{(1)} = 470.7$$ $$z^{(1)}_{\text{norm}} = [-0.704,\ +1.414,\ -0.710]$$

คำตอบของสไลด์: \(\mu^{(2)}=267.67\), \(\mu^{(3)}=401.67\), \(\mu^{(4)}=301\). สังเกตข้อสังเกตสำคัญจากขั้นที่ 4: LayerNorm เอาเลข 3 กับ 1000 ซึ่งคนละหน่วยมาเฉลี่ยรวมกัน ผลคือ neuron ที่ 1 และ 3 ถูกบีบให้เกือบเท่ากัน (\(-0.704\) กับ \(-0.710\)) ความต่างระหว่างสอง neuron นี้แทบหายไป — ต่างจาก BN ที่ normalize แยกต่อ neuron จึงรักษาความต่างนั้นไว้ได้. สไลด์ยังย้ำว่า "Similar to BN, LN also uses Scaling and shifting" คือมี \(\gamma\) และ \(\beta\) ที่เรียนรู้ได้เหมือนกัน.

คำนวณ: BN เทียบ LayerNorm บนข้อมูลชุดเดียวกัน (batch 2 ตัวอย่าง × 4 neuron)
\(Z = \begin{bmatrix}1&3&5&7\\2&2&6&6\end{bmatrix}\) (แถว = ตัวอย่าง, คอลัมน์ = neuron); ละ \(\epsilon,\gamma,\beta\)
  1. ขั้นที่ 1: BN — เฉลี่ยตามคอลัมน์ เช่น neuron ที่ 1 มีค่า \([1,2]\)
    $$\mu = 1.5,\ \sigma^2 = 0.25,\ \sigma=0.5 \Rightarrow z_{\text{norm}} = \left[\tfrac{1-1.5}{0.5},\ \tfrac{2-1.5}{0.5}\right] = [-1,\ +1]$$
  2. ขั้นที่ 2: LayerNorm — เฉลี่ยตามแถว ตัวอย่างที่ 1 คือ \([1,3,5,7]\)
    $$\mu = 4,\ \sigma^2 = 5,\ \sigma=2.236 \Rightarrow z_{\text{norm}} = [-1.342,\ -0.447,\ 0.447,\ 1.342]$$
  3. ขั้นที่ 3: LayerNorm ของตัวอย่างที่ 2 คือ \([2,2,6,6]\)
    $$\mu = 4,\ \sigma^2 = 4,\ \sigma=2 \Rightarrow z_{\text{norm}} = [-1,\ -1,\ +1,\ +1]$$
  4. ขั้นที่ 4: ทดสอบกรณีสุดขั้ว — ถ้า batch มีตัวอย่างเดียว

    BN: แต่ละ neuron เหลือค่าเดียว ⇒ \(z-\mu_B = 0\) ทุกตัว ⇒ output เป็น \(\beta\) ล้วน ข้อมูลหายหมด. LayerNorm: ยังมี 4 neuron ให้เฉลี่ยตามปกติ ⇒ ทำงานได้เหมือนเดิมทุกประการ

ข้อสรุปที่ต้องจำ: ทั้งสองใช้สูตรเดียวกัน ต่างกันแค่ "แกนที่เฉลี่ย" — BN เฉลี่ยข้ามตัวอย่าง (คอลัมน์), LN เฉลี่ยข้าม feature ภายในตัวอย่าง (แถว).

Slide Why Layer Normalization: independent of batch size because normalization does not depend on other examples in the mini-batch, stable for large-scale Transformer training as it avoids dependence on noisy or changing batch statistics, and same normalization behavior during training and inference; a comparison table states Batch Normalization normalizes across examples in a mini-batch, depends on batch statistics and is common in CNNs, while Layer Normalization normalizes across features within each example, is independent of batch statistics and is common in Transformers and LLMs
Slide: Why Layer Normalization? — ตารางเปรียบเทียบ BN กับ LN (Lecture5.pdf, p.19)

สไลด์หน้า 19 สรุปข้อดีของ LN ไว้ 3 ข้อ ซึ่งทั้งสามข้อเป็นผลโดยตรงจากการเปลี่ยนแกนที่เฉลี่ย. หนึ่ง Independent of batch size — การ normalize ไม่ขึ้นกับตัวอย่างอื่นใน mini-batch เลย (ตรงกับขั้นที่ 4 ที่เราคำนวณไว้: batch = 1 ก็ยังทำงาน). สอง Stable for large-scale Transformer training — เลี่ยงการพึ่งสถิติ batch ที่ noisy หรือเปลี่ยนไปเรื่อยๆ ซึ่งสำคัญมากเมื่อเทรนโมเดลใหญ่ที่ batch ต่อการ์ดจอมีไม่กี่ตัวอย่าง. สาม Same normalization behavior during training and inference — ไม่ต้องมี moving average ให้ต่างกันสองโหมดเหมือน BN จึงไม่มีบั๊กประเภท "เทรนดีแต่ตอน predict เพี้ยน".

ตารางท้ายหน้าเทียบสามแถว: BN "Normalizes across examples in a mini-batch" / LN "Normalizes across features within each example"; BN "Depends on batch statistics" / LN "Independent of batch statistics"; BN "Common in CNNs" / LN "Common in Transformers / LLMs". แถวสุดท้ายนี้อธิบายได้ด้วยเหตุผลจากขั้นที่ 4 ของโจทย์หน้า 18: ใน CNN แต่ละ channel คือ feature ชนิดเดียวกันที่ scale ใกล้กัน BN จึงเหมาะ; ใน Transformer มิติของ embedding อยู่ใน scale เดียวกันภายในโทเค็นเดียว และ batch มักเล็ก LN จึงเหมาะกว่า.

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
BN ต้องมี moving average เพราะ "หน่วยที่ใช้ normalize" ของมัน (กลุ่มตัวอย่างใน batch) หายไปตอน test ที่เหลือแค่ตัวอย่างเดียว; LN normalize ภายในตัวอย่างซึ่งมีครบเสมอทั้งตอน train และ test จึงไม่ต้องเก็บสถิติอะไรเลย.
เปลี่ยนแล้วเกิดอะไร
ใช้ LN กับ layer ที่มี neuron แค่ 2 ตัว: เฉลี่ยจาก 2 ค่าให้ผล \([-1,+1]\) เสมอ ข้อมูลหายแบบเดียวกับ BN ที่ batch = 2 — จุดอ่อนทางสถิติเดียวกัน แค่ย้ายจากแกน "batch เล็ก" ไปแกน "layer แคบ".
เชื่อมกับ
ทั้ง BN และ LN คือ z-score จาก Lesson 1 §4 ที่ย้ายมาใช้กลางเครือข่าย ต่างกันแค่เลือกแกน — เลือก BN เมื่อ batch ใหญ่และ feature คนละสเกล, เลือก LN เมื่อ batch เล็กหรือ feature สเกลใกล้กัน.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 9Layer vs Batch Norm
ข้อได้เปรียบหลักของ Layer Normalization เทียบกับ Batch Normalization ตามสไลด์หน้า 19 คืออะไร?
คำถาม 10คำนวณ LayerNorm
จากเมทริกซ์ในสไลด์หน้า 18 (แถวที่ 3 คือ [4, 1200, 1]) ค่า μ⁽³⁾ ของ LayerNorm เท่ากับเท่าไร?

5. Keras ในภาคปฏิบัติ: เดินครบ 5 ขั้นบน MNIST

ถึงจุดนี้เรามีทฤษฎีครบแล้ว — ที่เหลือคือ "แล้วเขียนเป็นโค้ดยังไง". ครึ่งหลังของเลกเชอร์ (หน้า 20–38) คือส่วนนี้ทั้งหมด และเป็นส่วนที่ข้อสอบเติมโค้ดดึงไปใช้โดยตรง. อ่านทุกบรรทัดให้เข้าใจว่าอาร์กิวเมนต์แต่ละตัวทำอะไร ไม่ใช่แค่จำหน้าตา เพราะข้อสอบมักสลับชื่ออาร์กิวเมนต์หรือสลับลำดับ layer.

5.0 Keras คืออะไร และขั้นตอนมาตรฐาน 5 ขั้น (สไลด์หน้า 20–21)

Slide Keras API: Keras is a high-level API for building and training deep learning models, designed to be simple and flexible, with user-friendly and consistent APIs, developed by Francois Chollet, running on top of three popular backends TensorFlow, PyTorch and JAX in Keras 3, and easily scaling to large GPU clusters or entire TPU pods; a screenshot from keras.io shows the same script running with each backend
Slide: Keras API — นิยามและจุดเด่น (Lecture5.pdf, p.20)

Keras คือ high-level API สำหรับสร้างและเทรน deep learning models ที่ออกแบบให้เรียบง่ายและยืดหยุ่น. คำว่า high-level หมายถึงเราสั่งด้วยภาษาระดับแนวคิด ("ขอ Dense 16 node ที่ใช้ relu") แล้วเครื่องมือไปจัดการรายละเอียดการคูณเมทริกซ์ การหาอนุพันธ์ และการจัดการหน่วยความจำให้เอง — เทียบกับตอนที่เราเขียน backprop ด้วยมือใน Lesson 3 ที่ต้องไล่ทุก \(\delta\) เอง. จุดอื่นที่สไลด์ระบุ: API ใช้ง่ายและสม่ำเสมอ (user-friendly & consistent), พัฒนาโดย François Chollet, รันบน backend ยอดนิยมได้ 3 ตัวคือ TensorFlow, PyTorch และ JAX (ตั้งแต่ Keras 3) โดยโค้ดชุดเดียวกันรันได้ทุก backend, และขยายขึ้นไปทำงานบนคลัสเตอร์ GPU ขนาดใหญ่หรือ TPU pod ทั้งชุดได้.

Slide Typical Keras Workflow listing five numbered steps: 1 loading the data and preprocess the loaded data if needed, 2 build the model choosing architecture with number of layers and number of nodes and activation functions, 3 compile the model configuring the learning process by choosing a cost function an optimizer and some metrics to monitor, 4 train the model by calling the fit method, 5 evaluate the model and make a prediction
Slide: Typical Keras Workflow — 5 ขั้นตอนมาตรฐาน (Lecture5.pdf, p.21)

ห้าขั้นตอนบนหน้า 21 คือโครงของทุกโปรเจกต์ Keras และเป็นโครงของหัวข้อ 5 นี้ทั้งหัวข้อ: (1) Loading the data and Preprocess — โหลดข้อมูลและเตรียม (ถ้าจำเป็น); (2) Build the model — เลือกสถาปัตยกรรมที่จับคู่ input กับ target โดยต้องตัดสินใจ "จำนวน layer" และ "จำนวน node กับ activation function"; (3) Compile the model — ตั้งค่ากระบวนการเรียนรู้โดยเลือก cost function, optimizer และ metric ที่จะเฝ้าดู; (4) Train the model — เรียกเมท็อด fit(); (5) Evaluate the model — ประเมินผลและทำนาย. สังเกตว่าลำดับนี้ห้ามสลับ: จะ fit() ไม่ได้ถ้ายังไม่ compile() เพราะโมเดลยังไม่รู้ว่าจะย่อ loss ตัวไหนด้วย optimizer อะไร.

5.1 อุ่นเครื่องด้วย Circle Dataset (สไลด์หน้า 22)

Slide Begin with Circle Dataset showing a complete Keras script building a Sequential model with three Dense(10, relu, he_normal) hidden layers and a Dense(1, sigmoid) output, compiling with Adam learning_rate 0.03 and binary_crossentropy, fitting for 50 epochs with batch_size 150, and evaluating; below are a loss/accuracy curve over 50 epochs, the circle-shaped decision boundary on the two-class scatter plot, and the training log showing per-epoch loss around 0.38 to 0.53 and val_accuracy around 0.70 to 0.88
Slide: Begin with Circle Dataset — สคริปต์ Keras ฉบับเต็มบรรทัดต่อบรรทัด (Lecture5.pdf, p.22)

ก่อนจะลุย MNIST อาจารย์ให้ดูสคริปต์สั้นที่มีครบทั้ง 5 ขั้นในหน้าเดียวบนชุดข้อมูล Circle (จุดสองกลุ่มวางเป็นวงแหวนซ้อนกัน แยกด้วยเส้นตรงไม่ได้ — ตัวอย่างเดียวกับที่ใช้ทดลอง initialization ในหน้า 7). ข้อมูลมี 2 feature ต่อจุด จึงเห็น input_shape=(2,) และเป็นปัญหา 2 คลาส จึงจบที่ Dense(1, activation='sigmoid').

model = models.Sequential()
model.add(Dense(10, activation='relu', kernel_initializer='he_normal', input_shape=(2,)))
model.add(Dense(10, activation='relu', kernel_initializer='he_normal'))
model.add(Dense(10, activation='relu', kernel_initializer='he_normal'))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.03),
              loss='binary_crossentropy', metrics=['accuracy'])

history = model.fit(X, y, validation_data=(Xval, yval), epochs=50, batch_size=150)

loss, acc = model.evaluate(Xtest, ytest)
โค้ดทำอะไร
model = models.Sequential()ขั้นที่ 2 ของ workflow: สร้างโมเดลเปล่าแบบเรียงต่อกันชั้นเดียว แล้วใช้ .add() ต่อ layer
Dense(10, activation='relu', kernel_initializer='he_normal', input_shape=(2,))hidden layer แรก: 10 node; activation='relu' ตามคำแนะนำหน้า 3; kernel_initializer='he_normal' คือ He initialization ซึ่งจับคู่กับ ReLU ตามหน้า 8 (ถ้าไม่ระบุ Keras ใช้ glorot_uniform); input_shape=(2,) คือ shape ของหนึ่งตัวอย่าง = 2 feature — ใส่เฉพาะ layer แรกเท่านั้น
Dense(10, ...) อีก 2 บรรทัดhidden layer ที่ 2 และ 3 ไม่ต้องระบุ input_shape แล้ว เพราะ Keras อนุมานจาก output ของ layer ก่อนหน้าได้เอง (คือ 10)
Dense(1, activation='sigmoid')output layer: 1 node เพราะเป็น binary classification และใช้ sigmoid เพื่อให้ได้ความน่าจะเป็น 0–1 (ตรงกับตารางในหน้า 34)
optimizer=keras.optimizers.Adam(learning_rate=0.03)ขั้นที่ 3: สร้าง optimizer เป็นออบเจ็กต์เพื่อกำหนด learning_rate เอง (ถ้าเขียน optimizer='adam' เฉยๆ จะได้ค่า default 0.001); 0.03 ถือว่าสูงมาก ใช้ได้เพราะข้อมูลง่ายและ He init คุม variance ไว้
loss='binary_crossentropy'cost function สำหรับ 2 คลาส คือ \(-[y\ln\hat y+(1-y)\ln(1-\hat y)]\) จาก Lesson 1 §8 — ต้องจับคู่กับ sigmoid ที่ output
metrics=['accuracy']ค่าที่อยาก "เฝ้าดู" ระหว่างเทรน ไม่ได้ถูก optimize; ต้องเป็น list เสมอ (มีวงเล็บเหลี่ยม)
history = model.fit(X, y, validation_data=(Xval, yval), epochs=50, batch_size=150)ขั้นที่ 4: validation_data รับ tuple ของชุด validation ที่แยกไว้แล้ว; epochs=50 = วนข้อมูลทั้งชุด 50 รอบ; batch_size=150 = อัปเดต \(\theta\) หนึ่งครั้งต่อ 150 ตัวอย่าง; ค่าที่คืนกลับคือออบเจ็กต์ history ที่เก็บ metric ทุก epoch
loss, acc = model.evaluate(Xtest, ytest)ขั้นที่ 5: คืนค่า สองค่าตามลำดับ คือ loss ก่อน แล้วตามด้วย metric ที่ระบุใน compile (ที่นี่คือ accuracy) — ลำดับนี้ออกสอบบ่อย
ผลลัพธ์ที่คาด (อ่านจาก log การเทรนบนสไลด์หน้า 22)

แต่ละ epoch พิมพ์บรรทัดแบบ 1/1 ━━━━ 0s 29ms/step - loss: 0.4232 - accuracy: 0.8333 - val_loss: 0.5438 - val_accuracy: 0.7300; ช่วง epoch 18–28 ค่า loss ไล่จากราว 0.53 ลงมาราว 0.38 และ val_accuracy ขยับจากราว 0.70 ขึ้นไปราว 0.88. กราฟทางซ้ายแสดงว่า acc กับ val_acc ไต่ขึ้นไปใกล้ 0.9 ขณะที่ loss ลดลงต่อเนื่อง ส่วน val_loss เริ่มแบนหลัง epoch ~40 และเส้นแบ่งที่ได้เป็นวงกลมล้อมกลุ่มจุดสีแดงตรงกลางพอดี

จุดที่ข้อสอบเติมโค้ดมักถาม (หน้า 22)

5.2 โจทย์จริง: MNIST (สไลด์หน้า 23)

Slide Understand Core Keras APIs via Examples: classify handwritten digits from the National Institute of Standards and Technology dataset of the 1980s; input features are grayscale images of handwritten digits at 28 by 28 pixels; target features are 10 categories or classes from 0 through 9; four sample training images showing a zero, a seven, a three and a nine are displayed
Slide: Understand Core Keras APIs via Examples — โจทย์ MNIST (Lecture5.pdf, p.23)

งานคือจำแนกตัวเลขเขียนมือจากชุดข้อมูลของ National Institute of Standards and Technology (ยุค 1980s). Input features คือภาพระดับเทา (grayscale) ขนาด \(28\times28\) พิกเซล ซึ่งแปลว่าหนึ่งภาพมีตัวเลข \(28\times28=784\) ค่า แต่ละค่าคือความเข้มของพิกเซลในช่วง 0–255 (0 = ดำสนิท, 255 = ขาวสุด). Target features คือ 10 คลาส (0 ถึง 9) ⇒ output layer ต้องมี 10 node พอดี. ตัวอย่างภาพ 4 รูปบนสไลด์ (เลข 0, 7, 3 และ 9) แสดงให้เห็นว่าลายมือแต่ละคนต่างกันมาก — เลข 9 ในภาพมีหัวกลมเล็กและหางตรง ขณะที่เลข 7 ลากเฉียงยาว — นี่คือเหตุผลที่ต้องใช้เครือข่ายเรียนรู้แทนการเขียนกฎเอง.

5.3 ขั้นที่ 1: โหลดข้อมูล (สไลด์หน้า 24)

Slide Loading the MNIST Dataset in Keras: code loading the dataset from tensorflow.keras.datasets with (X, y), (Xtest, ytest) = mnist.load_data(), then four print statements showing the shapes; the output box reads shape of X (60000, 28, 28), shape of y (60000,), shape of X (10000, 28, 28), shape of y (10000,)
Slide: Loading the MNIST Dataset in Keras — โค้ดโหลดข้อมูลและ shape ที่ได้ (Lecture5.pdf, p.24)

Keras มีชุดข้อมูลมาตรฐานติดมาในตัว จึงโหลดได้ด้วยบรรทัดเดียวโดยไม่ต้องดาวน์โหลดเอง. สิ่งที่ต้องสังเกตให้ดีคือ รูปแบบของค่าที่คืนกลับ ซึ่งเป็น tuple ซ้อน tuple.

# Load the MNIST dataset from tensorflow.keras.datasets
(X, y), (Xtest, ytest) = mnist.load_data()

# print the shape of training examples and its label
print("shape of X", X.shape)
print("shape of y", y.shape)
print("shape of X", Xtest.shape)
print("shape of y", ytest.shape)
โค้ดทำอะไร
(X, y), (Xtest, ytest) = mnist.load_data()ฟังก์ชันคืนค่าเป็น tuple สองชั้น: ก้อนแรกคือชุดเทรน (ภาพ, ป้าย) ก้อนที่สองคือชุดทดสอบ — Keras แบ่ง train/test มาให้แล้ว เราจึงไม่ต้องเรียก train_test_split เอง (เทียบกับหน้า 36 ที่เป็นกรณีข้อมูลยังไม่แบ่ง)
X.shapeมิติของอาร์เรย์: (60000, 28, 28) อ่านว่า 60,000 ภาพ แต่ละภาพเป็นตาราง 28 แถว × 28 คอลัมน์ — ยังเป็นอาร์เรย์ 3 มิติ ซึ่ง Dense รับตรงๆ ไม่ได้
y.shape(60000,) คือเวกเตอร์ 1 มิติยาว 60,000 — ป้ายเป็นจำนวนเต็มตัวเดียวต่อภาพ ไม่ใช่ one-hot (จุดนี้เป็นเหตุผลของการเลือก loss ในหน้า 34)
Xtest.shape / ytest.shape(10000, 28, 28) และ (10000,) — ชุดทดสอบ 10,000 ภาพ ซึ่งจะใช้ใน evaluate ตอนท้ายเท่านั้น ห้ามเอามาเทรน
ผลลัพธ์ที่คาด (ตรงกับกล่อง output บนสไลด์และผลรันจริงในโน้ตบุ๊กของอาจารย์)

shape of X (60000, 28, 28)
shape of y (60000,)
shape of X (10000, 28, 28)
shape of y (10000,)

หมายเหตุ: บรรทัดที่ 3 และ 4 บนสไลด์พิมพ์ข้อความว่า "shape of X" และ "shape of y" ซ้ำกับสองบรรทัดแรก ทั้งที่ค่าที่พิมพ์เป็นของ Xtest/ytest — เป็นการก็อปข้อความมาแล้วลืมแก้ ไม่ได้กระทบผลลัพธ์

5.4 สำรวจข้อมูลก่อนทำอย่างอื่น (สไลด์หน้า 25)

Slide Data Exploration: on the left a 28 by 28 grid of raw pixel numbers for a random test image where most cells are 0 and the strokes appear as runs of 64, 128, 191 and 255, forming the shape of the digit 8; on the right the same array rendered as a black-and-white image of an 8; below are two code blocks, one printing each row with the format '{:>3}' and one displaying the image with plt.imshow and matplotlib.cm.binary
Slide: Data Exploration — ภาพเดียวกันในรูปตัวเลขและรูปภาพ (Lecture5.pdf, p.25)

หน้านี้สอนนิสัยที่ดีที่สุดอย่างหนึ่งของงาน ML คือ "ดูข้อมูลดิบด้วยตาก่อนเสมอ". ตารางตัวเลขทางซ้ายคือภาพหนึ่งภาพที่สุ่มมาจาก test set พิมพ์ออกมาทีละแถว: ช่องส่วนใหญ่เป็น 0 (พื้นหลังดำ) ส่วนเส้นของตัวเลขปรากฏเป็นแถบของค่า 64, 128, 191, 255 เรียงต่อกัน และเมื่อเอาตารางเดียวกันไปวาดด้วย plt.imshow ก็ได้ภาพเลข 8 ทางขวา. สองอย่างนี้คือข้อมูลชุดเดียวกัน — บทเรียนคือ "ภาพ" ในสายตาคอมพิวเตอร์ก็คือตารางตัวเลข 784 ตัวเท่านั้น และค่าสูงสุดที่เจอคือ 255 ซึ่งเป็นตัวเลขที่เราจะใช้หารในหน้าถัดไป.

print("The random test image:")
random_inx = np.random.choice(Xtest.shape[0])

for row in Xtest[random_inx]:
    formatted_row = ['{:>3}'.format(item) for item in row]
    print(' '.join(formatted_row))

# Display one of the images
# # reshape to (28,28)
some_digit = Xtest[random_inx].reshape(28,28)
plt.imshow(some_digit, cmap = matplotlib.cm.binary)
plt.axis("off")
plt.show()
โค้ดทำอะไร
random_inx = np.random.choice(Xtest.shape[0])Xtest.shape[0] = 10000 (มิติแรก = จำนวนภาพ); np.random.choice(10000) สุ่มจำนวนเต็ม 1 ตัวจาก 0–9999 มาเป็นดัชนีภาพ
for row in Xtest[random_inx]:Xtest[random_inx] คือภาพเดียว shape (28,28); วนลูปบนอาร์เรย์ 2 มิติจะได้ทีละแถว (28 รอบ แถวละ 28 ค่า)
formatted_row = ['{:>3}'.format(item) for item in row]จัดรูปแบบแต่ละค่าให้ชิดขวาในความกว้าง 3 ตัวอักษร (> = ชิดขวา) เพื่อให้เลข 0, 64 และ 255 เรียงเป็นคอลัมน์ตรงกัน อ่านออกเป็นรูปได้
print(' '.join(formatted_row))ต่อ 28 ค่าในแถวเป็นสตริงเดียวคั่นด้วยเว้นวรรค แล้วพิมพ์ทีละแถว
some_digit = Xtest[random_inx].reshape(28,28)บังคับให้เป็นตาราง 28×28 (ถ้าภาพถูก flatten เป็น 784 ไปแล้วก็จะกลับมาเป็นตารางได้) — imshow ต้องการอาร์เรย์ 2 มิติ
plt.imshow(some_digit, cmap = matplotlib.cm.binary)วาดอาร์เรย์เป็นภาพ; cmap (color map) แบบ binary แปลงค่า 0 เป็นขาวและค่าสูงเป็นดำ จึงได้ตัวเลขสีดำบนพื้นขาว
plt.axis("off") / plt.show()ซ่อนแกนและขีดตัวเลขรอบภาพ แล้วสั่งแสดงผล
ผลลัพธ์ที่คาด (จากผลรันจริงในโน้ตบุ๊กของอาจารย์)

พิมพ์ข้อความ The random test image: แล้วตามด้วยตาราง 28 บรรทัด เช่นบรรทัดที่มีเส้นของตัวเลขจะอ่านได้ว่า 0 0 ... 0 128 128 191 255 255 191 0 ... 0 ส่วนบรรทัดบนสุดและล่างสุดเป็น 0 ทั้งแถว; จากนั้นแสดงภาพขาวดำขนาดเล็กของตัวเลขนั้น (ในสไลด์คือเลข 8)

5.5 เตรียมข้อมูล: reshape แล้ว scale (สไลด์หน้า 26)

Slide Data Preparation: for images, reshape into the shape the network expects such as one long vector ignoring the batch size, and scale so all values are in the [0,1] interval, noting that the data is reinterpreted using row-major semantics; the code reshapes X to (60000, 28*28), casts to float32 and divides by 255, does the same for Xtest with (10000, 28*28), and the output box shows shape of X (60000, 784) and shape of Xtest (10000, 784)
Slide: Data Preparation — reshape เป็นเวกเตอร์ 784 และ scale เป็น 0–1 (Lecture5.pdf, p.26)

สองสิ่งที่ต้องทำกับภาพก่อนป้อนเข้า Dense: (ก) reshape ให้เป็น "one long vector ignoring the batch size" คือยืดตาราง 28×28 เป็นเวกเตอร์ยาว 784 โดยไม่ยุ่งกับมิติแรกที่เป็นจำนวนตัวอย่าง และ (ข) scale ให้ทุกค่าอยู่ในช่วง \([0,1]\). สไลด์หมายเหตุไว้ว่า "the data is reinterpreted using row-major semantics" — คือการยืดทำโดยไล่ทีละแถว (แถวที่ 1 ทั้ง 28 ค่า ต่อด้วยแถวที่ 2 ...) ซึ่งเป็นระเบียบมาตรฐานของ numpy; ขอแค่ทุกภาพยืดด้วยระเบียบเดียวกัน เครือข่ายก็เรียนได้ เพราะพิกเซลตำแหน่งเดิมจะไปอยู่ตำแหน่งเดิมในเวกเตอร์เสมอ.

X = X.reshape(60000, 28 * 28)
X = X.astype('float32') / 255
print("shape of X", X.shape)

Xtest = Xtest.reshape((10000, 28 * 28))
Xtest = Xtest.astype('float32') / 255
print("shape of Xtest", Xtest.shape)
โค้ดทำอะไร
X.reshape(60000, 28 * 28)เปลี่ยนมิติจาก (60000,28,28) เป็น (60000,784) โดยจำนวนค่าทั้งหมดต้องเท่าเดิม (\(60000\times784\) = \(60000\times28\times28\)); เขียน 28*28 แทน 784 เพื่อให้อ่านออกว่ามาจากไหน
X.astype('float32')แปลงชนิดข้อมูลจากจำนวนเต็ม uint8 (0–255) เป็นทศนิยม 32 บิต — จำเป็นเพราะถ้าหารแบบจำนวนเต็มจะได้ 0 หมด และ float32 ใช้หน่วยความจำครึ่งเดียวของ float64 โดยความแม่นยำเพียงพอสำหรับ deep learning
/ 255min-max scaling ที่ min = 0 และ max = 255 (Lesson 1 §4): \(255/255=1\), \(128/255=0.502\), \(64/255=0.251\), \(0/255=0\) — ทุกค่าจึงอยู่ใน \([0,1]\) ทำให้ \(z\) ไม่ใหญ่เกินตั้งแต่ layer แรก
Xtest.reshape((10000, 28 * 28))ทำแบบเดียวกันกับชุดทดสอบ (สังเกตว่าบรรทัดนี้ส่ง shape เป็น tuple ในวงเล็บ ส่วนบรรทัดของ X ส่งเป็นสองอาร์กิวเมนต์ — reshape รับได้ทั้งสองแบบ ผลเหมือนกัน); สำคัญ: ต้องแปลงชุด test ด้วยวิธีเดียวกันเป๊ะ ไม่เช่นนั้นโมเดลจะเห็นข้อมูลคนละสเกล
ผลลัพธ์ที่คาด (ตรงกับกล่อง output บนสไลด์และผลรันจริง)

shape of X (60000, 784)
shape of Xtest (10000, 784)

5.6 เตรียม label: one-hot หรือใช้จำนวนเต็มตรงๆ (สไลด์หน้า 27)

Slide Preparing the Labels: in multi-class classification it is generally good practice to transform the target attribute into one hot encoding which aligns with the network's output probabilities for each class; code calls keras.utils.to_categorical on y and ytest and prints y[:10]; the integer labels [5 0 4 1 9 2 1 3 1 4] become a 10 by 10 matrix of zeros with a single 1 in each row; the last bullet says many times we use integer labels directly without one-hot encoding since it is convenient, saves memory space and gives better performance especially with mutually exclusive integer labels
Slide: Preparing the Labels — one-hot encoding กับการใช้ integer label ตรงๆ (Lecture5.pdf, p.27)

สไลด์ให้ทั้งสองทางเลือกพร้อมเหตุผล. ทางแรก: ใน multi-class classification "it is generally a good practice" ที่จะแปลง target เป็น one-hot encoding เพราะมันสอดคล้องกับ output ของเครือข่ายที่เป็นความน่าจะเป็นของแต่ละคลาส (output มี 10 ค่า ⇒ ป้ายก็ควรมี 10 ค่า จะได้เทียบกันตรงๆ ได้). ตัวอย่างบนสไลด์ชัดมาก: ป้าย 10 ตัวแรก [5 0 4 1 9 2 1 3 1 4] ถูกแปลงเป็นเมทริกซ์ 10×10 ที่แถวแรกเป็น [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.] (เลข 1 อยู่ตำแหน่งที่ 5 นับจาก 0 ⇒ แทนป้าย "5") แถวที่สองเป็น [1. 0. ...] (ป้าย "0") และแถวที่สามมี 1 ที่ตำแหน่งที่ 4 (ป้าย "4"). ทางที่สอง ซึ่งเป็นทางที่อาจารย์ใช้จริงในโน้ตบุ๊ก: "many times we use integer labels directly" เพราะสะดวก ประหยัดหน่วยความจำ และให้ประสิทธิภาพดีกว่าเมื่อป้ายเป็นจำนวนเต็มที่คลาสไม่ทับซ้อนกัน (mutually exclusive).

# using keras package calling to_categorical() function
# print(y[:10])  # [5 0 4 1 9 2 1 3 1 4]
y = keras.utils.to_categorical(y)
ytest = keras.utils.to_categorical(ytest)
print(y[:10])
โค้ดทำอะไร
# print(y[:10]) # [5 0 4 1 9 2 1 3 1 4]คอมเมนต์ที่บอกหน้าตาของป้ายก่อนแปลง: เป็นจำนวนเต็มตัวเดียวต่อภาพ shape (60000,)
y = keras.utils.to_categorical(y)แปลงเป็น one-hot: shape เปลี่ยนจาก (60000,) เป็น (60000, 10); ฟังก์ชันเดาจำนวนคลาสจากค่าสูงสุด (9) ⇒ ได้ 10 คอลัมน์ (ระบุเองได้ด้วย num_classes=10)
ytest = keras.utils.to_categorical(ytest)ต้องแปลงชุด test ด้วย มิฉะนั้น evaluate จะเทียบป้ายคนละรูปแบบกับ output แล้วพัง
print(y[:10])พิมพ์ 10 แถวแรกหลังแปลง เพื่อยืนยันว่าแต่ละแถวมีเลข 1 เพียงตัวเดียว
(ทางเลือกที่อาจารย์ใช้จริง)ข้ามสองบรรทัด to_categorical ไปเลย แล้วไปตั้ง loss='sparse_categorical_crossentropy' ตอน compile แทน — ผลลัพธ์ loss เท่ากันเป๊ะ (พิสูจน์ด้วยตัวเลขในหัวข้อ 6)
ผลลัพธ์ที่คาด (ตรงกับกล่องผลลัพธ์บนสไลด์)

ก่อนแปลง: [5 0 4 1 9 2 1 3 1 4] · หลังแปลงเป็นเมทริกซ์ 10 แถว เช่น
[[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]] ...

ตรวจง่ายๆ ว่าถูก: แถวที่ 5 (ป้ายจริง = 9) ต้องมี 1 อยู่ตำแหน่งสุดท้าย ✓

จุดที่ข้อสอบเติมโค้ดมักถาม (หน้า 24–27)

5.7 ขั้นที่ 2: สร้างโมเดล — สองวิธีที่ให้ผลเหมือนกัน (สไลด์หน้า 28)

Slide Build the Model with Sequential API: Alternative 1 builds the model through its constructor by passing a list of layers, Alternative 2 creates the instance and calls .add to append layers; the code shows both forms with Dense(16, relu, input_shape=(28*28,)), Dense(16, relu) and Dense(10, softmax); notes explain that a Dense layer contains a given number of units densely connected to the previous layer and is essential for learning complex patterns, that specifying input_shape is optional since layers adjust automatically during fit but is advised so model.summary shows the weights, and that training example or batch size is not required since any number of images can be passed into the Input layer
Slide: Build the Model with Sequential API — Alternative 1 และ Alternative 2 (Lecture5.pdf, p.28)

โครงสร้างที่จะใช้กับ MNIST คือ 784 → 16 → 16 → 10 และอาจารย์ให้เขียนได้สองแบบซึ่งให้โมเดลเหมือนกันทุกประการ. Alternative 1: สร้างผ่าน constructor โดยส่ง list ของ layer เข้าไปตอนสร้างเลย. Alternative 2: สร้างอินสแตนซ์เปล่าก่อนแล้วเรียก .add ต่อทีละชั้น (แบบที่เราใช้มาตลอดในหน้า 13–14, 17, 22). เลือกแบบไหนก็ได้ตามความถนัด แต่ข้อสอบอาจให้ดูโค้ดแบบหนึ่งแล้วถามว่าเทียบเท่ากับแบบไหน.

# input_shape=(28 * 28,) - the input of each training data example
# alternative 1
model = models.Sequential([Dense(16, activation='relu', input_shape=(28 * 28,)),
                           Dense(16, activation='relu'),
                           Dense(10, activation='softmax')])

# alternative 2
model = models.Sequential()
model.add(Dense(16, activation='relu', input_shape=(28*28,)))
model.add(Dense(16, activation='relu'))
model.add(Dense(10, activation='softmax'))
โค้ดทำอะไร
models.Sequential([ ... ])Alternative 1: ส่ง list ของ layer เรียงตามลำดับที่ข้อมูลจะไหลผ่าน — สังเกตวงเล็บเหลี่ยมและจุลภาคคั่นระหว่าง layer
Dense(16, activation='relu', input_shape=(28 * 28,))hidden layer แรก 16 node; input_shape=(784,) คือ shape ของหนึ่งตัวอย่างหลัง flatten แล้ว — จุลภาคท้ายบอกว่าเป็น tuple มิติเดียว
Dense(16, activation='relu')hidden layer ที่สอง — Keras รู้เองว่า input มี 16 ค่าจาก layer ก่อนหน้า
Dense(10, activation='softmax')output layer: 10 node เท่ากับจำนวนคลาส และ softmax เพื่อแปลงคะแนนดิบ 10 ค่าเป็นความน่าจะเป็นที่รวมกันได้ 1 (สูตร \(\hat y_k = e^{z_k}/\sum_j e^{z_j}\) จาก Lesson 3 §6)
model = models.Sequential()
model.add(...)
Alternative 2: ผลลัพธ์เหมือน Alternative 1 ทุกประการ ต่างแค่รูปแบบการเขียน; ข้อดีคือแทรก/คอมเมนต์ layer ทีละบรรทัดได้ง่าย

สามบันทึกใต้โค้ดบนสไลด์เป็นเนื้อหาที่ออกสอบเป็นข้อความได้ตรงๆ. หนึ่ง: Dense layer "contains a given number of units that are densely connected to the previous layer" คือ node ทุกตัวเชื่อมกับ node ทุกตัวของชั้นก่อนหน้า (จึงมีพารามิเตอร์ \(s_l\times(s_{l-1}+1)\)) และ layer นี้ "essential for learning complex patterns in the data through adjusting its parameters during training". สอง: การระบุ input_shape เป็นทางเลือก เพราะตอน fit() ข้อมูลจะถูกป้อนเข้ามาและ layer ปรับตัวเอง แต่อาจารย์แนะนำให้ระบุไว้ เพราะถ้าไม่ระบุ เรียก model.summary() ก่อนเทรนจะยังไม่เห็นจำนวน weight ของแต่ละชั้น. สาม: ไม่ต้องบอกจำนวนตัวอย่างหรือ batch size ใน input_shape เพราะ "we can pass any number of images into Input layer" — นี่คือเหตุผลที่ output shape ใน summary ขึ้นต้นด้วย None เสมอ.

5.8 อ่าน model.summary() และนับพารามิเตอร์ด้วยมือ (สไลด์หน้า 29)

Keras model.summary output for the sequential model: dense (Dense) with output shape (None, 16) and 12,560 params annotated as 16 x (784+1), dense_1 (Dense) with (None, 16) and 272 params annotated as 16 x (16+1), dense_2 (Dense) with (None, 10) and 170 params annotated as 10 x (16+1), and Total params 13,002 with Trainable params 13,002 annotated as 12560 + 272 + 170 and Non-trainable params 0
Slide: Concise Summary of the Network Topology — ตัวเลขจาก model.summary() พร้อมที่มา (Lecture5.pdf, p.29)

สไลด์อธิบายกฎการนับไว้เป็นประโยคว่า จำนวนพารามิเตอร์ของชั้นหนึ่งได้จาก "multiplying the number of neurons in that layer by the number of neurons in the previous layer (representing the weights connecting them) adding one (representing the associated biases)" — คือ \(s_l\times(s_{l-1}+1)\) ซึ่งเป็นสูตรเดียวกับที่เราได้มาใน Lesson 3 §4. บนสไลด์มีลูกศรกำกับที่มาของทั้งสามตัวเลขไว้ให้ครบ ลองคำนวณเองแล้วเทียบ.

คำนวณ: พารามิเตอร์ของโมเดล MNIST 784 → 16 → 16 → 10
กฎ: จำนวนพารามิเตอร์ของชั้น \(l\) = (จำนวน node ของชั้นนี้) × (จำนวน node ของชั้นก่อนหน้า + 1)
  1. ขั้นที่ 1: ชั้นแรก dense ต่อจาก input 784 ค่า ไปยัง 16 node
    $$16\times(784+1) = 16\times785 = 12{,}560$$

    ทำไมต้อง +1: แต่ละ node มี bias หนึ่งตัวของตัวเอง (node ละ 784 weight + 1 bias)

  2. ขั้นที่ 2: ชั้นที่สอง dense_1 จาก 16 ไป 16
    $$16\times(16+1) = 16\times17 = 272$$
  3. ขั้นที่ 3: ชั้น output dense_2 จาก 16 ไป 10
    $$10\times(16+1) = 10\times17 = 170$$
  4. ขั้นที่ 4: รวมทั้งโมเดล
    $$12{,}560 + 272 + 170 = 13{,}002$$

ตรงกับ Total params: 13,002 และ Trainable params: 13,002 บนสไลด์ทุกหลัก ส่วน Non-trainable params: 0 เพราะโมเดลนี้ไม่มี BatchNormalization (ถ้ามี จะมี moving mean/variance เป็น non-trainable อย่างที่คำนวณไว้ในหัวข้อ 3.2). ข้อผิดพลาดที่พบบ่อยที่สุด: ลืมบวก 1 แล้วได้ \(16\times784=12{,}544\) — ต่างจากคำตอบจริง 16 พอดี (= จำนวน bias)

อีกอย่างที่ควรสังเกตจากตัวเลขชุดนี้: ชั้นแรกกินพารามิเตอร์ถึง 96.6% ของทั้งโมเดล (\(12{,}560/13{,}002\)) เพราะมันต่อกับ input 784 ตัว. ถ้าจะลดขนาดโมเดล การลดความกว้างของชั้นแรกได้ผลมากกว่าการตัดชั้นหลังออกเสมอ.

5.9 ขั้นที่ 3: compile (สไลด์หน้า 30)

Slide Compile the model: before training, config three things as part of the compilation step, namely a loss function (cost or objective function) which is the quantity to be minimized during training, an optimizer which determines how the network will be updated based on the loss function, and metrics which are the measures of success monitored during training and testing, here only accuracy, with a note that since the metric is not intended to be optimized during training it does not need to be differentiable; the code calls model.compile with optimizer='adam', loss='categorical_crossentropy' with sparse_categorical_crossentropy commented as an alternative, and metrics=['accuracy']
Slide: Compile the model — สามสิ่งที่ต้องตั้งก่อนเทรน (Lecture5.pdf, p.30)

ขั้น compile ตั้งค่า 3 อย่างตามที่สไลด์ระบุ. หนึ่ง loss function (เรียกได้อีกว่า cost function หรือ objective function) = "the quantity to be minimized during training" คือปริมาณที่จะถูกทำให้น้อยที่สุด. สอง optimizer = "determine how the network will be updated based on the loss function" คือวิธีที่ weight จะถูกอัปเดตจาก gradient (Adam, SGD, RMSProp จาก Lesson 4 §5). สาม metrics = "the measures of success to be monitored during training and testing" — ที่นี่สนใจแค่ accuracy. อาจารย์หมายเหตุประเด็นที่ลึกและออกสอบได้: เพราะ metric ไม่ได้ถูก optimize ระหว่างเทรน มันจึง "doesn't need to be differentiable" — ต่างจาก loss ที่ต้องหาอนุพันธ์ได้เพราะ backprop ต้องใช้. accuracy เป็นตัวอย่างที่ดี: มันคือการนับว่าทายถูกกี่ตัว ซึ่งเป็นฟังก์ชันขั้นบันได หาอนุพันธ์แล้วได้ 0 ทุกจุด จึงใช้เป็น loss ไม่ได้ แต่ใช้เป็น metric ได้สบาย.

# compile the model
model.compile(optimizer='adam',
              loss='categorical_crossentropy',  #loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
โค้ดทำอะไร
optimizer='adam'ใช้ Adam ด้วยค่า default (learning_rate=0.001, beta_1=0.9, beta_2=0.999); ส่งเป็นสตริงได้เมื่อไม่ต้องการแก้ไฮเปอร์พารามิเตอร์ — ถ้าต้องการแก้ให้ส่งเป็นออบเจ็กต์แบบหน้า 34
loss='categorical_crossentropy'ใช้เมื่อ label ถูกแปลงเป็น one-hot แล้ว (ตามหน้า 27 ทางเลือกแรก) — สูตร \(-\sum_k y_k\ln\hat y_k\)
#loss='sparse_categorical_crossentropy'บรรทัดทางเลือกที่ถูกคอมเมนต์ไว้: ใช้เมื่อ label ยังเป็น จำนวนเต็ม 0–9 — ต้องเลือกให้ตรงกับรูปแบบ label ที่เตรียมไว้ มิฉะนั้นจะเกิด shape mismatch error
metrics=['accuracy']เฝ้าดูความแม่นยำ; ใส่ได้หลายตัวเช่น ['accuracy','AUC']; ชื่อคีย์ที่จะไปโผล่ใน history คือ accuracy และ val_accuracy
ผลลัพธ์ที่คาด

compile() ไม่คืนค่าและไม่พิมพ์อะไร — มันแค่ผูก loss/optimizer/metric เข้ากับโมเดล. สัญญาณว่าทำถูกคือหลังจากนี้เรียก fit() แล้วไม่ error; ถ้าเลือก loss ไม่ตรงกับรูปแบบ label จะได้ข้อความประเภท "Shapes (32, 1) and (32, 10) are incompatible" ตอนเริ่มเทรน

5.10 ขั้นที่ 4: fit และการนับจำนวน mini-batch (สไลด์หน้า 31)

Keras training call history = model.fit(X, y, epochs=40, validation_split=0.2 batch_size=64) with a worked calculation that the number of mini-batches equals number of training examples divided by batch size = 48,000/64 = 750 batches, matching the 750/750 progress bar in the epoch logs which show Epoch 1/40 with accuracy 0.3378 loss 2.0053 val_accuracy 0.7753 val_loss 0.8860 improving by Epoch 5/40 to accuracy 0.8961 loss 0.3759 val_accuracy 0.9076 val_loss 0.3250; a note says the fit method returns a history object and history.history.keys() gives loss, accuracy, val_loss, val_accuracy
Slide: Launch the training — สูตรนับ mini-batch พร้อม log จริง (Lecture5.pdf, p.31)

หน้านี้คือหน้าที่คำนวณออกสอบชัวร์ที่สุดของครึ่งหลัง เพราะอาจารย์เขียนสูตรไว้เต็มๆ บนสไลด์: จำนวน mini-batch = จำนวน training example หารด้วย batch size = \(48{,}000/64 = 750\) batches และเทียบให้ดูกับตัวเลข 750/750 ที่ปรากฏในแถบความคืบหน้าของ log จริงเพื่อยืนยันว่าตรงกัน. คำถามที่ต้องตอบให้ได้คือ "ทำไมเป็น 48,000 ทั้งที่ MNIST มี 60,000?"

# train the model
history = model.fit(X, y, epochs=40, validation_split=0.2, batch_size=64)
โค้ดทำอะไร
history = model.fit(...)เริ่มเทรนจริง และเก็บค่าที่คืนกลับไว้ในตัวแปร history; สไลด์หมายเหตุว่า ".fit method return history object containing the specified training (also validation metrics) from the metrics parameter passed to the compile method" — คือมีเฉพาะ metric ที่เราสั่งไว้ตอน compile เท่านั้น
X, yอาร์กิวเมนต์สองตัวแรกคือข้อมูลและป้าย ส่งตามตำแหน่ง (positional) ไม่ต้องใส่ชื่อ
epochs=40วนข้อมูลทั้งชุด 40 รอบ; หนึ่ง epoch = เห็นทุกตัวอย่างครบหนึ่งครั้ง
validation_split=0.2ให้ Keras ตัด 20% ท้ายของข้อมูลเทรน ออกไปเป็น validation set ให้อัตโนมัติ — ต่างจาก validation_data=(Xval,yval) ในหน้า 22 ที่เราเตรียมชุดแยกมาเอง; ใช้พร้อมกันทั้งสองตัวไม่ได้
batch_size=64อัปเดต \(\theta\) หนึ่งครั้งต่อ 64 ตัวอย่าง (mini-batch gradient descent จาก Lesson 4 §3); ถ้าไม่ระบุ ค่า default ของ Keras คือ 32
ข้อสังเกต: สไลด์หน้า 31 พิมพ์โค้ดตกจุลภาค

บรรทัดบนสไลด์เขียนว่า model.fit(X, y, epochs=40, validation_split=0.2 batch_size=64) — ขาดจุลภาคระหว่าง 0.2 กับ batch_size ซึ่งถ้าพิมพ์ตามจะได้ SyntaxError ทันที. โค้ดที่ถูกต้องคือใส่จุลภาคอย่างในกล่องด้านบน (โน้ตบุ๊กของอาจารย์ก็เขียนแบบมีจุลภาค). ถ้าข้อสอบให้แก้โค้ดที่ผิด จุดนี้คือคำตอบ

คำนวณ: จำนวน mini-batch ต่อ epoch และจำนวนครั้งที่ \(\theta\) ถูกอัปเดตทั้งหมด
MNIST มี 60,000 ตัวอย่าง; validation_split=0.2; batch_size=64; epochs=40
  1. ขั้นที่ 1: หักส่วน validation ออกก่อน — นี่คือขั้นที่คนลืมบ่อยที่สุด
    $$60{,}000\times0.2 = 12{,}000\ (\text{validation}),\qquad 60{,}000-12{,}000 = 48{,}000\ (\text{ใช้เทรนจริง})$$

    validation set ไม่ถูกใช้คำนวณ gradient จึงไม่นับรวมในจำนวน batch ที่อัปเดต

  2. ขั้นที่ 2: แบ่ง 48,000 ตัวอย่างเป็นก้อนละ 64
    $$\frac{48{,}000}{64} = 750\ \text{batches}$$

    ตรงกับแถบ 750/750 ใน log ของสไลด์ ✓ (ถ้าหารไม่ลงตัวให้ปัดขึ้น เพราะก้อนสุดท้ายที่ไม่เต็มก็ยังถูกใช้)

  3. ขั้นที่ 3: คูณด้วยจำนวน epoch เพื่อหาจำนวนก้าว gradient descent ทั้งหมด
    $$750\times40 = 30{,}000\ \text{ครั้ง}$$
  4. ขั้นที่ 4: เทียบกับ batch gradient descent แบบเต็มชุด (อัปเดต 1 ครั้งต่อ epoch)
    $$40\ \text{ครั้ง เทียบกับ } 30{,}000\ \text{ครั้ง} \Rightarrow \text{มากกว่า }750\text{ เท่า}$$

    นี่คือเหตุผลที่ mini-batch เรียนเร็วกว่ามากต่อหนึ่ง epoch

คำตอบ: 750 batch ต่อ epoch และอัปเดต \(\theta\) รวม 30,000 ครั้ง. ตัวอย่างการปัดขึ้น: ถ้า batch_size = 128 จะได้ \(48{,}000/128 = 375\) พอดี แต่ถ้า batch_size = 32 กับ test set 10,000 ตัวอย่าง จะได้ \(\lceil 10{,}000/32\rceil = 313\) batch — ซึ่งตรงกับเลข 313/313 ที่เห็นตอน evaluate ในหน้า 33

ผลลัพธ์ที่คาด (log จริงจากสไลด์หน้า 31)

Epoch 1/40 — 750/750 — 26s 34ms/step - accuracy: 0.3378 - loss: 2.0053 - val_accuracy: 0.7753 - val_loss: 0.8860
Epoch 3/40 — 750/750 — 27s 35ms/step - accuracy: 0.8608 - loss: 0.5015 - val_accuracy: 0.8838 - val_loss: 0.4069
Epoch 5/40 — 750/750 — 28s 37ms/step - accuracy: 0.8961 - loss: 0.3759 - val_accuracy: 0.9076 - val_loss: 0.3250
และ history.history.keys() คืน dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy']) — สี่คีย์นี้มาจาก metrics=['accuracy'] ที่ตั้งไว้ตอน compile บวกกับ loss และคู่ val_ ของทั้งสอง

5.11 พล็อตเส้นโค้งการเรียนรู้ (สไลด์หน้า 32)

Slide Plot the Performance of the Model: matplotlib code building an epochs range from the length of history.history['loss'] and plotting loss and accuracy with val_loss and val_accuracy commented out; two result charts are shown, one trained for 5 epochs where loss falls from about 1.55 to 0.37 and accuracy climbs from about 0.53 to 0.90 with the two validation curves still close to the training ones, and one trained for 40 epochs where training loss keeps falling to about 0.1 while val_loss flattens and rises slightly around 0.2, with a link to the TensorFlow overfit and underfit tutorial
Slide: Plot the Performance of the Model — เทียบ 5 epochs กับ 40 epochs (Lecture5.pdf, p.32)

หลังเทรนเสร็จ ข้อมูลทุก epoch อยู่ใน history.history ซึ่งเป็น dictionary ธรรมดา เราจึงพล็อตเองได้ด้วย matplotlib. ประเด็นที่อาจารย์อยากให้เห็นอยู่ที่กราฟสองรูปทางขวา: รูป 5 epochs เส้น loss ดิ่งจากราว 1.55 ลงมา 0.37 และ accuracy ไต่จากราว 0.53 ขึ้นไป 0.90 โดยเส้นของ validation ยังเกาะติดกับเส้นของ training — สถานการณ์ปกติดี. ส่วนรูป 40 epochs เส้น loss ของ training ยังลดต่อไปเรื่อยๆ จนเหลือราว 0.1 แต่ val_loss หยุดลดแถว 0.2 แล้วเริ่มกระดกขึ้นและแกว่ง — นี่คือหน้าตาของ overfitting: โมเดลเริ่มจำข้อมูลเทรนแทนที่จะเรียนรูปแบบทั่วไป (ลิงก์ที่สไลด์ให้ไว้คือบทเรียน overfit and underfit ของ TensorFlow). จุดที่ควรหยุดเทรนคือจุดที่ val_loss ต่ำสุด — ในการรันจริงของโน้ตบุ๊กอาจารย์คือ epoch ที่ 13 ด้วย val_loss 0.1642 ซึ่งเป็นแนวคิดที่ Lesson 6 จะทำให้อัตโนมัติด้วย EarlyStopping.

#plotthelossandaccuracy   (คอมเมนต์ "plot the loss and accuracy")
epochs = range(1, len(history.history['loss']) + 1)
plt.plot(epochs, history.history['loss'], label = 'loss')
#plt.plot(epochs, history.history['val_loss'], label = 'val_loss')
plt.plot(epochs, history.history['accuracy'], label = 'acc')
#plt.plot(epochs, history.history['val_accuracy'], label = 'val_acc')
plt.title('Loss, Accuracy vs Epochs')
plt.xlabel("Epochs")
plt.ylabel("Accuracy")
plt.legend()
plt.show()
โค้ดทำอะไร
epochs = range(1, len(history.history['loss']) + 1)สร้างแกน x เป็น 1, 2, …, N โดย N = จำนวน epoch ที่เทรนจริง; ใช้ len() แทนการเขียนเลข 40 ตายตัว เพื่อให้โค้ดใช้ได้แม้หยุดเทรนกลางคัน; ต้อง +1 เพราะ range ไม่รวมค่าสุดท้าย
history.history['loss']ลิสต์ของค่า loss หนึ่งค่าต่อหนึ่ง epoch (ความยาวเท่ากับจำนวน epoch) — คีย์ที่ใช้ได้คือ loss, accuracy, val_loss, val_accuracy ตามที่ keys() บอกในหน้า 31
plt.plot(epochs, ..., label = 'loss')วาดเส้น; label คือชื่อที่จะไปโผล่ใน legend
#plt.plot(epochs, history.history['val_loss'], ...)สองบรรทัดของ val_ ถูกคอมเมนต์ไว้ในสไลด์ — ถ้าเปิดใช้จะได้กราฟสี่เส้นเหมือนสองรูปตัวอย่างทางขวา ซึ่งจำเป็นถ้าต้องการดู overfitting
plt.title / plt.xlabel / plt.ylabelตั้งหัวเรื่องและชื่อแกน (สังเกตว่าแกน y ถูกตั้งชื่อว่า "Accuracy" ทั้งที่มีเส้น loss ปนอยู่ด้วย — เป็นชื่อที่ไม่ตรงนัก แต่ไม่กระทบการวาด)
plt.legend() / plt.show()legend() แสดงกล่องคำอธิบายสีเส้นโดยอ่านจาก label; show() สั่งแสดงผลจริง ถ้าลืมบรรทัดนี้ในสคริปต์ธรรมดาจะไม่มีอะไรปรากฏ
ผลลัพธ์ที่คาด

ได้กราฟหนึ่งรูปชื่อ Loss, Accuracy vs Epochs ที่มีสองเส้น: loss ลดลงต่อเนื่องและ acc เพิ่มขึ้นต่อเนื่อง. ถ้าเปิดสองบรรทัดที่คอมเมนต์ไว้ จะได้สี่เส้นเหมือนกราฟตัวอย่างบนสไลด์ — และจะเห็นว่า val_loss ของการเทรน 40 epoch แยกตัวขึ้นจาก loss ตั้งแต่ราว epoch ที่ 10 เป็นต้นไป

5.12 ขั้นที่ 5: ประเมินผลและทำนาย (สไลด์หน้า 33)

Slide Check the Model on the Test Set: code calling loss, acc = model.evaluate(Xtest, ytest) then printing test_loss and test_acc, with output 313/313 in 1s at 3ms/step showing loss 0.1992 and accuracy 0.9490, then test_loss 0.19921429455280304 and test_acc 0.9490000009536743; a second block does inference with pred = model.predict(Xtest[:1]) noting shape (1,10), prints pred and takes np.argmax(pred)
Slide: Check the Model on the Test Set — evaluate และ predict พร้อมผลจริง (Lecture5.pdf, p.33)

ขั้นสุดท้ายมีสองเรื่องที่ต้องแยกให้ออก. evaluate ใช้เมื่อเรามีเฉลย และอยากรู้ว่าโมเดลทำได้ดีแค่ไหน; predict คือสิ่งที่สไลด์เรียกว่า Inference — "using a model after training to make the predictions" คือใช้โมเดลกับข้อมูลใหม่ที่ไม่มีเฉลย.

loss, acc = model.evaluate(Xtest, ytest)
print('test_loss:', loss)
print('test_acc:', acc)

# make predictions from the model
pred = model.predict(Xtest[:1])   # shape (1,10)
print(pred)
np.argmax(pred)
โค้ดทำอะไร
loss, acc = model.evaluate(Xtest, ytest)รัน forward pass บนชุดทดสอบทั้ง 10,000 ภาพแล้วคืน [loss, metric] ตามลำดับที่ตั้งไว้ตอน compile — ไม่มีการอัปเดต weight ใดๆ; แถบความคืบหน้าแสดง 313/313 เพราะ \(\lceil 10{,}000/32\rceil=313\) (batch_size ของ evaluate ค่า default = 32)
print('test_loss:', loss)พิมพ์ค่า loss บนชุดทดสอบ — ตัวเลขที่ควรรายงาน ไม่ใช่ loss ของชุดเทรน
pred = model.predict(Xtest[:1])Xtest[:1] คือการ slice เอาภาพแรกโดยยังคงมิติ batch ไว้ ⇒ shape (1,784); ถ้าเขียน Xtest[0] จะได้ shape (784,) ซึ่ง predict ไม่รับ. ผลลัพธ์มี shape (1,10) ตามคอมเมนต์บนสไลด์ = 1 ตัวอย่าง × 10 คลาส
print(pred)แสดงเวกเตอร์ความน่าจะเป็น 10 ค่าที่ออกจาก softmax ซึ่งรวมกันได้ 1 เสมอ
np.argmax(pred)คืนดัชนีของค่าที่มากที่สุด ซึ่งในงานนี้ดัชนีตรงกับชื่อคลาสพอดี (ดัชนี 7 = เลข 7); ถ้าทำนายหลายภาพพร้อมกันต้องระบุแกนเป็น np.argmax(pred, axis=1) มิฉะนั้นจะได้ดัชนีเดียวของทั้งเมทริกซ์
ผลลัพธ์ที่คาด (ตัวเลขจริงจากสไลด์หน้า 33 และผลรันในโน้ตบุ๊กของอาจารย์)

313/313 ━━━━ 1s 3ms/step - loss: 0.1992 - accuracy: 0.9490
test_loss: 0.19921429455280304
test_acc: 0.9490000009536743
ส่วนผลของ predict บนภาพทดสอบภาพแรก (เฉลยคือเลข 7) ได้
[[4.31e-07 7.02e-10 1.07e-05 2.46e-04 9.09e-08 2.91e-06 5.42e-11 9.9962544e-01 2.81e-06 1.12e-04]]
np.argmax(pred) → 7 — ค่าที่ตำแหน่ง 7 คือ 0.99963 (มั่นใจ 99.96%) ส่วนคลาสอื่นรวมกันได้แค่ 0.00037 และผลรวมทั้ง 10 ค่าเท่ากับ 1 พอดีตามคุณสมบัติของ softmax

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
การนับพารามิเตอร์และนับ mini-batch ใน Keras ไม่ใช่ความรู้ใหม่ แต่คือสูตรเดิมจาก Lesson 3 (ขนาดของ \(\theta\)) และ Lesson 4 (batch size) ที่ใส่ตัวเลขจริง — Keras เป็นเพียงการ implement สิ่งที่เราคำนวณด้วยมือมาแล้วทั้งวิชา ถ้าสูตรมือแม่น โค้ดก็จะไม่หลง.
เปลี่ยนแล้วเกิดอะไร
เปลี่ยนโครงเป็น 784→32→10 (ตัด hidden ออกหนึ่งชั้นแต่ขยายชั้นที่เหลือ): \(32\times785 = 25{,}120\) บวก \(10\times33 = 330\) รวม 25,450 — มากกว่า 13,002 เกือบสองเท่าทั้งที่ layer น้อยลง เพราะชั้นที่ติดกับ input 784 ตัวยิ่งกว้างยิ่งโตเร็ว. เปลี่ยน batch_size จาก 64 เป็น 128: batch ต่อ epoch ลดจาก 750 เหลือ 375 ⇒ อัปเดตน้อยลงครึ่งหนึ่ง ต้องเพิ่ม epoch หรือ learning rate ชดเชย.
เชื่อมกับ
ความกว้างกับความลึกส่งผลต่อจำนวนพารามิเตอร์คนละแบบ (Lesson 3 §7, Lesson 4 §2); และ history ที่ได้จาก fit คือวัตถุดิบของ callback ทั้งหมดใน Lesson 6.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 11Keras Workflow
ตามสไลด์หน้า 21 ลำดับขั้นตอนมาตรฐานของการทำงานด้วย Keras คือข้อใด?
คำถาม 12นับพารามิเตอร์
โมเดล Keras โครงสร้าง 784→16→16→10 (Dense ทุกชั้น) จำนวนพารามิเตอร์ของชั้นแรกเท่ากับเท่าไร?
คำถาม 13นับ Mini-batch (อ่านโค้ด)
เรียก model.fit(X, y, epochs=40, validation_split=0.2, batch_size=64) บน MNIST 60,000 ตัวอย่าง จำนวน mini-batch ต่อ epoch เท่ากับเท่าไร?

6. Compile Options, Softmax Layer และวิธีแบ่งข้อมูล

หัวข้อ 5 ใช้ optimizer='adam' กับ loss=... โดยยังไม่ได้อธิบายว่าจะเลือกอย่างไรให้ถูกกับปัญหา. สไลด์หน้า 34 ให้ตารางที่ตอบคำถามนี้ทั้งตาราง และเป็นตารางที่ควรจำให้ได้ทั้งหมดเพราะออกสอบได้ทุกแถว.

Slide More Compile Options: an Adam optimizer configured as a python object with learning_rate 0.001, beta_1 0.9 and beta_2 0.999; a table of common last-layer activation and loss per problem type listing binary classification with sigmoid and binary_crossentropy, multiclass single-label with softmax and categorical_crossentropy, multiclass multilabel with sigmoid and binary_crossentropy, regression to arbitrary values with None and mse, regression to values between 0 and 1 with sigmoid and mse or binary_crossentropy; a second table compares categorical_crossentropy whose true label is a one-hot vector against sparse_categorical_crossentropy whose true label is an integer index, with identical final loss values and different memory efficiency
Slide: More Compile Options — ตาราง Last-layer activation กับ Loss และการเทียบ sparse (Lecture5.pdf, p.34)

อย่างแรกที่สไลด์แสดงคือการตั้ง optimizer ผ่านออบเจ็กต์ของ Python เพื่อปรับไฮเปอร์พารามิเตอร์เองได้ ("Try config with other optimizer via python objects").

optimizer_fn = keras.optimizers.Adam(learning_rate=0.001, beta_1=0.9, beta_2=0.999)
โค้ดทำอะไร
keras.optimizers.Adam(...)สร้างออบเจ็กต์ optimizer เก็บไว้ในตัวแปร แล้วค่อยส่งให้ model.compile(optimizer=optimizer_fn, ...) — ต่างจากการส่งสตริง 'adam' ตรงที่ตอนนี้เราคุมค่าได้เอง
learning_rate=0.001ขนาดก้าวของ gradient descent (ค่า default ของ Adam); ค่านี้คือ \(\alpha\) ใน \(\theta := \theta - \alpha\,\partial J/\partial\theta\)
beta_1=0.9สัมประสิทธิ์ EWA ของ momentum (โมเมนต์ที่หนึ่ง คือค่าเฉลี่ยของ gradient) — Lesson 4 §5
beta_2=0.999สัมประสิทธิ์ EWA ของ RMSProp (โมเมนต์ที่สอง คือค่าเฉลี่ยของ gradient ยกกำลังสอง); ทั้งสองค่านี้คือค่า default ที่แทบไม่ต้องแก้

ต่อมาคือ "Common Last-layer Activation and a Loss Function" ซึ่งเป็นตารางจับคู่ที่ต้องตรงกันเสมอ เพราะ activation กำหนดรูปแบบของ output ส่วน loss กำหนดวิธีวัดความผิด ถ้าสองอย่างนี้ไม่เข้าคู่กันโมเดลจะเรียนผิดทางหรือ error ทันที:

ชนิดปัญหา (Problem type)Last-layer activationLoss functionทำไมจึงเป็นคู่นี้
Binary classificationsigmoidbinary_crossentropyoutput 1 ค่าในช่วง 0–1 = ความน่าจะเป็นของคลาสบวก
Multiclass, single-label classificationsoftmaxcategorical_crossentropyคลาสไม่ทับซ้อน ⇒ ความน่าจะเป็นต้องรวมเป็น 1 (MNIST อยู่แถวนี้)
Multiclass, multilabel classificationsigmoidbinary_crossentropyหนึ่งภาพอาจเป็นหลายคลาสพร้อมกัน ⇒ แต่ละ node ตัดสินอิสระ ไม่รวมเป็น 1
Regression to arbitrary valuesNone (ไม่ใส่ activation)mseต้องให้ output ออกค่าได้ทุกช่วง จึงห้ามบีบด้วย sigmoid
Regression to values between 0 and 1sigmoidmse หรือ binary_crossentropyค่าเป้าหมายถูกจำกัดในช่วง 0–1 อยู่แล้ว จึงบีบด้วย sigmoid ได้

สังเกตว่าแถวที่ 2 กับ 3 ใช้คำว่า multiclass เหมือนกันแต่ได้คำตอบคนละอย่าง — ตัวตัดสินคือคำว่า single-label กับ multilabel. ตัวอย่างให้เห็นภาพ: คำถาม "รูปนี้เป็นเลขอะไร" ตอบได้คำตอบเดียว ⇒ softmax; คำถาม "รูปนี้ปรากฏวัตถุอะไรบ้าง" ตอบได้หลายอย่างพร้อมกัน ⇒ sigmoid หลายตัวที่ตัดสินอิสระ. นี่คือกับดักที่ข้อสอบชอบใช้มากที่สุดของตารางนี้.

ส่วนตารางขวามือเทียบ categorical_crossentropy กับ sparse_categorical_crossentropy ทีละแถว: True Label Format — one-hot vector (เช่น [0, 1, 0]) เทียบกับ integer index (เช่น 1); Math Formula — \(-\sum_i y_i\log(\hat y_i)\) เทียบกับ \(-\log(\hat y_{\text{correct\_index}})\); Final Loss Value — Identical ทั้งสองฝั่ง; Memory Efficiency — categorical ต่ำกว่า (เก็บเมทริกซ์ใหญ่ที่เต็มไปด้วยศูนย์) ส่วน sparse สูงกว่า (เก็บจำนวนเต็มตัวเดียว). และหมายเหตุใต้ตาราง: ให้ใช้ sparse_categorical_crossentropy เมื่อมี sparse labels (สำหรับ MNIST คือป้ายเป็นจำนวนเต็มหนึ่งตัวจาก 0 ถึง 9) และคลาสเป็น exclusive คือไม่ทับซ้อนกัน.

คำนวณ: พิสูจน์ว่า sparse กับ categorical ให้ loss เท่ากันเป๊ะ
ป้ายจริง \(y=3\) จาก 10 คลาส; softmax ให้ความน่าจะเป็นของคลาส 3 เท่ากับ \(\hat y_3 = 0.7\)
  1. ขั้นที่ 1: แบบ categorical — ป้ายเป็น one-hot \(y=[0,0,0,1,0,0,0,0,0,0]\)
    $$-\sum_{k=0}^{9} y_k\ln\hat y_k = -(0\cdot\ln\hat y_0 + \dots + 1\cdot\ln 0.7 + \dots) = -\ln 0.7 = 0.3567$$

    ทุกเทอมที่ \(y_k=0\) หายไปหมด เหลือเทอมเดียวที่ \(y_k=1\)

  2. ขั้นที่ 2: แบบ sparse — ป้ายเป็นเลข 3 ตัวเดียว
    $$-\ln\hat y_{3} = -\ln 0.7 = 0.3567$$

    สูตรหยิบเฉพาะความน่าจะเป็นของ index ที่ถูกต้องมาใช้ตรงๆ

  3. ขั้นที่ 3: เทียบพื้นที่เก็บป้ายของ 60,000 ตัวอย่าง
    $$\text{sparse}: 60{,}000\ \text{ตัวเลข},\qquad \text{one-hot}: 60{,}000\times10 = 600{,}000\ \text{ตัวเลข}$$

ค่า loss เท่ากันทุกหลัก (ตรงกับแถว "Final Loss Value: Identical" บนสไลด์) เพราะ one-hot ทำหน้าที่แค่ "เลือก" เทอมเดียวจากผลรวม ซึ่ง sparse ทำได้ด้วยการอ้าง index โดยตรง. ต่างกันแค่หน่วยความจำ \(K\) เท่า (ที่นี่ \(K=10\)) — ยิ่งจำนวนคลาสมาก (เช่นงานภาษาที่มี 50,000 คำ) ยิ่งต่างมหาศาล

6.1 สร้างโมเดลด้วย Input(), Flatten() และ Softmax() (สไลด์หน้า 35)

Slide Using Input(), Flatten() and Softmax(): the Input layer serves as the network's starting point where we provide the expected shape of each data element as a tuple; the Flatten layer transforms the input into a vector since the succeeding Dense layer needs flat input rather than a multidimensional array; the Softmax layer returns an array of 10 probability scores summing to 1; two code blocks show a Sequential model with Flatten(input_shape=(28,28)), two Dense(16, relu), Dense(10) and Softmax(), and a variant that first creates input_layer = Input(shape=(28,28)) and passes it to Sequential; a note says if you use Flatten you do not have to reshape data yourself
Slide: Using Input(), Flatten() and Softmax() — สองรูปแบบที่ให้ผลเหมือนกัน (Lecture5.pdf, p.35)

สไลด์แนะนำ layer สามตัวที่ทำให้โค้ดอ่านง่ายขึ้นและลดงานเตรียมข้อมูล. Input layer "serves as the network's starting point" คือจุดเริ่มต้นของเครือข่าย เราบอก shape ที่คาดหวังของข้อมูลหนึ่งชิ้นเป็น tuple ให้มัน. Flatten layer "transforms the input into a vector" เพราะ Dense ที่ตามมาต้องการ input ที่แบนราบ ไม่ใช่อาร์เรย์หลายมิติ — และหมายเหตุท้ายหน้าบอกผลพลอยได้ที่สำคัญมาก: "If you use Flatten, you don't have to reshape data yourself" คือไม่ต้องไปทำ X.reshape(60000, 784) เองอย่างหน้า 26 อีกแล้ว. Softmax layer "returns an array of 10 probability scores (sum to 1)" คือคืนความน่าจะเป็น 10 ค่าที่รวมกันได้ 1 โดยเขียนแยกเป็น layer ต่างหากแทนการใส่ activation='softmax' ใน Dense.

# Create a Sequential model with flatten and softmax
model = models.Sequential()
model.add(Flatten(input_shape=(28,28)))
model.add(Dense(16, activation='relu'))
model.add(Dense(16, activation='relu'))
model.add(Dense(10))
model.add(Softmax())

# Create a Sequential model with input, flatten and softmax
input_layer = Input(shape=(28,28))
model = models.Sequential([input_layer])
model.add(Flatten())
model.add(Dense(16, activation='relu'))
model.add(Dense(16, activation='relu'))
model.add(Dense(10))
model.add(Softmax())
โค้ดทำอะไร
model.add(Flatten(input_shape=(28,28)))รับภาพเป็นตาราง 2 มิติแล้วยืดเป็นเวกเตอร์ 784 ให้อัตโนมัติ; เพราะเป็น layer แรกจึงต้องบอก input_shape ที่นี่ — สังเกตว่าเป็น (28,28) ไม่ใช่ (784,) เพราะข้อมูลยังไม่ถูก reshape
model.add(Dense(10))ชั้น output ไม่ใส่ activation ⇒ ได้คะแนนดิบ (logits) 10 ค่าที่ยังบวกกันไม่เท่ากับ 1
model.add(Softmax())แยก softmax ออกมาเป็น layer ของตัวเอง ผลลัพธ์เท่ากับเขียน Dense(10, activation='softmax') ทุกประการ; ประโยชน์คือแทรก BatchNormalization ก่อน softmax ได้ (แบบสไลด์หน้า 13) และดู logits ได้ง่าย
input_layer = Input(shape=(28,28))ประกาศจุดเริ่มต้นแยกเป็นตัวแปร; ชื่ออาร์กิวเมนต์คือ shape ไม่ใช่ input_shape — จุดนี้ข้อสอบสลับได้ง่ายมาก
model = models.Sequential([input_layer])ส่ง Input เข้าไปเป็นสมาชิกตัวแรกของ list; เมื่อมี Input แล้ว Flatten() ที่ตามมาจึงไม่ต้องระบุ input_shape ซ้ำอีก
ผลลัพธ์ที่คาด

ทั้งสองบล็อกสร้างโมเดลที่มีพารามิเตอร์เท่ากันคือ 13,002 ตัว (เท่ากับหน้า 29 ทุกประการ เพราะ Flatten, Input และ Softmax ไม่มีพารามิเตอร์ที่ต้องเรียนรู้เลย — ใน model.summary() ทั้งสามชั้นนี้จะแสดง Param # = 0). ความต่างมีเพียงว่าโมเดลชุดนี้รับ X ที่ยังมี shape (60000, 28, 28) ได้โดยตรง ไม่ต้อง reshape ก่อน

6.2 เมื่อข้อมูลยังไม่ถูกแบ่ง: train_test_split (สไลด์หน้า 36)

Slide Other Ways of Splitting the Data: the first case is when datasets have already been packaged all the data for us, shown with code importing train_test_split from sklearn.model_selection, calling it on X and y with test_size = 0.2 and a commented model.fit using validation_data; the second case is when datasets have already been split for us, shown with model.fit(Xrain, y_train, validation_data=(Xval, yval))
Slide: Other Ways of Splitting the Data — สองสถานการณ์ของการแบ่งข้อมูล (Lecture5.pdf, p.36)

จนถึงตอนนี้เราเห็นวิธีแบ่ง validation มาแล้วสองแบบ: validation_split=0.2 (ให้ Keras ตัดให้) ในหน้า 31 และ validation_data=(Xval,yval) (เตรียมมาเอง) ในหน้า 22. หน้า 36 เติมกรณีที่สาม คือเมื่อ "datasets have already been packaged all the data for us" — ข้อมูลมาเป็นก้อนเดียวยังไม่แบ่ง เราต้องแบ่งเองด้วย train_test_split ของ scikit-learn ก่อน แล้วค่อยส่งเข้า fit; ส่วนกรณี "datasets have already been split all the data for us" คือแบ่งมาให้แล้ว (อย่าง MNIST) ก็ส่ง validation_data ได้เลย.

from sklearn.model_selection import train_test_split
Xrain, Xval, y_train, yval = train_test_split(X, y, test_size = 0.2)
#model.fit(Xrain, y_train, validation_data=(Xval,yval))

model.fit(Xrain, y_train, validation_data=(Xval,yval))
โค้ดทำอะไร
from sklearn.model_selection import train_test_splitฟังก์ชันนี้อยู่ใน scikit-learn ไม่ใช่ Keras — ข้อสอบชอบถามว่า import มาจากโมดูลไหน
Xrain, Xval, y_train, yval = train_test_split(X, y, test_size = 0.2)คืนค่า 4 ก้อนตามลำดับ X-train, X-test, y-train, y-test (สลับลำดับไม่ได้เด็ดขาด); test_size=0.2 = กันไว้ 20% เป็นชุดที่สอง. หมายเหตุ: ชื่อตัวแปร Xrain บนสไลด์สะกดตก "t" จาก Xtrain แต่ยังรันได้เพราะเป็นแค่ชื่อตัวแปรที่ใช้สอดคล้องกันทั้งสองบรรทัด
#model.fit(Xrain, y_train, validation_data=(Xval,yval))บรรทัดที่ถูกคอมเมนต์ไว้ก่อนเพื่อแสดงรูปแบบการเรียก
model.fit(Xrain, y_train, validation_data=(Xval,yval))เทรนด้วยก้อน train และใช้ก้อนที่แบ่งไว้เป็น validation; validation_data ต้องเป็น tuple ของ (ข้อมูล, ป้าย) — และใช้พร้อมกับ validation_split ไม่ได้
ผลลัพธ์ที่คาด

ถ้า X มี 60,000 ตัวอย่าง หลัง train_test_split(..., test_size=0.2) จะได้ Xrain.shape = (48000, 784) และ Xval.shape = (12000, 784) — ตัวเลขเดียวกับที่ validation_split=0.2 ให้ในหน้า 31 ต่างกันตรงที่ train_test_split สุ่มสลับข้อมูลก่อนแบ่ง ขณะที่ validation_split ของ Keras ตัดจากท้ายชุดโดยไม่สุ่ม (ถ้าข้อมูลเรียงตามคลาส validation จะมีแต่คลาสท้ายๆ ซึ่งเป็นกับดักจริงในทางปฏิบัติ)

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
sparse กับ categorical ให้ loss เท่ากันเพราะใช้สูตรเดียวกัน (\(-\sum_k y_k\ln\hat y_k\)) ต่างกันแค่วิธีเก็บ \(y\): one-hot เก็บเป็นเวกเตอร์ที่มีศูนย์เต็มไปหมด ส่วน sparse เก็บเฉพาะ index — Keras แปลงให้ข้างในเอง เราจึงเลือกได้ตามความสะดวก.
เปลี่ยนแล้วเกิดอะไร
ใช้ categorical_crossentropy กับป้ายจำนวนเต็มที่ยังไม่แปลง: shape ของป้าย \((32,1)\) ไม่ตรงกับ output \((32,10)\) ⇒ error ทันทีตอน fit; กลับกัน ใช้ sparse_categorical_crossentropy กับป้าย one-hot ก็ error เช่นกัน. เปลี่ยนแถวในตารางหน้า 34 จาก single-label เป็น multilabel: softmax + categorical เปลี่ยนเป็น sigmoid + binary ทั้งคู่.
เชื่อมกับ
cross-entropy ตัวเดียวกับ Lesson 1 §8 และ softmax ที่ให้ \(\delta = a-y\) ใน Lesson 3 §6; ส่วนการแบ่งข้อมูลเชื่อมกับ cross-validation ใน Lesson 1 §9.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 14Sparse vs Categorical
เพราะเหตุใดจึงใช้ loss="sparse_categorical_crossentropy" กับ label ของ MNIST แทน "categorical_crossentropy"?
คำถาม 15เติมโค้ด Flatten/Softmax
ต้องการสร้างโมเดล MNIST ที่รับ X ซึ่งยังมี shape (60000, 28, 28) โดยไม่ reshape เอง ข้อใดถูกต้อง?

7. Overcome Overfitting ในโค้ดจริง และการบ้าน IMDB

กราฟ 40 epochs ในหน้า 32 แสดงให้เห็นแล้วว่า val_loss กระดกขึ้นเมื่อเทรนนานเกินไป. สองหน้าสุดท้ายของเลกเชอร์จึงปิดท้ายด้วยเครื่องมือแก้ overfitting ในรูปของโค้ด Keras และการบ้านให้ไปฝึกครบทั้ง workflow ด้วยตัวเอง.

7.1 การบ้าน: จำแนกรีวิวหนัง IMDB (สไลด์หน้า 37)

Slide Homework Classify Movies Reviews (IMDB dataset): build a model classifying Internet Movie reviews as satisfied or not, with 50,000 examples in total split into 25,000 training examples and 25,000 test examples, where train_labels and test_labels take values 0 for negative and 1 for positive; below is one decoded review text with unknown words replaced by question marks, and the same review shown as a list of integer word indices starting [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, ...]
Slide: Homework — Classify Movies Reviews (IMDB dataset) (Lecture5.pdf, p.37)

โจทย์คือสร้างโมเดลจำแนกรีวิวหนังจาก Internet Movie Database ว่าผู้เขียน "พึงพอใจหรือไม่พึงพอใจ" มีข้อมูลทั้งหมด 50,000 ตัวอย่าง แบ่งเป็น training 25,000 และ test 25,000 โดย train_labels และ test_labels มีค่า 0 กับ 1 — 0 หมายถึง negative และ 1 หมายถึง positive. สิ่งที่น่าสนใจที่สุดบนหน้านี้คือสองรูปแบบของข้อมูลเดียวกัน: ด้านบนเป็นข้อความรีวิวที่ถอดกลับเป็นคำแล้ว (ขึ้นต้นด้วย "? this film was just brilliant casting location scenery story direction…" โดยเครื่องหมาย ? คือคำที่ไม่อยู่ในพจนานุกรมที่เลือกไว้) ส่วนด้านล่างเป็นรีวิวเดียวกันในรูปลิสต์ของจำนวนเต็ม [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, ...] — แต่ละเลขคือดัชนีของคำในพจนานุกรม (เลขยิ่งน้อยยิ่งเป็นคำที่พบบ่อย). นี่คือบทเรียนที่ต่อจากหน้า 25 โดยตรง: เครือข่ายไม่เคยเห็น "ภาพ" หรือ "ข้อความ" — มันเห็นแต่ตัวเลขเสมอ งานของเราคือแปลงข้อมูลดิบให้เป็นตัวเลขที่มีรูปทรงคงที่.

เชื่อมกับตารางหน้า 34: งานนี้เป็น binary classification ⇒ output layer ต้องเป็น Dense(1, activation='sigmoid') และ loss ต้องเป็น binary_crossentropy — ไม่ใช่ softmax 10 node แบบ MNIST. และเพราะแต่ละรีวิวยาวไม่เท่ากัน ต้องแปลงเป็นเวกเตอร์ความยาวคงที่ก่อน (เช่น multi-hot ความยาว 10,000 ตามจำนวนคำที่เลือก) เครือข่าย Dense จึงจะรับได้.

7.2 L2 Regularization และ Dropout ใน Keras (สไลด์หน้า 38)

Slide Overcome Overfitting: L2 regularization adds cost proportional to the square of the value of the weight coefficients and is added by passing weight regularizer instances to layers as keyword arguments, shown with Dense(100, activation elu, kernel_initializer he_normal, kernel_regularizer=keras.regularizers.l2(0.01)); Dropout deactivates neurons by the dropout rate and is used via a dropout layer applied to the output of the layer immediately before it, shown in a Sequential model with Flatten(input_shape=[28,28]), Dense(300, elu, he_normal), Dropout(rate=0.2), Dense(100, elu, he_normal), Dropout(rate=0.2), Dense(10, softmax); two validation-loss charts show the L2 model and the dropout model rising less steeply than the original model
Slide: Overcome Overfitting — L2 และ Dropout ในโค้ด Keras (Lecture5.pdf, p.38)

สไลด์สุดท้ายแปลงทฤษฎี regularization จาก Lesson 4 §6 เป็นโค้ดสองบรรทัด. L2 Regularization อธิบายไว้ว่าคือการ "add cost proportional to the square of the value of the weight coefficients" คือบวกค่าปรับที่แปรผันตามกำลังสองของ weight เข้าไปใน cost และวิธีใส่คือ "passing weight regularizer instances to layers as keyword arguments" — ส่งออบเจ็กต์ regularizer เข้าไปเป็นอาร์กิวเมนต์ของ layer. Dropout อธิบายว่า "deactivate neuron by the dropout rate" คือปิดการทำงานของ neuron ตามอัตราที่กำหนด และใช้งานโดยเพิ่มเป็น layer ซึ่ง "is applied to the output of the layer immediately before it" — ทำงานกับ output ของ layer ที่อยู่ก่อนหน้ามันทันที. กราฟ validation loss สองรูปทางขวาแสดงผล: เส้นของโมเดลที่ใส่ L2 และเส้นของโมเดลที่ใส่ dropout ไต่ขึ้นช้ากว่าเส้นของ original model อย่างชัดเจน ⇒ overfit น้อยลงจริง.

layer = keras.layers.Dense(100, activation="elu",
                           kernel_initializer="he_normal",
                           kernel_regularizer=keras.regularizers.l2(0.01))

model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=[28, 28]),
    keras.layers.Dense(300, activation="elu", kernel_initializer="he_normal"),
    keras.layers.Dropout(rate=0.2),
    keras.layers.Dense(100, activation="elu", kernel_initializer="he_normal"),
    keras.layers.Dropout(rate=0.2),
    keras.layers.Dense(10, activation="softmax")
])
โค้ดทำอะไร
kernel_regularizer=keras.regularizers.l2(0.01)เพิ่มเทอม \(0.01\sum\theta^2\) ของ layer นี้เข้าไปใน cost ⇒ optimizer จะถูกดึงให้ weight เล็กลง; เลข 0.01 คือ \(\lambda\); คำว่า kernel ใน Keras หมายถึงเมทริกซ์ weight (ไม่รวม bias — ถ้าจะปรับ bias ต้องใช้ bias_regularizer ต่างหาก)
activation="elu"ELU เป็นอีกตัวในตระกูล ReLU ที่ฝั่งลบไม่เป็น 0 สนิท จึงเลี่ยงปัญหา node ตายจากหน้า 3; ยังจับคู่กับ he_normal ตามกฎหน้า 8
keras.layers.Flatten(input_shape=[28, 28])เหมือนหน้า 35 ทุกประการ (ที่นี่เขียน shape ด้วยวงเล็บเหลี่ยมแทน tuple ซึ่ง Keras รับได้ทั้งสองแบบ)
keras.layers.Dropout(rate=0.2)วางไว้หลัง Dense(300) ⇒ ปิด node ของ Dense ชั้นนั้น 20% แบบสุ่มในแต่ละ step ของการเทรน; rate = สัดส่วนที่ปิด ไม่ใช่สัดส่วนที่เก็บ
keras.layers.Dense(10, activation="softmax")ชั้น output ตามปกติ — และไม่มี Dropout ต่อท้าย เพราะจะทำลายความน่าจะเป็นที่ต้องรวมเป็น 1
ผลลัพธ์ที่คาด

โค้ดนี้ประกาศโครงสร้างจึงยังไม่พิมพ์อะไร; ถ้าเรียก model.summary() จะได้ flatten → 0, dense (300) → 235,500 (\(300\times785\)), dropout → 0, dense_1 (100) → 30,100 (\(100\times301\)), dropout_1 → 0, dense_2 (10) → 1,010 (\(10\times101\)) รวม 266,610 พารามิเตอร์. สังเกตว่า Dropout มี Param # = 0 เสมอ — มันไม่มีอะไรให้เรียนรู้ และตอน evaluate/predict Keras ปิดการทำงานของมันให้อัตโนมัติ

คำนวณ: "rate" ของ Keras ไม่ใช่ keep_prob ของสไลด์ Lecture 4
Dropout ตอนเทรนจะปิด node บางส่วน แล้วคูณ activation ที่เหลือด้วยตัวคูณชดเชยเพื่อให้ผลรวมเฉลี่ยเท่าเดิม
  1. ขั้นที่ 1: Dropout(rate=0.2) ตามสไลด์หน้า 38
    $$\text{ปิด }20\% \Rightarrow \text{keep\_prob} = 1-0.2 = 0.8,\qquad \text{ตัวคูณชดเชย} = \frac{1}{0.8} = 1.25$$
  2. ขั้นที่ 2: ตรวจด้วยตัวเลขจริง — activation \([1,2,3,4]\) ถูกปิดไป 1 ตัว
    $$[1,\ 2,\ 0,\ 4]\times1.25 = [1.25,\ 2.5,\ 0,\ 5]$$

    ผลรวมเดิม 10 ผลรวมใหม่ 8.75 — ใกล้เคียงกัน (ถ้าเฉลี่ยข้ามหลายรอบจะเท่ากันพอดี) จึงไม่ทำให้ layer ถัดไปเห็นสเกลเปลี่ยน

  3. ขั้นที่ 3: ถ้าเปลี่ยนเป็น Dropout(0.6)
    $$\text{keep\_prob} = 0.4,\qquad \text{ตัวคูณ} = \frac{1}{0.4} = 2.5$$

ระวังสับสน: สไลด์ Lecture 4 พูดถึง keep_prob = ความน่าจะเป็นที่เก็บ node ไว้ ส่วน Keras ใช้ rate = สัดส่วนที่ปิด — สองค่านี้บวกกันได้ 1 เสมอ. ถ้าข้อสอบให้ rate=0.3 แล้วถาม keep_prob คำตอบคือ 0.7

กลไกที่ต้องอธิบายได้
ทำไมต้องมี
โค้ดบรรทัดเดียว (kernel_regularizer, Dropout) แทนสูตรทั้งหมดได้เพราะ Keras แยก "สถาปัตยกรรม" ออกจาก "พฤติกรรมตอนเทรน" — ผู้ใช้ประกาศเจตนา แล้ว autodiff (Lesson 3 §4) หาอนุพันธ์ของเทอมปรับให้เอง.
เปลี่ยนแล้วเกิดอะไร
ใส่ทั้ง L2 และ Dropout ใน layer เดียวกันได้ ไม่ขัดแย้ง เพราะเป็นคนละกลไก: L2 บีบขนาดของ \(\theta\) ทุก step ส่วน Dropout สุ่มปิด node ตอน forward. เพิ่ม \(\lambda\) จาก 0.01 เป็น 0.1: เทอมปรับใหญ่ขึ้น 10 เท่า ⇒ weight ถูกบีบแรงขึ้นจนอาจ underfit.
เชื่อมกับ
Lesson 4 §6 (L2 / Dropout / Max-Norm แก้ overfit คนละมุม) และกราฟ val_loss ในหน้า 32 ที่บอกว่าเมื่อไรควรใส่เครื่องมือเหล่านี้; Lesson 6 จะให้ tuner ค้นหาค่า \(\lambda\) และ rate ที่เหมาะให้อัตโนมัติ.
ออกสอบสูง — ข้อความที่อาจารย์ทำสี
คำถาม 16Dropout rate
ในโค้ดสไลด์หน้า 38 บรรทัด keras.layers.Dropout(rate=0.2) ที่วางหลัง Dense(300) หมายความว่าอย่างไร?


ชัยชนะของบทเรียนนี้
คุณควรอธิบายได้ว่าทำไมโซ่ chain rule ที่ยาวทำให้เกิด vanishing/exploding gradient (พร้อมตัวเลข \(0.25^{10}\), \(0.9^{50}\), \(1.1^{50}\)) และ clip gradient ได้ทั้งแบบ value และ norm; พิสูจน์ได้ว่า zero init ทำให้ node เหมือนกันตลอดกาล คำนวณ \(\operatorname{Var}(z)=n\operatorname{Var}(\theta)=100\) ตามโจทย์หน้า 5 และเล่าที่มาของ He/Xavier/LeCun พร้อมคู่ activation ที่ถูกต้อง; คำนวณ BatchNorm และ LayerNorm ด้วยตัวเลขจริงได้ (รวมเติมคำตอบโจทย์หน้า 18: \(\mu^{(2)}=267.67\), \(\mu^{(3)}=401.67\), \(\mu^{(4)}=301\)) นับพารามิเตอร์ BN \(4n\) ได้ 13,170/13,086/84 และอธิบาย train-vs-test กับ moving average ได้; และเขียน Keras ครบ 5 ขั้นจากความจำได้ตั้งแต่ mnist.load_data() จนถึง np.argmax(model.predict(...)) พร้อมคำนวณ 13,002 พารามิเตอร์ และ 750 mini-batch ได้ — ครบเนื้อหา Lecture 1–5 แล้ว บทถัดไป (Lesson 6) ต่อด้วย callbacks และการจูน hyperparameter.

ภาคผนวก: Slide ทั้งหมดใน Lecture5.pdf (38 หน้า) — เช็คว่าอ่านครบ

หน้าเนื้อหาสถานะ
1Vanishing and Exploding Gradients — ที่มาจาก chain rule, นิยาม 2 ปัญหา✓ หัวข้อ 1 (มีรูป + คำนวณตัวคูณสะสม)
2Activation Function — sigmoid saturation, อนุพันธ์สูงสุด 0.25✓ หัวข้อ 1.1 (มีรูป)
3Activation Function (2) — ReLU, dying ReLU, gradient clipping✓ หัวข้อ 1.2 (มีรูป + คำนวณ clipping 2 แบบ)
4Weight Initialization — ต้องสุ่ม, scale สำคัญกว่าชนิดการแจกแจง✓ หัวข้อ 2 (มีรูป)
5The Problem of Random Initialization — โจทย์ mean/variance ที่เว้นว่าง✓ หัวข้อ 2.2 (มีรูป + เติมคำตอบเต็ม)
6Weight initialization Techniques — LeCun / Xavier / He✓ หัวข้อ 2.3 (มีรูป + ตาราง + พิสูจน์ 2/fan_in)
7Experiments — Zero vs Random vs He Initialization✓ หัวข้อ 2.4 (มีรูป + อ่านตัวเลข 0.69306, 84.14%)
8Issues about Weight Initialization — คู่ที่ถูกต้องและข้อจำกัด✓ หัวข้อ 2.5 (มีรูป)
9Why Normalization helps Optimization — contour วงรี vs วงกลม✓ หัวข้อ 3.0 (มีรูป + สูตร z-score)
10–11Batch Normalization / (2) — Implementation, ช่วง Linear ของ sigmoid✓ หัวข้อ 3.0B (มีรูปทั้งสองหน้า)
12Batch Normalization (BN) — สูตร 4 บรรทัดและความหมายทุกสัญลักษณ์✓ หัวข้อ 3.1 (มีรูป + คำนวณ BN ของ batch 4 ตัว)
13–14Normalize z (pre-activation) / Normalize a (post-activation) + โค้ด✓ หัวข้อ 3.2 (มีรูป + โค้ด 2 ชุด + นับพารามิเตอร์ 4n)
15Issues about BN — training time vs test time, moving average, momentum✓ หัวข้อ 3.3 (มีรูป + คำนวณ moving mean)
16Issues about BN (2) — ข้อดีทั้งชุดของ BN✓ หัวข้อ 3.4 (มีรูป)
17Issues about BN (3) — regularization, batch เล็ก, dropout หลัง BN + โค้ด✓ หัวข้อ 3.5 (มีรูป + โค้ด)
18From Batch Normalization to Layer Normalization — โจทย์ μ ที่เว้นว่าง✓ หัวข้อ 4 (มีรูป + เติมคำตอบครบ 4 ตัว)
19Why Layer Normalization? — 3 ข้อดีและตารางเทียบ BN/LN✓ หัวข้อ 4 (มีรูป)
20–21Keras API / Typical Keras Workflow — นิยามและ 5 ขั้นตอน✓ หัวข้อ 5.0 (มีรูปทั้งสองหน้า)
22Begin with Circle Dataset — สคริปต์ Keras ฉบับเต็ม✓ หัวข้อ 5.1 (มีรูป + โค้ดอธิบายทุกบรรทัด)
23Understand Core Keras APIs via Examples — โจทย์ MNIST✓ หัวข้อ 5.2 (มีรูป)
24Loading the MNIST Dataset in Keras — โค้ดโหลดและ shape✓ หัวข้อ 5.3 (มีรูป + โค้ด + ผลลัพธ์จริง)
25Data Exploration — ตารางพิกเซลและการวาดภาพ✓ หัวข้อ 5.4 (มีรูป + โค้ด)
26Data Preparation — reshape เป็น 784 และหาร 255✓ หัวข้อ 5.5 (มีรูป + โค้ด)
27Preparing the Labels — one-hot vs integer label✓ หัวข้อ 5.6 (มีรูป + โค้ด + ผลลัพธ์)
28Build the Model with Sequential API — Alternative 1 / 2, Dense layer✓ หัวข้อ 5.7 (มีรูป + โค้ด)
29Concise Summary of the Network Topology — นับพารามิเตอร์ 13,002✓ หัวข้อ 5.8 (มีรูป + คำนวณเต็ม)
30Compile the model — loss, optimizer, metrics✓ หัวข้อ 5.9 (มีรูป + โค้ด)
31Launch the training — fit และสูตรนับ mini-batch 750✓ หัวข้อ 5.10 (มีรูป + โค้ด + คำนวณเต็ม)
32Plot the Performance of the Model — 5 epochs vs 40 epochs✓ หัวข้อ 5.11 (มีรูป + โค้ด)
33Check the Model on the Test Set — evaluate และ predict✓ หัวข้อ 5.12 (มีรูป + โค้ด + ผลลัพธ์จริง)
34More Compile Options — ตาราง activation/loss และ sparse vs categorical✓ หัวข้อ 6 (มีรูป + ตาราง + คำนวณ loss)
35Using Input(), Flatten() and Softmax()✓ หัวข้อ 6.1 (มีรูป + โค้ด 2 รูปแบบ)
36Other Ways of Splitting the Data — train_test_split✓ หัวข้อ 6.2 (มีรูป + โค้ด)
37Homework: Classify Movies Reviews (IMDB dataset)✓ หัวข้อ 7.1 (มีรูป + วิเคราะห์โจทย์)
38Overcome Overfitting — L2 และ Dropout ในโค้ด Keras✓ หัวข้อ 7.2 (มีรูป + โค้ด + คำนวณ rate)

สงสัยตรงไหน ถามได้เลยในแชท — ผมเป็นผู้สอนของคุณ ถามซ้ำได้ ถามลึกได้ ไม่ต้องเกรงใจ.