AI推荐系统:这次怎么落地的
先整理一下产品提出的需求:
- 商品详情页推荐:用户在查看商品详情时,显示"相关推荐"
- 个性化推荐:根据用户的浏览和购买历史,推荐可能感兴趣的商品
- 分类页推荐:在分类页面,根据用户在该分类的浏览行为推荐商品
- 搜索结果推荐:用户搜索后,在结果页下方展示相关推荐
技术团队给的约束条件也很现实:
背景和问题
前些天在重构一个电商后台的时候,产品经理跑过来问我:“能不能给用户加个推荐功能?就是那种用户看了A商品,给他推荐类似的商品。”
我当时心想这不就是一个简单的相似度匹配吗?直接用商品分类或者标签不就行了。但实际操作下来,发现事情没那么简单。
遇到的实际问题
首先,商品数据结构很乱。有些商品有详细标签,有些就只有一个分类名称;其次,用户行为数据也分散在不同的表里,浏览记录、收藏记录、购买记录都在各自的地盘;最坑的是,项目数据量不算太大,但也足够让那些复杂的深度学习模型跑不起来。
这些现实条件限制了我们的选择:不能用太复杂的模型,不能有太多外部依赖,还得能在有限的服务器资源上跑起来。
要解决什么
所以我需要设计一个实用、轻量、但又能提升用户体验的推荐系统。具体来说:
- 基于用户行为历史推荐相关商品
- 支持冷启动场景(新用户、新商品)
- 能够快速计算和响应
- 资源占用可控
这篇文章就是我折腾这个推荐系统的过程记录,希望能给类似场景的同学一些参考。
需求分析
业务需求
先整理一下产品提出的需求:
- 商品详情页推荐:用户在查看商品详情时,显示"相关推荐"
- 个性化推荐:根据用户的浏览和购买历史,推荐可能感兴趣的商品
- 分类页推荐:在分类页面,根据用户在该分类的浏览行为推荐商品
- 搜索结果推荐:用户搜索后,在结果页下方展示相关推荐
技术约束
技术团队给的约束条件也很现实:
- 服务器资源有限,不能部署大型深度学习模型
- 计算需要快速完成,用户等待时间不能超过200ms
- 需要支持在线实时更新,推荐结果要能及时反映最新数据
- 现有技术栈是 Python + PostgreSQL + Redis,尽量不引入新的重型依赖
数据现状
先看看我们有什么数据:
# 商品表结构(简化版)
class Product:
id: int
name: str
category: str
tags: List[str]
price: float
created_at: datetime
# ... 其他字段
# 用户行为表结构
class UserBehavior:
user_id: int
product_id: int
action: str # 'view', 'favorite', 'purchase'
timestamp: datetime
# ... 其他字段
数据量大约是:商品2万个,活跃用户5000个,历史行为记录50万条。这个量级其实挺适合做协同过滤的。
实现方案
基于以上分析,我决定采用混合推荐策略:以协同过滤为主,内容过滤为辅,同时处理冷启动问题。
系统架构
整个推荐系统的架构如下:
协同过滤实现
用户-商品矩阵构建
首先需要构建用户-商品交互矩阵。我选择了隐式反馈(用户浏览、收藏、购买),因为显式评分数据太少了。
import numpy as np
import pandas as pd
from collections import defaultdict
def build_user_item_matrix(behaviors):
"""
构建用户-商品交互矩阵
"""
# 初始化数据结构
user_ids = set()
item_ids = set()
interactions = defaultdict(lambda: defaultdict(float))
# 处理行为数据,给不同行为不同的权重
action_weights = {
'view': 1.0,
'favorite': 2.0,
'purchase': 5.0
}
for behavior in behaviors:
user_id = behavior['user_id']
item_id = behavior['product_id']
action = behavior['action']
user_ids.add(user_id)
item_ids.add(item_id)
# 累加行为权重
interactions[user_id][item_id] += action_weights.get(action, 0)
# 转换为矩阵形式
user_list = sorted(user_ids)
item_list = sorted(item_ids)
user_to_idx = {uid: idx for idx, uid in enumerate(user_list)}
item_to_idx = {iid: idx for idx, iid in enumerate(item_list)}
matrix = np.zeros((len(user_list), len(item_list)))
for user_id, items in interactions.items():
for item_id, weight in items.items():
matrix[user_to_idx[user_id], item_to_idx[item_id]] = weight
return matrix, user_to_idx, item_to_idx
矩阵分解
考虑到稀疏矩阵的问题,我使用了矩阵分解来降维和提高计算效率:
from scipy.sparse.linalg import svds
def matrix_factorization(matrix, n_factors=10):
"""
矩阵分解
"""
# 填充缺失值为0
user_means = np.mean(matrix, axis=1)
matrix_demeaned = matrix - user_means.reshape(-1, 1)
# SVD分解
U, sigma, Vt = svds(matrix_demeaned, k=n_factors)
# 重构预测矩阵
sigma = np.diag(sigma)
predicted_ratings = np.dot(np.dot(U, sigma), Vt) + user_means.reshape(-1, 1)
return predicted_ratings, U, sigma, Vt
相似度计算
对于商品相似度计算,我选择了余弦相似度:
from sklearn.metrics.pairwise import cosine_similarity
def calculate_item_similarity(Vt):
"""
计算商品相似度矩阵
"""
# Vt是商品特征矩阵的转置,转回来
item_features = Vt.T
# 计算余弦相似度
similarity_matrix = cosine_similarity(item_features)
return similarity_matrix
def get_similar_items(item_id, item_to_idx, similarity_matrix, top_k=10):
"""
获取相似商品
"""
if item_id not in item_to_idx:
return []
item_idx = item_to_idx[item_id]
similarities = similarity_matrix[item_idx]
# 获取最相似的top_k个商品
similar_indices = np.argsort(similarities)[::-1][1:top_k+1] # 排除自己
idx_to_item = {idx: iid for iid, idx in item_to_idx.items()}
similar_items = [
(idx_to_item[idx], similarities[idx])
for idx in similar_indices
if similarities[idx] > 0
]
return similar_items
内容过滤实现
为了处理冷启动问题,还需要基于内容特征的推荐:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def build_content_similarity(products):
"""
基于商品内容特征构建相似度
"""
# 提取文本特征
texts = []
for product in products:
# 组合名称、分类、标签
text = f"{product['name']} {product['category']} {' '.join(product['tags'])}"
texts.append(text)
# TF-IDF向量化
vectorizer = TfidfVectorizer(max_features=1000)
tfidf_matrix = vectorizer.fit_transform(texts)
# 计算相似度
similarity_matrix = cosine_similarity(tfidf_matrix)
return similarity_matrix, vectorizer
推荐服务整合
将所有部分整合到一个推荐服务中:
class RecommendationService:
def __init__(self):
self.user_item_matrix = None
self.predicted_ratings = None
self.item_similarity = None
self.content_similarity = None
self.user_to_idx = {}
self.item_to_idx = {}
self.products = {}
def train(self, behaviors, products):
"""
训练推荐模型
"""
# 构建用户-商品矩阵
self.user_item_matrix, self.user_to_idx, self.item_to_idx = \
build_user_item_matrix(behaviors)
# 矩阵分解
self.predicted_ratings, U, sigma, Vt = \
matrix_factorization(self.user_item_matrix)
# 商品相似度(协同过滤)
self.item_similarity = calculate_item_similarity(Vt)
# 内容相似度(用于冷启动)
self.content_similarity, _ = build_content_similarity(products)
# 保存商品信息
self.products = {p['id']: p for p in products}
def recommend_for_user(self, user_id, top_k=10):
"""
为用户推荐商品
"""
if user_id not in self.user_to_idx:
return self._get_popular_items(top_k)
user_idx = self.user_to_idx[user_id]
user_ratings = self.predicted_ratings[user_idx]
# 获取用户未交互的商品
interacted_items = set(self.user_item_matrix[user_idx].nonzero()[1])
recommendations = []
for item_idx, rating in enumerate(user_ratings):
if item_idx not in interacted_items:
item_id = list(self.item_to_idx.keys())[list(self.item_to_idx.values()).index(item_idx)]
recommendations.append((item_id, rating))
# 按预测评分排序
recommendations.sort(key=lambda x: x[1], reverse=True)
return recommendations[:top_k]
def recommend_similar_items(self, item_id, top_k=10):
"""
推荐相似商品
"""
if item_id in self.item_to_idx:
# 使用协同过滤的相似度
return get_similar_items(item_id, self.item_to_idx, self.item_similarity, top_k)
else:
# 使用内容相似度处理冷启动
return self._get_content_similar_items(item_id, top_k)
def _get_popular_items(self, top_k=10):
"""
获取热门商品
"""
# 简单实现:基于交互次数
item_popularity = np.sum(self.user_item_matrix, axis=0)
popular_indices = np.argsort(item_popularity)[::-1][:top_k]
idx_to_item = {idx: iid for iid, idx in self.item_to_idx.items()}
return [(idx_to_item[idx], item_popularity[idx]) for idx in popular_indices]
def _get_content_similar_items(self, item_id, top_k=10):
"""
基于内容获取相似商品
"""
# 这里简化处理,实际需要根据商品ID查找在content_similarity中的位置
return []
缓存优化
为了提高响应速度,我加了一层 Redis 缓存:
import redis
import json
import hashlib
class CachedRecommendationService:
def __init__(self, redis_host='localhost', redis_port=6379):
self.redis_client = redis.StrictRedis(
host=redis_host,
port=redis_port,
decode_responses=True
)
self.base_service = RecommendationService()
def _get_cache_key(self, method_name, *args):
"""
生成缓存键
"""
key_data = f"{method_name}:{args}"
return hashlib.md5(key_data.encode()).hexdigest()
def recommend_for_user(self, user_id, top_k=10, cache_ttl=3600):
"""
带缓存的用户推荐
"""
cache_key = self._get_cache_key('recommend_for_user', user_id, top_k)
# 尝试从缓存获取
cached_result = self.redis_client.get(cache_key)
if cached_result:
return json.loads(cached_result)
# 缓存未命中,调用基础服务
result = self.base_service.recommend_for_user(user_id, top_k)
# 写入缓存
self.redis_client.setex(
cache_key,
cache_ttl,
json.dumps(result)
)
return result
def invalidate_user_cache(self, user_id):
"""
使特定用户的缓存失效
"""
# 这里简化处理,实际可能需要更复杂的缓存键管理
pattern = f"*recommend_for_user:{user_id}*"
keys = self.redis_client.keys(pattern)
if keys:
self.redis_client.delete(*keys)
踩坑记录
实现过程中遇到了不少坑,这里记录几个比较典型的:
数据稀疏性问题
最开始直接用原始的用户-商品矩阵,结果发现99%以上的值都是0,这导致相似度计算效果很差。
解决方案:
- 增加数据预处理,过滤掉行为次数很少的用户和商品
- 使用矩阵分解降维
- 对交互行为加权,给更有意义的行为(如购买)更高的权重
冷启动难题
新用户没有历史行为,新商品没有交互记录,这导致协同过滤完全失效。
解决方案:
- 对新用户:先推荐热门商品,同时收集用户行为
- 对新商品:使用内容过滤,基于商品属性找相似商品
- 混合策略:同时使用多种推荐方法,加权合并结果
计算性能瓶颈
矩阵分解和相似度计算都很耗时,特别是数据量大的时候。
解决方案:
- 使用增量更新,只重新计算变化的部分
- 加 Redis 缓存,缓存热门用户和商品的推荐结果
- 降低矩阵分解的维度,在效果和性能间找平衡
- 使用稀疏矩阵存储,节省内存
实时性要求
用户刚浏览了商品,希望推荐结果能立即反映这个行为。
解决方案:
- 用户行为实时更新到缓存
- 对活跃用户使用更短的缓存时间
- 提供手动刷新接口,用户可以强制刷新推荐结果
评估指标选择
怎么衡量推荐效果好坏?准确率?召回率?还是用户点击率?
解决方案:
- 离线评估:使用准确率、召回率、NDCG等指标
- 在线评估:A/B测试,比较推荐系统的点击率、转化率
- 业务指标:关注推荐系统对GMV、用户留存的影响
实施结果
经过几周的折腾,推荐系统终于上线了。效果还算不错:
性能指标
- 推荐响应时间:平均150ms,满足<200ms的要求
- 缓存命中率:85%以上,大部分请求都能从缓存返回
- 资源占用:CPU使用率<30%,内存占用<2GB
业务效果
- 推荐商品点击率:提升15%
- 推荐商品转化率:提升8%
- 用户停留时长:增加12%
- 页面浏览量(PV):增加10%
用户反馈
收集了一些用户反馈,正面的包括:
- “推荐的商品确实是我感兴趣的”
- “有时候能发现一些不错的新商品”
- “比之前的随机推荐好多了”
负面反馈主要是:
- “有时候推荐的商品跟当前看的完全不相关”
- “推荐结果更新有点慢,刚浏览的商品没反映出来”
代码质量
- 单元测试覆盖率:80%
- 代码复杂度:控制在合理范围
- 文档完整:有API文档和使用说明
优化方向
虽然上线了,但还有很多可以优化的地方:
短期优化
- 增加更多行为数据:如搜索关键词、加入购物车等
- 优化冷启动策略:比如用用户注册信息做初步推荐
- 提升实时性:用户行为后推荐结果能立即更新
- 个性化调参:不同用户使用不同的推荐参数
中期优化
- 尝试其他算法:如基于深度学习的推荐模型
- 增加多目标优化:同时考虑点击率、转化率、多样性等
- 建立反馈机制:用户可以对推荐结果点赞/点踩
- A/B测试平台:快速验证新的推荐策略
长期规划
- 实时流处理:用Flink等工具处理实时数据流
- 分布式计算:用Spark等工具处理更大规模数据
- 深度学习模型:尝试用神经协同过滤等高级模型
- 跨域推荐:结合其他业务数据做跨域推荐
总结
这次做推荐系统的经历让我深刻体会到,学术上的算法和工业界的实践确实有很大差距。
学术论文里那些复杂的深度学习模型,在真实场景中往往很难落地。反而是那些简单、实用、可解释的方法,更容易产生价值。
推荐系统的核心不是算法有多复杂,而是:
- 理解业务需求:清楚要解决什么问题,达到什么目标
- 了解数据现状:知道有什么数据,数据质量如何
- 选择合适方案:在效果和成本间找到平衡点
- 持续优化迭代:根据反馈不断调整改进
对于我们这种中小型项目,简单有效的协同过滤+内容过滤的混合策略,可能比复杂的深度学习模型更合适。
希望这篇文章能给那些在类似场景下做推荐系统的同学一些参考。推荐系统这个领域还有很多值得探索的地方,下次有机会再分享更多实践经验。
参考资源
- 《推荐系统实践》项亮
- 《Recommender Systems Handbook》
- Surprise库:https://surpriselib.com/
- LightFM库:https://github.com/lyst/lightfm
版权声明: 本文首发于 指尖魔法屋-AI推荐系统:这次怎么落地的(https://blog.thinkmoon.cn/post/327-ai-recommendation-system-user-item-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。