基于PyTorch的Transformer-CNN时序预测实战:从特征工程到服务化部署

文章目录

    • 一、项目背景与整体流程
      • [1.1 技术栈说明](#1.1 技术栈说明)
      • [1.2 整体流程可视化](#1.2 整体流程可视化)
    • 二、环境搭建
      • [2.1 安装依赖库](#2.1 安装依赖库)
      • [2.2 验证环境](#2.2 验证环境)
    • 三、数据准备与探索
      • [3.1 数据集选择](#3.1 数据集选择)
      • [3.2 数据探索分析](#3.2 数据探索分析)
    • 四、特征工程
      • [4.1 特征构建](#4.1 特征构建)
      • [4.2 时序数据集构建](#4.2 时序数据集构建)
    • 五、Transformer-CNN模型构建
      • [5.1 模型架构设计](#5.1 模型架构设计)
      • [5.2 模型代码实现](#5.2 模型代码实现)
    • 六、模型训练与验证
      • [6.1 训练代码实现](#6.1 训练代码实现)
      • [6.2 运行训练脚本](#6.2 运行训练脚本)
    • 七、模型服务化部署
      • [7.1 部署流程设计](#7.1 部署流程设计)
      • [7.2 部署代码实现](#7.2 部署代码实现)
      • [7.3 启动并测试部署服务](#7.3 启动并测试部署服务)
        • [7.3.1 启动服务](#7.3.1 启动服务)
        • [7.3.2 健康检查](#7.3.2 健康检查)
        • [7.3.3 预测接口测试](#7.3.3 预测接口测试)
    • 八、项目优化与扩展建议
      • [8.1 模型优化](#8.1 模型优化)
      • [8.2 工程优化](#8.2 工程优化)
      • [8.3 功能扩展](#8.3 功能扩展)
    • 总结

一、项目背景与整体流程

时序预测是工业生产、金融风控、能源调度等领域的核心需求,传统的ARIMA、LSTM等方法在处理长序列依赖和局部特征提取时存在局限性。本文将结合Transformer的长序列建模能力和CNN的局部特征提取优势,基于PyTorch实现一个端到端的时序预测系统,完整覆盖从数据预处理、特征工程、模型构建、训练验证到最终服务化部署的全流程。

1.1 技术栈说明

  • 核心框架:PyTorch 2.0+(模型构建与训练)
  • 数据处理:Pandas、NumPy、Scikit-learn
  • 可视化:Matplotlib、Seaborn
  • 服务部署:FastAPI + Uvicorn
  • 环境依赖:Python 3.8+

1.2 整体流程可视化

以下是本次实战的完整流程,涵盖从数据准备到服务部署的所有关键环节:
数据准备
特征工程
数据集构建
模型设计
模型训练与验证
模型评估
模型导出
服务化部署
接口测试

二、环境搭建

2.1 安装依赖库

首先创建并激活虚拟环境(可选但推荐),然后安装所需依赖:

bash 复制代码
# 创建虚拟环境
conda create -n ts_pred python=3.8
conda activate ts_pred

# 安装核心依赖
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118
pip install pandas==2.0.3 numpy==1.24.3 scikit-learn==1.3.0 matplotlib==3.7.2 seaborn==0.12.2
pip install fastapi==0.103.1 uvicorn==0.23.2 pydantic==2.4.2 python-multipart==0.0.6
pip install tqdm==4.65.0 scipy==1.10.1

2.2 验证环境

创建env_check.py文件,验证关键库是否安装成功:

python 复制代码
import torch
import pandas as pd
import numpy as np
from sklearn import __version__ as sklearn_version
import fastapi

# 打印版本信息
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
print(f"Pandas版本: {pd.__version__}")
print(f"NumPy版本: {np.__version__}")
print(f"Scikit-learn版本: {sklearn_version}")
print(f"FastAPI版本: {fastapi.__version__}")

# 输出示例
# PyTorch版本: 2.0.1
# CUDA是否可用: True
# Pandas版本: 2.0.3
# NumPy版本: 1.24.3
# Scikit-learn版本: 1.3.0
# FastAPI版本: 0.103.1

运行该文件:

bash 复制代码
python env_check.py

若所有库版本正常输出且CUDA状态符合预期,说明环境搭建完成。

三、数据准备与探索

3.1 数据集选择

本文使用公开的电力负荷时序数据集(也可替换为自己的时序数据),数据格式为CSV,包含timestamp(时间戳)和load(电力负荷值)两列,时间粒度为1小时。

首先创建data目录,下载数据集(或生成模拟数据):

python 复制代码
# create_data.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 生成模拟时序数据(替代真实数据集,方便零基础用户测试)
def generate_synthetic_data():
    # 生成时间序列:2022年1月1日 00:00 到 2023年12月31日 23:00,1小时粒度
    start_date = datetime(2022, 1, 1, 0, 0)
    end_date = datetime(2023, 12, 31, 23, 0)
    time_range = pd.date_range(start=start_date, end=end_date, freq='H')
    
    # 生成带趋势、周期和噪声的电力负荷数据
    np.random.seed(42)
    t = np.arange(len(time_range))
    trend = 0.0001 * t  # 长期趋势
    daily_season = np.sin(2 * np.pi * t / 24)  # 日周期
    weekly_season = np.sin(2 * np.pi * t / (24*7))  # 周周期
    noise = np.random.normal(0, 0.1, len(t))  # 随机噪声
    load = 10 + trend + daily_season + weekly_season + noise
    
    # 构建DataFrame
    df = pd.DataFrame({
        'timestamp': time_range,
        'load': load
    })
    
    # 保存为CSV
    df.to_csv('data/electric_load.csv', index=False)
    print(f"生成数据量: {len(df)} 条")
    print(f"数据时间范围: {df['timestamp'].min()} 到 {df['timestamp'].max()}")
    print(f"数据预览:\n{df.head()}")

if __name__ == "__main__":
    generate_synthetic_data()

运行该脚本生成数据:

bash 复制代码
python create_data.py

3.2 数据探索分析

创建data_analysis.py文件,进行数据探索:

python 复制代码
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 设置中文字体(避免乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 加载数据
df = pd.read_csv('data/electric_load.csv')
df['timestamp'] = pd.to_datetime(df['timestamp'])

# 1. 基本信息
print("=== 数据基本信息 ===")
print(df.info())
print("\n=== 数据统计描述 ===")
print(df.describe())

# 2. 缺失值检查
print("\n=== 缺失值统计 ===")
missing_values = df.isnull().sum()
print(missing_values)

# 3. 时间范围检查
print("\n=== 时间范围 ===")
print(f"开始时间: {df['timestamp'].min()}")
print(f"结束时间: {df['timestamp'].max()}")
print(f"总时长: {df['timestamp'].max() - df['timestamp'].min()}")
print(f"数据点数量: {len(df)}")

# 4. 可视化数据趋势
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle('电力负荷数据探索分析', fontsize=16)

# 4.1 整体趋势
axes[0,0].plot(df['timestamp'], df['load'], color='#3498db', linewidth=0.8)
axes[0,0].set_title('电力负荷整体趋势')
axes[0,0].set_xlabel('时间')
axes[0,0].set_ylabel('负荷值')
axes[0,0].grid(True, alpha=0.3)

# 4.2 分布直方图
axes[0,1].hist(df['load'], bins=50, color='#e74c3c', alpha=0.7)
axes[0,1].set_title('电力负荷分布')
axes[0,1].set_xlabel('负荷值')
axes[0,1].set_ylabel('频次')
axes[0,1].grid(True, alpha=0.3)

# 4.3 日周期模式(取一周数据)
sample_week = df[(df['timestamp'] >= '2022-01-01') & (df['timestamp'] < '2022-01-08')]
sample_week['hour'] = sample_week['timestamp'].dt.hour
sns.boxplot(x='hour', y='load', data=sample_week, ax=axes[1,0], palette='viridis')
axes[1,0].set_title('日内负荷分布(周样本)')
axes[1,0].set_xlabel('小时')
axes[1,0].set_ylabel('负荷值')
axes[1,0].grid(True, alpha=0.3)

# 4.4 周周期模式
df['weekday'] = df['timestamp'].dt.weekday
sns.boxplot(x='weekday', y='load', data=df, ax=axes[1,1], palette='coolwarm')
axes[1,1].set_title('周内负荷分布')
axes[1,1].set_xlabel('星期(0=周一)')
axes[1,1].set_ylabel('负荷值')
axes[1,1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('data/load_analysis.png', dpi=300, bbox_inches='tight')
plt.show()

# 5. 相关性分析(滞后特征)
df['load_lag1'] = df['load'].shift(1)  # 滞后1小时
df['load_lag24'] = df['load'].shift(24)  # 滞后24小时
df['load_lag168'] = df['load'].shift(168)  # 滞后1周
corr = df[['load', 'load_lag1', 'load_lag24', 'load_lag168']].corr()
print("\n=== 滞后特征相关性 ===")
print(corr)

# 绘制相关性热力图
plt.figure(figsize=(8, 6))
sns.heatmap(corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1, fmt='.2f')
plt.title('滞后特征相关性热力图')
plt.savefig('data/correlation_heatmap.png', dpi=300, bbox_inches='tight')
plt.show()

运行该脚本,可得到数据的基本统计信息、可视化图表和相关性分析结果,为后续特征工程提供依据。

四、特征工程

时序预测的性能高度依赖特征工程,本节将构建时间特征、滞后特征、滚动统计特征等,并进行特征预处理。

4.1 特征构建

创建feature_engineering.py文件,实现特征构建逻辑:

python 复制代码
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler

def build_features(df):
    """
    构建时序特征
    参数:
        df: 原始DataFrame,包含timestamp和load列
    返回:
        df_features: 包含所有特征的DataFrame
        feature_cols: 特征列名列表
        target_col: 目标列名
    """
    # 复制数据避免修改原数据
    df_features = df.copy()
    
    # 1. 时间特征
    df_features['hour'] = df_features['timestamp'].dt.hour  # 小时(0-23)
    df_features['day'] = df_features['timestamp'].dt.day  # 日期(1-31)
    df_features['weekday'] = df_features['timestamp'].dt.weekday  # 星期(0-6)
    df_features['month'] = df_features['timestamp'].dt.month  # 月份(1-12)
    df_features['is_weekend'] = df_features['weekday'].apply(lambda x: 1 if x >= 5 else 0)  # 是否周末
    df_features['is_holiday'] = 0  # 简化处理,实际场景可添加节假日特征
    
    # 2. 滞后特征(根据相关性分析选择)
    lag_steps = [1, 2, 3, 6, 12, 24, 48, 168]  # 滞后1、2、3、6、12、24、48、168小时
    for lag in lag_steps:
        df_features[f'load_lag_{lag}'] = df_features['load'].shift(lag)
    
    # 3. 滚动统计特征
    window_sizes = [6, 12, 24, 48]  # 滚动窗口大小
    for window in window_sizes:
        # 滚动均值
        df_features[f'load_roll_mean_{window}'] = df_features['load'].rolling(window=window).mean()
        # 滚动标准差
        df_features[f'load_roll_std_{window}'] = df_features['load'].rolling(window=window).std()
        # 滚动最大值
        df_features[f'load_roll_max_{window}'] = df_features['load'].rolling(window=window).max()
        # 滚动最小值
        df_features[f'load_roll_min_{window}'] = df_features['load'].rolling(window=window).min()
    
    # 4. 差分特征(消除趋势)
    df_features['load_diff_1'] = df_features['load'].diff(1)
    df_features['load_diff_24'] = df_features['load'].diff(24)
    
    # 5. 目标变量(预测未来1小时的负荷)
    df_features['target'] = df_features['load'].shift(-1)
    
    # 删除缺失值(由滞后和滚动特征产生)
    df_features = df_features.dropna()
    
    # 定义特征列和目标列
    # 排除非特征列(时间戳、原始负荷、目标)
    feature_cols = [col for col in df_features.columns if col not in ['timestamp', 'load', 'target']]
    target_col = 'target'
    
    return df_features, feature_cols, target_col

def preprocess_features(df_features, feature_cols, target_col, test_size=0.2):
    """
    特征预处理:标准化、数据集划分
    参数:
        df_features: 包含特征和目标的DataFrame
        feature_cols: 特征列名列表
        target_col: 目标列名
        test_size: 测试集比例
    返回:
        X_train, X_test: 训练/测试特征
        y_train, y_test: 训练/测试目标
        scaler_X: 特征标准化器
        scaler_y: 目标标准化器
    """
    # 按时间顺序划分数据集(时序数据不能随机划分)
    split_idx = int(len(df_features) * (1 - test_size))
    train_data = df_features.iloc[:split_idx]
    test_data = df_features.iloc[split_idx:]
    
    # 初始化标准化器
    scaler_X = StandardScaler()
    scaler_y = StandardScaler()
    
    # 拟合并转换训练集
    X_train = scaler_X.fit_transform(train_data[feature_cols])
    y_train = scaler_y.fit_transform(train_data[[target_col]]).ravel()
    
    # 转换测试集(使用训练集的标准化器)
    X_test = scaler_X.transform(test_data[feature_cols])
    y_test = scaler_y.transform(test_data[[target_col]]).ravel()
    
    # 转换为DataFrame便于查看(可选)
    X_train_df = pd.DataFrame(X_train, columns=feature_cols, index=train_data.index)
    X_test_df = pd.DataFrame(X_test, columns=feature_cols, index=test_data.index)
    
    print(f"训练集大小: {X_train.shape}")
    print(f"测试集大小: {X_test.shape}")
    print(f"特征列数量: {len(feature_cols)}")
    print(f"\n特征标准化后训练集统计:\n{X_train_df.describe().round(2)}")
    
    return X_train, X_test, y_train, y_test, scaler_X, scaler_y

if __name__ == "__main__":
    # 加载数据
    df = pd.read_csv('data/electric_load.csv')
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    
    # 构建特征
    df_features, feature_cols, target_col = build_features(df)
    print(f"\n构建特征后数据形状: {df_features.shape}")
    print(f"\n特征列列表:\n{feature_cols}")
    
    # 预处理特征
    X_train, X_test, y_train, y_test, scaler_X, scaler_y = preprocess_features(
        df_features, feature_cols, target_col, test_size=0.2
    )
    
    # 保存预处理后的数据
    np.save('data/X_train.npy', X_train)
    np.save('data/X_test.npy', X_test)
    np.save('data/y_train.npy', y_train)
    np.save('data/y_test.npy', y_test)
    
    # 保存标准化器(后续部署需要)
    import joblib
    joblib.dump(scaler_X, 'data/scaler_X.pkl')
    joblib.dump(scaler_y, 'data/scaler_y.pkl')
    
    # 保存特征列名
    with open('data/feature_cols.txt', 'w') as f:
        for col in feature_cols:
            f.write(col + '\n')
    
    print("\n预处理完成,数据已保存到data目录!")

运行该脚本,将完成特征构建和预处理,并将处理后的数据集、标准化器、特征列名保存到data目录,供后续模型训练使用。

4.2 时序数据集构建

时序预测模型需要将特征组织成序列形式,创建dataset.py文件,实现PyTorch Dataset类:

python 复制代码
import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np

class TimeSeriesDataset(Dataset):
    """
    时序数据集类
    将扁平的特征矩阵转换为序列形式
    """
    def __init__(self, X, y, seq_len=24):
        """
        参数:
            X: 特征矩阵 (n_samples, n_features)
            y: 目标数组 (n_samples,)
            seq_len: 序列长度(使用前seq_len个时间步的特征预测当前目标)
        """
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.float32)
        self.seq_len = seq_len
        
        # 确保有足够的序列长度
        if len(self.X) < seq_len:
            raise ValueError(f"样本数量({len(self.X)})小于序列长度({seq_len})")
    
    def __len__(self):
        # 有效样本数 = 总样本数 - 序列长度 + 1
        return len(self.X) - self.seq_len + 1
    
    def __getitem__(self, idx):
        # 获取序列特征:[idx, idx+seq_len)
        x_seq = self.X[idx:idx+self.seq_len]
        # 获取对应目标:序列最后一个时间步的目标
        y_target = self.y[idx+self.seq_len-1]
        
        return x_seq, y_target

def create_data_loaders(X_train, X_test, y_train, y_test, seq_len=24, batch_size=32):
    """
    创建DataLoader
    参数:
        X_train/X_test: 训练/测试特征
        y_train/y_test: 训练/测试目标
        seq_len: 序列长度
        batch_size: 批次大小
    返回:
        train_loader, test_loader: 训练/测试DataLoader
    """
    # 创建数据集
    train_dataset = TimeSeriesDataset(X_train, y_train, seq_len)
    test_dataset = TimeSeriesDataset(X_test, y_test, seq_len)
    
    # 创建DataLoader
    train_loader = DataLoader(
        train_dataset,
        batch_size=batch_size,
        shuffle=False,  # 时序数据不能打乱
        num_workers=0,  # 新手建议设为0,避免多进程问题
        drop_last=True  # 丢弃最后一个不完整批次
    )
    
    test_loader = DataLoader(
        test_dataset,
        batch_size=batch_size,
        shuffle=False,
        num_workers=0,
        drop_last=False
    )
    
    print(f"训练集批次数量: {len(train_loader)}")
    print(f"测试集批次数量: {len(test_loader)}")
    print(f"单个序列特征形状: {train_dataset[0][0].shape}")
    print(f"单个目标形状: {train_dataset[0][1].shape}")
    
    return train_loader, test_loader

if __name__ == "__main__":
    # 加载预处理后的数据
    X_train = np.load('data/X_train.npy')
    X_test = np.load('data/X_test.npy')
    y_train = np.load('data/y_train.npy')
    y_test = np.load('data/y_test.npy')
    
    # 创建DataLoader
    train_loader, test_loader = create_data_loaders(
        X_train, X_test, y_train, y_test, seq_len=24, batch_size=32
    )
    
    # 测试数据加载
    for batch_idx, (x_batch, y_batch) in enumerate(train_loader):
        print(f"批次 {batch_idx+1}:")
        print(f"  特征形状: {x_batch.shape} (batch_size, seq_len, n_features)")
        print(f"  目标形状: {y_batch.shape} (batch_size,)")
        if batch_idx == 1:  # 只打印前2个批次
            break

运行该脚本,验证数据集和DataLoader是否正常工作,输出应类似:

复制代码
训练集批次数量: 427
测试集批次数量: 108
单个序列特征形状: torch.Size([24, 41])
单个目标形状: torch.Size([])
批次 1:
  特征形状: torch.Size([32, 24, 41]) (batch_size, seq_len, n_features)
  目标形状: torch.Size([32]) (batch_size,)
批次 2:
  特征形状: torch.Size([32, 24, 41]) (batch_size, seq_len, n_features)
  目标形状: torch.Size([32]) (batch_size,)

五、Transformer-CNN模型构建

5.1 模型架构设计

本文设计的Transformer-CNN混合模型结合了两者的优势:

  • CNN层:提取局部时序特征,降低计算复杂度
  • Transformer层:捕捉长序列依赖关系
  • 全连接层:最终预测

模型架构流程图如下:
输入序列
CNN特征提取
维度转换
Transformer编码
全局池化
全连接层
预测输出
卷积层1
池化1
卷积层2
池化2
位置编码
注意力机制
前馈网络
层归一化

5.2 模型代码实现

创建model.py文件,实现Transformer-CNN混合模型:

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class PositionalEncoding(nn.Module):
    """位置编码层:为时序序列添加位置信息"""
    def __init__(self, d_model, max_len=5000):
        super().__init__()
        # 初始化位置编码矩阵
        position = torch.arange(max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
        
        pe = torch.zeros(max_len, 1, d_model)
        pe[:, 0, 0::2] = torch.sin(position * div_term)
        pe[:, 0, 1::2] = torch.cos(position * div_term)
        self.register_buffer('pe', pe)  # 不参与训练的缓冲区

    def forward(self, x):
        """
        参数:
            x: 输入张量 (seq_len, batch_size, d_model)
        返回:
            x + 位置编码
        """
        x = x + self.pe[:x.size(0)]
        return x

class CNNBlock(nn.Module):
    """CNN特征提取块"""
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super().__init__()
        self.conv = nn.Conv1d(
            in_channels=in_channels,
            out_channels=out_channels,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding
        )
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool1d(kernel_size=2, stride=2)
    
    def forward(self, x):
        """
        参数:
            x: 输入张量 (batch_size, in_channels, seq_len)
        返回:
            提取的特征 (batch_size, out_channels, seq_len//2)
        """
        x = self.conv(x)
        x = self.relu(x)
        x = self.pool(x)
        return x

class TransformerCNNModel(nn.Module):
    """Transformer-CNN混合时序预测模型"""
    def __init__(
        self,
        n_features,    # 输入特征数
        seq_len,       # 输入序列长度
        n_head=8,      # 注意力头数
        n_layers=2,    # Transformer编码层数量
        d_model=128,   # 模型维度
        d_ff=256,      # 前馈网络维度
        dropout=0.1    # Dropout概率
    ):
        super().__init__()
        
        # 1. CNN特征提取模块
        self.cnn_block1 = CNNBlock(in_channels=n_features, out_channels=64)
        self.cnn_block2 = CNNBlock(in_channels=64, out_channels=128)
        
        # 计算CNN输出后的序列长度
        self.cnn_seq_len = seq_len // 2 // 2  # 经过两次MaxPool1d(2)
        
        # 2. 维度投影层(将CNN输出维度转换为d_model)
        self.projection = nn.Linear(128, d_model)
        
        # 3. 位置编码
        self.pos_encoding = PositionalEncoding(d_model=d_model, max_len=self.cnn_seq_len)
        
        # 4. Transformer编码层
        encoder_layers = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=n_head,
            dim_feedforward=d_ff,
            dropout=dropout,
            activation='relu',
            batch_first=False  # 输入格式: (seq_len, batch_size, d_model)
        )
        self.transformer_encoder = nn.TransformerEncoder(
            encoder_layer=encoder_layers,
            num_layers=n_layers,
            norm=nn.LayerNorm(d_model)
        )
        
        # 5. 输出层
        self.fc1 = nn.Linear(d_model, 64)
        self.fc2 = nn.Linear(64, 1)
        self.dropout = nn.Dropout(dropout)
        self.relu = nn.ReLU()
        
        # 初始化权重
        self._init_weights()
    
    def _init_weights(self):
        """初始化模型权重"""
        for m in self.modules():
            if isinstance(m, nn.Conv1d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight)
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.LayerNorm):
                nn.init.constant_(m.weight, 1.0)
                nn.init.constant_(m.bias, 0)
    
    def forward(self, x):
        """
        前向传播
        参数:
            x: 输入张量 (batch_size, seq_len, n_features)
        返回:
            预测值 (batch_size, 1)
        """
        # 1. CNN特征提取:调整维度为(batch_size, n_features, seq_len)
        x = x.permute(0, 2, 1)  # (batch, features, seq_len)
        x = self.cnn_block1(x)  # (batch, 64, seq_len//2)
        x = self.cnn_block2(x)  # (batch, 128, seq_len//4)
        
        # 2. 维度转换:(batch, 128, cnn_seq_len) -> (cnn_seq_len, batch, 128)
        x = x.permute(2, 0, 1)  # (seq_len, batch, features)
        
        # 3. 投影到d_model维度
        x = self.projection(x)  # (seq_len, batch, d_model)
        
        # 4. 添加位置编码
        x = self.pos_encoding(x)
        
        # 5. Transformer编码
        x = self.transformer_encoder(x)  # (seq_len, batch, d_model)
        
        # 6. 全局平均池化
        x = x.mean(dim=0)  # (batch, d_model)
        
        # 7. 输出层
        x = self.dropout(x)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.dropout(x)
        x = self.fc2(x)  # (batch, 1)
        
        return x

if __name__ == "__main__":
    # 测试模型
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f"使用设备: {device}")
    
    # 模型参数
    n_features = 41    # 特征数(与特征工程输出一致)
    seq_len = 24       # 序列长度
    batch_size = 32    # 批次大小
    
    # 创建模型实例
    model = TransformerCNNModel(
        n_features=n_features,
        seq_len=seq_len,
        n_head=8,
        n_layers=2,
        d_model=128,
        d_ff=256,
        dropout=0.1
    ).to(device)
    
    # 创建测试输入
    x_test = torch.randn(batch_size, seq_len, n_features).to(device)
    
    # 前向传播
    with torch.no_grad():
        y_pred = model(x_test)
    
    # 打印模型结构和输出形状
    print(f"\n模型结构:\n{model}")
    print(f"\n输入形状: {x_test.shape}")
    print(f"输出形状: {y_pred.shape}")
    
    # 计算模型参数量
    total_params = sum(p.numel() for p in model.parameters())
    trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
    print(f"\n总参数量: {total_params:,}")
    print(f"可训练参数量: {trainable_params:,}")

运行该脚本,验证模型结构和前向传播是否正常,输出应包含模型结构、输入输出形状和参数量信息。

六、模型训练与验证

6.1 训练代码实现

创建train.py文件,实现完整的训练流程:

python 复制代码
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import numpy as np
import matplotlib.pyplot as plt
import time
import os
from tqdm import tqdm

# 导入自定义模块
from dataset import TimeSeriesDataset, create_data_loaders
from model import TransformerCNNModel

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# 设备配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"训练设备: {device}")

# 训练参数配置
CONFIG = {
    'seq_len': 24,               # 序列长度
    'batch_size': 32,            # 批次大小
    'n_epochs': 50,              # 训练轮数
    'learning_rate': 1e-3,       # 学习率
    'weight_decay': 1e-5,        # 权重衰减(L2正则)
    'patience': 5,               # 早停耐心值
    'model_save_path': 'models/best_model.pth',  # 最佳模型保存路径
    'seed': 42                   # 随机种子
}

# 设置随机种子(确保结果可复现)
def set_seed(seed):
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

# 定义评估指标
def calculate_metrics(y_true, y_pred):
    """
    计算回归评估指标
    参数:
        y_true: 真实值数组
        y_pred: 预测值数组
    返回:
        mse, rmse, mae, r2: 均方误差、均方根误差、平均绝对误差、决定系数
    """
    # 转换为numpy数组
    y_true = np.array(y_true)
    y_pred = np.array(y_pred)
    
    # 均方误差
    mse = np.mean((y_true - y_pred) ** 2)
    # 均方根误差
    rmse = np.sqrt(mse)
    # 平均绝对误差
    mae = np.mean(np.abs(y_true - y_pred))
    # 决定系数R²
    ss_total = np.sum((y_true - np.mean(y_true)) ** 2)
    ss_res = np.sum((y_true - y_pred) ** 2)
    r2 = 1 - (ss_res / ss_total) if ss_total != 0 else 0
    
    return {
        'MSE': mse,
        'RMSE': rmse,
        'MAE': mae,
        'R2': r2
    }

# 训练函数
def train_model(model, train_loader, val_loader, criterion, optimizer, config):
    """
    模型训练函数
    """
    # 创建模型保存目录
    os.makedirs(os.path.dirname(config['model_save_path']), exist_ok=True)
    
    # 初始化训练记录
    train_losses = []
    val_losses = []
    val_metrics = []
    best_val_loss = float('inf')
    patience_counter = 0
    
    # 开始训练
    start_time = time.time()
    for epoch in range(config['n_epochs']):
        # 训练阶段
        model.train()
        train_loss = 0.0
        train_bar = tqdm(train_loader, desc=f'Epoch {epoch+1}/{config["n_epochs"]} [Train]')
        
        for batch_idx, (x_batch, y_batch) in enumerate(train_bar):
            # 数据移到设备
            x_batch = x_batch.to(device)
            y_batch = y_batch.to(device).unsqueeze(1)  # (batch,) -> (batch, 1)
            
            # 前向传播
            optimizer.zero_grad()
            y_pred = model(x_batch)
            loss = criterion(y_pred, y_batch)
            
            # 反向传播和优化
            loss.backward()
            # 梯度裁剪(防止梯度爆炸)
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()
            
            # 累计损失
            train_loss += loss.item() * x_batch.size(0)
            train_bar.set_postfix({'loss': loss.item()})
        
        # 计算平均训练损失
        avg_train_loss = train_loss / len(train_loader.dataset)
        train_losses.append(avg_train_loss)
        
        # 验证阶段
        model.eval()
        val_loss = 0.0
        val_y_true = []
        val_y_pred = []
        
        with torch.no_grad():
            val_bar = tqdm(val_loader, desc=f'Epoch {epoch+1}/{config["n_epochs"]} [Val]')
            for x_batch, y_batch in val_bar:
                x_batch = x_batch.to(device)
                y_batch = y_batch.to(device).unsqueeze(1)
                
                y_pred = model(x_batch)
                loss = criterion(y_pred, y_batch)
                
                val_loss += loss.item() * x_batch.size(0)
                
                # 收集真实值和预测值(转换为CPU)
                val_y_true.extend(y_batch.cpu().numpy().ravel())
                val_y_pred.extend(y_pred.cpu().numpy().ravel())
                
                val_bar.set_postfix({'loss': loss.item()})
        
        # 计算平均验证损失和评估指标
        avg_val_loss = val_loss / len(val_loader.dataset)
        val_losses.append(avg_val_loss)
        metrics = calculate_metrics(val_y_true, val_y_pred)
        val_metrics.append(metrics)
        
        # 打印epoch信息
        print(f'\nEpoch {epoch+1}/{config["n_epochs"]}:')
        print(f'  训练损失: {avg_train_loss:.6f}')
        print(f'  验证损失: {avg_val_loss:.6f}')
        print(f'  验证指标 - MSE: {metrics["MSE"]:.6f}, RMSE: {metrics["RMSE"]:.6f}, MAE: {metrics["MAE"]:.6f}, R2: {metrics["R2"]:.6f}')
        
        # 早停和最佳模型保存
        if avg_val_loss < best_val_loss:
            best_val_loss = avg_val_loss
            patience_counter = 0
            # 保存最佳模型
            torch.save({
                'epoch': epoch + 1,
                'model_state_dict': model.state_dict(),
                'optimizer_state_dict': optimizer.state_dict(),
                'best_val_loss': best_val_loss,
                'config': config
            }, config['model_save_path'])
            print(f'  保存最佳模型,验证损失: {best_val_loss:.6f}')
        else:
            patience_counter += 1
            print(f'  早停计数器: {patience_counter}/{config["patience"]}')
            if patience_counter >= config['patience']:
                print(f'  早停触发,训练结束!')
                break
    
    # 计算总训练时间
    total_time = time.time() - start_time
    print(f'\n训练完成!总耗时: {total_time:.2f} 秒 ({total_time/60:.2f} 分钟)')
    print(f'最佳验证损失: {best_val_loss:.6f}')
    
    return model, train_losses, val_losses, val_metrics

# 绘制训练曲线
def plot_training_curves(train_losses, val_losses, val_metrics):
    """
    绘制训练和验证曲线
    """
    fig, axes = plt.subplots(2, 2, figsize=(16, 12))
    fig.suptitle('模型训练过程', fontsize=16)
    
    # 损失曲线
    axes[0,0].plot(train_losses, label='训练损失', color='#3498db', linewidth=2)
    axes[0,0].plot(val_losses, label='验证损失', color='#e74c3c', linewidth=2)
    axes[0,0].set_title('训练/验证损失')
    axes[0,0].set_xlabel('Epoch')
    axes[0,0].set_ylabel('MSE Loss')
    axes[0,0].legend()
    axes[0,0].grid(True, alpha=0.3)
    
    # RMSE曲线
    rmse_values = [m['RMSE'] for m in val_metrics]
    axes[0,1].plot(rmse_values, color='#2ecc71', linewidth=2)
    axes[0,1].set_title('验证集RMSE')
    axes[0,1].set_xlabel('Epoch')
    axes[0,1].set_ylabel('RMSE')
    axes[0,1].grid(True, alpha=0.3)
    
    # MAE曲线
    mae_values = [m['MAE'] for m in val_metrics]
    axes[1,0].plot(mae_values, color='#f39c12', linewidth=2)
    axes[1,0].set_title('验证集MAE')
    axes[1,0].set_xlabel('Epoch')
    axes[1,0].set_ylabel('MAE')
    axes[1,0].grid(True, alpha=0.3)
    
    # R²曲线
    r2_values = [m['R2'] for m in val_metrics]
    axes[1,1].plot(r2_values, color='#9b59b6', linewidth=2)
    axes[1,1].set_title('验证集R²')
    axes[1,1].set_xlabel('Epoch')
    axes[1,1].set_ylabel('R²')
    axes[1,1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig('models/training_curves.png', dpi=300, bbox_inches='tight')
    plt.show()

# 主函数
if __name__ == "__main__":
    # 设置随机种子
    set_seed(CONFIG['seed'])
    
    # 1. 加载数据
    print("\n=== 加载数据 ===")
    X_train = np.load('data/X_train.npy')
    X_test = np.load('data/X_test.npy')
    y_train = np.load('data/y_train.npy')
    y_test = np.load('data/y_test.npy')
    
    # 2. 创建DataLoader
    print("\n=== 创建DataLoader ===")
    train_loader, test_loader = create_data_loaders(
        X_train, X_test, y_train, y_test,
        seq_len=CONFIG['seq_len'],
        batch_size=CONFIG['batch_size']
    )
    
    # 3. 创建模型
    print("\n=== 初始化模型 ===")
    n_features = X_train.shape[1]  # 特征数
    model = TransformerCNNModel(
        n_features=n_features,
        seq_len=CONFIG['seq_len'],
        n_head=8,
        n_layers=2,
        d_model=128,
        d_ff=256,
        dropout=0.1
    ).to(device)
    
    # 4. 定义损失函数和优化器
    criterion = nn.MSELoss()  # 均方误差损失
    optimizer = optim.AdamW(
        model.parameters(),
        lr=CONFIG['learning_rate'],
        weight_decay=CONFIG['weight_decay']
    )
    # 学习率调度器
    scheduler = optim.lr_scheduler.ReduceLROnPlateau(
        optimizer,
        mode='min',
        factor=0.5,
        patience=3,
        verbose=True
    )
    
    # 5. 训练模型
    print("\n=== 开始训练 ===")
    model, train_losses, val_losses, val_metrics = train_model(
        model, train_loader, test_loader, criterion, optimizer, CONFIG
    )
    
    # 6. 学习率调度
    for val_loss in val_losses:
        scheduler.step(val_loss)
    
    # 7. 绘制训练曲线
    print("\n=== 绘制训练曲线 ===")
    plot_training_curves(train_losses, val_losses, val_metrics)
    
    # 8. 加载最佳模型并评估
    print("\n=== 加载最佳模型 ===")
    checkpoint = torch.load(CONFIG['model_save_path'])
    model.load_state_dict(checkpoint['model_state_dict'])
    print(f"加载Epoch {checkpoint['epoch']}的最佳模型,验证损失: {checkpoint['best_val_loss']:.6f}")
    
    # 9. 最终评估
    print("\n=== 最终模型评估 ===")
    model.eval()
    test_y_true = []
    test_y_pred = []
    
    with torch.no_grad():
        for x_batch, y_batch in test_loader:
            x_batch = x_batch.to(device)
            y_batch = y_batch.to(device).unsqueeze(1)
            
            y_pred = model(x_batch)
            
            test_y_true.extend(y_batch.cpu().numpy().ravel())
            test_y_pred.extend(y_pred.cpu().numpy().ravel())
    
    # 计算最终指标
    final_metrics = calculate_metrics(test_y_true, test_y_pred)
    print("最终测试集指标:")
    for metric, value in final_metrics.items():
        print(f"  {metric}: {value:.6f}")
    
    # 绘制真实值vs预测值
    plt.figure(figsize=(12, 6))
    plt.plot(test_y_true[:200], label='真实值', color='#3498db', linewidth=1.5)
    plt.plot(test_y_pred[:200], label='预测值', color='#e74c3c', linewidth=1.5, alpha=0.8)
    plt.title('测试集真实值vs预测值(前200个样本)')
    plt.xlabel('样本索引')
    plt.ylabel('标准化负荷值')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.savefig('models/prediction_vs_true.png', dpi=300, bbox_inches='tight')
    plt.show()

6.2 运行训练脚本

执行训练脚本开始模型训练:

bash 复制代码
python train.py

训练过程中会输出每轮的训练/验证损失、评估指标,并在训练结束后绘制训练曲线和预测结果对比图。训练完成后,最佳模型会保存到models/best_model.pth

七、模型服务化部署

7.1 部署流程设计

模型部署流程如下:
服务端
客户端请求
FastAPI接口
请求参数验证
特征预处理
模型推理
结果反标准化
返回预测结果
加载模型
加载标准化器
加载特征列名

7.2 部署代码实现

创建deploy.py文件,实现FastAPI服务部署:

python 复制代码
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import uvicorn
import torch
import numpy as np
import pandas as pd
import joblib
import os
from datetime import datetime, timedelta

# 导入自定义模型
from model import TransformerCNNModel

# 初始化FastAPI应用
app = FastAPI(
    title="Transformer-CNN时序预测服务",
    description="基于PyTorch的电力负荷时序预测API",
    version="1.0.0"
)

# 全局变量
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
MODEL = None
SCALER_X = None
SCALER_Y = None
FEATURE_COLS = None
CONFIG = {
    'seq_len': 24,
    'n_features': None,
    'model_path': 'models/best_model.pth',
    'scaler_x_path': 'data/scaler_X.pkl',
    'scaler_y_path': 'data/scaler_y.pkl',
    'feature_cols_path': 'data/feature_cols.txt'
}

# 数据模型定义(请求参数)
class TimeSeriesRequest(BaseModel):
    """时序预测请求参数模型"""
    timestamp: str = Field(..., example="2024-01-01 00:00:00", description="预测时间戳")
    load_history: list[float] = Field(..., min_items=24, max_items=24, description="前24小时负荷值")
    
    class Config:
        schema_extra = {
            "example": {
                "timestamp": "2024-01-01 00:00:00",
                "load_history": [10.2, 9.8, 9.5, 9.3, 9.1, 9.0, 9.2, 9.5, 10.0, 10.5, 11.0, 11.2,
                                11.5, 11.3, 11.0, 10.8, 10.5, 10.3, 10.1, 9.9, 9.7, 9.6, 9.8, 10.0]
            }
        }

class PredictionResponse(BaseModel):
    """预测响应模型"""
    timestamp: str
    predicted_load: float
    prediction_time: str
    status: str = "success"

# 加载模型和预处理组件
def load_resources():
    """加载模型、标准化器和特征列"""
    global MODEL, SCALER_X, SCALER_Y, FEATURE_COLS, CONFIG
    
    # 1. 加载特征列名
    if not os.path.exists(CONFIG['feature_cols_path']):
        raise FileNotFoundError(f"特征列文件不存在: {CONFIG['feature_cols_path']}")
    
    with open(CONFIG['feature_cols_path'], 'r') as f:
        FEATURE_COLS = [line.strip() for line in f.readlines()]
    CONFIG['n_features'] = len(FEATURE_COLS)
    print(f"加载特征列: {len(FEATURE_COLS)} 个")
    
    # 2. 加载标准化器
    if not os.path.exists(CONFIG['scaler_x_path']):
        raise FileNotFoundError(f"特征标准化器不存在: {CONFIG['scaler_x_path']}")
    if not os.path.exists(CONFIG['scaler_y_path']):
        raise FileNotFoundError(f"目标标准化器不存在: {CONFIG['scaler_y_path']}")
    
    SCALER_X = joblib.load(CONFIG['scaler_x_path'])
    SCALER_Y = joblib.load(CONFIG['scaler_y_path'])
    print("加载标准化器完成")
    
    # 3. 加载模型
    if not os.path.exists(CONFIG['model_path']):
        raise FileNotFoundError(f"模型文件不存在: {CONFIG['model_path']}")
    
    # 创建模型实例
    model = TransformerCNNModel(
        n_features=CONFIG['n_features'],
        seq_len=CONFIG['seq_len'],
        n_head=8,
        n_layers=2,
        d_model=128,
        d_ff=256,
        dropout=0.1
    ).to(DEVICE)
    
    # 加载模型权重
    checkpoint = torch.load(CONFIG['model_path'], map_location=DEVICE)
    model.load_state_dict(checkpoint['model_state_dict'])
    model.eval()  # 设置为评估模式
    MODEL = model
    
    print(f"加载模型完成,最佳验证损失: {checkpoint['best_val_loss']:.6f}")
    print("所有资源加载完成,服务已准备就绪!")

# 特征构建函数(与训练阶段一致)
def build_prediction_features(timestamp_str, load_history):
    """
    为预测请求构建特征
    """
    # 解析时间戳
    try:
        timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
    except ValueError:
        raise HTTPException(status_code=400, detail="时间戳格式错误,应为: YYYY-MM-DD HH:MM:SS")
    
    # 验证历史数据长度
    if len(load_history) != CONFIG['seq_len']:
        raise HTTPException(
            status_code=400,
            detail=f"历史负荷数据长度应为 {CONFIG['seq_len']},当前为 {len(load_history)}"
        )
    
    # 创建特征字典
    features = {}
    
    # 时间特征
    features['hour'] = timestamp.hour
    features['day'] = timestamp.day
    features['weekday'] = timestamp.weekday()
    features['month'] = timestamp.month
    features['is_weekend'] = 1 if timestamp.weekday() >= 5 else 0
    features['is_holiday'] = 0
    
    # 滞后特征(使用历史数据)
    lag_steps = [1, 2, 3, 6, 12, 24, 48, 168]
    for lag in lag_steps:
        if lag <= len(load_history):
            features[f'load_lag_{lag}'] = load_history[-lag]
        else:
            # 对于超过历史长度的滞后,使用最后一个值填充
            features[f'load_lag_{lag}'] = load_history[-1]
    
    # 滚动统计特征
    window_sizes = [6, 12, 24, 48]
    for window in window_sizes:
        if window <= len(load_history):
            window_data = load_history[-window:]
            features[f'load_roll_mean_{window}'] = np.mean(window_data)
            features[f'load_roll_std_{window}'] = np.std(window_data)
            features[f'load_roll_max_{window}'] = np.max(window_data)
            features[f'load_roll_min_{window}'] = np.min(window_data)
        else:
            # 窗口超过历史长度时,使用所有历史数据计算
            features[f'load_roll_mean_{window}'] = np.mean(load_history)
            features[f'load_roll_std_{window}'] = np.std(load_history)
            features[f'load_roll_max_{window}'] = np.max(load_history)
            features[f'load_roll_min_{window}'] = np.min(load_history)
    
    # 差分特征
    if len(load_history) >= 1:
        features['load_diff_1'] = load_history[-1] - load_history[-2] if len(load_history)>=2 else 0
    if len(load_history) >= 24:
        features['load_diff_24'] = load_history[-1] - load_history[-24]
    else:
        features['load_diff_24'] = 0
    
    # 确保特征列顺序与训练一致
    feature_vals = []
    missing_cols = []
    for col in FEATURE_COLS:
        if col in features:
            feature_vals.append(features[col])
        else:
            missing_cols.append(col)
            feature_vals.append(0)  # 缺失特征填充0
    
    if missing_cols:
        print(f"警告:缺失特征列 {missing_cols},已填充0")
    
    # 转换为数组
    feature_array = np.array(feature_vals).reshape(1, -1)
    return feature_array

# API接口定义
@app.on_event("startup")
async def startup_event():
    """服务启动时加载资源"""
    try:
        load_resources()
    except Exception as e:
        print(f"资源加载失败: {str(e)}")
        raise e

@app.get("/health", summary="健康检查")
async def health_check():
    """检查服务是否正常运行"""
    if MODEL is None:
        raise HTTPException(status_code=503, detail="模型未加载")
    return {
        "status": "healthy",
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "device": str(DEVICE),
        "model_loaded": MODEL is not None
    }

@app.post("/predict", response_model=PredictionResponse, summary="时序预测接口")
async def predict(request: TimeSeriesRequest):
    """
    电力负荷时序预测接口
    """
    try:
        # 1. 构建特征
        feature_array = build_prediction_features(request.timestamp, request.load_history)
        
        # 2. 特征标准化
        feature_scaled = SCALER_X.transform(feature_array)
        
        # 3. 构建序列(重复seq_len次,实际应使用连续序列,此处简化)
        seq_array = np.repeat(feature_scaled, CONFIG['seq_len'], axis=0).reshape(1, CONFIG['seq_len'], -1)
        
        # 4. 转换为张量
        seq_tensor = torch.tensor(seq_array, dtype=torch.float32).to(DEVICE)
        
        # 5. 模型推理
        with torch.no_grad():
            pred_scaled = MODEL(seq_tensor)
        
        # 6. 反标准化
        pred_scaled_np = pred_scaled.cpu().numpy().reshape(-1, 1)
        pred_original = SCALER_Y.inverse_transform(pred_scaled_np)[0][0]
        
        # 7. 构建响应
        response = PredictionResponse(
            timestamp=request.timestamp,
            predicted_load=float(pred_original),
            prediction_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        )
        
        return response
    
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"预测失败: {str(e)}")

# 主函数
if __name__ == "__main__":
    # 启动服务
    uvicorn.run(
        "deploy:app",
        host="0.0.0.0",
        port=8000,
        reload=False,  # 生产环境设为False
        workers=1      # 根据CPU核心数调整
    )

7.3 启动并测试部署服务

7.3.1 启动服务

运行部署脚本启动FastAPI服务:

bash 复制代码
python deploy.py

服务启动成功后,会输出类似以下信息:

复制代码
加载特征列: 41 个
加载标准化器完成
加载模型完成,最佳验证损失: 0.012345
所有资源加载完成,服务已准备就绪!
INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
7.3.2 健康检查

访问健康检查接口,验证服务是否正常:

bash 复制代码
curl http://localhost:8000/health

正常响应:

json 复制代码
{
  "status":"healthy",
  "timestamp":"2024-01-01 12:00:00",
  "device":"cuda",
  "model_loaded":true
}
7.3.3 预测接口测试

使用curl测试预测接口:

bash 复制代码
curl -X POST "http://localhost:8000/predict" -H "Content-Type: application/json" -d '{
  "timestamp": "2024-01-01 00:00:00",
  "load_history": [10.2, 9.8, 9.5, 9.3, 9.1, 9.0, 9.2, 9.5, 10.0, 10.5, 11.0, 11.2, 11.5, 11.3, 11.0, 10.8, 10.5, 10.3, 10.1, 9.9, 9.7, 9.6, 9.8, 10.0]
}'

正常响应:

json 复制代码
{
  "timestamp":"2024-01-01 00:00:00",
  "predicted_load":10.123456,
  "prediction_time":"2024-01-01 12:01:00",
  "status":"success"
}

此外,FastAPI还提供了自动生成的接口文档,可通过以下地址访问:

八、项目优化与扩展建议

8.1 模型优化

  1. 超参数调优:使用Optuna、Ray Tune等工具优化序列长度、模型维度、学习率等超参数
  2. 模型改进
    • 添加注意力可视化,分析模型关注的关键特征
    • 尝试不同的CNN架构(如ResNet1D)
    • 增加Transformer层数或注意力头数
  3. 正则化:添加早停、Dropout、权重衰减等防止过拟合

8.2 工程优化

  1. 性能优化
    • 使用TorchScript导出模型,提高推理速度
    • 模型量化(INT8)降低显存占用
    • 批量预测提高吞吐量
  2. 部署优化
    • 使用Docker容器化部署
    • 添加负载均衡(Nginx)
    • 接入监控系统(Prometheus + Grafana)
  3. 数据优化
    • 添加更多特征(如天气、节假日、电价等)
    • 数据增强(时序数据扩充)

8.3 功能扩展

  1. 多步预测:修改模型输出,支持未来24/48小时的多步预测
  2. 异常检测:结合预测结果和实际值,实现异常负荷检测
  3. 可视化界面:开发前端页面,展示预测结果和历史数据

总结

关键点回顾

  1. 完整流程覆盖:本次实战从数据准备、特征工程、模型构建、训练验证到服务化部署,实现了端到端的时序预测解决方案,所有代码均可直接运行,零基础用户可按步骤复现。
  2. 模型设计思路:结合CNN的局部特征提取优势和Transformer的长序列依赖建模能力,解决了传统时序模型的局限性,通过详细的代码实现和注释,清晰展示了混合模型的构建过程。
  3. 工程化落地:基于FastAPI实现了模型的服务化部署,提供了标准化的API接口,包含参数验证、异常处理、健康检查等生产级特性,可直接应用于实际项目。

核心收获

  • 掌握时序数据的特征工程方法,包括时间特征、滞后特征、滚动统计特征的构建
  • 理解Transformer-CNN混合模型的设计原理和实现方式
  • 学会使用PyTorch构建、训练和验证时序预测模型
  • 掌握基于FastAPI的模型服务化部署方法,实现从模型到服务的落地
相关推荐
子夜江寒2 小时前
基于PyTorch的语言模型实现详解
pytorch·语言模型
薛定谔的猫19823 小时前
二十、使用PyTorch和Hugging Face Transformers训练中文GPT-2模型的技术实践
人工智能·pytorch·gpt
然哥依旧15 小时前
【轴承故障诊断】基于融合鱼鹰和柯西变异的麻雀优化算法OCSSA-VMD-CNN-BILSTM轴承诊断研究【西储大学数据】(Matlab代码实现)
算法·支持向量机·matlab·cnn
HDO清风19 小时前
CASIA-HWDB2.x 数据集DGRL文件解析(python)
开发语言·人工智能·pytorch·python·目标检测·计算机视觉·restful
小Tomkk19 小时前
PyTorch +YOLO + Label Studio + 图像识别 深度学习项目实战 (二)
pytorch·深度学习·yolo
工程师老罗19 小时前
Pytorch如何加载和读取VOC数据集用来做目标检测?
人工智能·pytorch·目标检测
副露のmagic20 小时前
草履虫级 Transformer code by hand
深度学习·bert·transformer
njsgcs1 天前
dqn和cnn有什么区别 dqn怎么保存训练经验到本地
人工智能·神经网络·cnn