C13.1

lstm(长短期记忆网络)

教材页

13章,长短期记忆网络

数据

数据集1:线性可分数据集
WTI.csv
,(附:程序Python格式)下载后存储到D:\下,如果是其它路径,则需要修改程序中文件的路径。
LSTM
神经网络预测算法

数据集:WTI原油期货价格(日度)

 

属性   含义

日期   交易日期

开盘   当日开盘价

  当日最高价

  当日最低价

收盘   当日收盘价

交易量 当日成交量

涨跌幅 当日价格涨跌百分比

在本实验中,根据历史60个交易日的数据预测下一交易日收盘价上涨或下跌。

构造的特征包括:

 

真实涨跌幅

ret1

ret3

ret5

ma5

ma10

ma20

macd

macd_signal

macd_hist

rsi14

bb_width

bb_pos

atr14

vol_ratio

 

预测目标:

上涨 = 1

下跌 = 0

 

任务

建立LSTM时间序列分类模型。

利用过去60天的历史信息预测下一交易日涨跌方向。

任务

建立支持LSTM模型。

## LSTM 全称为长短期记忆模型,其源于语言模型的处理方法,是对于RNN模型的推广,原理是维护两个向量,一个是当前向量,一个

## 是记忆向量。Y_n 由 当前向量 A_n 和往期的Y_{n-1}决定。而通过门控机制控制遗忘和记忆。这也就是长短期

## 记忆名字的由来,即下一个向量由当前向量和记忆向量组合而来。

##

## 本程序以 WTI 原油期货的历史行情为例,演示如何用 LSTM 预测"明天收盘价是否上涨"这一二分类任务。

## 完整流程如下:

##   1. 读取 & 清洗原始数据

##   2. 构造技术指标特征(MA / MACD / RSI / 布林带 / ATR / 成交量比率)

##   3. 滑动窗口切片 → 构造时序样本

##   4. 先划分训练/测试集,再做标准化(防止数据泄露)

##   5. 双层 LSTM + Dropout 建模

##   6. 梯度裁剪 + 学习率自适应调度训练

##   7. Accuracy & AUC 评估

Python

import numpy as np

import pandas as pd

import torch

import torch.nn as nn

from torch.utils.data import TensorDataset, DataLoader

from sklearn.preprocessing import StandardScaler

from sklearn.metrics import accuracy_score, roc_auc_score

 

# ── 随机种子 ──────────────────────────────────────────────────────

torch.manual_seed(42)

np.random.seed(42)

 

## 原始 CSV 是按时间倒序存储的(最新日期在前),用 iloc[::-1] 翻转为正序,

## reset_index 重建从 0 开始的行索引。

df = pd.read_csv("d:\\C3_1_WTI.csv")

df = df.iloc[::-1].reset_index(drop=True)

 

## 原始交易量带有单位后缀(如 "12.5K" "3.2M"),需要统一转换为数值。

## 涨跌幅带有百分号(如 "-0.44%"),去掉百分号后转 float 即为小数形式的涨跌比例。

def parse_volume(s):

    s = str(s).strip()

    try:

        if s.endswith("K"):

            return float(s[:-1]) * 1_000

        elif s.endswith("M"):

            return float(s[:-1]) * 1_000_000

        elif s.endswith("B"):

            return float(s[:-1]) * 1_000_000_000

        else:

            return float(s)

    except ValueError:

        return np.nan

 

df["真实交易量"] = df["交易量"].apply(parse_volume)

df["真实涨跌幅"] = df["涨跌幅"].str.replace("%", "").astype(float)

 

# ── 3. 数据修复 ───────────────────────────────────────────────────

base_cols = ["开盘", "", "", "收盘", "真实交易量", "真实涨跌幅"]

df[base_cols] = df[base_cols].replace([np.inf, -np.inf], np.nan)

nan_counts = df[base_cols].isna().sum()

if nan_counts.any():

    df[base_cols] = df[base_cols].ffill().bfill()

 

