
Image Source: Generated by Nano Banana
"AI 정확도가 100%가 아니면 현업에서 못 쓰는 거 아니에요?"라는 질문을 받았다. 답은 "아니다"였다.
들어가며
의류 반품 검수 현장은 생각보다 복잡하다.
하루 반품량: 5,000벌
검수 항목: 오염, 찢어짐, 변색, 보풀, 단추 이탈 등 12가지
작업자 1명당 처리량: 200벌/일
소요 시간: 1벌당 평균 2분문제는 검수 품질이다. 사람이 하루 종일 옷을 보면 집중력이 떨어진다. 저녁 6시쯤 되면 오탐률이 30%까지 치솟는다.
"AI로 자동화하면 되지 않나요?"
시도해봤다. YOLOv8 기반 불량 검출 모델을 만들어서 파일럿 테스트를 돌렸다.
결과:
- mAP50: 0.83 (83%)
- Precision: 0.79
- Recall: 0.8583%면 나쁘지 않다. 하지만 현장 반응은 싸늘했다.
"AI가 놓친 불량은 누가 책임지나요?"
"오탐이 17%면 직원이 다시 확인해야 해서 오히려 일이 늘어요."
그래서 우리는 방향을 바꿨다. AI가 모든 걸 하게 하지 말고, 사람과 협업하게 하자.
Human-in-the-Loop란?
AI가 판단하고, 애매한 케이스만 사람에게 물어보는 구조다.
전통적인 AI 시스템:
AI 판단 → 끝
(틀려도 그대로 진행)
Human-in-the-Loop:
AI 판단 → 신뢰도 낮으면 → 사람 확인 → 피드백 → AI 학습의료 영상 진단, 자율주행, 콘텐츠 검열 등 "실수가 치명적인" 분야에서 많이 쓴다.
우리 케이스에서는:
- AI가 확실한 것(신뢰도 95% 이상): 자동 통과/거부
- AI가 애매한 것(신뢰도 70~95%): 작업자에게 전달
- 작업자 판단 → 다시 AI 학습에 활용
시스템 아키텍처
2단계 파이프라인: Detection + Classification
우리는 단순한 객체 탐지를 넘어, 탐지와 분류를 분리한 2단계 파이프라인을 구축했다.
이미지 입력
↓
[1단계] RT-DETR-L
- 불량 영역 탐지 (Bounding Box)
- 빠른 추론 속도 (40ms)
↓
불량 후보 영역 추출
↓
[2단계] Qwen2.5-VL 7B Instruct
- 불량 유형 분류
- 심각도 판단
- 자연어로 설명 생성
↓
최종 판정 (통과/재검토/거부)왜 2단계로 나눴는가?
단일 모델 (YOLOv8):
✅ 간단한 구조
❌ 불량 유형 12가지를 동시에 학습하면 성능 저하
❌ 미세한 차이 구분 어려움 (오염 vs 변색)
2단계 파이프라인:
✅ RT-DETR: "어디에 뭔가 있다" 빠르게 찾기
✅ Qwen2.5-VL: "그게 정확히 무엇인지" 정밀 분석
✅ 각 모델이 자기 역할에만 집중 → 성능 향상1단계: RT-DETR로 불량 영역 탐지
class DefectDetector:
def __init__(self):
self.model = RTDETRForObjectDetection.from_pretrained(
'apparel-rtdetr-v1.2',
num_classes=1, # 불량 영역만 탐지 (유형 구분 안 함)
)
self.processor = RTDETRImageProcessor.from_pretrained('rtdetr')
async def detect(self, image: Image) -> List[BoundingBox]:
# 1. 이미지 전처리
inputs = self.processor(images=image, return_tensors="pt")
# 2. 추론
with torch.no_grad():
outputs = self.model(**inputs)
# 3. NMS 및 필터링
results = self.processor.post_process_object_detection(
outputs,
threshold=0.25, # 낮은 임계값 (Recall 우선)
target_sizes=[(image.height, image.width)]
)[0]
# 4. Bounding Box 추출
boxes = []
for score, label, box in zip(
results["scores"],
results["labels"],
results["boxes"]
):
if score >= 0.25:
boxes.append({
'bbox': box.tolist(),
'confidence': score.item(),
'area': (box[2] - box[0]) * (box[3] - box[1])
})
return boxes
RT-DETR 선택 이유:
YOLOv8 vs RT-DETR:
YOLOv8:
- 추론 속도: 30ms
- mAP50: 0.83
- NMS 필요 (후처리 복잡)
RT-DETR-L:
- 추론 속도: 40ms (조금 느림)
- mAP50: 0.87 (더 정확)
- NMS 불필요 (Transformer 기반)
- 작은 불량도 잘 탐지
결론: 10ms 느린 것보다 정확도 4%p 향상이 더 중요2단계: Qwen2.5-VL 7B로 불량 분류 및 판단
from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
class DefectClassifier:
def __init__(self):
self.model = Qwen2VLForConditionalGeneration.from_pretrained(
"Qwen/Qwen2.5-VL-7B-Instruct",
torch_dtype=torch.float16,
device_map="auto"
)
self.processor = AutoProcessor.from_pretrained(
"Qwen/Qwen2.5-VL-7B-Instruct"
)
# 불량 유형 정의
self.defect_types = [
"오염 (stain)",
"찢어짐 (tear)",
"변색 (discoloration)",
"보풀 (pilling)",
"구멍 (hole)",
"단추 이탈 (missing_button)",
# ... 12가지
]
async def classify(
self,
image: Image,
bbox: dict
) -> dict:
# 1. 불량 영역 크롭
x1, y1, x2, y2 = bbox['bbox']
cropped = image.crop((x1, y1, x2, y2))
# 2. 프롬프트 구성
prompt = f"""당신은 의류 품질 검수 전문가입니다.
이미지를 보고 다음을 판단하세요:
1. 불량 유형: {', '.join(self.defect_types)} 중 하나
2. 심각도: 경미(minor) / 중간(moderate) / 심각(severe)
3. 판정: 통과(pass) / 재검토(review) / 거부(reject)
4. 근거: 왜 그렇게 판단했는지 한 문장으로
반드시 JSON 형식으로 답변하세요:
{{
"defect_type": "...",
"severity": "...",
"decision": "...",
"reason": "..."
}}"""
# 3. VLM 추론
messages = [
{
"role": "user",
"content": [
{"type": "image", "image": cropped},
{"type": "text", "text": prompt}
]
}
]
text = self.processor.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
inputs = self.processor(
text=[text],
images=[cropped],
return_tensors="pt"
).to(self.model.device)
# 4. 생성
with torch.no_grad():
outputs = self.model.generate(
**inputs,
max_new_tokens=256,
temperature=0.1, # 낮은 temperature (일관성)
)
response = self.processor.decode(
outputs[0][inputs['input_ids'].shape[1]:],
skip_special_tokens=True
)
# 5. JSON 파싱
try:
result = json.loads(response)
result['confidence'] = bbox['confidence']
result['bbox'] = bbox['bbox']
return result
except json.JSONDecodeError:
# VLM이 JSON 형식 안 지킬 때 대비
return {
'defect_type': 'unknown',
'severity': 'review',
'decision': 'review', # 안전하게 사람 확인
'reason': 'Parse error',
'raw_response': response
}
Qwen2.5-VL 선택 이유:
왜 VLM(Vision-Language Model)인가?
기존 분류 모델 (ResNet, EfficientNet):
❌ 12가지 불량 유형을 하드코딩
❌ 새 불량 유형 추가 시 재학습 필요
❌ "왜 그렇게 판단했는지" 설명 불가
Qwen2.5-VL 7B:
✅ 자연어 프롬프트로 유연하게 제어
✅ 새 불량 유형 추가 시 프롬프트만 수정
✅ 판단 근거를 자연어로 설명 (작업자가 이해 가능)
✅ Few-shot learning 가능실제 추론 예시:
// Input: 흰 셔츠에 커피 얼룩
{
"bbox": [120, 340, 280, 480],
"confidence": 0.87
}
// Qwen2.5-VL Output:
{
"defect_type": "오염 (stain)",
"severity": "moderate",
"decision": "review",
"reason": "갈색 액체 얼룩이 약 3cm 크기로 앞면에 있으며, 세탁으로 제거 가능 여부 불확실"
}
통합 파이프라인
class QualityInspectionPipeline:
def __init__(self):
self.detector = DefectDetector() # RT-DETR
self.classifier = DefectClassifier() # Qwen2.5-VL
self.high_confidence_threshold = 0.95
self.low_confidence_threshold = 0.70
async def inspect(self, image_path: str):
image = Image.open(image_path)
# 1단계: RT-DETR로 불량 영역 탐지
detections = await self.detector.detect(image)
if not detections:
# 불량 없음
return {
'decision': 'pass',
'confidence': 1.0,
'route': 'auto',
'defects': []
}
# 2단계: 각 영역을 Qwen2.5-VL로 분석
classifications = []
for bbox in detections:
result = await self.classifier.classify(image, bbox)
classifications.append(result)
# 3단계: 최종 판정
return self._make_final_decision(classifications)
def _make_final_decision(self, classifications: List[dict]):
# 심각도별 가중치
severity_weights = {
'minor': 1,
'moderate': 2,
'severe': 3
}
# 가장 심각한 불량 찾기
max_severity = max(
(severity_weights[c['severity']] for c in classifications),
default=0
)
# VLM이 이미 판정을 내린 경우
reject_count = sum(
1 for c in classifications
if c['decision'] == 'reject'
)
if reject_count > 0:
# 하나라도 reject면 사람 확인
return {
'decision': 'review',
'route': 'human',
'priority': 'high',
'defects': classifications,
'reason': f'{reject_count}개 불량이 심각함'
}
# 전체 신뢰도 계산
avg_confidence = sum(
c['confidence'] for c in classifications
) / len(classifications)
if avg_confidence >= self.high_confidence_threshold:
# 확실한 판정
if max_severity >= 2: # moderate 이상
return {
'decision': 'reject',
'route': 'auto',
'confidence': avg_confidence,
'defects': classifications
}
else:
return {
'decision': 'review',
'route': 'human',
'priority': 'medium',
'defects': classifications
}
else:
# 애매함 → 사람 확인
return {
'decision': 'review',
'route': 'human',
'priority': self._calculate_priority(avg_confidence),
'defects': classifications
}
성능 최적화: 배치 처리
class BatchInspectionPipeline:
async def inspect_batch(
self,
image_paths: List[str],
batch_size: int = 8
):
# 1단계: RT-DETR 배치 추론
all_detections = await self.detector.detect_batch(
image_paths,
batch_size=batch_size
)
# 2단계: Qwen2.5-VL은 개별 처리
# (VLM은 배치 처리 시 GPU 메모리 부족)
results = []
for image_path, detections in zip(image_paths, all_detections):
if not detections:
results.append({
'image_path': image_path,
'decision': 'pass',
'route': 'auto'
})
continue
image = Image.open(image_path)
classifications = []
for bbox in detections:
result = await self.classifier.classify(image, bbox)
classifications.append(result)
final = self._make_final_decision(classifications)
final['image_path'] = image_path
results.append(final)
return results
실제 성능
RT-DETR-L:
- 추론 속도: 40ms/이미지
- mAP50: 0.87
- mAP50-95: 0.64
- GPU: RTX 4060 Ti (16GB)
Qwen2.5-VL 7B Instruct:
- 추론 속도: 1.2초/영역 (FP16)
- 정확도: 92% (사람과 비교)
- GPU: RTX 4090 (24GB)
통합 파이프라인:
- 평균 불량 영역: 1.3개/이미지
- 총 처리 시간: 1.6초/이미지
- 일일 처리량: 약 3만 건 (16시간 운영)1. 모델 평가 기준 설계
RT-DETR (Detection 모델) 기준:
# RT-DETR 평가 기준
detection_model:
pass_criteria:
mAP50:
min: 0.85
description: 불량 영역 탐지 정확도
recall:
min: 0.88
description: 불량을 놓치지 않는 것이 중요 (FN 최소화)
precision:
min: 0.75
description: 오탐은 VLM이 걸러주므로 다소 관대
Qwen2.5-VL (Classification 모델) 기준:
# VLM 평가 기준
classification_model:
pass_criteria:
accuracy:
min: 0.90
description: 불량 유형 분류 정확도
human_agreement:
min: 0.88
description: 전문가와 판단 일치율
explanation_quality:
min: 0.85
description: 설명의 적절성 (사람이 평가)
왜 RT-DETR은 Recall을, VLM은 Accuracy를 보는가?
RT-DETR 역할:
- "불량이 있을 수도 있는 영역" 모두 찾기
- 오탐(FP)은 괜찮음 → VLM이 나중에 걸러냄
- 미탐(FN)은 치명적 → 놓치면 불량품 유출
Qwen2.5-VL 역할:
- RT-DETR이 찾은 영역을 정밀 분석
- "이게 진짜 불량인지, 그림자인지" 구분
- 정확한 유형 분류 + 판단 근거 제시2. 메트릭 해석: 우리가 실제로 보는 것
mAP50 vs mAP50-95
metrics:
primary:
- name: mAP50
description: Mean Average Precision @ IoU 0.5
- name: mAP50-95
description: Mean Average Precision @ IoU 0.5:0.95
mAP50: "불량 위치를 대충이라도 맞췄나?"
예측 박스와 실제 박스가 50% 이상 겹치면 정답 인정
의류 불량 검수에서는 이 정도면 충분 (정확한 픽셀 위치보다 부위 파악이 중요)mAP50-95: "불량 위치를 정교하게 맞췄나?"
IoU 50%, 55%, 60%, ..., 95%까지 모두 측정
달성하기 훨씬 어려움 (우리는 0.62 정도)
로봇 팔 제어처럼 정밀한 좌표가 필요할 때 중요우리의 선택: mAP50 중심
이유:
- 불량 "존재 여부"가 더 중요
- 정확한 픽셀 위치는 작업자가 눈으로 확인하면 됨
- mAP50-95를 높이려면 데이터 라벨링 비용이 10배 증가Precision vs Recall의 Trade-off
secondary:
- name: precision
description: 정밀도 (TP / (TP + FP))
- name: recall
description: 재현율 (TP / (TP + FN))
실전 예시:
테스트 데이터: 100벌
실제 불량: 20벌
AI 예측: 불량 25벌
분석:
- TP (진짜 불량을 불량으로): 18벌
- FP (정상을 불량으로): 7벌
- FN (불량을 정상으로): 2벌
Precision = 18 / (18 + 7) = 0.72 (72%)
Recall = 18 / (18 + 2) = 0.90 (90%)의미:
Precision 72%:
- AI가 "불량"이라고 한 것 중 실제로는 28%가 정상
- 작업자가 오탐 7건을 다시 확인해야 함
Recall 90%:
- 실제 불량 20건 중 18건을 찾음
- 2건을 놓쳤음 (위험!)우리의 우선순위: Recall > Precision
# 불량을 놓치는 것(FN)이 더 위험
# 오탐(FP)은 작업자가 다시 보면 되지만
# 미탐(FN)은 불량품이 고객에게 간다
config = {
'confidence_threshold': 0.25, # 낮게 설정 (Recall 높이기)
'human_review_threshold': 0.70 # 애매한 것은 사람이 확인
}
3. 신뢰도 기반 라우팅
핵심 로직은 간단하다.
class QualityInspectionPipeline:
def __init__(self):
self.model = load_model('apparel-defect-v2.3')
self.high_confidence_threshold = 0.95
self.low_confidence_threshold = 0.70
async def inspect(self, image_path: str):
# 1. AI 예측
result = self.model.predict(image_path)
# 2. 최고 신뢰도 스코어 확인
max_confidence = max([det.confidence for det in result.detections])
# 3. 라우팅
if max_confidence >= self.high_confidence_threshold:
# 확실한 불량 → 자동 거부
return {
'decision': 'reject',
'confidence': max_confidence,
'route': 'auto',
'defects': result.detections
}
elif max_confidence >= self.low_confidence_threshold:
# 애매함 → 사람 확인 필요
return {
'decision': 'review_required',
'confidence': max_confidence,
'route': 'human',
'defects': result.detections,
'priority': self._calculate_priority(max_confidence)
}
else:
# 불량 없음 → 자동 통과
return {
'decision': 'pass',
'confidence': 1 - max_confidence,
'route': 'auto'
}
def _calculate_priority(self, confidence: float):
# 신뢰도가 낮을수록 우선순위 높음
if confidence < 0.75:
return 'high'
elif confidence < 0.85:
return 'medium'
else:
return 'low'
실제 운영 결과:
일일 검수량: 5,000벌
자동 처리 (신뢰도 95% 이상):
- 3,500벌 (70%)
- 작업자 개입 불필요
사람 확인 필요 (신뢰도 70~95%):
- 1,200벌 (24%)
- 우선순위 큐로 관리
자동 통과 (불량 없음):
- 300벌 (6%)
효과:
- 작업자 업무량 70% 감소
- 검수 정확도 향상 (사람이 애매한 것만 집중)4. 오분류 케이스 수집 및 재학습
Human-in-the-Loop의 핵심은 피드백 루프다.
class FeedbackCollector:
async def collect_human_decision(
self,
image_id: str,
ai_prediction: dict,
human_decision: dict
):
# 1. AI와 사람의 판단 비교
is_disagreement = (
ai_prediction['decision'] != human_decision['decision']
)
if is_disagreement:
# 2. 오분류 케이스 저장
await self.save_error_case({
'image_id': image_id,
'ai_prediction': ai_prediction,
'human_decision': human_decision,
'error_type': self._classify_error(
ai_prediction,
human_decision
),
'timestamp': datetime.now()
})
# 3. 사람 판단을 Ground Truth로 저장
await self.update_training_data(
image_id=image_id,
labels=human_decision['labels'],
verified=True,
verified_by=human_decision['operator_id']
)
def _classify_error(self, ai_pred, human_dec):
if ai_pred['decision'] == 'reject' and human_dec['decision'] == 'pass':
return 'false_positive' # 오탐
elif ai_pred['decision'] == 'pass' and human_dec['decision'] == 'reject':
return 'false_negative' # 미탐
else:
return 'classification_error' # 불량 유형 오분류
오분류 케이스 분석 대시보드:
// 주간 리포트
{
"week": "2024-W05",
"total_reviews": 8400,
"ai_human_agreement": 0.87,
"error_breakdown": {
"false_positive": 680, // AI가 불량이라 했는데 정상
"false_negative": 412, // AI가 정상이라 했는데 불량
"classification_error": 100 // 불량 유형 오분류
},
"top_error_patterns": [
{
"type": "light_stain_on_white",
"count": 156,
"action": "재학습 데이터에 추가"
},
{
"type": "shadow_misdetection",
"count": 98,
"action": "데이터 증강 강화"
}
]
}
5. 지속적 재학습 파이프라인
# 재학습 트리거 조건
class RetrainingTrigger:
def should_retrain(self) -> bool:
metrics = self.get_recent_metrics()
# 조건 1: 신규 검증 데이터 1,000건 이상
if metrics['verified_samples'] >= 1000:
return True
# 조건 2: AI-Human 불일치율 15% 이상
if metrics['disagreement_rate'] >= 0.15:
return True
# 조건 3: 특정 클래스 성능 하락
for class_name, perf in metrics['per_class'].items():
if perf['precision'] < 0.70:
return True
return False
# 재학습 프로세스
async def retrain_pipeline():
# 1. 신규 데이터 수집
new_data = await collect_verified_samples(min_count=1000)
# 2. 데이터 증강
augmented_data = augment_dataset(
new_data,
augmentation_config={
'rotation': [-15, 15],
'brightness': [0.8, 1.2],
'contrast': [0.9, 1.1],
}
)
# 3. 모델 학습
new_model = train_model(
base_model='apparel-defect-v2.3',
train_data=augmented_data,
val_split=0.2,
epochs=50
)
# 4. 평가
eval_results = evaluate_model(
new_model,
test_dataset='test-v3-latest'
)
# 5. 배포 기준 검증
if passes_criteria(eval_results):
# 6. 스테이징 배포 (10% 트래픽)
await deploy_to_staging(new_model, traffic_ratio=0.1)
# 7. A/B 테스트 (7일)
ab_results = await run_ab_test(
model_a='apparel-defect-v2.3',
model_b=new_model,
duration_days=7
)
# 8. 승자 결정
if ab_results['model_b_better']:
await promote_to_production(new_model)
logger.info(f"Model upgraded: v2.3 → v2.4")
else:
logger.warning("New model did not pass criteria")
await notify_ml_team(eval_results)
실전 운영 경험
배포 기준: 까다롭게 vs 유연하게?
초기에는 기준을 너무 빡빡하게 잡았다.
# v1.0 기준 (너무 엄격)
pass_criteria:
mAP50: 0.90
precision: 0.85
recall: 0.90
결과: 3개월 동안 재학습 8번 했는데 배포는 0번.
문제:
- 신규 데이터로 학습하면 기존 클래스 성능이 조금씩 떨어짐
- 완벽한 모델을 기다리느라 현장은 여전히 수작업기준을 현실적으로 조정했다.
# v2.0 기준 (실용적)
pass_criteria:
mAP50: 0.85
precision: 0.80
regression_check:
max_degradation: 0.02 # 2% 하락까지 허용
결과: 한 달에 1~2번 안정적으로 배포.
VLM 프롬프트 엔지니어링
초기 프롬프트는 단순했다.
# v1.0 프롬프트 (너무 단순)
prompt = "이 이미지의 불량 유형을 분류하세요."
결과: VLM이 일관성 없는 답변 생성.
// 같은 오염 사진인데
Image 1: "커피 얼룩"
Image 2: "갈색 오염"
Image 3: "액체 자국"
// → 분류 불가능
개선된 프롬프트:
# v2.0 프롬프트 (구조화)
prompt = f"""당신은 의류 품질 검수 전문가입니다.
이미지를 보고 다음을 판단하세요:
1. 불량 유형: {', '.join(self.defect_types)} 중 **정확히 하나**만 선택
2. 심각도:
- minor: 세탁으로 제거 가능하거나 거의 눈에 띄지 않음
- moderate: 눈에 띄지만 착용 가능할 수 있음
- severe: 판매 불가능, 즉시 거부해야 함
3. 판정: pass(통과) / review(재검토) / reject(거부)
4. 근거: 판단 이유를 구체적으로 한 문장으로
반드시 아래 JSON 형식으로만 답변하세요:
{{
"defect_type": "오염 (stain)",
"severity": "moderate",
"decision": "review",
"reason": "앞면 중앙에 약 3cm 크기의 갈색 액체 얼룩. 세탁으로 제거 가능 여부 불확실."
}}"""
Few-shot Learning 추가:
# v3.0 프롬프트 (Few-shot)
examples = """
예시 1:
입력: [흰 셔츠에 작은 먼지]
출력: {{"defect_type": "오염 (stain)", "severity": "minor", "decision": "pass", "reason": "작은 먼지로 세탁 시 제거 가능"}}
예시 2:
입력: [청바지 무릎 부분 5cm 찢어짐]
출력: {{"defect_type": "찢어짐 (tear)", "severity": "severe", "decision": "reject", "reason": "무릎 부분 5cm 찢어짐으로 수선 불가"}}
예시 3:
입력: [그림자가 진 부분]
출력: {{"defect_type": "정상 (normal)", "severity": "none", "decision": "pass", "reason": "조명에 의한 그림자로 실제 불량 아님"}}
이제 아래 이미지를 판단하세요:
"""
prompt = examples + base_prompt
결과: 정확도 78% → 92%
데이터 불균형 해결
초기 RT-DETR 학습 데이터:
오염 (stain): 5,000장
찢어짐 (tear): 3,200장
보풀 (pilling): 800장 ← 문제
변색 (discoloration): 1,200장보풀 검출 성능이 형편없었다.
보풀 클래스:
- Recall: 0.52 (놓침이 너무 많음)
- Precision: 0.61RT-DETR 레벨 해결책:
# 1. 클래스별 가중치 손실 함수
criterion = FocalLoss(
alpha=[1.0, 1.0, 3.0, 1.5], # 보풀에 3배 가중치
gamma=2.0
)
# 2. 데이터 증강 강화 (보풀만)
if class_name == 'pilling':
augmentations = [
A.RandomBrightnessContrast(p=0.8),
A.GaussNoise(p=0.5),
A.Blur(blur_limit=3, p=0.3),
# 보풀은 조명에 따라 잘 안 보임
]
VLM 레벨 해결책:
# Few-shot 예시에 보풀 케이스 많이 추가
few_shot_examples = {
'pilling': 5, # 보풀 예시 5개
'stain': 2,
'tear': 2,
# ...
}
결과:
보풀 클래스:
- Recall: 0.84 (↑ 0.32)
- Precision: 0.81 (↑ 0.20)신뢰도 임계값 튜닝
처음에는 임계값을 임의로 정했다.
high_confidence = 0.90
low_confidence = 0.60
2주 후 현장 피드백:
"AI가 확신 없다고 너무 많이 떠넘겨요"
- 사람 확인 필요: 40% (너무 많음)실험:
# 임계값 후보
thresholds = [
(0.95, 0.70),
(0.93, 0.75),
(0.90, 0.80),
]
# 실제 데이터로 시뮬레이션
for high, low in thresholds:
result = simulate_routing(
test_data,
high_threshold=high,
low_threshold=low
)
print(f"High={high}, Low={low}")
print(f" Auto: {result['auto_ratio']:.1%}")
print(f" Human: {result['human_ratio']:.1%}")
print(f" Accuracy: {result['accuracy']:.3f}")
최종 선택: (0.95, 0.70)
Auto: 70%
Human: 24%
Accuracy: 0.91
작업자 만족도: 높음성과
정량적 지표
Before (수작업):
- 일일 처리량: 200벌/인
- 검수 정확도: 82%
- 작업자 피로도: 높음
After (AI + Human):
- 일일 처리량: 680벌/인 (↑ 240%)
- 검수 정확도: 91% (↑ 9%p)
- 작업자 피로도: 중간
- AI 자동 처리: 70%비용 절감
인건비:
- 작업자 10명 → 4명
- 월 절감액: 약 1,800만 원
불량품 유출 감소:
- 유출률: 3.2% → 0.8%
- 고객 불만 감소: 72%정성적 효과
작업자 피드백:
"이제 AI가 확실한 건 다 걸러주니까 저는 애매한 것만 집중해서 볼 수 있어요.
예전에는 하루 종일 옷만 보다가 집에 가면 눈이 침침했는데 지금은 한결 낫습니다."
"처음엔 AI가 제 일자리 뺏는 줄 알았는데, 오히려 단순 반복 작업을 덜어주니 좋네요."
ML팀 입장:
"처음엔 mAP 90% 넘기려고 몇 달을 삽질했는데,
85%에서 멈추고 Human-in-the-Loop로 전환하니까 오히려 현장에서 더 만족해요."
배운 점
1. 완벽한 AI는 없다
목표가 "100% 자동화"면 실패한다.
목표가 "사람을 80% 도와주기"면 성공한다.2. 메트릭은 현장 맥락에 맞춰라
논문: mAP50-95가 높아야 좋은 모델
현장: mAP50만 괜찮아도 충분히 유용
중요한 건 "어떤 지표"가 아니라
"이 지표가 실제 업무에 무엇을 의미하는가"3. 빠른 피드백 루프가 핵심
수동 재학습 (3개월 주기):
- 성능 정체
- 현장 불만 누적
자동 재학습 (신규 데이터 1,000건):
- 지속적 개선
- 오류 패턴 빠르게 해결4. 사람과 AI의 역할 분담
AI가 잘하는 것:
- 반복 작업
- 일관성 유지
- 빠른 처리
사람이 잘하는 것:
- 애매한 케이스 판단
- 맥락 이해
- 새로운 불량 유형 발견
둘을 섞으면 1+1=3기술 스택
// ML Pipeline
Detection: RT-DETR-L (Transformer 기반 객체 탐지)
Classification: Qwen2.5-VL 7B Instruct (Vision-Language Model)
Training: PyTorch 2.1 + Hugging Face Accelerate
Deployment: FastAPI + Ray Serve (모델 서빙)
Monitoring: Weights & Biases + Custom Dashboard
// GPU Infrastructure
RT-DETR 추론: RTX 4060 Ti 16GB
Qwen2.5-VL 추론: RTX 4090 24GB (FP16 양자화)
학습: A100 40GB (Cloud)
// Backend
API: FastAPI 0.104
Queue: Redis + Celery (비동기 작업)
Database: PostgreSQL 15 (메타데이터) + S3 (이미지)
Caching: Redis (신뢰도 스코어 캐싱)
// Frontend (검수 작업자용 대시보드)
Framework: Next.js 14 App Router
UI: Tailwind CSS + shadcn/ui
Real-time: Server-Sent Events (SSE) - 실시간 작업 큐 업데이트
Visualization: Recharts (메트릭 대시보드)
왜 이 조합인가?
RT-DETR vs YOLOv8
초기: YOLOv8
- mAP50: 0.83
- 작은 불량 놓침 (보풀, 미세 찢어짐)
- NMS 후처리 필요
현재: RT-DETR-L
- mAP50: 0.87 (↑ 4%p)
- Transformer 기반 → 전역적 맥락 이해
- End-to-end → NMS 불필요
- 10ms 더 느리지만 정확도 trade-off 가치 있음Qwen2.5-VL vs 전통적 분류 모델
시도했던 것들:
1. EfficientNet + 12-class Classifier
- 정확도: 78%
- 새 불량 유형 추가 시 재학습 필요
- 판단 근거 없음
2. CLIP + Few-shot
- 정확도: 82%
- 빠르지만 섬세한 구분 어려움
3. Qwen2.5-VL 7B Instruct (최종)
- 정확도: 92%
- 프롬프트로 유연하게 제어
- 자연어 설명 생성 → 작업자 신뢰도 ↑다음 단계
1. Qwen2.5-VL 추론 속도 개선
현재 병목: VLM 추론이 1.2초로 느림.
# 시도 중인 최적화
optimizations = [
{
'method': 'vLLM 통합',
'expected': '1.2s → 0.4s (3배 개선)',
'status': 'testing'
},
{
'method': 'FP8 양자화',
'expected': 'GPU 메모리 24GB → 12GB',
'status': 'testing'
},
{
'method': 'Speculative Decoding',
'expected': 'Token 생성 속도 2배',
'status': 'research'
}
]
2. 멀티모달 확장
현재: 이미지만
추가 예정:
- 촉감 센서 데이터 (질감 불량)
→ "이 보풀은 얼마나 거칠까?"
- 작업자 음성 피드백
→ "이건 세탁 냄새가 나요"
→ Whisper로 음성 인식 → VLM에 텍스트로 전달
- 다각도 촬영
→ RT-DETR로 여러 각도 이미지 통합 분석프로토타입:
class MultiModalInspection:
async def inspect(
self,
images: List[Image], # 4방향 촬영
texture_data: Optional[dict] = None,
voice_note: Optional[str] = None
):
# 1. RT-DETR로 모든 각도 분석
all_detections = []
for img in images:
detections = await self.detector.detect(img)
all_detections.extend(detections)
# 2. VLM에 모든 정보 통합 전달
prompt = f"""
이미지: {len(images)}장 (전면, 후면, 좌측, 우측)
"""
if texture_data:
prompt += f"촉감: 거칠기 {texture_data['roughness']}\n"
if voice_note:
prompt += f"작업자 메모: {voice_note}\n"
# VLM 추론...
3. Active Learning 고도화
# VLM의 불확실성 측정
class UncertaintyEstimator:
async def estimate(self, image, bbox):
# 같은 입력에 여러 번 추론 (temperature 조정)
predictions = []
for temp in [0.1, 0.3, 0.5, 0.7]:
result = await self.vlm.generate(
image,
temperature=temp
)
predictions.append(result)
# 예측 일관성 측정
consistency = self._calculate_agreement(predictions)
if consistency < 0.7:
# 불확실함 → 학습 데이터로 우선 추가
return {
'uncertain': True,
'priority': 'high',
'predictions': predictions
}
4. Edge Deployment
현재: 클라우드 GPU 서버
문제: 네트워크 지연 (평균 200ms)
계획: 온프레미스 추론 서버
- RT-DETR: Jetson AGX Orin (40W)
- Qwen2.5-VL: 서버급 GPU는 여전히 필요
→ Hybrid: Detection은 Edge, Classification은 Cloud
목표:
- RT-DETR 추론: 40ms (현장)
- VLM 추론: 400ms (클라우드)
- 총 지연: 450ms (기존 1.6s에서 개선)5. 다국어 지원
# 현재: 한국어만
prompt = "이미지를 보고 불량을 판단하세요"
# 계획: Qwen2.5-VL은 다국어 지원
prompts = {
'ko': "이미지를 보고 불량을 판단하세요",
'en': "Analyze the image for defects",
'zh': "分析图像中的缺陷",
'ja': "画像の欠陥を分析してください"
}
# 작업자 언어 설정에 따라 자동 전환
정리
AI가 모든 걸 할 필요는 없다. 사람과 협업하면 된다.
Human-in-the-Loop의 핵심:
1. AI가 확실한 것만 자동화
2. 애매한 것은 사람에게
3. 사람의 판단을 다시 학습에 활용
4. 지속적으로 개선우리의 교훈:
처음 목표: AI 정확도 95%
실제 달성: AI 정확도 85% + Human 협업
결과:
- 현장 만족도: 훨씬 높음
- 처리 속도: 3배 빠름
- 정확도: 오히려 더 높음
결론: AI는 도구다.
사람을 대체하는 게 아니라 증강(Augment)하는 것이다.완벽한 AI를 기다리지 말라. 80% 정도면 충분하다. 나머지는 사람이 채운다. 그리고 그 과정에서 AI는 계속 똑똑해진다.
'AI · ML > Computer Vision' 카테고리의 다른 글
| PyTorch 하드웨어 의존성 제거하기: Hugging Face Accelerate로 갈아타야 하는 이유 (0) | 2026.01.28 |
|---|---|
| YOLO만 쓰던 개발자가 RT-DETR을 선택한 이유 (1) | 2026.01.21 |
| YOLO26: 엣지 디바이스를 위한 차세대 객체 탐지 모델 (0) | 2026.01.19 |
