🧪 Lab 3 จาก 6 · Spark MLlib

Spark MLlib: สร้าง Text Classifier ตัวแรก

ต่อยอดจาก Pipeline API ในบทที่ 9 — สร้างตัวจำแนกข้อความสแปม/ไม่สแปมแบบง่าย ด้วย Tokenizer → HashingTF → LogisticRegression ครบวงจรตั้งแต่ train จนถึงประเมินผล

🎯
เป้าหมายของ Lab นี้: สร้าง ML Pipeline ที่สมบูรณ์, แบ่งข้อมูล train/test, ประเมินผลด้วย accuracy/AUC, บันทึกและโหลดโมเดลกลับมาใช้

1เตรียมข้อมูลตัวอย่าง

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("Lab3-MLlib") \
    .master("spark://spark-master:7077") \
    .getOrCreate()

training = spark.createDataFrame([
    (0, "win free money now", 1.0),
    (1, "click here to claim your prize", 1.0),
    (2, "congratulations you won a lottery", 1.0),
    (3, "free gift card waiting for you", 1.0),
    (4, "meeting scheduled for tomorrow afternoon", 0.0),
    (5, "please review the attached document", 0.0),
    (6, "let us know if you have questions", 0.0),
    (7, "quarterly report is ready for review", 0.0),
    (8, "urgent claim your reward immediately", 1.0),
    (9, "team lunch this friday at noon", 0.0),
], ["id", "text", "label"])

training.show(truncate=False)
หมายเหตุเรื่องขนาดข้อมูล
ข้อมูลตัวอย่างนี้มีแค่ 10 แถวเพื่อความง่ายในการทำความเข้าใจ pipeline — ในทางปฏิบัติจริง โมเดลข้อความต้องการข้อมูล train หลักพันถึงหลักล้านตัวอย่างเพื่อให้ generalize ได้ดี บทเรียนนี้เน้นที่ "โครงสร้าง pipeline ถูกต้อง" ไม่ใช่ "โมเดลแม่นยำ"

2สร้าง ML Pipeline

เชื่อมโยงกับบทที่ 9
สามขั้นตอนนี้คือ Transformer → Transformer → Estimator ตามที่อธิบายไว้ในทฤษฎี
from pyspark.ml import Pipeline
from pyspark.ml.feature import Tokenizer, HashingTF
from pyspark.ml.classification import LogisticRegression

tokenizer = Tokenizer(inputCol="text", outputCol="words")
hashingTF = HashingTF(inputCol="words", outputCol="features", numFeatures=1000)
lr = LogisticRegression(maxIter=10, regParam=0.01, labelCol="label", featuresCol="features")

pipeline = Pipeline(stages=[tokenizer, hashingTF, lr])
model = pipeline.fit(training)

print("Train เสร็จแล้ว — pipeline มี", len(pipeline.getStages()), "ขั้นตอน")

3ทดสอบกับข้อความใหม่

test = spark.createDataFrame([
    (10, "you have won free money click now"),
    (11, "can we reschedule the meeting"),
    (12, "claim your urgent prize today"),
    (13, "attached is the report you requested"),
], ["id", "text"])

predictions = model.transform(test)
predictions.select("id", "text", "prediction", "probability").show(truncate=False)
สิ่งที่ควรสังเกต
คอลัมน์ prediction ควรเป็น 1.0 สำหรับข้อความที่มีคำคล้าย "free", "win", "claim", "prize" และ 0.0 สำหรับข้อความทั่วไป — เพราะ HashingTF แปลงคำเป็น feature vector แบบ bag-of-words ทำให้โมเดลเรียนรู้ว่าคำเหล่านี้สัมพันธ์กับ label สแปม

4ประเมินผลอย่างถูกวิธี: Train/Test Split และ Metric

ข้อควรระวังเชิงระเบียบวิธี
ตัวอย่างข้างบน test บนข้อมูลที่ไม่ได้อยู่ใน training set ก็จริง แต่ในงานจริงต้องแบ่งข้อมูลอย่างเป็นระบบด้วย randomSplit และวัดผลด้วย metric ที่เหมาะสม ไม่ใช่แค่ดูผลลัพธ์ด้วยตาเปล่า
from pyspark.ml.evaluation import BinaryClassificationEvaluator, MulticlassClassificationEvaluator

# สมมติมี dataset ใหญ่กว่านี้ — สาธิตวิธีแบ่งข้อมูลที่ถูกต้อง
train_df, test_df = training.randomSplit([0.8, 0.2], seed=42)
model = pipeline.fit(train_df)
result = model.transform(test_df)