# ── 4. 技术指标特征 ───────────────────────────────────────────────

close = df["收盘"]

high  = df[""]

low   = df[""]

vol   = df["真实交易量"]

 

#为了让特征更容易被学习,我们增加了部分技术特征,如布林带,动量,均线等,和LSTM本身无关

# --- 动量 / 均线 ---

df["ma5"]    = close.rolling(5).mean()

df["ma10"]   = close.rolling(10).mean()

df["ma20"]   = close.rolling(20).mean()

df["ema12"]  = close.ewm(span=12, adjust=False).mean()

df["ema26"]  = close.ewm(span=26, adjust=False).mean()

 

# --- MACD ---

df["macd"]        = df["ema12"] - df["ema26"]

df["macd_signal"] = df["macd"].ewm(span=9, adjust=False).mean()

df["macd_hist"]   = df["macd"] - df["macd_signal"]

 

# --- RSI(14) ---

delta = close.diff()

gain  = delta.clip(lower=0).rolling(14).mean()

loss  = (-delta.clip(upper=0)).rolling(14).mean()

df["rsi14"] = 100 - 100 / (1 + gain / loss.replace(0, np.nan))

 

# --- Bollinger Bands (20) ---

bb_mid        = close.rolling(20).mean()

bb_std        = close.rolling(20).std()

df["bb_upper"] = bb_mid + 2 * bb_std

df["bb_lower"] = bb_mid - 2 * bb_std

df["bb_width"] = (df["bb_upper"] - df["bb_lower"]) / bb_mid

df["bb_pos"]   = (close - df["bb_lower"]) / (df["bb_upper"] - df["bb_lower"] + 1e-9)

 

# --- ATR(14):衡量波动性 ---

tr = pd.concat([

    high - low,

    (high - close.shift()).abs(),

    (low  - close.shift()).abs(),

], axis=1).max(axis=1)

df["atr14"] = tr.rolling(14).mean()

 

# --- 收益率特征 ---

df["ret1"]  = close.pct_change(1)

df["ret3"]  = close.pct_change(3)

df["ret5"]  = close.pct_change(5)

 

# --- 成交量变化率 ---

df["vol_ma5"]    = vol.rolling(5).mean()

df["vol_ratio"]  = vol / (df["vol_ma5"] + 1e-9)

 

# ── 5. 预测目标 ───────────────────────────────────────────────────

df["Target"] = (close.shift(-1) > close).astype(int)

 

# 删掉因滚动窗口产生的 NaN 行,以及最后一行(Target NaN

df = df.dropna().reset_index(drop=True)

 

print(f"[数据] 有效行数: {len(df)}")

 

# ── 6. 特征列 ─────────────────────────────────────────────────────

features = [

    # 原始价格相对特征

    "真实涨跌幅",

    "ret1", "ret3", "ret5",

    # 均线比率(价格 / 均线,反映偏离程度)

    "ma5", "ma10", "ma20",

    # MACD

    "macd", "macd_signal", "macd_hist",

    # RSI

    "rsi14",

    # 布林带

    "bb_width", "bb_pos",

    # 波动性

    "atr14",

    # 成交量

    "vol_ratio",

]

 

n_feat = len(features)

print(f"[特征] {n_feat} : {features}")

 

# ── 7. 滑窗构造样本 ───────────────────────────────────────────────

## LSTM 需要输入形如 (样本数, 时间步, 特征数) 的三维张量。

## 滑动窗口:取连续 window_size 天的特征作为一个样本的输入,

## 对应的标签是第 window_size+1 天的涨跌。

## 窗口每次向后移动一天,因此总样本数 = len(df) - window_size

window_size = 60

 

X_raw, y_raw = [], []

for i in range(len(df) - window_size):

    X_raw.append(df[features].iloc[i : i + window_size].values)

    y_raw.append(df["Target"].iloc[i + window_size])

 

X_raw = np.array(X_raw, dtype=np.float64)

y_raw = np.array(y_raw)

 

print(f"[样本] X: {X_raw.shape}, y: {y_raw.shape}")

