[데이터파이프라인] 작업지시 분류 모델 - 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%

STEP C: 수시 작업지시 98,989건 재태깅

STEP D: 팀장 보고용 HTML 리포트 생성

전체 소요 시간은 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는 의미 없다