evaluator_auc = BinaryClassificationEvaluator(labelCol="label", metricName="areaUnderROC")
evaluator_acc = MulticlassClassificationEvaluator(labelCol="label", predictionCol="prediction", metricName="accuracy")

print("AUC:", evaluator_auc.evaluate(result))
print("Accuracy:", evaluator_acc.evaluate(result))
Metricใช้เมื่อไร
Accuracyเหมาะเมื่อ label ทั้งสองคลาสมีจำนวนใกล้เคียงกัน (balanced) — ถ้าข้อมูลเบ้มาก accuracy จะหลอกตาได้ง่าย
AUC (Area Under ROC)ทนต่อความเบ้ของ class ได้ดีกว่า accuracy เหมาะกับปัญหาสแปม/ไม่สแปมที่มักมีข้อมูลเบ้ไปทาง "ไม่สแปม" ในโลกจริง

5บันทึกและโหลดโมเดลกลับมาใช้

ทำไม Pipeline API ถึงมีประโยชน์ตรงนี้
เพราะทุกขั้นตอน (tokenizer, hashingTF, model) ถูกบันทึกรวมเป็นก้อนเดียว ตอน production ไม่ต้องเขียนโค้ด preprocessing ซ้ำ แค่โหลด PipelineModel กลับมาแล้วเรียก transform() ได้ทันที
# บันทึกโมเดล
model.write().overwrite().save("/home/jovyan/data/spam-classifier-model")

# โหลดกลับมาใช้ในเซสชันใหม่ (หรือ notebook อื่น)
from pyspark.ml import PipelineModel
loaded_model = PipelineModel.load("/home/jovyan/data/spam-classifier-model")

loaded_model.transform(test).select("text", "prediction").show(truncate=False)

6ตรวจ Transformer, Estimator และ Model ทีละ Stage

อย่ามอง Pipeline เป็นกล่องเดียว ให้พิมพ์ Schema/ตัวอย่างหลังแต่ละ Stage เพื่อรู้ว่าคอลัมน์เปลี่ยนอย่างไร

tokenized = tokenizer.transform(training)
tokenized.select("text", "words").show(truncate=False)

featured = hashingTF.transform(tokenized)
featured.select("words", "features").show(truncate=False)

# LogisticRegression เป็น Estimator ต้อง fit ก่อน
lr_model = lr.fit(featured)
lr_model.transform(featured).select(
    "text", "rawPrediction", "probability", "prediction"
).show(truncate=False)

Tokenizer/HashingTF เป็น Transformer เพราะไม่ต้องเรียนสถิติชุดข้อมูล ส่วน LogisticRegression เป็น Estimator และคืน Model ซึ่งเป็น Transformer Pipeline.fit จึงเดินผ่าน Stage และ Fit เฉพาะ Estimator

7แก้ Data Leakage ด้วยการแบ่งก่อน Fit

ถ้าเพิ่ม IDF, StringIndexer, StandardScaler หรือ Feature Selector ขั้นเหล่านี้เรียนจากข้อมูล ห้าม Fit บนข้อมูลทั้งหมดก่อนแบ่ง Train/Test

train, test = data.randomSplit([0.8, 0.2], seed=42)
pipeline_model = pipeline.fit(train)
predictions = pipeline_model.transform(test)

# ผิดหลัก: fitted = pipeline.fit(data) แล้วค่อยแบ่ง prediction
# เพราะ preprocessing และ model เห็น distribution ของ test
Pipeline ไม่ป้องกัน Leakage ให้เอง
Pipeline ทำให้ขั้นตอนเป็นระเบียบ แต่ถ้าเรียก fit(data) ผิดชุด มันก็รั่วอย่างเป็นระเบียบ ต้องแบ่งข้อมูลตามเวลา/กลุ่ม/ผู้ใช้ให้ตรงโลกจริงก่อน Fit

8Dataset เล็กเกินไปทำให้ Metric แกว่ง

ข้อมูลตัวอย่างไม่กี่สิบแถวมีไว้เรียน API ไม่เหมาะสรุปคุณภาพโมเดล สุ่ม Split คนละ Seed อาจทำให้ Test มีเพียงคลาสเดียว ทดลองหลาย Seed แล้วบันทึกจำนวน Positive/Negative

from pyspark.sql.functions import count

for seed in [1, 2, 3, 42, 99]:
    tr, te = data.randomSplit([0.8, 0.2], seed=seed)
    print("seed", seed, "train", tr.count(), "test", te.count())
    te.groupBy("label").count().show()

