通俗易懂的方式讲解各个模型的概念

我们暂时把复杂的数学公式放在一边,用一些生动的比喻来理解这些推荐算法模型。它们都属于“深度学习推荐模型”的大家庭,目标是比传统模型更智能地学习特征之间的关系。

在开始之前,我们先统一一个核心概念:特征交叉
想象一下你在电商网站买东西,你有“性别”和“商品品类”两个特征。

  • 性别=女品类=口红 单独看,可能都有一定的购买概率。
  • 但当它们组合在一起(“女性 & 口红”)时,这个组合特征会极大地提升购买概率。这种组合就是“特征交叉”。
    这些模型的核心任务,就是自动、高效地找到并利用这些强大的交叉特征。

1. FNN - 因子分解机神经网络

通俗解释:
FNN = FM + 深度神经网络

你可以把它想象成一个 “两步走”的学徒

  1. 第一步:拜师学艺。 它先跟着一位叫 FM 的老师(一个较简单的模型)学习。FM老师教会它如何为每个特征生成一个高质量的“特征向量”(可以理解为特征的“身份证”或“词向量”)。
  2. 第二步:深度学习。 学徒出师后,把这些已经学好的、有意义的“特征向量”作为初始知识,输入到一个深度神经网络中进行更深层次的学习和组合。

核心思想: 用FM预训练好的向量来初始化神经网络的输入层,而不是随机初始化,这样能让神经网络起点更高、训练更快、效果更好。

常见使用场景:

  • 可以作为其他复杂模型的基准模型
  • 适用于特征维度非常高,且希望模型能有一个“良好开端”的场景。它在早期深度学习推荐系统中很常见。

2. PNN - 乘积神经网络

通俗解释:
PNN = 在特征“相亲大会”上直接牵线搭桥

FNN是把特征向量直接扔进神经网络,让网络自己去慢慢摸索特征之间的关系。而PNN更“主动”。

想象一个“特征相亲大会”,所有特征向量都来了。

  • FNN 的做法是:让大家先随便坐着,然后让主持人(DNN)慢慢引导他们互相认识。
  • PNN 的做法是:一上来就先搞一个“破冰环节”——乘积层。在这个环节里,它强制让两个特征向量进行“内积”或“外积”操作(可以理解为让它们握手、交流,看看合不合拍),直接计算出它们之间的交叉信号。然后,再把所有这些“初次交流”的结果和原始特征一起,送给后面的神经网络去做深度判断。

核心思想: 在输入层之后,立即显式地引入一个“乘积层”来捕获特征之间的二阶交叉信息,然后再交给DNN处理高阶交叉。

常见使用场景:

  • 适用于需要强特征交叉信号的场景,比如广告点击率预估,其中“用户历史行为”和“当前广告内容”的交叉至关重要。
  • 当你认为特征之间的两两组合(二阶交叉)是影响预测结果的关键因素时。

3. ONN - 操作神经网络

通俗解释:
PNN的升级版、更灵活的“红娘”

PNN的“破冰环节”(乘积层)手段比较单一,主要就是内积和外积。ONN觉得这不够,它说:“为什么只能用这两种方式交流?我们可以有更多花样!”

ONN引入了一个更通用的概念——核函数。你可以把这些核函数想象成各种不同的“互动游戏”:

  • 游戏1:内积(比比谁更相似)
  • 游戏2:外积(一起合作创造新东西)
  • 游戏3:余弦相似度(忽略长度,只看方向是否一致)
  • … 还有很多自定义游戏。

每个特征对都可以选择自己喜欢的“游戏”来互动,产生更丰富的交叉信号。

核心思想: 将PNN中固定的乘积操作(内/外积)泛化为多种多样的、可学习的核函数,让模型自己选择最好的特征交叉方式。

常见使用场景:

  • 特征关系非常复杂的场景,单一的交叉方式不足以描述。
  • 是PNN的强大替代品,理论上效果应该更好,但计算可能更复杂。
  • 在处理类似“用户历史行为序列”这种多值特征时,这种灵活的交叉方式可能更有优势。

4. NFM - 神经网络因子分解机

通俗解释:
FM + 一个“特征交互浓缩层”

FM模型本身就很擅长学习二阶特征交叉,但它缺乏学习更高阶交叉的能力。NFM巧妙地将FM和深度学习结合。

