[데이터파이프라인] 작업지시 분류 모델 - Iterative Self-Training으로 정확도 개선하기

전편: [데이터파이프라인] VOC-작업지시 통합 분석 - Semi-supervised Learning으로 작업지시 분류 모델 구축

이전 글 요약

빌딩 관리 작업지시 130,000건을 자동 분류하기 위해 Semi-supervised Learning을 도입했다. 검수 데이터 1,280건(주제) + 900건(작업유형)에 pseudo-label 5,000건을 더해 학습한 결과:

분류 검수 only Semi-supervised
주제 63.6% 79.7%
작업유형 71.3% 87.2%

전편은 여기서 끝났고, “Iterative semi-supervised로 추가 개선 가능”이라는 과제를 남겼다. 이 글은 그 과제를 실행한 기록이다.

검수 데이터 확장: 정확도가 떨어졌다?

전편 이후 검수 데이터를 대폭 확장했다.

분류 이전 확장 후
주제 round3 1,280건 round3+4 4,125건
작업유형 worktype v1 900건 v1+v2 1,939건

더 많은 데이터로 학습했으니 정확도가 오를 거라 기대했지만, 결과는 달랐다.

분류 이전 (1,280/900건) 확장 후 (4,125/1,939건)
주제 하이브리드 79.7% 80.4%
작업유형 AI 87.2% 76.2%

주제는 소폭 올랐지만, 작업유형은 87.2%에서 76.2%로 11%p 하락했다. 모델이 나빠진 걸까?

원인 분석: 정확도가 떨어진 게 아니다

1. 평가 기준이 달라졌다

이전의 87.2%는 900건으로 측정한 것이고, 이번의 76.2%는 1,939건으로 측정한 것이다. 추가된 1,040건에는 모델이 어려워하는 카테고리가 집중되어 있었다.

카테고리 모델 정확도 (검수 기준)
기타>기타 1%
작업>설정/제어 45%
작업>설치 58%
불출/출고>자재입출고 59%

“설정/제어”는 텍스트에 “설정”이라는 단어가 거의 등장하지 않는다. “DDC 연동”, “예약 변경”, “스케줄 조정” 같은 간접적인 표현이 대부분이다. 이전 900건에서는 이런 어려운 카테고리의 비중이 작았을 뿐이다.

2. Pseudo-label의 품질 문제

핵심 문제는 pseudo-label이 이전 모델(900건 기준)로 생성된 것이라는 점이다.

학습 데이터 구성:
  검수 데이터:  1,939건 (정확한 정답)
  pseudo-label: 5,000건 (이전 모델이 예측한 라벨)
  ---
  pseudo 비중: 72% (5,000 / 6,939)

학습 데이터의 72%가 품질이 낮은 pseudo-label이다. 검수 데이터(28%)가 올바른 방향을 가리키고 있어도, 노이즈가 너무 많으면 학습이 노이즈 쪽으로 끌린다.

비유하자면, 교실에 정답을 아는 학생이 28명이고 찍기로 답한 학생이 72명인데, 다수결로 정답을 정하는 상황이다.

해결: Iterative Self-Training

아이디어는 간단하다. 모델이 좋아지면 pseudo-label도 좋아지고, pseudo-label이 좋아지면 모델도 좋아진다.

Iteration 1:
  pseudo-label (이전 모델 생성) + 검수 → 모델 학습
  → 새 모델로 pseudo-label 재생성

Iteration 2:
  pseudo-label (iter1 모델 생성) + 검수 → 모델 학습
  → 새 모델로 pseudo-label 재생성

Iteration 3:
  pseudo-label (iter2 모델 생성) + 검수 → 모델 학습
  → 최종 모델

각 iteration에서 pseudo-label의 품질이 조금씩 좋아지고, 더 좋은 pseudo-label로 학습한 모델은 더 정확해진다.

수렴 확인

pseudo-label 5,000건 중 이전 iteration과 라벨이 달라진 건수를 추적했다.

Iteration 주제 라벨 변경 작업유형 라벨 변경
1→2 58건 (1.2%) 11건 (0.2%)
2→3 11건 (0.2%) 3건 (0.06%)

2회차에서 이미 변경량이 급감했고, 3회차에서는 거의 없다. 3회면 수렴한다.

구현

핵심 코드는 짧다. 기존 학습 루프를 for문으로 감싸고, 매 iteration 끝에 _predict로 pseudo-label을 재생성하면 된다.

N_ITERATIONS = 3

