들어가며

댓글을 1차 필터링할 시스템이 필요했습니다.

대 AI 시대인 만큼, Classification AI를 사용하면 좋겠다고 생각했습니다. 모델은 Smilegate AI의 smilegate-ai/kor_unsmile. 분류 서버는 미리 띄워뒀습니다(본인 Tailscale 내부망).

POST /classify
{ "text": "..." }
{
  "scores": { "악플/욕설": 9.2, "clean": 0.3, ... },
  "top_label": "악플/욕설",
  "toxic_max": 0.92,
  "clean_score": 0.03,
  "is_toxic": true,
  "latency_ms": 31
}

kor_unsmile단일 라벨 분류기가 아니라 멀티라벨 분류기입니다. KcBERT 기반이고, 코드와 모델은 Apache 2.0, 학습 데이터셋만 CC-BY-NC-ND 4.0이라 개인 블로그에 쓰는 데는 문제없습니다. 한 문장을 넣으면 10개 라벨 각각에 0~1 sigmoid를 동시에 매깁니다.

여성/가족, 남성, 성소수자, 인종/국적, 연령, 지역, 종교, 기타 혐오, 악플/욕설, clean

즉 모델은 “이 댓글은 악플이다/아니다”를 말해주지 않습니다. 10개의 점수를 토해낼 뿐입니다. 그 점수들을 가지고 게시할지 말지를 결정하는 규칙은 내가 따로 얹어야 합니다. 따라서 실험의 중심은 모델 평가가 아니라 임계값과 운영점 선택입니다.

게이트 로직

아래는 제가 사용하는 확정된 임계값과 게이트 로직입니다.

gate_core.py
# 확정 임계값 ('균형'). 초과하면 그 카테고리가 '불붙은' 것으로 보고 보류.
TAU = {
    "여성/가족": 0.50, "남성": 0.50, "성소수자": 0.50, "인종/국적": 0.50,
    "연령": 0.50, "지역": 0.85, "종교": 0.50,
    "기타 혐오": 0.50, "악플/욕설": 0.85,
}
 
def gate(scores: dict) -> Decision:
    """kor_unsmile scores(dict) → 게이트 판정."""
    fired = [(cat, round(scores[cat], 3)) for cat, tau in TAU.items()
             if scores.get(cat, 0.0) > tau]
    fired.sort(key=lambda kv: -kv[1])
    hold = len(fired) > 0
    return Decision(hold=hold, reasons=fired, verdict="보류" if hold else "통과")

실험적인 결과에 따라 구성한 것인데, 시간이 난다면 관련 내용도 작성하면 좋을 것 같습니다. (로직을 보시면 아시겠지만, 저는 정상차단이 더 싫어서 조금 후하게 값을 설정했습니다.)