它的工作流程是:

  1. 像FM一样:把所有特征向量化。
  2. 进行两两交叉:但不是像FM那样直接把交叉结果相加。它把所有这些两两交叉的结果(一堆向量)收集起来。
  3. “浓缩”成一碗精华汤:这是一个关键步骤——Bi-Interaction Pooling(双向交互池化层)。它把所有交叉向量进行“求和并取平均”,浓缩成一个浓缩的、固定长度的“精华向量”。这个向量包含了所有二阶交叉信息的精华。
  4. 深度加工:最后,把这个“精华向量”喂给一个深度神经网络,让网络去学习更高阶、更复杂的交叉模式。

核心思想: 使用一个简洁高效的“双向交互池化层”来显式地捕获二阶特征交叉,并将其结果作为DNN的输入,以捕获高阶交叉。

常见使用场景:

  • 非常常用和有效的模型,被认为是结合FM和DNN的典范之一。
  • 在各种点击率预估、推荐系统排序阶段都有广泛应用。
  • 当你想比纯FM模型效果更好,又想避免像PNN/ONN那样复杂的结构时,NFM是一个非常好的折中选择。

总结与对比

模型 核心比喻 如何做特征交叉? 特点与场景
FNN 两步走学徒 先用FM预训练,再用DNN学习 起点高,训练快。是早期思想的代表。
PNN 主动红娘 在输入后立即加入“乘积层” 显式捕捉二阶交叉,适合强交叉信号场景。
ONN 灵活红娘 用多种“核函数”进行交叉 PNN的升级版,交叉方式更灵活,能力更强。
NFM 精华汤厨师 用“交互池化层”浓缩二阶交叉,再送DNN 经典且高效,平衡了效果与复杂度,应用广泛。

学习建议:
你可以按照这个顺序来理解它们的演进:
FNN (预热) -> PNN (显式交叉) -> ONN (更灵活交叉) -> NFM (优雅且高效的结合)

希望这个解释能帮助你轻松地理解这些模型!它们是构建现代推荐系统非常重要的基石。
下面我将为每个模型提供一个简化的PyTorch实现示例,帮助您理解它们的核心结构。这些示例都基于一个假设的点击率预测场景。

代码演示各个模型的使用

0.训练数据集准备

首先,让我们创建一个简单的模拟数据集:

