การจำแนกอารมณ์ด้วย Deep Learning

การวิเคราะห์สีหน้าของมนุษย์เพื่อบ่งบอกอารมณ์ (Emotion Recognition) เป็นหนึ่งในงานด้านคอมพิวเตอร์วิทัศน์ที่น่าสนใจและท้าทาย โดยทั่วไปแล้วจะใช้ การเรียนรู้เชิงลึก (Deep Learning) โดยเฉพาะโครงข่ายประสาทเทียมแบบคอนโวลูชัน (Convolutional Neural Network หรือ CNN) ในการเรียนรู้คุณลักษณะของใบหน้าและจำแนกอารมณ์ออกเป็นหมวดหมู่ต่าง ๆ โมเดลที่เราจะพัฒนานี้เป็นโมเดล Custom CNN ที่สร้างและเทรนขึ้นเองตั้งแต่ศูนย์ โดยไม่ใช้โมเดลที่ผ่านการฝึกมาก่อน (pretrained model)
สำหรับชุดข้อมูล ฝึกโมเดล เราใช้ชุดข้อมูล FER-2013 ซึ่งประกอบด้วยรูปภาพใบหน้าขาวดำความละเอียด 48x48 พิกเซล แต่ละภาพติดป้ายกำกับอารมณ์ที่บุคคลในภาพแสดงออก ชุดข้อมูลถูกแบ่งออกเป็นชุดฝึก (training set) และชุดทดสอบ (test set) โดยมีหมวดหมู่อารมณ์ทั้งหมด 7 ประเภท ได้แก่ โกรธ (angry), ขยะแขยง (disgust), กลัว (fear), ดีใจ (happy), เสียใจ (sad), ประหลาดใจ (surprise) และ เฉยๆ (neutral) ในการทดลองนี้ เราจะสร้างโมเดล CNN ด้วย Keras เพื่อจำแนกภาพใบหน้าตามอารมณ์ทั้ง 7 ประเภทดังกล่าว

เราจะสร้างระบบจำแนกอารมณ์จากภาพใบหน้าแบบ end-to-end โดยใช้ภาษา Python และไลบรารี Deep Learning (TensorFlow/Keras) ใน Google Colab ตั้งแต่การเตรียมข้อมูลจนถึงการทดสอบโมเดลกับภาพใหม่ โมเดล CNN ที่สร้างขึ้นคาดว่าจะมีความแม่นยำราว ๆ 60% บนชุดข้อมูล FER-2013 ซึ่งใกล้เคียงกับระดับ baseline ของงานนี้

Note: สามารถดูโค้ดและลองทำตามได้ที่ Google Colab Notebook → เปิด Colab เพื่อดูโค้ด

Note: โหลด Dataset FER-2013

โมเดล CNN ของเรามี Architecture ที่เรียบง่าย ประกอบด้วยชั้นคอนโวลูชัน (Conv2D) หลายชั้นสลับกับชั้นลดขนาด (MaxPooling2D) และการปรับค่าให้เป็นมาตรฐาน (BatchNormalization) พร้อมฟังก์ชันกระตุ้นแบบ ReLU และมีการใช้ Dropout เพื่อป้องกันการฟิตที่มากเกินไป (overfitting) หลังจากชั้นคอนโวลูชันเหล่านี้ เราจะคลี่อาร์เรย์เป็นเวกเตอร์ (Flatten) และผ่านเข้าชั้นเชื่อมต่อแบบเต็ม (Dense) เพื่อสรุปคุณลักษณะ ก่อนจะจบด้วยชั้น Dense สุดท้ายที่มี 7 นิวรอน (หนึ่งนิวรอนต่อประเภทอารมณ์) พร้อมฟังก์ชันกระตุ้นแบบ Softmax สำหรับการจำแนกหลายคลาส

