Skip to content

经典推荐算法与技术细节

在掌握了推荐系统的基本架构后,本章将深入探讨具体的推荐算法和技术实现细节,帮助你从理论走向实战。

1. 基于内容的推荐(Content-Based Recommendation)

1.1 核心思想

基于内容的推荐的核心原理是:给用户推荐之前喜欢的物品相似的物品

这包含两种常见方式:

  • U2I2I:用户→物品→物品

    1. 找用户喜欢的历史物品
    2. 找与这些物品相似的物品
    3. 推荐给用户
  • U2TAG2I:用户→标签→物品

    1. 提取用户的兴趣标签
    2. 找带有这些标签的物品
    3. 推荐给用户

1.2 用户向量与物品向量

1.2.1 向量表示

将用户和物品都表示为向量,通过计算向量相似度来衡量匹配程度。

用户向量:代表用户的兴趣偏好 物品向量:代表物品的特征属性

1.2.2 相似度计算方法

余弦相似度(最常用)

python
def cosine_similarity(vec1, vec2):
    dot_product = np.dot(vec1, vec2)
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    return dot_product / (norm1 * norm2)

欧氏距离

python
def euclidean_distance(vec1, vec2):
    return np.sqrt(np.sum((vec1 - vec2) ** 2))

皮尔逊相关系数

适用于处理评分数据,考虑用户评分习惯的差异。

1.3 基于内容推荐的优缺点

优点

  • ✅ 可解释性强,用户容易理解
  • ✅ 不依赖用户行为数据,冷启动问题较少
  • ✅ 推荐结果稳定,不会频繁变化

缺点

  • ❌ 很难发现用户未接触过的新类型物品
  • ❌ 物品特征需要人工标注或提取
  • ❌ 存在信息茧房风险,推荐范围窄

1.4 实际应用场景

  • 电商:相似商品推荐("看了这个的人还看了")
  • 视频:相似视频推荐("你可能也喜欢")
  • 音乐:相似歌曲推荐("猜你喜欢")

2. 基于协同过滤的推荐(Collaborative Filtering)

协同过滤是推荐系统中最经典、应用最广泛的算法,其核心思想是:根据群体的智慧进行推荐

2.1 核心概念

协同过滤假设:如果用户 A 和用户 B 过去的行为相似,那么他们对物品的偏好也相似;反之,如果物品 A 和物品 B 经常被相同的用户喜欢,那么这两个物品也相似。

2.2 基于记忆的协同过滤(Memory-Based CF)

2.2.1 User-based CF(基于用户的协同过滤)

原理:U2U2I - 和你兴趣相投的人也喜欢 XXX

流程

  1. 计算用户之间的相似度
  2. 找到与目标用户最相似的 K 个用户
  3. 找这些相似用户喜欢但目标用户未互动的物品
  4. 根据相似度加权,生成推荐列表

相似度计算

  • 余弦相似度
  • 皮尔逊相关系数
  • Jaccard 相似度

示例

用户 A 喜欢的物品:[物品1, 物品2, 物品3]
用户 B 喜欢的物品:[物品1, 物品2, 物品4]
用户 C 喜欢的物品:[物品1, 物品5]

给用户 C 推荐:
- 与用户 C 相似的用户是 A(都喜欢物品1)
- 用户 A 喜欢但用户 C 未看过的:物品2, 物品3
- 推荐:[物品2, 物品3]

2.2.2 Item-based CF(基于物品的协同过滤)

原理:U2I2I - 喜欢这个物品的人也喜欢 XXX

流程

  1. 计算物品之间的相似度
  2. 找到用户喜欢物品的相似物品
  3. 根据相似度加权,生成推荐列表

相似度计算

  • 基于用户共现度
  • 基于评分相关性

示例

物品1 被喜欢的人群:[用户A, 用户B, 用户C]
物品2 被喜欢的人群:[用户A, 用户B, 用户D]
物品3 被喜欢的人群:[用户A, 用户E]

用户 A 喜欢物品1,推荐:
- 与物品1 相似的物品是物品2(共现用户:A, B)
- 推荐:物品2

2.3 基于模型的协同过滤(Model-Based CF)

基于模型的方法通过机器学习算法构建用户-物品交互的数学模型。

2.3.1 矩阵分解(Matrix Factorization)

核心思想:将用户-物品评分矩阵分解为两个低维矩阵的乘积

$$R \approx U \times V^T$$