for iteration in range(N_ITERATIONS):
    # 검수 + pseudo-label 결합
    semi_subj_batch = np.concatenate([r3_batch, pseudo_batch_texts])
    semi_subj_labels = np.concatenate([subj_labels, pseudo_subj_labels])

    # 모델 학습
    wo_clf = AIClassifier()
    wo_clf._train_single("subject_combined", semi_subj_batch, semi_subj_labels.copy())
    wo_clf._train_single("work_combined", semi_work_batch, semi_work_labels.copy())

    # pseudo-label 재생성 (마지막 iteration은 생략)
    if iteration < N_ITERATIONS - 1:
        new_subj = wo_clf._predict("subject_combined", pseudo_batch_texts)
        new_work = wo_clf._predict("work_combined", pseudo_batch_texts)
        pseudo_subj_labels = np.array(new_subj)
        pseudo_work_labels = np.array(new_work)

pseudo-label 재생성은 이미 학습된 모델로 5,000건을 predict하는 것이므로, 1초도 안 걸린다. 전체 3회 반복의 학습 시간은 약 60초였다.

과적합 진단

Iterative self-training에는 위험이 있다. 모델이 자기가 생성한 pseudo-label로 다시 학습하니, 자기 오류를 강화(confirmation bias) 할 수 있다.

“정확도가 올랐다”는 것만으로는 과적합이 아닌지 확신할 수 없다. 체계적으로 확인해야 한다.

방법: Held-out Test Set

검수 데이터에서 20%를 완전히 분리해서 학습에 전혀 사용하지 않았다. 이 20%에 대한 정확도가 “모델이 처음 보는 데이터에서 얼마나 잘 하는가”의 지표다.

검수 4,125건 (주제)
  ├── train: 3,300건 (80%) → 학습에 사용
  └── test:    825건 (20%) → 평가에만 사용

검수 1,939건 (작업유형)
  ├── train: 1,551건 (80%) → 학습에 사용
  └── test:    388건 (20%) → 평가에만 사용

매 iteration마다 train accuracy(학습 데이터에 대한 정확도)와 test accuracy(held-out 데이터에 대한 정확도)를 측정하고, 그 차이(gap)를 추적했다.

결과: Train vs Test 정확도

Iter 주제 train 주제 test gap 작업유형 train 작업유형 test gap
1 95.9% 83.5% 12.4%p 94.8% 83.0% 11.9%p
2 96.0% 84.1% 11.9%p 94.8% 83.0% 11.8%p
3 96.0% 84.2% 11.8%p 94.7% 83.0% 11.7%p

세 가지를 확인할 수 있다.

1. 과적합이 심화되지 않는다. Gap이 12%p 수준에서 안정적이다. Iteration을 거듭해도 train만 오르고 test가 떨어지는 전형적인 과적합 패턴은 나타나지 않았다.

2. Test 정확도가 소폭 상승한다. 주제 test가 83.5% → 84.2%로 올랐다. Self-training이 실제 일반화 성능을 개선하고 있다.

3. Gap 12%p는 이 태스크에서 정상이다. 작업지시 텍스트는 평균 18자로 매우 짧고, 같은 텍스트가 다른 카테고리로 분류되는 모호한 케이스가 많다. 12%p gap은 모델 구조의 한계이지, 과적합의 징후가 아니다.

클래스별 성능 (Held-out Test Set)

어떤 카테고리가 잘 되고, 어떤 카테고리가 안 되는지를 확인해야 개선 방향이 보인다.

주제 (test 825건, f1 기준 상위/하위)

클래스 precision recall f1
시설>건축/영선 68.4% 65.0% 66.7%
환경>소음/냄새/환기 69.2% 69.2% 69.2%
서비스>임대관리 83.3% 62.5% 71.4%
     
시설>계측/검침 94.3% 100% 97.1%
환경>해충/방역 100% 88.6% 93.9%
weighted avg 84.4% 84.2% 84.1%

작업유형 (test 388건, f1 기준 상위/하위)

클래스 precision recall f1
점검>상태점검 82.5% 70.1% 75.8%
작업>유지보수 91.1% 67.2% 77.4%
     
점검>정기점검 83.0% 100% 90.7%
작업>청소/정리 93.9% 90.2% 92.0%
weighted avg 84.2% 83.0% 83.2%

흥미로운 패턴: “유지보수”는 precision 91%인데 recall 67%다. 모델이 “유지보수”라고 말하면 대부분 맞지만, 실제 유지보수인 건 중 33%는 다른 클래스(상태점검, 설치/철거)로 빠진다. 반대로 “정기점검”은 recall 100%인데 precision 83%로, 다른 클래스의 건이 정기점검으로 잘못 들어온다.

이런 패턴은 텍스트의 모호함에서 온다. “펌프 확인”이라는 텍스트가 있으면, 단순 확인(상태점검)인지 고장 수리(유지보수)인지 텍스트만으로는 알 수 없다. take_action(조치 내용)에 “교체” 같은 키워드가 있으면 판별할 수 있지만, 비어 있는 경우도 많다.

