PyTorch中VGG-16股票二分类预测准确率恒等于多数类占比问题排查
问题背景
- 基于20日K线图像数据训练CNN模型做股价预测,属于二分类任务,目标是判断股价未来上涨或持平。
- 已尝试调整学习率(搜索区间1e-1至1e-15)、weight_decay、batch_size、image_size等超参数,同时引入了lr_scheduler,但模型在验证集上的准确率始终与验证集的多数类占比持平:若验证集中标签0占比为60%,模型验证准确率就稳定在60%左右,调整验证集类别分布后该现象仍会复现。
- 数据集共包含30000张股票K线图,其中24000张作为训练集,3000张作为验证集,剩余3000张作为测试集;模型输入为尺寸64 * 64 * 3的股票K线图像,点击查看样例输入图像。
- 当前实验采用Adam优化器,损失函数为CrossEntropyLoss。
现有代码实现
VGG-16模型结构
import torch import torch.nn as nn import torch.nn.functional as F class VGG16(nn.Module): def __init__(self, img_size: int, num_classes: int): super(VGG16, self).__init__() self.conv1_1 = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1) self.conv1_2 = nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1) self.conv2_1 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1) self.conv2_2 = nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1) self.conv3_1 = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1) self.conv3_2 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) self.conv3_3 = nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1) self.conv4_1 = nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1) self.conv4_2 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1) self.conv4_3 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1) self.conv5_1 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1) self.conv5_2 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1) self.conv5_3 = nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1) self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) if img_size == 50: self.fc1 = nn.Linear(512, 4096) else: self.fc1 = nn.Linear(int(img_size * img_size / 2), 4096) self.fc2 = nn.Linear(4096, 4096) self.fc3 = nn.Linear(4096, num_classes) def forward(self, x): x = F.relu(self.conv1_1(x)) x = F.relu(self.conv1_2(x)) x = self.maxpool(x) x = F.relu(self.conv2_1(x)) x = F.relu(self.conv2_2(x)) x = self.maxpool(x) x = F.relu(self.conv3_1(x)) x = F.relu(self.conv3_2(x)) x = F.relu(self.conv3_3(x)) x = self.maxpool(x) x = F.relu(self.conv4_1(x)) x = F.relu(self.conv4_2(x)) x = F.relu(self.conv4_3(x)) x = self.maxpool(x) x = F.relu(self.conv5_1(x)) x = F.relu(self.conv5_2(x)) x = F.relu(self.conv5_3(x)) x = self.maxpool(x) x = x.reshape(x.shape[0], -1) x = F.relu(self.fc1(x)) x = F.dropout(x, 0.5) # dropout was included to combat over fitting x = F.relu(self.fc2(x)) x = F.dropout(x, 0.5) x = self.fc3(x) # x = torch.sigmoid(x) return x
训练核心逻辑
for epoch in range(self.config.num_epoch): self.model.train() train_losses = [] for batch in tqdm(self.train_loader, desc=f"Epoch[{epoch}] Train Step: "): self.optimizer.zero_grad() loss = self._train_batch(batch) train_losses.append(loss) loss.backward() self.optimizer.step() self.lr_scheduler.step()
def _train_batch(self, batch): images, labels = batch images = images.to(self.device) labels = labels.to(self.device) out = self.model(images) # Generate predictions loss = self.criterion(out, labels) # Calculate loss return loss
训练日志(验证集标签0、1占比50:50)
Epoch 1 Train: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 62/62 [00:06<00:00, 9.04it/s] Epoch 1 Evaluate: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 12.67it/s] 2022-06-09 19:41:33,054Z | src.stock_prediction.service.trainer | INFO | _train() : Epoch[1]: train_loss: 5.4477, valid_loss: 2.1717, valid_acc: 0.5084, lr: 0.0996 2022-06-09 19:41:33,403Z | src.stock_prediction.service.trainer | INFO | _save_model() : Successfully saved model Epoch 2 Train: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 62/62 [00:06<00:00, 9.04it/s] Epoch 2 Evaluate: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 12.49it/s] 2022-06-09 19:41:40,930Z | src.stock_prediction.service.trainer | INFO | _train() : Epoch[2]: train_loss: 2.3148, valid_loss: 1.8339, valid_acc: 0.5078, lr: 0.0994 2022-06-09 19:41:41,279Z | src.stock_prediction.service.trainer | INFO | _save_model() : Successfully saved model Epoch 3 Train: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 62/62 [00:06<00:00, 9.04it/s] Epoch 3 Evaluate: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 12.70it/s] 2022-06-09 19:41:48,790Z | src.stock_prediction.service.trainer | INFO | _train() : Epoch[3]: train_loss: 1.8587, valid_loss: 1.4972, valid_acc: 0.4961, lr: 0.0992 2022-06-09 19:41:49,136Z | src.stock_prediction.service.trainer | INFO | _save_model() : Successfully saved model Epoch 4 Train: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 62/62 [00:06<00:00, 9.02it/s] Epoch 4 Evaluate: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 11.53it/s] 2022-06-09 19:41:56,719Z | src.stock_prediction.service.trainer | INFO | _train() : Epoch[4]: train_loss: 1.6496, valid_loss: 1.4223, valid_acc: 0.4972, lr: 0.0990 2022-06-09 19:41:57,068Z | src.stock_prediction.service.trainer | INFO | _save_model() : Successfully saved model Epoch 5 Train: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 62/62 [00:06<00:00, 9.04it/s] Epoch 5 Evaluate: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7/7 [00:00<00:00, 12.71it/s] 2022-06-09 19:42:04,585Z | src.stock_prediction.service.trainer | INFO | _train() : Epoch[5]: train_loss: 1.3693, valid_loss: 0.9528, valid_acc: 0.4972, lr: 0.0988 2022-06-09 19:42:04,950Z | src.stock_prediction.service.trainer | INFO | _save_model() : Successfully saved model
排查方向与修复方案
模型准确率始终等于多数类占比,本质是模型没有从数据中学到任何有效特征,退化为恒定预测多数类的状态,按优先级从高到低排查以下问题:
- 学习率设置严重不合理
从日志看初始学习率在0.1左右,对于Adam优化器来说这个数值高出合理区间100-1000倍。Adam的默认最优初始学习率区间为1e-3~1e-5,0.1的学习率会导致参数更新步长过大,全程在损失面震荡,无法收敛到有效极小值。你之前搜索到1e-15的区间又过小,参数几乎不更新,同样无法收敛。
修复:将初始学习率设为1e-3,weight_decay先固定为1e-4,学习率调度器换成CosineAnnealingLR,不要用衰减速度过慢的调度器。 - 数据预处理或加载逻辑错误
- 先检查图像归一化:如果输入图像的像素值还处于0-255区间,没有通过
transforms.Normalize缩放到合理范围,会导致梯度剧烈震荡,模型无法收敛。 - 再检查标签匹配:抽几个dataloader输出的batch,可视化图像同时核对对应标签,确认没有因为shuffle逻辑错误、数据集索引错位导致图像和标签随机配对,这种情况下模型不可能学到有效规律。
- 先检查图像归一化:如果输入图像的像素值还处于0-255区间,没有通过
- 验证逻辑缺失
现有代码没有贴验证部分逻辑,需要确认验证阶段是否调用了model.eval(),是否用with torch.no_grad()包裹验证步骤,否则Dropout层在验证阶段仍处于随机丢弃状态,会导致验证准确率完全随机。 - 先做小样本过拟合测试
从训练集中挑100-200张样本,固定这部分数据反复训练,不做验证,如果训练10-20个epoch后训练准确率无法达到100%,说明代码本身存在bug,和超参数、模型结构、数据泛化性无关,优先排查前面的代码问题。 - 排查任务本身信噪比问题
纯裸K线的视觉特征对未来股价涨跌的预测信噪比极低,就算代码完全正确,也很难学到远高于随机猜测的准确率,但这是后续优化阶段要考虑的问题,当前模型完全不收敛,优先解决前面的代码和数据bug。
内容的提问来源于stack exchange,提问作者Kyle Lee
相关产品推荐
相关产品推荐

