MMoE 多任务学习实战:从人口统计预测到金融信用风险与授信额度联合建模
MMoE(Multi-gate Mixture-of-Experts,多门控混合专家网络) 通过引入共享专家网络和任务专属门控,让不同任务动态选择所需要的特征表示,在共享信息的同时保留差异。
本文以 Google 提出的经典 MMoE 架构为基础,结合两个典型场景,介绍模型原理和 PyTorch 实践:一是利用 Census Income 数据同时预测收入水平和婚姻状态;二是参考消费金融领域的真实研究,将违约风险分类与授信额度回归整合到一个多任务模型中。最后通过 Shared-Bottom 对照实验,分析 MMoE 的实际价值及适用条件。
一、为什么需要 MMoE?
1. 从单任务学习说起
假设一家消费金融机构希望同时完成两个预测任务。
任务一:预测客户未来是否违约。
输入客户的征信评分、历史逾期、负债水平和收入等信息,输出未来违约概率。
任务二:预测客户的授信额度。
输入相同或相近的客户信息,预测一个额度数值。
传统方法通常为两个任务分别训练模型:
客户数据
|
+---------+---------+
| |
违约预测模型 额度预测模型
| |
违约概率 授信额度
这种方案容易实现,但两个模型分别学习特征。
例如,收入、负债率、历史信用表现等变量可能同时影响两个目标。分别训练模型时,这些共同规律需要重复学习。
一种自然的改进方式是共享部分神经网络参数。
2. Shared-Bottom:最简单的多任务学习
Shared-Bottom 是经典的多任务神经网络结构。
客户特征 X
|
v
Shared Bottom
共享特征提取层
|
+--------+--------+
| |
Tower A Tower B
| |
违约概率 授信额度
两个任务共用底层网络,再使用各自的任务塔输出预测结果。
问题在于:不同任务对特征的需求可能并不一致。
例如:
- 历史逾期次数对违约风险预测非常重要。
- 收入和负债负担对额度预测可能更加重要。
- 某些特征对一个任务具有正向影响,对另一个任务的影响可能较弱,甚至呈现不同方向。
如果两个任务的梯度更新存在冲突,Shared-Bottom 的共享参数可能同时受到不同目标的拉扯。
这种现象称为负迁移(Negative Transfer)。
3. MMoE 的解决方法
Google 的研究人员在 KDD 2018 论文《Modeling Task Relationships in Multi-task Learning with Multi-gate Mixture-of-Experts》中提出了 MMoE。
其核心思想是:
让多个任务共享一组专家网络,再通过各自的 Gate 动态决定使用哪些专家,以及使用多大权重。
架构如下:
输入特征 X
|
+------------+------------+
| | |
Expert 1 Expert 2 Expert 3
| | |
+------------+------------+
|
+------------+------------+
| |
Gate A Gate B
违约风险门控 授信额度门控
| |
专家加权组合 专家加权组合
| |
Tower A Tower B
| |
违约概率 PD 授信额度
MMoE 由三个部分组成。
| 组件 | 主要功能 | 是否共享 |
|---|---|---|
| Expert | 提取输入特征的潜在表示 | 所有任务共享 |
| Gate | 根据输入决定各专家的组合权重 | 每个任务独立 |
| Tower | 学习任务特有的映射关系并输出预测 | 每个任务独立 |
例如,一个模型拥有三个专家。
对于某个客户,两个任务可能学习到不同的权重:
| 专家 | 违约任务 | 额度任务 |
|---|---|---|
| Expert 1 | 0.65 | 0.15 |
| Expert 2 | 0.25 | 0.30 |
| Expert 3 | 0.10 | 0.55 |
| 合计 | 1.00 | 1.00 |
这是示意数据。
可以看到,两个任务共用相同的三个专家,但违约任务更加依赖 Expert 1,而额度任务更加依赖 Expert 3。
实际训练时,专家通常没有预先指定的业务含义,其特征提取能力由模型训练自动学习。
而且,Gate 权重还会随客户输入变化,因此不同客户可能拥有不同的专家组合。
二、MMoE 的数学原理
假设模型有 (N) 个专家,输入特征为 (x)。
第 (i) 个专家网络输出:
[
E_i(x)
]
对于第 (k) 个任务,门控网络产生:
[
g_k(x)=\operatorname{Softmax}(W_kx+b_k)
]
其中:
[
g_k(x)=[g_{k,1}(x),\ldots,g_{k,N}(x)]
]
满足:
[
\sum_{i=1}^{N}g_{k,i}(x)=1
]
随后将专家输出加权组合:
[
h_k(x)=\sum_{i=1}^{N}g_{k,i}(x)E_i(x)
]
最后由任务塔输出预测:
[
\hat y_k=T_k(h_k(x))
]
如果有两个任务,整体损失函数通常写作:
[
\boxed{
L=\lambda_1L_1+\lambda_2L_2
}
]
其中:
- (L_1):任务一的损失。
- (L_2):任务二的损失。
- (\lambda_1,\lambda_2):不同任务的损失权重。
例如:
- 违约预测使用 Binary Cross-Entropy。
- 授信额度预测使用 Mean Squared Error。
两个任务可以拥有不同类型的输出。
需要注意,标准 MMoE 通常会计算全部专家的输出,再通过 Softmax 权重进行组合。它的主要作用是改善多任务特征共享,不应直接套用大语言模型稀疏 MoE 的计算节省结论。
三、先实现一个通用 MMoE 模型
为了更容易理解后面的两个案例,我们先用 PyTorch 实现一个能够处理两个任务的通用 MMoE。
同时实现一个 Shared-Bottom 基线,以便进行对照。
安装依赖:
pip install torch numpy pandas scikit-learn
创建文件 mmoe_core_demo.py。
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
class MultiTaskNet(nn.Module):
def __init__(
self,
n_features,
mode="mmoe",
n_experts=4,
hidden=32
):
super().__init__()
self.mode = mode
if mode == "mmoe":
# 多个共享专家
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(n_features, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU()
)
for _ in range(n_experts)
])
# 每个任务拥有自己的门控
self.gates = nn.ModuleList([
nn.Linear(n_features, n_experts)
for _ in range(2)
])
elif mode == "shared":
# Shared-Bottom 对照模型
self.bottom = nn.Sequential(
nn.Linear(n_features, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU()
)
else:
raise ValueError("Invalid mode")
# 两个独立的任务塔
self.towers = nn.ModuleList([
nn.Sequential(
nn.Linear(hidden, 16),
nn.ReLU(),
nn.Linear(16, 1)
)
for _ in range(2)
])
def forward(self, x, return_gates=False):
if self.mode == "shared":
h = self.bottom(x)
output = torch.cat([
tower(h)
for tower in self.towers
], dim=1)
if return_gates:
return output, None
return output
# [Batch, Experts, Hidden]
expert_output = torch.stack([
expert(x)
for expert in self.experts
], dim=1)
logits = []
gate_weights = []
for gate, tower in zip(
self.gates,
self.towers
):
# [Batch, Experts]
weights = torch.softmax(
gate(x), dim=1
)
# 对专家输出进行加权
h = (
expert_output *
weights.unsqueeze(-1)
).sum(dim=1)
logits.append(tower(h))
gate_weights.append(weights)
output = torch.cat(logits, dim=1)
if return_gates:
return output, gate_weights
return output
def train_model(
model,
x_train,
y_train,
x_valid,
y_valid,
kinds=("binary", "binary"),
epochs=12,
batch_size=512,
lr=0.002
):
torch.manual_seed(42)
optimizer = torch.optim.Adam(
model.parameters(),
lr=lr,
weight_decay=1e-4
)
loader = DataLoader(
TensorDataset(
torch.as_tensor(
x_train, dtype=torch.float32
),
torch.as_tensor(
y_train, dtype=torch.float32
)
),
batch_size=batch_size,
shuffle=True
)
xv = torch.as_tensor(
x_valid, dtype=torch.float32
)
yv = torch.as_tensor(
y_valid, dtype=torch.float32
)
for epoch in range(epochs):
model.train()
for xb, yb in loader:
out = model(xb)
losses = [
(
nn.functional.binary_cross_entropy_with_logits(
out[:, k], yb[:, k]
)
if kind == "binary"
else nn.functional.mse_loss(
out[:, k], yb[:, k]
)
)
for k, kind in enumerate(kinds)
]
loss = losses[0] + losses[1]
optimizer.zero_grad()
loss.backward()
optimizer.step()
# 输出验证集损失
if epoch in (0, epochs - 1):
model.eval()
with torch.no_grad():
out = model(xv)
val_losses = [
(
nn.functional.binary_cross_entropy_with_logits(
out[:, k], yv[:, k]
)
if kind == "binary"
else nn.functional.mse_loss(
out[:, k], yv[:, k]
)
)
for k, kind in enumerate(kinds)
]
print(
model.mode,
"epoch:", epoch + 1,
"valid_loss:",
[round(v.item(), 4) for v in val_losses]
)
return model
def predict(model, x):
model.eval()
with torch.no_grad():
output = model(
torch.as_tensor(
x, dtype=torch.float32
)
)
return output.numpy()
这个实现包含两种模式:
mode="shared":Shared-Bottom。mode="mmoe":MMoE。
两种模式都使用两个独立的任务塔,因此可以比较多门控专家机制带来的影响。
train_model() 还支持两种任务类型:
kinds=("binary", "binary")
表示两个二分类任务。
或者:
kinds=("binary", "regression")
表示一个分类任务和一个回归任务。
这样,我们就可以在两个不同的业务场景中复用同一套模型。
四、案例一:Census Income——同时预测收入与婚姻状态
1. 真实研究背景
MMoE 原始论文使用过 Census-Income 数据进行多任务分类实验,以分析任务相关程度对模型训练的影响。
公开的 MMoE 实现也经常采用两个预测任务:
- 预测个人收入是否超过每年 50,000 美元。
- 预测个人是否从未结婚。
这两个任务并非完全独立。
年龄、工作类型、教育程度、职业等变量可能同时与收入和婚姻状态存在统计关联。
但两个任务关注的变量关系又有所不同,因此适合演示多任务学习。
原始研究使用的 Census-Income 数据版本包含约 29.9 万条记录。
为了让实践更容易运行,我们改用 UCI 的 Adult(Census Income) 数据集。该版本共有 48,842 条记录,下面使用其中的 adult.data 文件,包含 32,561 条训练来源记录,并重新划分训练、验证和测试集。
这与原论文的数据版本和实验设置有所区别,因此属于独立的教学实验。
2. 获取数据
前往 UCI Machine Learning Repository,搜索 Adult。
下载:
adult.data
将文件放在程序目录中。
需要注意数据泄漏问题。
由于我们需要同时预测收入和婚姻状态,因此:
income是任务一的标签。marital-status是任务二的标签。
这两个字段都应当从输入特征中删除。
此外,relationship 字段中包含 Husband、Wife 等信息,与婚姻状态高度重合,也应当从输入中排除,以免模型通过直接代理变量完成预测。
本实验用于研究多任务学习。现实中若要将婚姻状态预测用于个人决策,需要另外考虑用途合理性、隐私保护和公平性。
3. 完整实践代码
创建 mmoe_census_demo.py,与前面的 mmoe_core_demo.py 放在同一目录。
import numpy as np
import pandas as pd
import torch
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from mmoe_core_demo import (
MultiTaskNet,
train_model,
predict
)
# 1. 数据字段
cols = [
"age", "workclass", "fnlwgt",
"education", "education-num",
"marital-status", "occupation",
"relationship", "race", "sex",
"capital-gain", "capital-loss",
"hours-per-week", "native-country",
"income"
]
# 2. 读取 Adult 数据
df = pd.read_csv(
"adult.data",
header=None,
names=cols,
na_values="?",
skipinitialspace=True
)
# 3. 构造两个任务标签
y_income = (
df["income"].str.strip() == ">50K"
).astype("float32")
y_marital = (
df["marital-status"] == "Never-married"
).astype("float32")
y = np.stack(
[y_income, y_marital],
axis=1
)
# 4. 定义输入变量
num_cols = [
"age",
"education-num",
"capital-gain",
"capital-loss",
"hours-per-week"
]
cat_cols = [
"workclass",
"occupation",
"education"
]
X = df[num_cols + cat_cols].copy()
# 5. 划分训练、验证和测试集
train_idx, rem_idx = train_test_split(
np.arange(len(df)),
test_size=0.4,
stratify=y[:, 0],
random_state=42
)
valid_idx, test_idx = train_test_split(
rem_idx,
test_size=0.5,
stratify=y[rem_idx, 0],
random_state=42
)
# 6. 数值和类别特征预处理
preprocess = ColumnTransformer([
(
"num",
Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler())
]),
num_cols
),
(
"cat",
Pipeline([
(
"imputer",
SimpleImputer(strategy="most_frequent")
),
(
"onehot",
OneHotEncoder(
handle_unknown="ignore",
sparse_output=False
)
)
]),
cat_cols
)
])
# 只使用训练集拟合预处理器
x_train = preprocess.fit_transform(
X.iloc[train_idx]
).astype("float32")
x_valid = preprocess.transform(
X.iloc[valid_idx]
).astype("float32")
x_test = preprocess.transform(
X.iloc[test_idx]
).astype("float32")
# 7. 分别训练 Shared-Bottom 和 MMoE
for mode in ["shared", "mmoe"]:
torch.manual_seed(42)
model = MultiTaskNet(
n_features=x_train.shape[1],
mode=mode
)
train_model(
model,
x_train,
y[train_idx],
x_valid,
y[valid_idx],
epochs=8
)
# 8. 计算两个任务的预测概率
logits = predict(model, x_test)
probs = 1 / (1 + np.exp(-logits))
# 9. 分别计算 AUC
auc_income = roc_auc_score(
y[test_idx, 0],
probs[:, 0]
)
auc_marital = roc_auc_score(
y[test_idx, 1],
probs[:, 1]
)
print(
mode,
"Income AUC:", round(auc_income, 4),
"Marital AUC:", round(auc_marital, 4)
)
4. 结果应该如何分析?
程序会输出两组结果:
Shared-Bottom
Income AUC: ...
Marital AUC: ...
MMoE
Income AUC: ...
Marital AUC: ...
真实数值需要在下载的数据集上训练后得到。
我们重点关注:
| 对比内容 | 需要回答的问题 |
|---|---|
| 收入预测 AUC | MMoE 是否改善收入预测? |
| 婚姻状态 AUC | MMoE 是否改善第二个任务? |
| 两个任务的变化 | 是否存在一个任务改善、另一个任务退化? |
| 多次随机种子实验 | 性能变化是否稳定? |
| 训练与推理时间 | 性能收益是否值得额外计算成本? |
例如,假设实验结果如下:
| 模型 | 收入 AUC | 婚姻状态 AUC |
|---|---|---|
| Shared-Bottom | 0.87 | 0.84 |
| MMoE | 0.88 | 0.86 |
这些数字是示意值。
如果两个任务都获得改善,说明在当前实验设置下,任务专属门控可能帮助模型更有效地组合共享特征。
但如果出现:
| 模型 | 收入 AUC | 婚姻状态 AUC |
|---|---|---|
| Shared-Bottom | 0.87 | 0.84 |
| MMoE | 0.89 | 0.82 |
则说明收入预测有所改善,婚姻状态预测反而退化。
这就是多任务学习中值得重点观察的 跷跷板现象(Seesaw Phenomenon)。
MMoE 能够缓解部分任务冲突,但不能保证每个任务都获得提升。
五、案例二:消费金融——同时预测违约风险与授信额度
1. 真实业务研究背景
第二个案例来自消费金融领域。
2021 年,阿里巴巴等机构的研究人员在 WSDM 会议发表论文:
《Credit Risk and Limits Forecasting in E-Commerce Consumer Lending Service via Multi-view-aware Mixture-of-experts Nets》
论文研究了两个关联任务:
- Credit Risk Forecasting: 预测客户信用风险。
- Credit Limits Forecasting: 预测授信额度。
研究提出了 MvMoE(Multi-view-aware Mixture-of-Experts),在多任务专家网络基础上结合多视图特征提取和更复杂的网络结构。
论文使用约 544 万名用户的真实业务数据进行实验,并报告了相对于其比较方法的风险预测 AP 和额度预测 MAE 的改善。
这说明,MMoE 的专家共享思想除了推荐系统,还可以用于金融风险与授信业务。
但是,这项研究使用的是 MvMoE 扩展架构,原始业务数据也没有作为公开实验数据提供。
因此,下面采用简化 MMoE 和合成客户数据,演示分类任务与回归任务如何在同一个网络中联合训练。
2. 为什么违约风险和授信额度适合联合建模?
这两个任务拥有一部分共同信息。
例如:
| 输入特征 | 对违约风险的潜在影响 | 对额度预测的潜在影响 |
|---|---|---|
| 信用评分 | 识别历史信用风险 | 反映风险承受能力 |
| 月收入 | 影响偿债能力 | 影响可承担额度 |
| 负债收入比 | 衡量债务压力 | 约束潜在授信额度 |
| 额度使用率 | 反映用信压力 | 反映客户用信状态 |
| 历史逾期次数 | 识别信用恶化 | 反映风险约束 |
| 信用查询次数 | 反映近期融资活动 | 辅助判断借贷需求 |
两个任务涉及相近的客户特征,但学习目标不同。
违约风险需要输出概率:
[
\hat y_1=P(\text{Default}=1\mid X)
]
额度预测需要输出一个连续数值:
[
\hat y_2=\text{Credit Limit}
]
因此,可以设计:
[
L=L_{\text{BCE}}+\lambda L_{\text{MSE}}
]
其中分类任务使用二元交叉熵,回归任务使用均方误差。
这里有一个重要细节:额度通常以千元或万元计量,如果直接计算 MSE,其损失值可能远高于分类任务的 BCE。
因此,训练前通常需要对额度标签进行标准化,或者合理调整任务损失权重。
3. 构造模拟金融数据
我们模拟 12,000 名客户,每位客户包含以下特征:
- 月收入。
- 信用评分。
- 额度使用率。
- 负债收入比。
- 历史逾期次数。
- 信用历史长度。
- 征信查询次数。
- 现有负债余额。
同时生成两个标签:
label_default:是否发生违约。limit:模拟额度标签。
这里的额度标签由预设的生成规则产生,只用于训练技术演示。真实业务中,历史批准额度受到既有审批政策和风险偏好的影响,不能直接视为客户的最优授信额度。
4. Python 完整实践
创建 mmoe_credit_demo.py。
import numpy as np
import torch
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (
roc_auc_score,
average_precision_score,
mean_absolute_error
)
from mmoe_core_demo import (
MultiTaskNet,
train_model,
predict
)
torch.manual_seed(42)
rng = np.random.default_rng(42)
# 1. 模拟客户数据
n = 12000
income = rng.lognormal(
np.log(12000), 0.45, n
)
score = np.clip(
rng.normal(665, 55, n),
350, 850
)
util = rng.beta(2.3, 2.2, n)
dti = rng.uniform(0.05, 0.9, n)
late = rng.poisson(0.4, n)
tenure = rng.uniform(0, 15, n)
queries = rng.poisson(1.2, n)
balance = (
income * util *
rng.uniform(0.3, 1.5, n)
)
# 2. 构造输入特征
raw_features = np.column_stack([
np.log1p(income),
score,
util,
dti,
late,
tenure,
queries,
np.log1p(balance)
]).astype("float32")
# 3. 模拟违约概率
log_odds = (
-2.7
- 0.017 * (score - 650)
+ 1.6 * util
+ 1.1 * dti
+ 0.3 * late
+ 0.16 * queries
- 0.03 * tenure
)
p_default = 1 / (1 + np.exp(-log_odds))
# 根据模拟概率生成违约标签
label_default = rng.binomial(
1, p_default
)
# 4. 模拟额度标签
limit = (
income
* (0.55 + 0.6 * (score - 450) / 400)
* (1 - 0.35 * util)
* (1 - 0.2 * dti)
+ rng.normal(0, 2000, n)
)
limit = np.clip(
limit, 1000, 40000
)
# 5. 划分训练、验证、测试集
idx_train, idx_rem = train_test_split(
np.arange(n),
test_size=0.4,
random_state=42,
stratify=label_default
)
idx_val, idx_test = train_test_split(
idx_rem,
test_size=0.5,
random_state=42,
stratify=label_default[idx_rem]
)
# 6. 特征标准化
scaler_x = StandardScaler().fit(
raw_features[idx_train]
)
x = scaler_x.transform(
raw_features
).astype("float32")
# 7. 额度标签标准化
limit_mean = limit[idx_train].mean()
limit_std = limit[idx_train].std()
y = np.stack([
label_default,
(limit - limit_mean) / limit_std
], axis=1).astype("float32")
# 8. 对比 Shared-Bottom 与 MMoE
for mode in ["shared", "mmoe"]:
torch.manual_seed(42)
model = MultiTaskNet(
n_features=x.shape[1],
mode=mode
)
train_model(
model,
x[idx_train],
y[idx_train],
x[idx_val],
y[idx_val],
kinds=("binary", "regression"),
epochs=8
)
# 9. 预测
pred = predict(
model,
x[idx_test]
)
# 分类输出转换为概率
prob = 1 / (1 + np.exp(-pred[:, 0]))
# 额度还原到原始单位
predicted_limit = (
pred[:, 1] * limit_std
+ limit_mean
)
# 10. 分别评价两个任务
auc = roc_auc_score(
label_default[idx_test],
prob
)
ap = average_precision_score(
label_default[idx_test],
prob
)
mae = mean_absolute_error(
limit[idx_test],
predicted_limit
)
print(
mode,
"AUC:", round(auc, 4),
"AP:", round(ap, 4),
"Limit MAE:", round(mae, 2)
)
# 11. 查看两个任务的 Gate 权重
if mode == "mmoe":
with torch.no_grad():
_, gate_w = model(
torch.as_tensor(
x[idx_test][:200],
dtype=torch.float32
),
return_gates=True
)
print(
"Default Gate:",
gate_w[0].mean(0).numpy().round(3)
)
print(
"Limit Gate:",
gate_w[1].mean(0).numpy().round(3)
)
5. 如何评价两个任务?
这里的两个任务采用不同的指标。
| 任务 | 输出 | 评价指标 |
|---|---|---|
| 违约风险预测 | 0~1 概率 | AUC、AP |
| 额度预测 | 连续数值 | MAE |
| Gate 分析 | 专家权重 | 权重分布 |
AUC 和 AP 越高通常越好,MAE 越低通常越好。
在本次合成数据测试中,使用 CPU、固定随机种子及上述 8 轮训练设置,得到以下结果:
| 模型 | 违约 AUC | 违约 AP | 额度 MAE |
|---|---|---|---|
| Shared-Bottom | 0.7509 | 0.4721 | 1614.77 |
| MMoE | 0.7484 | 0.4692 | 1614.78 |
这是上述合成数据程序的实际测试结果,并非 WSDM 论文的实验成绩。
可以看到,在这组较简单的数据生成机制下,MMoE 并没有优于 Shared-Bottom。
这恰恰说明:MMoE 的优势取决于任务关系、数据结构、模型容量和训练方式。增加专家网络与门控机制,并不会自动带来更高的预测准确率。
在这组数据中,两个任务的规律都相对简单,共享底层网络已经可以学习大部分有效特征。
因此,采用更复杂的 MMoE 不一定有明显收益。
如果真实业务中的任务相关程度更复杂,或不同任务存在更加明显的特征偏好与梯度冲突,MMoE 才更有机会体现价值。
6. Gate 权重能告诉我们什么?
代码最后还会输出两个任务的平均专家权重。
例如,本次实验的部分测试结果为:
Default Gate:
[0.155, 0.328, 0.330, 0.186]
Limit Gate:
[0.302, 0.277, 0.245, 0.175]
四个数字分别对应四个共享专家。
这说明两个任务学习到了不同的专家组合。
不过,Gate 权重只能直接说明模型内部的特征表示分配方式。
例如,Expert 1 在授信额度任务中权重更高,并不意味着它一定专门学习收入或偿债能力。
如果希望解释专家网络究竟学到了什么,还需要结合特征扰动、梯度分析、消融实验以及具体业务数据进行判断。
六、MMoE 的关键参数怎么调?
MMoE 的核心超参数主要集中在专家数量、专家网络结构、任务塔和损失权重。
| 参数 | 作用 | 实践建议 |
|---|---|---|
n_experts |
专家网络数量 | 可以从 2、4、8 个进行比较 |
hidden |
专家网络隐藏层维度 | 小规模表格数据可从 16~64 开始 |
learning_rate |
学习率 | Adam 可以从 0.001~0.003 尝试 |
batch_size |
每批样本数量 | 根据数据规模与显存调整 |
loss_weights |
各任务损失权重 | 重点控制任务之间的梯度影响 |
tower |
任务塔深度与宽度 | 可根据任务难度分别调整 |
weight_decay |
权重正则化 | 用于控制模型复杂度 |
这里有三个比较容易被忽略的细节。
1. 专家数量越多越好吗?
假设模型只有两个任务,却使用 64 个专家。
如果数据量不大,部分专家可能学不到稳定的规律。
此外,标准 MMoE 通常会计算全部专家,因此专家数量增加会直接增加参数量和计算成本。
对于一般的结构化表格任务,可以先从 4 个专家开始。
然后比较 2、4、8 个专家的性能变化。
2. 多任务损失是否应该直接相加?
假设一个模型同时预测:
- 信用卡是否违约:二分类。
- 信用卡未来消费金额:回归。
如果消费金额范围在数万元,那么直接使用 MSE,其数值可能远大于 BCE。
这样在训练时,回归任务可能主导梯度更新。
因此,可以考虑:
- 对连续目标进行标准化。
- 为任务损失设置不同权重。
- 分析各任务对共享参数产生的梯度大小。
- 在更复杂的项目中研究动态损失权重。
本文第二个案例先将额度标签标准化,再进行多任务训练,就是为了减少两个任务损失尺度的差异。
但标准化只能缓解数值尺度问题,并不能自动消除任务之间的梯度冲突。
3. 如何判断发生了负迁移?
只比较 MMoE 和 Shared-Bottom 还不够。
更完整的实验应当至少包含三个基线:
| 模型 | 架构 | 比较目标 |
|---|---|---|
| Single-Task | 每个任务独立模型 | 观察各任务独立建模能力 |
| Shared-Bottom | 共享底层网络 | 判断简单共享是否有效 |
| MMoE | 共享专家+独立门控 | 判断任务动态共享是否有价值 |
例如,假设某次实验结果为:
| 模型 | 任务 A AUC | 任务 B AUC |
|---|---|---|
| Single-Task | 0.85 | 0.81 |
| Shared-Bottom | 0.84 | 0.83 |
| MMoE | 0.87 | 0.84 |
这是一组示意数据。
从中可以发现:
Shared-Bottom 提高了任务 B 的性能,但任务 A 出现下降,可能存在负迁移。
MMoE 则在两个任务上都获得改善,表明其门控共享机制可能缓解了冲突。
真实项目还应进一步使用多个随机种子重复实验,以防止把初始化或数据划分产生的偶然差异误判为架构优势。
七、MMoE 与 PLE 有什么区别?
研究 MMoE 时,经常会接触到另一个多任务架构:PLE(Progressive Layered Extraction)。
二者都试图处理多任务学习中的信息共享与任务冲突。
但专家组织方式有所区别。
MMoE:
所有任务共享一组专家,每个任务通过自己的 Gate 分配权重。
PLE:
进一步区分共享专家和任务专属专家,并通过分层结构逐步提取不同任务需要的信息。
| 维度 | Shared-Bottom | MMoE | PLE |
|---|---|---|---|
| 共享特征 | 是 | 是 | 是 |
| 独立任务塔 | 是 | 是 | 是 |
| 多专家结构 | 通常无 | 有 | 有 |
| 每任务独立 Gate | 通常无 | 有 | 有 |
| 显式任务专属专家 | 通常无 | 无 | 有 |
| 结构复杂度 | 较低 | 中等 | 较高 |
在任务关联度较高、数据规模有限的情况下,Shared-Bottom 可能已经足够。
当不同任务对共享特征的需求存在明显差异时,可以尝试 MMoE。
如果任务之间冲突较明显,并且拥有足够数据支持更复杂的参数结构,可以进一步考虑 PLE。
需要注意,具体效果取决于任务设置和实验数据。正式项目中,应以统一的数据切分、训练预算和评价指标进行比较。
八、MMoE 在金融风控中需要特别注意什么?
MMoE 可以很自然地扩展到信用风险管理,但金融数据具有一些不同于普通推荐数据的特点。
1. 不同任务可能拥有不同的观察期
例如,某银行希望同时预测:
- 未来 1 个月是否出现 30+ DPD。
- 未来 3 个月是否出现 60+ DPD。
- 未来 12 个月是否发生违约。
从网络架构上看,可以为三个任务分别设置 Gate 和 Tower。
但前提是每个任务都具有定义清晰的标签。
如果某个客户只观察了未来三个月,就无法直接确定其未来十二个月是否发生违约。
因此,需要根据各任务的表现期成熟情况,处理标签缺失和观察删失。
2. 不同任务的训练样本可能不一致
例如:
- 动支意愿可以在全部申请客户中观察。
- 还款表现通常只能在实际获得贷款的客户中观察。
- 信用损失或回收率通常只能在满足特定违约条件的账户中观察。
这些任务可以共享部分输入信息,但标签可观测范围不同。
在真实项目中,通常需要对各任务设置相应的样本掩码:
[
L=
\sum_k\lambda_k
\frac{
\sum_i m_{ik}L_{ik}
}{
\sum_i m_{ik}
}
]
其中:
[
m_{ik}=
\begin{cases}
1,&\text{样本 }i\text{ 的任务 }k\text{ 标签有效}\
0,&\text{该标签不可观测}
\end{cases}
]
这种方法能够控制缺失标签参与训练的问题。
但它无法自动解决由历史授信审批、客户选择或数据缺失机制引起的样本偏差。
3. 多期限 PD 可能需要概率一致性约束
例如,同一观察时点、同一违约定义下:
[
PD_{1M}\leq PD_{3M}\leq PD_{12M}
]
这是累计违约概率应当满足的关系。
如果分别设置三个独立输出头,模型可能产生:
PD_1M = 5%
PD_3M = 4%
PD_12M = 8%
这种结果违反了累计概率随预测期限不下降的要求。
因此,进行多期限 PD 建模时,可以考虑预测逐期条件违约概率,再通过生存概率关系生成累计 PD。
例如:
[
PD(1,T)=1-\prod_{t=1}^{T}(1-h_t)
]
这里的 (h_t) 表示此前未违约条件下,第 (t) 期发生违约的概率。
MMoE 可以负责提取多期限任务所需的共享特征,但输出层仍然需要满足相应的业务与数学约束。
九、总结
MMoE 的核心思想可以概括为:
多个任务共享专家网络,每个任务通过独立 Gate 动态组合专家输出,再由任务专属 Tower 完成预测。
它主要用于解决多任务学习中的特征共享与任务差异问题。
本文通过两个应用场景介绍了 MMoE。
第一个案例基于公开人口统计数据,同时预测收入水平和婚姻状态,展示 MMoE 在两个二分类任务中的应用。
第二个案例参考真实消费金融研究,将违约风险预测与授信额度预测结合起来,展示 MMoE 如何同时处理分类和回归目标。
其中最重要的实验结论是:MMoE 的结构更加灵活,但模型复杂度的增加并不保证预测性能提高。
在本文的合成金融数据实验中,简单的 Shared-Bottom 就取得了与 MMoE 接近的效果,部分指标甚至略好。
这意味着,如果任务关系较简单,采用共享底层网络可能更高效。
只有当任务之间存在足够复杂的关联与差异,而且数据量能够支持多个专家网络的训练时,MMoE 才更有机会发挥其优势。
对于实际工程项目,建议首先建立独立任务模型和 Shared-Bottom 基线,再尝试 MMoE,并重点检查:
- 各任务是否都得到稳定改善。
- 是否存在负迁移或跷跷板现象。
- Gate 是否学习到了具有差异的专家权重。
- 模型增加的计算成本是否带来了实际业务收益。
- 不同任务的标签定义、样本范围与预测时点是否一致。
在推荐系统、消费金融、客户经营和多期限信用风险预测中,MMoE 都提供了一种值得研究的多任务学习方法。
真正决定它是否值得采用的,是任务之间能够共享多少有效信息,以及共享机制是否带来了可验证的泛化收益。
- 点赞
- 收藏
- 关注作者
评论(0)