编写自定义损失函数的注意事项及正弦波交易策略模型训练失效问题咨询
- 对齐核心任务目标:损失函数必须直接锚定你要优化的最终目标——比如你这里是最大化交易盈利,而非拟合收益率曲线,所以损失得和盈利强绑定,但要兼顾梯度的可优化性。
- 保证梯度可计算:PyTorch靠自动求导更新参数,所以损失函数里不能有硬阈值这类不可导操作,如果必须用,得换成近似可导的替代(比如用
torch.sigmoid代替阶跃函数)。 - 注意数值稳定性:要避免梯度爆炸或消失的情况,比如当你的损失涉及乘积、除法时,得考虑输入值的范围,防止计算后数值过小/过大稀释梯度信号。
- 匹配损失尺度与优化器:损失值的量级要和学习率适配,如果损失波动太大,要么调整学习率,要么对损失做归一化处理,不然模型参数更新会乱套。
- 贴合任务特性:比如交易场景里的正负样本不平衡、交易成本、滑点这些因素,必要时都要加入损失函数,让模型更贴合真实场景。
咱们来一步步拆解你遇到的问题:用盈利导向的损失模型学不到东西,但MSE能正常训练,核心原因和解决方向如下:
1. 损失函数的特性差异是核心
你用的盈利损失-torch.mean(pred*y)是非凸函数,而且梯度信号极弱——当模型预测和真实收益率y相关性很低时,pred*y的期望接近0,梯度是随机的(因为y有正有负),模型相当于在随机游走,根本找不到稳定的优化方向。
而MSE是凸函数,梯度直接和预测误差挂钩,只要预测和真实值有偏差,就有明确的优化指引,模型自然能稳步收敛。
2. 数值量级不匹配稀释了梯度信号
你的模型输出用tanh限制在[-1,1],但y是torch.log(s[i+1]/s[i])——你的s是正弦波加常数5,所以收益率的绝对值非常小(大概在±0.4以内)。这就导致pred*y的乘积量级极低,比如pred=1、y=0.1,乘积才0.1,批量平均后损失值只有-0.05左右,梯度更是小到可以忽略(比如batch_size=1000时,梯度是±0.1/1000=±1e-4),SGD用0.1的学习率,参数更新量也只有±1e-5,几乎看不到变化。
解决办法:
- 缩放收益率
y的量级:把y乘以一个系数(比如10),增强梯度信号。修改数据集的__getitem__:return self.s[i:i+self.l], 10 * torch.log(self.s[i+self.l+1]/self.s[i+self.l]) - 调整模型输出范围:既然
y的范围很小,tanh的[-1,1]输出范围其实过大,可以换成sigmoid缩放到[0,1]再映射到[-0.5,0.5],或者直接暂时去掉tanh,让模型输出范围先贴合y,后续再做截断。
3. 优化器选择不适合非凸损失
SGD对非凸损失的收敛性很差,尤其是梯度信号弱的时候。换成Adam这类自适应学习率的优化器会好很多,它能自动调整每个参数的学习率,适配不同的梯度量级。
修改模型的configure_optimizers方法:
def configure_optimizers(self): return torch.optim.Adam(self.parameters(), lr=self.learning_rate)
学习率可以尝试从0.001到0.01的范围调整,比SGD的学习率小一些更稳妥。
4. 简化模型结构先做验证
你当前用了Conv1d+两层线性,可能有点复杂,先换成简单的全连接网络,验证损失函数是否能生效,再逐步增加复杂度:
class MyModel(pl.LightningModule): def __init__(self, learning_rate=0.01): super().__init__() self.learning_rate = learning_rate self.network = nn.Sequential( nn.Linear(5, 10), nn.ReLU(), nn.Linear(10, 1), nn.Tanh() ) def forward(self, x): return self.network(x)
5. 加入监控指标辅助调试
训练过程中,除了损失,还要监控以下指标,帮你判断模型是否在学习:
- 盈利值:直接记录
torch.mean(self(x)*y),看它是否持续上升 - 预测与真实值的相关性:用
torch.corrcoef(torch.stack([self(x).flatten(), y.flatten()]))[0,1],查看相关性是否逐渐提升
6. 尝试转换任务类型
如果直接优化盈利损失效果不好,可以先把任务转换成分类任务:把收益率y转换成正负标签(正收益率为1,负为0),让模型先学会判断涨跌方向,再把分类结果转换成交易信号计算盈利。
比如修改数据集和模型:
# 修改数据集__getitem__ def __getitem__(self, i): y_real = torch.log(self.s[i+self.l+1]/self.s[i+self.l]) return self.s[i:i+self.l], (y_real > 0).float(), y_real # 模型输出用sigmoid,损失用BCELoss def forward(self, x): ... return nn.Sigmoid()(out) def step(self, batch, batch_idx, stage): x, y_cls, y_real = batch pred = self(x) loss = F.binary_cross_entropy(pred, y_cls.unsqueeze(1)) # 同时监控实际盈利 profit = torch.mean( (2*pred-1)*y_real ) self.log("profit", profit, prog_bar=True) return loss
分类损失是凸函数,更容易训练,等模型学会判断涨跌后,再转回优化盈利的损失函数,效果会好很多。
内容的提问来源于stack exchange,提问作者cjm2671

