三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于用户行为数据的风格偏好建模与个性化推荐系统实战

基于用户行为数据的风格偏好建模与个性化推荐系统实战

最近在开发一个社区类应用时,遇到了一个很有意思的需求:用户希望系统能根据他们的浏览和互动行为,自动推荐符合其“口味”的内容。这让我深入研究了如何量化并应用“风格偏好”这一抽象概念。本文将围绕如何通过技术手段识别、建模和应用用户偏好的“风格”,分享一套从数据采集、特征工程到算法应用的全链路实战方案。无论你是想为产品增加个性化推荐,还是单纯对用户画像和机器学习应用感兴趣,这篇文章都能提供可直接复用的代码和清晰的实现思路。

1. 风格偏好:从抽象概念到可计算特征

“风格”是一个宽泛且主观的概念,在技术实现中,我们需要将其拆解为一系列可观测、可量化的数据指标。简单来说,我们的目标是将用户一句感性的“我超喜欢这种风格”,转化为机器可以理解和计算的向量或标签。

1.1 什么是可计算的“风格”?

在不同的业务场景下,“风格”的指代截然不同,但其技术内核是相通的:通过用户的行为数据,抽象出其在内容选择上的稳定模式或倾向

  • 内容社区/资讯平台:“风格”可能指内容主题(如科技、娱乐)、表达形式(如长文、短视频、图文)、情感倾向(如幽默、严肃)或发布者类型。
  • 电商平台:“风格”可能指商品的设计风格(如简约、复古)、品牌、价位段或材质。
  • 音乐/视频平台:“风格”则直接对应具体的流派(如流行、摇滚、古典)、语种、年代或创作者。

无论哪种场景,技术落地的第一步都是定义风格的维度。我们不能让算法去理解“风格”这个词,而是告诉算法,我们关注内容的哪些属性。

1.2 核心数据源:用户行为日志

用户不会直接告诉我们他喜欢什么风格,但他的行为会“说话”。我们需要埋点收集以下关键行为数据:

  1. 曝光行为:用户看到了哪些内容(content_id)。这是负样本的重要来源。
  2. 点击行为:用户点击了哪些内容。这是最直接的兴趣表达。
  3. 互动行为:点赞、收藏、评论、分享、完播/完读。这些行为的权重通常高于简单的点击。
  4. 负反馈行为:不感兴趣、拉黑、快速划过。这对于优化推荐结果、避免信息茧房至关重要。
  5. 上下文信息:行为发生的时间、地理位置、设备、网络环境等。这些信息有助于理解偏好的场景依赖性。

这些行为数据通常以日志的形式记录,每条日志可能包含:user_id,content_id,event_type(click, like, etc.),timestamp,device_info,page_location等字段。

1.3 特征工程:构建内容和用户向量

有了原始数据,我们需要将其转化为机器学习的特征。这分为两大块:内容侧特征用户侧特征

内容侧特征(Item Features):描述内容本身的属性。

  • 结构化特征:类别、标签、作者、发布时间、字数/时长等。这些可以直接作为类别特征或数值特征。
  • 非结构化特征:文本内容、封面图、音频/视频帧。这些需要通过NLP(如BERT、Word2Vec)或CV(如ResNet)模型提取为稠密向量(Embedding)。
  • 统计特征:历史点击率、平均阅读时长、互动率等。这些反映了内容的受欢迎程度。

用户侧特征(User Features):描述用户的属性及其历史兴趣。

  • 静态特征:人口统计学信息(如注册时填写的年龄、性别、地域),但这些信息可能不全或不准确。
  • 动态兴趣特征:这是核心。通过聚合用户历史交互过的内容特征来得到。例如:
    • 加权平均池化(Weighted Average Pooling):将用户点击过的所有内容的Embedding向量,按照互动类型(如收藏权重为2,点击权重为1)进行加权平均,得到一个代表用户兴趣的向量。
    • 序列建模:使用GRU、Transformer等模型,将用户的行为序列(按时间排序的content_id或内容向量)进行编码,输出一个表征用户当前兴趣状态的向量。这对捕捉兴趣演化非常有效。

