协同过滤算法实战:从原理到代码实现与性能优化
1. 协同过滤算法入门:从生活场景到数学原理
第一次听说协同过滤这个词时,我正坐在咖啡馆里看朋友刷购物APP。他突然抬头问我:"你说这APP怎么知道我想买登山杖?我从来没搜过啊。"这个看似神奇的推荐背后,很可能就是协同过滤在发挥作用。
简单来说,协同过滤就像一群口味相似的朋友互相安利好物。算法通过分析大量用户的行为数据,发现"喜欢A的人也喜欢B"的规律。比如在豆瓣电影,给《星际穿越》打高分的人往往也喜欢《盗梦空间》,系统就会把后者推荐给只看过前者的用户。
核心思想其实很直观:用户的行为会留下痕迹,这些痕迹之间存在隐藏的关联。算法要做的是挖掘三种关键关系:
- 用户与用户之间的相似度(UserCF)
- 物品与物品之间的相似度(ItemCF)
- 用户与物品之间的交互关系
我最早在电商平台实践时,用到的评分矩阵长这样:
| 用户\电影 | 电影A | 电影B | 电影C |
|---|---|---|---|
| 用户1 | 5 | 3 | 0 |
| 用户2 | 4 | 0 | 2 |
| 用户3 | 1 | 4 | 5 |
表格里的0表示未评分。要预测用户1对电影C的评分,UserCF会这样做:
- 计算用户1与其他用户的相似度
- 找出最相似的K个用户
- 用这些用户对电影C的评分加权平均
相似度计算就像比较两个人的购物车重合度。数学上有两个经典方法:
- 余弦相似度:计算向量夹角的余弦值
from sklearn.metrics.pairwise import cosine_similarity cosine_similarity([5,3,0], [4,0,2]) # 输出0.73- 皮尔逊系数:考虑评分偏置的改进版
from scipy.stats import pearsonr pearsonr([5,3,0], [4,0,2])[0] # 输出0.66实际项目中我发现,当用户评分尺度差异大时(比如有人习惯打1-5分,有人只用4-5分),皮尔逊系数效果更好。这就像比较两个美食家,一个给普通餐馆打3分,米其林打5分;另一个觉得3分就算好吃,5分是惊艳。皮尔逊系数能消除这种个人标准差异。
2. UserCF实战:用Python实现电影推荐
去年给本地影院做推荐系统时,我用了MovieLens数据集练手。这个经典数据集包含10万条电影评分,非常适合练手。先看看数据长什么样:
import pandas as pd ratings = pd.read_csv('ratings.csv') movies = pd.read_csv('movies.csv') print(ratings.head())输出结果:
userId movieId rating timestamp 0 1 1 4.0 964982703 1 1 3 4.0 964981247 2 1 6 4.0 964982224 3 1 47 5.0 964983815 4 1 50 5.0 964982931第一步:构建共现矩阵这里有个坑要注意——不是所有用户都评价过相同电影,直接计算会导致维度不一致。我的解决办法是用pivot_table:
rating_matrix = ratings.pivot_table( index='userId', columns='movieId', values='rating' ).fillna(0)第二步:计算用户相似度直接计算全量用户相似度太耗内存,我改用KNN只保留TopN相似用户:
from sklearn.neighbors import NearestNeighbors knn = NearestNeighbors(metric='cosine', algorithm='brute') knn.fit(rating_matrix)第三步:预测评分对于目标用户,找到相似用户后,用加权平均预测评分:
def predict_rating(user_id, movie_id, n_neighbors=5): distances, indices = knn.kneighbors( rating_matrix.loc[user_id].values.reshape(1,-1), n_neighbors=n_neighbors+1 ) neighbor_ratings = rating_matrix.iloc[indices[0][1:], movie_id] return neighbor_ratings.mean()实际跑下来发现几个问题:
- 冷启动用户(新用户)无法处理
- 计算耗时随用户量增长而剧增
- 当电影数量达到万级时,用户共同评分过的电影可能只有个位数
后来优化时,我加入了评分时间衰减因子——最近3个月的评分权重更高。这就像你更相信朋友最近的安利,而不是他五年前推荐的手机型号。
3. ItemCF实战:电商商品推荐优化
在跨境电商项目里,我放弃了UserCF转向ItemCF。原因很现实:我们有2000万用户但只有10万商品,UserCF的相似度矩阵会大到内存爆炸。
ItemCF的优势在于:
- 商品数量通常远少于用户数
- 商品相似度更稳定(不像用户兴趣变化快)
- 可解释性强("买了牙膏的人也会买牙刷"比"和你相似的人买了牙刷"更有说服力)
实现步骤有所不同:
- 计算物品共现矩阵
item_sim_matrix = rating_matrix.corr(method='pearson')- 生成推荐列表
def recommend_items(user_id, top_k=10): user_ratings = rating_matrix.loc[user_id] # 找出用户评分高的物品 high_rated_items = user_ratings[user_ratings > 3].index # 根据相似度矩阵找到相似物品 similar_items = item_sim_matrix[high_rated_items].mean(axis=0) # 过滤已评分的 similar_items = similar_items.drop(user_ratings[user_ratings > 0].index) return similar_items.sort_values(ascending=False)[:top_k]实际部署时,我做了三点重要优化:
- 滑动时间窗口:只计算最近3个月的行为数据,让推荐更时效性
- 热门商品降权:避免总是推荐爆款,用TF-IDF思想处理相似度
- 多维度融合:加入类目相似度(同品类商品更可能相关)
有个有趣的发现:在美妆品类,ItemCF准确率比UserCF高23%,但在图书品类差异不大。后来分析发现,美妆用户常跨品类购买(如同时买护肤品和化妆品),而图书读者往往专注特定类型。
4. 性能优化:从单机到分布式的演进
当数据量超过百万级时,我在笔记本上跑的Python脚本直接卡死。后来逐步摸索出一套优化方案:
第一阶段:算法层优化
- 稀疏矩阵存储:用scipy.sparse节省内存
from scipy.sparse import csr_matrix sparse_matrix = csr_matrix(rating_matrix.values)- 近似最近邻(ANN):用Spotify的Annoy库加速
from annoy import AnnoyIndex annoy_index = AnnoyIndex(n_features, 'angular')第二阶段:工程化改造
- 定时离线计算:每晚更新相似度矩阵
- 分片处理:按用户/商品ID哈希分片
- 缓存热点数据:用Redis缓存Top100商品相似度
第三阶段:Spark分布式最终方案迁移到PySpark:
from pyspark.ml.recommendation import ALS als = ALS( rank=10, maxIter=5, regParam=0.01, userCol="userId", itemCol="movieId", ratingCol="rating" ) model = als.fit(ratings)在千万级数据测试中,Spark比单机快60倍。但要注意几个坑:
- 数据倾斜问题:少数热门商品会导致计算卡住
- 参数调优:rank值过大容易过拟合
- 冷启动处理:需要混合内容推荐策略
有一次线上事故让我记忆犹新:春节促销期间,推荐系统突然响应变慢。查日志发现是某个爆款商品被30%用户点击,导致ItemCF计算时数据倾斜。临时方案是对该商品做采样处理,长期则改为分段相似度计算。
5. 效果评估与AB测试
推荐系统不能只追求技术先进,关键要看业务指标。我们建立了完整的评估体系:
离线指标
- 准确率:RMSE、MAE
- 覆盖率:推荐商品占全集比例
- 多样性:推荐列表的品类分布
在线指标
- CTR(点击通过率)
- 转化率
- 客单价变化
AB测试时发现一个反直觉现象:在ItemCF中加入购买时间衰减因子后,离线指标提升但线上转化率下降。分析后发现,母婴用品这类周期性消费品,两年前购买过的用户现在可能仍有需求。于是改为按品类设置不同衰减周期。
评估代码示例:
from surprise import Dataset, accuracy from surprise.model_selection import train_test_split data = Dataset.load_builtin('ml-100k') trainset, testset = train_test_split(data, test_size=0.2) predictions = algo.test(testset) accuracy.rmse(predictions) # 计算RMSE最近尝试的改进方向是融合用户画像:将协同过滤的推荐结果与用户 demographic 特征结合。比如给年轻男性推荐游戏周边时,适当降低办公用品的推荐权重。这种混合策略在测试中使CTR提升了15%。