其中:

  • R:用户-物品评分矩阵(稀疏)
  • U:用户隐含因子矩阵
  • V:物品隐含因子矩阵

优势

  • 可以处理稀疏数据
  • 捕捉潜在特征
  • 预测缺失值

算法

  • SVD(Singular Value Decomposition)
  • ALS(Alternating Least Squares)
  • NMF(Non-negative Matrix Factorization)

2.3.2 深度学习模型

Neural Collaborative Filtering (NCF)

  • 用神经网络替代传统矩阵分解
  • 学习用户和物品的非线性交互

Deep & Wide

  • Wide 层:记忆能力,学习历史特征组合
  • Deep 层:泛化能力,学习潜在特征

2.4 协同过滤的优缺点

优点

  • ✅ 不需要物品特征信息
  • ✅ 可以发现用户潜在兴趣
  • ✅ 推荐新颖性和多样性好

缺点

  • ❌ 冷启动问题:新用户/新物品无法推荐
  • ❌ 数据稀疏性:用户行为稀疏时效果差
  • ❌ 热门偏向:热门物品被过度推荐

2.5 实际应用

  • 电商:个性化商品推荐
  • 视频:视频推荐
  • 音乐:音乐推荐

3. 多路召回融合策略

在实际推荐系统中,往往采用多路召回策略,然后对召回结果进行融合排序。

3.1 融合策略对比

3.1.1 按顺序展示

方法:召回1 结果 → 召回2 结果 → 召回3 结果...

优点:实现简单

缺点

  • 早期召回占主导
  • 后期召回难以曝光

适用场景:召回效果差异大,有明确优先级

3.1.2 平均法(Averaging)

方法:对所有召回的结果列表取平均

优点:公平对待所有召回

缺点

  • 忽略召回质量差异
  • 简单平均可能不合理

3.1.3 加权投票(Weighted Voting)

方法:根据召回质量设置权重

$$Score_{final} = \sum_{i=1}^{n} w_i \times Score_i$$

其中:

  • $w_i$:第 i 个召回路径的权重
  • $Score_i$:第 i 个召回路径的打分

设置权重原则

  • 质量高的召回路径权重高
  • 热门召回路径权重适中
  • 长尾召回路径权重较低

3.1.4 动态加权法(Dynamic Weighting)

方法:根据用户特征、物品特征动态调整权重

示例

python
def dynamic_weight(user_id, item_features):
    weights = {
        'cf': 0.5,      # 协同过滤
        'content': 0.3, # 内容推荐
        'hot': 0.2      # 热门推荐
    }
    
    # 新用户降低协同过滤权重
    if is_new_user(user_id):
        weights['cf'] = 0.2
        weights['content'] = 0.5
        weights['hot'] = 0.3
    
    return weights

3.1.5 机器学习权重法(ML-based Weighting)

方法:使用机器学习模型自动学习召回路径权重

流程

  1. 收集用户历史交互数据
  2. 标记每个物品的召回来源
  3. 训练模型预测用户点击概率
  4. 模型输出即为权重

优势

  • 自动优化权重
  • 适应数据变化

缺点

  • 需要大量标注数据
  • 训练成本高

3.2 融合策略选择建议

策略实现难度效果适用场景
按顺序展示⭐⭐召回差异大
平均法⭐⭐⭐⭐⭐快速上线
加权投票⭐⭐⭐⭐⭐⭐⭐稳定期
动态加权⭐⭐⭐⭐⭐⭐⭐⭐⭐个性化强
机器学习⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐大规模系统

3.3 成本考虑

如果召回路径质量接近,按照成本选择:

  • 实时计算(如协同过滤):成本低,权重可设高
  • 离线计算(如深度学习模型):成本高,权重可设低
  • 第三方接口(如 API):成本高,权重需谨慎设置

4. AB 测试系统设计与实现

AB 测试是推荐系统优化的核心工具,通过科学对比不同策略的效果,选择最优方案。

4.1 AB 测试基本流程

4.1.1 系统架构

用户请求

AB 测试服务

根据用户ID 分桶(分桶A / 分桶B)

访问后端推荐服务

返回对应分桶的推荐结果

用户交互(展现、点击、购买)

日志记录(带分桶标志)

数据分析

结果对比

全量部署获胜分桶

4.1.2 分桶算法

最常用的是哈希取模

python
def get_bucket(user_id, bucket_count=10):
    """
    将用户分配到指定数量的分桶中
    """
    hash_value = hash(user_id)
    return hash_value % bucket_count