2. 环境准备与工具选型

在开始动手之前,我们需要搭建一个可以进行数据处理、模型训练和评估的轻量级环境。本项目以Python为核心,利用其丰富的数据科学和机器学习库。

2.1 基础环境与版本说明

  • 操作系统:Linux / macOS / Windows (WSL2推荐)
  • Python:3.8 或 3.9(本文示例基于3.9)
  • 包管理pipconda

核心Python库

  • 数据处理pandas,numpy
  • 机器学习框架scikit-learn(用于传统模型和评估)
  • 深度学习框架torch(PyTorch) 或tensorflow(本文示例使用PyTorch)
  • 自然语言处理transformers(Hugging Face,用于文本特征提取)
  • 可视化matplotlib,seaborn(用于分析数据分布和模型效果)

2.2 项目初始化与依赖安装

创建一个新的项目目录,并初始化虚拟环境。

# 创建项目目录 mkdir style_preference_recommendation cd style_preference_recommendation # 创建虚拟环境 (以 conda 为例) conda create -n style_rec python=3.9 conda activate style_rec # 安装核心依赖 pip install pandas numpy scikit-learn matplotlib seaborn jupyter pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu # CPU版本,根据实际情况选择CUDA版本 pip install transformers

2.3 示例数据结构

为了便于演示,我们模拟一个简单的数据集。假设我们有一个文章推荐场景,每篇文章有类别和关键词,用户有点击行为。

我们创建以下模拟数据文件data/user_behavior.csv

user_id,article_id,category,keywords,click_time,click 1001,2001,technology,python machine-learning, 2023-10-01 09:00:00,1 1001,2002,entertainment,music movie, 2023-10-01 10:00:00,0 1001,2003,technology,ai deep-learning, 2023-10-02 14:00:00,1 1002,2001,technology,python machine-learning, 2023-10-01 09:05:00,0 1002,2004,lifestyle,cooking travel, 2023-10-01 11:00:00,1 1002,2002,entertainment,music movie, 2023-10-02 16:00:00,1

3. 核心流程实现:从数据到推荐

接下来,我们分步骤实现一个简易的风格偏好推荐模型。我们将使用“加权平均兴趣向量”和“余弦相似度”来构建一个可解释的推荐系统。

3.1 数据加载与预处理

首先,加载数据并进行基本清洗和特征编码。

# file: data_processor.py import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder from datetime import datetime class DataProcessor: def __init__(self, filepath): self.df = pd.read_csv(filepath) self.user_encoder = LabelEncoder() self.item_encoder = LabelEncoder() self.category_encoder = LabelEncoder() def preprocess(self): """数据预处理""" # 解析时间 self.df['click_time'] = pd.to_datetime(self.df['click_time']) # 编码用户ID和文章ID(为后续可能用到) self.df['user_id_encoded'] = self.user_encoder.fit_transform(self.df['user_id']) self.df['article_id_encoded'] = self.item_encoder.fit_transform(self.df['article_id']) # 编码类别 self.df['category_encoded'] = self.category_encoder.fit_transform(self.df['category']) # 处理关键词:这里简单拆分为列表,实际中可能需要更复杂的文本向量化 self.df['keywords_list'] = self.df['keywords'].apply(lambda x: x.split()) print(f"数据预览:\n{self.df[['user_id', 'article_id', 'category', 'click']].head()}") print(f"用户数: {self.df['user_id'].nunique()}, 文章数: {self.df['article_id'].nunique()}") return self.df if __name__ == "__main__": processor = DataProcessor('data/user_behavior.csv') df = processor.preprocess()

3.2 构建内容特征向量

我们需要为每篇文章构建一个特征向量。这里我们使用一个简单的方法:将类别编码和关键词的简单表示(如TF-IDF)拼接起来。在实际生产中,可能会使用预训练模型得到更丰富的Embedding。

