虾皮推荐算法一面
一、看你简历中写到了DeepFM,讲一下FM怎么实现的
FM 的核心思想就是用向量内积来学习特征之间的 二阶交互(如性别 × 购买记录)
FM通过以下公式优化交叉项的计算:
∑
i
=
1
n
∑
j
=
i
+
1
n
⟨
v
i
,
v
j
⟩
x
i
x
j
=
1
2
(
(
∑
i
=
1
n
v
i
x
i
)
2
−
∑
i
=
1
n
(
v
i
x
i
)
2
)
\sum_{i=1}^{n} \sum_{j=i+1}^{n} \langle v_i, v_j \rangle x_i x_j = \frac{1}{2} \left( \left( \sum_{i=1}^{n} v_i x_i \right)^2 - \sum_{i=1}^{n} (v_i x_i)^2 \right)
i=1∑nj=i+1∑n⟨vi,vj⟩xixj=21
(i=1∑nvixi)2−i=1∑n(vixi)2
这种优化将计算复杂度从
O
(
n
2
)
O(n^2)
O(n2) 降低到
O
(
n
)
O(n)
O(n),极大地提高了计算效率。
import torch
import torch.nn as nn
class FM(nn.Module):
def __init__(self, field_dims, embed_dim=8):
"""
参数:
- field_dims: List[int],每个类别特征的取值范围大小。
- embed_dim: int,嵌入向量维度。
"""
super().__init__()
self.num_fields = len(field_dims)
self.embed_dim = embed_dim
self.offsets = nn.Parameter(
torch.tensor([0] + field_dims[:-1]).cumsum(dim=0), requires_grad=False
)
total_dim = int(sum(field_dims))
# 一阶权重
self.linear_weight = nn.Embedding(total_dim, 1)
self.bias = nn.Parameter(torch.zeros(1))
# 嵌入向量
self.embedding = nn.Embedding(total_dim, embed_dim)
def forward(self, x_cat):
# x_cat: [B, F]
x = x_cat + self.offsets # 对齐到大 embedding 空间
# 一阶项
linear_part = self.linear_weight(x).sum(dim=1) + self.bias # [B,1]
# 二阶项
v = self.embedding(x) # [B,F,E]
sum_of_emb = v.sum(dim=1) # [B,E]
square_of_sum = sum_of_emb * sum_of_emb
sum_of_square = (v * v).sum(dim=1)
data = v * v
second_order = 0.5 * (square_of_sum - sum_of_square).sum(dim=1, keepdim=True)
return linear_part + second_order # [B,1]
if __name__ == "__main__":
field_dims = [1000, 500, 100]
model = FM(field_dims, embed_dim=8)
# batch 输入
x_cat = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.long)
logit = model(x_cat)
prob = torch.sigmoid(logit)
print("Logits:", logit)
print("Pred prob:", prob)
二、介绍一下常用的优化器。
神经网络的优化器(Optimizer)是用于更新模型参数(如权重和偏差)的算法,核心目标是最小化损失函数:
2.1 基础的优化器
- 随机梯度下降(SGD, Stochastic Gradient Descent)
- 公式:
θ
=
θ
−
η
⋅
∇
L
(
θ
)
\theta = \theta - \eta \cdot \nabla L(\theta)
θ=θ−η⋅∇L(θ)
- η \eta η:学习率
- ∇ L ( θ ) \nabla L(\theta) ∇L(θ):损失函数 𝐿(𝜃) 对参数 𝜃 的梯度
- 公式:
θ
=
θ
−
η
⋅
∇
L
(
θ
)
\theta = \theta - \eta \cdot \nabla L(\theta)
θ=θ−η⋅∇L(θ)
2.2 带动量的优化算法
- 动量梯度下降(Momentum SGD)
- 公式: v t = β v t − 1 + ( 1 − β ) ∇ L ( θ ) θ = θ − η v t v_t = \beta v_{t-1} + (1-\beta) \nabla L(\theta) \\ \ \\ \theta = \theta - \eta v_t vt=βvt−1+(1−β)∇L(θ) θ=θ−ηvt
- 通过指数加权平均(EMA)的方法,缓解震荡问题;
- 在陡峭方向收敛更快,在平坦方向减小震荡
- Nesterov 动量(NAG, Nesterov Accelerated Gradient)
- 公式 v t = β v t − 1 + ( 1 − β ) ∇ L ( θ − η v t − 1 ) θ = θ − η v t v_t = \beta v_{t-1} + (1 - \beta) \nabla L(\theta - \eta v_{t-1}) \\ \ \\ \theta = \theta - \eta v_t vt=βvt−1+(1−β)∇L(θ−ηvt−1) θ=θ−ηvt
- 计算梯度时使用“提前一步”的权重,收敛更快
- 比普通 Momentum 具有更好的泛化能力
2.3 自适应学习率优化算法
-
Adagrad(Adaptive Gradient)
- 公式: G t = G t − 1 + ∇ L ( θ ) ⊙ ∇ L ( θ ) θ = θ − η G t + ϵ ∇ L ( θ ) G_t = G_{t-1} + \nabla L(\theta) \odot \nabla L(\theta) \\ \ \\ \theta = \theta - \frac{\eta}{\sqrt{G_t} + \epsilon} \nabla L(\theta) Gt=Gt−1+∇L(θ)⊙∇L(θ) θ=θ−Gt+ϵη∇L(θ)
- 特点
- 自适应调整不同参数的学习率
- 主要问题是随着时间推移,学习率不断减小,最终趋近于 0
-
RMSprop(Root Mean Square Propagation)
- 公式:
G t = β G t − 1 + ( 1 − β ) ∇ L ( θ ) ⊙ ∇ L ( θ ) θ = θ − η G t + ϵ ∇ L ( θ ) G_t = \beta G_{t-1} + (1 - \beta) \nabla L(\theta) \odot \nabla L(\theta) \\ \ \\ \theta = \theta - \frac{\eta}{\sqrt{G_t} + \epsilon} \nabla L(\theta) Gt=βGt−1+(1−β)∇L(θ)⊙∇L(θ) θ=θ−Gt+ϵη∇L(θ) - 特点
- 解决了 Adagrad 学习率衰减过快的问题
- 适用于时序任务
- 公式:
-
Adam(Adaptive Moment Estimation)
m t = β 1 m t − 1 + ( 1 − β 1 ) ∇ L ( θ ) v t = β 2 v t − 1 + ( 1 − β 2 ) ∇ L ( θ ) ⊙ ∇ L ( θ ) m t ^ = m t 1 − β 1 t , v t ^ = v t 1 − β 2 t θ = θ − η v t ^ + ϵ m t ^ m_t = \beta_1 m_{t-1} + (1 - \beta_1) \nabla L(\theta) \\ \ \\ v_t = \beta_2 v_{t-1} + (1 - \beta_2) \nabla L(\theta) \odot \nabla L(\theta) \\ \ \\ \hat{m_t} = \frac{m_t}{1 - \beta_1^t}, \quad \hat{v_t} = \frac{v_t}{1 - \beta_2^t} \\ \ \\ \theta = \theta - \frac{\eta}{\sqrt{\hat{v_t}} + \epsilon} \hat{m_t} mt=β1mt−1+(1−β1)∇L(θ) vt=β2vt−1+(1−β2)∇L(θ)⊙∇L(θ) mt^=1−β1tmt,vt^=1−β2tvt θ=θ−vt^+ϵηmt^ -
AdamW(Weight Decay Adam)
- 公式:
θ = θ − η ( m t ^ v t ^ + ϵ + λ θ ) \theta = \theta - \eta \left( \frac{\hat{m_t}}{\sqrt{\hat{v_t}} + \epsilon} + \lambda \theta \right) θ=θ−η(vt^+ϵmt^+λθ) - 特点:
- AdamW 在 Adam 的基础上增加了 权重衰减(Weight Decay)
- 公式:
3.4. 其他优化算法
- AdaDelta
进一步改进 RMSprop,使学习率自适应变化
Δ θ t = − Δ θ t − 1 2 + ϵ G t + ϵ ∇ L ( θ ) \Delta \theta_t = - \frac{\sqrt{\Delta \theta_{t-1}^2 + \epsilon}}{\sqrt{G_t + \epsilon}} \nabla L(\theta) Δθt=−Gt+ϵΔθt−12+ϵ∇L(θ) - Nadam(Nesterov-accelerated Adam)
结合 Adam 和 NAG 的优化方法:
m t = β 1 m t − 1 + ( 1 − β 1 ) ∇ L ( θ ) m t ^ = m t 1 − β 1 t θ = θ − η ( β 1 m t ^ + ( 1 − β 1 ) ∇ L ( θ ) v t ^ + ϵ ) m_t = \beta_1 m_{t-1} + (1 - \beta_1) \nabla L(\theta) \\ \ \\ \hat{m_t} = \frac{m_t}{1 - \beta_1^t} \\ \ \\ \theta = \theta - \eta \left( \frac{\beta_1 \hat{m_t} + (1 - \beta_1) \nabla L(\theta)}{\sqrt{\hat{v_t}} + \epsilon} \right) mt=β1mt−1+(1−β1)∇L(θ) mt^=1−β1tmt θ=θ−η(vt^+ϵβ1mt^+(1−β1)∇L(θ)) - Lion(EvoLved Sign Momentum)
m t = β 1 m t − 1 + ( 1 − β 1 ) ∇ L ( θ ) θ = θ − η ( sign ( m t ) + λ θ ) m_t = \beta_1 m_{t-1} + (1 - \beta_1) \nabla L(\theta) \\ \ \\ \theta = \theta - \eta (\text{sign}(m_t) + \lambda \theta) mt=β1mt−1+(1−β1)∇L(θ) θ=θ−η(sign(mt)+λθ)
优化器 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
SGD | 适用于小型数据集 | 计算简单,稳定 | 震荡大,收敛慢 |
Momentum | 适用于梯度震荡较大的情况 | 解决震荡问题,加速收敛 | 需要调参(β 值) |
NAG | 适用于快速收敛的场景 | 提前更新,收敛更快 | 计算量略大 |
Adagrad | 适用于稀疏数据 | 自适应学习率 | 学习率下降过快 |
RMSprop | 适用于非平稳目标 | 平稳收敛,适用于 RNN | 需要调 β |
Adam | 适用于大多数任务 | 结合 Momentum 和 RMSprop | 泛化能力较差 |
AdamW | 适用于大规模深度网络 | 加入 Weight Decay,提高泛化 | 计算量略大 |
Nadam | 适用于 NLP 任务 | 结合 Adam 和 NAG | 计算量大 |
三、谈到对回归任务做多分桶,是如何实现的
为每个样本根据分桶规则(等频、等距)打上分桶标签,输出分类结果和回归结果
3.1. 后处理校正(最常用),强行约束结果
3.2. 用分类结果指导回归
y final = ∑ c p ( c ) ⋅ center ( c ) + Δ y reg y_{\text{final}} = \sum_c p(c) \cdot \text{center}(c) + \Delta y_{\text{reg}} yfinal=c∑p(c)⋅center(c)+Δyreg
- p ( c ) p(c) p(c) :分类预测的概率
- center ( c ) \text{center}(c) center(c) :桶 c c c 的中心值
- Δ y reg \Delta y_{\text{reg}} Δyreg :回归分支的修正项
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import KBinsDiscretizer
from sklearn.model_selection import train_test_split
import numpy as np
# ===== 1. 造数据 =====
np.random.seed(42)
X = np.random.rand(500, 5)
y = 10 * X[:, 0] + 5 * X[:, 1]**2 + np.random.randn(500)
# ===== 2. 分桶 =====
n_bins = 5
discretizer = KBinsDiscretizer(n_bins=n_bins, encode="ordinal", strategy="quantile")
y_class = discretizer.fit_transform(y.reshape(-1, 1)).astype(int).ravel()
# train/test
X_train, X_test, y_class_train, y_class_test, y_reg_train, y_reg_test = train_test_split(
X, y_class, y, test_size=0.2, random_state=42
)
# 转 tensor
X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_class_train = torch.tensor(y_class_train, dtype=torch.long)
y_class_test = torch.tensor(y_class_test, dtype=torch.long)
y_reg_train = torch.tensor(y_reg_train, dtype=torch.float32).view(-1,1)
y_reg_test = torch.tensor(y_reg_test, dtype=torch.float32).view(-1,1)
# ===== 3. 定义 MLP 模型 =====
class MultiTaskMLP(nn.Module):
def __init__(self, input_dim, hidden_dim, n_classes):
super().__init__()
# 共享层
self.shared = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU()
)
# 分类头
self.class_head = nn.Linear(hidden_dim, n_classes)
# 回归头
self.reg_head = nn.Linear(hidden_dim, 1)
def forward(self, x):
h = self.shared(x)
class_out = self.class_head(h) # logits
reg_out = self.reg_head(h) # continuous
return class_out, reg_out
# ===== 4. 初始化模型 =====
model = MultiTaskMLP(input_dim=5, hidden_dim=64, n_classes=n_bins)
criterion_cls = nn.CrossEntropyLoss()
criterion_reg = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.01)
# ===== 5. 训练 =====
epochs = 100
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
out_cls, out_reg = model(X_train)
loss_cls = criterion_cls(out_cls, y_class_train)
loss_reg = criterion_reg(out_reg, y_reg_train)
loss = loss_cls + loss_reg # 联合loss
loss.backward()
optimizer.step()
if (epoch+1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs}, Loss={loss.item():.4f}, "
f"Cls={loss_cls.item():.4f}, Reg={loss_reg.item():.4f}")
# ===== 6. 评估 =====
model.eval()
with torch.no_grad():
pred_cls, pred_reg = model(X_test)
acc = (pred_cls.argmax(1) == y_class_test).float().mean().item()
mse = criterion_reg(pred_reg, y_reg_test).item()
print("桶分类准确率:", acc)
print("连续值预测 MSE:", mse)
3.3. 给回归头的loss添加约束项()
def consistency_loss(pred_reg, pred_cls, bin_edges):
pred_bucket = torch.argmax(pred_cls, dim=1)
bin_edges = torch.tensor(bin_edges, dtype=torch.float32, device=pred_reg.device)
low = bin_edges[pred_bucket]
high = bin_edges[pred_bucket + 1]
y_val = pred_reg.squeeze(1)
# relu : max(0,x)
penalty = torch.relu(low - y_val) + torch.relu(y_val - high)
return penalty.mean()
# ===== 5. 训练 =====
bin_edges = discretizer.bin_edges_[0] # 取第 0 个特征的 bin_edges
print(bin_edges)
epochs = 100
for epoch in range(epochs):
model.train()
optimizer.zero_grad()
out_cls, out_reg = model(X_train)
loss_cls = criterion_cls(out_cls, y_class_train)
loss_reg = criterion_reg(out_reg, y_reg_train)
loss_cons = consistency_loss(out_reg, out_cls, bin_edges) # 约束项
loss = loss_cls + loss_reg + loss_cons
loss.backward()
optimizer.step()
if (epoch+1) % 10 == 0:
print(f"Epoch {epoch+1}/{epochs}, Loss={loss.item():.4f}, "
f"Cls={loss_cls.item():.4f}, Reg={loss_reg.item():.4f}")
# ===== 6. 评估 =====
model.eval()
with torch.no_grad():
pred_cls, pred_reg = model(X_test)
acc = (pred_cls.argmax(1) == y_class_test).float().mean().item()
mse = criterion_reg(pred_reg, y_reg_test).item()
print("桶分类准确率:", acc)
print("连续值预测 MSE:", mse)
四、除了梯度下降,还有哪些参数求解算法?
在机器学习和深度学习中,除了梯度下降(Gradient Descent)之外,还有多种用于参数求解的算法,具体如下:
方法类别 | 是否迭代 | 是否适合大规模 | 是否可导 | 典型应用 |
---|---|---|---|---|
正规方程 | 否 | 否 | 可导 | 线性回归 |
牛顿法 | 是 | 否 | 可导 | 小型回归或分类问题 |
L-BFGS | 是 | ✅ | 可导 | 逻辑回归、神经网络 |
坐标下降 | 是 | ✅ | 可导/分段导 | Lasso、稀疏模型 |
ALS | 是 | ✅ | 不要求可导 | 推荐系统、矩阵分解 |
EM算法 | 是 | 中等 | 不要求 | GMM、隐马尔可夫模型 |
遗传算法/PSO | 是 | ✅ | 不可导也可 | 黑盒优化、组合优化问题 |
4.1. 用牛顿法和梯度下降法求解 sqrt(x)
见【搜广推校招面经八十】