哈喽,各位机器学习爱好者!
先问你一个扎心的问题:你有没有过这样的经历?打开购物APP,首页全是同类型的商品,看3秒就觉得腻;刷短视频,前两条还精准戳中兴趣,后面突然刷到完全不相关的内容,瞬间没了继续刷的欲望;甚至看资讯APP,推荐的文章要么全是"标题党",要么深度不够,越看越觉得"算法不懂我"。
其实,这些问题的核心,就是入门和进阶推荐系统没解决的"高阶难题":如何在精准匹配用户兴趣的同时,兼顾多样性?如何让推荐系统同时满足多个业务目标(比如既想让用户点击,又想让用户下单)?如何让系统实时跟上用户兴趣的变化?
今天这篇,咱们就用"接地气的比喻+具体的实现逻辑+真实的业务价值",把这三个高阶技术扒得明明白白。全程无晦涩公式堆砌,只有让你拍大腿的"原来如此",看到最后还有下一篇高阶内容的悬念预告,准备好开启修炼了吗?
一、混排:不止于"排",更是"精准搭配"的艺术
先从最贴近我们感知的"混排"说起。入门阶段的推荐系统,大多是"单品类排序"------比如给你推荐的全是衣服,或者全是零食。但真实的产品场景里,用户的需求是"多元且动态"的:比如你在购物APP上搜"运动跑鞋",可能同时也想看看运动袜、运动背包;刷视频时,既想看搞笑段子放松,也想偶尔看个知识科普充电。
混排,就是解决这个问题的"神器"。它的核心不是简单把不同品类的内容混在一起,而是像餐厅的"配菜师"一样,根据你的"口味(用户兴趣)""用餐场景(当前行为)""餐厅目标(业务指标)",把不同类型的"菜品(推荐内容)"搭配成一套让你满意、同时让餐厅盈利的"套餐"。
1. 为什么需要混排?------ 破解"精准陷阱"
很多人觉得,推荐系统越精准越好,但过度精准会陷入"精准陷阱":
-
用户兴趣固化:一直给用户推同类型内容,用户会觉得枯燥,慢慢流失(比如你喜欢看科幻电影,天天推科幻片,一周后可能就想换个口味);
-
业务目标单一:只追求点击率,可能导致用户"光点不买",或者只看免费内容,不产生付费转化;
-
内容覆盖不足:新上线的内容、小众品类的内容,很难被用户发现,形成"马太效应"(热门内容越热门,冷门内容越冷门)。
而混排,就是通过"品类多样化""内容层级化"的搭配,既保留精准推荐的核心优势,又解决这些痛点。比如购物APP的首页,会同时给你推"你最近浏览的跑鞋""搭配跑鞋的袜子""热门运动装备榜单",既让你觉得"算法懂我",又让你有新的发现欲。
2. 混排的核心逻辑:如何平衡"多样性"与"精准度"?
混排的本质,是"多目标下的资源分配问题"------把有限的推荐位,分给不同类型、不同价值的内容,实现"用户体验"和"业务目标"的双赢。这里有两个核心步骤,用"配菜"的比喻就能看懂:
-
第一步:确定"菜品池"------ 筛选候选内容。先通过基础推荐模型(比如深度学习模型),筛选出用户可能感兴趣的不同品类内容,形成一个"候选池"(比如候选池里有跑鞋、袜子、背包、运动手环四类内容);
-
第二步:制定"搭配规则"------ 动态分配推荐位。这是混排的核心,常见的规则有三种:
-
规则式混排:比如固定比例"3:2:1",3个核心品类(跑鞋)、2个搭配品类(袜子、背包)、1个探索品类(运动手环),适合业务初期,简单易落地;
-
模型式混排:用专门的混排模型(比如多目标排序模型),预测每类内容在"点击率、转化率、用户停留时长"等多个指标上的得分,然后动态分配推荐位,适合业务成熟后,追求更精准的平衡;
-
强化学习混排:让系统通过"试错"学习最优搭配策略(比如这次推2个探索品类用户停留变久,下次就适当增加比例),适合用户兴趣变化快的场景(比如短视频、资讯)。
-
这里要注意一个关键:混排不是"平均分配",而是"动态调整"。比如用户刚搜完"跑鞋",就多给核心品类推荐位;用户连续刷了5条同类型内容,就立刻增加探索品类的比例------这就是混排的"智慧"。
3. 混排的真实价值:从"用户停留"到"业务增长"
很多产品做混排后,都看到了明显的效果:比如某购物APP做混排后,用户首页停留时长提升了25%,跨品类购买率提升了30%;某短视频APP通过混排,用户日均刷视频次数提升了18%,留存率提升了12%。
核心原因就是:混排满足了用户的"潜在需求"和"探索欲",让用户觉得"这个APP总能给我惊喜",从而愿意花更多时间,产生更多消费------这就是混排从"用户体验"到"业务增长"的转化逻辑。
二、多任务学习:让推荐系统"举一反三"的修炼秘诀
聊完混排,咱们再看第二个高阶技术------多任务学习。先问你一个问题:如果让推荐系统同时完成两个目标,比如"预测用户是否点击"和"预测用户是否下单",你会怎么做?
入门阶段的做法是:分别训练两个独立的模型,一个预测点击率(CTR),一个预测转化率(CVR),然后再把两个模型的结果结合起来。但这种做法有个很大的问题:两个模型是"各自为战"的,没有利用到"点击"和"下单"之间的关联信息(比如喜欢点击运动装备的用户,下单运动背包的概率也更高),而且训练成本高、推理速度慢。
而多任务学习,就是让推荐系统像"学霸"一样,能够"举一反三"------通过一次训练,同时掌握多个相关任务的能力,既提升模型效果,又降低成本。
1. 多任务学习的核心:共享"知识",差异化"任务"
多任务学习的本质,是"共享特征提取层,差异化任务输出层"。用"老师教学生"的比喻来理解:
比如老师要教学生"语文阅读"和"语文写作"两个任务。这两个任务都需要"理解文字含义""把握文章逻辑"这些基础能力------这就是"共享知识"。老师不会分别教两次基础能力,而是先教一次基础能力,再针对"阅读"和"写作"的不同要求,教差异化的技巧(比如阅读要学会找重点,写作要学会组织结构)。
推荐系统的多任务学习也是一样:
-
共享层:用深度学习模型(比如DNN)提取用户和内容的通用特征(比如用户的年龄、兴趣标签,内容的品类、关键词),这些特征是多个任务都需要的;
-
任务层:针对不同的业务目标(CTR预测、CVR预测、停留时长预测),分别设计独立的输出层,用不同的损失函数训练;
-
优势:共享层的特征可以相互促进(比如CVR任务的特征能提升CTR任务的预测精度),而且只需要训练一个模型,降低了训练和推理成本。
2. 常见的多任务学习架构:从"简单共享"到"动态调整"
多任务学习的架构有很多,从易到难主要有三种,适合不同的业务场景:
-
Hard Parameter Sharing(硬共享):最基础的架构,所有任务共享同一个特征提取层,只保留任务专属的输出层。优点是简单易实现,适合任务相关性很高的场景(比如CTR和CVR预测);缺点是如果任务相关性低(比如预测点击和预测用户性别),会出现"任务干扰"(一个任务的训练会影响另一个任务的效果);
-
Soft Parameter Sharing(软共享):每个任务有自己的特征提取层,但让不同任务的特征提取层参数"相似"(比如通过正则化约束)。优点是适合任务相关性较低的场景,避免了"任务干扰";缺点是训练复杂度比硬共享高;
-
MMoE(Mixture of Experts):最灵活的架构,把特征提取层分成多个"专家网络",每个专家网络负责处理不同类型的特征,然后用一个"门控网络"根据用户和内容的特点,动态选择哪些专家网络参与不同任务的训练。优点是能自动适配不同任务的相关性,避免"任务干扰",效果最好;缺点是实现复杂,适合业务场景复杂、任务较多的大型推荐系统(比如淘宝、抖音)。
3. 多任务学习的落地价值:一次训练,多效提升
多任务学习在实际业务中应用非常广泛,比如某电商平台用多任务学习同时优化CTR和CVR,最终CTR提升了15%,CVR提升了20%;某资讯APP用多任务学习同时优化"点击""停留时长""分享"三个任务,用户日均使用时长提升了22%。
核心原因就是:多任务学习充分利用了任务之间的关联信息,让模型更全面地理解用户需求,同时降低了系统的训练和运维成本------这就是"举一反三"的力量。
三、在线学习系统:让推荐系统"实时进化"的核心引擎
最后,咱们聊第三个高阶技术------在线学习系统。不知道你有没有发现,很多推荐系统存在"滞后性":比如你今天突然对"露营装备"感兴趣,刷了很多相关内容,但推荐系统要等到第二天甚至第三天,才会给你推大量露营装备。这种"滞后性"很容易让用户失去兴趣,因为用户的兴趣可能是"即时且短暂"的(比如周末想露营,周内兴趣就消失了)。
而在线学习系统,就是让推荐系统具备"实时进化"的能力------能够实时捕捉用户的最新行为,实时更新模型参数,让推荐结果紧跟用户兴趣的变化。
1. 在线学习vs离线学习:从"定期更新"到"实时迭代"
要理解在线学习,先对比一下我们之前聊的"离线学习":
| 对比维度 | 离线学习 | 在线学习 |
|---|---|---|
| 数据使用 | 使用历史数据(比如过去一周的数据)批量训练 | 使用用户实时行为数据(比如用户刚完成的点击、浏览)增量训练 |
| 模型更新频率 | 定期更新(比如每天一次、每周一次) | 实时更新(比如每秒钟更新一次、每处理一个用户行为就更新一次) |
| 响应速度 | 慢,存在滞后性 | 快,能实时跟上用户兴趣变化 |
| 适用场景 | 用户兴趣稳定的场景(比如图书推荐) | 用户兴趣变化快的场景(比如短视频、资讯、直播) |
简单来说,离线学习是"批量生产"模型,在线学习是"个性化实时定制"模型------后者更适合当下快节奏、个性化的产品场景。
2. 在线学习的核心挑战:如何在"实时"和"稳定"之间平衡?
在线学习听起来很完美,但落地时面临一个核心挑战:实时性和稳定性的平衡。如果模型每收到一个用户行为就更新一次,很容易被"噪声数据"(比如用户误点击)影响,导致模型参数波动过大,推荐结果不稳定(比如刚给用户推了露营装备,下一秒因为误点击了一次美食视频,就立刻全推美食)。
为了解决这个问题,在线学习系统有三个核心设计:
-
增量训练算法:不用重新训练整个模型,只需要用新的实时数据对模型参数进行"微调"。常见的算法有SGD(随机梯度下降)、FTRL(Follow The Regularized Leader)等,这些算法能快速更新参数,同时通过正则化约束参数波动;
-
数据过滤和采样:对实时用户行为数据进行预处理,过滤掉明显的噪声数据(比如点击时长小于0.5秒的误点击),同时对高频行为(比如同一用户反复点击同一内容)进行采样,避免数据分布失衡;
-
模型监控和回滚:实时监控模型的关键指标(比如点击率、用户停留时长),如果指标突然下降,说明模型可能被噪声数据影响,立刻触发模型回滚,恢复到之前的稳定版本。
3. 在线学习的经典架构:Flink + 在线模型服务
实际业务中,在线学习系统的经典架构是"实时数据处理框架 + 在线模型服务":
-
实时数据处理:用Flink、Spark Streaming等框架,实时采集用户的行为数据(点击、浏览、下单、收藏等),进行数据清洗、特征提取;
-
增量训练:将处理后的实时特征和标签,输入到在线模型中(比如FTRL模型、在线DNN模型),进行增量训练,更新模型参数;
-
模型服务:将更新后的模型参数实时部署到推荐服务中,用户每次请求推荐时,都用最新的模型进行预测,返回最贴合用户当前兴趣的推荐结果。
比如某短视频APP的在线学习系统,能在用户完成一次点击后,1秒内更新模型参数,下一次给用户推荐的内容就会立刻贴合最新兴趣------这就是为什么你刷短视频时,会觉得"越刷越懂我"。
四、项目实战:推荐系统高阶功能演示
python
"""
推荐系统高阶技术可视化平台 - 单文件版本
包含所有HTML、CSS和JS代码
"""
from flask import Flask, render_template_string, jsonify, request
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
import random
from collections import defaultdict
import json
from typing import Dict, List, Tuple, Any
app = Flask(__name__)
# ==================== 推荐系统核心类 ====================
class UserProfile:
"""用户画像类"""
def __init__(self, user_id: int, name: str = None):
self.user_id = user_id
self.name = name or f"用户{user_id}"
self.base_interests = self._generate_base_interests()
self.real_time_interests = self.base_interests.copy()
self.history_behavior = []
self.recommendation_history = []
self.created_at = datetime.now()
def _generate_base_interests(self) -> Dict[str, float]:
"""生成用户基础兴趣"""
categories = ['科技', '娱乐', '体育', '财经', '时尚', '教育', '健康', '美食']
interests = {}
# 每个用户随机选择3-5个兴趣类别
selected_categories = random.sample(categories, random.randint(3, 5))
for category in selected_categories:
interests[category] = random.uniform(0.4, 0.95)
return interests
def update_realtime_interests(self, action: Dict[str, Any]):
"""更新实时兴趣"""
category = action['category']
action_type = action['type']
if action_type == 'click':
weight = 0.1
elif action_type == 'purchase':
weight = 0.3
elif action_type == 'watch_long':
weight = 0.2
else:
weight = 0.05
if category in self.real_time_interests:
self.real_time_interests[category] = min(1.0, self.real_time_interests[category] + weight)
else:
self.real_time_interests[category] = weight
# 兴趣衰减(非交互类别)
for cat in list(self.real_time_interests.keys()):
if cat != category:
self.real_time_interests[cat] = max(0.1, self.real_time_interests[cat] - 0.02)
self.history_behavior.append({
'timestamp': datetime.now().strftime("%H:%M:%S"),
'action': action,
'category': category
})
# 只保留最近50条行为
if len(self.history_behavior) > 50:
self.history_behavior = self.history_behavior[-50:]
def get_interest_score(self, category: str) -> float:
"""获取用户对某类别的兴趣分数"""
return self.real_time_interests.get(category, 0.0)
def to_dict(self):
"""转换为字典格式"""
return {
'user_id': self.user_id,
'name': self.name,
'base_interests': self.base_interests,
'real_time_interests': self.real_time_interests,
'history_count': len(self.history_behavior),
'recommendation_count': len(self.recommendation_history)
}
class ContentPool:
"""内容池"""
def __init__(self):
self.contents = self._generate_contents()
self.categories = ['科技', '娱乐', '体育', '财经', '时尚', '教育', '健康', '美食']
def _generate_contents(self) -> List[Dict]:
"""生成模拟内容"""
categories = ['科技', '娱乐', '体育', '财经', '时尚', '教育', '健康', '美食']
contents = []
content_id = 0
for category in categories:
# 每个类别生成12-15个内容
for i in range(random.randint(12, 15)):
contents.append({
'id': content_id,
'title': f"{category}内容_{i}",
'category': category,
'ctr': random.uniform(0.01, 0.2), # 预估点击率
'cvr': random.uniform(0.001, 0.05), # 预估转化率
'quality_score': random.uniform(0.5, 1.0),
'freshness': random.randint(1, 30), # 内容新鲜度(天数)
'description': self._generate_description(category, i),
'image_color': self._generate_color()
})
content_id += 1
return contents
def _generate_description(self, category: str, index: int) -> str:
"""生成内容描述"""
descriptions = {
'科技': ['最新科技动态', '前沿技术分析', '数码产品评测', 'AI发展趋势'],
'娱乐': ['明星八卦新闻', '电影电视剧推荐', '综艺节目看点', '娱乐热点追踪'],
'体育': ['体育赛事报道', '运动员专访', '比赛分析预测', '健身锻炼技巧'],
'财经': ['股市行情分析', '投资理财建议', '经济政策解读', '商业趋势洞察'],
'时尚': ['潮流穿搭指南', '美妆护肤技巧', '时尚品牌推荐', '设计风格解析'],
'教育': ['学习方法分享', '考试备考策略', '教育资源推荐', '教育政策解读'],
'健康': ['健康养生知识', '疾病预防指南', '运动健身建议', '心理健康贴士'],
'美食': ['美食制作教程', '餐厅探店推荐', '营养搭配建议', '饮食文化分享']
}
return f"{random.choice(descriptions[category])} - {category}领域优质内容"
def _generate_color(self) -> str:
"""生成随机颜色用于卡片展示"""
colors = [
'#4CAF50', '#2196F3', '#FF9800', '#9C27B0',
'#00BCD4', '#FF5722', '#673AB7', '#3F51B5'
]
return random.choice(colors)
def get_contents_by_category(self, category: str, limit: int = 20) -> List[Dict]:
"""按类别获取内容"""
filtered = [c for c in self.contents if c['category'] == category]
return sorted(filtered, key=lambda x: x['ctr'], reverse=True)[:limit]
def get_content_by_id(self, content_id: int) -> Dict:
"""根据ID获取内容"""
for content in self.contents:
if content['id'] == content_id:
return content
return None
class MixedRanking:
"""混排算法"""
def __init__(self):
pass
def rule_based_mix(self, user_profile: UserProfile, contents_by_category: Dict[str, List[Dict]]) -> List[Dict]:
"""基于规则的混排"""
# 规则: 70%用户兴趣内容 + 20%相关品类 + 10%探索内容
result = []
# 1. 主要兴趣内容 (70%)
main_interests = sorted(user_profile.real_time_interests.items(),
key=lambda x: x[1], reverse=True)[:2]
for category, score in main_interests:
if category in contents_by_category:
result.extend(contents_by_category[category][:3])
# 2. 相关品类 (20%)
all_categories = list(contents_by_category.keys())
related_categories = [c for c in all_categories if c not in dict(main_interests)]
if related_categories:
category = random.choice(related_categories)
result.extend(contents_by_category[category][:2])
# 3. 探索内容 (10%)
if len(result) < 8: # 保证至少8条内容
explore_category = random.choice(all_categories)
explore_contents = [c for c in contents_by_category.get(explore_category, [])
if c not in result]
result.extend(explore_contents[:1])
# 打乱顺序增加多样性
random.shuffle(result)
return result[:10] # 返回前10条
def model_based_mix(self, user_profile: UserProfile, contents_by_category: Dict[str, List[Dict]]) -> List[Dict]:
"""基于模型的混排"""
all_contents = []
for category, contents in contents_by_category.items():
for content in contents:
# 计算综合得分 (多目标)
interest_score = user_profile.get_interest_score(category)
diversity_penalty = self._calculate_diversity_penalty(content, all_contents)
# 多目标加权得分
score = (
content['ctr'] * 0.4 + # 点击率权重
content['cvr'] * 0.3 + # 转化率权重
interest_score * 0.2 + # 用户兴趣权重
(1 / content['freshness']) * 0.05 + # 新鲜度权重
diversity_penalty * 0.05 # 多样性权重
)
all_contents.append({
'content': content,
'score': score,
'category': category
})
# 按得分排序并确保类别多样性
sorted_contents = sorted(all_contents, key=lambda x: x['score'], reverse=True)
# 类别多样性控制
final_result = []
category_count = defaultdict(int)
for item in sorted_contents:
if len(final_result) >= 10:
break
category = item['category']
if category_count[category] < 3: # 每个类别最多3条
final_result.append(item['content'])
category_count[category] += 1
return final_result
def _calculate_diversity_penalty(self, content: Dict, selected_contents: List) -> float:
"""计算多样性惩罚"""
if not selected_contents:
return 1.0
same_category_count = sum(1 for item in selected_contents
if item['content']['category'] == content['category'])
# 相同类别越多,惩罚越大
return max(0, 1.0 - (same_category_count * 0.2))
class MultiTaskModel:
"""多任务学习模型"""
def __init__(self):
self.shared_layer_weights = np.random.randn(10, 8) # 共享层权重
self.ctr_task_weights = np.random.randn(8, 1) # CTR任务权重
self.cvr_task_weights = np.random.randn(8, 1) # CVR任务权重
self.update_count = 0
def predict(self, user_features: np.ndarray, content_features: np.ndarray) -> Tuple[float, float]:
"""预测CTR和CVR"""
# 特征拼接
combined_features = np.concatenate([user_features, content_features])
# 共享层
shared_output = np.tanh(np.dot(combined_features, self.shared_layer_weights))
# 任务特定层
ctr_prediction = sigmoid(np.dot(shared_output, self.ctr_task_weights))
cvr_prediction = sigmoid(np.dot(shared_output, self.cvr_task_weights))
return float(ctr_prediction), float(cvr_prediction)
def update_with_feedback(self, user_features: np.ndarray,
content_features: np.ndarray,
ctr_label: float,
cvr_label: float,
learning_rate: float = 0.01):
"""根据反馈更新模型(简化版)"""
self.update_count += 1
# 在实际中,这里应该实现梯度下降更新
# 这里简化为随机调整
if random.random() > 0.7: # 30%的概率调整
adjustment = np.random.randn(*self.shared_layer_weights.shape) * 0.1
self.shared_layer_weights += adjustment
def get_model_info(self):
"""获取模型信息"""
return {
'update_count': self.update_count,
'shared_layer_shape': self.shared_layer_weights.shape,
'ctr_layer_shape': self.ctr_task_weights.shape,
'cvr_layer_shape': self.cvr_task_weights.shape
}
class OnlineLearningSystem:
"""在线学习系统"""
def __init__(self):
self.user_models = {} # 用户ID -> 模型
self.global_model = MultiTaskModel()
self.recent_feedback = []
self.update_interval = 5 # 5秒更新一次
self.last_update = datetime.now()
self.total_updates = 0
self.is_running = True
def process_feedback(self, user_id: int, action: Dict[str, Any]):
"""处理用户反馈"""
feedback = {
'user_id': user_id,
'action': action,
'timestamp': datetime.now().strftime("%H:%M:%S")
}
self.recent_feedback.append(feedback)
# 检查是否需要更新模型
self._check_model_update()
def _check_model_update(self):
"""检查并执行模型更新"""
current_time = datetime.now()
if (current_time - self.last_update).seconds >= self.update_interval:
if self.recent_feedback:
self._update_models()
self.recent_feedback = []
self.last_update = current_time
def _update_models(self):
"""更新模型(简化实现)"""
self.total_updates += 1
for feedback in self.recent_feedback:
# 生成模拟特征
user_features = np.random.randn(5)
content_features = np.random.randn(5)
# 根据action类型生成标签
action_type = feedback['action']['type']
ctr_label = 1.0 if action_type in ['click', 'watch_long'] else 0.0
cvr_label = 1.0 if action_type == 'purchase' else 0.0
# 更新全局模型
self.global_model.update_with_feedback(
user_features, content_features, ctr_label, cvr_label
)
def get_system_info(self):
"""获取系统信息"""
return {
'total_updates': self.total_updates,
'recent_feedback_count': len(self.recent_feedback),
'last_update': self.last_update.strftime("%H:%M:%S"),
'update_interval': self.update_interval,
'user_model_count': len(self.user_models)
}
class RecommendationSystem:
"""推荐系统主类"""
def __init__(self):
self.users = {}
self.content_pool = ContentPool()
self.mixed_ranking = MixedRanking()
self.online_learning = OnlineLearningSystem()
self.multi_task_model = MultiTaskModel()
self.system_metrics = {
'total_recommendations': 0,
'total_interactions': 0,
'average_diversity': 0.0,
'performance_history': []
}
def get_or_create_user(self, user_id: int, name: str = None) -> UserProfile:
"""获取或创建用户"""
if user_id not in self.users:
self.users[user_id] = UserProfile(user_id, name)
self.online_learning.user_models[user_id] = self.users[user_id]
return self.users[user_id]
def generate_recommendations(self, user_id: int, strategy: str = 'model_based') -> List[Dict]:
"""生成推荐列表"""
user = self.get_or_create_user(user_id)
# 1. 获取候选内容
categories = list(user.real_time_interests.keys())
if len(categories) < 3:
categories.extend(['科技', '娱乐', '体育']) # 默认类别
contents_by_category = {}
for category in categories:
contents = self.content_pool.get_contents_by_category(category, 10)
if contents:
contents_by_category[category] = contents
# 2. 应用混排策略
if strategy == 'rule_based':
recommendations = self.mixed_ranking.rule_based_mix(user, contents_by_category)
else:
recommendations = self.mixed_ranking.model_based_mix(user, contents_by_category)
# 3. 使用多任务模型重新排序(可选)
reranked_recommendations = self._rerank_with_multi_task(user, recommendations)
# 记录推荐历史
for content in reranked_recommendations:
user.recommendation_history.append({
'timestamp': datetime.now().strftime("%H:%M:%S"),
'content_id': content['id'],
'title': content['title'],
'category': content['category']
})
# 更新系统指标
self.system_metrics['total_recommendations'] += 1
return reranked_recommendations
def _rerank_with_multi_task(self, user: UserProfile, recommendations: List[Dict]) -> List[Dict]:
"""使用多任务模型重新排序"""
reranked = []
for content in recommendations:
# 生成特征(简化)
user_features = np.array([user.get_interest_score(content['category'])] * 5)
content_features = np.array([
content['ctr'],
content['cvr'],
content['quality_score'],
1 / content['freshness'],
random.random()
])
# 预测
ctr_pred, cvr_pred = self.multi_task_model.predict(user_features, content_features)
# 综合得分
final_score = ctr_pred * 0.7 + cvr_pred * 0.3
reranked.append((content, final_score, ctr_pred, cvr_pred))
# 按综合得分排序
reranked.sort(key=lambda x: x[1], reverse=True)
# 添加预测分数到内容
result = []
for content, final_score, ctr_pred, cvr_pred in reranked:
content_with_score = content.copy()
content_with_score['final_score'] = round(final_score, 3)
content_with_score['ctr_pred'] = round(ctr_pred, 3)
content_with_score['cvr_pred'] = round(cvr_pred, 3)
result.append(content_with_score)
return result[:10]
def process_user_action(self, user_id: int, content_id: int, action_type: str):
"""处理用户行为"""
user = self.get_or_create_user(user_id)
content = self.content_pool.get_content_by_id(content_id)
if not content:
return False
action = {
'content_id': content_id,
'title': content['title'],
'category': content['category'],
'type': action_type
}
# 更新用户兴趣
user.update_realtime_interests(action)
# 在线学习处理反馈
self.online_learning.process_feedback(user_id, action)
# 更新系统指标
self.system_metrics['total_interactions'] += 1
return True
def get_system_stats(self):
"""获取系统统计信息"""
# 计算平均多样性
diversity_scores = []
for user_id, user in self.users.items():
if user.recommendation_history:
categories = [rec['category'] for rec in user.recommendation_history[-20:]]
if categories:
category_counts = {}
for category in categories:
category_counts[category] = category_counts.get(category, 0) + 1
total = len(categories)
hhi = sum((count/total)**2 for count in category_counts.values())
diversity_scores.append(1 - hhi)
avg_diversity = sum(diversity_scores) / len(diversity_scores) if diversity_scores else 0
return {
'total_users': len(self.users),
'total_recommendations': self.system_metrics['total_recommendations'],
'total_interactions': self.system_metrics['total_interactions'],
'average_diversity': round(avg_diversity, 3),
'online_learning_updates': self.online_learning.total_updates
}
def get_user_info(self, user_id: int):
"""获取用户详细信息"""
if user_id not in self.users:
return None
user = self.users[user_id]
return {
'user_info': user.to_dict(),
'recent_behavior': user.history_behavior[-10:],
'recent_recommendations': user.recommendation_history[-10:]
}
def sigmoid(x):
"""Sigmoid函数"""
return 1 / (1 + np.exp(-x))
# ==================== Flask应用初始化 ====================
recommendation_system = RecommendationSystem()
# 创建一些初始用户
for i in range(1, 4):
names = ['张三', '李四', '王五', '赵六', '钱七']
recommendation_system.get_or_create_user(i, names[i-1])
# ==================== HTML模板 ====================
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>推荐系统高阶技术可视化平台</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
text-align: center;
}
.header h1 {
font-size: 2rem;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
font-weight: 300;
}
/* 主内容布局 */
.main-content {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
padding: 20px;
min-height: 800px;
}
@media (min-width: 1200px) {
.main-content {
grid-template-columns: 300px 1fr 300px;
}
}
/* 卡片通用样式 */
.card {
background: white;
border-radius: 15px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.card h2 {
color: #4a5568;
font-size: 1.2rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 8px;
}
/* 用户管理样式 */
.user-list {
margin-bottom: 15px;
}
.user-item {
padding: 10px 12px;
background: #f7fafc;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.user-item:hover {
background: #edf2f7;
transform: translateX(3px);
}
.user-item.active {
border-color: #4299e1;
background: #ebf8ff;
}
.user-name {
font-weight: 600;
color: #2d3748;
margin-bottom: 3px;
}
.user-meta {
font-size: 0.8rem;
color: #718096;
display: flex;
justify-content: space-between;
}
/* 按钮样式 */
.btn {
padding: 10px 15px;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
justify-content: center;
width: 100%;
margin-bottom: 10px;
}
.btn-primary {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
}
.btn-primary:hover {
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(66, 153, 225, 0.4);
}
/* 推荐内容样式 */
.recommendations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
margin-top: 15px;
}
.recommendation-item {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border: 1px solid #e2e8f0;
}
.recommendation-item:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.content-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 12px;
color: white;
font-weight: 600;
font-size: 1rem;
}
.content-body {
padding: 15px;
}
.content-title {
font-size: 1rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 8px;
line-height: 1.3;
}
.content-description {
font-size: 0.85rem;
color: #718096;
margin-bottom: 12px;
line-height: 1.4;
}
.content-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #4a5568;
margin-bottom: 12px;
}
.content-category {
background: #edf2f7;
padding: 2px 8px;
border-radius: 12px;
font-weight: 600;
}
.content-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
text-align: center;
}
.stat-item {
background: #f7fafc;
padding: 6px;
border-radius: 6px;
}
.stat-label {
font-size: 0.7rem;
color: #718096;
margin-bottom: 2px;
}
.stat-value {
font-weight: 700;
color: #2d3748;
font-size: 0.9rem;
}
.content-actions {
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
padding: 6px 8px;
border: none;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
}
.action-click {
background: #4299e1;
color: white;
}
.action-click:hover {
background: #3182ce;
}
.action-purchase {
background: #48bb78;
color: white;
}
.action-purchase:hover {
background: #38a169;
}
.action-watch {
background: #ed8936;
color: white;
}
.action-watch:hover {
background: #dd6b20;
}
/* 图表容器 */
.chart-container {
height: 200px;
margin-bottom: 15px;
position: relative;
}
/* 技术标签页 */
.tech-tabs {
display: flex;
gap: 8px;
margin-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 8px;
}
.tech-tab {
padding: 6px 12px;
background: #edf2f7;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
color: #4a5568;
transition: all 0.3s ease;
font-size: 0.85rem;
}
.tech-tab:hover {
background: #e2e8f0;
}
.tech-tab.active {
background: #4299e1;
color: white;
}
/* 统计网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-box {
background: #f7fafc;
padding: 12px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #4299e1;
margin-bottom: 3px;
}
.stat-label {
font-size: 0.85rem;
color: #718096;
}
/* 行为列表 */
.behavior-list {
max-height: 250px;
overflow-y: auto;
}
.behavior-item {
padding: 10px;
background: #f7fafc;
border-radius: 6px;
margin-bottom: 8px;
border-left: 4px solid #4299e1;
font-size: 0.85rem;
}
.behavior-time {
font-size: 0.75rem;
color: #718096;
margin-bottom: 3px;
}
.behavior-content {
font-weight: 600;
color: #2d3748;
margin-bottom: 3px;
}
.behavior-type {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.7rem;
font-weight: 600;
margin-right: 5px;
}
.type-click { background: #bee3f8; color: #2c5282; }
.type-purchase { background: #c6f6d5; color: #276749; }
.type-watch { background: #fed7d7; color: #9b2c2c; }
.type-skip { background: #e2e8f0; color: #4a5568; }
/* 策略选择器 */
.strategy-selector {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.strategy-btn {
padding: 6px 12px;
background: #edf2f7;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
color: #4a5568;
transition: all 0.3s ease;
font-size: 0.85rem;
}
.strategy-btn:hover {
background: #e2e8f0;
}
.strategy-btn.active {
background: #4299e1;
color: white;
}
.diversity-score {
background: #48bb78;
color: white;
padding: 4px 10px;
border-radius: 12px;
font-weight: 600;
font-size: 0.85rem;
}
/* 兴趣列表 */
.interest-list {
margin-top: 15px;
}
.interest-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: #f7fafc;
border-radius: 6px;
margin-bottom: 6px;
font-size: 0.85rem;
}
.interest-category {
font-weight: 600;
color: #2d3748;
}
.interest-values {
display: flex;
gap: 12px;
}
.interest-value {
text-align: center;
}
.interest-label {
font-size: 0.7rem;
color: #718096;
}
/* 表单样式 */
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
margin-bottom: 3px;
color: #4a5568;
font-weight: 600;
font-size: 0.9rem;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 2px solid #e2e8f0;
border-radius: 6px;
font-size: 0.9rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4299e1;
}
/* 底部样式 */
.footer {
background: #2d3748;
color: white;
padding: 15px 30px;
text-align: center;
border-top: 1px solid #4a5568;
font-size: 0.9rem;
}
.footer p {
margin-bottom: 3px;
}
.update-time {
font-size: 0.8rem;
opacity: 0.7;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
border-radius: 10px;
padding: 10px;
}
.header {
padding: 15px;
}
.header h1 {
font-size: 1.5rem;
}
.main-content {
padding: 10px;
}
.recommendations-grid {
grid-template-columns: 1fr;
}
.strategy-selector {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 头部 -->
<header class="header">
<h1><i class="fas fa-robot"></i> 推荐系统高阶技术可视化平台</h1>
<p class="subtitle">混排 · 多任务学习 · 在线学习</p>
</header>
<div class="main-content">
<!-- 左侧面板 -->
<div class="left-panel">
<!-- 用户管理 -->
<div class="card user-management">
<h2><i class="fas fa-users"></i> 用户管理</h2>
<div class="user-list" id="userList">
<!-- 用户列表将通过JS动态加载 -->
</div>
<div class="user-actions">
<button class="btn btn-primary" onclick="simulateActions()">
<i class="fas fa-play"></i> 模拟用户行为
</button>
<div class="form-group">
<label>模拟次数:</label>
<input type="number" id="actionCount" value="3" min="1" max="10">
</div>
</div>
</div>
<!-- 系统统计 -->
<div class="card system-stats">
<h2><i class="fas fa-chart-bar"></i> 系统统计</h2>
<div class="stats-grid" id="systemStats">
<!-- 统计信息将通过JS动态加载 -->
</div>
</div>
<!-- 技术说明 -->
<div class="card tech-info">
<h2><i class="fas fa-info-circle"></i> 技术说明</h2>
<div class="tech-tabs">
<button class="tech-tab active" onclick="showTechTab('mixed')">混排技术</button>
<button class="tech-tab" onclick="showTechTab('multi')">多任务学习</button>
<button class="tech-tab" onclick="showTechTab('online')">在线学习</button>
</div>
<div class="tech-content" id="mixedTech">
<h3>混排技术 (Mixed Ranking)</h3>
<p>通过智能搭配不同品类内容,平衡精准推荐和内容多样性。</p>
<ul>
<li><strong>基于规则混排</strong>: 固定比例分配推荐位</li>
<li><strong>基于模型混排</strong>: 多目标优化动态分配</li>
<li><strong>解决精准陷阱</strong>: 避免用户兴趣固化</li>
</ul>
</div>
<div class="tech-content" id="multiTech" style="display: none;">
<h3>多任务学习 (Multi-Task Learning)</h3>
<p>同时优化多个业务目标,让模型更全面理解用户需求。</p>
<ul>
<li><strong>共享特征层</strong>: 提取通用用户和内容特征</li>
<li><strong>任务特定层</strong>: 针对不同目标分别优化</li>
<li><strong>协同优化</strong>: CTR、CVR、停留时长等多目标</li>
</ul>
</div>
<div class="tech-content" id="onlineTech" style="display: none;">
<h3>在线学习 (Online Learning)</h3>
<p>实时捕捉用户行为,动态更新模型参数。</p>
<ul>
<li><strong>增量训练</strong>: 使用实时数据微调模型</li>
<li><strong>快速响应</strong>: 秒级更新推荐结果</li>
<li><strong>动态适应</strong>: 紧跟用户兴趣变化</li>
</ul>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-panel">
<!-- 推荐结果 -->
<div class="card recommendations">
<div class="card-header">
<h2><i class="fas fa-th-list"></i> 推荐结果</h2>
<div class="strategy-selector">
<span>推荐策略:</span>
<button class="strategy-btn active" onclick="changeStrategy('model_based')">模型混排</button>
<button class="strategy-btn" onclick="changeStrategy('rule_based')">规则混排</button>
<span class="diversity-score" id="diversityScore">多样性: --</span>
</div>
</div>
<div class="recommendations-grid" id="recommendationsGrid">
<!-- 推荐内容将通过JS动态加载 -->
</div>
</div>
<!-- 用户兴趣分析 -->
<div class="card user-interests">
<h2><i class="fas fa-chart-line"></i> 用户兴趣分析</h2>
<div class="chart-container">
<canvas id="interestChart"></canvas>
</div>
<div class="interest-list" id="interestList">
<!-- 兴趣列表将通过JS动态加载 -->
</div>
</div>
<!-- 在线学习状态 -->
<div class="card online-learning">
<h2><i class="fas fa-sync-alt"></i> 在线学习状态</h2>
<div class="online-stats" id="onlineStats">
<!-- 在线学习状态将通过JS动态加载 -->
</div>
<div class="update-log" id="updateLog">
<h3>最近更新记录</h3>
<div class="log-list" id="logList">
<!-- 更新日志将通过JS动态加载 -->
</div>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 用户行为记录 -->
<div class="card behavior-log">
<h2><i class="fas fa-history"></i> 用户行为记录</h2>
<div class="behavior-list" id="behaviorList">
<!-- 行为记录将通过JS动态加载 -->
</div>
</div>
<!-- 内容分类统计 -->
<div class="card category-stats">
<h2><i class="fas fa-tags"></i> 内容分类统计</h2>
<div class="category-chart">
<canvas id="categoryChart"></canvas>
</div>
<div class="category-list" id="categoryList">
<!-- 分类统计将通过JS动态加载 -->
</div>
</div>
<!-- 多任务模型信息 -->
<div class="card model-info">
<h2><i class="fas fa-network-wired"></i> 多任务模型</h2>
<div class="model-details" id="modelDetails">
<!-- 模型信息将通过JS动态加载 -->
</div>
</div>
</div>
</div>
<!-- 底部信息 -->
<footer class="footer">
<p>推荐系统高阶技术演示平台 | 混排 + 多任务学习 + 在线学习</p>
<p class="update-time" id="updateTime">最后更新: --</p>
</footer>
</div>
<script>
// 全局变量
let currentUserId = 1;
let currentStrategy = 'model_based';
let interestChart = null;
let categoryChart = null;
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadUsers();
loadSystemStats();
loadUserData(currentUserId);
loadContentCategories();
loadOnlineLearningStats();
// 设置自动刷新
setInterval(updateDashboard, 5000);
// 初始化技术标签页
showTechTab('mixed');
});
// 加载用户列表
async function loadUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
const userList = document.getElementById('userList');
userList.innerHTML = '';
data.users.forEach(user => {
const userItem = document.createElement('div');
userItem.className = `user-item ${user.id === currentUserId ? 'active' : ''}`;
userItem.innerHTML = `
<div class="user-name">${user.name}</div>
<div class="user-meta">
<span>兴趣: ${user.interest_count}</span>
<span>行为: ${user.behavior_count}</span>
</div>
`;
userItem.onclick = () => {
currentUserId = user.id;
loadUserData(user.id);
document.querySelectorAll('.user-item').forEach(item => {
item.classList.remove('active');
});
userItem.classList.add('active');
};
userList.appendChild(userItem);
});
} catch (error) {
console.error('加载用户失败:', error);
}
}
// 加载用户数据
async function loadUserData(userId) {
try {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
// 加载推荐
loadRecommendations();
// 加载用户兴趣
loadUserInterests(userId);
// 加载行为记录
loadBehaviorLog(data.recent_behavior);
} catch (error) {
console.error('加载用户数据失败:', error);
}
}
// 加载推荐结果
async function loadRecommendations() {
try {
const response = await fetch(`/api/recommend/${currentUserId}?strategy=${currentStrategy}`);
const data = await response.json();
const grid = document.getElementById('recommendationsGrid');
grid.innerHTML = '';
// 更新多样性分数
document.getElementById('diversityScore').textContent = `多样性: ${data.diversity_score}`;
data.recommendations.forEach(content => {
const item = document.createElement('div');
item.className = 'recommendation-item';
// 为不同类别设置不同颜色
const categoryColors = {
'科技': '#2196F3',
'娱乐': '#FF9800',
'体育': '#4CAF50',
'财经': '#9C27B0',
'时尚': '#E91E63',
'教育': '#00BCD4',
'健康': '#8BC34A',
'美食': '#FF5722'
};
const bgColor = categoryColors[content.category] || '#607D8B';
item.innerHTML = `
<div class="content-header" style="background: ${bgColor}">
${content.category}
</div>
<div class="content-body">
<div class="content-title">${content.title}</div>
<div class="content-description">${content.description}</div>
<div class="content-meta">
<span class="content-category">${content.category}</span>
<span>新鲜度: ${content.freshness}天</span>
</div>
<div class="content-stats">
<div class="stat-item">
<div class="stat-label">CTR预测</div>
<div class="stat-value">${content.ctr_pred}</div>
</div>
<div class="stat-item">
<div class="stat-label">CVR预测</div>
<div class="stat-value">${content.cvr_pred}</div>
</div>
<div class="stat-item">
<div class="stat-label">综合得分</div>
<div class="stat-value">${content.final_score}</div>
</div>
</div>
<div class="content-actions">
<button class="action-btn action-click" onclick="processAction(${content.id}, 'click')">
<i class="fas fa-mouse-pointer"></i> 点击
</button>
<button class="action-btn action-watch" onclick="processAction(${content.id}, 'watch_long')">
<i class="fas fa-eye"></i> 观看
</button>
<button class="action-btn action-purchase" onclick="processAction(${content.id}, 'purchase')">
<i class="fas fa-shopping-cart"></i> 购买
</button>
</div>
</div>
`;
grid.appendChild(item);
});
} catch (error) {
console.error('加载推荐失败:', error);
}
}
// 处理用户行为
async function processAction(contentId, actionType) {
try {
const response = await fetch('/api/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUserId,
content_id: contentId,
action_type: actionType
})
});
const data = await response.json();
if (data.success) {
// 重新加载数据
loadUserData(currentUserId);
loadSystemStats();
loadOnlineLearningStats();
// 显示成功消息
showNotification(`用户行为已处理: ${getActionName(actionType)}`, 'success');
}
} catch (error) {
console.error('处理行为失败:', error);
showNotification('处理失败,请重试', 'error');
}
}
// 获取行为名称
function getActionName(actionType) {
const names = {
'click': '点击',
'watch_long': '观看',
'purchase': '购买',
'skip': '跳过'
};
return names[actionType] || actionType;
}
// 加载系统统计
async function loadSystemStats() {
try {
const response = await fetch('/api/system/stats');
const data = await response.json();
const statsGrid = document.getElementById('systemStats');
statsGrid.innerHTML = `
<div class="stat-box">
<div class="stat-value">${data.system_stats.total_users}</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.system_stats.total_recommendations}</div>
<div class="stat-label">推荐次数</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.system_stats.total_interactions}</div>
<div class="stat-label">交互次数</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.system_stats.average_diversity}</div>
<div class="stat-label">平均多样性</div>
</div>
`;
// 更新模型信息
const modelDetails = document.getElementById('modelDetails');
modelDetails.innerHTML = `
<div class="stat-box">
<div class="stat-value">${data.multi_task_model.update_count}</div>
<div class="stat-label">更新次数</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.multi_task_model.shared_layer_shape[0]}×${data.multi_task_model.shared_layer_shape[1]}</div>
<div class="stat-label">共享层尺寸</div>
</div>
`;
} catch (error) {
console.error('加载系统统计失败:', error);
}
}
// 加载用户兴趣
async function loadUserInterests(userId) {
try {
const response = await fetch(`/api/user/${userId}/interests`);
const data = await response.json();
// 更新兴趣列表
const interestList = document.getElementById('interestList');
interestList.innerHTML = '';
data.interests.forEach(interest => {
const item = document.createElement('div');
item.className = 'interest-item';
// 计算变化指示器
let changeIndicator = '';
if (interest.change > 0.1) {
changeIndicator = '<i class="fas fa-arrow-up" style="color: #48bb78;"></i>';
} else if (interest.change < -0.1) {
changeIndicator = '<i class="fas fa-arrow-down" style="color: #f56565;"></i>';
}
item.innerHTML = `
<div class="interest-category">${interest.category}</div>
<div class="interest-values">
<div class="interest-value">
<div class="interest-label">基础</div>
<div>${interest.base_interest}</div>
</div>
<div class="interest-value">
<div class="interest-label">当前</div>
<div>${interest.current_interest} ${changeIndicator}</div>
</div>
</div>
`;
interestList.appendChild(item);
});
// 更新图表
updateInterestChart(data.interests);
} catch (error) {
console.error('加载用户兴趣失败:', error);
}
}
// 更新兴趣图表
function updateInterestChart(interests) {
const ctx = document.getElementById('interestChart').getContext('2d');
const categories = interests.map(i => i.category);
const baseInterests = interests.map(i => i.base_interest);
const currentInterests = interests.map(i => i.current_interest);
if (interestChart) {
interestChart.destroy();
}
interestChart = new Chart(ctx, {
type: 'bar',
data: {
labels: categories,
datasets: [
{
label: '基础兴趣',
data: baseInterests,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: '当前兴趣',
data: currentInterests,
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 1,
title: {
display: true,
text: '兴趣度'
}
}
},
plugins: {
legend: {
position: 'top',
},
tooltip: {
mode: 'index',
intersect: false
}
}
}
});
}
// 加载行为记录
function loadBehaviorLog(behaviors) {
const behaviorList = document.getElementById('behaviorList');
behaviorList.innerHTML = '';
if (!behaviors || behaviors.length === 0) {
behaviorList.innerHTML = '<p style="text-align: center; color: #718096;">暂无行为记录</p>';
return;
}
behaviors.reverse().forEach(behavior => {
const item = document.createElement('div');
item.className = 'behavior-item';
const actionType = behavior.action.type;
const typeClass = `type-${actionType}`;
item.innerHTML = `
<div class="behavior-time">${behavior.timestamp}</div>
<div class="behavior-content">${behavior.action.title}</div>
<div>
<span class="behavior-type ${typeClass}">${getActionName(actionType)}</span>
<span style="color: #718096; font-size: 0.9rem;">${behavior.category}</span>
</div>
`;
behaviorList.appendChild(item);
});
}
// 加载内容分类统计
async function loadContentCategories() {
try {
const response = await fetch('/api/content/categories');
const data = await response.json();
// 更新分类列表
const categoryList = document.getElementById('categoryList');
categoryList.innerHTML = '';
Object.entries(data.categories).forEach(([category, stats]) => {
const item = document.createElement('div');
item.className = 'interest-item';
item.innerHTML = `
<div class="interest-category">${category}</div>
<div class="interest-values">
<div class="interest-value">
<div class="interest-label">内容数</div>
<div>${stats.count}</div>
</div>
<div class="interest-value">
<div class="interest-label">平均CTR</div>
<div>${stats.avg_ctr}</div>
</div>
<div class="interest-value">
<div class="interest-label">平均CVR</div>
<div>${stats.avg_cvr}</div>
</div>
</div>
`;
categoryList.appendChild(item);
});
// 更新分类图表
updateCategoryChart(data.categories);
} catch (error) {
console.error('加载分类统计失败:', error);
}
}
// 更新分类图表
function updateCategoryChart(categories) {
const ctx = document.getElementById('categoryChart').getContext('2d');
const categoryNames = Object.keys(categories);
const contentCounts = categoryNames.map(name => categories[name].count);
// 生成颜色数组
const backgroundColors = [
'#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0',
'#9966FF', '#FF9F40', '#8AC926', '#1982C4'
];
if (categoryChart) {
categoryChart.destroy();
}
categoryChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: categoryNames,
datasets: [{
data: contentCounts,
backgroundColor: backgroundColors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
boxWidth: 12,
padding: 15
}
}
}
}
});
}
// 加载在线学习状态
async function loadOnlineLearningStats() {
try {
const response = await fetch('/api/system/stats');
const data = await response.json();
const onlineStats = document.getElementById('onlineStats');
onlineStats.innerHTML = `
<div class="stats-grid">
<div class="stat-box">
<div class="stat-value">${data.online_learning.total_updates}</div>
<div class="stat-label">总更新次数</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.online_learning.recent_feedback_count}</div>
<div class="stat-label">待处理反馈</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.online_learning.update_interval}s</div>
<div class="stat-label">更新间隔</div>
</div>
<div class="stat-box">
<div class="stat-value">${data.online_learning.user_model_count}</div>
<div class="stat-label">用户模型数</div>
</div>
</div>
`;
// 更新日志
const logList = document.getElementById('logList');
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
now.getMinutes().toString().padStart(2, '0') + ':' +
now.getSeconds().toString().padStart(2, '0');
const logItem = document.createElement('div');
logItem.className = 'behavior-item';
logItem.innerHTML = `
<div class="behavior-time">${timeStr}</div>
<div class="behavior-content">系统状态检查 - 在线学习运行正常</div>
<div>
<span class="behavior-type type-click">更新</span>
<span style="color: #718096; font-size: 0.9rem;">模型检查完成</span>
</div>
`;
logList.insertBefore(logItem, logList.firstChild);
// 保持日志数量不超过10条
if (logList.children.length > 10) {
logList.removeChild(logList.lastChild);
}
// 更新最后更新时间
document.getElementById('updateTime').textContent = `最后更新: ${timeStr}`;
} catch (error) {
console.error('加载在线学习状态失败:', error);
}
}
// 更改推荐策略
function changeStrategy(strategy) {
currentStrategy = strategy;
// 更新按钮状态
document.querySelectorAll('.strategy-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// 重新加载推荐
loadRecommendations();
}
// 显示技术标签页
function showTechTab(tabName) {
// 隐藏所有内容
document.querySelectorAll('.tech-content').forEach(content => {
content.style.display = 'none';
});
// 更新标签按钮
document.querySelectorAll('.tech-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// 显示对应内容
document.getElementById(tabName + 'Tech').style.display = 'block';
}
// 模拟用户行为
async function simulateActions() {
const actionCount = document.getElementById('actionCount').value;
try {
const response = await fetch('/api/demo/simulate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUserId,
action_count: parseInt(actionCount)
})
});
const data = await response.json();
if (data.success) {
// 重新加载数据
loadUserData(currentUserId);
loadSystemStats();
loadOnlineLearningStats();
// 显示模拟结果
showNotification(`成功模拟${data.actions.length}个用户行为`, 'success');
// 添加到日志
const logList = document.getElementById('logList');
data.actions.forEach(action => {
const logItem = document.createElement('div');
logItem.className = 'behavior-item';
logItem.innerHTML = `
<div class="behavior-time">${action.timestamp}</div>
<div class="behavior-content">${action.content_title}</div>
<div>
<span class="behavior-type type-${action.action_type}">${getActionName(action.action_type)}</span>
<span style="color: #718096; font-size: 0.9rem;">${action.category}</span>
</div>
`;
logList.insertBefore(logItem, logList.firstChild);
});
}
} catch (error) {
console.error('模拟行为失败:', error);
showNotification('模拟失败,请重试', 'error');
}
}
// 更新仪表板
function updateDashboard() {
loadSystemStats();
loadOnlineLearningStats();
loadUserInterests(currentUserId);
}
// 显示通知
function showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i>
<span>${message}</span>
`;
// 添加到页面
document.body.appendChild(notification);
// 添加样式
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
background: ${type === 'success' ? '#48bb78' : '#f56565'};
color: white;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
animation: slideIn 0.3s ease;
font-size: 0.9rem;
`;
// 添加动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
// 3秒后移除
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
</script>
</body>
</html>
'''
# ==================== Flask路由 ====================
@app.route('/')
def index():
"""主页面"""
return render_template_string(HTML_TEMPLATE)
@app.route('/api/users')
def get_users():
"""获取所有用户列表"""
users = []
for user_id, user in recommendation_system.users.items():
users.append({
'id': user_id,
'name': user.name,
'interest_count': len(user.real_time_interests),
'behavior_count': len(user.history_behavior)
})
return jsonify({'users': users})
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
"""获取用户详细信息"""
user_info = recommendation_system.get_user_info(user_id)
if user_info:
return jsonify(user_info)
return jsonify({'error': '用户不存在'}), 404
@app.route('/api/recommend/<int:user_id>')
def get_recommendations(user_id):
"""获取用户推荐"""
strategy = request.args.get('strategy', 'model_based')
recommendations = recommendation_system.generate_recommendations(user_id, strategy)
# 计算推荐列表的多样性
categories = [r['category'] for r in recommendations]
category_counts = {}
for category in categories:
category_counts[category] = category_counts.get(category, 0) + 1
total = len(categories)
hhi = sum((count/total)**2 for count in category_counts.values())
diversity = 1 - hhi
return jsonify({
'recommendations': recommendations,
'diversity_score': round(diversity, 3),
'strategy': strategy,
'count': len(recommendations)
})
@app.route('/api/action', methods=['POST'])
def process_action():
"""处理用户行为"""
data = request.json
user_id = data.get('user_id')
content_id = data.get('content_id')
action_type = data.get('action_type')
if not all([user_id, content_id, action_type]):
return jsonify({'error': '参数不完整'}), 400
success = recommendation_system.process_user_action(user_id, content_id, action_type)
if success:
return jsonify({'success': True, 'message': '行为已处理'})
else:
return jsonify({'error': '处理失败'}), 400
@app.route('/api/system/stats')
def get_system_stats():
"""获取系统统计信息"""
stats = recommendation_system.get_system_stats()
online_info = recommendation_system.online_learning.get_system_info()
model_info = recommendation_system.multi_task_model.get_model_info()
return jsonify({
'system_stats': stats,
'online_learning': online_info,
'multi_task_model': model_info
})
@app.route('/api/content/categories')
def get_content_categories():
"""获取内容分类统计"""
categories = recommendation_system.content_pool.categories
stats = {}
for category in categories:
contents = recommendation_system.content_pool.get_contents_by_category(category, 100)
avg_ctr = sum(c['ctr'] for c in contents) / len(contents) if contents else 0
avg_cvr = sum(c['cvr'] for c in contents) / len(contents) if contents else 0
stats[category] = {
'count': len(contents),
'avg_ctr': round(avg_ctr, 3),
'avg_cvr': round(avg_cvr, 4)
}
return jsonify({'categories': stats})
@app.route('/api/user/<int:user_id>/interests')
def get_user_interests(user_id):
"""获取用户兴趣变化"""
if user_id not in recommendation_system.users:
return jsonify({'error': '用户不存在'}), 404
user = recommendation_system.users[user_id]
# 基础兴趣
base_interests = user.base_interests
# 当前实时兴趣
current_interests = user.real_time_interests
# 所有兴趣类别
all_categories = list(set(list(base_interests.keys()) + list(current_interests.keys())))
interests_data = []
for category in all_categories:
interests_data.append({
'category': category,
'base_interest': round(base_interests.get(category, 0), 3),
'current_interest': round(current_interests.get(category, 0), 3),
'change': round(current_interests.get(category, 0) - base_interests.get(category, 0), 3)
})
# 按当前兴趣排序
interests_data.sort(key=lambda x: x['current_interest'], reverse=True)
return jsonify({'interests': interests_data})
@app.route('/api/demo/simulate', methods=['POST'])
def simulate_actions():
"""模拟用户行为"""
data = request.json
user_id = data.get('user_id', 1)
action_count = data.get('action_count', 5)
user = recommendation_system.get_or_create_user(user_id)
actions = []
for i in range(action_count):
# 生成推荐
recommendations = recommendation_system.generate_recommendations(user_id)
if recommendations:
# 随机选择一条内容
selected_content = random.choice(recommendations[:5])
# 随机选择行为类型
action_types = ['click', 'watch_long', 'purchase', 'skip']
weights = [0.5, 0.3, 0.1, 0.1]
action_type = random.choices(action_types, weights=weights)[0]
# 处理行为
recommendation_system.process_user_action(
user_id, selected_content['id'], action_type
)
actions.append({
'content_title': selected_content['title'],
'category': selected_content['category'],
'action_type': action_type,
'timestamp': datetime.now().strftime("%H:%M:%S")
})
return jsonify({
'success': True,
'message': f'成功模拟{len(actions)}个行为',
'actions': actions
})
if __name__ == '__main__':
print("="*70)
print("推荐系统高阶技术可视化平台")
print("访问地址: http://127.0.0.1:5000")
print("="*70)
app.run(debug=True, threaded=True)
这段代码是一个完整的推荐系统高阶技术可视化平台 ,集成了三个核心推荐系统技术:混排(Mixed Ranking)、多任务学习(Multi-Task Learning)、在线学习(Online Learning)
1.技术栈
-
后端:Flask框架 + Python
-
前端:HTML5 + CSS3 + JavaScript + Chart.js
-
数据结构:Python类 + 随机数据生成
-
部署:单文件Flask应用
2. 核心功能模块
推荐系统核心类分析
- UserProfile(用户画像类)
python
功能:
- 生成用户基础兴趣(随机3-5个类别)
- 实时更新用户兴趣(基于用户行为)
- 兴趣衰减机制(非交互类别兴趣下降)
- 行为历史记录(最多50条)
技术特点:
- 模拟真实用户兴趣变化
- 支持兴趣的动态调整
- 记录完整行为轨迹
- ContentPool(内容池类)
python
功能:
- 生成8个类别的模拟内容(科技、娱乐等)
- 每个内容包含:标题、类别、CTR、CVR、质量分、新鲜度
- 为每个类别生成独特的描述和颜色
技术特点:
- 模拟真实内容多样性
- 包含多维度内容特征
- 支持按类别检索
- MixedRanking(混排算法类)
python
两种混排策略:
1. 规则混排:
- 70%用户兴趣内容 + 20%相关品类 + 10%探索内容
- 固定比例,简单直观
2. 模型混排:
- 多目标优化:CTR(40%) + CVR(30%) + 用户兴趣(20%) + 新鲜度(5%) + 多样性(5%)
- 动态权重分配
- 多样性惩罚机制(避免同类内容过多)
技术特点:
- 平衡精准推荐与内容多样性
- 解决"精准陷阱"问题
- 支持多目标优化
- MultiTaskModel(多任务学习模型)
python
神经网络架构:
- 共享层:10×8的全连接层(提取通用特征)
- 任务特定层:
- CTR预测层:8×1
- CVR预测层:8×1
技术特点:
- 共享特征提取,降低计算成本
- 同时优化CTR和CVR两个目标
- 模型参数可实时更新
- OnlineLearningSystem(在线学习系统)
python
功能:
- 实时处理用户反馈
- 每5秒更新一次模型参数
- 增量训练(使用随机梯度下降简化版)
技术特点:
- 秒级响应时间
- 实时适应用户兴趣变化
- 处理噪声数据的鲁棒性
- RecommendationSystem(推荐系统主类)
python
整合所有组件:
1. 用户管理:创建/获取用户画像
2. 推荐生成:混排策略 → 多任务重排
3. 行为处理:更新兴趣 + 在线学习
4. 系统统计:多样性计算、性能指标
工作流程:
用户请求 → 候选内容筛选 → 混排 → 多任务重排 → 返回推荐
可视化功能分析
- 前端界面布局
python
三栏响应式设计:
- 左侧面板:用户管理 + 系统统计 + 技术说明
- 中间面板:推荐结果 + 用户兴趣图表 + 在线学习状态
- 右侧面板:行为记录 + 分类统计 + 模型信息
- 核心可视化组件
a) 推荐结果展示
彩色卡片设计(不同类别不同颜色)
显示内容:标题、描述、类别、新鲜度
预测指标:CTR预测、CVR预测、综合得分
交互按钮:点击、观看、购买
b) 用户兴趣分析
柱状图对比:基础兴趣 vs 当前兴趣
兴趣变化指示器(箭头图标)
兴趣数值列表(带变化值)
c) 系统统计仪表盘
实时指标:用户数、推荐次数、交互次数、多样性
在线学习状态:更新次数、待处理反馈、更新间隔
模型信息:更新次数、网络结构
d) 数据图表
用户兴趣柱状图(Chart.js)
内容分类环形图(Chart.js)
- 交互功能
python
1. 用户切换:点击用户卡片切换当前用户
2. 策略切换:模型混排 vs 规则混排
3. 行为模拟:一键模拟多个用户行为
4. 实时交互:点击推荐内容的不同行为按钮
5. 技术说明:标签页切换查看不同技术原理
API接口设计
python
1. GET /api/users # 获取用户列表
2. GET /api/user/<user_id> # 获取用户详细信息
3. GET /api/recommend/<user_id> # 获取推荐内容
4. POST /api/action # 处理用户行为
5. GET /api/system/stats # 获取系统统计
6. GET /api/content/categories # 获取内容分类统计
7. GET /api/user/<id>/interests # 获取用户兴趣
8. POST /api/demo/simulate # 模拟用户行为
运行效果


运行后:
-
看到完整的可视化界面
-
可以选择用户、切换策略
-
可以点击推荐内容进行交互
-
观察用户兴趣和系统状态的变化
五、总结:高阶推荐系统的核心逻辑------从"精准"到"智能"
今天我们聊了推荐系统高阶的三个核心技术:混排、多任务学习、在线学习。其实这三个技术的核心逻辑,都是从入门阶段的"追求精准",升级到高阶的"追求智能"------
-
混排:让推荐从"单品类精准"升级到"多品类智能搭配",兼顾用户体验和业务目标;
-
多任务学习:让模型从"单一目标优化"升级到"多目标协同优化",更全面理解用户需求;
-
在线学习:让系统从"定期更新"升级到"实时进化",紧跟用户兴趣的动态变化。
这三个技术不是孤立的,在实际业务中通常会结合使用:比如用多任务学习训练混排模型,再通过在线学习系统实时更新模型参数,最终实现"实时、精准、多样化"的推荐效果。
看到这里,你是不是对高阶推荐系统有了清晰的认知?其实这些技术看似复杂,但核心都是"以用户为中心",解决实际业务中的痛点。接下来的下一篇高阶内容,我们会聊"推荐系统的工程化实践"------比如如何应对高并发推荐请求?如何设计推荐系统的缓存策略?如何做推荐效果的AB测试?这些都是把高阶技术落地的关键。
如果今天的内容对你有帮助,欢迎在评论区分享你的学习心得,或者提出你想了解的高阶问题。咱们下一篇,继续解锁机器学习高阶技能!