做量化的人迟早会撞上同一堵墙:回测漂亮的因子模型,一上实盘就拉胯。原因往往不是信号没用,而是模型在「训练集里把噪声也背下来了」——它学的是这段历史里偶然成立的局部规律,而不是真正稳健的因子结构。
传统机器学习靠海量标注数据撑着泛化,可量化领域没有这个奢侈:你只有几千个交易日、几百只股票,标签(下期涨跌)还被交易成本、停牌、幸存者偏差反复污染。样本越少、标签越脏,模型越容易「背答案」而不是「学规律」。
这条墙有三道裂缝:样本量不足导致的过拟合、特征空间单调导致的脆弱决策边界、市场环境漂移导致的分布失配。本文用两套从零实现的模型,逐一演示两种补墙技术——合成数据增强与对抗训练——它们一个管「泛化」、一个管「鲁棒」,正好补上两道最深的裂缝。

一、为什么因子模型特别需要数据增强#
在图像领域,把一张猫旋转 5°、调亮 10%,它还是猫——增强样本天然成立。但在因子模型里,「增强」必须尊重金融数据的物理结构,否则就是在制造未来函数(look-ahead)或虚假相关性。这正是很多人对数据增强望而却步的原因。
可问题在于,如果你什么都不做,模型会在小样本上迅速过拟合。我们用一个受控实验把这件事摆到台面上:构造一个含真实信号(8 个量价因子的线性组合)但带 22% 标签噪声的合成数据集,只给模型 100 个训练样本,其余 4000 个留作测试。
import numpy as np
rng = np.random.default_rng(42)
def make_data(n, signal=1.3, label_noise=0.22, shift=0.0):
X = rng.normal(0, 1, size=(n, 8))
X = X - shift * np.arange(8)[None, :] # 协变量漂移(用于鲁棒性测试)
w_true = np.array([1.1, -0.7, 0.9, 0.0, -1.0, 0.5, 0.0, 0.8])
logit = signal * (X @ w_true)
p = 1 / (1 + np.exp(-logit))
y = (rng.uniform(0, 1, n) < p).astype(int)
flip = rng.uniform(0, 1, n) < label_noise # 标签噪声:可学习的"噪音"
y = (y ^ flip).astype(int)
return X, y
Xtr, ytr = make_data(100)
Xte, yte = make_data(4000)python用一个 2 隐层 MLP(64×32,tanh)训练后,基线模型在训练集上准确率 0.870,测试集只有 0.620——典型的过拟合:它把训练标签里的噪声也记下来了。这 gap 就是我们要填的坑。
二、合成数据增强:给输入加「受控噪声」#
最直接的增强是对特征加高斯噪声:在训练时对每个样本 x 扰动成 x + ε,ε ~ N(0, σ²)。这迫使模型不能依赖某个特征的精确数值,而是学到「在邻域内都成立」的规律。本质上,它是一种比 L2 更主动的正则化。
def forward(X, P):
z1 = X @ P["W1"] + P["b1"]; a1 = np.tanh(z1)
z2 = a1 @ P["W2"] + P["b2"]; a2 = np.tanh(z2)
return 1 / (1 + np.exp(-(a2 @ P["W3"] + P["b3"]))), (z1, a1, z2, a2)
def train(X, y, augment=False, epochs=600, lr=0.008, lam=1e-5):
n = X.shape[0]; P = init_params(); y = y.reshape(-1, 1)
for _ in range(epochs):
p, (z1, a1, z2, a2) = forward(X, P)
g3 = (p - y) / n
g2 = g3 @ P["W3"].T * (1 - a2 ** 2)
g1 = g2 @ P["W2"].T * (1 - a1 ** 2)
W3g = a2.T @ g3 + 2 * lam * P["W3"]; b3g = g3.sum(0)
W2g = a1.T @ g2 + 2 * lam * P["W2"]; b2g = g2.sum(0)
W1g = X.T @ g1 + 2 * lam * P["W1"]; b1g = g1.sum(0)
if augment: # 关键:加噪样本,梯度取平均
xn = X + rng.normal(0, 0.10, X.shape)
pn, (_, an1, _, an2) = forward(xn, P)
gn3 = (pn - y) / n
gn2 = gn3 @ P["W3"].T * (1 - an2 ** 2)
gn1 = gn2 @ P["W2"].T * (1 - an1 ** 2)
W3g = (W3g + an2.T @ gn3) / 2 + 2 * lam * P["W3"]
W2g = (W2g + an1.T @ gn2) / 2 + 2 * lam * P["W2"]
W1g = (W1g + xn.T @ gn1) / 2 + 2 * lam * P["W1"]
for k, g in zip(P, [W1g, b1g, W2g, b2g, W3g, b3g]):
P[k] -= lr * g
return Ppython在我们的实验里,增强把测试集 AUC 从 0.658 提到 0.673,决策边界也明显更平滑(见上图中间一列)。噪声强度 σ 是道权衡题:σ 太小没正则效果,σ 太大把信号也洗掉了——经验上取特征标准差的 5%~15% 起步,再用验证集调。
除了加噪,实务里还有两类增强值得提一句:
- SMOTE 式样本混合:在特征空间对少数类近邻做线性插值
x_new = λx_i + (1-λ)x_j,适合涨跌极度不平衡的标签; - 截面 shuffle / bootstrap:对同日的横截面样本做有放回重采样,制造「另类历史路径」,但务必保证不跨时间泄露。
当标签的涨跌极度不平衡(比如暴雷样本极少)时,光加噪还不够,可以用 SMOTE 式混合在特征空间造少数类近邻,把决策边界主动拉向少数类一侧:
def smote(X, y, k=5, n_new=50):
Xpos = X[y == 1]
out = []
for _ in range(n_new):
i = rng.integers(0, len(Xpos)); j = rng.integers(0, len(Xpos))
lam = rng.uniform(0, 1)
out.append(lam * Xpos[i] + (1 - lam) * Xpos[j]) # 同标签近邻间线性插值
return np.vstack([X, np.array(out)]), np.concatenate([y, np.ones(n_new)])python这一步缓解类别失衡带来的过拟合偏向。关键是插值只在同标签近邻间做,绝不跨时间、绝不跨标签,避免造出自相矛盾的样本。
三、对抗训练:在最坏方向上练肌肉#
数据增强让边界更平滑,但它改善的是「平均情况」。如果你想防的是定向攻击或极端行情(比如某只股票被错标、某个因子被瞬间拉爆),需要的是对抗训练(Adversarial Training)。
核心思想来自 Goodfellow 的 FGSM:对输入施加一个「让模型最容易犯错」的微小扰动 δ = ε·sign(∇_x L),然后在「原样本 + 对抗样本」上联合训练。模型被迫在扰动邻域内都做对,于是它的有效鲁棒半径被撑大。
对线性模型,这件事有解析的最美形式:损失对输入的梯度就是 ∇_x L = (p-y)·w,方向恒为 sign(w)。所以对抗样本就是 x_adv = x + ε·sign(w),训练时把这个样本混进来即可:
def train_logreg(X, y, adv=False, eps=0.4, epochs=300, lr=0.05, lam=1e-3):
n, d = X.shape; w = np.zeros(d); b = 0.0; y = y.reshape(-1, 1)
for _ in range(epochs):
p = 1 / (1 + np.exp(-(X @ w + b)))[:, None]
gw = (X.T @ (p - y)).ravel() / n + 2 * lam * w
if adv: # FGSM:沿 sign(w) 推一步
xa = np.clip(X + eps * np.sign(w), -5, 5)
pa = 1 / (1 + np.exp(-(xa @ w + b)))[:, None]
gw = (gw + (xa.T @ (pa - y)).ravel() / n + 2 * lam * w) / 2
w = w - lr * gw; b = b - lr * (p - y).mean()
return w, bpython对深度网络(如上面的 MLP),FGSM 的梯度要逐层反传:
# MLP 的 FGSM 输入梯度(省略前向细节,g1 为隐层一阶梯度)
gx = g1 * (1 - a1 ** 2) @ P["W1"].T # dL/dx 的形状与 X 相同
xa = np.clip(X + eps * np.sign(gx), -5, 5) # 对抗样本python我们的白盒测试很说明问题:用每个模型自己的梯度造 FGSM 对抗样本去攻击测试集,在扰动幅度 ε=0.4 时——
| 模型 | 干净准确率 | ε=0.4 攻击下准确率 |
|---|---|---|
| 基线逻辑回归 | 0.631 | 0.569 |
| +对抗训练 | 0.618 | 0.609 |
对抗训练牺牲了约 1.3 个点的干净准确率(它变得”保守”),却把对抗攻击下的准确率从 0.569 拉到 0.609。这就是它存在的意义:用一点平均性能,换来分布尾部的安全性。

