简介:基于Transformer架构的资产定价前沿研究(NBER工作论文),面向金融工程与量化投资领域的研究人员、机构从业者及进阶投资者,旨在解决传统机器学习模型在股票回报预测中定价误差偏大、跨资产信息利用不足的问题。论文提出人工智能定价模型(AIPM),将Transformer嵌入随机贴现因子,通过线性和非线性投资组合转换器实现跨资产信息共享;研究采用1963—2022年美国股票市场数据,在与多种经典机器学习模型的对比中取得更低的定价误差和更高的预测精度,并进一步剖析了信息共享机制与模型复杂度对收益提升的贡献,同时指出线性变换也能从特征交互中获益。压缩包内共1个PDF文件,大小681KB,内容覆盖模型设定、理论推导、实证流程与结果解读,适合用于了解注意力机制在资产定价中的最新应用,也可为量化策略研发和学术研究提供方法参考。目前已有124人学习下载。 Transformer 在自然语言处理里已经成了标配,这两年越来越多的量化团队开始尝试把它引入金融市场。具体到资产定价这个场景,深度学习模型要做的事情,就是从一堆行情、基本面、另类数据里,学到资产未来收益率的条件分布,并给出比传统因子模型更精准的预测。这篇文章不聊玄乎的理论,直接围绕“金融市场中的 Transformer 模型”这个主题,讲清楚它到底怎么改进资产定价,以及我实际落地时踩过的坑和验证过的做法,适合正在做量化研究、想用深度学习替换传统多因子框架的同学参考。
1. 为什么资产定价需要Transformer
1.1 传统资产定价模型的底牌
资产定价这个领域,学术上绕不开 CAPM 和 Fama-French 因子模型。CAPM 的核心假设是:资产的预期超额收益只跟市场组合的 beta 相关,风险溢价用一个线性系数就能刻画。这玩意儿在课堂上讲得通,拿到实盘数据上就有点尴尬,因为单一因子对横截面收益差异的解释力相当有限。后来三因子、五因子模型加上了规模、账面市值比、盈利和投资因子,逐步把 R² 拉上去,但本质上还是线性回归的框架。
线性模型的问题在于,它假设因子和预期收益之间是直线关系,因子之间是独立叠加的。实际上 A 股市场里小市值因子在特定流动性环境下非常强,换手率因子在牛市和熊市的符号甚至可能反转,这些特征用固定线性系数根本表达不出来。机器学习的思路就不一样,树模型和神经网络默认去拟合非线性关系,不需要人工指定交互项。这也是为什么从 2018 年开始,Gu, Kelly 和 Xiu 那篇用神经网络做资产定价的论文出来后,量化圈开始大规模尝试用深度学习替代传统线性因子模型。
1.2 Transformer 的核心优势与金融场景的契合点
Transformer 相比 LSTM 和 GRU 这类序列模型,最大的特点是用自注意力机制直接建模序列中任意两个位置的关系。LSTM 的隐状态传递是逐步进行的,长距离信息要经过很多时间步才能传导,训练时容易出现梯度衰减。Transformer 的注意力矩阵一次计算就建立全局依赖,无论两个时间点相隔多远,都能直接交互。
金融数据恰恰是一个高度依赖长程依赖和变量交互的领域。一只股票的收益不仅受过去几天走势影响,还受宏观经济变量、行业联动、市场情绪等多种因素共同作用。传统因子模型很难自动发现这些复杂关系,而 Transformer 的多头注意力机制可以并行地去学习不同维度的特征交互:一个头可能学到行业联动,另一个头可能学到波动率聚集效应。
另一个很实际的优势是计算效率。LSTM 必须逐步迭代,Transformer 的 attention 计算可以高度并行,在 GPU 上训练要快得多。对量化研究来说,这意味着可以更快地迭代特征和超参数组合,实验效率直接影响研究产出。
2. 整体设计:从因子到收益率的端到端方案
2.1 数据准备:特征工程与样本构造
要把 Transformer 用到资产定价,第一步就是把原始行情数据加工成模型能吃的结构化样本。这里最常用的数据组织形式是面板数据,每一行对应某只股票在某个时间点的特征值,整体是一个三维结构:时间维度、股票维度、特征维度。
特征工程方面,常见输入包括动量类因子(过去 5 日、20 日、60 日收益率)、波动率类因子(已实现波动率、下行波动率)、成交量类因子(换手率、成交量 Z-Score)、流动性因子(Amihud 非流动性指标),以及基本面因子(市盈率、市净率、ROE 等)。特征不是越多越好,我实际测试下来,先从一个经过 IC 筛选的特征池开始,规模控制在 20 到 50 个之间效果最稳,太多噪声特征会让注意力机制学到虚假关联。
标签构造是资产定价模型的关键环节。通常用未来 N 日的收益率作为预测目标,短周期用未来 5 日收益,长周期用未来 20 日收益。这里必须强调一个核心原则:标签只能包含 T 时刻之后的信息,特征只能包含 T 时刻及之前的信息。如果标签用了未来数据,模型在回测里表现再好都是假的,属于典型的未来函数问题。
样本集的划分也要格外小心。时间序列数据不能像图像分类那样随机打乱,否则训练集里会出现未来样本,验证集的意义就完全失效了。正确做法是按时间顺序划分:前 60% 时间作为训练集,中间 20% 作为验证集,最后 20% 作为测试集。
2.2 模型架构:Transformer 在定价任务中的具体形态
Transformer 在资产定价中怎么搭,业界其实有两套主流做法。第一种是把单只股票的时间序列作为输入序列,类似文本处理方式,每个时间步的特征相当于一个 token,位置编码表示时间先后顺序,最终输出该股票未来收益的预测值。第二种是把横截面数据作为输入序列,即在同一时间点,把不同股票当作一个序列,每个股票的特征作为 token,通过注意力机制让模型自动发现股票之间的联动关系。
我的实践经验是,时间序列模式更适合单股票择时,横截面模式更适合横截面选股。做资产定价的量化策略,横截面模式往往是更好的起点,因为它直接从选股角度出发,和组合构建衔接更自然。具体架构上,我在横截面模式中会用行业信息作为辅助编码,让模型知道哪些股票属于同一行业,注意力机制就能更容易学到行业内联动。
位置编码的选择上,时间维度需要加上时间位置编码,但我不太推荐用原版 Transformer 的固定正弦位置编码。金融数据是高度非平稳的,不同年份的市场状态差异很大,固定的位置编码对模型来说意思不大。我倾向于使用可学习的位置嵌入,让模型自己去决定时间顺序在预测中的重要性。
2.3 训练策略与损失函数的选择逻辑
损失函数的选择直接影响模型的预测目标。最直接的是均方误差损失,让模型去拟合未来 N 日收益率的具体数值。但金融数据信噪比极低,收益率预测误差普遍很大,MSE 很容易让模型只顾着拟合高波动股票而忽略低波动股票。所以我在实际项目中,会在标签构造时做一次截面标准化,即在同一时间点上,将所有股票的收益率减去均值再除以标准差,让模型预测的是截面上的相对强弱,而不是绝对收益水平。
从业务角度看,选股策略本质上关心的是股票收益的排序,而不是具体数值。因此除了 MSE,我还经常用 RankIC 作为辅助监督信号。具体做法是同时对预测值和真实值计算秩相关系数,并把排名损失作为正则项加到总损失里。这样模型能在数值预测精度和排序能力之间做权衡,实际回测下来,排序能力强的模型在分层回测中表现更稳定。
优化器的选择上也有些讲究。Transformer 的训练对学习率比较敏感,直接用 Adam 很容易在训练前期出现 loss 振荡。我一般用 AdamW,配合学习率 warmup 策略:前 5% 的步数让学习率从很小的值线性增长到峰值,之后用余弦退火逐步衰减。这个做法来自 NLP 领域的经验,但在金融数据上也同样有效。
批量大小方面,横截面模式下每个 batch 对应一个时间点上的全部股票,这样能在保证样本量的同时保持时间一致性。如果 GPU 显存不够,可以按行业分组切分,但要保证同一 batch 里的股票来自同一个时间点,否则注意力机制可能会学到跨时间的虚假关联。
3. 实操过程与核心环节实现
3.1 环境准备与依赖搭建
实际开发中,我一般直接用 Python 3.9 加 PyTorch 2.x 作为主力框架,配合 pandas 做数据处理、scikit-learn 做标准化和指标计算、matplotlib 做可视化。搭建深度学习环境时,建议用 conda 创建一个独立的环境,避免依赖冲突。
conda create -n fin_transformer python=3.9 conda activate fin_transformer pip install torch pandas numpy scikit-learn matplotlib tqdm如果有一张 8GB 显存的 GPU,跑横截面 Transformer 已经非常够用了。没有 GPU 也能跑,只是训练会慢不少,可以先在小规模数据上验证逻辑,再上 GPU 跑全量数据。
3.2 数据加载与预处理代码
用一个例子说明数据预处理的核心流程。假设原始数据是 CSV 文件,包含 date、stock_code、feature_1 到 feature_n 和 forward_return 这几个字段。加载后首先要按时间排序,然后做截面标准化。
import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler df = pd.read_csv('stock_panel.csv') df = df.sort_values(['date', 'stock_code']).reset_index(drop=True) # 构造特征列表,排除标识符和标签列 feature_cols = [c for c in df.columns if c.startswith('feature_')] # 按日期分组做截面标准化 def zscore_group(group): scaler = StandardScaler() group[feature_cols] = scaler.fit_transform(group[feature_cols]) return group df = df.groupby('date', group_keys=False).apply(zscore_group)这里有个非常容易踩的坑:截面标准化必须按日期分组,每一行股票的特征是在这个时间点的横截面上做标准化的。如果对整个数据集做全局标准化,等于是用了未来数据的信息,会造成数据泄漏。这个细节我在初版代码里就犯过错误,回测指标被虚高了很多。
时间序列的划分也要注意,不能用 train_test_split 随机切分,而是按日期边界切:
train_end = df['date'].quantile(0.6) val_end = df['date'].quantile(0.8) train_df = df[df['date'] <= train_end] val_df = df[(df['date'] > train_end) & (df['date'] <= val_end)] test_df = df[df['date'] > val_end]3.3 核心模型代码与训练循环
下面是一个精简但可用的横截面 Transformer 定价模型代码。核心思路是:输入(股票数,特征数),把每只股票当作一个 token,特征当作 token 的 embedding,经过位置编码和多层 TransformerEncoder,最后输出每只股票的预测收益率。
import torch import torch.nn as nn import math class FinancialTransformer(nn.Module): def __init__(self, feature_dim, d_model=64, nhead=4, num_layers=3, dropout=0.1): super().__init__() self.input_proj = nn.Linear(feature_dim, d_model) self.pos_embed = nn.Parameter(torch.randn(1, 500, d_model) * 0.02) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=128, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.head = nn.Sequential( nn.Linear(d_model, 32), nn.GELU(), nn.Linear(32, 1) ) def forward(self, x): # x: (batch_size, num_stocks, feature_dim) batch_size, num_stocks, _ = x.shape x = self.input_proj(x) x = x + self.pos_embed[:, :num_stocks, :] x = self.encoder(x) pred = self.head(x).squeeze(-1) return pred这里用batch_first=True让输入维度更直观。位置嵌入我设置了一个最大股票数 500,这个数字可以根据实际股票池规模调整,但要留出一定余量。预测头用了 GELU 激活函数,比 ReLU 在训练前期更平滑,不容易出现神经元死亡的问题。
训练循环里,有几个细节值得注意。每个 batch 对应一个时间点的全部股票,也就是说我在 DataLoader 里按日期分组,每个样本是一个日期的横截面数据。
def train_one_epoch(model, train_loader, optimizer, criterion, device): model.train() total_loss = 0 for x, y in train_loader: x = x.to(device) y = y.to(device) optimizer.zero_grad() pred = model(x) # 标签也做截面标准化 y_std = (y - y.mean(dim=-1, keepdim=True)) / (y.std(dim=-1, keepdim=True) + 1e-8) loss = criterion(pred, y_std) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss += loss.item() return total_loss / len(train_loader)梯度裁剪那行代码很多人会忽略,但对 Transformer 来说非常重要。金融数据的噪声大,偶尔会出现异常样本导致梯度爆炸,梯度裁剪能有效稳住训练过程。我一般把 max_norm 设置在 1.0 到 5.0 之间,具体值根据训练稳定性调整。
3.4 评估指标与回测要点
模型训练完成后,评估环节比训练本身更能决定策略是否可用。我最常用的指标有三个:IC、RankIC 和分层回测。
IC 的计算方式是:在每个时间截面上,计算预测收益率和真实收益率的 Pearson 相关系数,然后对时间维度取均值。RankIC 则用 Spearman 秩相关系数,更关注排序关系是否准确。
def compute_ic(pred_df, true_df): # pred_df: date x stock 的预测值表 # true_df: date x stock 的真实值表 ic_series = pred_df.apply(lambda x: x.corr(true_df.loc[x.name]), axis=1) rank_ic_series = pred_df.apply( lambda x: x.rank().corr(true_df.loc[x.name].rank()), axis=1 ) return ic_series.mean(), rank_ic_series.mean()分层回测则是把预测值按十分位分组,计算每组等权组合的未来收益率均值。一个有效的模型,分组收益应该是单调递增或递减的,多头组和空头组的收益差就是多空组合的收益。只看 IC 有一个问题,它只衡量了线性相关程度,无法反映预测值在极端分位上的区分能力,所以一定要配合分层回测看单调性。
我自己的标准是:测试集上 RankIC 达到 0.03 以上,且分层回测中多头组收益显著优于空头组,模型才值得进入下一步的因子合成或组合优化流程。
4. 常见问题与排查技巧实录
4.1 过拟合与时间序列泄漏
我在这个项目里遇到的最典型的问题,就是训练集 IC 很高、测试集 IC 趋近于零甚至变成负数。这种症状十有八九是过拟合或者数据泄漏。
数据泄漏的排查可以从几个角度入手。检查代码里是否对全样本做了标准化;检查标签是否意外包含了未来信息;检查训练集和验证集是否真的按时间切开了。我记得有一次 debug 了半天,最后发现是 pandas 的 groupby 默认排序把数据顺序打乱了,导致时间切分失效。这个教训让我养成了一个习惯:在任何数据处理前后都打印一下日期范围确认时间顺序。
过拟合的应对策略有几个层面。最基础的是提高 dropout 比例,Transformer 的 dropout 我一般设在 0.1 到 0.3 之间。其次是减小模型容量,把 d_model 从 128 降到 64、层数从 6 层降到 3 层,往往效果立竿见影。金融数据比文本数据的样本量小得多,模型太大很容易记住噪声。再者就是早停,在验证集 RankIC 连续 10 个 epoch 没有提升时就停止训练。
4.2 训练不稳定与超参数调整
Transformer 训练不稳定的表现通常是 loss 像过山车一样上下震荡,或者训练到一半梯度爆炸导致 loss 变成 NaN。这类问题的主要原因有三个:学习率过大、warmup 步数不够、数据里有极端异常值。
学习率的设置上,我试过从 1e-4 到 3e-4 这个区间通常比较安全,超过 5e-4 大概率会震荡。warmup 步数占训练总步数的比例建议在 5% 到 10% 之间,太少达不到预热效果,太多会让训练变慢。数据异常值方面,特征虽然做了截面标准化,但极端行情下仍可能出现偏离均值 10 个标准差的值,可以在数据预处理时做 winsorize 处理,把上下 1% 分位以外的值截断。
还有一个容易被忽略的问题:注意力机制的权重退化。如果位置编码初始化不当,模型可能学会忽略大部分股票、只关注少数几个 token,导致有效感受野很小。我检查这个问题的办法是把注意力权重矩阵打印出来做可视化,如果发现某些头总是集中在同一个维度上,就需要调整初始化或增加 dropout。
4.3 业务落地中的坑
模型在回测里表现不错,不等于实盘能用。我在落地过程中遇到过几个很现实的坑。
第一个是交易成本。高频调仓的模型在回测里换手率可能非常高,但实盘交易成本会把这些收益吃掉。需要在训练时加入换手率惩罚,或者在回测时用更保守的成本假设。我通常的做法是在回测中按双边千分之三的费率扣除成本,虽然比实际偏高,但至少不会过度乐观。
第二个是 Alpha 衰减。市场对因子的认识和拥挤度会随时间变化,一个在历史数据上有效的模型,未来几个月可能失效。所以模型需要定期重训。我一般是每两周重训一次,每次用最新的数据滚动更新训练集,保证模型能跟上市场风格变化。
第三个是行业和风格暴露。如果模型的预测结果在某一个行业上过度集中,组合的风险就会很大。解决方案是在特征中加入行业哑变量,让模型显式感知行业信息,同时在组合构建阶段做行业中性化处理,把行业暴露压到零。
5. 实操总结与个人体会
做金融 Transformer 这个项目最大的体会是:不要一开始就把模型搞复杂。先跑通一个简单的线性基准,比如直接用线性回归预测收益率,记录下它的 IC 和分层回测结果。然后在这个基础上逐步加复杂度——先试一个浅层全连接网络,再加一层 Transformer。这样每一步的改进都能归因到具体的技术变化,而不是被模型的随机性干扰判断。
我在实际测试中发现,模型到了一个复杂程度之后,继续堆层数带来的收益会非常有限,反而过拟合风险直线上升。金融数据的信噪比决定了模型的容量天花板比 NLP 任务低得多,一个 3 层、hidden size 64 的 Transformer 往往就能达到不错的效果,没必要追求大模型。
最后分享一个小技巧:每次实验的训练参数、数据切分日期、特征列表、评估指标一定要完整记录下来。我用的是最简单的 CSV 日志方式,每次跑完实验就把关键指标追加到一行记录里。这样回溯的时候能快速对比不同版本的效果差异,排查问题时也能快速定位是哪一次改动引入了 bug。做量化模型,实验管理的规范性直接决定了研究效率,这一点长期做下来体会特别深。
本文还有配套的精品资源,点击获取