PawMatchAI / dynamic_weight_calculator.py
DawnC's picture
Upload 19 files
1b3ab7b verified
# %%writefile dynamic_weight_calculator.py
import numpy as np
from typing import Dict, List, Tuple, Set, Optional, Any
from dataclasses import dataclass, field
import traceback
@dataclass
class WeightAllocationResult:
"""權重分配結果"""
dynamic_weights: Dict[str, float] = field(default_factory=dict)
allocation_method: str = 'balanced'
high_priority_count: int = 0
mentioned_dimensions: Set[str] = field(default_factory=set)
weight_sum: float = 1.0
allocation_notes: List[str] = field(default_factory=list)
class DynamicWeightCalculator:
"""
動態權重計算器
根據使用者優先級動態調整維度權重
策略:
- 1個高優先級 → 固定預留 40%
- 2個高優先級 → 固定預留 40% + 25%
- 3個高優先級 → 固定預留 30% + 27% + 23%
- 4+個高優先級 → 倍數正規化法
"""
def __init__(self):
"""初始化動態權重計算器"""
self.default_weights = self._initialize_default_weights()
self.dimension_name_mapping = self._initialize_dimension_mapping()
self.high_priority_threshold = 1.4 # Balanced threshold (not too high, not too low)
self.min_weight_floor = 0.05
self.contextual_weight_distribution = {
'critical_dimensions_weight': 0.50, # Moderate emphasis on critical dimensions
'mentioned_dimensions_weight': 0.35, # Good weight for mentioned dimensions
'other_dimensions_weight': 0.15 # Reasonable baseline for other dimensions
}
def _initialize_default_weights(self) -> Dict[str, float]:
"""初始化預設權重(平衡配置)"""
return {
'activity_compatibility': 0.18,
'noise_compatibility': 0.16,
'spatial_compatibility': 0.13,
'family_compatibility': 0.13,
'maintenance_compatibility': 0.13,
'experience_compatibility': 0.15, # 新增獨立的experience維度
'health_compatibility': 0.12
}
def _initialize_dimension_mapping(self) -> Dict[str, str]:
"""初始化維度名稱映射"""
return {
'noise': 'noise_compatibility',
'size': 'spatial_compatibility', # size更適合映射到spatial
'exercise': 'activity_compatibility',
'activity': 'activity_compatibility',
'grooming': 'maintenance_compatibility',
'maintenance': 'maintenance_compatibility',
'family': 'family_compatibility',
'experience': 'experience_compatibility', # 獨立映射
'health': 'health_compatibility', # 獨立映射
'spatial': 'spatial_compatibility',
'space': 'spatial_compatibility'
}
def calculate_dynamic_weights(self,
dimension_priorities: Dict[str, float],
user_mentions: Optional[Set[str]] = None,
use_contextual: bool = True) -> WeightAllocationResult:
"""
計算動態權重
Args:
dimension_priorities: 維度優先級 {dimension: priority_score}
user_mentions: 使用者明確提到的維度
use_contextual: 是否使用情境相對評分(關鍵維度80%)
Returns:
WeightAllocationResult: 權重分配結果
"""
try:
if user_mentions is None:
user_mentions = set()
# Step 1: 標準化維度名稱
normalized_priorities = self._normalize_dimension_names(dimension_priorities)
# Step 2: 分類維度
high_priority_dims = {
dim: score for dim, score in normalized_priorities.items()
if score >= self.high_priority_threshold
}
high_count = len(high_priority_dims)
# Step 3: 根據高優先級數量選擇策略
if high_count == 0:
# 無優先級 → 使用預設權重
result = self._allocate_default_weights(user_mentions)
result.allocation_method = 'default_balanced'
elif use_contextual:
# 使用情境相對評分(關鍵維度80%)
result = self._allocate_contextual_weights(
normalized_priorities, user_mentions
)
result.allocation_method = 'contextual_relative'
elif high_count == 1:
# 單一高優先級 → 固定預留40%
result = self._allocate_single_priority(
normalized_priorities, user_mentions
)
result.allocation_method = 'single_fixed'
elif high_count <= 3:
# 2-3個高優先級 → 階梯固定預留法
result = self._allocate_multiple_priorities_fixed(
normalized_priorities, user_mentions, high_count
)
result.allocation_method = f'multiple_fixed_{high_count}'
else:
# 4+個高優先級 → 倍數正規化法
result = self._allocate_multiple_priorities_proportional(
normalized_priorities, user_mentions
)
result.allocation_method = 'proportional'
# Step 4: 應用最低權重保護
result.dynamic_weights = self._apply_weight_floor(result.dynamic_weights)
# Step 5: 正規化確保總和為1.0
result.dynamic_weights = self._normalize_weights(result.dynamic_weights)
result.weight_sum = sum(result.dynamic_weights.values())
result.high_priority_count = high_count
result.mentioned_dimensions = user_mentions
return result
except Exception as e:
print(f"Error calculating dynamic weights: {str(e)}")
print(traceback.format_exc())
return WeightAllocationResult(
dynamic_weights=self.default_weights.copy(),
allocation_method='fallback'
)
def _normalize_dimension_names(self,
priorities: Dict[str, float]) -> Dict[str, float]:
"""標準化維度名稱"""
normalized = {}
for dim, score in priorities.items():
mapped_dim = self.dimension_name_mapping.get(dim, dim)
# 如果mapped_dim不在default_weights中,保留原維度名
if mapped_dim not in self.default_weights:
mapped_dim = dim
normalized[mapped_dim] = max(normalized.get(mapped_dim, 1.0), score)
return normalized
def _allocate_default_weights(self,
user_mentions: Set[str]) -> WeightAllocationResult:
"""分配預設平衡權重"""
weights = self.default_weights.copy()
notes = ["Using default balanced weights (no priorities detected)"]
return WeightAllocationResult(
dynamic_weights=weights,
allocation_notes=notes
)
def _allocate_contextual_weights(self,
priorities: Dict[str, float],
user_mentions: Set[str]) -> WeightAllocationResult:
"""
情境相對權重分配(關鍵維度50%)
"""
weights = {}
notes = []
# 標準化user_mentions維度名稱
normalized_mentions = set()
for mention in user_mentions:
normalized_name = self.dimension_name_mapping.get(mention, mention)
if normalized_name in self.default_weights:
normalized_mentions.add(normalized_name)
# 分類維度
critical_dims = [d for d, s in priorities.items() if s >= self.high_priority_threshold]
mentioned_dims = [d for d in normalized_mentions if d not in critical_dims]
other_dims = [d for d in self.default_weights.keys()
if d not in critical_dims and d not in mentioned_dims]
# 權重分配
total_critical = self.contextual_weight_distribution['critical_dimensions_weight']
total_mentioned = self.contextual_weight_distribution['mentioned_dimensions_weight']
total_other = self.contextual_weight_distribution['other_dimensions_weight']
# 關鍵維度:按優先級比例分配50%
if critical_dims:
critical_priority_sum = sum(priorities.get(d, 1.0) for d in critical_dims)
for dim in critical_dims:
weight = (priorities.get(dim, 1.0) / critical_priority_sum) * total_critical
weights[dim] = weight
notes.append(f"Critical dimensions ({len(critical_dims)}): {total_critical:.0%} weight")
# 提及維度:平均分配35%
if mentioned_dims:
for dim in mentioned_dims:
weights[dim] = total_mentioned / len(mentioned_dims)
notes.append(f"Mentioned dimensions ({len(mentioned_dims)}): {total_mentioned:.0%} weight")
# 其他維度:平均分配15%
if other_dims:
for dim in other_dims:
weights[dim] = total_other / len(other_dims)
notes.append(f"Other dimensions ({len(other_dims)}): {total_other:.0%} weight")
# 填充未覆蓋的維度
for dim in self.default_weights.keys():
if dim not in weights:
weights[dim] = 0.05
return WeightAllocationResult(
dynamic_weights=weights,
allocation_notes=notes
)
def _allocate_single_priority(self,
priorities: Dict[str, float],
user_mentions: Set[str]) -> WeightAllocationResult:
"""單一高優先級:固定預留40%"""
weights = {}
notes = []
# 找到高優先級維度
high_priority_dim = None
max_priority = 0
for dim, score in priorities.items():
if score >= self.high_priority_threshold and score > max_priority:
high_priority_dim = dim
max_priority = score
if high_priority_dim:
# 高優先級維度:40%
weights[high_priority_dim] = 0.40
notes.append(f"{high_priority_dim}: 40% (high priority)")
# 其他維度:平均分配剩餘60%
other_dims = [d for d in self.default_weights.keys() if d != high_priority_dim]
remaining_weight = 0.60
for dim in other_dims:
weights[dim] = remaining_weight / len(other_dims)
else:
weights = self.default_weights.copy()
return WeightAllocationResult(
dynamic_weights=weights,
allocation_notes=notes
)
def _allocate_multiple_priorities_fixed(self,
priorities: Dict[str, float],
user_mentions: Set[str],
high_count: int) -> WeightAllocationResult:
"""2-3個高優先級:階梯固定預留法"""
weights = {}
notes = []
# 排序高優先級維度
high_priority_dims = sorted(
[(dim, score) for dim, score in priorities.items()
if score >= self.high_priority_threshold],
key=lambda x: x[1],
reverse=True
)
# 根據數量分配固定權重
if high_count == 2:
# 2個高優先級:40% + 25%
fixed_weights = [0.40, 0.25]
remaining = 0.35
notes.append("2 high priorities: 40% + 25%, others share 35%")
elif high_count == 3:
# 3個高優先級:30% + 27% + 23%
fixed_weights = [0.30, 0.27, 0.23]
remaining = 0.20
notes.append("3 high priorities: 30% + 27% + 23%, others share 20%")
else:
# 降級處理
return self._allocate_multiple_priorities_proportional(
priorities, user_mentions
)
# 分配固定權重
for i, (dim, score) in enumerate(high_priority_dims[:high_count]):
weights[dim] = fixed_weights[i]
# 其他維度:平均分配剩餘
other_dims = [d for d in self.default_weights.keys()
if d not in [dim for dim, _ in high_priority_dims[:high_count]]]
if other_dims:
for dim in other_dims:
weights[dim] = remaining / len(other_dims)
return WeightAllocationResult(
dynamic_weights=weights,
allocation_notes=notes
)
def _allocate_multiple_priorities_proportional(self,
priorities: Dict[str, float],
user_mentions: Set[str]) -> WeightAllocationResult:
"""4+個高優先級:倍數正規化法"""
weights = {}
notes = []
# 計算原始權重(基於優先級倍數)
raw_weights = {}
for dim in self.default_weights.keys():
priority_score = priorities.get(dim, 1.0)
raw_weights[dim] = 1.0 * priority_score
# 正規化
total_raw = sum(raw_weights.values())
for dim, raw_weight in raw_weights.items():
weights[dim] = raw_weight / total_raw
notes.append(f"Proportional allocation for {len([d for d, s in priorities.items() if s >= self.high_priority_threshold])} high priorities")
return WeightAllocationResult(
dynamic_weights=weights,
allocation_notes=notes
)
def _apply_weight_floor(self, weights: Dict[str, float]) -> Dict[str, float]:
"""應用最低權重保護"""
protected_weights = {}
for dim, weight in weights.items():
protected_weights[dim] = max(self.min_weight_floor, weight)
return protected_weights
def _normalize_weights(self, weights: Dict[str, float]) -> Dict[str, float]:
"""正規化權重確保總和為1.0"""
total = sum(weights.values())
if total == 0:
return self.default_weights.copy()
normalized = {dim: weight / total for dim, weight in weights.items()}
return normalized
def get_weight_summary(self, result: WeightAllocationResult) -> Dict[str, Any]:
"""
獲取權重分配摘要
Args:
result: 權重分配結果
Returns:
Dict[str, Any]: 權重摘要
"""
return {
'allocation_method': result.allocation_method,
'high_priority_count': result.high_priority_count,
'weight_sum': result.weight_sum,
'weights': result.dynamic_weights,
'top_3_dimensions': sorted(
result.dynamic_weights.items(),
key=lambda x: x[1],
reverse=True
)[:3],
'allocation_notes': result.allocation_notes
}
def calculate_weights_from_priorities(dimension_priorities: Dict[str, float],
user_mentions: Optional[Set[str]] = None,
use_contextual: bool = True) -> WeightAllocationResult:
"""
便利函數: 從優先級計算權重
Args:
dimension_priorities: 維度優先級
user_mentions: 使用者提及的維度
use_contextual: 使用情境相對評分
Returns:
WeightAllocationResult: 權重分配結果
"""
calculator = DynamicWeightCalculator()
return calculator.calculate_dynamic_weights(
dimension_priorities, user_mentions, use_contextual
)
def get_weight_summary(dimension_priorities: Dict[str, float],
user_mentions: Optional[Set[str]] = None) -> Dict[str, Any]:
"""
便利函數: 獲取權重摘要
Args:
dimension_priorities: 維度優先級
user_mentions: 使用者提及的維度
Returns:
Dict[str, Any]: 權重摘要
"""
calculator = DynamicWeightCalculator()
result = calculator.calculate_dynamic_weights(dimension_priorities, user_mentions)
return calculator.get_weight_summary(result)