print(f"[标签] 上涨比例: {y_raw.mean():.3f}")

 

# ── 8. 划分训练 / 测试集 ──────────────────────────────────────────

## 时序数据必须按时间顺序划分,前 80% 训练,后 20% 测试,

## 不能随机打乱——否则未来数据会泄露到训练集,导致评估虚高。

split = int(len(X_raw) * 0.8)

 

X_train_raw, X_test_raw = X_raw[:split], X_raw[split:]

y_train,      y_test     = y_raw[:split], y_raw[split:]

 

# ── 9. 标准化────────────────────────────────────

## StandardScaler 将每个特征变换为均值=0、方差=1 的分布,

## 消除不同特征之间量级差异(如价格 vs RSI vs 成交量比率)对梯度的影响。

##

## 关键:只在训练集上 fit(计算均值/方差),

##       再用同一套参数 transform 测试集。

## 若在全量数据上 fit,测试集的统计信息会渗入训练过程,造成数据泄露。

##

## 因为 X 是三维数组 (N, T, F)StandardScaler 只接受二维输入,

## 所以先 reshape (N*T, F) fit/transform,再 reshape 回去。

scaler = StandardScaler()

 

X_train_s = scaler.fit_transform(

    X_train_raw.reshape(-1, n_feat)

).reshape(len(X_train_raw), window_size, n_feat)

 

X_test_s = scaler.transform(

    X_test_raw.reshape(-1, n_feat)

).reshape(len(X_test_raw), window_size, n_feat)

 

assert not (np.isnan(X_train_s).any() or np.isinf(X_train_s).any()), \

    "标准化后 X_train 仍含 NaN/inf"

 

# ── 10. 转为 Tensor ───────────────────────────────────────────────

X_train = torch.tensor(X_train_s, dtype=torch.float32)

X_test  = torch.tensor(X_test_s,  dtype=torch.float32)

y_train_t = torch.tensor(y_train, dtype=torch.float32)

y_test_t  = torch.tensor(y_test,  dtype=torch.float32)

 

# ── 11. DataLoader ────────────────────────────────────────────────

train_loader = DataLoader(

    TensorDataset(X_train, y_train_t),

    batch_size=64,

    shuffle=False,

)

 

# ── 12. 模型:双层 LSTM + Dropout ─────────────────────────────────

## LSTM 内部有四个门:输入门、遗忘门、输出门、候选记忆。

## - 遗忘门:决定上一时刻记忆向量中哪些信息需要丢弃。

## - 输入门:决定当前输入中哪些信息值得写入记忆。

## - 输出门:决定当前时刻从记忆中读取哪些信息作为输出。

##

## num_layers=2:堆叠两层 LSTM,第一层的输出作为第二层的输入,

##               可以捕捉更高层次的时序抽象(类似深层 CNN 捕捉高阶纹理)。

## dropout=0.3:在两层 LSTM 之间随机丢弃 30% 的神经元,防止过拟合。

## 全连接头:LSTM 最后时间步的隐状态 → Linear(6432) ReLU Dropout Linear(321)

##           输出一个标量 logit,配合 BCEWithLogitsLoss 完成二分类。

class OilLSTM(nn.Module):

    def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.3):

        super().__init__()

        self.lstm = nn.LSTM(

            input_size=input_size,

            hidden_size=hidden_size,

            num_layers=num_layers,

            batch_first=True,

            dropout=dropout,        # 层间 dropoutnum_layers>1 才生效)

        )

        self.head = nn.Sequential(

            nn.Linear(hidden_size, 32),

            nn.ReLU(),

            nn.Dropout(dropout),

            nn.Linear(32, 1),

        )

 

    def forward(self, x):

        out, _ = self.lstm(x)

        return self.head(out[:, -1, :])

 

model     = OilLSTM(input_size=n_feat)

criterion = nn.BCEWithLogitsLoss()

optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(

    optimizer, mode="min", factor=0.5, patience=3, verbose=True

)

 