注意事项

  • 同一用户必须始终分配到同一分桶
  • 分桶分布应尽量均匀
  • 新用户/老用户可以分流策略

4.2 AB 测试实施步骤

4.2.1 开发两个策略分支

开发同一个接口的两个分支:

  • 分桶 A:使用旧策略(Baseline)
  • 分桶 B:使用新策略(Experiment)

4.2.2 配置实验分流

在 AB 测试配置中设置分流比例:

json
{
  "experiment_id": "exp_001",
  "bucket_allocation": {
    "A": 0.6,  // 60% 流量
    "B": 0.4   // 40% 流量
  },
  "status": "running"
}

4.2.3 用户请求处理

1. 用户访问推荐接口
2. 请求携带用户ID
3. AB 测试服务根据用户ID计算分桶
4. 返回分桶结果:{user_id: "123456", bucket: "A"}
5. 根据分桶选择对应的策略返回推荐结果

4.2.4 日志收集

所有用户交互日志都带上分桶标志:

json
{
  "user_id": "123456",
  "bucket": "A",
  "event_type": "click",
  "item_id": "item_001",
  "timestamp": 1640000000
}

关键指标

  • 展现(Impression):物品展示次数
  • 点击(Click):用户点击次数
  • 购买(Purchase):用户购买次数
  • CTR(Click-Through Rate):点击率 = 点击 / 展现
  • CVR(Conversion Rate):转化率 = 购买 / 点击
  • GMV(Gross Merchandise Value):交易总额

4.2.5 结果分析

A/B 分桶对比表

指标分桶 A(Baseline)分桶 B(Experiment)提升幅度
展现量100,000100,0000%
点击量5,0005,500+10%
CTR5%5.5%+0.5pp
购买量500600+20%
CVR10%10.9%+0.9pp
GMV$50,000$60,000+20%

4.2.6 统计显著性检验

不能只看绝对值,需要进行统计检验:

  • t 检验:检验均值差异是否显著
  • 卡方检验:检验比率差异是否显著
  • 置信区间:计算差异的 95% 置信区间

判断标准

  • P值 < 0.05:差异显著
  • P值 ≥ 0.05:差异不显著,可能是随机波动

4.3 实验优化与全量部署

4.3.1 调整流量

如果分桶 B 表现更好:

  • 调大分桶 B 的流量比例(如 60% → 80%)
  • 继续观察,确认稳定性

4.3.2 全量部署

如果分桶 B 持续优于 A:

  1. 将分桶 B 流量调整到 100%
  2. 停止分桶 A
  3. 分桶 B 成为新的 Baseline
  4. 开始下一轮实验

4.4 AB 测试最佳实践

✅ 应该做的

  • 实验周期足够长(至少 7 天)
  • 控制其他变量(如流量入口)
  • 多指标综合评估
  • 统计显著性检验

❌ 不应该做的

  • 实验周期过短(如 1 天)
  • 忽略统计显著性
  • 只看单一指标
  • 数据窥视(提前终止实验)

5. 内容召回全流程实现

内容召回是推荐系统中最基础、最常用的召回方式之一。本节将详细讲解从内容采集到最终服务的完整实现流程。

5.1 核心目标

主要目标:计算物品与其它最相似的物品列表

应用场景

  • 相似推荐:商品详情页的"相似商品"
  • 扩展推荐:基于用户历史物品找相似物
  • 替代推荐:当某个物品不可用时推荐替代品

5.2 完整流程图

内容采集

中文分词 & 关键词提取

向量化表示(Word2Vec / Doc2Vec)

TopN 相似近邻搜索(余弦相似度)

存储(Redis)

Web 服务(Flask / Java)

返回相似物品列表

5.3 详细实现步骤

5.3.1 内容采集

采集的物品信息

字段说明示例
ID物品唯一标识"item_001"
title标题"Python 编程从入门到实践"
description详细描述"本书是 Python 入门经典..."
tags标签["编程", "Python", "入门"]
category分类"计算机"

数据来源

  • 数据库查询(MySQL / MongoDB)
  • 爬虫采集
  • API 接口
  • 手动标注

5.3.2 中文分词 & 关键词提取

工具:jieba 分词

python
import jieba
import jieba.analyse

# 加载自定义词典
jieba.load_userdict("custom_dict.txt")