งานส่งควรเพิ่มข้อมูลอย่างน้อยหลักร้อยและแยก Holdout ที่ไม่แตะระหว่างพัฒนา หากข้อความจากผู้ส่งคนเดียวอยู่ทั้ง Train/Test โมเดลอาจจำรูปแบบผู้ส่ง จึงพิจารณา Group Split

9Confusion Matrix และ Metric ที่อ่านได้

predictions.groupBy("label", "prediction").count() \
    .orderBy("label", "prediction").show()

tp = predictions.filter("label = 1 AND prediction = 1").count()
fp = predictions.filter("label = 0 AND prediction = 1").count()
fn = predictions.filter("label = 1 AND prediction = 0").count()

precision = tp / (tp + fp) if tp + fp else 0
recall = tp / (tp + fn) if tp + fn else 0
print("precision", precision, "recall", recall)

Spam Filter อาจเลือก Threshold ตาม Cost: False Positive ทำให้เมลสำคัญหาย ส่วน False Negative ทำให้ผู้ใช้รำคาญ ค่า 0.5 ไม่ใช่กฎศักดิ์สิทธิ์ ทดลอง Threshold หลายค่าและสร้างตาราง Precision/Recall

10Hash Collision และ Feature Space

HashingTF ไม่เก็บ Vocabulary แต่คำต่างกันชน Index เดียวได้ ทดลอง numFeatures 16, 128, 1024, 16384 แล้วดู Metric, Model Size และเวลา

numFeaturesCollisionMemory/Model
เล็กสูงเล็ก
ใหญ่ต่ำลงใหญ่ขึ้น

เพิ่ม IDF เพื่อให้น้ำหนักคำหายาก แต่ IDF เป็น Estimator ต้อง Fit จาก Train เท่านั้น

from pyspark.ml.feature import IDF
idf = IDF(inputCol="rawFeatures", outputCol="features")

11Cross-validation และต้นทุนที่คูณ

from pyspark.ml.tuning import ParamGridBuilder, CrossValidator

grid = ParamGridBuilder() \
    .addGrid(hashingTF.numFeatures, [128, 1024, 4096]) \
    .addGrid(lr.regParam, [0.0, 0.01, 0.1]) \
    .build()

cv = CrossValidator(
    estimator=pipeline,
    estimatorParamMaps=grid,
    evaluator=evaluator_auc,
    numFolds=3,
    parallelism=2
)
cv_model = cv.fit(train)

3 × 3 Parameter Maps × 3 Folds = 27 Fits ยังไม่รวม Final Model การขนานลด Wall-clock แต่ไม่ลด Compute Cost ควรเริ่ม Baseline และ Search Space ที่มีเหตุผล

12Error Analysis: อ่านสิ่งที่โมเดลทายผิด

predictions.filter("label != prediction") \
  .select("text", "label", "prediction", "probability") \
  .show(50, truncate=False)

จัดกลุ่ม Error เช่นคำโฆษณาในข้อความปกติ, ภาษาไทย/อังกฤษ, URL, ข้อความสั้น, Spam แบบใหม่ แล้วตัดสินว่าจะเพิ่มข้อมูล ปรับ Tokenization หรือเปลี่ยน Model อย่า Tune ตัวเลขโดยไม่อ่านตัวอย่างผิด

13Training–Serving Skew และ Model Contract

โหลด PipelineModel ช่วยให้ Tokenizer/Hashing เหมือนตอนฝึก แต่ Production ยังต้องตรงเรื่อง Schema, Null, Encoding และข้อความยาว สร้าง Contract และ Test ตัวอย่างเดียวกันก่อน/หลัง Save–Load

before = model.transform(test).select("text", "prediction").orderBy("text")
loaded = PipelineModel.load("/home/jovyan/data/spam-classifier-model")
after = loaded.transform(test).select("text", "prediction").orderBy("text")

assert before.collect() == after.collect()

บันทึก Model Version, Data Version, Code/Library Version, Parameters, Metric และ Training Time ข้าง Model File ไม่ใช่เก็บเพียงโฟลเดอร์ชื่อ final_model_really_final

14สิ่งที่ต้องส่งและสรุปขั้นตอนถัดไป

สรุปและขั้นตอนถัดไป: Lab นี้ฝึก Batch ML ที่มีจุดจบ Lab 4 จะเปลี่ยน Input เป็น Stream ที่ไม่มีวันจบ เราต้องเพิ่ม Event Time, Window, Watermark, State และ Checkpoint โดยยังรักษาหลักเดียวกันคืออย่าให้ผลลัพธ์ “รันได้” บังคำถามว่ามันนับอะไรและลืมเมื่อใด