# file: content_encoder.py from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np class ContentEncoder: def __init__(self): self.tfidf_vectorizer = None self.category_dim = 0 def fit_transform(self, df): """训练并生成内容特征向量""" # 1. 类别特征:One-Hot编码 categories = df['category'].unique() self.category_dim = len(categories) category_to_idx = {cat: i for i, cat in enumerate(categories)} category_vectors = np.zeros((len(df), self.category_dim)) for i, cat in enumerate(df['category']): category_vectors[i, category_to_idx[cat]] = 1.0 # 2. 文本特征:基于关键词的TF-IDF # 将关键词列表重新组合成字符串 keyword_texts = df['keywords'].fillna('') self.tfidf_vectorizer = TfidfVectorizer(max_features=50) # 限制特征数量 keyword_vectors = self.tfidf_vectorizer.fit_transform(keyword_texts).toarray() # 3. 拼接特征向量 content_vectors = np.hstack([category_vectors, keyword_vectors]) # 创建文章ID到向量的映射 self.item_vector_map = {row['article_id']: content_vectors[i] for i, (_, row) in enumerate(df.drop_duplicates('article_id').iterrows())} print(f"内容向量维度: {content_vectors.shape}") return self.item_vector_map def get_vector(self, article_id): """获取指定文章的特征向量""" return self.item_vector_map.get(article_id) if __name__ == "__main__": import sys sys.path.append('.') from data_processor import DataProcessor processor = DataProcessor('data/user_behavior.csv') df = processor.preprocess() encoder = ContentEncoder() item_vectors = encoder.fit_transform(df) print(f"文章2001的向量示例 (前10维): {item_vectors[2001][:10]}")

3.3 构建用户兴趣向量(风格偏好)

用户兴趣向量通过聚合其历史正反馈(点击=1)内容向量得到。我们给点击行为赋予权重。

# file: user_profile.py import numpy as np from collections import defaultdict class UserProfileBuilder: def __init__(self, item_vector_map): self.item_vector_map = item_vector_map self.user_profile_map = {} # user_id -> interest_vector def build_profiles(self, df, weight_positive=1.0): """构建用户兴趣画像""" # 按用户分组 user_groups = df.groupby('user_id') for user_id, group in user_groups: interest_vector = None total_weight = 0 for _, row in group.iterrows(): article_id = row['article_id'] click = row['click'] if click == 1 and article_id in self.item_vector_map: # 只考虑正反馈 weight = weight_positive vec = self.item_vector_map[article_id] if interest_vector is None: interest_vector = np.zeros_like(vec) interest_vector += vec * weight total_weight += weight # 计算加权平均 if interest_vector is not None and total_weight > 0: interest_vector = interest_vector / total_weight self.user_profile_map[user_id] = interest_vector else: # 如果用户无正反馈,可以初始化为零向量或全局平均向量 self.user_profile_map[user_id] = np.zeros_like(next(iter(self.item_vector_map.values()))) print(f"已构建 {len(self.user_profile_map)} 个用户画像") return self.user_profile_map def get_profile(self, user_id): """获取用户兴趣向量""" return self.user_profile_map.get(user_id) if __name__ == "__main__": import sys sys.path.append('.') from data_processor import DataProcessor from content_encoder import ContentEncoder processor = DataProcessor('data/user_behavior.csv') df = processor.preprocess() encoder = ContentEncoder() item_vectors = encoder.fit_transform(df) profile_builder = UserProfileBuilder(item_vectors) user_profiles = profile_builder.build_profiles(df) print(f"用户1001的兴趣向量示例 (前10维): {user_profiles[1001][:10]}")

3.4 生成推荐:计算相似度

有了用户向量和内容向量,我们就可以通过计算余弦相似度,为用户推荐与其兴趣向量最相似的内容。

