AI가 100% 정확하지 않아도 괜찮다: Human-in-the-Loop로 만드는 의류 불량 검수 시스템

2026. 2. 4. 13:51·AI · ML/Computer Vision
728x90
반응형

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.85

83%면 나쁘지 않다. 하지만 현장 반응은 싸늘했다.

"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.61

RT-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는 계속 똑똑해진다.

728x90
반응형
저작자표시 비영리 변경금지 (새창열림)

'AI · ML > Computer Vision' 카테고리의 다른 글

PyTorch 하드웨어 의존성 제거하기: Hugging Face Accelerate로 갈아타야 하는 이유  (0) 2026.01.28
YOLO만 쓰던 개발자가 RT-DETR을 선택한 이유  (1) 2026.01.21
YOLO26: 엣지 디바이스를 위한 차세대 객체 탐지 모델  (0) 2026.01.19
'AI · ML/Computer Vision' 카테고리의 다른 글
  • PyTorch 하드웨어 의존성 제거하기: Hugging Face Accelerate로 갈아타야 하는 이유
  • YOLO만 쓰던 개발자가 RT-DETR을 선택한 이유
  • YOLO26: 엣지 디바이스를 위한 차세대 객체 탐지 모델
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (144) N
      • Frontend Development (64)
      • Backend Development (27)
      • AI · ML (4) N
        • Computer Vision (4) N
      • Algorithm (35)
        • 백준 (11)
        • 프로그래머스 (18)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (4)
      • Language (6)
        • JavaScript (6)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    모델비교
    객체탐지
    transformer
    Qwen2.5-VL
    frontend development
    tailwindcss
    YOLO
    Object Detection
    CNN
    AI
    컴퓨터비전
    Human-in-the-Loop
    mlops
    rt-detr
    딥러닝
    Vision-Language-Model
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
AI가 100% 정확하지 않아도 괜찮다: Human-in-the-Loop로 만드는 의류 불량 검수 시스템
상단으로

티스토리툴바