# 分词
text = "Python 编程从入门到实践"
words = jieba.lcut(text)
# 输出:['Python', '编程', '从', '入门', '到', '实践']

# 关键词提取
keywords = jieba.analyse.extract_tags(text, topK=10, withWeight=True)
# 输出:[('编程', 0.5), ('Python', 0.4), ('入门', 0.3), ...]

关键词提取算法

  • TF-IDF:关键词重要性
  • TextRank:基于图的关键词排序
  • 自定义词典:人工标注的关键词

5.3.3 向量化表示

将文本转换为数值向量,便于计算相似度。

Word2Vec

核心思想:将词语映射到高维向量空间,相似的词在向量空间中距离较近。

工具

  • gensim(Python)
  • Spark Word2Vec(分布式)
  • 腾讯 Word2Vec(预训练模型)

示例

python
from gensim.models import Word2Vec

# 训练 Word2Vec 模型
sentences = [
    ["编程", "Python", "入门", "实践"],
    ["编程", "Java", "进阶", "实战"],
    ["编程", "JavaScript", "前端", "开发"]
]

model = Word2Vec(sentences, vector_size=100, window=5, min_count=1, workers=4)

# 获取词向量
vector = model.wv["Python"]

# 计算相似词
similar_words = model.wv.most_similar("编程")
# 输出:[('开发', 0.9), ('实战', 0.8), ...]
Doc2Vec

核心思想:将整个文档映射到向量空间,适用于文档相似度计算。

示例

python
from gensim.models.doc2vec import Doc2Vec, TaggedDocument

# 准备文档
documents = [
    TaggedDocument(words=["Python", "编程", "入门"], tags=["doc1"]),
    TaggedDocument(words=["Java", "编程", "进阶"], tags=["doc2"]),
    TaggedDocument(words=["JavaScript", "前端", "开发"], tags=["doc3"])
]

# 训练模型
model = Doc2Vec(documents, vector_size=100, window=5, min_count=1, workers=4)

# 获取文档向量
vector = model.dv["doc1"]

# 计算相似文档
similar_docs = model.dv.most_similar("doc1")
向量聚合(平均 / 加权平均)

对于长文本,可以将词向量聚合为文档向量:

python
import numpy as np

def document_to_vector(text, model):
    words = jieba.lcut(text)
    vectors = [model.wv[word] for word in words if word in model.wv]
    
    # 平均
    doc_vector = np.mean(vectors, axis=0)
    
    # 加权平均(使用 TF-IDF 权重)
    # doc_vector = np.average(vectors, axis=0, weights=weights)
    
    return doc_vector

5.3.4 TopN 相似近邻搜索

余弦相似度(最常用)
python
from scipy.spatial.distance import cosine

def cosine_similarity(vec1, vec2):
    return 1 - cosine(vec1, vec2)

# 计算物品 A 和物品 B 的相似度
similarity = cosine_similarity(vector_a, vector_b)
LSH 局部敏感哈希(大规模场景)

问题:如果有 100 万物品,两两计算相似度需要 5 万亿次计算,性能无法接受。

LSH 解决方案

  • 将相似物品哈希到同一个桶中
  • 只计算同桶内的物品相似度
  • 大幅减少计算量

工具

  • datasketch(Python)
  • Spark LSH
python
from datasketch import MinHashLSH

# 创建 LSH 索引
lsh = MinHashLSH(threshold=0.5, num_perm=128)

# 添加物品
lsh.insert("item1", minhash1)
lsh.insert("item2", minhash2)
lsh.insert("item3", minhash3)

# 查询相似物品
result = lsh.query(minhash1)
# 输出:["item1", "item2"]  # item2 与 item1 相似

5.3.5 存储与服务

Redis 存储设计

数据结构:Hash

Key: item:{item_id}
Field: similar_items
Value: [{"item_id": "item2", "similarity": 0.9}, {"item_id": "item3", "similarity": 0.8}, ...]

Redis 命令

bash
# 存储相似物品列表
HSET item:item1 similar_items '[{"item_id":"item2","similarity":0.9},...]'

# 查询相似物品
HGET item:item1 similar_items
Web 服务实现(Flask)
python
from flask import Flask, jsonify
import redis

app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, db=0)

@app.route('/recommend/similar/<item_id>')
def get_similar_items(item_id):
    # 从 Redis 获取相似物品
    similar_items_json = r.hget(f'item:{item_id}', 'similar_items')
    
    if similar_items_json is None:
        return jsonify({"error": "Item not found"}), 404
    
    similar_items = json.loads(similar_items_json)
    
    return jsonify({
        "item_id": item_id,
        "similar_items": similar_items
    })