# file: recommender.py import numpy as np from sklearn.metrics.pairwise import cosine_similarity class StylePreferenceRecommender: def __init__(self, user_profile_builder, content_encoder, df): self.user_profiles = user_profile_builder.user_profile_map self.item_vectors = content_encoder.item_vector_map self.df = df # 获取所有文章的ID列表 self.all_item_ids = list(self.item_vectors.keys()) def recommend_for_user(self, user_id, top_k=5, filter_viewed=True): """为用户生成Top-K推荐""" if user_id not in self.user_profiles: return [] user_vec = self.user_profiles[user_id].reshape(1, -1) # 准备所有候选物品的向量矩阵 candidate_ids = [] candidate_vectors = [] for item_id in self.all_item_ids: # 可选:过滤掉用户已经看过的物品 if filter_viewed: viewed_items = set(self.df[self.df['user_id'] == user_id]['article_id']) if item_id in viewed_items: continue candidate_ids.append(item_id) candidate_vectors.append(self.item_vectors[item_id]) if not candidate_vectors: return [] candidate_matrix = np.array(candidate_vectors) # 计算余弦相似度 similarities = cosine_similarity(user_vec, candidate_matrix).flatten() # 获取相似度最高的Top-K个物品的索引 top_indices = np.argsort(similarities)[-top_k:][::-1] # 构建推荐结果列表 [(item_id, similarity_score), ...] recommendations = [] for idx in top_indices: item_id = candidate_ids[idx] score = similarities[idx] recommendations.append((item_id, score)) return recommendations def batch_recommend(self, user_list, top_k=5): """为一批用户生成推荐""" results = {} for user_id in user_list: results[user_id] = self.recommend_for_user(user_id, top_k) return results if __name__ == "__main__": import sys sys.path.append('.') from data_processor import DataProcessor from content_encoder import ContentEncoder from user_profile import UserProfileBuilder # 1. 加载并处理数据 processor = DataProcessor('data/user_behavior.csv') df = processor.preprocess() # 2. 编码内容 encoder = ContentEncoder() item_vectors = encoder.fit_transform(df) # 3. 构建用户画像 profile_builder = UserProfileBuilder(item_vectors) user_profiles = profile_builder.build_profiles(df) # 4. 初始化推荐器并生成推荐 recommender = StylePreferenceRecommender(profile_builder, encoder, df) # 为用户1001推荐 recs = recommender.recommend_for_user(1001, top_k=3) print("为用户 1001 的推荐结果:") for item_id, score in recs: # 获取文章信息用于展示 article_info = df[df['article_id'] == item_id].iloc[0] print(f" 文章ID: {item_id}, 类别: {article_info['category']}, 关键词: {article_info['keywords']}, 相似度: {score:.4f}")

运行以上代码,你会得到基于用户1001历史行为(点击了technology类别的文章)计算出的推荐列表,理论上会推荐与其兴趣向量相似的其他technology类文章。

4. 评估与优化:让推荐更准

一个推荐系统不能只做推荐,还需要评估其效果。离线评估是迭代优化模型的关键。

4.1 划分训练集与测试集

我们需要按时间将用户行为数据划分为训练集和测试集,用训练集构建用户画像,在测试集上评估推荐效果。

# file: evaluator.py import numpy as np from sklearn.model_selection import train_test_split class RecEvaluator: @staticmethod def train_test_split_by_time(df, test_size=0.2): """按时间划分训练集和测试集""" df_sorted = df.sort_values('click_time').reset_index(drop=True) split_idx = int(len(df_sorted) * (1 - test_size)) train_df = df_sorted.iloc[:split_idx] test_df = df_sorted.iloc[split_idx:] return train_df, test_df @staticmethod def evaluate_precision_at_k(recommender, test_df, k=5): """计算Precision@K""" total_precision = 0.0 user_count = 0 # 获取测试集中的所有用户 test_users = test_df['user_id'].unique() for user_id in test_users: # 获取该用户在测试集中的正反馈物品(点击=1) user_test_positives = set(test_df[(test_df['user_id'] == user_id) & (test_df['click'] == 1)]['article_id']) if not user_test_positives: continue # 如果测试集没有正样本,跳过 # 为该用户生成推荐(注意:推荐器是基于训练集构建的) recommendations = recommender.recommend_for_user(user_id, top_k=k, filter_viewed=True) if not recommendations: continue recommended_items = {item_id for item_id, _ in recommendations} # 计算交集:推荐中且用户真正喜欢的 hits = recommended_items.intersection(user_test_positives) precision = len(hits) / k total_precision += precision user_count += 1 avg_precision = total_precision / user_count if user_count > 0 else 0 return avg_precision if __name__ == "__main__": import sys sys.path.append('.') from data_processor import DataProcessor from content_encoder import ContentEncoder from user_profile import UserProfileBuilder from recommender import StylePreferenceRecommender processor = DataProcessor('data/user_behavior.csv') df = processor.preprocess() # 按时间划分数据集 train_df, test_df = RecEvaluator.train_test_split_by_time(df, test_size=0.3) print(f"训练集大小: {len(train_df)}, 测试集大小: {len(test_df)}") # 使用训练集构建模型 encoder = ContentEncoder() item_vectors = encoder.fit_transform(train_df) # 注意:只在训练集上fit profile_builder = UserProfileBuilder(item_vectors) user_profiles = profile_builder.build_profiles(train_df) recommender = StylePreferenceRecommender(profile_builder, encoder, train_df) # 在测试集上评估 precision_at_5 = RecEvaluator.evaluate_precision_at_k(recommender, test_df, k=5) print(f"Precision@5 on test set: {precision_at_5:.4f}")