"""
@Author  : lidp
@WeChat : 851298348
@Version : 1.0
@Desc    : 功能描述
跳槽涨薪,就学大模型开发;
一对一技术指导,加VX:851298348
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

# 设置随机种子
torch.manual_seed(42)

# 模拟数据参数
batch_size = 32
num_features = 10  # 特征字段数量
embed_dim = 4      # 嵌入维度

# 创建模拟输入数据 (假设是稀疏特征的one-hot编码)
# 在实际应用中,这些会是用户、物品等特征的嵌入表示
x_sparse = torch.randint(0, 2, (batch_size, num_features)).float()
print(f"输入数据形状: {x_sparse.shape}")

1. FNN (因子分解机神经网络)

class FNN(nn.Module):
    def __init__(self, num_features, embed_dim, hidden_dims):
        super(FNN, self).__init__()
        
        # FM预训练层 (这里简化为线性变换模拟预训练效果)
        self.fm_layer = nn.Linear(num_features, num_features * embed_dim)
        
        # 深度神经网络部分
        layers = []
        input_dim = num_features * embed_dim
        
        for hidden_dim in hidden_dims:
            layers.extend([
                nn.Linear(input_dim, hidden_dim),
                nn.ReLU(),
                nn.Dropout(0.2)
            ])
            input_dim = hidden_dim
        
        layers.append(nn.Linear(input_dim, 1))
        layers.append(nn.Sigmoid())
        
        self.dnn = nn.Sequential(*layers)
    
    def forward(self, x):
        # FM特征变换
        fm_features = self.fm_layer(x)  # [batch_size, num_features * embed_dim]
        
        # DNN处理
        output = self.dnn(fm_features)
        return output

# 演示FNN
fnn_model = FNN(num_features=num_features, embed_dim=embed_dim, 
                hidden_dims=[64, 32, 16])
fnn_output = fnn_model(x_sparse)
print(f"FNN输出形状: {fnn_output.shape}, 值范围: [{fnn_output.min():.3f}, {fnn_output.max():.3f}]")

2. PNN (乘积神经网络)

class PNN(nn.Module):
    def __init__(self, num_features, embed_dim, hidden_dims):
        super(PNN, self).__init__()
        self.num_features = num_features
        self.embed_dim = embed_dim
        
        # 特征嵌入层
        self.embedding = nn.Linear(num_features, num_features * embed_dim)
        
        # 乘积层计算
        self.product_dim = num_features * (num_features - 1) // 2
        
        # 组合线性部分和乘积部分
        total_dim = num_features * embed_dim + self.product_dim
        
        # DNN部分
        layers = []
        input_dim = total_dim
        
        for hidden_dim in hidden_dims:
            layers.extend([
                nn.Linear(input_dim, hidden_dim),
                nn.ReLU(),
                nn.Dropout(0.2)
            ])
            input_dim = hidden_dim
        
        layers.append(nn.Linear(input_dim, 1))
        layers.append(nn.Sigmoid())
        
        self.dnn = nn.Sequential(*layers)
    
    def forward(self, x):
        batch_size = x.size(0)
        
        # 获取嵌入特征
        embedded = self.embedding(x).view(batch_size, self.num_features, self.embed_dim)
        
        # 线性部分 (原始嵌入)
        linear_part = embedded.view(batch_size, -1)
        
        # 乘积部分 (内积计算)
        product_list = []
        for i in range(self.num_features):
            for j in range(i + 1, self.num_features):
                # 计算特征i和j的内积
                inner_product = torch.sum(embedded[:, i, :] * embedded[:, j, :], dim=1, keepdim=True)
                product_list.append(inner_product)
        
        product_part = torch.cat(product_list, dim=1)
        
        # 拼接线性部分和乘积部分
        combined = torch.cat([linear_part, product_part], dim=1)
        
        # DNN处理
        output = self.dnn(combined)
        return output

# 演示PNN
pnn_model = PNN(num_features=num_features, embed_dim=embed_dim, 
                hidden_dims=[64, 32])
pnn_output = pnn_model(x_sparse)
print(f"PNN输出形状: {pnn_output.shape}, 值范围: [{pnn_output.min():.3f}, {pnn_output.max():.3f}]")

3. ONN (操作神经网络)

class ONN(nn.Module):
    def __init__(self, num_features, embed_dim, hidden_dims):
        super(ONN, self).__init__()
        self.num_features = num_features
        self.embed_dim = embed_dim
        
        # 多种核的嵌入层
        self.kernel_types = ['inner', 'outer', 'concat']
        self.embeddings = nn.ModuleDict({
            kernel: nn.Linear(num_features, num_features * embed_dim) 
            for kernel in self.kernel_types
        })
        
        # 计算不同核的输出维度
        inner_dim = num_features * (num_features - 1) // 2  # 内积
        outer_dim = num_features * (num_features - 1) // 2 * embed_dim  # 外积
        concat_dim = num_features * embed_dim  # 拼接
        
        total_dim = inner_dim + outer_dim + concat_dim
        
        # DNN部分
        layers = []
        input_dim = total_dim
        
        for hidden_dim in hidden_dims:
            layers.extend([
                nn.Linear(input_dim, hidden_dim),
                nn.ReLU(),
                nn.Dropout(0.2)
            ])
            input_dim = hidden_dim
        
        layers.append(nn.Linear(input_dim, 1))
        layers.append(nn.Sigmoid())
        
        self.dnn = nn.Sequential(*layers)
    
    def forward(self, x):
        batch_size = x.size(0)
        
        # 不同核的特征表示
        kernel_outputs = []
        
        for kernel_type in self.kernel_types:
            embedded = self.embeddings[kernel_type](x).view(batch_size, self.num_features, self.embed_dim)
            
            if kernel_type == 'inner':
                # 内积操作
                inner_products = []
                for i in range(self.num_features):
                    for j in range(i + 1, self.num_features):
                        product = torch.sum(embedded[:, i, :] * embedded[:, j, :], dim=1, keepdim=True)
                        inner_products.append(product)
                kernel_output = torch.cat(inner_products, dim=1)
                
            elif kernel_type == 'outer':
                # 外积操作 (简化版)
                outer_products = []
                for i in range(self.num_features):
                    for j in range(i + 1, self.num_features):
                        # 外积的扁平化版本
                        outer_product = (embedded[:, i, :].unsqueeze(2) * embedded[:, j, :].unsqueeze(1)).view(batch_size, -1)
                        outer_products.append(outer_product)
                kernel_output = torch.cat(outer_products, dim=1)
                
            else:  # concat
                # 拼接操作
                kernel_output = embedded.view(batch_size, -1)
            
            kernel_outputs.append(kernel_output)
        
        # 组合所有核的输出
        combined = torch.cat(kernel_outputs, dim=1)
        
        # DNN处理
        output = self.dnn(combined)
        return output

# 演示ONN
onn_model = ONN(num_features=num_features, embed_dim=embed_dim, 
                hidden_dims=[128, 64])
onn_output = onn_model(x_sparse)
print(f"ONN输出形状: {onn_output.shape}, 值范围: [{onn_output.min():.3f}, {onn_output.max():.3f}]")

4. NFM (神经网络因子分解机)

class NFM(nn.Module):
    def __init__(self, num_features, embed_dim, hidden_dims):
        super(NFM, self).__init__()
        self.num_features = num_features
        self.embed_dim = embed_dim
        
        # 特征嵌入层
        self.embedding = nn.Linear(num_features, num_features * embed_dim)
        
        # Bi-Interaction Pooling 层
        # 输出维度为 embed_dim
        
        # DNN部分
        layers = []
        input_dim = embed_dim  # Bi-Interaction Pooling的输出维度
        
        for hidden_dim in hidden_dims:
            layers.extend([
                nn.Linear(input_dim, hidden_dim),
                nn.ReLU(),
                nn.Dropout(0.2)
            ])
            input_dim = hidden_dim
        
        layers.append(nn.Linear(input_dim, 1))
        layers.append(nn.Sigmoid())
        
        self.dnn = nn.Sequential(*layers)
    
    def forward(self, x):
        batch_size = x.size(0)
        
        # 获取嵌入特征
        embedded = self.embedding(x).view(batch_size, self.num_features, self.embed_dim)
        
        # Bi-Interaction Pooling (核心创新)
        # 公式: 1/2 * [ (sum_i v_i)^2 - sum_i (v_i)^2 ]
        sum_of_vectors = torch.sum(embedded, dim=1)  # [batch_size, embed_dim]
        sum_of_squares = torch.sum(embedded * embedded, dim=1)  # [batch_size, embed_dim]
        
        # Bi-Interaction 输出
        bi_interaction = 0.5 * (sum_of_vectors * sum_of_vectors - sum_of_squares)  # [batch_size, embed_dim]
        
        # DNN处理
        output = self.dnn(bi_interaction)
        return output

# 演示NFM
nfm_model = NFM(num_features=num_features, embed_dim=embed_dim, 
                hidden_dims=[64, 32])
nfm_output = nfm_model(x_sparse)
print(f"NFM输出形状: {nfm_output.shape}, 值范围: [{nfm_output.min():.3f}, {nfm_output.max():.3f}]")

综合比较和训练示例

# 比较所有模型的输出
print("\n=== 模型输出比较 ===")
models = {
    'FNN': fnn_output,
    'PNN': pnn_output, 
    'ONN': onn_output,
    'NFM': nfm_output
}

for name, output in models.items():
    print(f"{name}: 均值={output.mean().item():.4f}, 标准差={output.std().item():.4f}")

# 简单的训练循环示例
def train_demo(model, data, epochs=100):
    """演示训练过程"""
    model.train()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    criterion = nn.BCELoss()
    
    # 创建模拟标签
    y_demo = torch.randint(0, 2, (data.size(0), 1)).float()
    
    for epoch in range(epochs):
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, y_demo)
        loss.backward()
        optimizer.step()
        
        if epoch % 20 == 0:
            print(f'Epoch {epoch}, Loss: {loss.item():.4f}')

# 演示训练NFM (最实用的模型)
print("\n=== NFM训练演示 ===")
train_demo(nfm_model, x_sparse, epochs=100)

关键要点总结

  1. FNN: 两阶段模型,先用FM初始化,再用DNN精调
  2. PNN: 在输入层后立即添加乘积操作来捕捉特征交叉
  3. ONN: PNN的扩展,支持多种特征交互操作(内积、外积、拼接等)
  4. NFM: 使用Bi-Interaction Pooling高效捕捉二阶特征交互,再输入DNN

这些简化示例展示了每个模型的核心思想。在实际应用中,您需要:

  • 使用真实的稀疏特征嵌入
  • 调整模型结构和超参数
  • 添加正则化防止过拟合
  • 使用合适的数据预处理和特征工程

希望这些示例能帮助您更好地理解这些推荐算法模型!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