เราจะแบ่งส่วนโค้ดออกเป็นขั้นตอนต่าง ๆ เพื่อความเข้าใจง่าย ดังนี้:

  1. โหลดและเตรียมข้อมูลชุดฝึก/ชุดทดสอบ: อัปโหลดไฟล์ข้อมูลรูปภาพ (เช่น ไฟล์ ZIP ที่ประกอบด้วยโฟลเดอร์ train และ test), แตกไฟล์ และเตรียม path สำหรับชุดข้อมูล
  2. สร้างตัวช่วยเตรียมข้อมูล (Data Generator): ใช้ ImageDataGenerator จาก Keras เพื่ออ่านรูปภาพเป็น batch พร้อมปรับแต่ง (augmentation) เช่น หมุนหรือพลิกรูป เพื่อเพิ่มความหลากหลายของข้อมูล
  3. กำหนด Architecture Model CNN: สร้างโมเดล Sequential และเพิ่มเลเยอร์ต่าง ๆ (Conv2D, MaxPooling2D, ฯลฯ) ตามโครงสร้างที่ต้องการ
  4. Compile โมเดล: กำหนดฟังก์ชัน Loss, Optimizer และ Metrics ที่ใช้ในการฝึกโมเดล (ในที่นี้ใช้ categorical crossentropy, Adam optimizer และ accuracy)
  5. ฝึกสอนโมเดล (Training): ใช้ข้อมูลชุดฝึกป้อนเข้าโมเดลทีละ batch ในแต่ละ epoch พร้อมตรวจสอบประสิทธิภาพกับชุดทดสอบ (validation) เพื่อวัดความแม่นยำ
  6. ทำนายและประเมินผล: นำโมเดลที่ฝึกเสร็จแล้วมาทำนายอารมณ์จากภาพชุดทดสอบ ดูผลลัพธ์ที่ได้เปรียบเทียบกับคำตอบที่ถูกต้อง

ต่อไปนี้เป็นรายละเอียดของแต่ละขั้นตอน
ขั้นตอนที่ 1 – โหลดข้อมูลชุดฝึกและชุดทดสอบ
บน Colab เราสามารถอัปโหลดชุดข้อมูลได้ง่าย ๆ ด้วย files.upload() จากนั้นใช้ zipfile ใน Python ในการแตกไฟล์ zip ไปยังโฟลเดอร์ปลายทาง (ในที่นี้เราจะใช้โฟลเดอร์ชื่อ fer_data ซึ่งภายในมีสองโฟลเดอร์ย่อยคือ train และ testสำหรับชุดฝึกและชุดทดสอบตามลำดับ)

from google.colab import files
uploaded = files.upload()  # อัปโหลดไฟล์ข้อมูล (ในที่นี้จะใช้ archive.zip)

import zipfile, os
with zipfile.ZipFile("archive.zip", 'r') as zip_ref:
    zip_ref.extractall("fer_data")  # แตกไฟล์ zip ลงโฟลเดอร์ fer_data

เมื่อแตกไฟล์สำเร็จ เราจะได้โครงสร้างไดเรกทอรี fer_data/train และ fer_data/test ซึ่งภายในมีรูปภาพที่ถูกจัดอยู่ในโฟลเดอร์ย่อยตามชื่ออารมณ์แต่ละประเภท (7 โฟลเดอร์สำหรับ 7 อารมณ์) Keras จะใช้โครงสร้างนี้ในการโหลดภาพและระบุ label ให้อัตโนมัติ

ขั้นตอนที่ 2 – สร้าง Data Generator สำหรับจัดการรูปภาพ
เราจะใช้คลาส ImageDataGenerator จาก tensorflow.keras.preprocessing.image เพื่อช่วยโหลดรูปภาพเป็น batch และทำ Data Augmentation เพิ่มความหลากหลายของข้อมูลเฉพาะชุดฝึก (เช่น การหมุนภาพ, ซูม, พลิกแนวนอน เป็นต้น) โดยตั้งค่ารีสเกล (rescale) ค่า pixel ให้อยู่ในช่วง [0,1] ด้วย การเตรียม Data Generator

