从Sum Pooling到序列建模:Python实战中的推荐算法特征聚合演进
如果你刚开始接触推荐算法,可能会被各种复杂的模型和术语搞得晕头转向。但别担心,今天我们不谈那些高深的理论,就从最基础、最实用的特征聚合操作开始。想象一下,你手头有一堆用户行为数据——点击、浏览、收藏,每个行为都对应着一串数字特征。如何把这些零散的信息整合成一个能代表用户兴趣的“画像”?这就是特征聚合要解决的问题。
在众多聚合方法中,Sum Pooling(求和池化)无疑是最简单直接的一种。它就像把散落一地的珠子用一根线串起来——把多个特征值相加,得到一个总和。别小看这个简单的加法操作,在工业级的推荐系统中,它往往是构建复杂模型的基石。无论是用户画像的构建,还是物品特征的表示,Sum Pooling都扮演着基础而重要的角色。
本文面向有一定Python基础、希望深入推荐算法实践的开发者。我们将从NumPy实现Sum Pooling开始,逐步扩展到更复杂的序列建模技术。通过完整的代码示例和实战技巧,你将不仅掌握基础操作,还能理解这些技术在实际推荐系统中的应用场景和演进路径。
1. 基础篇:NumPy实现Sum Pooling及其核心原理
1.1 Sum Pooling的数学本质与NumPy实现
Sum Pooling的核心思想极其简单:给定一个特征向量,将其所有元素相加。从数学角度看,如果有一个n维特征向量x = [x₁, x₂, ..., xₙ],那么Sum Pooling的结果就是:
s = Σᵢ₌₁ⁿ xᵢ
这个简单的操作在推荐系统中有着广泛的应用场景。比如,用户在一段时间内点击了多个商品,每个商品都有价格、类别、品牌等多个特征维度。通过Sum Pooling,我们可以将这些点击行为的特征聚合起来,形成一个综合的用户兴趣表示。
在Python中,使用NumPy实现Sum Pooling只需要一行代码:
import numpy as np
# 模拟用户行为特征:3个用户,每个用户有4个行为,每个行为有5个特征维度
user_behavior_features = np.array([
[[1.2, 0.8, 3.1, 2.5, 1.0], # 用户1的行为1
[0.5, 1.2, 2.8, 1.9, 0.7], # 用户1的行为2
[1.8, 0.9, 3.5, 2.1, 1.2], # 用户1的行为3
[0.9, 1.1, 2.9, 2.3, 0.8]], # 用户1的行为4
[[2.1, 1.5, 4.2, 3.1, 1.8], # 用户2的行为1
[1.2, 2.1, 3.8, 2.9, 1.5], # 用户2的行为2
[2.5, 1.8, 4.5, 3.3, 2.0], # 用户2的行为3
[1.8, 2.0, 4.0, 3.0, 1.7]], # 用户2的行为4
[[0.8, 0.5, 2.1, 1.8, 0.6], # 用户3的行为1
[0.4, 0.7, 1.9, 1.5, 0.5], # 用户3的行为2
[1.0, 0.6, 2.3, 2.0, 0.7], # 用户3的行为3
[0.6, 0.8, 2.0, 1.7, 0.6]] # 用户3的行为4
])
# 对每个用户的所有行为特征进行Sum Pooling
# axis=1表示沿着行为维度求和,axis=2表示沿着特征维度求和
user_sum_pooled = np.sum(user_behavior_features, axis=1)
print("Sum Pooling结果(每个用户的聚合特征):")
print(user_sum_pooled)
print(f"\n聚合后特征形状:{user_sum_pooled.shape}")
运行这段代码,你会得到每个用户经过Sum Pooling后的聚合特征向量。原本每个用户有4个行为×5个特征=20个数值,现在被压缩成了5个数值——这就是特征聚合的魔力。
注意:在实际应用中,特征值通常需要先进行归一化处理,避免某些维度值过大主导求和结果。常见的归一化方法包括Min-Max归一化和Z-score标准化。
1.2 Sum Pooling的变体与应用场景
虽然基础的Sum Pooling很简单,但在实际应用中,我们经常需要根据具体场景进行调整。以下是几种常见的变体:
加权Sum Pooling:不是所有特征都同等重要。比如在电商推荐中,购买行为比浏览行为更能反映用户兴趣。我们可以为不同行为赋予不同权重:
# 定义行为权重:购买>加购>收藏>浏览
behavior_weights = np.array([0.8, 0.6, 0.4, 0.2]) # 对应4个行为
# 加权Sum Pooling
weighted_sum = np.sum(user_behavior_features * behavior_weights[:, np.newaxis, np.newaxis], axis=1)
分层Sum Pooling:对于多级特征,可以先在子层级进行Sum Pooling,再在父层级聚合。比如商品特征可以按类别先聚合,再整体聚合。
带掩码的Sum Pooling:实际数据中经常存在缺失值或无效行为,我们需要用掩码过滤:
# 创建行为掩码(1表示有效,0表示无效)
behavior_mask = np.array([
[1, 1, 1, 0], # 用户1只有前3个行为有效
[1, 1, 1, 1], # 用户2所有行为都有效
[1, 0, 1, 1] # 用户3第2个行为无效
])
# 带掩码的Sum Pooling
masked_sum = np.sum(user_behavior_features * behavior_mask[:, :, np.newaxis], axis=1)
在实际推荐系统中,Sum Pooling常用于以下场景:
- 用户画像构建:将用户的历史行为特征(点击、购买、评分等)聚合为统一的用户表示
- 物品特征提取:聚合物品的多维度特征(价格、类别、品牌、材质等)
- 会话特征表示:将单次会话中的多个交互行为聚合为会话级特征
- 实时特征计算:在流式处理中快速计算滑动窗口内的特征总和
1.3 性能优化与常见陷阱
虽然Sum Pooling操作简单,但在大规模数据场景下,性能优化至关重要。以下是一些实用技巧:
向量化操作:避免使用Python循环,充分利用NumPy的向量化计算:
# 不推荐:使用循环
result = np.zeros((3, 5))
for i in range(3):
for j in range(4):
result[i] += user_behavior_features[i, j]
# 推荐:使用向量化操作
result = np.sum(user_behavior_features, axis=1)
内存优化:对于超大矩阵,可以使用分块计算:
def chunked_sum_pooling(features, chunk_size=1000):
"""分块Sum Pooling,避免内存溢出"""
n_users = features.shape[0]
result = np.zeros((n_users, features.shape[2]))
for start in range(0, n_users, chunk_size):
end = min(start + chunk_size, n_users)
chunk = features[start:end]
result[start:end] = np.sum(chunk, axis=1)
return result
数值稳定性:当特征值范围差异很大时,直接求和可能导致数值溢出或精度问题:
# 数值稳定的Sum Pooling
def stable_sum_pooling(features, epsilon=1e-8):
# 先对每个特征维度进行标准化
mean = np.mean(features, axis=1, keepdims=True)
std = np.std(features, axis=1, keepdims=True) + epsilon
normalized = (features - mean) / std
# 再进行求和
return np.sum(normalized, axis=1)
提示:在实际生产环境中,Sum Pooling通常不是性能瓶颈。但如果需要处理数十亿级别的用户行为数据,考虑使用分布式计算框架(如Spark)或专门的向量数据库。
2. 进阶篇:从Sum Pooling到注意力机制的演进
2.1 Sum Pooling的局限性分析
尽管Sum Pooling简单高效,但它有一个根本性的缺陷:平等对待所有特征。在现实世界的推荐场景中,用户的不同行为、物品的不同特征,其重要性差异很大。比如:
- 昨天的购买行为 vs. 一个月前的浏览行为
- 商品价格 vs. 商品颜色
- 核心品类偏好 vs. 偶然性点击
Sum Pooling无法捕捉这些重要性差异,导致信息损失。为了解决这个问题,推荐算法领域发展出了更精细的特征聚合方法。
2.2 加权Pooling:引入重要性权重
最简单的改进是为不同特征分配不同的权重。这可以通过两种方式实现:
静态权重:基于业务经验手动设定权重。比如在电商场景中,可以给购买行为分配权重0.8,加购0.6,收藏0.4,浏览0.2:
# 静态加权Sum Pooling实现
def static_weighted_pooling(features, behavior_types):
"""
features: 行为特征矩阵,形状为(n_users, n_behaviors, n_features)
behavior_types: 行为类型列表,长度等于n_behaviors
"""
# 定义行为类型到权重的映射
weight_map = {
'purchase': 0.8,
'add_to_cart': 0.6,
'favorite': 0.4,
'click': 0.2,
'view': 0.1
}
# 创建权重矩阵
weights = np.array([weight_map[b] for b in behavior_types])
weights = weights[:, np.newaxis] # 扩展维度以匹配特征
# 加权求和
weighted_features = features * weights[np.newaxis, :, :]
return np.sum(weighted_features, axis=1)
动态权重:基于特征本身计算权重。比如可以根据行为的时间衰减计算权重:
def time_decay_weighted_pooling(features, timestamps, decay_rate=0.1):
"""
features: 行为特征矩阵
timestamps: 行为时间戳(相对于当前时间的小时数)
decay_rate: 衰减率
"""
# 计算时间衰减权重:越近的行为权重越高
time_weights = np.exp(-decay_rate * timestamps)
# 归一化权重
time_weights = time_weights / np.sum(time_weights, axis=1, keepdims=True)
# 加权求和
weighted_features = features * time_weights[:, :, np.newaxis]
return np.sum(weighted_features, axis=1)
2.3 注意力机制:让模型学习权重分配
静态权重和简单动态权重虽然有所改进,但仍然不够灵活。真正的突破来自注意力机制——让模型自动学习每个特征的重要性。
基础注意力机制实现:
import torch
import torch.nn as nn
import torch.nn.functional as F
class SimpleAttentionPooling(nn.Module):
"""简单的注意力池化层"""
def __init__(self, feature_dim, hidden_dim=64):
super().__init__()
self.feature_dim = feature_dim
# 注意力网络:将特征映射到注意力分数
self.attention_net = nn.Sequential(
nn.Linear(feature_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
nn.Softmax(dim=1) # 在行为维度上归一化
)
def forward(self, behavior_features):
"""
behavior_features: 形状为(batch_size, n_behaviors, feature_dim)
返回:聚合后的特征,形状为(batch_size, feature_dim)
"""
batch_size, n_behaviors, feature_dim = behavior_features.shape
# 计算每个行为的注意力分数
attention_scores = self.attention_net(
behavior_features.view(-1, feature_dim)
).view(batch_size, n_behaviors, 1)
# 加权求和
weighted_sum = torch.sum(behavior_features * attention_scores, dim=1)
return weighted_sum, attention_scores
# 使用示例
if __name__ == "__main__":
# 模拟数据
batch_size = 32
n_behaviors = 10
feature_dim = 64
# 随机生成行为特征
behavior_features = torch.randn(batch_size, n_behaviors, feature_dim)
# 初始化注意力池化层
attention_pooling = SimpleAttentionPooling(feature_dim)
# 前向传播
aggregated_features, attention_weights = attention_pooling(behavior_features)
print(f"输入形状: {behavior_features.shape}")
print(f"聚合后形状: {aggregated_features.shape}")
print(f"注意力权重形状: {attention_weights.shape}")
print(f"注意力权重示例(第一个样本): {attention_weights[0, :5].squeeze().detach().numpy()}")
目标感知的注意力机制:在推荐系统中,我们经常需要根据目标物品来调整对用户历史行为的注意力。这就是DIN(Deep Interest Network)模型的核心思想:
class TargetAttentionPooling(nn.Module):
"""目标感知的注意力池化(类似DIN)"""
def __init__(self, feature_dim, hidden_dim=64):
super().__init__()
self.feature_dim = feature_dim
# 注意力网络:考虑目标物品和用户行为的交互
self.attention_net = nn.Sequential(
nn.Linear(feature_dim * 3, hidden_dim), # 拼接:行为特征、目标特征、交互特征
nn.ReLU(),
nn.Linear(hidden_dim, 1),
nn.Sigmoid() # 注意:这里使用Sigmoid而不是Softmax
)
def forward(self, user_behaviors, target_item):
"""
user_behaviors: 用户历史行为特征,(batch_size, n_behaviors, feature_dim)
target_item: 目标物品特征,(batch_size, feature_dim)
返回:聚合后的用户兴趣表示
"""
batch_size, n_behaviors, _ = user_behaviors.shape
# 扩展目标特征以匹配行为数量
target_expanded = target_item.unsqueeze(1).expand(-1, n_behaviors, -1)
# 计算交互特征(元素级乘积)
interaction = user_behaviors * target_expanded
# 拼接所有特征
concatenated = torch.cat([
user_behaviors,
target_expanded,
interaction
], dim=-1)
# 计算注意力分数
attention_scores = self.attention_net(
concatenated.view(-1, self.feature_dim * 3)
).view(batch_size, n_behaviors, 1)
# 加权求和(注意:这里不是Softmax归一化,而是每个行为独立计算重要性)
weighted_sum = torch.sum(user_behaviors * attention_scores, dim=1)
return weighted_sum, attention_scores
2.4 多头注意力与Transformer架构
当注意力机制遇上多头设计,就形成了Transformer的核心组件。在推荐系统中,Transformer被广泛用于序列建模:
class MultiHeadAttentionPooling(nn.Module):
"""多头注意力池化"""
def __init__(self, feature_dim, num_heads=4, dropout=0.1):
super().__init__()
assert feature_dim % num_heads == 0, "feature_dim必须能被num_heads整除"
self.feature_dim = feature_dim
self.num_heads = num_heads
self.head_dim = feature_dim // num_heads
# 线性变换层
self.query_proj = nn.Linear(feature_dim, feature_dim)
self.key_proj = nn.Linear(feature_dim, feature_dim)
self.value_proj = nn.Linear(feature_dim, feature_dim)
self.output_proj = nn.Linear(feature_dim, feature_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, behavior_features):
batch_size, seq_len, _ = behavior_features.shape
# 线性变换
Q = self.query_proj(behavior_features)
K = self.key_proj(behavior_features)
V = self.value_proj(behavior_features)
# 重塑为多头
Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# 计算注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) / (self.head_dim ** 0.5)
attention_weights = F.softmax(scores, dim=-1)
attention_weights = self.dropout(attention_weights)
# 应用注意力权重
context = torch.matmul(attention_weights, V)
# 合并多头
context = context.transpose(1, 2).contiguous().view(
batch_size, seq_len, self.feature_dim
)
# 输出投影
output = self.output_proj(context)
# 池化:对序列维度取平均

&spm=1001.2101.3001.5002&articleId=151945046&d=1&t=3&u=9b985bd5affd48a686650c6a1af96377)
24

被折叠的 条评论
为什么被折叠?



