跳转至

基于内容的推荐

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

基于内容的推荐

📖 章节导读

基于内容的推荐(Content-Based Recommendation)是推荐系统的重要方法之一,它利用物品的内容特征和用户的历史偏好进行推荐。本章将介绍内容推荐的基本原理、特征提取、相似度计算和实际应用。

🎯 学习目标

  • 理解基于内容推荐的基本原理
  • 掌握内容特征提取技术
  • 熟练使用相似度计算方法
  • 能够实现基于内容的推荐系统
  • 了解内容推荐的优缺点

4.1 基于内容推荐概述

4.1.1 基本原理

基于内容推荐的核心思想是:推荐与用户过去喜欢的物品相似的物品

步骤: 1. 提取物品的内容特征 2. 构建用户画像(基于用户历史偏好) 3. 计算物品与用户画像的相似度 4. 推荐相似度最高的物品

4.1.2 优缺点

优点: - 可解释性强:可以告诉用户推荐理由 - 冷启动问题相对容易解决:新物品只要有内容特征就可以推荐 - 不依赖其他用户的数据:适合小众物品

缺点: - 信息茧房:推荐结果过于单一 - 难以发现新兴趣:只能推荐与历史相似的物品 - 特征工程复杂:需要提取有效的特征

4.2 内容特征提取

4.2.1 文本特征提取

TF-IDF:

Python
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:

Python
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:

Python
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 图像特征提取

传统特征:

Python
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

深度特征:

Python
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 结构化特征

数值特征:

Python
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

类别特征:

Python
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 基于历史行为的画像

Python
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 基于兴趣标签的画像

Python
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 余弦相似度

Python
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 欧氏距离

Python
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 完整实现

Python
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 实战案例

案例:新闻推荐系统

Python
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})")

📝 本章小结

本章介绍了基于内容推荐的核心内容:

  1. ✅ 基于内容推荐的基本原理
  2. ✅ 内容特征提取技术
  3. ✅ 用户画像构建方法
  4. ✅ 相似度计算方法
  5. ✅ 实战案例

通过本章学习,你应该能够: - 理解基于内容推荐的工作原理 - 提取有效的物品特征 - 构建用户画像 - 实现基于内容的推荐系统

🔗 下一步

下一章我们将学习矩阵分解技术,这是推荐系统中最重要的技术之一。

继续学习: 05-矩阵分解技术.md

💡 思考题

  1. 基于内容的推荐和协同过滤各有什么优缺点?

    基于内容:无冷启动(新物品有特征即可推荐)、可解释性强、不依赖他人行为,但过度专业化(只推相似的)、特征提取质量影响大。协同过滤:能发现惊喜、无需物品特征,但冷启动严重、稀疏性问题。实践:内容解决冷启动、协同提升发现性。

  2. 如何提取有效的文本特征?

    传统:TF-IDF(关键词权重)、BM25(检索排序)。深度学习:BERT/Sentence-BERT(语义Embedding)、LLM生成标签。实践路线:标题+标签(TF-IDF) + 内容(BERT Embedding) + 实体提取(NER)。多模态:图像用CLIP提取视觉Embedding,视频抽帧+音频转文本。

  3. 如何构建高质量的用户画像?

    输入:行为数据(点击/购买序列) + 用户属性 + 上下文(时间/设备)。方法:①行为统计(各类目偏好度) ②Embedding(行为序列→Transformer编码) ③兴趣衰减(时间加权,近期行为杆更高) ④多兴趣建模(多向量Embedding表示多个兴趣簇)。

  4. 基于内容的推荐如何解决冷启动问题?

    这正是它的优势!新物品:提取内容特征即可推荐给偏好相似特征的用户。新用户:①注册引导选标签 ②基于用户属性(年龄/来源渠道)推荐 ③热门内容托底 ④少量行为后快速更新画像(Explore-Exploit)。

  5. 如何提高基于内容推荐的多样性?

    ①MMR(Maximal Marginal Relevance):在相关性和多样性之间平衡 ②Sub-modular函数优化(贪婪选择边际增益最大的物品) ③类目分散约束(强制推荐列表覆盖多个类别) ④探索注入(以一定比例推荐与用户历史不同的内容) ⑤支持用户多兴趣点→建多向量给用户画像。

📚 参考资料

  1. 《Recommender Systems Handbook》- Chapter 4
  2. 《推荐系统实践》- 第4章
  3. "Content-based Recommendation Systems" - Lops et al.
  4. Scikit-learn Documentation
  5. Gensim Documentation