CoolFace
Apppublic

zzy1213/A-Share-Quant-Radar

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
model.py328 linesDownload Raw Back to root
1"""2model.py — A 股多模态量化分析深度学习模型(v2 强化版)3 4承接 data_fetcher.py 的数据:5  - 时序分支:1D-CNN 提取局部模式 → Transformer Encoder 捕捉长程依赖6  - 文本分支:多条新闻嵌入经 Self-Attention 动态加权聚合7  - 回归输出:预测明日最高价 & 明日最低价8"""9 10import torch11import torch.nn as nn12import torch.nn.functional as F13import math14 15 16# ============================================================17# 1. 时序分支:CNN + Transformer 混合架构18# ============================================================19class TimeSeriesBranch(nn.Module):20    """21    混合时序编码器:22      1) 多尺度 1D-CNN 提取局部特征(3日/5日/7日窗口)23      2) Transformer Encoder 建模全局时序依赖24      3) 均值池化输出固定维度向量25    """26 27    def __init__(28        self,29        in_features: int = 10,30        cnn_channels: int = 64,31        d_model: int = 128,32        nhead: int = 4,33        ff_dim: int = 256,34        dropout: float = 0.1,35    ):36        super().__init__()37 38        # --- 多尺度 1D-CNN(并行三组不同 kernel_size) ---39        # Conv1d 输入格式: (B, C_in, L),所以 forward 中需要 transpose40        self.conv3 = nn.Conv1d(in_features, cnn_channels, kernel_size=3, padding=1)41        self.conv5 = nn.Conv1d(in_features, cnn_channels, kernel_size=5, padding=2)42        self.conv7 = nn.Conv1d(in_features, cnn_channels, kernel_size=7, padding=3)43        self.bn = nn.BatchNorm1d(cnn_channels * 3)44 45        # --- 将 CNN 输出映射到 Transformer 的 d_model ---46        self.proj = nn.Linear(cnn_channels * 3, d_model)47 48        # --- 位置编码 ---49        self.pos_encoding = PositionalEncoding(d_model=d_model, dropout=dropout)50 51        # --- Transformer Encoder(2 层) ---52        encoder_layer = nn.TransformerEncoderLayer(53            d_model=d_model,54            nhead=nhead,55            dim_feedforward=ff_dim,56            dropout=dropout,57            batch_first=True,58        )59        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=2)60        self.out_dim = d_model61 62    def forward(self, x: torch.Tensor) -> torch.Tensor:63        """64        Args:65            x: (B, S, F) — batch, seq_len, features66        Returns:67            (B, d_model)68        """69        # Conv1d 需要 (B, F, S)70        x_t = x.transpose(1, 2)                          # (B, F, S)71 72        # 多尺度卷积 + 拼接73        c3 = F.relu(self.conv3(x_t))                     # (B, cnn_ch, S)74        c5 = F.relu(self.conv5(x_t))                     # (B, cnn_ch, S)75        c7 = F.relu(self.conv7(x_t))                     # (B, cnn_ch, S)76        x_cnn = torch.cat([c3, c5, c7], dim=1)           # (B, cnn_ch*3, S)77        x_cnn = self.bn(x_cnn)78 79        # 转回 (B, S, cnn_ch*3),映射到 d_model80        x_cnn = x_cnn.transpose(1, 2)                    # (B, S, cnn_ch*3)81        x_proj = self.proj(x_cnn)                        # (B, S, d_model)82 83        # Transformer 编码84        x_enc = self.pos_encoding(x_proj)85        x_enc = self.transformer(x_enc)                  # (B, S, d_model)86 87        # 均值池化88        return x_enc.mean(dim=1)                          # (B, d_model)89 90 91# ============================================================92# 2. 文本分支:Self-Attention 动态聚合多条新闻93# ============================================================94class NewsSelfAttention(nn.Module):95    """96    对多条新闻的嵌入向量进行自注意力加权聚合。97 98    输入: (B, num_news, embed_dim)  — 每个样本有 num_news 条新闻99    输出: (B, hidden_dim)           — 加权聚合后的单一新闻表征100    """101 102    def __init__(self, embed_dim: int = 768, hidden_dim: int = 128, dropout: float = 0.1):103        super().__init__()104 105        # 先将每条新闻投影到 hidden_dim106        self.proj = nn.Linear(embed_dim, hidden_dim)107 108        # 自注意力:标准 scaled dot-product,单头即可109        self.query = nn.Linear(hidden_dim, hidden_dim)110        self.key = nn.Linear(hidden_dim, hidden_dim)111        self.value = nn.Linear(hidden_dim, hidden_dim)112        self.scale = math.sqrt(hidden_dim)113 114        # 聚合后的输出层115        self.out_mlp = nn.Sequential(116            nn.Linear(hidden_dim, hidden_dim),117            nn.ReLU(),118            nn.Dropout(dropout),119        )120        self.out_dim = hidden_dim121 122    def forward(self, x: torch.Tensor) -> torch.Tensor:123        """124        Args:125            x: (B, N, embed_dim) — N 条新闻的嵌入126        Returns:127            (B, hidden_dim)128        """129        h = F.relu(self.proj(x))                         # (B, N, hidden_dim)130 131        Q = self.query(h)                                 # (B, N, hidden_dim)132        K = self.key(h)                                   # (B, N, hidden_dim)133        V = self.value(h)                                 # (B, N, hidden_dim)134 135        # Scaled dot-product attention136        attn_scores = torch.bmm(Q, K.transpose(1, 2)) / self.scale  # (B, N, N)137        attn_weights = F.softmax(attn_scores, dim=-1)                # (B, N, N)138        attended = torch.bmm(attn_weights, V)                        # (B, N, hidden_dim)139 140        # 对所有新闻维度求均值,得到聚合表征141        pooled = attended.mean(dim=1)                     # (B, hidden_dim)142        return self.out_mlp(pooled)                       # (B, hidden_dim)143 144 145# ============================================================146# 3. 多模态量化模型(v2 — 回归输出明日最高/最低价)147# ============================================================148class MultimodalQuantModel(nn.Module):149    """150    双分支多模态网络 v2:151      - 时序分支:多尺度 1D-CNN + Transformer Encoder152      - 文本分支:Self-Attention 聚合多条新闻153      - 融合层:拼接 → MLP → 输出 2 个连续值(明日最高价、明日最低价)154    """155 156    def __init__(157        self,158        ts_features: int = 10,159        cnn_channels: int = 64,160        ts_d_model: int = 128,161        ts_nhead: int = 4,162        ts_ff_dim: int = 256,163        text_embedding_dim: int = 768,164        text_hidden: int = 128,165        num_news: int = 10,166        fusion_hidden: int = 128,167        num_outputs: int = 2,          # 明日最高价 + 明日最低价168        dropout: float = 0.1,169    ):170        super().__init__()171 172        # ---------- 时序分支 ----------173        self.ts_branch = TimeSeriesBranch(174            in_features=ts_features,175            cnn_channels=cnn_channels,176            d_model=ts_d_model,177            nhead=ts_nhead,178            ff_dim=ts_ff_dim,179            dropout=dropout,180        )181 182        # ---------- 文本分支 ----------183        self.text_branch = NewsSelfAttention(184            embed_dim=text_embedding_dim,185            hidden_dim=text_hidden,186            dropout=dropout,187        )188 189        # ---------- 融合回归头 ----------190        fused_dim = self.ts_branch.out_dim + self.text_branch.out_dim191        self.fusion = nn.Sequential(192            nn.Linear(fused_dim, fusion_hidden),193            nn.ReLU(),194            nn.Dropout(dropout),195            nn.Linear(fusion_hidden, fusion_hidden // 2),196            nn.ReLU(),197            nn.Linear(fusion_hidden // 2, num_outputs),198        )199 200    def forward(self, ts_input: torch.Tensor, text_input: torch.Tensor) -> torch.Tensor:201        """202        Args:203            ts_input:   (B, seq_len, ts_features) — 日线量价序列204            text_input: (B, num_news, text_embedding_dim) — 多条新闻嵌入205 206        Returns:207            predictions: (B, 2) — [明日预测最高价, 明日预测最低价]208        """209        x_ts = self.ts_branch(ts_input)                   # (B, ts_d_model)210        x_text = self.text_branch(text_input)              # (B, text_hidden)211 212        fused = torch.cat([x_ts, x_text], dim=1)          # (B, ts_d_model + text_hidden)213        return self.fusion(fused)                          # (B, 2)214 215 216# ============================================================217# 4. 正弦位置编码218# ============================================================219class PositionalEncoding(nn.Module):220    """标准正弦-余弦位置编码,batch_first=True。"""221 222    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 500):223        super().__init__()224        self.dropout = nn.Dropout(p=dropout)225 226        pe = torch.zeros(max_len, d_model)227        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)228        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))229        pe[:, 0::2] = torch.sin(position * div_term)230        pe[:, 1::2] = torch.cos(position * div_term)231        pe = pe.unsqueeze(0)232        self.register_buffer("pe", pe)233 234    def forward(self, x: torch.Tensor) -> torch.Tensor:235        x = x + self.pe[:, :x.size(1), :]236        return self.dropout(x)237 238 239# ============================================================240# 5. 自定义 Huber Loss — 应对 A 股极端跳空噪声241# ============================================================242class HuberLoss(nn.Module):243    """244    自定义 Huber Loss(Smooth L1 的推广形式)。245 246    |error| <= delta → 0.5 * error²(MSE,对小误差敏感)247    |error| >  delta → delta * |error| - 0.5 * delta²(MAE,容忍极端跳空)248 249    适用于 A 股涨跌停、极端跳空场景,有效抑制离群点主导梯度。250    """251 252    def __init__(self, delta: float = 1.0):253        super().__init__()254        self.delta = delta255 256    def forward(self, y_pred: torch.Tensor, y_true: torch.Tensor) -> torch.Tensor:257        residual = torch.abs(y_true - y_pred)258        quadratic = torch.clamp(residual, max=self.delta)259        linear = residual - quadratic260        loss = 0.5 * quadratic ** 2 + self.delta * linear261        return loss.mean()262 263 264# ============================================================265# 6. 测试代码 — 验证维度对齐266# ============================================================267if __name__ == "__main__":268    # ----- 超参设定 -----269    BATCH_SIZE = 4270    SEQ_LEN = 10               # ~10 个交易日271    TS_FEATURES = 10            # open/close/high/low/volume/amount/amplitude/pct_change/change/turnover272    NUM_NEWS = 10               # 每个样本 10 条新闻273    TEXT_EMBEDDING_DIM = 768    # 预训练模型嵌入维度274 275    print("=" * 60)276    print("  MultimodalQuantModel v2 结构测试")277    print("=" * 60)278 279    # ----- 实例化模型 -----280    model = MultimodalQuantModel(281        ts_features=TS_FEATURES,282        num_news=NUM_NEWS,283        text_embedding_dim=TEXT_EMBEDDING_DIM,284    )285 286    total_params = sum(p.numel() for p in model.parameters())287    trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)288    print(f"\n[模型参数量]  总计: {total_params:,}  |  可训练: {trainable_params:,}")289    print(f"\n[模型结构]\n{model}\n")290 291    # ----- 生成 dummy data -----292    dummy_ts = torch.randn(BATCH_SIZE, SEQ_LEN, TS_FEATURES)293    dummy_text = torch.randn(BATCH_SIZE, NUM_NEWS, TEXT_EMBEDDING_DIM)294 295    print("[输入维度]")296    print(f"  时序输入 (ts_input):   {dummy_ts.shape}   ← (batch, seq_len, features)")297    print(f"  文本输入 (text_input): {dummy_text.shape}  ← (batch, num_news, embed_dim)")298 299    # ----- 前向传播 -----300    model.eval()301    with torch.no_grad():302        preds = model(dummy_ts, dummy_text)303 304    print(f"\n[输出维度]")305    print(f"  predictions: {preds.shape}  ← (batch, 2) = [明日最高价, 明日最低价]")306    print(f"\n[示例输出(第 1 条样本)]")307    print(f"  预测明日最高价: {preds[0][0].item():.4f}")308    print(f"  预测明日最低价: {preds[0][1].item():.4f}")309 310    # ----- 测试 Huber Loss -----311    print(f"\n{'=' * 60}")312    print("  HuberLoss 回归测试")313    print("=" * 60)314 315    criterion = HuberLoss(delta=1.0)316    fake_pred = torch.tensor([[15.2, 14.0], [20.5, 19.1]])317    fake_true = torch.tensor([[15.0, 13.8], [25.0, 18.5]])318    loss = criterion(fake_pred, fake_true)319    print(f"  pred:  {fake_pred.tolist()}")320    print(f"  true:  {fake_true.tolist()}")321    print(f"  Huber Loss: {loss.item():.4f}")322 323    # ----- 维度断言 -----324    assert preds.shape == (BATCH_SIZE, 2), f"输出维度错误: {preds.shape}"325    print(f"\n{'=' * 60}")326    print("  所有维度检查通过!v2 模型骨架就绪。")327    print("=" * 60)328