Phase 2: Deployment 모델 학습

과적합 진단이 끝나면 전체 검수 데이터(100%) 로 최종 모델을 학습한다. 진단용 모델은 80%로 학습했기 때문에 20% 만큼의 데이터를 낭비하고 있다. 실제 배포할 모델은 가용한 모든 데이터를 써야 한다.

Phase 1 (진단): 80% train → 과적합 여부 확인
Phase 2 (배포): 100% train → 최종 모델 pkl 저장 → 재태깅

전체 데이터로 학습한 deployment 모델의 5-fold CV 결과:

분류 1회 학습 (이전) 3회 반복 (이번) 개선
주제 하이브리드 80.4% 85.6% +5.2%p
주제 AI only 80.2% 84.7% +4.5%p
작업유형 AI 76.2% 84.0% +7.8%p

작업유형이 76.2% → 84.0%로 +7.8%p 개선됐다. 검수 데이터 확장과 iterative self-training의 결합 효과다.

전체 파이프라인

최종 파이프라인 구조는 다음과 같다.

STEP A: Iterative Self-Training (3회 반복)
  Phase 1: 80% train으로 학습 + 과적합 진단
    Iteration 1~3: 학습 → train/test 정확도 → pseudo-label 재생성
    → 클래스별 precision/recall 리포트 → CSV 저장
  Phase 2: 100% train으로 deployment 모델 학습
    Iteration 1~3: 학습 → pseudo-label 재생성
    → pkl 저장

STEP B: 5-fold CV 정확도 측정 (deployment 모델)
  주제 하이브리드: 85.6%
  작업유형 AI: 84.0%

전체 소요 시간은 15분이다. STEP A(학습 반복)가 101초, STEP B(CV)가 34초, STEP C(재태깅)가 734초로, 재태깅이 대부분을 차지한다.

정리

지표 전편 (1회 학습) 이번 (3회 반복) 누적 개선
주제 하이브리드 79.7% 85.6% +5.9%p
작업유형 AI 87.2% (900건 기준) 84.0% (1,939건 기준) 평가 기준 변경
과적합 gap 미측정 12%p (안정) -
검수 데이터 1,280/900건 4,125/1,939건 x3
pseudo-label 1회 생성 3회 반복 정제 -

작업유형의 숫자만 보면 87.2% → 84.0%로 떨어진 것 같지만, 이전 87.2%는 “쉬운” 900건 기준이었고 이번 84.0%는 “어려운” 카테고리를 포함한 1,939건 기준이다. 동일 기준(1,939건)으로 비교하면 76.2% → 84.0%로 +7.8%p 개선이다.

배운 것

1. Pseudo-label 품질이 모델 성능의 상한을 결정한다. 나쁜 pseudo-label로 학습한 모델이 나쁜 pseudo-label을 생성하는 악순환을 끊으려면, 반복적으로 정제해야 한다. 3회 반복으로 변경량이 1.2% → 0.06%로 수렴했다.

2. 정확도 하락이 항상 모델 악화를 의미하지 않는다. 평가 데이터가 바뀌면 숫자가 달라진다. 같은 기준으로 비교해야 한다.

3. 과적합 진단은 반드시 해야 한다. Self-training은 모델이 자기 출력을 학습하는 구조라, confirmation bias 위험이 있다. Held-out test set으로 train/test gap을 추적하면, 과적합 여부를 iteration 단위로 모니터링할 수 있다. 이번 케이스에서는 gap이 12%p로 안정적이어서, self-training이 오히려 일반화 성능을 개선하고 있음을 확인했다.

남은 과제

과제 현재 목표
주제 정확도 85.6% 90%+
작업유형 정확도 84.0% 90%+

개선 방향:

  • 저성능 클래스 집중 검수: 건축/영선(f1 67%), 소음/냄새/환기(69%), 유지보수(77%) 등 f1이 낮은 클래스의 검수 데이터를 추가하면, 해당 클래스의 성능이 올라가면서 전체도 개선된다
  • 텍스트 특성 기반 feature 추가: take_action에 “교체”가 있으면 작업유형=교체로 오버라이드하는 규칙이 이미 있다. 이런 도메인 규칙을 더 체계적으로 적용
  • 사전학습 모델 고도화: KLUE-BERT fine-tuning을 실험했으나, 동일 데이터 기준 주제 80.2%(TF-IDF 82.4%), 작업유형 69.8%(TF-IDF 80.4%)로 오히려 낮았다. 평균 18자짜리 짧은 텍스트에서는 contextual embedding이 발휘될 문맥이 없고, 3,300건으로는 BERT를 충분히 fine-tuning할 수도 없다. 학습 시간도 87~140배 느리다. 이 태스크에서 transformer는 의미 없다