四、学习曲线:增强让数据更”经用”#
另一视角是看「要用多少数据才能训好」。下图横轴是训练样本数,纵轴是测试准确率。增强和对抗训练的曲线更早爬升、也更平——意味着在样本稀缺时,它们能用更少的数据达到相近的泛化水平。这对因子模型至关重要:你多一个季度的样本往往要等一年,而增强让你”现有的数据更经用”。

五、真实陷阱:这些坑会让增强变成毒药#
- 增强样本里的未来函数:对时间序列加噪声必须只在「截面/特征维度」上做,绝对不能在时间维度上 shuffle 或插值——那等于把未来信息喂给过去,回测必暴利、实盘必腰斩。
- 增强幅度不校验:
σ或ε过大,等于在训练一个”看不清信号”的模型。务必用样本外验证集调,而不是训练集。 - 对抗训练只防它见过的攻击:FGSM 训出来的鲁棒性,主要覆盖 FGSM 这类一阶攻击;遇到更强的 PGD(多步迭代)攻击仍可能破防。真要上强度,得用 PGD 做对抗样本。
- 标签噪声不是增强能救的:本文 22% 标签噪声是”可学习的软噪声”,增强有效;但如果是系统性标注错误(如复权错误、停牌日误标),增强只会让模型更自信地错。
- 计算成本:对抗训练每个 batch 要多做一次前向+反向来造样本,约 2 倍算力。深度网络上建议用 FGSM 而非 PGD 做日常训练,PGD 留给最终压力测试。
六、落地建议#
- 小样本 + 高标签噪声的因子模型,优先上高斯噪声增强,成本低、收益稳,先把过拟合压下去;
- 如果你的信号要用于杠杆产品、期权对冲或极端行情风控,再把对抗训练加进 pipeline,用 FGSM 日常、PGD 做定期体检;
- 增强/对抗的超参(
σ、ε)一律用滚动窗口的样本外集调,且每次改数据频率都要重调; - 永远保留一条未增强的基线做对照——如果增强后样本外反而掉,说明你的增强假设(“邻域仍同分布”)在当下市场不成立,别硬上。
合成数据增强与对抗训练不是银弹,它们解决的是「模型在已知分布内更稳、在微小扰动下不崩」。真正跨市场的泛化,最终还是回到因子经济学含义与严谨的样本外验证上——这两个技术,是让你那点宝贵的样本数据”物尽其用”的放大器,不是替代研究的捷径。