4.2 优化方向

上述基础模型有很多可以优化的地方:

  1. 更丰富的特征:使用预训练模型(如Sentence-BERT)提取文本的语义向量,使用图像模型提取封面图特征。
  2. 更复杂的用户建模
    • 序列建模:使用GRU/Transformer对用户行为序列建模,捕捉兴趣变化。
    • 注意力机制:对用户的历史行为施加注意力,让模型更关注与当前候选物品相关的历史行为。
    • 多兴趣提取:使用如MIND等模型,为一个用户提取多个兴趣向量,以表征其广泛的兴趣。
  3. 先进的推荐模型:从协同过滤(如Matrix Factorization)升级到深度学习模型(如YouTube DNN, DeepFM, DIN等)。
  4. 实时更新:用户兴趣会变化,需要设计机制(如滑动窗口、实时特征工程)来更新用户画像。
  5. 探索与利用(E&E):在推荐已知感兴趣的内容(利用)和推荐新内容以探索用户潜在兴趣(探索)之间取得平衡,可以使用Bandit算法。

5. 工程落地与生产注意事项

将原型模型部署到生产环境,需要考虑更多工程问题。

5.1 系统架构概览

一个简单的推荐系统微服务可能包含以下组件:

  • 日志收集服务:实时收集用户行为日志,写入Kafka等消息队列。
  • 实时特征计算:消费行为日志,实时更新用户特征向量,存入Redis。
  • 模型服务:加载训练好的模型,提供推荐接口。可以使用TF Serving, TorchServe或简单的Flask/FastAPI封装。
  • 召回与排序:工业级系统通常分两步:先从一个巨大的物品池中快速召回几百个候选(召回层),再用一个复杂模型对这几百个物品进行精细排序(排序层)。我们上面实现的是简化版的召回+排序一体。
  • AB测试平台:用于对比新旧模型的效果。

5.2 关键配置与代码片段(FastAPI示例)

以下是一个使用FastAPI提供推荐服务的极简示例:

# file: app/main.py from fastapi import FastAPI, HTTPException import numpy as np from pydantic import BaseModel from typing import List import pickle import os app = FastAPI(title="风格偏好推荐API") # 假设我们已经训练好并保存了模型组件 MODEL_DIR = "model_artifacts" class RecommendRequest(BaseModel): user_id: int top_k: int = 10 filter_viewed: bool = True class RecommendResponse(BaseModel): user_id: int recommendations: List[dict] # 每个dict包含item_id和score # 服务启动时加载模型 (实际中可能需要更复杂的加载和更新逻辑) @app.on_event("startup") async def load_models(): global item_vectors, user_profiles, df_train try: with open(os.path.join(MODEL_DIR, 'item_vectors.pkl'), 'rb') as f: item_vectors = pickle.load(f) with open(os.path.join(MODEL_DIR, 'user_profiles.pkl'), 'rb') as f: user_profiles = pickle.load(f) with open(os.path.join(MODEL_DIR, 'df_train.pkl'), 'rb') as f: df_train = pickle.load(f) print("模型加载成功") except FileNotFoundError as e: print(f"模型文件未找到: {e}") # 初始化空数据 item_vectors, user_profiles, df_train = {}, {}, None def recommend_core(user_id, top_k, item_vectors, user_profiles, df_train): """核心推荐逻辑(与前面Recommender类类似)""" # ... 此处省略具体实现,可复用前面Recommender类的代码 ... # 返回格式: [(item_id, score), ...] pass @app.post("/recommend", response_model=RecommendResponse) async def recommend(request: RecommendRequest): user_id = request.user_id top_k = request.top_k if user_id not in user_profiles: # 新用户:可以返回热门推荐、随机推荐或基于人口统计的推荐 raise HTTPException(status_code=404, detail="User profile not found. New user handling not implemented.") rec_list = recommend_core(user_id, top_k, item_vectors, user_profiles, df_train) recommendations = [{"item_id": item_id, "score": float(score)} for item_id, score in rec_list] return RecommendResponse(user_id=user_id, recommendations=recommendations) @app.get("/health") async def health_check(): return {"status": "healthy"}

5.3 常见生产问题与排查思路

问题现象可能原因排查思路与解决方案
推荐结果重复、单调用户兴趣向量过于集中;物品特征区分度不够;热门物品权重过高。1. 检查特征工程,增加多样性特征(如多个子类别)。
2. 在相似度计算中引入多样性惩罚项。
3. 实施探索策略(如ε-greedy,UCB)。
新用户/新物品推荐效果差(冷启动)新用户无行为数据;新物品无交互数据,无法计算向量或相似度。新用户:利用注册信息、引导兴趣选择、推荐热门/多样性内容。
新物品:利用内容特征(冷启动向量)进行推荐,或将其放入探索池。
线上服务延迟高候选物品池过大;相似度计算复杂度高;模型未优化。1. 采用两阶段架构:召回(快速筛选千级候选)+排序(精排百级候选)。
2. 使用向量检索引擎(如Faiss, Annoy)加速最近邻搜索。
3. 对模型进行量化、剪枝等优化。
离线评估指标高,线上效果差离线/在线数据分布不一致;评估指标不能完全反映业务目标。1. 进行A/B测试,以线上核心业务指标(如点击率、停留时长、转化率)为准。
2. 检查特征在线计算与离线计算是否一致。
3. 考虑更贴近业务的离线评估指标(如考虑位置偏差的NDCG)。
用户兴趣漂移捕捉慢用户画像更新频率低(如天级别)。1. 设计实时/近实时特征管道,更新用户最近的行为序列。
2. 在模型中使用注意力机制,让模型自动关注近期行为。

6. 最佳实践与进阶思考

  1. 特征为王:推荐系统的效果上限很大程度上取决于特征的质量和丰富度。持续迭代特征工程。
  2. 评估体系化:不要只依赖一个指标。构建包含准确性(Precision, Recall)、多样性(覆盖率、新颖性)、新颖性惊喜度商业价值(CTR, GMV)的综合评估体系。
  3. 迭代流程化:建立从数据分析->特征实验->模型训练->离线评估->A/B测试->线上发布的完整迭代闭环。
  4. 重视可解释性:在可能的情况下,让推荐结果可解释(例如,“因为你喜欢了A,所以推荐了B”),这能增加用户信任和产品黏性。
  5. 伦理与安全:避免推荐低质、虚假、有害信息。注意算法偏差,防止产生“信息茧房”或加剧社会偏见。建立内容安全审核和推荐结果干预机制。

从“我超喜欢这种风格”这句感性的用户表达,到构建一个可计算、可迭代的推荐系统,核心在于将抽象偏好转化为具体的数据和模型。本文通过一个完整的实战案例,演示了从数据模拟、特征构建、用户画像、相似度计算到评估的每一步。虽然示例进行了简化,但其中体现的数据驱动特征工程评估迭代的思想是通用的。在实际业务中,你需要根据具体的数据规模、业务场景和性能要求,选择合适的架构、算法和工具进行深化和扩展。

← 返回列表