from tensorflow.keras.preprocessing.image import ImageDataGenerator

train_dir = 'fer_data/train'
test_dir  = 'fer_data/test'

# กำหนด Data Generator สำหรับชุดฝึก (พร้อม Augmentation) และชุดทดสอบ (เฉพาะ rescale)
train_datagen = ImageDataGenerator(
    rescale=1.0/255,
    rotation_range=20,
    zoom_range=0.2,
    horizontal_flip=True,
    shear_range=0.2,
    width_shift_range=0.1,
    height_shift_range=0.1
)
test_datagen = ImageDataGenerator(rescale=1.0/255)

# สร้าง generator สำหรับชุดฝึกและชุดทดสอบ
train_generator = train_datagen.flow_from_directory(
    train_dir,
    target_size=(48, 48),
    color_mode='grayscale',
    batch_size=64,
    class_mode='categorical'
)
test_generator = test_datagen.flow_from_directory(
    test_dir,
    target_size=(48, 48),
    color_mode='grayscale',
    batch_size=64,
    class_mode='categorical'
)

เมื่อรันโค้ดด้านบน ระบบจะอ่านไฟล์รูปภาพจากโฟลเดอร์และสรุปจำนวนภาพที่พบในแต่ละชุด เช่น ในชุดฝึกจะพบภาพจำนวน 28,709 รูปใน 7 ประเภทอารมณ์ดังนี้:

Found 28709 images belonging to 7 classes.
Found 7178 images belonging to 7 classes.

ข้อมูลเหล่านี้จะถูกส่งออกมาเป็น batch (ครั้งละ 64 ภาพ) พร้อมทั้งสุ่มปรับแต่งภาพ (สำหรับ train_generator) ทุกครั้งที่ดึงข้อมูลชุดใหม่ ทำให้โมเดลเห็นภาพที่หลากหลายมากขึ้นในระหว่างการฝึก

ขั้นตอนที่ 3 – กำหนด Architecture Model
เราจะสร้างโมเดล CNN แบบ Sequential โดยเพิ่มเลเยอร์ต่าง ๆ ทีละชั้น โครงสร้างโมเดลนี้ประกอบด้วย 3 blocks หลักของคอนโวลูชัน แต่ละ block มี Conv2D (3x3 kernel, ใช้ relu activation) ตามด้วย BatchNormalization และ MaxPooling2D เพื่อลดขนาด feature map ลงครึ่งหนึ่ง จากนั้นใส่ Dropout 25% เพื่อลดการ overfitting เมื่อจบ 3 blocks นี้แล้ว เราจะแปลง feature maps เป็นเวกเตอร์ด้วย Flatten และต่อด้วย Dense 512 นิวรอน (relu) จากนั้น Dropout 50% และ Dense 7 นิวรอน (softmax) สำหรับ output ทั้งหมด

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization, Activation

model = Sequential()

