目 录CONTENT

文章目录

面向高风险模型的专家级特征工程:高级技术

Administrator
2025-11-12 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

📢 转载信息

原文链接:https://machinelearningmastery.com/expert-level-feature-engineering-advanced-techniques-for-high-stakes-models/

原文作者:Iván Palomares Carrascosa


在本文中,您将学习三种专家级特征工程策略——反事实特征领域约束表示因果不变特征——用于构建高风险环境下的稳健且可解释的模型。

我们将涵盖的主题包括:

  • 如何生成反事实敏感度特征以感知决策边界。
  • 如何训练一个将单调性领域规则编码到其表示中的约束自编码器。
  • 如何发现跨环境保持稳定的因果不变特征

废话不多说,让我们开始吧。

专家级特征工程:面向高风险模型的先进技术
图片来源:Editor

Introduction

在金融、医疗和关键基础设施等高风险场景中构建机器学习模型,通常需要稳健性、可解释性以及其他特定领域的约束。在这些情况下,值得超越经典的特征工程技术,采用针对此类环境量身定制的先进、专家级策略。

本文介绍了这三种技术,解释了它们的工作原理,并强调了它们的实际影响。

Counterfactual Feature Generation

反事实特征生成包含量化预测对决策边界敏感程度的技术,方法是基于原始特征的微小变化来构建假设数据点。这个想法很简单:询问“原始特征值必须改变多少,模型的预测才会跨越关键阈值?”这些派生特征提高了可解释性——例如,“患者距离诊断有多近?”或“获得贷款批准所需的最低收入增长是多少?”——并且它们直接在特征空间中编码了敏感度,从而可以提高模型的稳健性。

下面的 Python 示例创建了一个反事实敏感度特征 cf_delta_feat0,用于衡量输入特征 feat_0(在所有其他特征保持不变的情况下)必须改变多少才能跨越分类器的决策边界。我们将使用 NumPypandasscikit-learn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
 
# Toy data and baseline linear classifier
X, y = make_classification(n_samples=500, n_features=5, random_state=42)
df = pd.DataFrame(X, columns=[f"feat_{i}" for i in range(X.shape[1])])
df['target'] = y
 
scaler = StandardScaler()
X_scaled = scaler.fit_transform(df.drop(columns="target"))
clf = LogisticRegression().fit(X_scaled, y)
 
# Decision boundary parameters
weights = clf.coef_[0]
bias = clf.intercept_[0]
 
def counterfactual_delta_feat0(x, eps=1e-9):
    """
    Minimal change to feature 0, holding other features fixed,
    required to move the linear logit score to the decision boundary (0).
    For a linear model: delta = -score / w0
    """
    score = np.dot(weights, x) + bias
    w0 = weights[0]
    return -score / (w0 + eps)
 
df['cf_delta_feat0'] = [counterfactual_delta_feat0(x) for x in X_scaled]
df.head()

Domain-Constrained Representation Learning (Constrained Autoencoders)

自编码器被广泛用于无监督表示学习。我们可以将其改编用于领域约束的表示学习:在学习压缩表示(潜在特征)的同时,强制执行明确的领域规则(例如,安全裕度或单调性定律)。与无约束的潜在因子不同,领域约束表示经过训练以遵守物理、伦理或监管约束。

下面,我们训练一个自编码器,该编码器学习三个潜在特征,并在重建输入的同时,柔性地强制执行单调性规则:feat_0 的值越高,正标签的可能性不应降低。我们添加了一个简单的监督预测头,并通过有限差分单调性损失来惩罚违反行为。实现使用了 PyTorch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
 
# Supervised split using the earlier DataFrame `df`
X_train, X_val, y_train, y_val = train_test_split(
    df.drop(columns="target").values, df['target'].values, test_size=0.2, random_state=42
)
 
X_train = torch.tensor(X_train, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32).unsqueeze(1)
 
torch.manual_seed(42)
 
class ConstrainedAutoencoder(nn.Module):
    def __init__(self, input_dim, latent_dim=3):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 8), nn.ReLU(),
            nn.Linear(8, latent_dim)
        )
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 8), nn.ReLU(),
            nn.Linear(8, input_dim)
        )
        # Small predictor head on top of the latent code (logit output)
        self.predictor = nn.Linear(latent_dim, 1)
 
    def forward(self, x):
        z = self.encoder(x)
        recon = self.decoder(z)
        logit = self.predictor(z)
        return recon, z, logit
 
model = ConstrainedAutoencoder(input_dim=X_train.shape[1])
optimizer = optim.Adam(model.parameters(), lr=1e-3)
recon_loss_fn = nn.MSELoss()
pred_loss_fn = nn.BCEWithLogitsLoss()
 