แบบฝึกหัดก่อนไป Lab 4
  1. เพิ่มข้อมูล training อีก 10-20 ตัวอย่าง (ทั้งสแปมและไม่สแปม) แล้ว train ใหม่ — สังเกตว่า prediction บนข้อความทดสอบเปลี่ยนไปไหม
  2. ลองเปลี่ยน numFeatures ใน HashingTF จาก 1000 เป็น 20 แล้วสังเกตผลกระทบต่อความแม่นยำ (คำใบ้: hash collision จะเกิดบ่อยขึ้นเมื่อ feature space เล็กลง)
  3. ลองแทนที่ LogisticRegression ด้วย from pyspark.ml.classification import NaiveBayes ในโครงสร้าง pipeline เดิม แล้วเปรียบเทียบ accuracy

20วิธีทำแล็บให้ได้มากกว่าคำว่า “รันผ่าน”

แล็บนี้ไม่ได้วัดว่าเราพิมพ์คำสั่งตามตัวอย่างได้ครบหรือไม่ เพราะการรันผ่านอาจเกิดจากความบังเอิญ ค่า Default หรือข้อมูลที่เล็กเกินไปจนมองไม่เห็นปัญหา เป้าหมายคือฝึกตั้งสมมติฐาน เปลี่ยนตัวแปรครั้งละอย่าง เก็บหลักฐาน และอธิบายผลด้วยแนวคิดของระบบกระจาย ถ้าผลไม่ตรงกับที่คาด นั่นไม่ใช่ความล้มเหลว แต่เป็นจุดเริ่มต้นของคำถามที่ดี

เปรียบเหมือนการทดลองในครัว
ถ้าเราเปลี่ยนทั้งเตา ภาชนะ ปริมาณวัตถุดิบ และเวลาปรุงพร้อมกัน แม้อาหารอร่อยขึ้น เราก็ไม่รู้ว่าอะไรเป็นสาเหตุ การทดลองระบบก็เหมือนกัน ควรเปลี่ยนทีละปัจจัย และจดสิ่งที่คงเดิมไว้ให้ชัด
  1. คาดก่อนรัน: เขียนหนึ่งหรือสองประโยคว่าคิดว่าจะเกิดอะไรและเพราะอะไร
  2. กำหนดสิ่งที่คุม: ใช้ข้อมูล โค้ด และทรัพยากรเดิม แล้วเปลี่ยนเฉพาะตัวแปรที่กำลังศึกษา
  3. เก็บหลักฐาน: บันทึกเวลา แผนการทำงาน Metric, Log หรือภาพ Spark UI ไม่อาศัยความรู้สึกว่า “เร็วขึ้น”
  4. อธิบายกลไก: เชื่อมผลกับ Partition, Shuffle, State, Memory, Network หรือ Scheduling
  5. บอกข้อจำกัด: ผลจาก Laptop และข้อมูลจำลองอาจไม่เหมือน Production ให้ระบุสิ่งที่ยังสรุปไม่ได้

21สมุดบันทึกการทดลอง

รายการสิ่งที่ควรบันทึกเหตุผล
คำถามครั้งนี้ต้องการรู้อะไรเพียงเรื่องเดียวกันการทดลองหลุดประเด็น
สมมติฐานผลที่คาดและกลไกที่คิดว่าเป็นสาเหตุทำให้ตรวจความเข้าใจได้
ตัวแปรสิ่งที่เปลี่ยน สิ่งที่คุม และค่าตั้งต้นทำให้เปรียบเทียบอย่างเป็นธรรม
หลักฐานเวลา Metric, Plan, Log, UI และ Sample outputทำให้คนอื่นตรวจซ้ำได้
ข้อสรุปหลักฐานสนับสนุนหรือหักล้างสมมติฐานอย่างไรแยกข้อเท็จจริงออกจากความเห็น
คำถามใหม่ถ้ามีเวลาเพิ่ม จะเปลี่ยนอะไรต่อเชื่อมไปสู่การทดลองรอบถัดไป

อย่าจดเฉพาะผลลัพธ์สุดท้าย ควรจด Environment ด้วย เช่น Spark version, Python version, จำนวน Core, Memory, จำนวน Partition, Seed และขนาดข้อมูล เปรียบเหมือนใบเสร็จและสูตรอาหารที่ทำให้คนอื่นย้อนทำจานเดิมได้ หากไม่มีข้อมูลเหล่านี้ ผลที่ดูน่าเชื่อถืออาจทำซ้ำไม่ได้

22ชุดการทดลองหลัก: Spark MLlib

การทดลอง 1: กันข้อมูลรั่ว

ลงมือทำ: แยก Train/Test ก่อน fit ตัวแปลงที่เรียนรู้จากข้อมูล ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: Pipeline diagram และจุดที่อาจเกิด Leakage แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

