基于内容的推荐¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
📖 章节导读¶
基于内容的推荐(Content-Based Recommendation)是推荐系统的重要方法之一,它利用物品的内容特征和用户的历史偏好进行推荐。本章将介绍内容推荐的基本原理、特征提取、相似度计算和实际应用。
🎯 学习目标¶
- 理解基于内容推荐的基本原理
- 掌握内容特征提取技术
- 熟练使用相似度计算方法
- 能够实现基于内容的推荐系统
- 了解内容推荐的优缺点
4.1 基于内容推荐概述¶
4.1.1 基本原理¶
基于内容推荐的核心思想是:推荐与用户过去喜欢的物品相似的物品。
步骤: 1. 提取物品的内容特征 2. 构建用户画像(基于用户历史偏好) 3. 计算物品与用户画像的相似度 4. 推荐相似度最高的物品
4.1.2 优缺点¶
优点: - 可解释性强:可以告诉用户推荐理由 - 冷启动问题相对容易解决:新物品只要有内容特征就可以推荐 - 不依赖其他用户的数据:适合小众物品
缺点: - 信息茧房:推荐结果过于单一 - 难以发现新兴趣:只能推荐与历史相似的物品 - 特征工程复杂:需要提取有效的特征
4.2 内容特征提取¶
4.2.1 文本特征提取¶
TF-IDF:
from sklearn.feature_extraction.text import TfidfVectorizer
def extract_text_features(texts):
"""
提取文本TF-IDF特征
"""
vectorizer = TfidfVectorizer(max_features=1000,
stop_words='english')
features = vectorizer.fit_transform(texts)
return features, vectorizer
Word Embeddings:
from gensim.models import Word2Vec
import numpy as np
def extract_word_embeddings(texts, vector_size=100):
"""
提取词向量特征
"""
# 分词
tokenized_texts = [text.split() for text in texts]
# 训练Word2Vec
model = Word2Vec(sentences=tokenized_texts,
vector_size=vector_size,
window=5,
min_count=1)
# 计算文档向量
doc_vectors = []
for tokens in tokenized_texts:
vectors = [model.wv[token] for token in tokens
if token in model.wv]
if vectors:
doc_vector = np.mean(vectors, axis=0)
else:
doc_vector = np.zeros(vector_size)
doc_vectors.append(doc_vector)
return np.array(doc_vectors), model # np.array创建NumPy数组
BERT Embeddings:
from transformers import BertTokenizer, BertModel
import torch
def extract_bert_features(texts, model_name='bert-base-chinese'):
"""
提取BERT特征
"""
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)
features = []
for text in texts:
inputs = tokenizer(text, return_tensors='pt',
truncation=True,
padding=True)
with torch.no_grad(): # 禁用梯度计算,节省内存
outputs = model(**inputs)
# 使用[CLS] token的表示
feature = outputs.last_hidden_state[:, 0, :].numpy()
features.append(feature[0])
return np.array(features)
4.2.2 图像特征提取¶
传统特征:
import cv2
import numpy as np
def extract_color_features(image_path):
"""
提取颜色直方图特征
"""
image = cv2.imread(image_path)
# 计算RGB直方图
hist_r = cv2.calcHist([image], [0], None, [256], [0, 256])
hist_g = cv2.calcHist([image], [1], None, [256], [0, 256])
hist_b = cv2.calcHist([image], [2], None, [256], [0, 256])
# 归一化
hist_r = hist_r / hist_r.sum()
hist_g = hist_g / hist_g.sum()
hist_b = hist_b / hist_b.sum()
# 拼接特征
feature = np.concatenate([hist_r.flatten(),
hist_g.flatten(),
hist_b.flatten()])
return feature
深度特征:
from torchvision import models, transforms
import torch
def extract_cnn_features(image_path, model_name='resnet50'):
"""
提取CNN特征
"""
# 加载预训练模型
if model_name == 'resnet50':
model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
elif model_name == 'vgg16':
model = models.vgg16(weights=models.VGG16_Weights.DEFAULT)
# 移除最后的分类层
model = torch.nn.Sequential(*list(model.children())[:-1])
model.eval() # eval()评估模式
# 图像预处理
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 加载图像
from PIL import Image
image = Image.open(image_path)
image_tensor = preprocess(image).unsqueeze(0) # unsqueeze增加一个维度
# 提取特征
with torch.no_grad():
features = model(image_tensor)
return features.squeeze().numpy() # squeeze压缩维度
4.2.3 结构化特征¶
数值特征:
import pandas as pd
def extract_numerical_features(items_df, numerical_columns=None):
"""
提取并归一化数值特征(需要在整个数据集上计算min/max)
items_df: DataFrame,包含所有物品
numerical_columns: 需要提取的数值列名列表
"""
if numerical_columns is None:
numerical_columns = ['price', 'rating', 'sales', 'reviews']
features = items_df[numerical_columns].copy()
# Min-Max归一化(在整个数据集上计算)
for col in numerical_columns:
col_min = features[col].min()
col_max = features[col].max()
if col_max - col_min > 0:
features[col] = (features[col] - col_min) / (col_max - col_min)
else:
features[col] = 0.0
return features
类别特征:
from sklearn.preprocessing import OneHotEncoder
def extract_categorical_features(items, categorical_columns):
"""
提取类别特征
"""
encoder = OneHotEncoder(sparse=False)
features = encoder.fit_transform(items[categorical_columns])
return features, encoder
4.3 用户画像构建¶
4.3.1 基于历史行为的画像¶
def build_user_profile(user_history, item_features):
"""
基于用户历史行为构建用户画像
"""
profile = np.zeros_like(item_features[0])
total_weight = 0
for item_id, behavior in user_history:
# 获取物品特征
item_feature = item_features[item_id]
# 根据行为类型计算权重
weight = get_behavior_weight(behavior['type'])
# 累加特征
profile += weight * item_feature
total_weight += weight
# 归一化
if total_weight > 0:
profile /= total_weight
return profile
4.3.2 基于兴趣标签的画像¶
def build_interest_profile(user_behaviors, item_tags):
"""
基于兴趣标签构建用户画像
"""
interest_scores = defaultdict(float) # defaultdict访问不存在的键时返回默认值
for item_id, behavior in user_behaviors:
# 获取物品标签
tags = item_tags.get(item_id, [])
# 根据行为类型计算权重
weight = get_behavior_weight(behavior['type'])
# 累加标签得分
for tag in tags:
interest_scores[tag] += weight
# 归一化
total = sum(interest_scores.values())
if total > 0:
interest_scores = {k: v/total for k, v in interest_scores.items()}
return interest_scores
4.4 相似度计算¶
4.4.1 余弦相似度¶
def cosine_similarity(vec1, vec2):
"""
计算余弦相似度
"""
dot_product = np.dot(vec1, vec2) # np.dot矩阵/向量点乘
norm1 = np.linalg.norm(vec1) # np.linalg线性代数运算
norm2 = np.linalg.norm(vec2)
if norm1 == 0 or norm2 == 0:
return 0
return dot_product / (norm1 * norm2)
4.4.2 欧氏距离¶
def euclidean_distance(vec1, vec2):
"""
计算欧氏距离
"""
return np.linalg.norm(vec1 - vec2)
def euclidean_similarity(vec1, vec2):
"""
将欧氏距离转换为相似度
"""
distance = euclidean_distance(vec1, vec2)
return 1 / (1 + distance)
4.5 完整实现¶
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
class ContentBasedRecommender:
def __init__(self):
self.item_features = None
self.user_profiles = None
self.vectorizer = None
def fit(self, items, user_histories):
"""
训练模型
"""
# 提取物品特征
self.item_features, self.vectorizer = self._extract_features(items)
# 构建用户画像
self.user_profiles = {}
for user_id, history in user_histories.items():
self.user_profiles[user_id] = self._build_profile(history)
def _extract_features(self, items):
"""
提取物品特征
"""
texts = [item['description'] for item in items]
vectorizer = TfidfVectorizer(max_features=1000)
features = vectorizer.fit_transform(texts)
return features.toarray(), vectorizer
def _build_profile(self, user_history):
"""
构建用户画像
"""
profile = np.zeros(self.item_features.shape[1])
total_weight = 0
for item_id, behavior in user_history:
item_feature = self.item_features[item_id]
weight = self._get_weight(behavior)
profile += weight * item_feature
total_weight += weight
if total_weight > 0:
profile /= total_weight
return profile
def _get_weight(self, behavior):
"""
获取行为权重
"""
weights = {
'purchase': 1.0,
'favorite': 0.8,
'share': 0.7,
'like': 0.5,
'view': 0.3,
'click': 0.1
}
return weights.get(behavior['type'], 0.1)
def recommend(self, user_id, n=10, exclude_seen=True):
"""
为用户推荐物品
"""
if user_id not in self.user_profiles:
return []
user_profile = self.user_profiles[user_id]
# 计算相似度
similarities = cosine_similarity([user_profile],
self.item_features)[0]
# 排序
item_scores = list(enumerate(similarities)) # enumerate同时获取索引和元素
item_scores.sort(key=lambda x: x[1], reverse=True) # lambda匿名函数
# 返回Top N
recommendations = [item_id for item_id, score in item_scores[:n]]
return recommendations
4.6 实战案例¶
案例:新闻推荐系统¶
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# 1. 加载数据
news = pd.read_csv('news_data.csv')
user_behaviors = pd.read_csv('user_behaviors.csv')
# 2. 提取新闻特征
vectorizer = TfidfVectorizer(max_features=1000, stop_words='english')
news_features = vectorizer.fit_transform(news['content']).toarray()
# 3. 构建用户画像
def build_user_profile(user_id):
"""
构建用户画像
"""
user_news = user_behaviors[user_behaviors['user_id'] == user_id]
profile = np.zeros(news_features.shape[1])
total_weight = 0
for _, row in user_news.iterrows():
news_id = row['news_id']
behavior_type = row['behavior_type']
news_feature = news_features[news_id]
weight = get_behavior_weight(behavior_type)
profile += weight * news_feature
total_weight += weight
if total_weight > 0:
profile /= total_weight
return profile
# 4. 推荐新闻
def recommend_news(user_id, n=10):
"""
推荐新闻
"""
user_profile = build_user_profile(user_id)
# 计算相似度
similarities = cosine_similarity([user_profile], news_features)[0]
# 排序
news_scores = list(enumerate(similarities))
news_scores.sort(key=lambda x: x[1], reverse=True)
# 返回Top N
recommendations = [{'news_id': news_id, 'score': score}
for news_id, score in news_scores[:n]]
return recommendations
# 5. 测试
user_id = 1
recommendations = recommend_news(user_id, n=5)
print(f"为用户{user_id}推荐的新闻:")
for rec in recommendations:
news_info = news.iloc[rec['news_id']]
print(f"- {news_info['title']} (相似度: {rec['score']:.4f})")
📝 本章小结¶
本章介绍了基于内容推荐的核心内容:
- ✅ 基于内容推荐的基本原理
- ✅ 内容特征提取技术
- ✅ 用户画像构建方法
- ✅ 相似度计算方法
- ✅ 实战案例
通过本章学习,你应该能够: - 理解基于内容推荐的工作原理 - 提取有效的物品特征 - 构建用户画像 - 实现基于内容的推荐系统
🔗 下一步¶
下一章我们将学习矩阵分解技术,这是推荐系统中最重要的技术之一。
继续学习: 05-矩阵分解技术.md
💡 思考题¶
-
基于内容的推荐和协同过滤各有什么优缺点?
基于内容:无冷启动(新物品有特征即可推荐)、可解释性强、不依赖他人行为,但过度专业化(只推相似的)、特征提取质量影响大。协同过滤:能发现惊喜、无需物品特征,但冷启动严重、稀疏性问题。实践:内容解决冷启动、协同提升发现性。
-
如何提取有效的文本特征?
传统:TF-IDF(关键词权重)、BM25(检索排序)。深度学习:BERT/Sentence-BERT(语义Embedding)、LLM生成标签。实践路线:标题+标签(TF-IDF) + 内容(BERT Embedding) + 实体提取(NER)。多模态:图像用CLIP提取视觉Embedding,视频抽帧+音频转文本。
-
如何构建高质量的用户画像?
输入:行为数据(点击/购买序列) + 用户属性 + 上下文(时间/设备)。方法:①行为统计(各类目偏好度) ②Embedding(行为序列→Transformer编码) ③兴趣衰减(时间加权,近期行为杆更高) ④多兴趣建模(多向量Embedding表示多个兴趣簇)。
-
基于内容的推荐如何解决冷启动问题?
这正是它的优势!新物品:提取内容特征即可推荐给偏好相似特征的用户。新用户:①注册引导选标签 ②基于用户属性(年龄/来源渠道)推荐 ③热门内容托底 ④少量行为后快速更新画像(Explore-Exploit)。
-
如何提高基于内容推荐的多样性?
①MMR(Maximal Marginal Relevance):在相关性和多样性之间平衡 ②Sub-modular函数优化(贪婪选择边际增益最大的物品) ③类目分散约束(强制推荐列表覆盖多个类别) ④探索注入(以一定比例推荐与用户历史不同的内容) ⑤支持用户多兴趣点→建多向量给用户画像。
📚 参考资料¶
- 《Recommender Systems Handbook》- Chapter 4
- 《推荐系统实践》- 第4章
- "Content-based Recommendation Systems" - Lops et al.
- Scikit-learn Documentation
- Gensim Documentation