# Block 1
model.add(Conv2D(64, (3, 3), padding='same', input_shape=(48, 48, 1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# Block 2
model.add(Conv2D(128, (3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# Block 3
model.add(Conv2D(256, (3, 3), padding='same'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))

# Fully connected layer
model.add(Flatten())
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(7, activation='softmax'))

จากโค้ดข้างต้น จะเห็นว่าโมเดลมีจำนวนพารามิเตอร์ค่อนข้างมากพอสมควร (โดยเฉพาะจาก Dense 512 นิวรอน) แต่โครงสร้างเช่นนี้เป็น Architecture พื้นฐานที่นิยมใช้กับงานจำแนกภาพขนาดเล็กอย่าง FER-2013 เนื่องจากความง่ายและประมวลผลได้เร็ว

ขั้นตอนที่ 4 – Compile โมเดล
ขั้นตอนที่ 4 – Compile โมเดล
ก่อนที่จะฝึกโมเดล เราต้องทำการ compile โมเดล โดยกำหนด loss function, optimizer และ metrics ที่สนใจ ในปัญหาการจำแนกหลายคลาส เราใช้ loss แบบ categorical crossentropy ส่วน optimizer เราใช้ Adam พร้อม learning rate ที่กำหนดเองเล็กน้อย (0.0005) เพื่อการปรับค่า weight ที่นุ่มนวลขึ้น (ค่าปริยายของ Adam คือ 0.001) และสุดท้ายตั้งค่า metric เป็น accuracy เพื่อให้แสดงความแม่นยำของการจำแนกในแต่ละ epoch

from tensorflow.keras.optimizers import Adam

model.compile(
    loss='categorical_crossentropy',
    optimizer=Adam(learning_rate=0.0005),
    metrics=['accuracy']
)

ขั้นตอนที่ 5 – ฝึกสอนโมเดลด้วยชุดข้อมูลฝึก (Training)
เมื่อโมเดล compile แล้ว เราสามารถเริ่มการฝึกได้ โดยใช้เมธอด model.fit ในการป้อนข้อมูล train_generator เข้าโมเดลทีละ batch ไปจนครบหนึ่ง epoch และใช้ test_generator เป็น validation data เพื่อวัดผลการจำแนกกับข้อมูลที่โมเดลไม่เคยเห็น (ชุดทดสอบ) ในแต่ละ epoch ในที่นี้เรากำหนดจำนวน epoch = 50 (โมเดลจะวนการเรียนรู้ชุดข้อมูล 50 รอบ)

history = model.fit(
    train_generator,
    validation_data=test_generator,
    epochs=50
)

ในระหว่างการฝึก Keras จะพิมพ์ผลลัพธ์ของแต่ละ epoch ออกมา เช่น ความสูญเสีย (loss) และความแม่นยำ (accuracy) บนชุดฝึก และชุด validation (ทดสอบ) ซึ่งเราสามารถใช้ข้อมูลนี้ในการวิเคราะห์ว่าโมเดลกำลังเรียนรู้ได้ดีเพียงใด ตัวอย่างผลลัพธ์ที่เกิดขึ้นในช่วงแรก ๆ และช่วงท้ายของการฝึก:

Epoch 1/100 - accuracy: 0.22 - val_accuracy: 0.25
...
Epoch 10/100 - accuracy: 0.33 - val_accuracy: 0.44
...
Epoch 50/100 - accuracy: 0.41 - val_accuracy: 0.50
...
Epoch 100/100 - accuracy: 0.63 - val_accuracy: 0.60

จาก log ข้างต้น จะเห็นว่าในรอบแรกความแม่นยำ (accuracy) ของชุดฝึกอยู่ที่ ~22% และเพิ่มขึ้นอย่างต่อเนื่องเมื่อ epoch เพิ่มขึ้น ส่วนความแม่นยำบนชุดทดสอบตอนเริ่มต้น ~25% และขึ้นมาประมาณ 60% ที่ epoch ที่ 100 นั่นหมายความว่าโมเดลสามารถเรียนรู้จำแนกรูปภาพได้พอสมควร แม้ว่าจะยังมีข้อผิดพลาดอยู่ (ความแม่นยำเพียง 60% บนชุดทดสอบ แสดงว่ายังทำนายผิดอยู่มากในหลายๆ กรณี) การปรับปรุงโมเดลเพิ่มเติมอาจทำให้ accuracy สูงขึ้นได้ (เราจะกล่าวถึงในส่วนสรุป)

ขั้นตอนที่ 6 – ทำนายอารมณ์บนภาพชุดทดสอบและดูผลลัพธ์
หลังจากการฝึกเสร็จสิ้น เราจะนำโมเดลที่ได้มาทดสอบกับภาพใหม่ที่โมเดลไม่เคยเห็นมาก่อน เพื่อดูความสามารถในการจำแนกจริง เราสามารถสุ่มหยิบภาพจาก test set ขึ้นมาหนึ่งภาพ แล้วใช้เมธอด model.predict เพื่อทำนาย จากนั้นนำผลลัพธ์มาเทียบกับ label ที่ถูกต้อง ซึ่งในโค้ดด้านล่าง เราใช้วิธีดึง batch หนึ่งจาก test_generator (จะได้ภาพหลายภาพเก็บในตัวแปร x และ label ที่ถูกต้องใน y), จากนั้นเลือกภาพสุ่ม 1 ภาพจาก batch นั้น ทำการทำนาย และแปลงผลทำนาย (ซึ่งเป็น one-hot vector)

import numpy as np
import matplotlib.pyplot as plt

# ดึงหนึ่ง batch จาก test_generator
x, y = next(test_generator)

# สุ่มเลือกภาพหนึ่งจาก batch
random_index = np.random.randint(0, len(x))
image = x[random_index]
true_label = np.argmax(y[random_index])

# ใช้โมเดลทำนายอารมณ์จากภาพนี้
pred = model.predict(image.reshape(1, 48, 48, 1))
pred_label = np.argmax(pred)

# เตรียม map index -> ชื่อคลาสอารมณ์
class_indices_inv = {v: k for k, v in test_generator.class_indices.items()}

# แสดงภาพและผลลัพธ์การทำนาย
plt.imshow(image.reshape(48, 48), cmap='gray')
plt.title(f"จริง: {class_indices_inv[true_label]} / ทำนาย: {class_indices_inv[pred_label]}")
plt.axis('off')
plt.show()

ในโค้ดข้างต้น test_generator.class_indices จะเป็นดิกชันนารี mapping จากชื่อคลาส (string) ไปเป็นหมายเลข index โมเดล เราจึงสร้าง class_indices_inv กลับด้านเพื่อจะแปลง index ที่โมเดลทำนายกลับไปเป็นชื่ออารมณ์ที่มนุษย์อ่านได้ แล้วนำมาแสดงบน title ของภาพ รวมถึงแสดง label จริงควบคู่กัน

ตัวอย่างผลลัพธ์จากโมเดล

Image description

ภาพตัวอย่างจากชุดทดสอบ: โมเดลทำนายว่าอารมณ์ "surprise" และคำตอบที่ถูกต้องคือ "surprise" เช่นกัน (โมเดลทำนายถูกต้องในตัวอย่างนี้)

ภาพด้านบนแสดงตัวอย่างผลการทำนายหนึ่งภาพจากชุดทดสอบ ซึ่งโมเดลทำนายออกมาว่าบุคคลในภาพมีอารมณ์ surprise (ประหลาดใจ) ตรงกับคำตอบจริงที่ควรจะเป็น ถือว่าโมเดลทำนายได้ถูกต้องในกรณีนี้ จะเห็นว่าภาพใบหน้ามีลักษณะตกใจอย่างชัดเจน (ดวงตาเบิกกว้าง ปากอ้าค้าง) ทำให้โมเดลสามารถจับอารมณ์ได้ถูกต้อง

อย่างไรก็ตาม โมเดลไม่ได้ทำนายถูกต้องกับทุกภาพเสมอไป ในบางกรณีที่อารมณ์มีลักษณะใกล้เคียงกันหรือภาพใบหน้ามีสัญญาณอารมณ์ไม่ชัดเจน โมเดลอาจทำนายผิดได้ เช่น อาจคาดเดาว่าเป็นอารมณ์ fear แทนที่จะเป็น surprise หากใบหน้ามีลักษณะคล้ายคลึงกัน หรือทำนาย sad สลับกับ neutral เป็นต้น ตัวอย่างเช่น ภาพใบหน้าที่แสดงอาการโกรธและขยะแขยงอาจมีบางส่วนคล้ายกัน (เช่นการขมวดคิ้ว) ทำให้โมเดลสับสน

สรุปผลและข้อเสนอแนะ

โดยสรุป เราได้สร้างโมเดล CNN ด้วย Keras เพื่อจำแนกอารมณ์จากภาพใบหน้า ซึ่งโมเดลที่สร้างขึ้นสามารถเรียนรู้จากข้อมูลและจำแนกอารมณ์ได้ในระดับหนึ่ง (ความแม่นยำประมาณ 60% บนชุดทดสอบในกรณีทดลองนี้) ความสามารถของโมเดลเพียงพอที่จะทำนายอารมณ์ที่เด่นชัดบางประเภทได้ถูกต้อง (เช่น ความดีใจหรือความประหลาดใจที่แสดงออกชัดเจน) แต่ยังมีข้อจำกัดในการจำแนกอารมณ์ที่ซับซ้อนหรือคล้ายคลึงกัน โมเดลยังคงทำนายผิดพลาดอยู่ในหลายกรณี
แนวทางปรับปรุงโมเดลในอนาคต: เพื่อเพิ่มประสิทธิภาพของการจับอารมณ์ใบหน้า เราสามารถพัฒนาโมเดลเพิ่มเติมได้หลากหลายวิธี เช่น
เพิ่มเทคนิค Data Augmentation: แม้ว่าเราได้ใช้ augmentation บางส่วนแล้ว แต่สามารถเพิ่มเติมหรือปรับแต่งมากขึ้น เช่น

  • การสุ่มปรับความสว่างหรือคอนทราสต์ของภาพ ซึ่งอาจช่วยให้โมเดลเรียนรู้ความหลากหลายของรูปใบหน้าและสภาวะแสงได้ดียิ่งขึ้น
  • เพิ่มความลึกหรือความซับซ้อนของโมเดล: ลองเพิ่มจำนวนชั้นคอนโวลูชันหรือจำนวนฟิลเตอร์ในแต่ละชั้นให้มากขึ้น หรือเพิ่มชั้น Dense ที่ใหญ่ขึ้น (พร้อมการ regularization ที่เหมาะสม) โมเดลที่ลึกและซับซ้อนขึ้นอาจจับ pattern ที่ซ่อนอยู่ได้มากขึ้น แต่ต้องระวังเรื่อง overfitting และเวลาในการเทรนที่สูงขึ้นตามมา
  • ใช้ Transfer Learning: เป็นการนำโมเดลที่ผ่านการฝึกบนชุดข้อมูลขนาดใหญ่อื่น ๆ มาใช้เป็นตัวตั้งต้น (เช่น โมเดลจำแนกภาพทั่วไปอย่าง VGG, ResNet หรือโมเดลที่ถูกฝึกมากับใบหน้ามนุษย์โดยเฉพาะ) แล้วปรับ fine-tune ให้เข้ากับงานจับอารมณ์ วิธีนี้มักช่วยเพิ่มความแม่นยำได้เร็วและมากขึ้น เพราะโมเดลมีความรู้เบื้องต้นเกี่ยวกับลักษณะใบหน้าอยู่แล้ว
  • ปรับจูนไฮเปอร์พารามิเตอร์: ทดลองปรับค่า learning rate, batch size, หรือ optimizer ชนิดอื่น ๆ รวมถึงจำนวน epoch ที่มากขึ้น เผื่อโมเดลอาจยังเรียนรู้ได้ไม่เต็มที่ใน 50 epochs แรก การลดค่า learning rate เมื่อเทรนไปหลาย epoch แล้ว (learning rate scheduling) ก็อาจช่วยให้โมเดลปรับตัวได้ดีขึ้นในช่วงท้าย ๆ

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

Note: ในส่วนสุดท้ายของ Colab มีให้ลอง Upload รูปของตัวเองด้วยยยย!!