คำถามที่ต้องตอบ: ผลต่างมาจากการคำนวณ การสื่อสาร การอ่านเขียนข้อมูล หรือการรอคิว? ถ้าขยายข้อมูลสิบเท่า แนวโน้มเดิมน่าจะยังอยู่หรือไม่ เพราะอะไร?

การทดลอง 2: สร้าง Baseline

ลงมือทำ: เทียบโมเดลกับ Majority Class หรือกฎง่าย ๆ ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: Metric ที่แสดงว่าโมเดลเพิ่มคุณค่าจริง แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

คำถามที่ต้องตอบ: ผลต่างมาจากการคำนวณ การสื่อสาร การอ่านเขียนข้อมูล หรือการรอคิว? ถ้าขยายข้อมูลสิบเท่า แนวโน้มเดิมน่าจะยังอยู่หรือไม่ เพราะอะไร?

การทดลอง 3: อ่าน Confusion Matrix

ลงมือทำ: ตรวจ False Positive และ False Negative แยกกัน ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: คำอธิบายต้นทุนของความผิดพลาดทั้งสองแบบ แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

คำถามที่ต้องตอบ: ผลต่างมาจากการคำนวณ การสื่อสาร การอ่านเขียนข้อมูล หรือการรอคิว? ถ้าขยายข้อมูลสิบเท่า แนวโน้มเดิมน่าจะยังอยู่หรือไม่ เพราะอะไร?

การทดลอง 4: ทดสอบความเสถียร

ลงมือทำ: เปลี่ยน Seed หลายค่าแล้วรายงานการกระจายของ Metric ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: ค่าเฉลี่ย ช่วง และข้อจำกัดจากข้อมูลขนาดเล็ก แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

คำถามที่ต้องตอบ: ผลต่างมาจากการคำนวณ การสื่อสาร การอ่านเขียนข้อมูล หรือการรอคิว? ถ้าขยายข้อมูลสิบเท่า แนวโน้มเดิมน่าจะยังอยู่หรือไม่ เพราะอะไร?

การทดลอง 5: ตรวจ Training-Serving Skew

ลงมือทำ: บันทึก Schema, Feature order และขั้นตอนแปลงข้อมูลพร้อม Model ก่อนรันให้เขียนผลที่คาดไว้ แล้วรันอย่างน้อยสามครั้งหากกำลังเปรียบเทียบเวลา เพื่อไม่ให้การเริ่ม JVM, Cache หรือ Background process เพียงครั้งเดียวหลอกเรา

หลักฐานที่ต้องเก็บ: Model contract สำหรับนำไปใช้กับข้อมูลใหม่ แนบค่าตั้งต้นและขนาดข้อมูล เพื่อให้เพื่อนสามารถตรวจซ้ำได้

คำถามที่ต้องตอบ: ผลต่างมาจากการคำนวณ การสื่อสาร การอ่านเขียนข้อมูล หรือการรอคิว? ถ้าขยายข้อมูลสิบเท่า แนวโน้มเดิมน่าจะยังอยู่หรือไม่ เพราะอะไร?

23บันไดแก้ปัญหาเมื่อผลไม่เป็นอย่างที่คิด

เวลางานล้ม อย่าเริ่มด้วยการลบทุกอย่างแล้วติดตั้งใหม่ เพราะเราอาจทำลายหลักฐานที่บอกสาเหตุ ให้ไล่จากชั้นนอกเข้าสู่ชั้นในเหมือนช่างไฟที่ตรวจตั้งแต่ปลั๊ก สายไฟ ฟิวส์ ไปจนถึงเครื่องใช้ไฟฟ้า

  1. ยืนยันอาการ: คัดลอก Error แรก ไม่ใช่เฉพาะบรรทัดสุดท้าย และบันทึกคำสั่งที่ทำให้เกิดซ้ำ
  2. ลดปัญหา: ใช้ข้อมูลเล็กที่สุดและโค้ดสั้นที่สุดที่ยังทำให้ Error เกิด
  3. ตรวจสัญญาข้อมูล: ดู Schema, Type, Null, Key และ Path ก่อนสงสัย Cluster
  4. ตรวจ Plan และ UI: ดูว่างานเริ่มจริงหรือค้างอยู่ก่อนสร้าง Job; แยกปัญหา Driver, Executor และ Storage
  5. ตรวจทรัพยากร: ดู Memory, Disk, Port, Permission และ Container status
  6. เปลี่ยนทีละจุด: เมื่อแก้แล้ว ให้ย้อนอธิบายว่าการเปลี่ยนนั้นจัดการสาเหตุใด
