目录
基本概念
协同过滤
协同过滤(Collaborative Filtering,CF)是推荐系统中最常用和最成功的技术之一。它的核心思想是利用用户群体的集体智慧来为个体用户做出推荐。
协同过滤主要基于以下假设:
- 有相似兴趣的用户会对相似的物品做出相似的评价。
- 用户喜欢的物品往往具有相似的特征。
协同过滤主要分为两大类:
-
基于用户的协同过滤(User-based CF):
- 原理:找到与目标用户相似的用户群体,然后将这些相似用户喜欢但目标用户还未接触的物品推荐给目标用户。
- 优点:能够发现用户的潜在兴趣。
- 缺点:当用户数量很大时,计算用户相似度的开销较大。
-
基于物品的协同过滤(Item-based CF):
- 原理:计算物品之间的相似度,然后根据用户已经喜欢的物品推荐相似的物品。
- 优点:物品相似度相对稳定,可以预先计算和缓存,提高推荐效率。
- 缺点:难以推荐与用户已有兴趣完全不同的新物品。
协同过滤的主要优点:
- 不需要对物品或用户进行详细的特征建模。
- 能够发现用户的潜在兴趣。
- 推荐结果通常具有很好的多样性。
主要缺点:
- 冷启动问题:对于新用户或新物品,由于缺乏历史数据,难以做出准确推荐。
- 数据稀疏问题:用户-物品交互矩阵通常非常稀疏,可能影响推荐质量。
- 计算复杂度:随着用户和物品数量的增加,计算开销可能变得很大。
基于图的协同过滤
-
基本概念:
- 图结构:用户和物品被表示为图中的节点,它们之间的交互(如评分、点击)表示为边。
- 异构图:包含多种类型的节点和边,可以表示更复杂的关系。
-
主要优势:
- 能够捕捉复杂的关系:不仅包括用户-物品直接关系,还可以包含用户-用户、物品-物品关系。
- 可以整合多种信息:如用户属性、物品特征、上下文信息等。
- 具有更强的表达能力:可以描述高阶关系和复杂的交互模式。
-
工作原理:
- 构建图:根据用户行为数据和其他相关信息构建图结构。
- 图分析:使用各种图算法(如随机游走、路径分析、图神经网络等)来分析图结构。
- 生成推荐:基于图分析结果,为目标用户推荐相关物品。
-
常用技术:
-
随机游走:如 PersonalRank 算法。
从起始节点开始,按照一定的概率规则在图上随机移动。
游走的过程中记录访问各节点的频率,用于估计节点之间的相关性。
代表算法:
- PersonalRank:个性化版的PageRank算法,为每个用户构建个性化的物品排序。
- ItemRank:类似于PersonalRank,但更关注物品之间的关系。
-
图嵌入:如 DeepWalk、Node2Vec 等。
将图中的节点映射到低维向量空间,保留节点之间的结构关系。
在嵌入空间中计算节点相似度,用于推荐。
- DeepWalk:利用随机游走生成节点序列,然后使用Skip-gram模型学习节点表示。
- Node2Vec:在DeepWalk基础上,引入了偏向性随机游走,可以更灵活地平衡局部和全局结构。
- LINE:直接优化保留一阶和二阶邻近性的目标函数。
-
图神经网络:如 GCN、GAT 等。
直接在图结构上进行端到端的学习。
通过消息传递机制,聚合邻居节点的信息来更新中心节点的表示。
- GCN (Graph Convolutional Network):使用拉普拉斯矩阵的特征分解来定义图上的卷积操作。
- GAT (Graph Attention Network):引入注意力机制,为不同邻居分配不同的重要性。
- GraphSAGE:通过采样和聚合邻居信息来生成节点嵌入,可以处理动态图。
-
元路径分析:用于异构图的分析。
在异构图中定义有意义的元路径(不同类型节点的序列)。
基于这些元路径计算节点相似性或进行随机游走。
- PathSim:基于元路径的相似性度量。
- HeteSim:考虑路径的异质性的相似性度量。
- Metapath2vec:结合元路径和Skip-gram模型的异构图嵌入方法。
Embedding
Embeddings是机器学习和自然语言处理中的一个核心概念,它将离散的对象(如单词、句子、用户、商品等)映射到连续的向量空间中。这种表示方法能够捕捉对象之间的语义关系,使得计算机能够更好地理解和处理这些对象。
代码示例
import numpy as np # 导入NumPy库,用于高效的数值计算 from collections import defaultdict # 导入defaultdict,用于创建默认值的字典 # 示例语料库,包含三个简单的句子 corpus = [ "the quick brown fox jumps over the lazy dog", "the lazy dog sleeps all day", "the quick brown fox is quick" ] # 步骤1: 预处理 def preprocess(corpus): words = [] # 初始化一个空列表来存储所有单词 for sentence in corpus: # 遍历语料库中的每个句子 words.extend(sentence.lower().split()) # 将句子转换为小写,分割成单词,并添加到words列表 return list(set(words)) # 返回去重后的单词列表 vocab = preprocess(corpus) # 获取词汇表(去重后的单词列表) print(vocab) word_to_ix = {word: i for i, word in enumerate(vocab)} # 创建单词到索引的映射字典 ix_to_word = {i: word for word, i in word_to_ix.items()} # 创建索引到单词的映射字典 # 步骤2: 生成训练数据 def generate_training_data(corpus, word_to_ix, window_size=2): data = [] # 初始化一个空列表来存储训练数据 for sentence in corpus: # 遍历语料库中的每个句子 words = sentence.lower().split() # 将句子转换为小写并分割成单词列表 for i, word in enumerate(words): # 遍历句子中的每个单词及其索引 # 在指定窗口大小内选择上下文单词 for j in range(max(0, i - window_size), min(len(words), i + window_size + 1)): if i != j: # 排除当前单词自身 # 将(中心词, 上下文词)对添加到训练数据中,使用它们的索引表示 data.append((word_to_ix[word], word_to_ix[words[j]])) return data # 返回训练数据列表 training_data = generate_training_data(corpus, word_to_ix) # 生成训练数据 # 步骤3: 定义模型 class SkipGramModel: def __init__(self, vocab_size, embedding_dim): # 初始化词嵌入矩阵,使用随机值 self.embeddings = np.random.randn(vocab_size, embedding_dim) # 初始化输出权重矩阵,使用随机值 self.output_weights = np.random.randn(embedding_dim, vocab_size) def forward(self, input_word): hidden = self.embeddings[input_word] # 获取输入词的嵌入向量 output = np.dot(hidden, self.output_weights) # 计算输出层的结果 return hidden, output # 返回隐藏层(嵌入)和输出层的结果 def backward(self, input_word, hidden, output, target, learning_rate): exp_output = np.exp(output) # 计算输出的指数 softmax_output = exp_output / np.sum(exp_output) # 计算softmax概率 loss = -np.log(softmax_output[target]) # 计算交叉熵损失 d_output = softmax_output # 输出层的梯度 d_output[target] -= 1 # 调整目标词的梯度 # 计算隐藏层(嵌入)的梯度 grad_hidden = np.dot(d_output, self.output_weights.T) # 计算输出权重的梯度 grad_output_weights = np.outer(hidden, d_output) # 更新嵌入 self.embeddings[input_word] -= learning_rate * grad_hidden # 更新输出权重 self.output_weights -= learning_rate * grad_output_weights return loss # 返回本次迭代的损失 # 步骤4: 训练模型 vocab_size = len(vocab) # 词汇表大小 embedding_dim = 10 # 嵌入维度 epochs = 1000 # 训练轮数 learning_rate = 0.01 # 学习率 model = SkipGramModel(vocab_size, embedding_dim) # 初始化模型 for epoch in range(epochs): # 遍历每个训练轮次 total_loss = 0 # 初始化总损失 for input_word, target_word in training_data: # 遍历每个训练样本 hidden, output = model.forward(input_word) # 前向传播 loss = model.backward(input_word, hidden, output, target_word, learning_rate) # 反向传播 total_loss += loss # 累加损失 if epoch % 100 == 0: # 每100轮打印一次损失 print(f"Epoch {epoch}, Loss: {total_loss}") # 步骤5: 使用训练好的嵌入 word_embeddings = model.embeddings # 获取训练好的词嵌入 # 打印"fox"这个词的嵌入向量 print("Embedding for 'fox':", word_embeddings[word_to_ix['fox']])
Dropoutnet
DropoutNet是一个典型的双搭结构,用户tower用来学习用户的潜空间向量表示;对应地,物品tower用来学习物品的潜空间向量表示。当用户对当前物品具有某种交互行为,比如点击、购买时,模型的损失函数设计设定用户的向量表示与物品的向量表示距离尽可能近;当给用户展现了某物品,并且用户没有对该物品产生任何交互行为时,对应的用户、物品pair构成一条负样本,模型会尽量让对应样本中用户的向量表示与物品的向量表示距离尽可能远。
DropoutNet借鉴了降噪自动编码机(denoising autoencoder)的思想,即训练模型接受被corrupted的输入来重建原始的输入,也就是学习一个模型使其能够在部分输入特征缺失的情况下仍然能够得到比较精确的向量表示,具体地,模型是要使得在输入被corrupted的情况下学习到的用户向量与物品向量的相关性分尽可能接近输入在没有被corrupted的情况下学习到的用户向量与物品向量的相关性分。
目标函数:
U和V分别是外部输入的、作为监督信号的用户和物品向量表示,一般是通过其他模型学习得到。
为了使模型适用于用户冷启动场景,训练过程中对用户和物品的偏好统计特征进行dropout:
LightGCN
LightGCN
LightGCN-代码训练截图