Spaces:
Running
on
Zero
Running
on
Zero
| import numpy as np | |
| import json | |
| from typing import Dict, List, Tuple, Optional, Any, Set | |
| from dataclasses import dataclass, field | |
| from abc import ABC, abstractmethod | |
| import traceback | |
| from sentence_transformers import SentenceTransformer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| from dog_database import get_dog_description | |
| from breed_health_info import breed_health_info | |
| from breed_noise_info import breed_noise_info | |
| from query_understanding import QueryDimensions | |
| from constraint_manager import FilterResult | |
| class DimensionalScores: | |
| """多維度評分結果""" | |
| semantic_scores: Dict[str, float] = field(default_factory=dict) | |
| attribute_scores: Dict[str, float] = field(default_factory=dict) | |
| fused_scores: Dict[str, float] = field(default_factory=dict) | |
| bidirectional_scores: Dict[str, float] = field(default_factory=dict) | |
| confidence_weights: Dict[str, float] = field(default_factory=dict) | |
| class BreedScore: | |
| """品種總體評分結果""" | |
| breed_name: str | |
| final_score: float | |
| dimensional_breakdown: Dict[str, float] = field(default_factory=dict) | |
| semantic_component: float = 0.0 | |
| attribute_component: float = 0.0 | |
| bidirectional_bonus: float = 0.0 | |
| confidence_score: float = 1.0 | |
| explanation: Dict[str, Any] = field(default_factory=dict) | |
| class ScoringHead(ABC): | |
| """抽象評分頭基類""" | |
| def score_dimension(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions, | |
| dimension_type: str) -> float: | |
| """為特定維度評分""" | |
| pass | |
| class SemanticScoringHead(ScoringHead): | |
| """語義評分頭""" | |
| def __init__(self, sbert_model: Optional[SentenceTransformer] = None): | |
| self.sbert_model = sbert_model | |
| self.dimension_embeddings = {} | |
| if self.sbert_model: | |
| self._build_dimension_embeddings() | |
| def _build_dimension_embeddings(self): | |
| """建立維度模板嵌入""" | |
| dimension_templates = { | |
| 'spatial_apartment': "small apartment living, limited space, no yard, urban environment", | |
| 'spatial_house': "house with yard, outdoor space, suburban living, large property", | |
| 'activity_low': "low energy, minimal exercise needs, calm lifestyle, indoor activities", | |
| 'activity_moderate': "moderate exercise, daily walks, balanced activity level", | |
| 'activity_high': "high energy, vigorous exercise, outdoor sports, active lifestyle", | |
| 'noise_low': "quiet, rarely barks, peaceful, suitable for noise-sensitive environments", | |
| 'noise_moderate': "moderate barking, occasional vocalizations, average noise level", | |
| 'noise_high': "vocal, frequent barking, alert dog, comfortable with noise", | |
| 'size_small': "small compact breed, easy to handle, portable size", | |
| 'size_medium': "medium sized dog, balanced proportions, moderate size", | |
| 'size_large': "large impressive dog, substantial presence, bigger breed", | |
| 'family_children': "child-friendly, gentle with kids, family-oriented, safe around children", | |
| 'family_elderly': "calm companion, gentle nature, suitable for seniors, low maintenance", | |
| 'maintenance_low': "low grooming needs, minimal care requirements, easy maintenance", | |
| 'maintenance_moderate': "regular grooming, moderate care needs, standard maintenance", | |
| 'maintenance_high': "high grooming requirements, professional care, intensive maintenance" | |
| } | |
| for key, template in dimension_templates.items(): | |
| if self.sbert_model: | |
| embedding = self.sbert_model.encode(template, convert_to_tensor=False) | |
| self.dimension_embeddings[key] = embedding | |
| def score_dimension(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions, | |
| dimension_type: str) -> float: | |
| """語義維度評分""" | |
| if not self.sbert_model or dimension_type not in self.dimension_embeddings: | |
| return 0.5 # 預設中性分數 | |
| try: | |
| # 建立品種描述 | |
| breed_description = self._create_breed_description(breed_info, dimension_type) | |
| # 生成嵌入 | |
| breed_embedding = self.sbert_model.encode(breed_description, convert_to_tensor=False) | |
| dimension_embedding = self.dimension_embeddings[dimension_type] | |
| # 計算相似度 | |
| similarity = cosine_similarity([breed_embedding], [dimension_embedding])[0][0] | |
| # 正規化到 0-1 範圍 | |
| normalized_score = (similarity + 1) / 2 # 從 [-1,1] 轉換到 [0,1] | |
| return max(0.0, min(1.0, normalized_score)) | |
| except Exception as e: | |
| print(f"Error in semantic scoring for {dimension_type}: {str(e)}") | |
| return 0.5 | |
| def _create_breed_description(self, breed_info: Dict[str, Any], | |
| dimension_type: str) -> str: | |
| """為特定維度創建品種描述""" | |
| breed_name = breed_info.get('display_name', breed_info.get('breed_name', '')) | |
| if dimension_type.startswith('spatial_'): | |
| size = breed_info.get('size', 'medium') | |
| exercise = breed_info.get('exercise_needs', 'moderate') | |
| return f"{breed_name} is a {size} dog with {exercise} exercise needs" | |
| elif dimension_type.startswith('activity_'): | |
| exercise = breed_info.get('exercise_needs', 'moderate') | |
| temperament = breed_info.get('temperament', '') | |
| return f"{breed_name} has {exercise} exercise requirements and {temperament} temperament" | |
| elif dimension_type.startswith('noise_'): | |
| noise_level = breed_info.get('noise_level', 'moderate') | |
| temperament = breed_info.get('temperament', '') | |
| return f"{breed_name} has {noise_level} noise level and {temperament} nature" | |
| elif dimension_type.startswith('size_'): | |
| size = breed_info.get('size', 'medium') | |
| return f"{breed_name} is a {size} sized dog breed" | |
| elif dimension_type.startswith('family_'): | |
| children = breed_info.get('good_with_children', 'Yes') | |
| temperament = breed_info.get('temperament', '') | |
| return f"{breed_name} is {children} with children and has {temperament} temperament" | |
| elif dimension_type.startswith('maintenance_'): | |
| grooming = breed_info.get('grooming_needs', 'moderate') | |
| care_level = breed_info.get('care_level', 'moderate') | |
| return f"{breed_name} requires {grooming} grooming and {care_level} care level" | |
| return f"{breed_name} is a dog breed with various characteristics" | |
| class AttributeScoringHead(ScoringHead): | |
| """屬性評分頭""" | |
| def __init__(self): | |
| self.scoring_matrices = self._initialize_scoring_matrices() | |
| def _initialize_scoring_matrices(self) -> Dict[str, Dict[str, float]]: | |
| """初始化評分矩陣""" | |
| return { | |
| 'spatial_scoring': { | |
| # (user_preference, breed_attribute) -> score | |
| ('apartment', 'small'): 1.0, | |
| ('apartment', 'medium'): 0.6, | |
| ('apartment', 'large'): 0.2, | |
| ('apartment', 'giant'): 0.0, | |
| ('house', 'small'): 0.7, | |
| ('house', 'medium'): 0.9, | |
| ('house', 'large'): 1.0, | |
| ('house', 'giant'): 1.0, | |
| }, | |
| 'activity_scoring': { | |
| ('low', 'low'): 1.0, | |
| ('low', 'moderate'): 0.7, | |
| ('low', 'high'): 0.2, | |
| ('low', 'very high'): 0.0, | |
| ('moderate', 'low'): 0.8, | |
| ('moderate', 'moderate'): 1.0, | |
| ('moderate', 'high'): 0.8, | |
| ('high', 'moderate'): 0.7, | |
| ('high', 'high'): 1.0, | |
| ('high', 'very high'): 1.0, | |
| }, | |
| 'noise_scoring': { | |
| ('low', 'low'): 1.0, | |
| ('low', 'moderate'): 0.6, | |
| ('low', 'high'): 0.1, | |
| ('moderate', 'low'): 0.8, | |
| ('moderate', 'moderate'): 1.0, | |
| ('moderate', 'high'): 0.7, | |
| ('high', 'low'): 0.7, | |
| ('high', 'moderate'): 0.9, | |
| ('high', 'high'): 1.0, | |
| }, | |
| 'size_scoring': { | |
| ('small', 'small'): 1.0, | |
| ('small', 'medium'): 0.5, | |
| ('small', 'large'): 0.2, | |
| ('medium', 'small'): 0.6, | |
| ('medium', 'medium'): 1.0, | |
| ('medium', 'large'): 0.6, | |
| ('large', 'medium'): 0.7, | |
| ('large', 'large'): 1.0, | |
| ('large', 'giant'): 0.9, | |
| }, | |
| 'maintenance_scoring': { | |
| ('low', 'low'): 1.0, | |
| ('low', 'moderate'): 0.6, | |
| ('low', 'high'): 0.2, | |
| ('moderate', 'low'): 0.8, | |
| ('moderate', 'moderate'): 1.0, | |
| ('moderate', 'high'): 0.7, | |
| ('high', 'low'): 0.6, | |
| ('high', 'moderate'): 0.8, | |
| ('high', 'high'): 1.0, | |
| } | |
| } | |
| def score_dimension(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions, | |
| dimension_type: str) -> float: | |
| """屬性維度評分""" | |
| try: | |
| if dimension_type.startswith('spatial_'): | |
| return self._score_spatial_compatibility(breed_info, dimensions) | |
| elif dimension_type.startswith('activity_'): | |
| return self._score_activity_compatibility(breed_info, dimensions) | |
| elif dimension_type.startswith('noise_'): | |
| return self._score_noise_compatibility(breed_info, dimensions) | |
| elif dimension_type.startswith('size_'): | |
| return self._score_size_compatibility(breed_info, dimensions) | |
| elif dimension_type.startswith('family_'): | |
| return self._score_family_compatibility(breed_info, dimensions) | |
| elif dimension_type.startswith('maintenance_'): | |
| return self._score_maintenance_compatibility(breed_info, dimensions) | |
| else: | |
| return 0.5 # 預設中性分數 | |
| except Exception as e: | |
| print(f"Error in attribute scoring for {dimension_type}: {str(e)}") | |
| return 0.5 | |
| def _score_spatial_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """空間相容性評分""" | |
| if not dimensions.spatial_constraints: | |
| return 0.5 | |
| breed_size = breed_info.get('size', 'medium').lower() | |
| total_score = 0.0 | |
| for spatial_constraint in dimensions.spatial_constraints: | |
| key = (spatial_constraint, breed_size) | |
| score = self.scoring_matrices['spatial_scoring'].get(key, 0.5) | |
| total_score += score | |
| return total_score / len(dimensions.spatial_constraints) | |
| def _score_activity_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """活動相容性評分""" | |
| if not dimensions.activity_level: | |
| return 0.5 | |
| breed_exercise = breed_info.get('exercise_needs', 'moderate').lower() | |
| # 清理品種運動需求字串 | |
| if 'very high' in breed_exercise: | |
| breed_exercise = 'very high' | |
| elif 'high' in breed_exercise: | |
| breed_exercise = 'high' | |
| elif 'low' in breed_exercise: | |
| breed_exercise = 'low' | |
| else: | |
| breed_exercise = 'moderate' | |
| total_score = 0.0 | |
| for activity_level in dimensions.activity_level: | |
| key = (activity_level, breed_exercise) | |
| score = self.scoring_matrices['activity_scoring'].get(key, 0.5) | |
| total_score += score | |
| return total_score / len(dimensions.activity_level) | |
| def _score_noise_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """噪音相容性評分""" | |
| if not dimensions.noise_preferences: | |
| return 0.5 | |
| breed_noise = breed_info.get('noise_level', 'moderate').lower() | |
| total_score = 0.0 | |
| for noise_pref in dimensions.noise_preferences: | |
| key = (noise_pref, breed_noise) | |
| score = self.scoring_matrices['noise_scoring'].get(key, 0.5) | |
| total_score += score | |
| return total_score / len(dimensions.noise_preferences) | |
| def _score_size_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """尺寸相容性評分""" | |
| if not dimensions.size_preferences: | |
| return 0.5 | |
| breed_size = breed_info.get('size', 'medium').lower() | |
| total_score = 0.0 | |
| for size_pref in dimensions.size_preferences: | |
| key = (size_pref, breed_size) | |
| score = self.scoring_matrices['size_scoring'].get(key, 0.5) | |
| total_score += score | |
| return total_score / len(dimensions.size_preferences) | |
| def _score_family_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """家庭相容性評分""" | |
| if not dimensions.family_context: | |
| return 0.5 | |
| good_with_children = breed_info.get('good_with_children', 'Yes') | |
| temperament = breed_info.get('temperament', '').lower() | |
| total_score = 0.0 | |
| score_count = 0 | |
| for family_context in dimensions.family_context: | |
| if family_context == 'children': | |
| if good_with_children == 'Yes': | |
| total_score += 1.0 | |
| elif good_with_children == 'No': | |
| total_score += 0.1 | |
| else: | |
| total_score += 0.6 | |
| score_count += 1 | |
| elif family_context == 'elderly': | |
| # 溫和、冷靜的品種適合老年人 | |
| if any(trait in temperament for trait in ['gentle', 'calm', 'docile']): | |
| total_score += 1.0 | |
| elif any(trait in temperament for trait in ['energetic', 'hyperactive']): | |
| total_score += 0.3 | |
| else: | |
| total_score += 0.7 | |
| score_count += 1 | |
| elif family_context == 'single': | |
| # 大多數品種都適合單身人士 | |
| total_score += 0.8 | |
| score_count += 1 | |
| return total_score / max(1, score_count) | |
| def _score_maintenance_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """維護相容性評分""" | |
| if not dimensions.maintenance_level: | |
| return 0.5 | |
| breed_grooming = breed_info.get('grooming_needs', 'moderate').lower() | |
| total_score = 0.0 | |
| for maintenance_level in dimensions.maintenance_level: | |
| key = (maintenance_level, breed_grooming) | |
| score = self.scoring_matrices['maintenance_scoring'].get(key, 0.5) | |
| total_score += score | |
| return total_score / len(dimensions.maintenance_level) | |
| class MultiHeadScorer: | |
| """ | |
| 多頭評分系統 | |
| 結合語義和屬性評分,提供雙向相容性評估 | |
| """ | |
| def __init__(self, sbert_model: Optional[SentenceTransformer] = None): | |
| self.sbert_model = sbert_model | |
| self.semantic_head = SemanticScoringHead(sbert_model) | |
| self.attribute_head = AttributeScoringHead() | |
| self.dimension_weights = self._initialize_dimension_weights() | |
| self.head_fusion_weights = self._initialize_head_fusion_weights() | |
| def _initialize_dimension_weights(self) -> Dict[str, float]: | |
| """初始化維度權重""" | |
| return { | |
| 'activity_compatibility': 0.35, # 最高優先級:生活方式匹配 | |
| 'noise_compatibility': 0.25, # 關鍵:居住和諧 | |
| 'spatial_compatibility': 0.15, # 基本:物理約束 | |
| 'family_compatibility': 0.10, # 重要:社交相容性 | |
| 'maintenance_compatibility': 0.10, # 實際:持續護理評估 | |
| 'size_compatibility': 0.05 # 基本:偏好匹配 | |
| } | |
| def _initialize_head_fusion_weights(self) -> Dict[str, Dict[str, float]]: | |
| """初始化頭融合權重""" | |
| return { | |
| 'activity_compatibility': {'semantic': 0.4, 'attribute': 0.6}, | |
| 'noise_compatibility': {'semantic': 0.3, 'attribute': 0.7}, | |
| 'spatial_compatibility': {'semantic': 0.3, 'attribute': 0.7}, | |
| 'family_compatibility': {'semantic': 0.5, 'attribute': 0.5}, | |
| 'maintenance_compatibility': {'semantic': 0.4, 'attribute': 0.6}, | |
| 'size_compatibility': {'semantic': 0.2, 'attribute': 0.8} | |
| } | |
| def score_breeds(self, candidate_breeds: Set[str], | |
| dimensions: QueryDimensions) -> List[BreedScore]: | |
| """ | |
| 為候選品種評分 | |
| Args: | |
| candidate_breeds: 通過約束篩選的候選品種 | |
| dimensions: 查詢維度 | |
| Returns: | |
| List[BreedScore]: 品種評分結果列表 | |
| """ | |
| try: | |
| breed_scores = [] | |
| # 為每個品種計算分數 | |
| for breed in candidate_breeds: | |
| breed_info = self._get_breed_info(breed) | |
| score_result = self._score_single_breed(breed_info, dimensions) | |
| breed_scores.append(score_result) | |
| # 按最終分數排序 | |
| breed_scores.sort(key=lambda x: x.final_score, reverse=True) | |
| return breed_scores | |
| except Exception as e: | |
| print(f"Error scoring breeds: {str(e)}") | |
| print(traceback.format_exc()) | |
| return [] | |
| def _get_breed_info(self, breed: str) -> Dict[str, Any]: | |
| """獲取品種資訊""" | |
| try: | |
| # 基本品種資訊 | |
| breed_info = get_dog_description(breed) or {} | |
| # 健康資訊 | |
| health_info = breed_health_info.get(breed, {}) | |
| # 噪音資訊 | |
| noise_info = breed_noise_info.get(breed, {}) | |
| # 整合資訊 | |
| return { | |
| 'breed_name': breed, | |
| 'display_name': breed.replace('_', ' '), | |
| 'size': breed_info.get('Size', '').lower(), | |
| 'exercise_needs': breed_info.get('Exercise Needs', '').lower(), | |
| 'grooming_needs': breed_info.get('Grooming Needs', '').lower(), | |
| 'temperament': breed_info.get('Temperament', '').lower(), | |
| 'good_with_children': breed_info.get('Good with Children', 'Yes'), | |
| 'care_level': breed_info.get('Care Level', '').lower(), | |
| 'lifespan': breed_info.get('Lifespan', '10-12 years'), | |
| 'noise_level': noise_info.get('noise_level', 'moderate').lower(), | |
| 'description': breed_info.get('Description', ''), | |
| 'raw_breed_info': breed_info, | |
| 'raw_health_info': health_info, | |
| 'raw_noise_info': noise_info | |
| } | |
| except Exception as e: | |
| print(f"Error getting breed info for {breed}: {str(e)}") | |
| return { | |
| 'breed_name': breed, | |
| 'display_name': breed.replace('_', ' ') | |
| } | |
| def _score_single_breed(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> BreedScore: | |
| """為單一品種評分""" | |
| try: | |
| dimensional_scores = {} | |
| semantic_total = 0.0 | |
| attribute_total = 0.0 | |
| # 動態權重分配(基於用戶表達的維度) | |
| active_dimensions = self._get_active_dimensions(dimensions) | |
| adjusted_weights = self._adjust_dimension_weights(active_dimensions) | |
| # 為每個活躍維度評分 | |
| for dimension, weight in adjusted_weights.items(): | |
| # 語義評分 | |
| semantic_score = self.semantic_head.score_dimension( | |
| breed_info, dimensions, dimension | |
| ) | |
| # 屬性評分 | |
| attribute_score = self.attribute_head.score_dimension( | |
| breed_info, dimensions, dimension | |
| ) | |
| # 頭融合 | |
| fusion_weights = self.head_fusion_weights.get( | |
| dimension, {'semantic': 0.5, 'attribute': 0.5} | |
| ) | |
| fused_score = (semantic_score * fusion_weights['semantic'] + | |
| attribute_score * fusion_weights['attribute']) | |
| dimensional_scores[dimension] = fused_score | |
| semantic_total += semantic_score * weight | |
| attribute_total += attribute_score * weight | |
| # 雙向相容性評估 | |
| bidirectional_bonus = self._calculate_bidirectional_bonus( | |
| breed_info, dimensions | |
| ) | |
| # Apply size bias correction | |
| bias_correction = self._calculate_size_bias_correction(breed_info, dimensions) | |
| # 計算最終分數 | |
| base_score = sum(score * adjusted_weights[dim] | |
| for dim, score in dimensional_scores.items()) | |
| # Apply corrections | |
| final_score = max(0.0, min(1.0, base_score + bidirectional_bonus + bias_correction)) | |
| # 信心度評估 | |
| confidence_score = self._calculate_confidence(dimensions) | |
| return BreedScore( | |
| breed_name=breed_info.get('display_name', breed_info['breed_name']), | |
| final_score=final_score, | |
| dimensional_breakdown=dimensional_scores, | |
| semantic_component=semantic_total, | |
| attribute_component=attribute_total, | |
| bidirectional_bonus=bidirectional_bonus, | |
| confidence_score=confidence_score, | |
| explanation=self._generate_explanation(breed_info, dimensions, dimensional_scores) | |
| ) | |
| except Exception as e: | |
| print(f"Error scoring breed {breed_info.get('breed_name', 'unknown')}: {str(e)}") | |
| return BreedScore( | |
| breed_name=breed_info.get('display_name', breed_info.get('breed_name', 'Unknown')), | |
| final_score=0.5, | |
| confidence_score=0.0 | |
| ) | |
| def _get_active_dimensions(self, dimensions: QueryDimensions) -> Set[str]: | |
| """獲取活躍的維度""" | |
| active = set() | |
| if dimensions.spatial_constraints: | |
| active.add('spatial_compatibility') | |
| if dimensions.activity_level: | |
| active.add('activity_compatibility') | |
| if dimensions.noise_preferences: | |
| active.add('noise_compatibility') | |
| if dimensions.size_preferences: | |
| active.add('size_compatibility') | |
| if dimensions.family_context: | |
| active.add('family_compatibility') | |
| if dimensions.maintenance_level: | |
| active.add('maintenance_compatibility') | |
| return active | |
| def _adjust_dimension_weights(self, active_dimensions: Set[str]) -> Dict[str, float]: | |
| """調整維度權重""" | |
| if not active_dimensions: | |
| return self.dimension_weights | |
| # 只為活躍維度分配權重 | |
| active_weights = {dim: weight for dim, weight in self.dimension_weights.items() | |
| if dim in active_dimensions} | |
| # 正規化權重總和為 1.0 | |
| total_weight = sum(active_weights.values()) | |
| if total_weight > 0: | |
| active_weights = {dim: weight / total_weight | |
| for dim, weight in active_weights.items()} | |
| return active_weights | |
| def _calculate_bidirectional_bonus(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """計算雙向相容性獎勵""" | |
| try: | |
| bonus = 0.0 | |
| # 正向相容性:品種滿足用戶需求 | |
| forward_compatibility = self._assess_forward_compatibility(breed_info, dimensions) | |
| # 反向相容性:用戶生活方式適合品種需求 | |
| reverse_compatibility = self._assess_reverse_compatibility(breed_info, dimensions) | |
| # 雙向獎勵(較為保守) | |
| bonus = min(0.1, (forward_compatibility + reverse_compatibility) * 0.05) | |
| return bonus | |
| except Exception as e: | |
| print(f"Error calculating bidirectional bonus: {str(e)}") | |
| return 0.0 | |
| def _assess_forward_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """評估正向相容性""" | |
| compatibility = 0.0 | |
| # 空間需求匹配 | |
| if 'apartment' in dimensions.spatial_constraints: | |
| size = breed_info.get('size', '') | |
| if 'small' in size: | |
| compatibility += 0.3 | |
| elif 'medium' in size: | |
| compatibility += 0.1 | |
| # 活動需求匹配 | |
| if 'low' in dimensions.activity_level: | |
| exercise = breed_info.get('exercise_needs', '') | |
| if 'low' in exercise: | |
| compatibility += 0.3 | |
| elif 'moderate' in exercise: | |
| compatibility += 0.1 | |
| return compatibility | |
| def _assess_reverse_compatibility(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions) -> float: | |
| """評估反向相容性""" | |
| compatibility = 0.0 | |
| # 品種是否能在用戶環境中茁壯成長 | |
| exercise_needs = breed_info.get('exercise_needs', '') | |
| if 'high' in exercise_needs: | |
| # 高運動需求品種需要確認用戶能提供足夠運動 | |
| if ('high' in dimensions.activity_level or | |
| 'house' in dimensions.spatial_constraints): | |
| compatibility += 0.2 | |
| else: | |
| compatibility -= 0.2 | |
| # 品種護理需求是否與用戶能力匹配 | |
| grooming_needs = breed_info.get('grooming_needs', '') | |
| if 'high' in grooming_needs: | |
| if 'high' in dimensions.maintenance_level: | |
| compatibility += 0.1 | |
| elif 'low' in dimensions.maintenance_level: | |
| compatibility -= 0.1 | |
| return compatibility | |
| def _calculate_size_bias_correction(self, breed_info: Dict, | |
| dimensions: QueryDimensions) -> float: | |
| """Correct systematic bias toward larger breeds""" | |
| breed_size = breed_info.get('size', '').lower() | |
| # Default no bias correction | |
| correction = 0.0 | |
| # Detect if user specified moderate/balanced preferences | |
| if any(term in dimensions.activity_level for term in ['moderate', 'balanced', 'average']): | |
| # Penalize extremes | |
| if breed_size in ['giant', 'toy']: | |
| correction = -0.1 | |
| elif breed_size in ['large']: | |
| correction = -0.05 | |
| # Boost medium breeds for moderate requirements | |
| if 'medium' in breed_size and 'balanced' in str(dimensions.activity_level): | |
| correction = 0.1 | |
| return correction | |
| def _calculate_confidence(self, dimensions: QueryDimensions) -> float: | |
| """計算推薦信心度""" | |
| # 基於維度覆蓋率和信心分數計算 | |
| dimension_count = sum([ | |
| len(dimensions.spatial_constraints), | |
| len(dimensions.activity_level), | |
| len(dimensions.noise_preferences), | |
| len(dimensions.size_preferences), | |
| len(dimensions.family_context), | |
| len(dimensions.maintenance_level), | |
| len(dimensions.special_requirements) | |
| ]) | |
| # 基礎信心度 | |
| base_confidence = min(1.0, dimension_count * 0.15) | |
| # 品種提及獎勵 | |
| breed_bonus = min(0.2, len(dimensions.breed_mentions) * 0.1) | |
| # 整體信心分數 | |
| overall_confidence = dimensions.confidence_scores.get('overall', 0.5) | |
| return min(1.0, base_confidence + breed_bonus + overall_confidence * 0.3) | |
| def _generate_explanation(self, breed_info: Dict[str, Any], | |
| dimensions: QueryDimensions, | |
| dimensional_scores: Dict[str, float]) -> Dict[str, Any]: | |
| """生成評分解釋""" | |
| try: | |
| explanation = { | |
| 'strengths': [], | |
| 'considerations': [], | |
| 'match_highlights': [], | |
| 'score_breakdown': dimensional_scores | |
| } | |
| breed_name = breed_info.get('display_name', '') | |
| # 分析各維度表現 | |
| for dimension, score in dimensional_scores.items(): | |
| if score >= 0.8: | |
| explanation['strengths'].append(self._get_strength_text(dimension, breed_info)) | |
| elif score <= 0.3: | |
| explanation['considerations'].append(self._get_consideration_text(dimension, breed_info)) | |
| else: | |
| explanation['match_highlights'].append(f"{dimension}: {score:.2f}") | |
| return explanation | |
| except Exception as e: | |
| print(f"Error generating explanation: {str(e)}") | |
| return {'strengths': [], 'considerations': [], 'match_highlights': []} | |
| def _get_strength_text(self, dimension: str, breed_info: Dict[str, Any]) -> str: | |
| """Get strength description""" | |
| breed_name = breed_info.get('display_name', '') | |
| if dimension == 'activity_compatibility': | |
| return f"{breed_name} has an activity level that matches your lifestyle very well" | |
| elif dimension == 'noise_compatibility': | |
| return f"{breed_name} has noise characteristics that fit your environment" | |
| elif dimension == 'spatial_compatibility': | |
| return f"{breed_name} is very suitable for your living space" | |
| elif dimension == 'family_compatibility': | |
| return f"{breed_name} performs well in a family environment" | |
| elif dimension == 'maintenance_compatibility': | |
| return f"{breed_name} has grooming needs that match your willingness to commit" | |
| else: | |
| return f"{breed_name} shows strong performance in {dimension}" | |
| def _get_consideration_text(self, dimension: str, breed_info: Dict[str, Any]) -> str: | |
| """Get consideration description""" | |
| breed_name = breed_info.get('display_name', '') | |
| if dimension == 'activity_compatibility': | |
| return f"{breed_name} may have exercise needs that differ from your lifestyle" | |
| elif dimension == 'noise_compatibility': | |
| return f"{breed_name} has noise characteristics that require special consideration" | |
| elif dimension == 'maintenance_compatibility': | |
| return f"{breed_name} has relatively high grooming requirements" | |
| else: | |
| return f"{breed_name} requires extra consideration in {dimension}" | |
| def score_breed_candidates(candidate_breeds: Set[str], | |
| dimensions: QueryDimensions, | |
| sbert_model: Optional[SentenceTransformer] = None) -> List[BreedScore]: | |
| """ | |
| 便利函數:為候選品種評分 | |
| Args: | |
| candidate_breeds: 候選品種集合 | |
| dimensions: 查詢維度 | |
| sbert_model: 可選的SBERT模型 | |
| Returns: | |
| List[BreedScore]: 評分結果列表 | |
| """ | |
| scorer = MultiHeadScorer(sbert_model) | |
| return scorer.score_breeds(candidate_breeds, dimensions) | |