# 임베딩 단일화 기반 감정·윤리 통합 분류 시스템 작성일: 2025-08-15 작성자: Claude & happybell80 상태: 아이디어 → 실험 예정 관련: 감정 시스템, 윤리 시스템, 임베딩 서비스 > 2025-11-22 업데이트: 현재 운영 임베딩 서비스는 ko-sroberta 768차원으로 전환되었으며, 본 문서의 384차원 가정은 역사 기록으로만 참고. 차원 불일치 대응은 [251122_happybell80_chromadb_dimension_mismatch.md] 참고. ## 개요 현재 분리된 3개 모델(임베딩, 감정, 윤리)을 단일 임베딩 모델로 통합하여 메모리 67% 절감, 속도 3배 향상을 달성하는 아키텍처입니다. 하나의 벡터로 기억 저장, 감정 분류, 윤리 판단을 동시에 수행합니다. ## 핵심 아이디어 ### 현재 문제점 ``` 현재: 텍스트 → 임베딩(384차원) → ChromaDB 텍스트 → 감정 모델(442MB) → 7개 감정 텍스트 → 윤리 모델(300MB) → 도덕성 판단 문제: 3번 추론, 3배 메모리, 3배 지연 ``` ### 제안 솔루션 ``` 제안: 텍스트 → 임베딩(384차원) → 프로토타입 매칭 → 감정/윤리/저장 장점: 1번 추론, 1개 모델, 통합 벡터 공간 ``` ## 아키텍처 설계 ### 4단계 파이프라인 ```python def unified_classification_pipeline(text, context=None): """ 규칙 → 임베딩 → 프로토타입 → 선형 헤드 → LLM 승격 """ # Stage 0: 전처리 normalized = normalize_korean(text) # 자모, 띄어쓰기, 존댓말 정규화 # Stage 1: 규칙 기반 필터 (고위험 차단) rule_result = rule_engine.check(normalized) if rule_result.is_critical: return Decision(block=True, reason=rule_result.reason) # Stage 2: 단일 임베딩 (한 번만!) embedding = embedder.encode(normalized) # 384차원 if context: embedding = 0.7 * embedding + 0.3 * context.mean_embedding # Stage 3: 프로토타입 매칭 emotion_scores = match_prototypes(embedding, emotion_prototypes) ethics_scores = match_prototypes(embedding, ethics_prototypes) # Stage 4: 신뢰도 검증 if not is_confident(emotion_scores, ethics_scores): # Stage 4a: 선형 분류기 보조 emotion_linear = emotion_head.predict_proba(embedding) ethics_linear = ethics_head.predict_proba(embedding) if still_uncertain(emotion_linear, ethics_linear): # Stage 4b: LLM 승격 return escalate_to_llm(text, context) return Decision( emotion=emotion_scores.top1, ethics=ethics_scores.top1, confidence=min(emotion_scores.confidence, ethics_scores.confidence), embedding=embedding # ChromaDB 저장용 ) ``` ## 프로토타입 구축 방법 ### 1. 견고한 센트로이드 계산 ```python def build_robust_prototype(embeddings, label): """ 노이즈에 강한 프로토타입 생성 """ # Z-정규화 normalized = (embeddings - embeddings.mean(0)) / embeddings.std(0) # 상하위 10% 절삭 평균 trimmed = trim_outliers(normalized, percentile=10) prototype = trimmed.mean(0) # 다양성이 큰 클래스는 다중 프로토타입 if normalized.std() > DIVERSITY_THRESHOLD: kmeans = KMeans(n_clusters=3) prototypes = kmeans.fit(normalized).cluster_centers_ return prototypes # 3개 프로토타입 return prototype # 단일 프로토타입 ``` ### 2. 한국어 특화 프로토타입 ```python # 감정 프로토타입 예시 emotion_prototypes = { "happiness": build_from_examples([ "정말 기쁘고 행복해요", "오늘은 최고의 날이에요", "너무 좋아서 웃음이 나와요" ]), "sadness": build_from_examples([ "슬퍼서 눈물이 나요", "마음이 아프고 힘들어요", "우울하고 외로워요" ]), # ... 7개 감정 } # 윤리 프로토타입 예시 ethics_prototypes = { "moral": build_from_examples([ "서로 돕고 배려해요", "감사하고 존중합니다", "함께 성장해요" ]), "immoral_hate": build_from_examples([ # AI Hub 데이터셋에서 추출 ]), # ... 8개 카테고리 } ``` ## 핵심 위험 관리 ### 1. 임계치와 마진 설정 ```python class ConfidenceValidator: def __init__(self): self.T_max = 0.65 # 최소 유사도 self.T_unknown = 0.35 # Unknown 임계치 self.margin = 0.12 # 1위-2위 최소 차이 def is_confident(self, scores): top2 = sorted(scores.values(), reverse=True)[:2] # Unknown 감지 if top2[0] < self.T_unknown: return False, "unknown" # 마진 부족 (애매한 경우) if top2[0] - top2[1] < self.margin: return False, "ambiguous" # 신뢰할 만한 분류 if top2[0] > self.T_max: return True, "confident" return False, "low_confidence" ``` ### 2. 다중 레이블 처리 ```python def handle_mixed_emotions(scores, threshold=0.5): """ 복합 감정 처리 (예: 화나면서 슬픈) """ active_emotions = [ emotion for emotion, score in scores.items() if score > threshold ] if len(active_emotions) > 1: return { "primary": max(scores, key=scores.get), "secondary": active_emotions, "is_mixed": True } return { "primary": active_emotions[0] if active_emotions else "neutral", "is_mixed": False } ``` ### 3. 문맥 통합 ```python class ContextAwareEmbedding: def __init__(self, alpha=0.7, beta=0.3): self.alpha = alpha # 현재 문장 가중치 self.beta = beta # 문맥 가중치 self.history = deque(maxlen=5) # 최근 5개 발화 def get_contextual_embedding(self, current_embedding): if not self.history: return current_embedding context_embedding = np.mean(self.history, axis=0) combined = self.alpha * current_embedding + self.beta * context_embedding # 정규화 return combined / np.linalg.norm(combined) ``` ### 4. 고위험 카테고리 특별 처리 ```python HIGH_RISK_CATEGORIES = ["violence", "hate", "self_harm", "crime"] def handle_high_risk(text, embedding, scores): """ 고위험 카테고리는 보수적으로 처리 """ # 1. 규칙 기반 추가 검사 if contains_high_risk_patterns(text): return Decision(escalate=True, reason="high_risk_pattern") # 2. 여러 고위험 카테고리에 걸쳐있으면 risk_scores = {k: v for k, v in scores.items() if k in HIGH_RISK_CATEGORIES} if sum(risk_scores.values()) > 0.8: return Decision(escalate=True, reason="multiple_risks") # 3. 애매한 고위험은 무조건 LLM if max(risk_scores.values()) > 0.3 and max(risk_scores.values()) < 0.7: return Decision(escalate=True, reason="ambiguous_risk") return None ``` ## 한국어 특화 전처리 ### 1. 정규화 파이프라인 ```python def normalize_korean(text): """ 한국어 특성을 고려한 정규화 """ # 1. 자모 분해 표현 복원 text = restore_jamo_decomposition(text) # ㅂㅅ → 비속어 # 2. 이모티콘/특수문자 정규화 text = normalize_emojis(text) # 3. 반말/존댓말 통일 text = standardize_honorifics(text) # 4. 띄어쓰기 교정 text = correct_spacing(text) # 5. 은어/속어 사전 치환 text = replace_slang(text) return text ``` ### 2. 완곡 표현 감지 ```python EUPHEMISM_PATTERNS = { "그거": ["성관계", "폭력", "욕설"], # 문맥 필요 "저기": ["민감한_부위", "금기어"], "그렇게": ["부적절한_행동"], } def detect_euphemism(text, context): """ 완곡 표현을 문맥으로 해석 """ for euphemism, possible_meanings in EUPHEMISM_PATTERNS.items(): if euphemism in text: # 문맥 임베딩과 각 의미의 프로토타입 비교 context_scores = { meaning: cosine_similarity(context.embedding, meaning_proto[meaning]) for meaning in possible_meanings } likely_meaning = max(context_scores, key=context_scores.get) if context_scores[likely_meaning] > 0.6: return likely_meaning return None ``` ## 검증 계획 ### 1. 오프라인 검증 ```python def offline_validation(test_data): """ 프로토타입 방식 vs BERT 분류기 비교 """ results = { "prototype": {"emotion": [], "ethics": []}, "bert": {"emotion": [], "ethics": []}, "latency": {"prototype": [], "bert": []} } for text, true_emotion, true_ethics in test_data: # 프로토타입 방식 t1 = time.time() proto_result = prototype_classifier.classify(text) results["latency"]["prototype"].append(time.time() - t1) results["prototype"]["emotion"].append(proto_result.emotion == true_emotion) results["prototype"]["ethics"].append(proto_result.ethics == true_ethics) # BERT 방식 t2 = time.time() bert_result = bert_classifier.classify(text) results["latency"]["bert"].append(time.time() - t2) results["bert"]["emotion"].append(bert_result.emotion == true_emotion) results["bert"]["ethics"].append(bert_result.ethics == true_ethics) # 성능 비교 print(f"프로토타입 정확도: {np.mean(results['prototype']['emotion']):.2%}") print(f"BERT 정확도: {np.mean(results['bert']['emotion']):.2%}") print(f"속도 향상: {np.mean(results['latency']['bert']) / np.mean(results['latency']['prototype']):.1f}x") ``` ### 2. 온라인 A/B 테스트 ```python AB_CONFIG = { "control": {"method": "separate_models", "traffic": 0.7}, "treatment": {"method": "unified_embedding", "traffic": 0.3}, "duration": "2weeks", "metrics": [ "accuracy", "latency_p95", "memory_usage", "unknown_rate", "escalation_rate", "user_satisfaction" ], "guardrails": { "max_latency_ms": 200, "min_accuracy": 0.85, "max_escalation_rate": 0.15 } } ``` ## 모니터링 지표 ### 1. 실시간 대시보드 ```python MONITORING_METRICS = { # 성능 "classification_accuracy": {"threshold": 0.85, "alert": "below"}, "latency_p95": {"threshold": 100, "alert": "above", "unit": "ms"}, # 신뢰도 "unknown_rate": {"threshold": 0.10, "alert": "above"}, "low_confidence_rate": {"threshold": 0.15, "alert": "above"}, "margin_distribution": {"type": "histogram"}, # 드리프트 "embedding_drift": {"method": "PSI", "threshold": 0.2}, "prototype_distance": {"method": "cosine", "threshold": 0.1}, # 비용 "llm_escalation_rate": {"threshold": 0.10, "alert": "above"}, "memory_usage_mb": {"threshold": 500, "alert": "above"} } ``` ### 2. 드리프트 감지 ```python class DriftDetector: def __init__(self, baseline_embeddings): self.baseline_mean = baseline_embeddings.mean(0) self.baseline_std = baseline_embeddings.std(0) def detect_drift(self, new_embeddings, method="PSI"): if method == "PSI": return self.calculate_psi(new_embeddings) elif method == "KS": return self.kolmogorov_smirnov_test(new_embeddings) elif method == "MMD": return self.maximum_mean_discrepancy(new_embeddings) def calculate_psi(self, new_embeddings): """ Population Stability Index """ new_mean = new_embeddings.mean(0) new_std = new_embeddings.std(0) psi = np.sum( (new_mean - self.baseline_mean) * np.log(new_std / self.baseline_std) ) return psi # > 0.2이면 significant drift ``` ## 구현 로드맵 ### Phase 1: 프로토타입 구축 (1주) 1. AI Hub 데이터에서 각 카테고리별 임베딩 추출 2. 견고한 센트로이드 계산 3. 프로토타입 저장 및 버전 관리 ### Phase 2: 분류기 구현 (1주) 1. 단일 임베딩 파이프라인 구현 2. 신뢰도 검증 로직 3. 선형 분류기 헤드 학습 ### Phase 3: 통합 테스트 (2주) 1. 오프라인 정확도 검증 2. 지연/메모리 벤치마크 3. A/B 테스트 설정 ### Phase 4: 배포 및 모니터링 (진행중) 1. 점진적 롤아웃 2. 실시간 모니터링 3. 드리프트 감지 및 재학습 ## 예상 성과 ### 리소스 절감 | 항목 | 현재 (3개 모델) | 제안 (단일 임베딩) | 개선 | |------|----------------|-------------------|------| | 메모리 | 1,260MB | 420MB | -67% | | 추론 시간 | 300ms | 100ms | -67% | | 모델 수 | 3개 | 1개 | -67% | ### 정확도 트레이드오프 | 작업 | BERT 분류기 | 프로토타입 | 차이 | |------|------------|-----------|------| | 감정 분류 | 89% | 85% | -4% | | 윤리 판단 | 87% | 83% | -4% | | 고위험 탐지 | 92% | 90% | -2% | **결론**: 약간의 정확도 손실(-4%)을 감수하고 67%의 리소스를 절감할 수 있습니다. ## 위험 요소 및 대응 ### 1. 정확도 하락 - **위험**: BERT 대비 4-5% 정확도 하락 - **대응**: 고위험 카테고리만 BERT 유지, 나머지 프로토타입 ### 2. Unknown 폭증 - **위험**: 신조어/이슈에서 unknown 다발 - **대응**: 주간 프로토타입 업데이트, 동적 임계치 ### 3. 다중 레이블 처리 - **위험**: 복합 감정/윤리 상황 오판 - **대응**: Top-K 분류, 확률 합이 임계치 이상인 모든 레이블 반환 ### 4. 문맥 손실 - **위험**: 단일 문장만으로 판단 오류 - **대응**: 이전 K개 발화 임베딩 가중 평균 ### 5. 적대적 공격 - **위험**: 의도적 오분류 유도 - **대응**: 규칙 기반 1차 필터, 이상 패턴 감지 ## 임베딩 모델 진화 전략 ### 차원과 성능의 실제 관계 **주의**: "차원↑ = 성능↑"는 단순화된 도식입니다. 실제 성능은 다음 요소들이 복합적으로 작용합니다: 1. **사전학습 목표**: SimCSE vs MultiTask vs Contrastive 2. **학습 데이터 도메인**: 일반 코퍼스 vs 도메인 특화 3. **풀링과 정규화**: Mean pooling vs CLS token, L2 정규화 vs Whitening 4. **후처리 설계**: 프로토타입 개수, 거리 함수, 마진 설정 ### 한국어 임베딩 모델 벤치마크 | 모델 | 차원 | 크기 | 한국어 성능 | 특징 | 권장 용도 | |------|------|------|------------|------|-----------| | MiniLM-L12-v2 | 384 | 134MB | 기본 | 경량, 빠름 | 일반 대화 | | ko-sroberta-multitask | 768 | 400MB | 우수 | 한국어 균형 | 감정/의도 | | KoSimCSE-roberta-large | 1024 | 1.2GB | 최고 | 의미 유사도 강점 | 고정밀 검색 | | BGE-M3 | 1024 | 560MB | 우수 | 다국어, 장문 | 문서 처리 | | E5-large | 1024 | 1.3GB | 우수 | 범용성 | 하이브리드 | ### 마진 기반 에스컬레이션 아키텍처 ```python def adaptive_embedding_with_escalation(text, context=None): """ 마진 기반 3단계 에스컬레이션 낮은 확신도일 때만 고급 모델 사용 """ # Stage 1: 빠른 임베딩 (기본) emb_fast = minilm_embed(text) # 384차원, 10ms pred_fast, margin_fast = prototype_classify(emb_fast) if margin_fast >= 0.3: # 충분한 마진 return pred_fast, "fast", margin_fast # Stage 2: 정확한 임베딩 (한국어 특화) emb_accurate = kosroberta_embed(text) # 768차원, 20ms pred_acc, margin_acc = prototype_classify(emb_accurate) if margin_acc >= 0.25 or (margin_acc - margin_fast) >= 0.15: return pred_acc, "accurate", margin_acc # Stage 3: 정밀 임베딩 (고위험/애매한 경우) emb_precise = kosimcse_large_embed(text) # 1024차원, 35ms pred_precise, margin_precise = prototype_classify(emb_precise) if margin_precise < 0.2: # 여전히 불확실 return escalate_to_llm(text, context) # LLM 판단 return pred_precise, "precise", margin_precise ``` ### 프로토타입 분류 강화 기법 #### 1. 다중 프로토타입 ```python # 클래스당 1개 → K개 프로토타입 prototypes = { "happiness": [ proto_formal, # "기쁩니다" proto_casual, # "좋아요" proto_intense # "너무 행복해요!" ] } ``` #### 2. 거리 함수 고도화 ```python # 단순 코사인 → Mahalanobis 거리 def mahalanobis_distance(x, prototype, covariance): diff = x - prototype return np.sqrt(diff.T @ np.linalg.inv(covariance) @ diff) ``` #### 3. 임베딩 후처리 표준화 ```python def standardize_embedding(embedding): # L2 정규화 normalized = embedding / np.linalg.norm(embedding) # Whitening (분포 정규화) whitened = (normalized - mean_vector) @ whitening_matrix return whitened ``` ### 통합형 vs 분리형 모델 비교 | 구분 | 통합형 (BERT 분류기) | 분리형 (임베딩+프로토타입) | |------|---------------------|-------------------------| | 구조 | 발화→BERT(E2E)→분류 | 발화→임베딩→프로토타입→분류 | | 크기 | 442MB (감정 전용) | 449MB(공유) + 1MB | | 학습 | 임베딩+분류 동시 | 분류 헤드만 학습 | | 정확도 | 89% | 85% (개선 가능) | | 유연성 | 낮음 (고정) | 높음 (모듈 교체) | | 확장성 | 작업별 모델 필요 | 임베딩 재사용 | ### 실전 적용 로드맵 #### Phase 1: 프로토타입 고도화 (즉시) - 다중 프로토타입 구축 (클래스당 3-5개) - L2 정규화 + Whitening 적용 - 마진 임계값 튜닝 (0.2-0.3) #### Phase 2: 한국어 임베딩 업그레이드 (1주) - ko-sroberta-multitask 테스트 - 감정/윤리만 768차원 적용 - A/B 테스트로 검증 #### Phase 3: 에스컬레이션 파이프라인 (2주) - 3단계 임베딩 라우팅 구현 - 마진 기반 자동 선택 - 메트릭 모니터링 #### Phase 4: 고급 최적화 (1개월) - KoSimCSE-large 선택적 도입 - ONNX FP16 양자화 - 토크나이저 병렬화 ### 핵심 설계 원칙 1. **마진이 답이다**: Top1-Top2 차이가 모든 것을 결정 2. **계층적 접근**: 빠른 것부터 시도, 필요시만 승격 3. **도메인 특화**: 감정/윤리는 한국어 특화 모델 우선 4. **측정 후 결정**: 벤치마크보다 실제 데이터로 판단 ## 결론 임베딩 단일화는 **리소스 효율성**과 **통합 관리**의 큰 이점을 제공합니다. 중요한 것은 차원 수가 아니라 **마진 기반 에스컬레이션**과 **프로토타입 품질**입니다. **핵심 성공 요소**: 1. 마진 기반 3단계 라우팅 2. 다중 프로토타입과 고급 거리 함수 3. 한국어 특화 모델 선택적 활용 4. 통합형→분리형 점진적 전환 **즉시 적용 가능한 개선**: - 프로토타입 3개로 확장: +5% 정확도 - L2 정규화 + Whitening: +3% 정확도 - 마진 기반 거절: 오분류 50% 감소 --- *"차원보다 중요한 것은 마진이다"* **다음 단계**: 마진 기반 에스컬레이션 파일럿 테스트