กติกาสำคัญ
การ Restart อาจทำให้อาการหาย แต่ยังไม่ใช่คำอธิบาย ถ้าต้อง Restart ให้เก็บ Log ก่อนและเขียนไว้ตรง ๆ ว่ายังหาสาเหตุรากไม่พบ ในงานจริง ความซื่อสัตย์ต่อหลักฐานสำคัญกว่ารายงานที่ดูเรียบร้อย

24คำถามชวนคิดหลังทำแล็บ

  1. ผลที่เห็นเกิดจาก Algorithm หรือเกิดจากการจัดวางข้อมูลและข้อจำกัดของเครื่อง?
  2. ถ้าข้อมูลใหญ่ขึ้นหนึ่งร้อยเท่า ขั้นตอนไหนจะพังก่อน และเรามีหลักฐานอะไร?
  3. ถ้าเครื่องหนึ่งหายไประหว่างงาน คำตอบจะยังถูกต้องหรือเพียงแค่งานยังรันต่อได้?
  4. ค่า Default ใดช่วยเราอยู่ และค่าใดอาจกลายเป็นกับดักเมื่อขึ้น Production?
  5. ถ้าต้องอธิบายผลให้เพื่อนที่ไม่ได้เห็นหน้าจอ เรามีหลักฐานครบพอหรือยัง?

25สิ่งที่ต้องส่ง

26เกณฑ์ตรวจงานแบบสั้น

ด้านงานที่ผ่านงานที่เข้าใจจริง
ความถูกต้องรันได้และได้คำตอบตรวจ Schema, Edge case และเทียบกับคำตอบเล็กที่คำนวณเองได้
การทดลองมีผลหนึ่งชุดคุมตัวแปร ทำซ้ำ และอธิบายความแปรปรวน
หลักฐานมี Screenshotเลือก Metric หรือ Plan ที่ตอบคำถามและตีความถูก
ความเข้าใจบอกว่าอะไรเร็วกว่าอธิบายกลไกและเงื่อนไขที่ข้อสรุปอาจกลับด้าน
การสื่อสารมี Codeผู้อื่นทำซ้ำได้และเห็นเส้นทางจากคำถามไปถึงข้อสรุป

27สรุปและขั้นตอนถัดไป

หัวใจของแล็บนี้ไม่ใช่จำคำสั่ง แต่คือการมองเห็นว่า Spark MLlib ตัดสินใจอย่างไรเมื่อข้อมูล งาน และทรัพยากรถูกกระจายออกจากกัน เมื่อทำเสร็จแล้วควรตอบได้สามเรื่อง: ระบบทำอะไร หลักฐานใดแสดงว่ามันทำเช่นนั้น และเงื่อนไขใดทำให้พฤติกรรมเปลี่ยน

ก่อนเปิดแล็บถัดไป ลองย่อสิ่งที่เรียนรู้ให้เหลือหนึ่งภาพ หนึ่งตาราง และสามประโยค หากย่อไม่ได้ อาจยังมีส่วนที่เราเห็นผลแต่ยังไม่เข้าใจกลไก ให้กลับไปเลือกการทดลองที่เล็กลง แล้วค่อยต่อภาพกลับขึ้นมาใหม่

28สถานการณ์จำลอง: จากข้อมูลก้อนเล็กไปสู่งานที่เริ่มมีปัญหา

ให้เริ่มด้วยข้อมูลก้อนเล็กที่ตรวจคำตอบด้วยมือได้ จากนั้นขยายเป็นสามระดับ โดยไม่เปลี่ยนความหมายของโจทย์ ระดับแรกใช้ตรวจความถูกต้อง ระดับที่สองใช้เปิดให้เห็น Parallelism และระดับที่สามใช้เปิดให้เห็นคอขวด วิธีนี้ช่วยแยกคำถามสองข้อที่มักปนกัน คือ “คำตอบถูกหรือไม่” กับ “ระบบรองรับขนาดงานได้หรือไม่” ระบบที่เร็วแต่ตอบผิดไม่มีประโยชน์ และระบบที่ตอบถูกเฉพาะข้อมูลสิบแถวก็ยังไม่ใช่คำตอบของงานขนาดใหญ่

