feat : 代码梳理 移除所有敏感密钥 通过环境变量方式配置
All checks were successful
git commit AiDA python develop 分支构建部署 / scheduled_deploy (push) Has been skipped
All checks were successful
git commit AiDA python develop 分支构建部署 / scheduled_deploy (push) Has been skipped
This commit is contained in:
@@ -1,214 +0,0 @@
|
||||
"""
|
||||
推荐接口实现
|
||||
实现探索/利用分支、向量检索、Softmax抽样等功能
|
||||
"""
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import numpy as np
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from app.service.recommendation_system.config import RECOMMENDATION_CONFIG, REDIS_KEY_USER_PREF_PREFIX
|
||||
from app.service.recommendation_system.milvus_client import search_similar_vectors, query_random_candidates
|
||||
from app.service.recommendation_system.precompute import compute_user_preference_vector
|
||||
from app.service.recommendation_system.vector_utils import normalize_vector
|
||||
from app.service.utils.redis_utils import Redis
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_user_preference_vector(user_id: int, category: str) -> Optional[np.ndarray]:
|
||||
"""
|
||||
获取用户偏好向量
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
category: 类别
|
||||
|
||||
Returns:
|
||||
用户偏好向量(2048维),失败返回 None
|
||||
"""
|
||||
# 1. 从 Redis 获取
|
||||
key = f"{REDIS_KEY_USER_PREF_PREFIX}:{user_id}:{category}"
|
||||
vector_json = Redis.read(key)
|
||||
|
||||
if vector_json:
|
||||
try:
|
||||
vector_list = json.loads(vector_json)
|
||||
return np.array(vector_list, dtype=np.float32)
|
||||
except Exception as e:
|
||||
logger.warning(f"解析 Redis 向量失败 [user={user_id}, category={category}]: {e}")
|
||||
|
||||
# 2. 如果不存在,实时计算
|
||||
logger.info(f"Redis 中不存在用户偏好向量,实时计算 [user={user_id}, category={category}]")
|
||||
preference_vector = compute_user_preference_vector(user_id, category)
|
||||
|
||||
if preference_vector is not None:
|
||||
# 写入 Redis
|
||||
vector_json = json.dumps(preference_vector.tolist())
|
||||
Redis.write(
|
||||
key=key,
|
||||
value=vector_json,
|
||||
expire=RECOMMENDATION_CONFIG["redis_expire_seconds"]
|
||||
)
|
||||
|
||||
return preference_vector
|
||||
|
||||
|
||||
def explore_branch(category: str, style: Optional[str] = None) -> List[str]:
|
||||
"""
|
||||
探索分支(随机推荐)
|
||||
|
||||
Args:
|
||||
category: 类别
|
||||
style: 风格(可选)
|
||||
|
||||
Returns:
|
||||
推荐结果列表,每个元素包含 path, style, category 等字段
|
||||
"""
|
||||
# 查询候选(随机池)
|
||||
pool_size = 10 # 固定查询10个,然后随机选择
|
||||
|
||||
candidates = query_random_candidates(category, style, limit=pool_size)
|
||||
|
||||
if not candidates:
|
||||
logger.warning(f"探索分支:类别 {category} 没有候选数据")
|
||||
return []
|
||||
|
||||
# 随机选择
|
||||
if len(candidates) > 1:
|
||||
import random
|
||||
candidates = random.sample(candidates, 1)
|
||||
|
||||
# 格式化返回结果
|
||||
return [candidate.get("path", "") for candidate in candidates[:1]]
|
||||
|
||||
|
||||
def exploit_branch(
|
||||
user_id: int,
|
||||
category: str,
|
||||
style: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
利用分支(基于向量相似度推荐)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
category: 类别
|
||||
num_recommendations: 返回数量
|
||||
style: 风格(可选,用于加分)
|
||||
|
||||
Returns:
|
||||
推荐结果列表,每个元素包含 path, style, category, similarity, sample_score 等字段
|
||||
"""
|
||||
# 1. 获取用户偏好向量
|
||||
embedding = get_user_preference_vector(user_id, category)
|
||||
|
||||
if embedding is None:
|
||||
logger.warning(f"利用分支:无法获取用户偏好向量,回退到探索分支 [user={user_id}, category={category}]")
|
||||
return explore_branch(category, style)
|
||||
|
||||
# 2. Milvus 相似度检索(内积 IP)
|
||||
topk = RECOMMENDATION_CONFIG["topk"]
|
||||
results = search_similar_vectors(embedding, category, topk)
|
||||
|
||||
if not results:
|
||||
logger.warning(f"利用分支:向量检索无结果,回退到探索分支 [user={user_id}, category={category}]")
|
||||
return explore_branch(category, style)
|
||||
|
||||
# 3. Style 加分(可选,需传入 style 参数)
|
||||
style_bonus = RECOMMENDATION_CONFIG["style_bonus"]
|
||||
if style:
|
||||
for result in results:
|
||||
similarity = result["score"]
|
||||
if result.get("style") == style:
|
||||
# 加分:相似度 * (1 + style_bonus)
|
||||
similarity = similarity * (1 + style_bonus)
|
||||
result["final_score"] = similarity
|
||||
else:
|
||||
for result in results:
|
||||
result["final_score"] = result["score"]
|
||||
|
||||
# 4. Softmax 抽样
|
||||
scores = [r["final_score"] for r in results]
|
||||
probabilities = softmax_with_temperature(scores, RECOMMENDATION_CONFIG["softmax_temperature"])
|
||||
|
||||
# 根据概率抽样
|
||||
if not results:
|
||||
return []
|
||||
|
||||
selected_index = np.random.choice(len(results), size=1, p=probabilities, replace=False)
|
||||
selected_results = [results[int(selected_index[0])]]
|
||||
|
||||
# 5. 返回结果
|
||||
return [result.get("path", "") for result in selected_results]
|
||||
|
||||
|
||||
def softmax_with_temperature(scores: List[float], temperature: float = 1.0) -> List[float]:
|
||||
"""
|
||||
Softmax 函数(带温度参数)
|
||||
|
||||
Args:
|
||||
scores: 分数列表
|
||||
temperature: 温度参数
|
||||
|
||||
Returns:
|
||||
概率列表
|
||||
"""
|
||||
if not scores:
|
||||
return []
|
||||
|
||||
# 除以温度
|
||||
scaled_scores = [s / temperature for s in scores]
|
||||
|
||||
# 减去最大值(数值稳定性)
|
||||
max_score = max(scaled_scores)
|
||||
exp_scores = [math.exp(s - max_score) for s in scaled_scores]
|
||||
|
||||
# 归一化
|
||||
sum_exp = sum(exp_scores)
|
||||
if sum_exp == 0:
|
||||
# 如果所有分数都是负无穷或非常小,返回均匀分布
|
||||
return [1.0 / len(scores)] * len(scores)
|
||||
|
||||
probabilities = [exp_s / sum_exp for exp_s in exp_scores]
|
||||
return probabilities
|
||||
|
||||
|
||||
def get_recommendations(
|
||||
user_id: int,
|
||||
category: str,
|
||||
style: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
获取推荐结果(主函数)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
category: 类别(如 female_skirt)
|
||||
num_recommendations: 返回推荐数量(默认 1)
|
||||
style: 风格(可选):若传入,则在利用分支对同 style 的候选进行加分
|
||||
|
||||
Returns:
|
||||
推荐结果列表,每个元素包含 path 等字段
|
||||
"""
|
||||
try:
|
||||
# 1. 读取配置参数
|
||||
explore_ratio = RECOMMENDATION_CONFIG["explore_ratio"]
|
||||
|
||||
# 2. 探索/利用决策
|
||||
r = random.random() # 生成随机数 (0-1)
|
||||
|
||||
if r < explore_ratio:
|
||||
logger.debug(f"探索分支 [user={user_id}, category={category}]")
|
||||
return explore_branch(category, style)
|
||||
|
||||
logger.debug(f"利用分支 [user={user_id}, category={category}]")
|
||||
return exploit_branch(user_id, category, style)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取推荐结果失败 [user={user_id}, category={category}]: {e}", exc_info=True)
|
||||
# 容错:回退到探索分支
|
||||
return explore_branch(category, style)
|
||||
|
||||
Reference in New Issue
Block a user