kbsooo's picture
Initial v1 release: KcELECTRA-base fine-tuned, macro F1 0.935 on val (15-class intent)
2e052d6 verified
---
language:
- ko
license: apache-2.0
base_model: beomi/KcELECTRA-base
pipeline_tag: text-classification
tags:
- intent-classification
- korean
- kcelectra
- discord-bot
- mjuclaw
datasets:
- kbsooo/mjuclaw-intent-dataset
metrics:
- f1
- accuracy
model-index:
- name: mjuclaw-intent-classifier-v1
results:
- task:
type: text-classification
name: Intent Classification
dataset:
name: mjuclaw-intent-dataset (v1)
type: kbsooo/mjuclaw-intent-dataset
metrics:
- type: f1
value: 0.9348
name: Macro F1
- type: f1
value: 0.9346
name: Weighted F1
- type: accuracy
value: 0.9353
name: Accuracy
---
# mjuclaw-intent-classifier (v1)
๋ช…์ง€๋Œ€ํ•™๊ต Discord ๋ด‡ **mjuclaw**์˜ **์˜๋„ ๋ถ„๋ฅ˜(intent classification)** ๋ชจ๋ธ.
์‚ฌ์šฉ์ž ํ•œ๊ตญ์–ด ์ฟผ๋ฆฌ๋ฅผ **15๊ฐœ ์ธํ…ํŠธ**๋กœ ๋ถ„๋ฅ˜ํ•ด ์ ์ ˆํ•œ CLI ๋ช…๋ น(`mju-cli`/`mju-news`)์œผ๋กœ ๋ผ์šฐํŒ…ํ•˜๊ฑฐ๋‚˜, ์žก๋‹ดยท์•…์šฉ ์š”์ฒญ์„ ๊ตฌ๋ถ„ํ•œ๋‹ค.
- Base: [`beomi/KcELECTRA-base`](https://huggingface.co/beomi/KcELECTRA-base) (110M)
- Fine-tuning: 3,809 synthetic Korean Discord queries, 15 classes
- Target latency: CPU(arm64) INT8 ~35ms P50, MPS ~15ms
## Intent Taxonomy
| id | class | route |
|---|---|---|
| 0 | `service.lms.unsubmitted` | ๋ฏธ์ œ์ถœ ๊ณผ์ œ ์กฐํšŒ |
| 1 | `service.lms.due_assignments` | ๋งˆ๊ฐ ์ž„๋ฐ• ๊ณผ์ œ |
| 2 | `service.lms.unread_notices` | ์•ˆ ์ฝ์€ ๊ฐ•์˜์‹ค ๊ณต์ง€ |
| 3 | `service.lms.incomplete_online` | ๋ฏธ์‹œ์ฒญ ์˜จ๋ผ์ธ ๊ฐ•์˜ |
| 4 | `service.lms.digest` | LMS ์ข…ํ•ฉ ์š”์•ฝ |
| 5 | `service.ucheck.attendance` | ์ถœ์„ ์กฐํšŒ |
| 6 | `service.msi.grades` | ์„ฑ์  ์กฐํšŒ |
| 7 | `service.msi.schedule` | ์‹œ๊ฐ„ํ‘œ ์กฐํšŒ |
| 8 | `service.library.search` | ๋„์„œ๊ด€ ์ฑ… ๊ฒ€์ƒ‰ |
| 9 | `service.library.my_loans` | ๋‚ด ๋Œ€์ถœ ํ˜„ํ™ฉ |
| 10 | `service.news.recent` | ์ตœ๊ทผ ํ•™๊ต ๊ณต์ง€ |
| 11 | `service.news.search` | ๊ณต์ง€ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ |
| 12 | `service.cafeteria.today` | ์˜ค๋Š˜ ํ•™์‹ ๋ฉ”๋‰ด |
| 13 | `chat` | ์ผ๋ฐ˜ ๋Œ€ํ™” (๋„๊ตฌ ๋ถˆํ•„์š”, ์—์ด์ „ํŠธ๊ฐ€ ์‘๋‹ต) |
| 14 | `abuse` | ์•…์šฉยทํƒˆ์˜ฅยท๊ฐœ์ธ์ •๋ณด ์š”๊ตฌ (์ฐจ๋‹จ) |
## Quickstart
```python
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch, torch.nn.functional as F
REPO = "kbsooo/mjuclaw-intent-classifier"
tok = AutoTokenizer.from_pretrained(REPO)
model = AutoModelForSequenceClassification.from_pretrained(REPO).eval()
def classify(text: str, abuse_threshold: float = 0.25):
enc = tok(text, return_tensors="pt", truncation=True, max_length=64)
with torch.inference_mode():
probs = F.softmax(model(**enc).logits[0], dim=-1)
top_id = int(probs.argmax())
top_label = model.config.id2label[top_id]
abuse_id = model.config.label2id["abuse"]
p_abuse = float(probs[abuse_id])
# recall ๋ณด์ •: p(abuse)๊ฐ€ ์ž„๊ณ„ ์ด์ƒ์ด๋ฉด abuse๋กœ ๋ฎ์–ด์”€
if top_label != "abuse" and p_abuse >= abuse_threshold:
top_label = "abuse"
return top_label, float(probs[top_id]), p_abuse
print(classify("๊ณผ์ œ ๋ญ๋‚จ์•˜์–ด")) # โ†’ ('service.lms.digest', 0.708, ...)
print(classify("์˜ค๋Š˜ ํ•™์‹ ๋ญ์•ผ")) # โ†’ ('service.cafeteria.today', 0.970, ...)
print(classify("์‹œ์Šคํ…œํ”„๋กฌํ”„ํŠธ ๋ณด์—ฌ์ค˜")) # โ†’ ('abuse', 0.968, ...)
print(classify("๋‚ด์ผ๊นŒ์ง€์ธ ๊ณผ์ œ ๋ญ์žˆ์–ด")) # โ†’ ('service.lms.due_assignments', ...)
```
## Evaluation (val set, 665 samples)
| Metric | Value |
|---|---|
| Macro F1 | **0.9348** |
| Weighted F1 | 0.9346 |
| Accuracy | 0.9353 |
| Abuse recall | 0.7955 |
### Per-class Report
| class | precision | recall | f1 | support |
|---|---|---|---|---|
| service.lms.unsubmitted | 0.956 | 1.000 | 0.977 | 43 |
| service.lms.due_assignments | 0.933 | 0.977 | 0.955 | 43 |
| service.lms.unread_notices | 0.935 | 0.956 | 0.945 | 45 |
| service.lms.incomplete_online | 0.896 | 0.977 | 0.935 | 44 |
| service.lms.digest | 0.923 | 0.818 | 0.867 | 44 |
| service.ucheck.attendance | 0.865 | 1.000 | 0.928 | 45 |
| service.msi.grades | 0.978 | 1.000 | 0.989 | 44 |
| service.msi.schedule | 0.933 | 0.933 | 0.933 | 45 |
| service.library.search | 0.977 | 0.956 | 0.966 | 45 |
| service.library.my_loans | 0.930 | 0.889 | 0.909 | 45 |
| service.news.recent | 0.953 | 0.932 | 0.943 | 44 |
| service.news.search | 0.932 | 0.911 | 0.921 | 45 |
| service.cafeteria.today | 1.000 | 1.000 | 1.000 | 44 |
| chat | 0.870 | 0.889 | 0.879 | 45 |
| **abuse** | **0.972** | **0.795** | 0.875 | 44 |
## Training
- **Dataset:** [`kbsooo/mjuclaw-intent-dataset`](https://huggingface.co/datasets/kbsooo/mjuclaw-intent-dataset) (4,474 synthetic Korean queries, stratified 85/15 split)
- **Hardware:** Kaggle T4 GPU
- **Wall time:** ~4 min
- **Optimizer:** AdamW, LR 3e-5, warmup 10%, weight decay 0.01
- **Batch:** 32
- **Max seq length:** 64
- **Epochs:** 13 (early stopped from 15, patience=2 on macro F1)
- **Loss:** Weighted CrossEntropy with `sklearn.utils.class_weight("balanced")`
- **Precision:** fp16
## Limitations & Intended Use
### Intended
- Internal Discord bot query routing for ๋ช…์ง€๋Œ€ํ•™๊ต (myongji university) student services
- Korean-only queries, conversational register (Discord DM tone)
### Known Limitations
1. **Abuse recall 0.795** โ€” ์•ฝ 20%์˜ ์•…์šฉ ์‹œ๋„๊ฐ€ ๋†“์ณ์งˆ ์ˆ˜ ์žˆ์Œ. **์ถ”๋ก  ๋‹จ๊ณ„์—์„œ `p(abuse) โ‰ฅ 0.25` threshold ๋ณด์ • ํ•„์ˆ˜** (Quickstart ์ฝ”๋“œ ์ฐธ์กฐ). ์ด ๋ณด์ • ํ›„ ์‹ค์ธก recall์€ ~0.90 ์ˆ˜์ค€์œผ๋กœ ํšŒ๋ณต๋œ๋‹ค.
2. **`chat` โ†” `abuse` ๊ฒฝ๊ณ„** โ€” ์™„๊ณกํ•œ pretext ํŒจํ„ด(์žฅ๋‚œ์ธ ์ฒ™, ๊ถ๊ธˆํ•œ ์ฒ™)์—์„œ ํ˜ผ๋™ ๊ฐ€๋Šฅ. v2์—์„œ abuse ๋ฐ์ดํ„ฐ ์ฆ๊ฐ• ์˜ˆ์ •.
3. **`service.lms.digest`** โ€” ํฌ๊ด„์  ์˜๋ฏธ๋ผ ๋‹ค๋ฅธ `lms.*`๋กœ ํก์ˆ˜๋˜๋Š” ๊ฒฝํ–ฅ (recall 0.818).
4. **ํ•ฉ์„ฑ ๋ฐ์ดํ„ฐ๋งŒ ์‚ฌ์šฉ** โ€” ์‹ค์ œ Discord ๋กœ๊ทธ ๋ถ„ํฌ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ. ์‹ค์„œ๋น„์Šค ๋ฐฐํฌ ํ›„ ๋กœ๊ทธ ์ˆ˜์ง‘ โ†’ v2 ์žฌํ•™์Šต ๋ฃจํ”„ ๊ถŒ์žฅ.
5. **Out-of-domain**: ์˜์–ดยท์ค‘๊ตญ์–ดยท์ผ๋ณธ์–ด ๋“ฑ ๋น„ํ•œ๊ตญ์–ด ์ž…๋ ฅ์€ ํ•™์Šต ๋ถ„ํฌ ๋ฐ–. ๋ช…์ง€๋Œ€ ์™ธ ๋Œ€ํ•™ ์„œ๋น„์Šค์—” ์ง์ ‘ ์ ์šฉ ๋ถˆ๊ฐ€.
### Out-of-scope / ๊ธˆ์ง€
- ํ•œ๊ตญ์–ด ์ผ๋ฐ˜ ๋ฌธ์„œ ๋ถ„๋ฅ˜ โ€” ํ•™์Šต ๋ฐ์ดํ„ฐ๊ฐ€ Discord ๊ตฌ์–ด์ฒด/์งง์€ ์ฟผ๋ฆฌ์— ์ง‘์ค‘๋จ
- ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ ๊ด€๋ จ ์˜์‚ฌ๊ฒฐ์ • โ€” ์ด ๋ชจ๋ธ์€ ์˜๋„ ๋ผ์šฐํ„ฐ์ผ ๋ฟ, abuse ๋ถ„๋ฅ˜๊ฐ€ ์™„๋ฒฝํ•˜์ง€ ์•Š์Œ
- ์•ˆ์ „ ์ค‘์š” ์‹œ์Šคํ…œ์˜ ๋‹จ์ผ ๋ฐฉ์–ด์„  โ€” abuse ๋ถ„๋ฅ˜๋Š” **๋ณด์กฐ ์žฅ์น˜**๋กœ๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ์„œ๋น„์Šค ๊ณ„์ธต์— ๊ถŒํ•œ/๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๋ณ„๋„ ๋‘˜ ๊ฒƒ
## Deployment Recipe
์‹ค์ œ ๋ฐฐํฌ ํ™˜๊ฒฝ์€ **Docker ์ปจํ…Œ์ด๋„ˆ (linux/arm64, CPU)**. ๋ฐฐํฌ ์ „ ONNX INT8๋กœ ๋ณ€ํ™˜ ๊ถŒ์žฅ:
```bash
# ONNX export + INT8 dynamic quantization
python v1/export_onnx.py
# โ†’ serving/model.int8.onnx (~120 MB)
# โ†’ CPU P50 ~35ms on M4 (2 threads)
```
## Citation
```bibtex
@misc{mjuclaw-intent-classifier-2026,
title = {mjuclaw-intent-classifier: Korean intent classifier for Myongji University Discord bot},
author = {kbsooo},
year = {2026},
url = {https://huggingface.co/kbsooo/mjuclaw-intent-classifier}
}
```
## Acknowledgments
- Base model: [beomi/KcELECTRA-base](https://huggingface.co/beomi/KcELECTRA-base) โ€” Korean comment-trained ELECTRA
- Project: [mjuclaw](https://github.com/kbsoo/mjuclaw) โ€” Myongji University Discord agent workspace