Model ini merupakan model konvolusi berukuran kecil yang digunakan untuk mengenali empat kondisi emosi yang berkaitan dengan engagement belajar dari satu gambar wajah (face crop). Keempat kondisi diprediksi secara independen (multi-label), sehingga satu gambar dapat memiliki lebih dari satu label secara bersamaan. Sebagai contoh, seorang siswa dapat menunjukkan tanda-tanda boredom dan confusion pada saat yang sama.
| Keadaan | Boredom · Engagement · Confusion · Frustration |
| Formulasi | Multi-label (satu sigmoid untuk setiap label) |
| Backbone | convnextv2_femto.fcmae_ft_in1k (timm) |
| Parameter | ±4,9 juta (4,8 juta backbone + 0,1 juta head klasifikasi) |
| Input | Crop wajah RGB berukuran 224×224 dengan normalisasi ImageNet |
| Macro-F1 validasi | 0,7908 |
| Macro-F1 test | 0,8289 |
Catatan parameter. Di tabel benchmark timm,
convnextv2_femtotercatat 5,23 juta parameter karena angka tersebut sudah termasuk head klasifikasi ImageNet (1000 kelas). Pada model ini head bawaan tersebut (~0,39 juta) dibuang dan diganti head 4-label (~0,10 juta), sehingga totalnya menjadi ~4,9 juta parameter.
Repositori ini berisi seluruh konfigurasi dan kode yang digunakan untuk menghasilkan model yang dirilis. Bobot model terlatih dalam format ONNX (convnextv2_femto_v3_crop.onnx) tidak disertakan di repositori ini, melainkan tersedia di HuggingFace: aitf-ub-2026/ub-sr-01-model-fer-convnextv2-datasetv3. Menjalankan train.py menggunakan konfigurasi bawaan akan menghasilkan model yang setara dengan checkpoint tersebut. Perbedaan hasil yang mungkin muncul umumnya hanya disebabkan oleh sifat non-deterministik selama training pada GPU.
Arsitektur model menggunakan backbone ConvNeXt V2-Femto yang telah melalui proses pre-training menggunakan metode Fully Convolutional Masked Autoencoder (FCMAE) dan kemudian di-fine-tune pada ImageNet-1K. Di atas backbone tersebut ditambahkan head klasifikasi dua lapis untuk memprediksi empat label secara independen.
Training dilakukan dalam dua tahap. Pada tahap awal, parameter backbone di-freeze sehingga hanya head klasifikasi yang diperbarui. Pendekatan ini membantu head mempelajari representasi awal tanpa mengubah fitur hasil pre-training. Setelah beberapa epoch, seluruh parameter model dibuka dan dilakukan fine-tuning menggunakan learning rate yang dinaikkan secara bertahap (linear warmup), kemudian diturunkan mengikuti cosine learning rate schedule.
Distribusi data yang tidak seimbang menjadi tantangan utama karena label engagement jauh lebih sering muncul dibandingkan frustration. Untuk mengatasi hal tersebut, training menggunakan Focal Loss yang dipadukan dengan pembobotan kelas berdasarkan rasio jumlah sampel negatif dan positif (pos_weight). Dengan cara ini, model tetap memberikan perhatian pada kelas-kelas yang jumlahnya lebih sedikit.
Setelah setiap epoch selesai, nilai threshold untuk masing-masing label tidak langsung menggunakan nilai baku 0,5. Threshold ditentukan melalui pencarian nilai yang memberikan skor F1 terbaik pada sebagian data validasi, kemudian dievaluasi pada bagian validasi lainnya agar tidak terjadi penyesuaian terhadap data yang sama (data leakage). Model dengan nilai Macro-F1 validasi terbaik kemudian disimpan sebagai checkpoint akhir.
Seluruh data training menggunakan hasil crop wajah yang diperoleh dari detektor wajah yang sama dengan yang digunakan saat inferensi pada website MVP. Konsistensi proses ini penting karena perbedaan cara melakukan crop dapat mengubah distribusi data yang diterima model. Pada percobaan awal, penggunaan metode crop yang berbeda menyebabkan wajah yang berada dekat dengan kamera lebih sering diprediksi sebagai confusion. Dengan menggunakan proses deteksi dan crop yang konsisten, perbedaan distribusi tersebut dapat diminimalkan sehingga hasil prediksi menjadi lebih stabil.
Model dievaluasi pada set test sebanyak 1.831 gambar menggunakan threshold per label hasil pencarian pada data validasi. Tabel berikut merangkum kinerja keseluruhan.
| Metrik | Nilai |
|---|---|
| Macro-F1 | 0,8289 |
| Subset accuracy | 0,7002 |
| Rata-rata akurasi per label | 0,9089 |
| Rata-rata AUC | 0,9174 |
Rincian kinerja untuk masing-masing label adalah sebagai berikut.
| Label | F1 | AUC | Akurasi |
|---|---|---|---|
| Boredom | 0,9136 | 0,9975 | 0,9924 |
| Engagement | 0,9836 | 0,8719 | 0,9689 |
| Confusion | 0,7232 | 0,8645 | 0,7952 |
| Frustration | 0,6952 | 0,9356 | 0,8793 |
Subset accuracy mengukur proporsi gambar yang keempat labelnya diprediksi benar secara bersamaan, sehingga merupakan metrik yang paling ketat. Skor pada label Confusion dan Frustration relatif lebih rendah karena jumlah sampel kedua kelas tersebut jauh lebih sedikit dibandingkan Engagement dan Boredom.
Nilai berikut merupakan konfigurasi bawaan pada config.py yang digunakan untuk menghasilkan model yang dirilis.
| Hyperparameter | Nilai |
|---|---|
| Fungsi loss | Focal Loss (α = 0,5; γ = 1,5) |
| Label smoothing | 0,1 |
| Pembobotan kelas | pos_weight = jumlah negatif / positif per kelas |
| Optimizer | AdamW |
| Learning rate backbone | 7.412564594596496e-05 |
| Learning rate head | 10× learning rate backbone |
| Weight decay | 1e-3 |
| Batch size | 128 |
| Gradient accumulation | 1 |
| Gradient clipping | 1,0 (L2 norm) |
| Jumlah epoch | 25 |
| Pembekuan backbone | 3 epoch pertama (hanya head yang dilatih) |
| Warmup learning rate | 3 epoch (linear), lalu cosine decay |
| Dropout (head) | 0,5 |
| Seed | 42 |
config.py Konfigurasi dan hyperparameter
model.py Backbone ConvNeXt V2-Femto dan head klasifikasi
dataset.py Dataset berbasis CSV, augmentasi, dataloader, dan pembobotan kelas
losses.py Implementasi BCE, Focal Loss, dan Asymmetric Focal Loss
metrics.py Perhitungan F1, AUC, akurasi, serta pencarian threshold
train.py Proses training (freeze → unfreeze → warmup → cosine)
export_onnx.py Ekspor checkpoint ke format ONNX beserta metadata
.env.example Contoh konfigurasi path dataset
requirements.txt Daftar dependensi Python
Training menggunakan tiga berkas CSV dan satu direktori yang berisi hasil crop wajah.
<FER_LABEL_DIR>/train.csv
<FER_LABEL_DIR>/val.csv
<FER_LABEL_DIR>/test.csv
Setiap berkas CSV memuat kolom frame_path, yang akan dicocokkan terhadap direktori <FER_IMAGE_ROOT>, serta empat kolom biner yang menunjukkan keberadaan masing-masing label.
frame_path,Boredom,Engagement,Confusion,Frustration
subject01/000123.jpg,0,1,0,0Dataset wajah siswa yang digunakan dalam penelitian ini bersifat privat sehingga tidak disertakan dalam repositori.
Pasang seluruh dependensi terlebih dahulu.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtSelanjutnya salin berkas konfigurasi contoh dan sesuaikan lokasi dataset pada komputer Anda.
cp .env.example .env
# ubah FER_LABEL_DIR, FER_IMAGE_ROOT, dan FER_OUTPUT_DIRBerkas .env tidak akan ikut tersimpan di Git sehingga konfigurasi lokal tetap aman.
Untuk mereproduksi model yang dirilis, jalankan:
python train.pyHyperparameter tertentu juga dapat diubah melalui baris perintah, misalnya:
python train.py --epochs 25 --batch-size 128 --loss focalHasil setiap proses training akan disimpan pada direktori <FER_OUTPUT_DIR>/<timestamp>/, termasuk checkpoint terbaik (checkpoints/best.pth) dan riwayat metrik training (metrics.csv).
python export_onnx.py runs/<timestamp>/checkpoints/best.pth model.onnxModel ONNX yang dihasilkan menyimpan informasi tambahan berupa daftar label, arsitektur backbone, serta threshold terbaik untuk setiap label sebagai metadata. Informasi ini dapat langsung digunakan oleh aplikasi inferensi tanpa memerlukan konfigurasi tambahan.
Unduh terlebih dahulu bobot model dari HuggingFace, lalu jalankan:
import json
import cv2
import numpy as np
import onnxruntime as ort
session = ort.InferenceSession("convnextv2_femto_v3_crop.onnx")
metadata = session.get_modelmeta().custom_metadata_map
labels = json.loads(metadata["labels"])
thresholds = json.loads(metadata["thresholds"])
mean = np.array([0.485, 0.456, 0.406], np.float32)
std = np.array([0.229, 0.224, 0.225], np.float32)
face = cv2.cvtColor(cv2.imread("face.jpg"), cv2.COLOR_BGR2RGB)
face = cv2.resize(face, (224, 224)).astype(np.float32) / 255.0
tensor = ((face - mean) / std).transpose(2, 0, 1)[None]
logits = session.run(["logits"], {"image": tensor})[0][0]
probabilitas = 1.0 / (1.0 + np.exp(-logits))
for label, p in zip(labels, probabilitas):
print(f"{label:<12} p={p:.3f} aktif={p >= thresholds[label]}")