ระดับจุดประสงค์สิ่งที่ควรเห็นสิ่งที่ยังสรุปไม่ได้
เล็กมากตรวจคำตอบด้วยมือและทดสอบ Edge caseOutput ทุกแถวและลำดับการแปลงข้อมูลประสิทธิภาพและการกระจายงาน
พอดีเครื่องดูจำนวน Task, Partition และการใช้ Coreงานหลาย Task ทำพร้อมกันและมี Metric ให้อ่านพฤติกรรมเมื่อ Memory ไม่พอ
เริ่มเกินหน่วยความจำดู Spill, Shuffle, GC และการอ่านเขียน Diskคอขวดปรากฏชัดใน UIพฤติกรรมของคลัสเตอร์ Production ขนาดใหญ่
ข้อมูลเอียงดูผลของ Key หรือกลุ่มที่ใหญ่ผิดปกติTask บางตัวช้ากว่าเพื่อนอย่างชัดเจนว่าวิธีแก้หนึ่งแบบเหมาะกับข้อมูลทุกชุด
Metaphor: ซ้อมขนย้ายบ้าน
กล่องเล็กหนึ่งใบช่วยตรวจว่าเราติดป้ายที่อยู่ถูก แต่ไม่ช่วยบอกว่ารถบรรทุกพอหรือไม่ ส่วนการขนของทั้งบ้านตั้งแต่ครั้งแรกทำให้เราแยกไม่ออกว่าช้าเพราะป้ายผิด ทางแคบ หรือรถไม่พอ การเพิ่มขนาดทีละระดับทำให้เห็นสาเหตุชัดขึ้น

29อ่าน Spark UI ให้เป็นเรื่องราว ไม่ใช่เพียงหน้าจอสีสวย

เริ่มจาก Job ว่าถูกสร้างเพราะ Action ใด แล้วลงไปที่ Stage เพื่อดูว่า Shuffle แบ่งเส้นทางตรงไหน จากนั้นจึงดู Task ว่ากระจายตัวสม่ำเสมอหรือมีตัวใดลากยาว สุดท้ายดู Executor ว่างานไปอยู่เครื่องใด ใช้ Memory เท่าไร และมี Failure หรือ Retry หรือไม่ ลำดับนี้เหมือนอ่านแผนที่จากประเทศ จังหวัด ถนน แล้วจึงถึงบ้าน หากกระโดดไปดู Task หนึ่งตัวทันที เราอาจเห็นอาการแต่ไม่เห็นบริบท

หน้าหรือ Metricคำถามที่ตอบได้สัญญาณที่ควรสงสัย
JobsAction ใดเริ่มการคำนวณ และจบหรือยังมี Job มากกว่าที่คาดจากการเรียก Action ซ้ำ
Stagesงานถูกตัดตรงไหนและ Stage ใดใช้เวลามากStage หลัง Shuffle ใช้เวลาครองงานทั้งหมด
Tasksงานย่อยกระจายสม่ำเสมอหรือไม่Max สูงกว่า Median มาก บ่งชี้ Skew หรือเครื่องช้า
Executorsใช้ Core/Memory/Storage อย่างไรExecutor หาย, Task Failed, GC สูง หรือ Disk Spill มาก
SQL PlanOptimizer เลือก Scan, Join และ Exchange แบบใดอ่านคอลัมน์เกินจำเป็น มี Exchange ซ้ำ หรือ Join ผิดคาด
StorageCache อะไร อยู่ระดับใด และกินพื้นที่เท่าไรCache ไว้แต่ไม่ถูกใช้ หรือผลักข้อมูลสำคัญออกจาก Memory

การจับภาพ UI ควรใส่ลูกศรหรือคำอธิบายว่ากำลังใช้ภาพนั้นสนับสนุนข้อสรุปใด ภาพทั้งหน้าที่ไม่มีคำอธิบายเหมือนแนบผลตรวจสุขภาพโดยไม่บอกว่าค่าไหนผิดปกติ หลักฐานที่ดีไม่จำเป็นต้องเยอะ แต่ต้องเชื่อมกับคำถามได้ตรงจุด

30แยกความถูกต้อง ความเร็ว และความทนทานออกจากกัน