if __name__ == '__main__':
    app.run(port=5000)
Web 服务实现(Java Spring Boot)
java
@RestController
@RequestMapping("/recommend")
public class RecommendController {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @GetMapping("/similar/{itemId}")
    public ResponseEntity<?> getSimilarItems(@PathVariable String itemId) {
        String key = "item:" + itemId;
        String json = redisTemplate.opsForHash().get(key, "similar_items");
        
        if (json == null) {
            return ResponseEntity.status(404).body("Item not found");
        }
        
        ObjectMapper mapper = new ObjectMapper();
        List<SimilarItem> similarItems = mapper.readValue(json, new TypeReference<List<SimilarItem>>(){});
        
        Map<String, Object> response = new HashMap<>();
        response.put("item_id", itemId);
        response.put("similar_items", similarItems);
        
        return ResponseEntity.ok(response);
    }
}

5.6 完整代码示例

python
import jieba
import jieba.analyse
import numpy as np
from gensim.models import Word2Vec
from scipy.spatial.distance import cosine
import redis
import json

# 1. 加载 Word2Vec 模型
model = Word2Vec.load("word2vec.model")

# 2. 文本向量化
def text_to_vector(text):
    words = jieba.lcut(text)
    vectors = [model.wv[word] for word in words if word in model.wv]
    if not vectors:
        return np.zeros(model.vector_size)
    return np.mean(vectors, axis=0)

# 3. 计算相似度
def calculate_similarity(vec1, vec2):
    return 1 - cosine(vec1, vec2)

# 4. 构建相似物品索引
def build_similarity_index(items):
    redis_client = redis.Redis(host='localhost', port=6379, db=0)
    
    for item in items:
        item_id = item['id']
        item_text = item['title'] + ' ' + item['description']
        item_vector = text_to_vector(item_text)
        
        # 查找最相似的 Top10 物品
        similarities = []
        for other_item in items:
            if other_item['id'] == item_id:
                continue
            other_text = other_item['title'] + ' ' + other_item['description']
            other_vector = text_to_vector(other_text)
            similarity = calculate_similarity(item_vector, other_vector)
            similarities.append({
                'item_id': other_item['id'],
                'similarity': similarity
            })
        
        # 排序并取 Top10
        similarities.sort(key=lambda x: x['similarity'], reverse=True)
        top10 = similarities[:10]
        
        # 存储到 Redis
        redis_client.hset(f'item:{item_id}', 'similar_items', json.dumps(top10))
    
    print("Similarity index built successfully!")

# 5. 查询相似物品
def get_similar_items(item_id):
    redis_client = redis.Redis(host='localhost', port=6379, db=0)
    json_str = redis_client.hget(f'item:{item_id}', 'similar_items')
    
    if json_str:
        return json.loads(json_str)
    else:
        return []

5.7 性能优化建议

✅ 优化建议

  1. 批量计算:离线批量计算相似度,避免在线计算
  2. 缓存热点:Redis 缓存热门物品的相似列表
  3. 增量更新:新物品增量计算相似度,而非全量重算
  4. LSH 加速:大规模场景使用 LSH 减少计算量
  5. 向量压缩:使用 PCA 降维或量化压缩向量

⏱️ 性能指标

  • 召回时间:< 50ms(从 Redis 读取)
  • 离线计算:百万级物品 < 1 小时
  • 增量更新:新增物品 < 10s

6. 总结

本章详细介绍了推荐系统的经典算法与技术细节,包括:

  1. 基于内容的推荐:通过计算用户向量和物品向量的相似度来推荐
  2. 协同过滤推荐
    • 基于记忆的方法(User-based、Item-based)
    • 基于模型的方法(矩阵分解、深度学习)
  3. 多路召回融合:按顺序、平均、加权、动态、机器学习等策略
  4. AB 测试:科学评估推荐效果的完整流程
  5. 内容召回全流程:从内容采集到 Web 服务的完整实现

通过这两章的学习,你已经掌握了推荐系统的基础架构和核心算法,可以开始动手实践了!


💡 下一步建议

  • 动手实现一个简单的推荐系统(如电影推荐)
  • 学习深度学习推荐模型(如 Wide & Deep、DeepFM)
  • 关注工业级推荐系统的性能优化和工程实践

MIT