Loss Function มาจากไหน: จาก Likelihood ไม่ใช่ธรรมเนียม
เส้นสามเส้นบนข้อมูลเดียวกัน กฎให้คะแนนสามแบบ ผู้ชนะสามคน squared error คือทางเลือก ไม่ใช่ข้อเท็จจริง
ในหน้านี้
ใบมีดที่ตัดชิ้นงานสึกลงเรื่อย ๆ ตลอดกะสิบชั่วโมง มันเสียคมมากพอให้ชิ้นงานที่ออกจากสายพานกว้างกว่าตอนเริ่มอยู่เศษเสี้ยวมิลลิเมตร และเมื่อเกิน 23.5 มิลลิเมตร ฝ่ายตรวจสอบก็ปฏิเสธทันที ไม่มีใครในโรงงานรู้ว่าจุดนั้นเกิดขึ้นเมื่อไร สิ่งที่พวกเขามีคือเวอร์เนียร์ สมุดบันทึก และค่าที่อ่านได้ยี่สิบค่าจากวันอังคารที่แล้ว: จำนวนชั่วโมงตั้งแต่เปลี่ยนใบมีด และความกว้างของชิ้นงานที่วัดในขณะนั้น
ใครสักคนลากเส้นผ่านจุดเหล่านั้น อีกคนลากเส้นที่ต่างออกไปเล็กน้อย คนที่สามลากเส้นที่สาม ทั้งสามเส้นดูสมเหตุสมผลบนกระดาษ และทั้งสามไม่เห็นตรงกันว่าควรเปลี่ยนใบมีดเมื่อไร ต่างกันหลายชั่วโมง — ซึ่งในโรงงานนี้คือความต่างระหว่างสัปดาห์ที่ราบรื่นกับ batch ที่ต้องทิ้งทั้งชุด
เส้นไหนดีกว่า?
เมื่อถามแบบนี้ คำถามนี้ไม่มีคำตอบ ไม่ใช่คำตอบยาก — แต่ไม่มีคำตอบเลย “ดีกว่า” ไม่ใช่คุณสมบัติของเส้นแบบเดียวกับความชันของมัน แต่เป็นคุณสมบัติของเส้น ร่วมกับกฎสำหรับให้คะแนนเส้น และจนกว่าจะมีใครเขียนกฎนั้นลงมา ก็ไม่มีอะไรให้คำนวณ บทนี้จะถือประโยคนั้นอย่างจริงจัง และจบด้วยการค้นพบว่ากฎที่ใช้กันบ่อยที่สุดใน machine learning ไม่ใช่ธรรมเนียม แต่เป็นผลจากข้ออ้างเกี่ยวกับโลก — ข้ออ้างที่คุณทดสอบได้ และบางครั้งก็ผิด
ขอสารภาพก่อน code บรรทัดแรก ค่าที่อ่านได้ยี่สิบค่านี้ไม่ได้มาจากโรงงานจริง: ผมสร้างมันจากเส้นที่ผมเลือกไว้ บวก random noise ที่มีการกระจายประมาณหนึ่งในสิบมิลลิเมตร เรื่องนี้สำคัญ เพราะทุกอย่างด้านล่างเป็นเรื่องว่าวิธีหนึ่งกู้คืนความจริงได้หรือไม่ และวิธีเดียวที่จะตรวจได้คือต้องรู้ความจริงล่วงหน้า ดังนั้น: 0.30 มิลลิเมตรต่อชั่วโมงคือคำตอบท้ายเล่ม คุณใช้มันไม่ได้ ใช้ได้แค่ตรวจเทียบ
กฎสามแบบ ผู้ชนะสามคน
ลิงก์ไปยังส่วน: กฎสามแบบ ผู้ชนะสามคนนี่คือค่าที่อ่านได้และเส้นสามเส้น ให้คะแนนสามแบบ: squared error ที่ทุกคนมักหยิบมาใช้; absolute error ที่นักสถิติอาจเลือก; และ worst error ที่ช่างเครื่องจะเลือก เพราะผู้ตรวจสอบไม่สนใจค่าเฉลี่ยของคุณ — เขาปฏิเสธชิ้นงานชิ้นเดียวที่เกิน tolerance
NumPy เข้ามาตรงนี้ หนึ่งบทหลังจาก perceptron แบบ pure-Python ด้วยเหตุผลเดียว: เมื่อจบบทนี้ เราจะประเมินเส้นผู้สมัครสี่แสนเส้นกับค่าที่อ่านได้เส้นละยี่สิบค่า และ Python loop เป็นเครื่องมือที่ผิดสำหรับงานนั้น อีกทั้งนี่คือ notation ที่แหล่งอ้างอิงทุกชิ้นด้านล่างใช้
import numpy as np
# Hours since the blade was changed, and the width of the part measured then.
SHIFT = np.array([
(0.5, 20.17), (1.0, 20.28), (1.5, 20.53), (2.0, 20.61), (2.5, 20.69),
(3.0, 20.94), (3.5, 21.21), (4.0, 21.31), (4.5, 21.27), (5.0, 21.35),
(5.5, 21.58), (6.0, 21.80), (6.5, 21.67), (7.0, 22.07), (7.5, 22.10),
(8.0, 22.31), (8.5, 22.48), (9.0, 22.66), (9.5, 22.90), (10.0, 23.13),
])
h, y = SHIFT[:, 0], SHIFT[:, 1]
LINES = {"A": (20.10, 0.26), "B": (20.20, 0.28), "C": (20.30, 0.26)}
for name, (a, b) in LINES.items():
r = y - (a + b * h)
print(f"{name} mean square {np.mean(r**2):.5f}"
f" mean absolute {np.mean(np.abs(r)):.5f}"
f" worst {np.max(np.abs(r)):.3f}") ปริมาณในบรรทัดที่ไฮไลต์คือ residual: สิ่งที่เส้นบอก ลบด้วยสิ่งที่เวอร์เนียร์บอก หนึ่งจำนวนต่อค่าที่อ่านได้หนึ่งค่า กฎให้คะแนนทุกแบบในบทนี้ และ loss function ทุกตัวในอีกยี่สิบแปดบทหลังจากนี้ คือวิธีใดวิธีหนึ่งในการบีบรายการ residual ให้เหลือจำนวนเดียว ต่างกันแค่ว่าบีบอย่างไร
A mean square 0.02699 mean absolute 0.12600 worst 0.430
B mean square 0.02524 mean absolute 0.13700 worst 0.350
C mean square 0.03179 mean absolute 0.15000 worst 0.320อ่านคอลัมน์ ไม่ใช่แถว squared error บอกว่า B, absolute error บอกว่า A, worst error บอกว่า C: กฎสามแบบ ผู้ชนะสามคน บนจุดยี่สิบจุดเดียวกัน
ผมเลือกเส้นสามเส้นนี้ให้มันไม่เห็นตรงกัน และควรพูดตรง ๆ ว่าเป็นเช่นนั้น ประเด็นคือมันทำได้ง่ายแค่ไหน — ใช้เวลาไม่กี่นาทีค้นหา intercept และ slope ที่ดูสมเหตุสมผล ก็เจอชุดแบบนี้ได้เป็นร้อย การจัดอันดับเป็นคุณสมบัติของกฎที่คุณเลือก ไม่ใช่ข้อเท็จจริงเกี่ยวกับเส้น ดังนั้นกฎจึงไม่ใช่รายละเอียดการ implement: มัน คือ นิยามของปัญหา ซึ่งทำให้เกิดคำถามที่บทนี้มีไว้ตอบ: คุณเลือกมันด้วยเหตุผลอะไร?
หนึ่ง parameter และหุบเขาหนึ่งลูก
ลิงก์ไปยังส่วน: หนึ่ง parameter และหุบเขาหนึ่งลูกเริ่มจากเรื่องที่เล็กกว่า เพราะจริง ๆ แล้วไม่ได้มีเส้นสามเส้น แต่มีอนันต์เส้น ตอนนี้ใช้ squared error ไปก่อน เพราะเป็นสิ่งที่ทุกคนใช้ และย่อปัญหาให้เหลือจำนวนเดียวด้วยเคล็ดลับที่ช่วย perceptron ประหยัดไปสิบเอ็ดพัน epoch ใน บทที่ 1: ลบค่าเฉลี่ยออกจากทั้งสองคอลัมน์ เมื่อกลุ่มจุดถูกจัดให้อยู่ตรงกลางที่จุดกำเนิดแล้ว เส้นที่ดีที่สุดภายใต้ squared error จะผ่านจุดกำเนิดพอดี — ดังนั้น intercept จึงถูกกำหนดแล้ว เหลือให้เลือกแค่ slope
u, v = h - h.mean(), y - y.mean() # 5.25 hours, 21.553 mm
def mse(theta):
return np.mean((v - theta * u) ** 2)
grid = np.arange(0.0, 0.6001, 0.001)
curve = np.array([mse(t) for t in grid])
print(grid.size, "candidates ->", f"theta={grid[curve.argmin()]:.3f}", f"mse={curve.min():.6f}")601 candidates -> theta=0.293 mse=0.010115slope ผู้สมัครหกร้อยหนึ่งค่า ผู้ชนะหนึ่งค่า: 0.293 มิลลิเมตรต่อชั่วโมง เทียบกับความจริง 0.300 ค่าที่อ่านได้ยี่สิบค่าที่มี noise กับ for-loop พาเราเข้าใกล้ภายในหนึ่งในร้อยมิลลิเมตรต่อชั่วโมง — สองจุดสามเปอร์เซ็นต์
ส่วนที่น่าสนใจไม่ใช่ผู้ชนะ แต่เป็นรูปทรงของการค้นหา พิมพ์ทั้ง curve ออกมา หมุนให้ loss วิ่งจากซ้ายไปขวา:
ts = np.arange(0.0, 0.6001, 0.04)
ls = np.array([mse(t) for t in ts])
for t, l in zip(ts, ls):
col = round(l / ls.max() * 50)
print(f"theta={t:.2f} |{' ' * col}*{' ' * (50 - col)}| mse={l:7.4f}")theta=0.00 | * | mse= 0.7244
theta=0.04 | * | mse= 0.5428
theta=0.08 | * | mse= 0.3878
theta=0.12 | * | mse= 0.2593
theta=0.16 | * | mse= 0.1575
theta=0.20 | * | mse= 0.0822
theta=0.24 | * | mse= 0.0336
theta=0.28 | * | mse= 0.0116
theta=0.32 | * | mse= 0.0161
theta=0.36 | * | mse= 0.0473
theta=0.40 | * | mse= 0.1050
theta=0.44 | * | mse= 0.1894
theta=0.48 | * | mse= 0.3004
theta=0.52 | * | mse= 0.4379
theta=0.56 | * | mse= 0.6021
theta=0.60 | *| mse= 0.7928นั่นคือหุบเขา มองจากด้านข้าง มันมีก้นเดียว ผนังสูงขึ้นอย่างราบรื่นทั้งสองด้าน และ — นี่คือส่วนที่บันไดของบทที่ 1 ให้ไม่ได้ — ทุกจุดบนมันมีทิศทาง “ลงเขา” ที่นิยามชัดเจน จำรูปทรงนี้ไว้ บทที่ 3 จะว่าด้วยการเดินลงมันโดยไม่ต้องไปเยือนทั้งหกร้อยหนึ่งจุด และว่ามีอะไรเปลี่ยนไปเมื่อหุบเขามีก้นมากกว่าหนึ่งจุด
แล้วทำไมต้องยกกำลังสอง?
ลิงก์ไปยังส่วน: แล้วทำไมต้องยกกำลังสอง?เราได้หุบเขาเพราะเรายกกำลังสอง Absolute error จะให้มุมหักที่ก้นหุบเขา; worst error จะให้ช่วงแบน ๆ ที่ขยับเส้นแล้วไม่เปลี่ยนอะไรเลย การยกกำลังสองสะดวกอย่างปฏิเสธไม่ได้ — และความสะดวกก็เป็นเหตุผลคร่าว ๆ ที่คอร์สส่วนใหญ่ให้ โดยแต่งตัวมาได้สี่แบบ: มันทำให้ error เป็นบวก (absolute value ก็ทำได้); มันลงโทษ error ใหญ่หนักกว่า (แล้วทำไมควรเป็นแบบนั้น?); มัน differentiable (ยกกำลังสี่ก็เป็น); มันคือสิ่งที่ทุกคนใช้ (จริง และนั่นไม่ใช่เหตุผล)
จุดยืนที่ซื่อตรงคือแบบนี้ squared error เลือกเส้น B และ absolute error เลือกเส้น A หนึ่งในสองอันนั้นถูกสำหรับโรงงานนี้ อีกอันผิด และสิ่งที่พูดมาถึงตอนนี้บอกคุณไม่ได้ว่าอันไหนถูก หากจะเลือกกฎ คุณต้องรู้อะไรบางอย่างเกี่ยวกับ ค่าที่อ่านได้มาแตกต่างจากเส้นได้อย่างไร และนั่นเป็นคำถามเกี่ยวกับโลก ไม่ใช่เกี่ยวกับคณิตศาสตร์ การตอบมันต้องใช้กลไกเล็ก ๆ หนึ่งชิ้น
Likelihood ของเส้นหนึ่งเส้น
ลิงก์ไปยังส่วน: Likelihood ของเส้นหนึ่งเส้นนี่คือข้ออ้างที่เปลี่ยน “เส้นไหนดีกว่า” ให้เป็นคำถามที่มีคำตอบ
สมมติว่าความกว้างของชิ้นงานคือเส้นบวก random error และสมมติว่า error นั้นถูกสุ่มจาก Gaussian — bell curve — ที่มี mean เป็นศูนย์และ standard deviation :
density ของ Gaussian คือ
ตอนนี้ทำสิ่งที่ perceptron ทำไม่ได้ สำหรับ slope ผู้สมัคร ที่กำหนดไว้ ค่าที่อ่านได้ทุกค่ามี residual และสูตรด้านบนเปลี่ยน residual นั้นให้เป็นตัวเลข: error ที่มีขนาดเท่านี้พอดีน่าเชื่อแค่ไหน ถ้า slope นี้คือความจริง? ค่าที่อ่านได้ซึ่งอยู่บนเส้นได้เลขใหญ่ ค่าที่ห่างไปครึ่งมิลลิเมตรได้เลขเล็ก
ค่าที่อ่านได้เป็นอิสระต่อกัน — เวอร์เนียร์จำชิ้นงานก่อนหน้าไม่ได้ — ดังนั้น product rule บอกว่าความน่าเชื่อของ สมุดบันทึกทั้งเล่ม คือผลคูณของ density แต่ละค่า ผลคูณนั้นคือ likelihood ของ .1 สังเกตทิศทาง เพราะนี่คือทิศทางที่ Bayes' rule พูดถึง: data ถูกตรึงและรู้แล้ว ส่วน parameter เป็นสิ่งที่เปลี่ยน นี่ไม่ใช่ “ความน่าจะเป็นของ slope” แต่มันคือความน่าจะเป็นที่ model กำหนดให้ data ที่คุณได้มาจริง อ่านในฐานะ function ของ slope
SIGMA = 0.12
def gaussian(r, sigma):
return np.exp(-r ** 2 / (2 * sigma ** 2)) / (sigma * np.sqrt(2 * np.pi))
def likelihood(theta):
return np.prod(gaussian(v - theta * u, SIGMA))
for t in (0.25, 0.293, 0.35):
print(f"theta={t} likelihood = {likelihood(t):.6g}")theta=0.25 likelihood = 521.952
theta=0.293 likelihood = 2.42028e+07
theta=0.35 likelihood = 0.190312slope 0.293 ทำให้สมุดบันทึกนี้น่าเชื่อกว่า 0.25 อยู่สี่หมื่นหกพันเท่า และน่าเชื่อกว่า 0.35 อยู่หนึ่งร้อยยี่สิบเจ็ดล้านเท่า Maximum likelihood คือหลักการที่ให้คุณเลือก parameter ที่ทำให้สิ่งที่สังเกตเห็นจริงไม่น่าประหลาดใจที่สุดเท่าที่เป็นไปได้ มันไม่ใช่ theorem แต่เป็นข้อเสนอว่า “ดีที่สุด” ควรหมายถึงอะไร — ข้อเสนอที่มีเนื้อหา เพราะมันบังคับให้คุณระบุ assumption เกี่ยวกับ noise ก่อนที่คุณจะให้คะแนนอะไรได้
ผลคูณพัง
ลิงก์ไปยังส่วน: ผลคูณพังรัน code สามบรรทัดเดียวกันกับกะทำงานหนึ่งเดือนแทนที่จะเป็นกะเดียว แล้ววิธีก็ล้ม
rng = np.random.default_rng(7)
u_big = rng.uniform(-5.25, 5.25, 2000) # 2000 readings, not 20
v_big = 0.30 * u_big + 0.12 * rng.standard_normal(2000)
print("2000 readings, sigma = 0.12 mm :", np.prod(gaussian(v_big - 0.30 * u_big, 0.12)))
noisy = 0.30 * u_big + 2.0 * rng.standard_normal(2000)
print("2000 readings, sigma = 2.00 mm :", np.prod(gaussian(noisy - 0.30 * u_big, 2.0)))
print("largest float64 :", np.finfo(np.float64).max)RuntimeWarning: overflow encountered in reduce
2000 readings, sigma = 0.12 mm : inf
2000 readings, sigma = 2.00 mm : 0.0
largest float64 : 1.7976931348623157e+308การคูณสองพันครั้งและคำตอบคือ inf เปลี่ยน constant หนึ่งตัว — เวอร์เนียร์หยาบกว่าเดิม ทำให้ density ออกมาเล็กกว่า 1 แทนที่จะใหญ่กว่า — แล้ว code เดิมคืนค่า 0.0 คำตอบทั้งคู่ผิดคนละทิศ ไม่มีอันไหนโยน exception ที่คุณจับได้ และอันที่สองไม่แม้แต่จะพิมพ์ warning
คณิตศาสตร์ไม่ได้ผิดอะไร likelihood ที่ setting เหล่านั้นเป็นจำนวน finite ที่นิยามชัดเจน: natural logarithm ของมันคือ 1400.91 ดังนั้นตัวเลขเองอยู่ประมาณ ปัญหาคือคอมพิวเตอร์ของคุณไม่มีตัวเลขนั้น และควรเข้าใจให้ชัดว่ามันมีตัวเลขใดบ้าง เพราะนี่ไม่ใช่ครั้งสุดท้ายที่มันจะตัดสินผลลัพธ์
กำลังสองมาจากไหน
ลิงก์ไปยังส่วน: กำลังสองมาจากไหนวิธีแก้ผลคูณที่ระเบิดคือวิธีปกติ: ใช้ logarithm logarithm เปลี่ยนผลคูณเป็นผลรวม มัน strictly increasing จึงย้ายตำแหน่งของ maximum ไม่ได้ และผลรวมของตัวเลขระดับปานกลางสองพันตัวเป็นสิ่งที่ float64 จัดการได้โดยไม่บ่น ตาม convention เราใช้ negative log-likelihood เพื่อให้ดีขึ้นหมายถึงเล็กลง ตอนนี้แทน density ของ Gaussian แล้วดูว่าเกิดอะไรขึ้น
-
เริ่มจากผลคูณ. likelihood คือ โดย คือ Gaussian density ด้านบน
-
ใส่ minus หน้า log. ผลคูณกลายเป็นผลรวม และ exponential ใน density ถูกตัดกับ logarithm โดยตรง:
- ทิ้งทุกอย่างที่ไม่มี . พจน์แรกเป็น constant ส่วน หน้า sum เป็น positive constant และการ scale function ด้วย positive constant ไม่สามารถย้ายตำแหน่ง minimum ของมันได้ สิ่งที่เหลือคือ
ซึ่งคือผลรวมของ residual ยกกำลังสอง — สิ่งที่เราเริ่มบทนี้ด้วย เพราะมันคือสิ่งแรกที่ใคร ๆ ก็นึกถึง
นี่คือผลลัพธ์ที่บทนี้มีไว้เพื่อให้ได้ และควรพูดแบบไม่ต้องเผื่อ: squared error ไม่ใช่ธรรมเนียม แต่มันคือ negative log-likelihood ของ Gaussian เมื่อตัด constant ออกแล้ว การ minimise squared error คือการกระทำเดียวกันเป๊ะกับการยืนยันว่า error ของคุณเป็น Gaussian และถามว่า parameter ใดทำให้ data ของคุณน่าประหลาดใจน้อยที่สุด คุณยืนยันแบบนั้นมาตลอด เพียงแต่ไม่มีใครบอกคุณ
ความเทียบเท่านี้ตรวจได้ ก็ตรวจเลย: scan slope หกร้อยหนึ่งค่าเดิมด้วย negative log-likelihood เต็มรูปแบบ รวม constant ทั้งหมด และด้วย squared error ธรรมดา
N = v.size
def nll(theta):
r = v - theta * u
return N * np.log(SIGMA * np.sqrt(2 * np.pi)) + np.sum(r ** 2) / (2 * SIGMA ** 2)
nlls = np.array([nll(t) for t in grid])
mses = np.array([mse(t) for t in grid])
print(f"argmin of the negative log-likelihood : theta={grid[nlls.argmin()]:.3f} nll={nlls.min():.6f}")
print(f"argmin of the mean squared error : theta={grid[mses.argmin()]:.3f} mse={mses.min():.6f}")
print("same index:", nlls.argmin() == mses.argmin())argmin of the negative log-likelihood : theta=0.293 nll=-17.001977
argmin of the mean squared error : theta=0.293 mse=0.010115
same index: Trueตัวเลขบนแกนตั้งต่างกัน และหนึ่งในนั้นเป็นลบ ซึ่งผลรวมกำลังสองไม่มีทางเป็น: negative log-likelihood อาจต่ำกว่าศูนย์ได้ เพราะ density อาจเกิน 1 ก้นเดียวกันของหุบเขาเดียวกัน ถึง grid point สุดท้าย
แสดงการไล่สูตรทั้งหมด
จริง ๆ แล้วทิ้งอะไรได้อย่างปลอดภัยบ้าง? กลวิธีเดียวกันนี้ปรากฏในทุกบทที่ derive loss และมันไม่ได้บริสุทธิ์ไร้พิษภัยเสมอไป
การทิ้ง additive constant ปลอดภัยเมื่อมันไม่ขึ้นกับ parameter ที่คุณ optimise และการทิ้ง positive multiplicative constant ปลอดภัย เพราะ สำหรับ ใด ๆ ทั้งสองอย่างล้มเหลวทันทีที่ ถูก fit ด้วย: ตอนนั้น ไม่ใช่ constant เลย มันคือพจน์ที่หยุด model ไม่ให้ประกาศว่า และได้ plausibility เป็น infinity นั่นคือ section ถัดไปพอดี
มันล้มเหลวต่างออกไปอีกในบทที่ 3: multiplicative constant ไม่ย้าย minimum แต่ scale gradient และ gradient ถูกคูณด้วย learning rate การหารด้วย เพื่อให้ได้ mean squared error แทน sum มองไม่เห็นในคำตอบ แต่มองเห็นมากใน training run — ถ้าใช้ sum การเพิ่ม batch size เป็นสองเท่าจะทำให้ทุก step ที่คุณเดินใหญ่เป็นสองเท่า
Sigma ก็ไม่ได้ฟรี
ลิงก์ไปยังส่วน: Sigma ก็ไม่ได้ฟรีเราตรึง ไว้ที่ 0.12 โดย fiat และไม่มีใครในโรงงานรู้การกระจายของ error ของเวอร์เนียร์ ให้ถือมันเป็น unknown ตัวที่สอง และปล่อยให้ maximum likelihood ตัดสินด้วย ตรงนี้ constant term ที่เราเพิ่งทิ้งไปกลับมา เพราะมันเป็นสิ่งเดียวที่ขวางระหว่าง model กับการอ้าง precision สมบูรณ์แบบ
r = v - 0.293 * u
sigmas = np.arange(0.01, 1.0001, 0.0001)
nll_sigma = N * np.log(sigmas * np.sqrt(2 * np.pi)) + np.sum(r ** 2) / (2 * sigmas ** 2)
print("best sigma on the grid :", round(float(sigmas[nll_sigma.argmin()]), 4))
print("sqrt(mean squared residual) :", round(float(np.sqrt(np.mean(r ** 2))), 4))best sigma on the grid : 0.1006
sqrt(mean squared residual) : 0.1006สองค่านี้ตรงกันถึงสี่ตำแหน่งทศนิยม และไม่ใช่เรื่องบังเอิญ: differentiate expression นั้นแล้วตั้งให้เป็นศูนย์ จะได้ พอดี ดังนั้น mean squared error ไม่ได้แค่ คล้าย variance ภายใต้ model นี้ มัน คือ maximum-likelihood estimate ของ variance ของ noise — ตัวเลขที่คุณ minimise มาตลอดคือ estimate ว่า sensor ของคุณ noisy แค่ไหน
มีรอยย่นหนึ่งอย่าง พูดง่ายแต่แพงที่จะค้นพบใหม่ภายหลัง: estimate นั้น bias ต่ำ เพราะ residual ถูกวัดเทียบกับ fit ที่ถูกเลือกมาเพื่อทำให้มันเล็กอยู่แล้ว ลอง simulate — สมุดบันทึกสองแสนเล่ม เล่มละยี่สิบค่า อ่านจาก distribution ที่ true variance เท่ากับ 1 พอดี โดย parameter หนึ่งตัวของ fit ถูก estimate จากค่าที่อ่านได้เอง การหาร sum of squares ด้วย ให้ค่าเฉลี่ย 0.9501; หารด้วย ให้ 1.0001; และ คือ 0.95 ตรง ๆ parameter ทุกตัวที่คุณ fit ใช้ degree of freedom หนึ่งตัว และนี่คือ instance ที่เล็กที่สุดที่มองเห็นได้ของปัญหาใหญ่กว่านั้นมาก: model มักดูดีกว่าบน data ที่มันถูก fit มา บทที่ 4 เปลี่ยนสิ่งนั้นเป็นวินัยของการกัน data ไว้ และบทที่ 6 ตั้งชื่อให้ผลกระทบนี้
Loss คือข้ออ้างเกี่ยวกับ noise
ลิงก์ไปยังส่วน: Loss คือข้ออ้างเกี่ยวกับ noiseถ้า squared error ยืนยันว่า noise เป็น Gaussian คำถามถัดไปคือจะเกิดอะไรขึ้นเมื่อข้อยืนยันนั้นเป็นเท็จ ไม่ใช่เท็จเล็กน้อย — แต่เท็จแบบที่การวัดจริง ๆ เท็จ
บนพื้นโรงงาน ค่าที่อ่านจากเวอร์เนียร์ส่วนใหญ่ดีในระดับหนึ่งในสิบมิลลิเมตร และหนึ่งหรือสองครั้งต่อกะ เศษ swarf ชิ้นหนึ่งเข้าไปอยู่ใต้ปากวัด ทำให้ค่าที่อ่านได้เพี้ยนไปหลายมิลลิเมตร error แบบนั้นคือ heavy-tailed: เล็กเกือบตลอดเวลา ใหญ่เป็นครั้งคราว และใหญ่บ่อยกว่าที่ bell curve อนุญาตไว้มาก Cauchy distribution คือ model มาตรฐานที่สะอาดสำหรับพฤติกรรมนี้ และ density ของมันเรียบง่ายพอ ๆ กับของ Gaussian:
ความต่างอยู่ที่ tail: Gaussian ลดลงแบบ เร็วอย่างโหด และ Cauchy ลดลงแบบ แทบไม่ลดเลย ผลลัพธ์เห็นง่ายกว่าพูด:
rng = np.random.default_rng(3)
g = 0.12 * rng.standard_normal(10 ** 6) # Gaussian noise
c = 0.12 * rng.standard_cauchy(10 ** 6) # Cauchy noise, same scale
for k in (10 ** 2, 10 ** 3, 10 ** 4, 10 ** 5, 10 ** 6):
print(f"{k:>9,} samples gaussian var {g[:k].var():.4f} cauchy var {c[:k].var():10.2f}") 100 samples gaussian var 0.0164 cauchy var 0.26
1,000 samples gaussian var 0.0146 cauchy var 59.88
10,000 samples gaussian var 0.0145 cauchy var 358.17
100,000 samples gaussian var 0.0144 cauchy var 3097.98
1,000,000 samples gaussian var 0.0144 cauchy var 32886.10sample variance ของ Gaussian นิ่งที่ 0.0144 ซึ่งคือ และอยู่ตรงนั้น ส่วนของ Cauchy ไต่ขึ้น และไต่ต่อไปตราบเท่าที่คุณ sample เพราะไม่มีอะไรให้มัน converge ไปหา: Cauchy distribution ไม่มี variance และไม่มี mean ด้วย squared error ซึ่งงานทั้งหมดคือ minimise ค่าเฉลี่ยของกำลังสอง กำลังถูกขอให้หาปริมาณที่ไม่มีอยู่จริง
ดังนั้นนี่คือกะหนึ่งที่เวอร์เนียร์ถูกหลอก ยี่สิบชั่วโมงเดียวกัน ใบมีดเดียวกัน drift เดียวกัน 0.30 มิลลิเมตรต่อชั่วโมง — มีเพียง noise ที่ตอนนี้เป็น Cauchy Fit มันสองครั้ง: ครั้งหนึ่งโดย minimise squared residuals อีกครั้งโดย minimise negative log-likelihood ของ noise ที่สร้าง data จริง ๆ เคล็ดลับ centring ช่วยอะไรไม่ได้ตรงนี้ — มันตรึง intercept ได้เฉพาะสำหรับ squared error — ดังนั้น fit ทั้งคู่จึงใช้ brute force บน grid ของ intercept และ slope เพราะเรายังไม่มีวิธีหาก้นหุบเขานอกจากต้องไปเยือนมัน
SWARF = np.array([
(0.5, 20.08), (1.0, 21.95), (1.5, 20.86), (2.0, 27.51), (2.5, 20.64),
(3.0, 20.75), (3.5, 21.01), (4.0, 21.03), (4.5, 21.37), (5.0, 20.60),
(5.5, 22.03), (6.0, 21.95), (6.5, 21.98), (7.0, 22.01), (7.5, 21.73),
(8.0, 22.97), (8.5, 22.60), (9.0, 22.66), (9.5, 22.44), (10.0, 22.78),
])
hs, ys = SWARF[:, 0], SWARF[:, 1]
A = np.arange(18.0, 22.001, 0.005) # 801 intercepts
B = np.arange(-0.20, 0.8001, 0.002) # 501 slopes
R = ys - (A[:, None, None] + B[None, :, None] * hs) # every line against every point
SCALE = 0.12
square = np.sum(R ** 2, axis=2) # least squares
cauchy = np.sum(np.log(1 + (R / SCALE) ** 2), axis=2) # Cauchy likelihood
for name, surface in (("least squares", square), ("Cauchy likelihood", cauchy)):
i, j = np.unravel_index(surface.argmin(), surface.shape)
print(f"{name:>18}: width = {A[i]:.3f} + {B[j]:.4f} * hours"
f" -> 23.5 mm at hour {(23.5 - A[i]) / B[j]:.2f}")
print(f"{'the truth':>18}: width = 20.000 + 0.3000 * hours"
f" -> 23.5 mm at hour {(23.5 - 20.0) / 0.30:.2f}")
print(f"{A.size * B.size:,} candidate lines evaluated")บรรทัดที่ไฮไลต์สองบรรทัดคือความต่างทั้งหมดระหว่าง fit ทั้งสอง เอา log ของ Cauchy density ทิ้ง constant เหมือนเดิม และ คือสิ่งที่เหลือ สูตรเดิม ข้ออ้างเกี่ยวกับ noise ต่างกัน
least squares: width = 21.380 + 0.1080 * hours -> 23.5 mm at hour 19.63
Cauchy likelihood: width = 19.935 + 0.3020 * hours -> 23.5 mm at hour 11.80
the truth: width = 20.000 + 0.3000 * hours -> 23.5 mm at hour 11.67
401,301 candidate lines evaluatedLeast squares รายงาน drift 0.108 มิลลิเมตรต่อชั่วโมง ประมาณหนึ่งในสามของอัตราจริง และสรุปว่าใบมีดยังใช้ได้ถึงชั่วโมงที่ 19.6 คำตอบจริงคือชั่วโมงที่ 11.7 หากทำตาม fit นั้น โรงงานจะเดินเครื่อง press ต่ออีกแปดชั่วโมง ผลิตชิ้นงานที่เกิน tolerance ด้วยอำนาจของ loss function ที่เป็นมาตรฐานที่สุดในสนามนี้ Cauchy fit ใช้ค่าที่อ่านได้ยี่สิบค่าเดียวกัน grid เดียวกัน และต่างกันแค่ code หนึ่งบรรทัด ลงที่ชั่วโมง 11.8
มีข้อโต้แย้งสองข้อที่ควรตอบ เพราะทั้งคู่คือสิ่งแรกที่วิศวกรที่ดีจะพูด
outlier เห็นชัด — ก็ลบทิ้งสิ. คุณทำได้ และมันช่วย และยังไม่พอ การลบค่าที่อ่านได้ที่แย่ที่สุดค่าเดียวทำให้ least-squares slope ขยับจาก 0.108 เป็น 0.239 ซึ่งยังทำให้เปลี่ยนใบมีดที่ชั่วโมง 13.1 ช้าไปหนึ่งชั่วโมงครึ่ง; ลบค่าที่แย่ที่สุด fit ใหม่ แล้วลบสิ่งที่แย่ที่สุด ตอนนี้ จะได้ 0.286 — และสังเกตว่านี่เป็น procedure แล้ว ไม่ใช่ observation: ถ้าลบ residual สองค่าที่ใหญ่ที่สุดของ fit เดิมแทน คุณจะลงที่ 0.223 แต่ตอนนี้คุณได้ตัดสินใจเชิงดุลพินิจที่เขียนลงหรือปกป้องไม่ได้ และการ automate กฎก็ไม่ช่วยมัน: drop-the-largest-residual-then-refit เมื่อรันบนกะจำลองหนึ่งพันกะ มี median slope error 0.0177 เทียบกับ likelihood fit ที่ 0.0100 และผิดเกิน 0.05 ใน 14.7% ของกะ เทียบกับ 1.3% การลบคือ patch บน assumption ที่ผิด likelihood ไม่ต้องการ patch เพราะมันไม่เคย assume ว่า outlier เป็นไปไม่ได้
คุณเลือก dataset ที่โชคดี. ข้อโต้แย้งนั้นถูกต้องพอดี และนั่นคือเหตุผลที่ experiment สุดท้าย simulate กะอิสระหนึ่งพันกะ แล้ว refit ทั้งสองวิธีในแต่ละกะ
A = np.arange(18.0, 22.001, 0.02) # a coarser grid: a thousand fits to do
B = np.arange(-0.20, 0.8001, 0.005)
lines = A[:, None, None] + B[None, :, None] * hs
rng = np.random.default_rng(2026)
err_sq, err_ca = [], []
for _ in range(1000): # 1000 independent shifts
ys = 20.00 + 0.30 * hs + SCALE * rng.standard_cauchy(hs.size)
R = ys - lines
_, j = np.unravel_index(np.sum(R ** 2, axis=2).argmin(), (A.size, B.size))
_, q = np.unravel_index(np.sum(np.log1p((R / SCALE) ** 2), axis=2).argmin(), (A.size, B.size))
err_sq.append(abs(B[j] - 0.30))
err_ca.append(abs(B[q] - 0.30))
err_sq, err_ca = np.array(err_sq), np.array(err_ca)
for name, e in (("least squares", err_sq), ("Cauchy likelihood", err_ca)):
print(f"{name:>18}: median slope error {np.median(e):.4f} mm/h"
f" off by more than 0.05 in {100 * np.mean(e > 0.05):4.1f}% of shifts"
f" worst {e.max():.3f}")
print(f"the likelihood fit is the closer of the two in {100 * np.mean(err_ca < err_sq):.1f}% of shifts") least squares: median slope error 0.0350 mm/h off by more than 0.05 in 40.4% of shifts worst 0.500
Cauchy likelihood: median slope error 0.0100 mm/h off by more than 0.05 in 1.3% of shifts worst 0.090
the likelihood fit is the closer of the two in 75.6% of shiftsใช้ median ไม่ใช่ mean ด้วยเหตุผลเดียวกับทุกอย่างใน section นี้: error ของ least-squares ถูกขับโดย Cauchy ดังนั้นค่าเฉลี่ยของมันไม่ใช่สิ่งที่ stable พอจะรายงาน Least squares ผิดหนักสองกะในห้า; likelihood fit ผิดหนักหนึ่งกะในเจ็ดสิบเจ็ด และ failure ที่แย่ที่สุดของมันในหนึ่งพันกะน้อยกว่าหนึ่งในห้าของ failure ที่แย่ที่สุดของ least squares
ทั้งหมดนี้ไม่ได้ทำให้ squared error แย่ แต่มันทำให้ squared error เฉพาะเจาะจง และ arithmetic บอกชัดว่าทำไม เอา residual 0.1 mm กับ 7 mm เมื่อยกกำลังสอง ค่าที่อ่านได้แย่มีส่วนใน total มากกว่าค่าดี 4,900 เท่า เส้นจึงถูกลากทั้งตัวเข้าหามัน; ภายใต้ Cauchy log-likelihood residual สองค่านี้มีส่วนเป็น 0.527 และ 8.133 อัตราส่วน 15.4 ค่าที่อ่านได้แย่ยังนับอยู่ แค่มันไม่ได้เป็นคนตัดสิน นี่คือจุดเริ่มของ robust statistics ซึ่ง loss ของ Huber ปี 1964 แบ่งครึ่งความต่างโดยทำตัวเป็น quadratic สำหรับ residual เล็ก และ linear สำหรับ residual ใหญ่,7 และที่ Tukey เคยแสดงไว้แล้วว่าการปนเปื้อนเพียงเล็กน้อยทำให้ sample variance เป็นเครื่องมือที่แย่กว่า mean absolute deviation ได้แค่ไหน8
มีบันทึกประวัติศาสตร์หนึ่งอย่างที่ดีเกินกว่าจะละไว้ Least squares ถูกตีพิมพ์ก่อน โดย Legendre ในปี 1805 ในฐานะอุปกรณ์พีชคณิตที่สะดวก โดยไม่มีเหตุผลรองรับนอกจากมันใช้งานได้9 สี่ปีต่อมา Gauss เดินเหตุผลย้อนกลับ: เขาถือว่า arithmetic mean เป็นวิธีที่ถูกต้องในการรวมการวัดซ้ำ ๆ ถามว่า error distribution ใดทำให้ mean เป็นค่าที่น่าจะเป็นที่สุด และแสดงว่าโดยพื้นฐานแล้วมีอยู่เพียงอันเดียว — อันที่ตอนนี้ตั้งชื่อตามเขา10 derivation ในบทนี้เป็นของเขา มีอายุมากกว่าสองศตวรรษ และยังเป็นส่วนที่คอร์สส่วนใหญ่ละไว้
ตอนนี้คุณพูดอะไรได้ และยังทำอะไรไม่ได้
ลิงก์ไปยังส่วน: ตอนนี้คุณพูดอะไรได้ และยังทำอะไรไม่ได้สิ่งที่ได้มาแล้ว. loss function คือกฎให้คะแนน และอันดับที่มันสร้างเป็นคุณสมบัติของกฎ ไม่ใช่ของผู้สมัคร loss ทุกตัวในคอร์สนี้คือ negative log-likelihood ของ assumption บางอย่างเกี่ยวกับ noise เมื่อตัด constant ออก — Gaussian ให้ squared error ตรงนี้, Bernoulli ให้ cross-entropy ในบทที่ 4 และ categorical distribution เหนือ vocabulary ให้ next-token loss ในบทที่ 8 สูตรไม่เคยเปลี่ยน: ระบุ noise, เขียน likelihood, ใส่ minus หน้า log และเมื่อ assumption ผิด model ไม่ได้แค่ไม่แม่น แต่มันผิดไปในทิศทางที่คุณทำนายได้
ยังขาดอยู่. เราหาก้นหุบเขาโดยไปเยือนทุกจุดในนั้น วิธีนี้ใช้ได้กับหนึ่ง parameter และผู้สมัครหกร้อยค่า และยังรอดกับสอง parameter ที่มีผู้สมัคร 401,301 ค่าในหนึ่งในห้าวินาที สาม parameter ที่ resolution เดียวกันคือผู้สมัคร 201,051,801 ค่า และไม่พออยู่ใน array เดียวอีกต่อไป; network เล็ก ๆ ใน บทที่ 5 มี parameter หลายพันตัว และ models ที่บทที่ 10 ใส่ราคาให้มีเป็นพันล้าน Brute force ตรงนี้ไม่ใช่ช้า แต่มันเป็นไปไม่ได้ทาง arithmetic และไม่มีอะไรในบทนี้เสนอทางเลือก
อย่างไรก็ตาม ลองมองกลับไปที่หุบเขา ยืนอยู่ที่ พร้อม loss 0.0822 ทิศทาง “ลงเขา” ไม่ใช่เรื่องลึกลับ — คุณเห็นมันบนหน้าได้เลย curve ลาดลงไปทางขวา ถ้าคุณถาม loss function ได้ว่ามันลาดเอียงไปทางไหน ณ จุดที่คุณยืนอยู่ โดยไม่ต้อง evaluate มันที่อื่น คุณก็เดินไปทางนั้นหนึ่ง step ถามอีกครั้ง แล้วทำซ้ำจนพื้นราบ
คำถามนั้นมีชื่อ slope ของ function ณ จุดหนึ่งคือ derivative และสำหรับ function ของ parameter หลายตัว ชุดของ slope ในทุกทิศพร้อมกันคือ gradient บทที่ 1 ใช้มันไม่ได้ เพราะ error ของ perceptron เป็นบันไดที่ไม่มี slope ให้ถาม บทนี้สร้างสิ่งที่ดีกว่าแล้ว: loss ที่ smooth ทุกที่ และมาจาก assumption ที่ระบุไว้ ไม่ใช่จาก preference
ดังนั้นคำถามสำหรับบทที่ 3 จึงไม่ใช่อีกต่อไปว่า slope มีอยู่หรือไม่ แต่คือจะคำนวณมันอย่างไร ทำไมการขยับ สวนทาง กับมันจึงลงเขาแทนที่จะขึ้นเขา — เครื่องหมายที่แทบทุกคอร์สขอให้คุณเชื่อ — และควรเดินไกลแค่ไหนก่อนถามใหม่ ซึ่งกลายเป็นตัวเลขเดียวที่ตัดสินว่า training run จะ converge, แกว่งรอบคำตอบไปตลอดกาล หรือวิ่งออกไป infinity
แหล่งอ้างอิงและวิธีการ
ลิงก์ไปยังส่วน: แหล่งอ้างอิงและวิธีการควรอ่านควบคู่กับบทนี้ด้วย: Prince, Understanding Deep Learning §5.1–5.2 และ Appendix C ซึ่งสร้าง loss ทุกตัวในหนังสือจาก maximum likelihood ตามลำดับที่ใช้ตรงนี้; Goodfellow, Bengio and Courville, Deep Learning §3.1–3.11 และ §5.5 ซึ่ง section maximum-likelihood ยัง derive KL divergence ที่บทที่ 4 ต้องใช้; Murphy, Probabilistic Machine Learning: An Introduction chapter 2 และ §4.2 ว่า maximum likelihood รับประกันและไม่รับประกันอะไร; Deisenroth, Faisal and Ong, Mathematics for Machine Learning §6.1–6.4 สำหรับ sum rule, product rule และ Bayes' rule แบบทำถูกต้อง; note สั้น ๆ ของ Tom Mitchell จาก CMU เรื่อง Estimating Probabilities: MLE and MAP (2016); และ §22.7 ของ Dive into Deep Learning ซึ่งไปถึงผลลัพธ์เดียวกันใน code ที่รันได้
รายการอ้างอิง
ลิงก์ไปยังส่วน: รายการอ้างอิง-
Fisher, R. A. On the mathematical foundations of theoretical statistics. Philosophical Transactions of the Royal Society A 222, pp. 309–368 (1922). ที่ซึ่ง likelihood ถูกวางเป็นวิธีทั่วไป พร้อมกับ “parameter”, “statistic”, sufficiency และ efficiency ส่วนการตั้งชื่อเองและการแยกออกจาก probability มาก่อนหนึ่งปี: Fisher, R. A., On the “probable error” of a coefficient of correlation deduced from a small sample, Metron 1, pp. 3–32 (1921), pp. 24–25. ↩
-
IEEE Standard for Floating-Point Arithmetic, IEEE 754-2019. นิยาม binary32 และ binary16 รวมถึงกฎ rounding ที่ทำให้ experiment การบวกออกมาตามนั้น ↩
-
Kalamkar, D. et al. A Study of BFLOAT16 for Deep Learning Training. arXiv:1905.12322 (2019). parameter ของ format และเหตุผลในการแลก mantissa bits กับ exponent bits ↩
-
Micikevicius, P. et al. Mixed Precision Training. ICLR 2018, arXiv:1710.03740. loss scaling และขนาด gradient ที่วัดได้ซึ่งทำให้มันจำเป็นใน float16 ↩
-
Goldberg, D. What Every Computer Scientist Should Know About Floating-Point Arithmetic. ACM Computing Surveys 23(1), pp. 5–48 (1991). ยังเป็นคำอธิบายเดี่ยวที่ดีที่สุดว่าทำไมลำดับการบวกสองแบบจึงไม่ตรงกัน ↩
-
Kahan, W. Pracniques: further remarks on reducing truncation errors. Communications of the ACM 8(1), p. 40 (1965). compensated summation ในครึ่งหน้า ↩
-
Huber, P. J. Robust estimation of a location parameter. The Annals of Mathematical Statistics 35(1), pp. 73–101 (1964). loss ที่เป็น quadratic ใกล้ศูนย์และ linear ใน tail ซึ่ง derive มา ไม่ใช่ patch ต่อกัน ↩
-
Tukey, J. W. A survey of sampling from contaminated distributions, in Contributions to Probability and Statistics (Stanford University Press, 1960), pp. 448–485. ↩
-
Legendre, A. M. Nouvelles méthodes pour la détermination des orbites des comètes (Paris, 1805), appendix Sur la méthode des moindres quarrés. การตีพิมพ์ least squares ครั้งแรก ในฐานะเครื่องมือคำนวณ ↩
-
Gauss, C. F. Theoria Motus Corporum Coelestium (Hamburg, 1809), Book II, §§175–179. เหตุผลจาก arithmetic mean ไปสู่ normal error law และจากตรงนั้นไปสู่ least squares ↩