# ── 13. 训练 ──────────────────────────────────────────────────────

## BCEWithLogitsLoss:数值上等价于 Sigmoid + 二元交叉熵,

##                    合并计算更稳定(避免 Sigmoid 饱和区的数值误差)。

## Adam:自适应学习率优化器,收敛比 SGD 更快,适合大多数深度学习任务。

## weight_decay=1e-4L2 正则化,惩罚过大的权重,减轻过拟合。

## ReduceLROnPlateau:当训练 loss 连续 3 epoch 不下降时,

##                    将学习率乘以 0.5,帮助跳出平台区。

epochs = 50

 

for epoch in range(epochs):

    model.train()

    total_loss = 0.0

 

    for batch_x, batch_y in train_loader:

        optimizer.zero_grad()

        loss = criterion(model(batch_x).squeeze(), batch_y)

        loss.backward()

        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)  # 梯度裁剪

        optimizer.step()

        total_loss += loss.item()

 

    avg_loss = total_loss / len(train_loader)

    scheduler.step(avg_loss)

 

    if (epoch + 1) % 5 == 0:

        print(f"Epoch [{epoch + 1:>2}/{epochs}]  Loss: {avg_loss:.4f}")

 

# ── 14. 评估 ──────────────────────────────────────────────────────

## model.eval():关闭 Dropout,使推理结果确定性。

## torch.no_grad():关闭梯度计算,节省显存和计算量。

##

## Accuracy:预测正确的样本比例,直观但对不平衡数据不够敏感。

## AUCROC 曲线下面积):衡量模型区分正负样本的综合能力,

##   0.5 = 随机猜,1.0 = 完美分类,不受分类阈值影响,更稳健。

model.eval()

with torch.no_grad():

    outputs = model(X_test)

    preds = (torch.sigmoid(outputs.squeeze()) > 0.5).int()

    probs = torch.sigmoid(outputs).squeeze().numpy()

 

acc = accuracy_score(y_test, preds.numpy())

auc = roc_auc_score(y_test, probs)

 

print(f"\nAccuracy : {acc:.4f}")

print(f"AUC      : {auc:.4f}")

C++

输出

结果

Epoch [ 5/50]  Loss: 0.6899

Epoch [10/50]  Loss: 0.6882

Epoch [15/50]  Loss: 0.6865

Epoch [20/50]  Loss: 0.6844

Epoch [25/50]  Loss: 0.6811

Epoch [30/50]  Loss: 0.6727

Epoch [35/50]  Loss: 0.6663

Epoch [40/50]  Loss: 0.6570

Epoch [45/50]  Loss: 0.6462

Epoch [50/50]  Loss: 0.6361

 

Accuracy : 0.4878

AUC      : 0.5204

 

结果分析

本实验使用双层LSTM网络对WTI原油期货价格进行涨跌方向预测。

模型输入为过去60个交易日构成的时间序列窗口,共包含15个技术分析特征,包括移动平均线(MA)、MACDRSI、布林带(Bollinger Bands)、ATR波动率以及成交量相关指标。

从训练过程来看,损失函数由0.6899逐步下降至0.6361,说明模型成功学习到了部分时间序列结构信息。

然而在测试集上的分类准确率仅为48.78%AUC0.5204,仅略高于随机猜测(AUC=0.5)。

这表明:

  1. WTI原油市场的短期涨跌方向具有较强随机性;
  2. 单纯依靠历史价格与技术指标难以准确预测下一交易日涨跌;
  3. LSTM虽然能够学习时间序列特征,但其预测能力受到金融市场弱式有效性的限制;
  4. 技术指标中包含的信息有限,无法提供足够强的预测信号。

因此,本实验验证了LSTM在金融时间序列分析中的应用流程,但同时也说明对于原油市场,仅依赖历史价格信息进行短期方向预测具有较大难度。

 

书籍

姜维.《数据分析与数据挖掘》、《数据分析》,电子工业出版社, 20232026

软件

PythonC++(附加orsci包)。