epsilon = 1e-2  # finite-difference step for monotonicity on feat_0
for epoch in range(50):
    model.train()
    optimizer.zero_grad()
 
    recon, z, logit = model(X_train)
    # Reconstruction + supervised prediction loss
    loss_recon = recon_loss_fn(recon, X_train)
    loss_pred  = pred_loss_fn(logit, y_train)
 
    # Monotonicity penalty: y_logit(x + e*e0) - y_logit(x) should be >= 0
    X_plus = X_train.clone()
    X_plus[:, 0] = X_plus[:, 0] + epsilon
    _, _, logit_plus = model(X_plus)
 
    mono_violation = torch.relu(logit - logit_plus)  # negative slope if > 0
    loss_mono = mono_violation.mean()
 
    loss = loss_recon + 0.5 * loss_pred + 0.1 * loss_mono
    loss.backward()
    optimizer.step()
 
# Latent features now reflect the monotonic constraint
with torch.no_grad():
    _, latent_feats, _ = model(X_train)
latent_feats[:5]

Causal-Invariant Features

因果不变特征是其关系与结果在不同背景或环境中保持稳定的变量。通过针对因果信号而非虚假相关性,模型可以在分布外设置中更好地泛化。一种实用的方法是惩罚跨环境风险梯度的变化,这样模型就不能依赖环境特定的捷径。

下面的示例模拟了两个环境。只有第一个特征是真正具有因果关系的;第二个特征在环境 1 中与标签产生虚假相关性。我们跨环境训练一个共享的线性模型,同时惩罚梯度不匹配,从而鼓励模型依赖不变的(因果)结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
 
torch.manual_seed(42)
np.random.seed(42)
 
# Two environments with a spurious signal in env1
n = 300
X_env1 = np.random.randn(n, 2)
X_env2 = np.random.randn(n, 2)
 
# True causal relation: y depends only on X[:,0]
y_env1 = (X_env1[:, 0] + 0.1*np.random.randn(n) > 0).astype(int)
y_env2 = (X_env2[:, 0] + 0.1*np.random.randn(n) > 0).astype(int)
 
# Inject spurious correlation in env1 via feature 1
X_env1[:, 1] = y_env1 + 0.1*np.random.randn(n)
X1, y1 = torch.tensor(X_env1, dtype=torch.float32), torch.tensor(y_env1, dtype=torch.float32)
X2, y2 = torch.tensor(X_env2, dtype=torch.float32), torch.tensor(y_env2, dtype=torch.float32)
 
class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.w = nn.Parameter(torch.randn(2, 1))
    def forward(self, x):
        return x @ self.w
model = LinearModel()
optimizer = optim.Adam(model.parameters(), lr=1e-2)
 
def env_risk(x, y, w):
    logits = x @ w
    return torch.mean((logits.squeeze() - y)**2)
 
for epoch in range(2000):
    optimizer.zero_grad()
    risk1 = env_risk(X1, y1, model.w)
    risk2 = env_risk(X2, y2, model.w)
    # Invariance penalty: align risk gradients across environments
    grad1 = torch.autograd.grad(risk1, model.w, create_graph=True)[0]
    grad2 = torch.autograd.grad(risk2, model.w, create_graph=True)[0]
    penalty = torch.sum((grad1 - grad2)**2)
    loss = (risk1 + risk2) + 100.0 * penalty
    loss.backward()
    optimizer.step()
print("Learned weights:", model.w.data.numpy().ravel())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
 
torch.manual_seed(42)
np.random.seed(42)
 
# Two environments with a spurious signal in env1
n = 300
X_env1 = np.random.randn(n, 2)
X_env2 = np.random.randn(n, 2)
 
# True causal relation: y depends only on X[:,0]
y_env1 = (X_env1[:, 0] + 0.1*np.random.randn(n) > 0).astype(int)
y_env2 = (X_env2[:, 0] + 0.1*np.random.randn(n) > 0).astype(int)
 
# Inject spurious correlation in env1 via feature 1
X_env1[:, 1] = y_env1 + 0.1*np.random.randn(n)
X1, y1 = torch.tensor(X_env1, dtype=torch.float32), torch.tensor(y_env1, dtype=torch.float32)
X2, y2 = torch.tensor(X_env2, dtype=torch.float32), torch.tensor(y_env2, dtype=torch.float32)
 
class LinearModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.w = nn.Parameter(torch.randn(2, 1))
    def forward(self, x):
        return x @ self.w
model = LinearModel()
optimizer = optim.Adam(model.parameters(), lr=1e-2)
 
def env_risk(x, y, w):
    logits = x @ w
    return torch.mean((logits.squeeze() - y)**2)
 
for epoch in range(2000):
    optimizer.zero_grad()
    risk1 = env_risk(X1, y1, model.w)
    risk2 = env_risk(X2, y2, model.w)
    # Invariance penalty: align risk gradients across environments
    grad1 = torch.autograd.grad(risk1, model.w, create_graph=True)[0]
    grad2 = torch.autograd.grad(risk2, model.w, create_graph=True)[0]
    penalty = torch.sum((grad1 - grad2)**2)
    loss = (risk1 + risk2) + 100.0 * penalty
    loss.backward()
    optimizer.step()
print("Learned weights:", model.w.data.numpy().ravel())




🚀 想要体验更好更全面的AI调用?

欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。

0

评论区