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