สามเรื่องนี้สัมพันธ์กันแต่ทดแทนกันไม่ได้ งานอาจเร็วขึ้นเพราะเผลอทิ้งข้อมูลบางส่วน งานอาจทนต่อ Worker หายแต่สร้างข้อมูลซ้ำ หรือคำตอบอาจถูกแต่ใช้ทรัพยากรมากเกินจำเป็น ทุกครั้งที่ปรับระบบจึงควรตรวจทั้งสามแกน ไม่ประกาศชัยชนะจากเวลาเพียงตัวเดียว

  1. ความถูกต้อง: สร้างข้อมูลจิ๋วที่รู้คำตอบล่วงหน้า ใส่ Null, Duplicate, Key ที่ไม่มีคู่ และค่าขอบเขต แล้วตรวจผลทีละกรณี
  2. ความเร็ว: วัดหลายรอบ แยก Warm-up ออกจากรอบจริง ระบุขนาดข้อมูลและทรัพยากร และรายงานทั้งค่ากลางกับช่วง ไม่เลือกเฉพาะรอบที่ดีที่สุด
  3. ความทนทาน: ทำให้ส่วนหนึ่งล้มอย่างตั้งใจ ตรวจว่างาน Retry, Resume หรือคำนวณใหม่จาก Lineage และตรวจว่าคำตอบหลังการกู้ไม่ซ้ำหรือขาด
  4. ต้นทุน: เวลาเร็วขึ้นอาจแลกกับ Memory, Storage, Network หรือความซับซ้อนในการดูแล ให้บันทึกสิ่งที่จ่ายเพิ่มเสมอ
แบบตรวจสั้นก่อนเชื่อผล Benchmark
ข้อมูลเท่ากันหรือไม่? Output เท่ากันหรือไม่? Cache ของแต่ละรอบเท่ากันหรือไม่? มีงานอื่นแย่งเครื่องหรือไม่? จำนวน Partition และ Core เท่ากันหรือไม่? ถ้าตอบข้อใดไม่ได้ ให้เรียกผลนั้นว่า “ข้อสังเกตเบื้องต้น” ก่อน ยังไม่ควรเรียกว่า “ข้อสรุป”

31จาก Error Message ไปสู่คำอธิบาย

ให้เลือก Error จริงหนึ่งครั้งจากแล็บแล้วเขียน Postmortem สั้น ๆ โดยไม่กล่าวโทษผู้ใช้หรือเครื่องมือ เริ่มจากผลกระทบ ตามด้วยลำดับเหตุการณ์ สาเหตุใกล้ตัว สาเหตุราก วิธีแก้เฉพาะหน้า และวิธีป้องกันซ้ำ ตัวอย่างเช่น “Job ล้มเพราะ Out of Memory” ยังเป็นเพียงอาการ สาเหตุรากอาจเป็นการ collect ข้อมูลทั้งหมดมายัง Driver, Broadcast ตารางใหญ่เกินไป หรือ Partition หนึ่งก้อนใหญ่ผิดปกติ ซึ่งมีวิธีแก้คนละแบบ

ส่วนของ Postmortemตัวอย่างคำถาม
ผลกระทบงานใดหยุด คำตอบใดขาด และเสียเวลาเท่าไร
ลำดับเหตุการณ์คำสั่งใดเกิดก่อน Error; Metric เริ่มผิดปกติเมื่อใด
สาเหตุรากการตัดสินใจหรือข้อสมมติใดทำให้ปัญหาเกิดได้
การกู้ทำอย่างไรให้งานกลับมา และข้อมูลถูกตรวจซ้ำอย่างไร
การป้องกันเพิ่ม Test, Validation, Alert หรือเปลี่ยน Design ตรงไหน

จุดสำคัญคือแยก “แก้ให้งานผ่านครั้งนี้” ออกจาก “แก้ไม่ให้เกิดซ้ำ” การเพิ่ม Memory อาจช่วยงานรอบนี้ แต่ถ้าข้อมูลโตต่อเนื่อง ปัญหาจะกลับมา การเปลี่ยนรูปแบบ Aggregation, ลดข้อมูลก่อน Shuffle หรือเลิกดึงทุกอย่างเข้า Driver อาจแตะสาเหตุได้ตรงกว่า

32แบบฝึกสอนกลับ: อธิบายให้เพื่อนฟังในห้านาที

จับคู่กับเพื่อนแล้วผลัดกันอธิบาย Spark MLlib โดยห้ามเริ่มจากชื่อ Class หรือ API ให้เริ่มจากปัญหาที่ระบบกำลังแก้ ใช้ภาพหนึ่งภาพและตัวอย่างข้อมูลไม่เกินสิบแถว ผู้ฟังมีหน้าที่ถามว่า “รู้ได้อย่างไร” และ “ถ้าข้อมูลใหญ่ขึ้นจะเกิดอะไร” วิธีนี้ช่วยเปิดจุดที่เราจำคำสั่งได้แต่ยังเชื่อมเหตุผลไม่ครบ

คำถามปิดแล็บ
ถ้าต้องลบโค้ดตัวอย่างทั้งหมดออก แต่เก็บบทเรียนไว้ได้เพียงสามประโยค จะเก็บประโยคใด? คำตอบควรเป็นหลักคิดที่ยังใช้ได้แม้ Spark API เปลี่ยนชื่อในอนาคต ไม่ใช่เพียงลำดับคำสั่ง