[데이터파이프라인] VOC 분류 시스템 - AI 기반 고객 피드백 자동 분류
목적
-
빌딩 관리 서비스에서 수집되는 고객 VOC(Voice of Customer) 데이터를 AI 기반으로 자동 분류(VOC주제, 작업유형)
VOC 원문 주제 대분류 주제 중분류 작업유형 대분류 작업유형 중분류 화장실이 너무 더러워요 환경 청결/미화 작업 청소/정리 에어컨이 안 켜져요 시설 냉난방/공조 작업 유지보수 주차장 조명이 너무 어두워요 시설 전기/조명 작업 교체 -
각 빌딩에서 월간 VOC 현황 보고서를 HTML로 다운로드할 수 있게 한다 (통계 및 워드클라우드 포함 대시보드)
개요
- 매월 1일 APScheduler 배치로 전월 VOC 데이터 분석 → 자동 태깅 → HTML 리포트 생성 → S3 업로드
- 자동 태깅은 Two-pass 하이브리드 방식:
- 1차 키워드 태깅: DB의 분류 체계(voc_taxonomy) 키워드 매칭으로 분류
- 2차 AI 오버레이: AI 모델이 키워드 결과를 보정
- 주제: 키워드 점수 vs AI 확률 비교 → 높은 쪽 사용
- 작업유형: AI로 전면 교체
- 자동 태깅 결과를 Google Sheets에 업로드하여, 비개발자인 도메인 전문가(빌딩 관리자)가 직접 검수할 수 있도록 한다
- 드롭다운 메뉴로 분류 체계(taxonomy) 선택 → 오분류 항목만 수정
- 코드 수정 없이 브라우저에서 바로 검수 가능
- 검수 완료 후 refresh 모드로 HTML 재생성하여 검수 결과 즉각 반영
- 축적된 검수 데이터로 매 배치 시 AI 모델 재학습 → 자동 태깅 정확도 지속 개선 (Human-in-the-loop)
VOC 처리 Flow
flowchart LR
subgraph U1["[민원인]"]
A["VOC 등록"]
end
subgraph SYS1["[월간 배치 - full]"]
B["키워드+AI 하이브리드<br/>자동 태깅"]
end
subgraph U2["[작업자]"]
C["VOC 확인<br/>+ 처리"]
end
subgraph U3["[도메인 전문가]"]
E["Google Sheets 검수<br/>주제/작업유형 수동 확정"]
end
subgraph SYS3["[refresh 배치]"]
G["is_reviewed=TRUE<br/>DB UPSERT"]
end
subgraph SYS2["[다음 full 배치]"]
F["AI 모델 재학습<br/>정확도 향상"]
end
A -->|"주제/작업유형 자동 분류"| B
B -->|"classify_source=batch"| C
C --> E
E -->|"검수 완료"| G
G -->|"classify_source=refresh"| F
F -.->|"다음 배치부터 개선된 모델 적용"| B
| 단계 | 주체 | 내용 | classify_source |
|---|---|---|---|
| VOC 등록 | 민원인 | 민원 접수 | — |
| 자동 태깅 | 월간 배치 (full) | 키워드+AI 하이브리드 분류 | batch |
| VOC 처리 | 작업자 | 분류 결과 참고하여 처리 | — |
| 검수 | 도메인 전문가 (Google Sheets) | 주제/작업유형 수동 확정 | — |
| 검수 반영 | refresh 배치 | is_reviewed=TRUE DB UPSERT | refresh |
| 재학습 | 다음 full 배치 | 검수 누적 데이터로 모델 개선 | — |
전체 아키텍처
flowchart TB
subgraph DataSource["데이터 소스"]
DB[(PostgreSQL VOC)]
TAX[(voc_taxonomy)]
VC[(voc_classification<br/>is_reviewed=TRUE)]
end
subgraph Training["AI 학습 - ai_classifier.py"]
T1["voc_classification.is_reviewed=TRUE<br/>전체 누적 데이터 로드"]
T2[데이터 전처리]
T3["AI 모델 학습"]
T4[모델 저장]
T1 --> T2 --> T3 --> T4
end
subgraph Inference["추론 파이프라인 - Two-pass"]
I1[VOC 데이터 추출]
I2["1차: 키워드 태깅<br/>nlp_model.py + voc_taxonomy"]
I3["2차: AI 오버레이<br/>ai_classifier.py"]
I4["주제: 키워드 vs AI 점수 비교<br/>작업유형: AI 전면 교체"]
I5[태깅 CSV 저장]
I1 --> I2 --> I3 --> I4 --> I5
end
subgraph Batch["월간 배치 오케스트레이터 - run_monthly.py"]
B1["APScheduler<br/>매월 1일 02:00 KST - full<br/>매월 15일 02:00 KST - refresh"]
B2["run_monthly.py --classify ai-hybrid"]
B3{실행 모드?}
B1 --> B2 --> B3
end
subgraph ModeFull["mode: full"]
F1["키워드 + AI 하이브리드 자동 태깅"]
F2["keyword_analysis.py HTML 생성"]
F3[S3 업로드]
F4[검수시트 업로드]
F1 --> F2 --> F3 --> F4
end
subgraph ModeRefresh["mode: refresh"]
R1["검수시트에서 검수 데이터 다운로드"]
R2["S3에서 기존 태깅 CSV 다운로드"]
R3["검수 데이터 병합 + is_reviewed 플래그 설정"]
R4["DB UPSERT - is_reviewed=TRUE"]
R5["keyword_analysis.py HTML 재생성"]
R6["S3 업로드 HTML 덮어쓰기"]
R1 --> R3
R2 --> R3
R3 --> R4 --> R5 --> R6
end
subgraph Storage["저장소"]
S3[(S3\ntagged CSV / reviewed CSV / HTML)]
GS[Google Sheets 검수 스프레드시트]
end
subgraph Review["검수 워크플로우"]
RV1[자동 태깅 결과 업로드]
RV2["검수자 수동 검수 - 드롭다운 선택"]
RV3["검수완료 = Y"]
RV1 --> RV2 --> RV3
end
DB --> I1
TAX --> I2
T4 --> I3
B3 -->|full| ModeFull
B3 -->|refresh| ModeRefresh
F3 --> S3
F4 --> GS
GS --> RV1
RV3 -->|refresh 트리거| R1
R2 --> S3
R4 -.->|누적 축적| VC
VC --> T1
R6 --> S3
핵심 흐름:
- AI 학습: DB
voc_classification.is_reviewed=TRUE인 분류값 학습 →ai_classifier.py→ AI 모델 학습 (배치 시작 시 1회, 메모리 상주) - 추론 (full 모드): VOC 원천 데이터 → 1차 키워드 태깅 → 2차 AI 오버레이 → HTML 생성 → S3/검수시트 업로드
- 검수 반영 (refresh 모드): 검수시트 + S3 기존 CSV → 병합 +
is_reviewed=TRUEUPSERT → HTML 재생성 → S3 덮어쓰기 (매월 15일 자동 실행) - 검수: Google Sheets에서 검수 →
refresh배치가voc_classification.is_reviewed=TRUE로 DB에 누적 → 다음 full 배치의 AI 학습 소스로 재사용
프로젝트 구조
voc-nlp/
├── .env.dev, .env.stg, .env.prd # 환경별 설정
├── Dockerfile # Docker 빌드
├── entrypoint.sh # batch/scheduler 모드 선택
├── requirements.txt
├── batch/
│ ├── run_monthly.py # 배치 오케스트레이터 (메인)
│ ├── ai_classifier.py # AI 분류기 (하이브리드 오버레이)
│ ├── nlp_model.py # 키워드 태깅 모듈
│ ├── nlp_model_core.py # 전처리/분류 핵심 함수
│ ├── keyword_analysis.py # 키워드 분석 + HTML 리포트 생성
│ ├── s3_uploader.py # S3 업로드 모듈
│ ├── gspread_manager.py # Google Sheets 연동
│ ├── common_db.py # DB 연결 공통 모듈
│ └── scheduler.py # APScheduler 기반 월간 스케줄러
└── output/
├── tagging/ # 태깅 결과 CSV
├── html/ # 대시보드 HTML
├── reviewed/ # 검수 반영 CSV
└── models/ # 학습된 모델 파일
Docker Entrypoint 모드
하나의 Docker 이미지로 2가지 컨테이너를 실행한다.
| 모드 | 실행 대상 | 수명 | 용도 |
|---|---|---|---|
batch |
run_monthly.py |
1회 실행 후 종료 | 수동/임시 배치 실행 |
scheduler |
scheduler.py (APScheduler) |
상주 (데몬) | 매월 1일/15일 02:00 KST 자동 배치 |
# entrypoint.sh
case "$1" in
batch) exec python batch/run_monthly.py "$@" ;;
scheduler) exec python -m batch.scheduler "$@" ;;
esac
운영에서는 scheduler 컨테이너가 상시 실행되고, 수동 재태깅이 필요할 때 docker exec으로 scheduler 컨테이너 안에서 batch를 실행한다.
배치 실행 모드
run_monthly.py는 3가지 모드를 지원한다.
| 모드 | 설명 | 처리 순서 |
|---|---|---|
full |
전체 처리 (기본) | 태깅 → HTML → S3 → 검수시트 |
refresh |
검수 반영 | 검수시트 다운로드 → 기존CSV 병합 → HTML 재생성 → S3 |
tagging-only |
태깅만 | 태깅 → 검수시트 (HTML 생성 안함) |
Docker 기반 실행:
# 전체 빌딩 full 배치 (AI 하이브리드)
docker run --rm -e ENV=prd \
-v /path/to/output:/app/output \
-v /path/to/logs:/app/logs \
-v /path/to/.credentials:/app/.credentials \
-v ~/.aws:/root/.aws:ro \
voc-nlp-prd:latest batch \
--mode full --all-buildings --auto-month --classify ai-hybrid
# 검수 완료 후 HTML 재생성
docker run --rm -e ENV=prd \
-v /path/to/output:/app/output \
-v /path/to/logs:/app/logs \
-v /path/to/.credentials:/app/.credentials \
-v ~/.aws:/root/.aws:ro \
voc-nlp-prd:latest batch \
--mode refresh --all-buildings --auto-month
하이브리드 분류 파이프라인 상세
분류 전략
| 분류 항목 | 전략 | 이유 |
|---|---|---|
| 주제 (대분류+중분류) | 키워드 vs AI 점수 비교 → 높은 쪽 사용 | 키워드와 AI 정확도가 비슷 → 점수 비교로 각각의 장점 활용 |
| 작업유형 (대분류+중분류) | AI로 전면 교체 | AI가 키워드 대비 정확도 우세 |
1. nlp_model.py (1차: 키워드 태깅)
빌딩 관리 도메인에는 엘리베이터, SMPS, 공조기 등 특수 장비명과 시설 용어가 많아, 일반 AI 모델만으로는 놓치기 쉬운 분류 신호가 존재한다. 키워드 태깅을 병행하여 이러한 도메인 특화 단어에 대한 분류 정확도를 보완한다.
DB의 분류 체계(voc_taxonomy)에 등록된 키워드를 기반으로 VOC 텍스트를 분류한다. 텍스트 전처리 후 voc_taxonomy 키워드를 순회하며 위치 가중치·히트 수·priority를 곱한 점수를 계산하고, 최고 점수 분류를 선택한다.
키워드 점수 공식:
score = sent_w × (hits + length_bonus_scale × length_bonus) × priority
sent_w: 문장 내 위치 가중치 (앞쪽일수록 높음)hits: 키워드 매칭 횟수priority: voc_taxonomy의 priority 값 (기타 분류를 낮게 설정하여 기타 선택 확률 감소)
2. ai_classifier.py (2차: AI 오버레이)
DB에서 is_reviewed=TRUE 데이터를 로드하여 결합 라벨("대분류>중분류")로 AI 모델을 학습한다. 키워드 태깅 CSV에 오버레이 적용 시 키워드 점수와 AI 확률을 비교하여 최종 분류를 결정한다.
alpha란: 키워드 점수를 [0, 1]로 정규화할 때 쓰는 스케일 파라미터다 (kw_conf = score / (score + alpha)). 도메인 지식(키워드)과 AI(통계 학습) 간의 균형을 조절하는 역할을 한다. alpha가 작으면 키워드 우선, 크면 AI 우선이 된다.
기본값은 400이며, 배치 학습 시 CV(교차 검증) 기반으로 최적 alpha를 자동 탐색하여 alpha_config.json에 저장한다.
# 점수 정규화: kw_conf = score / (score + alpha)
# 키워드 score = 100 예시
alpha=50 → kw_conf = 100 / (100 + 50) = 0.67 (키워드 우선)
alpha=400 → kw_conf = 100 / (100 + 400) = 0.20 (AI 우선)
3. run_monthly.py (오케스트레이터)
배치 시작 시 AI 모델을 1회 학습한 후, 각 빌딩에 대해 키워드 태깅 → AI 오버레이를 순차 적용한다. DB에 is_reviewed=TRUE가 1건이라도 있으면 DB에서 학습하고, 0건이면 Google Sheets를 fallback으로 사용한다.
S3 경로 구조
s3://<your-bucket>/stat/voc/
└── {yyyymm}/{building_id}/
├── tagged_{building_id}_{yyyymm}_{run_id}.csv # full 모드
├── reviewed_{building_id}_{yyyymm}_{run_id}.csv # refresh 모드
└── dashboard_{building_id}_{yyyymm}_{run_id}.html # HTML 리포트
run_id 형식: {prefix}_{yyyymmdd}_{hhmmss}
- prefix: 실행 모드(
full,refresh,tagging-only) 또는scheduler
Human-in-the-loop 워크플로우
flowchart LR
subgraph Step1["1. 자동 태깅 (full)"]
A1["키워드 태깅<br/>+ AI 오버레이"]
A2["하이브리드<br/>자동 분류"]
end
subgraph Step2["2. 검수"]
B1[Google Sheets]
B2["드롭다운으로<br/>수동 검수"]
end
subgraph Step3["3. 반영 (refresh)"]
C1["is_reviewed=TRUE<br/>DB UPSERT"]
C2[HTML 재생성]
end
subgraph Step4["4. 재학습"]
D1[ai_classifier.py]
D2["DB 학습<br/>정확도↑"]
end
Step1 --> Step2 --> Step3 --> Step4
Step4 -.->|다음 배치| Step1
초기 설정 (1회):
기존 검수 데이터(Sheets에 쌓여있던 과거 전체)를 DB에 적재하기 위해, 서비스 최초 도입 시 과거 전월 데이터를 일괄 refresh한다.
이후 voc_classification.is_reviewed=TRUE 레코드가 DB에 적재되어 다음 full 배치부터 DB 학습이 적용된다.
월간 운영 사이클 (정상 운영):
- 매월 1일 02:00 (자동):
full모드 실행 → DBis_reviewed=TRUE전체 누적 데이터로 AI 학습 (0건이면 Sheets fallback) → 하이브리드 자동 태깅 + HTML 생성 + 검수시트 업로드 - 매월 1~14일: 검수자가 Google Sheets에서 드롭다운으로 태깅 검수/수정 (검수완료 = Y)
- 매월 15일 02:00 (자동):
refresh모드 실행 → 검수 데이터를is_reviewed=TRUE로 DB UPSERT → HTML 재생성 → 다음 달 1일 full 배치 학습에 반영
is_reviewed 누적 구조:
refresh 실행 시마다 검수완료=Y 데이터가 DB에 is_reviewed=TRUE로 쌓임
→ fetch_reviewed_training_data() 는 날짜 필터 없이 전체 누적 조회
→ 매달 학습 데이터가 늘어날수록 모델 정확도 향상
분류 전략 성과
하이브리드 전략의 효과:
- 주제: 키워드와 AI 정확도가 비슷 → 점수 비교로 각각의 장점 활용, 키워드 매칭 실패(기타) 케이스를 AI가 보완
- 작업유형: AI가 키워드 대비 정확도 우세 → AI로 전면 교체
학습 데이터가 축적될수록 AI 정확도가 지속 향상되며, alpha 최적화를 통해 키워드와 AI 간 균형을 자동 조정한다.
스케줄링 (APScheduler + Docker)
cron 대신 Docker 컨테이너 내부에서 APScheduler로 월간 배치를 스케줄링한다.
# scheduler.py
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
scheduler = BlockingScheduler(timezone="Asia/Seoul")
scheduler.add_job(run_batch, CronTrigger(day=1, hour=2, minute=0), id="monthly_batch")
scheduler.add_job(run_refresh, CronTrigger(day=15, hour=2, minute=0), id="monthly_refresh")
scheduler.start()
run_batch: 매월 1일 02:00 KST — full 배치 (태깅 + AI 학습 + HTML + 검수시트)run_refresh: 매월 15일 02:00 KST — refresh 배치 (검수 반영 + is_reviewed UPSERT)
Docker 실행:
# 스케줄러 컨테이너 (상시 실행, 매월 1일/15일 자동 배치)
docker run -d --name nlp-scheduler -e ENV=prd \
-v /path/to/output:/app/output \
-v /path/to/logs:/app/logs \
-v /path/to/.credentials:/app/.credentials \
-v ~/.aws:/root/.aws:ro \
voc-nlp-prd:latest scheduler
환경 설정
.env 환경변수:
# DB
DB_HOST=your-db-host
DB_PORT=5432
DB_NAME=your-db-name
DB_USER=your-user
DB_PASSWORD=your-password
# S3 (환경별 prefix 분리)
S3_BUCKET=your-bucket
S3_PREFIX=stat/voc # 운영: stat/voc, 개발: stat/voc-dev, 스테이징: stat/voc-stg
# 경로
OUT_DIR=/app/output
LOG_DIR=/app/logs
검수 데이터가 축적될수록 AI 모델 재학습 품질이 향상되며, 자동 태깅 정확도가 개선되어 검수 부담이 줄어드는 선순환 구조를 목표로 한다.