文章目录
-
- 一、项目背景与整体流程
-
- [1.1 技术栈说明](#1.1 技术栈说明)
- [1.2 整体流程可视化](#1.2 整体流程可视化)
- 二、环境搭建
-
- [2.1 安装依赖库](#2.1 安装依赖库)
- [2.2 验证环境](#2.2 验证环境)
- 三、多维数据准备与探索
-
- [3.1 数据集说明](#3.1 数据集说明)
- [3.2 生成模拟多维数据集](#3.2 生成模拟多维数据集)
- [3.3 多维数据探索分析](#3.3 多维数据探索分析)
- 四、多维数据预处理与特征工程
-
- [4.1 数据预处理](#4.1 数据预处理)
- [4.2 时序数据集构建](#4.2 时序数据集构建)
- 五、TCN-GRU混合模型构建
-
- [5.1 模型架构设计](#5.1 模型架构设计)
- [5.2 TCN-GRU模型代码实现](#5.2 TCN-GRU模型代码实现)
- 六、模型训练与验证
-
- [6.1 训练代码实现](#6.1 训练代码实现)
- [6.2 运行训练脚本](#6.2 运行训练脚本)
- 七、模型服务化封装
-
- [7.1 FastAPI服务封装](#7.1 FastAPI服务封装)
- 八、Docker容器化构建
-
- [8.1 Docker容器化流程](#8.1 Docker容器化流程)
- [8.2 编写Dockerfile](#8.2 编写Dockerfile)
- [8.3 编写.dockerignore](#8.3 编写.dockerignore)
- [8.4 构建Docker镜像](#8.4 构建Docker镜像)
-
- [8.4.1 前置条件](#8.4.1 前置条件)
- [8.4.2 构建命令](#8.4.2 构建命令)
- [8.4.3 验证镜像](#8.4.3 验证镜像)
- [8.5 本地测试Docker容器](#8.5 本地测试Docker容器)
-
- [8.5.1 验证容器运行状态](#8.5.1 验证容器运行状态)
- [8.5.2 本地接口测试](#8.5.2 本地接口测试)
- [8.5.3 停止/删除本地容器(可选)](#8.5.3 停止/删除本地容器(可选))
- 九、Docker云端部署
-
- [9.1 云端部署前置条件](#9.1 云端部署前置条件)
- [9.2 镜像推送与拉取(两种方式)](#9.2 镜像推送与拉取(两种方式))
-
- 方式1:本地镜像打包→上传云服务器→加载(适合小镜像,零基础推荐)
- [方式2:使用Docker镜像仓库(阿里云镜像仓库/华为云镜像仓库/Docker Hub,适合生产环境)](#方式2:使用Docker镜像仓库(阿里云镜像仓库/华为云镜像仓库/Docker Hub,适合生产环境))
- [9.3 云服务器启动Docker容器](#9.3 云服务器启动Docker容器)
- [9.4 云端服务公网访问](#9.4 云端服务公网访问)
- 十、接口测试与实际调用
-
- [10.1 浏览器/Postman测试](#10.1 浏览器/Postman测试)
- [10.2 Python代码调用](#10.2 Python代码调用)
- [10.3 常见问题排查](#10.3 常见问题排查)
- 十一、项目优化与扩展建议
-
- [11.1 模型优化](#11.1 模型优化)
- [11.2 工程优化](#11.2 工程优化)
- [11.3 功能扩展](#11.3 功能扩展)
- 十二、项目总结
-
- [12.1 核心技术亮点](#12.1 核心技术亮点)
- [12.2 核心收获](#12.2 核心收获)
- [12.3 实际应用价值](#12.3 实际应用价值)
一、项目背景与整体流程
电力负荷预测是智能电网调度、能源规划和电力市场交易的核心技术支撑,传统的单一模型(如GRU、LSTM)在处理长时序依赖和局部特征捕捉上存在不足。TCN(Temporal Convolutional Network,时序卷积网络)凭借其并行计算能力和长序列建模优势,与GRU(Gated Recurrent Unit,门控循环单元)的时序记忆特性结合,能够有效提升电力负荷预测的精度和效率。
本文将基于PyTorch实现TCN-GRU混合模型的电力负荷预测系统,完整覆盖多维数据预处理→特征工程→模型构建→训练验证→模型优化→Docker容器化→云端部署 的全流程,所有代码均可直接运行,零基础用户可按步骤复现并落地。
1.1 技术栈说明
- 核心框架:PyTorch 2.0+(模型构建与训练)
- 数据处理:Pandas、NumPy、Scikit-learn
- 可视化:Matplotlib、Seaborn
- 模型优化:TorchMetrics、Learning Rate Scheduler
- 容器化:Docker
- 部署服务:FastAPI + Uvicorn
- 环境依赖:Python 3.8+
1.2 整体流程可视化
以下是本次实战的完整流程,涵盖从数据准备到云端部署的所有关键环节:
多维数据准备
数据清洗与预处理
多维特征工程
时序数据集构建
TCN-GRU模型设计
模型训练与验证
模型评估与优化
模型导出与封装
Docker镜像构建
云端部署与接口测试
二、环境搭建
2.1 安装依赖库
首先创建并激活虚拟环境(可选但推荐),然后安装所需依赖:
bash
# 创建虚拟环境
conda create -n tcn_gru_pred python=3.8
conda activate tcn_gru_pred
# 安装PyTorch(根据CUDA版本选择,此处为CUDA 11.8)
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 tqdm==4.65.0 scipy==1.10.1
# 模型评估与优化
pip install torchmetrics==1.2.0 optuna==3.3.0
# 服务部署依赖
pip install fastapi==0.103.1 uvicorn==0.23.2 pydantic==2.4.2 python-multipart==0.0.6
# 其他工具
pip install joblib==1.3.2 python-dotenv==1.0.0
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
import torchmetrics
import optuna
# 打印版本信息
print(f"PyTorch版本: {torch.__version__}")
print(f"CUDA是否可用: {torch.cuda.is_available()}")
print(f"CUDA版本: {torch.version.cuda if torch.cuda.is_available() else 'N/A'}")
print(f"Pandas版本: {pd.__version__}")
print(f"NumPy版本: {np.__version__}")
print(f"Scikit-learn版本: {sklearn_version}")
print(f"FastAPI版本: {fastapi.__version__}")
print(f"TorchMetrics版本: {torchmetrics.__version__}")
print(f"Optuna版本: {optuna.__version__}")
# 输出示例
# PyTorch版本: 2.0.1
# CUDA是否可用: True
# CUDA版本: 11.8
# Pandas版本: 2.0.3
# NumPy版本: 1.24.3
# Scikit-learn版本: 1.3.0
# FastAPI版本: 0.103.1
# TorchMetrics版本: 1.2.0
# Optuna版本: 3.3.0
运行该文件:
bash
python env_check.py
若所有库版本正常输出且CUDA状态符合预期,说明环境搭建完成。
三、多维数据准备与探索
3.1 数据集说明
本文使用多维电力负荷数据集(包含电力负荷、温度、湿度、风速、节假日等特征),数据格式为CSV,字段说明如下:
| 字段名 | 说明 | 类型 |
|---|---|---|
| timestamp | 时间戳(小时级) | datetime |
| load | 电力负荷值(kW) | float |
| temperature | 温度(℃) | float |
| humidity | 湿度(%) | float |
| wind_speed | 风速(m/s) | float |
| is_holiday | 是否节假日(0/1) | int |
| is_weekend | 是否周末(0/1) | int |
| hour | 小时(0-23) | int |
| month | 月份(1-12) | int |
3.2 生成模拟多维数据集
为方便零基础用户测试,创建generate_data.py生成模拟多维电力负荷数据:
python
# generate_data.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
def generate_electric_load_data():
"""生成模拟多维电力负荷数据"""
# 设置随机种子确保可复现
np.random.seed(42)
# 生成时间序列:2022-01-01 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')
n_samples = len(time_range)
# 生成基础电力负荷(包含趋势、日周期、周周期)
t = np.arange(n_samples)
trend = 0.0001 * t # 长期增长趋势
daily_season = 2 * np.sin(2 * np.pi * t / 24) # 日周期波动
weekly_season = 1 * np.sin(2 * np.pi * t / (24*7)) # 周周期波动
base_load = 100 + trend + daily_season + weekly_season
# 生成温度数据(与负荷强相关)
temperature = 15 + 10 * np.sin(2 * np.pi * t / (24*365)) + np.random.normal(0, 3, n_samples)
# 温度对负荷的影响(夏季/冬季用电高峰)
temp_effect = np.where(temperature > 25, 0.5*(temperature-25),
np.where(temperature < 5, 0.3*(5-temperature), 0))
# 生成湿度和风速数据
humidity = 60 + 20 * np.sin(2 * np.pi * t / (24*14)) + np.random.normal(0, 5, n_samples)
wind_speed = 2 + 3 * np.random.normal(0, 1, n_samples)
# 生成节假日和周末特征
df = pd.DataFrame({'timestamp': time_range})
df['is_weekend'] = df['timestamp'].dt.weekday.apply(lambda x: 1 if x >= 5 else 0)
df['is_holiday'] = 0
# 手动添加节假日(元旦、春节、五一、国庆)
holidays = [
'2022-01-01', '2022-02-01', '2022-02-02', '2022-02-03', '2022-05-01',
'2022-10-01', '2022-10-02', '2022-10-03', '2023-01-01', '2023-01-22',
'2023-01-23', '2023-01-24', '2023-05-01', '2023-10-01', '2023-10-02', '2023-10-03'
]
for holiday in holidays:
df.loc[df['timestamp'].dt.date == datetime.strptime(holiday, '%Y-%m-%d').date(), 'is_holiday'] = 1
# 最终负荷(加入温度、节假日、随机噪声影响)
holiday_effect = df['is_holiday'].values * 5
weekend_effect = df['is_weekend'].values * 3
noise = np.random.normal(0, 1, n_samples)
final_load = base_load + temp_effect + holiday_effect + weekend_effect + noise
# 组装最终数据集
df['load'] = final_load
df['temperature'] = temperature
df['humidity'] = humidity
df['wind_speed'] = wind_speed
df['hour'] = df['timestamp'].dt.hour
df['month'] = df['timestamp'].dt.month
# 重新排列列顺序
df = df[['timestamp', 'load', 'temperature', 'humidity', 'wind_speed',
'is_holiday', 'is_weekend', 'hour', 'month']]
# 保存为CSV
df.to_csv('data/electric_load_multidim.csv', index=False)
print(f"生成数据量: {len(df)} 条")
print(f"数据时间范围: {df['timestamp'].min()} 到 {df['timestamp'].max()}")
print(f"\n数据预览:\n{df.head(10)}")
return df
if __name__ == "__main__":
# 创建data目录
import os
os.makedirs('data', exist_ok=True)
# 生成数据
generate_electric_load_data()
运行该脚本:
bash
python generate_data.py
生成的数据集会保存到data/electric_load_multidim.csv,包含约17520条小时级多维数据。
3.3 多维数据探索分析
创建data_analysis.py文件,进行全面的多维数据探索:
python
# data_analysis.py
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
def load_and_analyze_data():
"""加载并分析多维电力负荷数据"""
# 1. 加载数据
df = pd.read_csv('data/electric_load_multidim.csv')
df['timestamp'] = pd.to_datetime(df['timestamp'])
# 2. 基本信息分析
print("=== 1. 数据基本信息 ===")
print(df.info())
print("\n=== 2. 数据统计描述 ===")
print(df.describe().round(2))
# 3. 缺失值检查
print("\n=== 3. 缺失值统计 ===")
missing_values = df.isnull().sum()
missing_percent = (missing_values / len(df)) * 100
missing_df = pd.DataFrame({
'缺失值数量': missing_values,
'缺失比例(%)': missing_percent.round(2)
})
print(missing_df)
# 4. 负荷数据分布分析
fig, axes = plt.subplots(2, 3, figsize=(18, 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('负荷值 (kW)')
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('负荷值 (kW)')
axes[0,1].set_ylabel('频次')
axes[0,1].grid(True, alpha=0.3)
# 4.3 负荷与温度相关性
axes[0,2].scatter(df['temperature'], df['load'], alpha=0.5, color='#2ecc71', s=5)
axes[0,2].set_title('负荷 vs 温度')
axes[0,2].set_xlabel('温度 (℃)')
axes[0,2].set_ylabel('负荷值 (kW)')
axes[0,2].grid(True, alpha=0.3)
# 4.4 日内负荷分布
sns.boxplot(x='hour', y='load', data=df, ax=axes[1,0], palette='viridis')
axes[1,0].set_title('日内负荷分布')
axes[1,0].set_xlabel('小时')
axes[1,0].set_ylabel('负荷值 (kW)')
axes[1,0].grid(True, alpha=0.3)
# 4.5 周末/工作日负荷对比
sns.boxplot(x='is_weekend', y='load', data=df, ax=axes[1,1], palette='coolwarm')
axes[1,1].set_title('周末/工作日负荷对比')
axes[1,1].set_xlabel('是否周末 (0=否, 1=是)')
axes[1,1].set_ylabel('负荷值 (kW)')
axes[1,1].grid(True, alpha=0.3)
# 4.6 节假日/非节假日负荷对比
sns.boxplot(x='is_holiday', y='load', data=df, ax=axes[1,2], palette='Set2')
axes[1,2].set_title('节假日/非节假日负荷对比')
axes[1,2].set_xlabel('是否节假日 (0=否, 1=是)')
axes[1,2].set_ylabel('负荷值 (kW)')
axes[1,2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('data/data_exploration_1.png', dpi=300, bbox_inches='tight')
plt.show()
# 5. 特征相关性分析
print("\n=== 4. 特征相关性分析 ===")
# 选择数值特征进行相关性分析
numeric_features = ['load', 'temperature', 'humidity', 'wind_speed',
'is_holiday', 'is_weekend', 'hour', 'month']
corr_matrix = df[numeric_features].corr()
print(corr_matrix.round(2))
# 绘制相关性热力图
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1,
fmt='.2f', linewidths=0.5, cbar_kws={'label': '相关系数'})
plt.title('特征相关性热力图')
plt.tight_layout()
plt.savefig('data/correlation_heatmap.png', dpi=300, bbox_inches='tight')
plt.show()
# 6. 滞后特征分析(负荷的时间依赖性)
print("\n=== 5. 负荷滞后特征分析 ===")
# 添加滞后特征
df['load_lag_1'] = df['load'].shift(1) # 滞后1小时
df['load_lag_6'] = df['load'].shift(6) # 滞后6小时
df['load_lag_12'] = df['load'].shift(12) # 滞后12小时
df['load_lag_24'] = df['load'].shift(24) # 滞后24小时
df['load_lag_48'] = df['load'].shift(48) # 滞后48小时
# 计算滞后特征与当前负荷的相关性
lag_corr = df[['load', 'load_lag_1', 'load_lag_6', 'load_lag_12', 'load_lag_24', 'load_lag_48']].corr()['load']
print("负荷滞后特征相关性:")
print(lag_corr.round(3))
# 绘制滞后相关性图
plt.figure(figsize=(10, 6))
lag_hours = [1, 6, 12, 24, 48]
lag_corr_values = [lag_corr[f'load_lag_{h}'] for h in lag_hours]
plt.bar(lag_hours, lag_corr_values, color='#9b59b6', alpha=0.8)
plt.title('负荷滞后特征相关性')
plt.xlabel('滞后小时数')
plt.ylabel('与当前负荷的相关系数')
plt.grid(True, alpha=0.3)
plt.xticks(lag_hours)
plt.savefig('data/lag_correlation.png', dpi=300, bbox_inches='tight')
plt.show()
return df
if __name__ == "__main__":
load_and_analyze_data()
运行该脚本,可得到:
- 数据基本统计信息和缺失值检查结果
- 负荷时序趋势、分布、与环境特征的关系可视化
- 特征相关性热力图
- 负荷滞后特征相关性分析
这些分析结果将为后续的特征工程提供关键依据。
四、多维数据预处理与特征工程
4.1 数据预处理
创建data_preprocessing.py文件,实现数据清洗、异常值处理、特征标准化等预处理步骤:
python
# data_preprocessing.py
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.impute import SimpleImputer
import joblib
def preprocess_multidim_data():
"""多维电力负荷数据预处理"""
# 1. 加载数据
df = pd.read_csv('data/electric_load_multidim.csv')
df['timestamp'] = pd.to_datetime(df['timestamp'])
print(f"原始数据形状: {df.shape}")
# 2. 异常值处理(3σ原则)
print("\n=== 异常值处理 ===")
def detect_outliers(df, column):
"""使用3σ原则检测异常值"""
mean = df[column].mean()
std = df[column].std()
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std
outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
return outliers, lower_bound, upper_bound
# 处理负荷异常值
load_outliers, load_lower, load_upper = detect_outliers(df, 'load')
print(f"负荷异常值数量: {len(load_outliers)}")
# 替换异常值为边界值
df['load'] = np.where(df['load'] < load_lower, load_lower, df['load'])
df['load'] = np.where(df['load'] > load_upper, load_upper, df['load'])
# 处理温度异常值
temp_outliers, temp_lower, temp_upper = detect_outliers(df, 'temperature')
print(f"温度异常值数量: {len(temp_outliers)}")
df['temperature'] = np.where(df['temperature'] < temp_lower, temp_lower, df['temperature'])
df['temperature'] = np.where(df['temperature'] > temp_upper, temp_upper, df['temperature'])
# 3. 缺失值处理(虽然模拟数据无缺失,但保留通用处理逻辑)
print("\n=== 缺失值处理 ===")
# 初始化填充器
num_imputer = SimpleImputer(strategy='median') # 数值特征用中位数填充
# 选择需要填充的数值列
num_columns = ['load', 'temperature', 'humidity', 'wind_speed']
# 填充缺失值
df[num_columns] = num_imputer.fit_transform(df[num_columns])
# 保存填充器(后续部署用)
joblib.dump(num_imputer, 'data/num_imputer.pkl')
print("缺失值填充完成")
# 4. 特征工程:构建时序特征
print("\n=== 特征工程 ===")
df_features = df.copy()
# 4.1 时间特征细化
df_features['day'] = df_features['timestamp'].dt.day
df_features['weekday'] = df_features['timestamp'].dt.weekday
# 时段特征
df_features['time_period'] = pd.cut(
df_features['hour'],
bins=[0, 6, 12, 18, 24],
labels=['凌晨', '上午', '下午', '晚上'],
right=False
)
# 时段特征数值化
time_period_map = {'凌晨': 0, '上午': 1, '下午': 2, '晚上': 3}
df_features['time_period_num'] = df_features['time_period'].map(time_period_map)
# 4.2 滞后特征(基于相关性分析选择)
lag_steps = [1, 6, 12, 24, 48]
for lag in lag_steps:
df_features[f'load_lag_{lag}'] = df_features['load'].shift(lag)
df_features[f'temp_lag_{lag}'] = df_features['temperature'].shift(lag)
# 4.3 滚动统计特征
window_sizes = [6, 12, 24]
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'temp_roll_mean_{window}'] = df_features['temperature'].rolling(window=window).mean()
# 4.4 差分特征(消除趋势)
df_features['load_diff_1'] = df_features['load'].diff(1)
df_features['load_diff_24'] = df_features['load'].diff(24)
# 5. 处理特征工程产生的缺失值
df_features = df_features.dropna()
print(f"特征工程后数据形状: {df_features.shape}")
# 6. 特征选择
print("\n=== 特征选择 ===")
# 定义目标列和特征列
target_col = 'load'
# 排除非特征列和目标列
exclude_cols = ['timestamp', 'time_period', target_col]
feature_cols = [col for col in df_features.columns if col not in exclude_cols]
# 7. 数据标准化
print("\n=== 数据标准化 ===")
# 初始化标准化器
scaler_X = StandardScaler() # 特征标准化
scaler_y = StandardScaler() # 目标值标准化
# 标准化特征
X = scaler_X.fit_transform(df_features[feature_cols])
# 标准化目标值
y = scaler_y.fit_transform(df_features[[target_col]]).ravel()
# 8. 按时间顺序划分训练集和测试集(时序数据不能随机划分)
print("\n=== 数据集划分 ===")
test_size = 0.2
split_idx = int(len(df_features) * (1 - test_size))
# 划分数据
X_train = X[:split_idx]
X_test = X[split_idx:]
y_train = y[:split_idx]
y_test = y[split_idx:]
# 保存时间戳用于后续分析
timestamps_train = df_features['timestamp'][:split_idx].reset_index(drop=True)
timestamps_test = df_features['timestamp'][split_idx:].reset_index(drop=True)
print(f"训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")
print(f"特征列数量: {len(feature_cols)}")
# 9. 保存预处理结果
print("\n=== 保存预处理结果 ===")
# 保存数组
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)
# 保存标准化器
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')
# 保存时间戳
timestamps_train.to_csv('data/timestamps_train.csv', index=False)
timestamps_test.to_csv('data/timestamps_test.csv', index=False)
# 保存处理后的完整数据
df_features.to_csv('data/df_features.csv', index=False)
print("数据预处理完成,所有文件已保存到data目录!")
return (X_train, X_test, y_train, y_test,
scaler_X, scaler_y, feature_cols, df_features)
if __name__ == "__main__":
preprocess_multidim_data()
运行该脚本,完成数据预处理和特征工程,并将处理后的数据集、标准化器、特征列名等保存到data目录。
4.2 时序数据集构建
创建dataset.py文件,实现PyTorch Dataset和DataLoader,将扁平特征转换为时序序列:
python
# dataset.py
import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np
class ElectricLoadDataset(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):
"""返回有效样本数"""
return len(self.X) - self.seq_len
def __getitem__(self, idx):
"""获取单个样本"""
# 序列特征:从idx到idx+seq_len
x_seq = self.X[idx:idx+self.seq_len]
# 目标值:seq_len时间步对应的负荷值
y_target = self.y[idx+self.seq_len]
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 = ElectricLoadDataset(X_train, y_train, seq_len)
test_dataset = ElectricLoadDataset(X_test, y_test, seq_len)
# 创建DataLoader(时序数据shuffle=False)
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_dataset)}")
print(f"测试集样本数: {len(test_dataset)}")
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
)
# 测试数据加载
print("\n=== 测试数据加载 ===")
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是否正常工作,输出应类似:
训练集样本数: 13999
测试集样本数: 3499
训练集批次数量: 437
测试集批次数量: 110
单个序列特征形状: torch.Size([24, 31])
单个目标形状: torch.Size([])
=== 测试数据加载 ===
批次 1:
特征形状: torch.Size([32, 24, 31]) (batch_size, seq_len, n_features)
目标形状: torch.Size([32]) (batch_size,)
批次 2:
特征形状: torch.Size([32, 24, 31]) (batch_size, seq_len, n_features)
目标形状: torch.Size([32]) (batch_size,)
五、TCN-GRU混合模型构建
5.1 模型架构设计
TCN-GRU混合模型结合了TCN的局部特征提取和并行计算优势,以及GRU的时序记忆特性,架构设计如下:
渲染错误: Mermaid 渲染失败: Parse error on line 2: ...rt LR A[输入序列
(batch, seq_len, n_f ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
5.2 TCN-GRU模型代码实现
创建model.py文件,实现完整的TCN-GRU混合模型:
python
# model.py
import torch
import torch.nn as nn
import torch.nn.functional as F
class CausalConv1d(nn.Module):
"""
因果卷积层(时序卷积专用,确保不使用未来信息)
"""
def __init__(self, in_channels, out_channels, kernel_size, dilation=1, stride=1):
super().__init__()
# 因果卷积需要的padding
padding = (kernel_size - 1) * dilation
self.conv = nn.Conv1d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
padding=padding,
dilation=dilation,
stride=stride
)
self.kernel_size = kernel_size
self.dilation = dilation
def forward(self, x):
"""
参数:
x: 输入张量 (batch_size, in_channels, seq_len)
返回:
因果卷积结果 (batch_size, out_channels, seq_len)
"""
out = self.conv(x)
# 裁剪padding,保证因果性
if self.kernel_size > 1:
out = out[:, :, :-self.padding] if self.padding > 0 else out
return out
class TCNBlock(nn.Module):
"""
TCN模块块:因果卷积 + 门控激活 + 残差连接
"""
def __init__(self, in_channels, out_channels, kernel_size=3, dilation=1):
super().__init__()
# 因果卷积层
self.conv1 = CausalConv1d(in_channels, out_channels, kernel_size, dilation)
self.conv2 = CausalConv1d(out_channels, out_channels, kernel_size, dilation)
# 门控激活单元
self.gate = nn.Sigmoid()
# 残差连接(维度匹配)
self.residual = nn.Conv1d(in_channels, out_channels, 1) if in_channels != out_channels else nn.Identity()
# 层归一化
self.norm1 = nn.LayerNorm(out_channels)
self.norm2 = nn.LayerNorm(out_channels)
# Dropout
self.dropout = nn.Dropout(0.1)
def forward(self, x):
"""
参数:
x: 输入张量 (batch_size, in_channels, seq_len)
返回:
输出张量 (batch_size, out_channels, seq_len)
"""
# 残差连接
residual = self.residual(x)
# 第一层卷积 + 激活
out = self.conv1(x)
out = self.norm1(out.transpose(1, 2)).transpose(1, 2) # LayerNorm需要 (batch, seq, channels)
out = F.relu(out)
out = self.dropout(out)
# 第二层卷积 + 门控激活
out = self.conv2(out)
out = self.norm2(out.transpose(1, 2)).transpose(1, 2)
out = out * self.gate(out) # 门控激活
out = self.dropout(out)
# 残差连接 + 激活
out = F.relu(out + residual)
return out
class TCNGRUModel(nn.Module):
"""
TCN-GRU混合模型:用于电力负荷预测
"""
def __init__(
self,
n_features, # 输入特征数
seq_len=24, # 序列长度
tcn_channels=[64, 128], # TCN通道数
tcn_kernel_size=3, # TCN卷积核大小
gru_hidden_dim=128, # GRU隐藏层维度
gru_layers=2, # GRU层数
dropout=0.1 # Dropout概率
):
super().__init__()
# 1. TCN特征提取模块
self.tcn_blocks = nn.Sequential()
in_channels = n_features
for i, out_channels in enumerate(tcn_channels):
dilation = 2 ** i # 指数增长的膨胀系数
self.tcn_blocks.add_module(
f'tcn_block_{i}',
TCNBlock(in_channels, out_channels, tcn_kernel_size, dilation)
)
in_channels = out_channels
# TCN输出维度
self.tcn_out_channels = tcn_channels[-1]
# 2. GRU时序建模模块
self.gru = nn.GRU(
input_size=self.tcn_out_channels,
hidden_size=gru_hidden_dim,
num_layers=gru_layers,
batch_first=True,
dropout=dropout if gru_layers > 1 else 0,
bidirectional=False # 单向GRU,保证时序性
)
# 3. 输出层
self.fc1 = nn.Linear(gru_hidden_dim, 64)
self.fc2 = nn.Linear(64, 1)
# 4. 其他层
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.GRU):
for name, param in m.named_parameters():
if 'weight' in name:
nn.init.orthogonal_(param)
if 'bias' in name:
nn.init.constant_(param, 0)
def forward(self, x):
"""
前向传播
参数:
x: 输入张量 (batch_size, seq_len, n_features)
返回:
预测值 (batch_size, 1)
"""
# 1. 调整维度:(batch, seq_len, features) -> (batch, features, seq_len)
x = x.permute(0, 2, 1)
# 2. TCN特征提取
tcn_out = self.tcn_blocks(x) # (batch, tcn_channels, seq_len)
# 3. 调整维度用于GRU:(batch, seq_len, tcn_channels)
tcn_out = tcn_out.permute(0, 2, 1)
# 4. GRU时序建模
gru_out, _ = self.gru(tcn_out) # (batch, seq_len, gru_hidden_dim)
# 5. 取最后一个时间步的输出
gru_last = gru_out[:, -1, :] # (batch, gru_hidden_dim)
# 6. 输出层
out = self.dropout(gru_last)
out = self.fc1(out)
out = self.relu(out)
out = self.dropout(out)
out = self.fc2(out) # (batch, 1)
return out
if __name__ == "__main__":
# 测试模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用设备: {device}")
# 模型参数
n_features = 31 # 特征数(与特征工程输出一致)
seq_len = 24 # 序列长度
batch_size = 32 # 批次大小
# 创建模型实例
model = TCNGRUModel(
n_features=n_features,
seq_len=seq_len,
tcn_channels=[64, 128],
tcn_kernel_size=3,
gru_hidden_dim=128,
gru_layers=2,
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
# train.py
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
import torchmetrics
import numpy as np
import matplotlib.pyplot as plt
import time
import os
from tqdm import tqdm
import json
# 导入自定义模块
from dataset import ElectricLoadDataset, create_data_loaders
from model import TCNGRUModel
# 设置中文字体
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}")
# 训练配置(可通过Optuna调优)
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', # 最佳模型保存路径
'metrics_save_path': 'models/training_metrics.json', # 指标保存路径
'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
# 定义评估指标
class MetricsCalculator:
"""评估指标计算器"""
def __init__(self, device):
self.mse = torchmetrics.MeanSquaredError().to(device)
self.rmse = torchmetrics.MeanSquaredError(squared=False).to(device)
self.mae = torchmetrics.MeanAbsoluteError().to(device)
self.r2 = torchmetrics.R2Score().to(device)
def reset(self):
"""重置指标"""
self.mse.reset()
self.rmse.reset()
self.mae.reset()
self.r2.reset()
def update(self, y_pred, y_true):
"""更新指标"""
self.mse.update(y_pred, y_true)
self.rmse.update(y_pred, y_true)
self.mae.update(y_pred, y_true)
self.r2.update(y_pred, y_true)
def compute(self):
"""计算指标"""
return {
'MSE': self.mse.compute().item(),
'RMSE': self.rmse.compute().item(),
'MAE': self.mae.compute().item(),
'R2': self.r2.compute().item()
}
# 训练函数
def train_model(model, train_loader, val_loader, criterion, optimizer, config):
"""
模型训练函数
"""
# 创建模型保存目录
os.makedirs(os.path.dirname(config['model_save_path']), exist_ok=True)
# 初始化评估指标
train_metrics_calc = MetricsCalculator(device)
val_metrics_calc = MetricsCalculator(device)
# 初始化训练记录
training_history = {
'train_loss': [],
'val_loss': [],
'train_metrics': [],
'val_metrics': []
}
best_val_rmse = float('inf')
patience_counter = 0
# 学习率调度器
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min',
factor=0.5,
patience=3,
verbose=True
)
# 开始训练
start_time = time.time()
for epoch in range(config['n_epochs']):
# ========== 训练阶段 ==========
model.train()
train_loss = 0.0
train_metrics_calc.reset()
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_metrics_calc.update(y_pred, y_batch)
# 更新进度条
train_bar.set_postfix({'loss': loss.item()})
# 计算训练集平均损失和指标
avg_train_loss = train_loss / len(train_loader.dataset)
train_metrics = train_metrics_calc.compute()
training_history['train_loss'].append(avg_train_loss)
training_history['train_metrics'].append(train_metrics)
# ========== 验证阶段 ==========
model.eval()
val_loss = 0.0
val_metrics_calc.reset()
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)
val_metrics_calc.update(y_pred, y_batch)
val_bar.set_postfix({'loss': loss.item()})
# 计算验证集平均损失和指标
avg_val_loss = val_loss / len(val_loader.dataset)
val_metrics = val_metrics_calc.compute()
training_history['val_loss'].append(avg_val_loss)
training_history['val_metrics'].append(val_metrics)
# ========== 日志输出 ==========
print(f'\nEpoch {epoch+1}/{config["n_epochs"]}:')
print(f' 训练损失: {avg_train_loss:.6f} | 验证损失: {avg_val_loss:.6f}')
print(f' 训练集指标 - MSE: {train_metrics["MSE"]:.6f}, RMSE: {train_metrics["RMSE"]:.6f}, MAE: {train_metrics["MAE"]:.6f}, R2: {train_metrics["R2"]:.6f}')
print(f' 验证集指标 - MSE: {val_metrics["MSE"]:.6f}, RMSE: {val_metrics["RMSE"]:.6f}, MAE: {val_metrics["MAE"]:.6f}, R2: {val_metrics["R2"]:.6f}')
# ========== 早停和模型保存 ==========
if val_metrics['RMSE'] < best_val_rmse:
best_val_rmse = val_metrics['RMSE']
patience_counter = 0
# 保存最佳模型
torch.save({
'epoch': epoch + 1,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'best_val_rmse': best_val_rmse,
'config': config,
'training_history': training_history
}, config['model_save_path'])
print(f' ✨ 保存最佳模型,验证集RMSE: {best_val_rmse:.6f}')
else:
patience_counter += 1
print(f' ⏳ 早停计数器: {patience_counter}/{config["patience"]}')
if patience_counter >= config['patience']:
print(f' 🛑 早停触发,训练结束!')
break
# ========== 学习率调度 ==========
scheduler.step(val_metrics['RMSE'])
# ========== 训练结束 ==========
total_time = time.time() - start_time
print(f'\n🏁 训练完成!总耗时: {total_time:.2f} 秒 ({total_time/60:.2f} 分钟)')
print(f'🏆 最佳验证集RMSE: {best_val_rmse:.6f}')
# 保存训练指标
with open(config['metrics_save_path'], 'w') as f:
json.dump(training_history, f, indent=4)
return model, training_history
# 绘制训练曲线
def plot_training_curves(training_history):
"""绘制训练曲线"""
# 提取指标
train_loss = training_history['train_loss']
val_loss = training_history['val_loss']
train_rmse = [m['RMSE'] for m in training_history['train_metrics']]
val_rmse = [m['RMSE'] for m in training_history['val_metrics']]
train_r2 = [m['R2'] for m in training_history['train_metrics']]
val_r2 = [m['R2'] for m in training_history['val_metrics']]
# 创建图表
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
fig.suptitle('TCN-GRU模型训练过程', fontsize=16)
# 损失曲线
axes[0,0].plot(train_loss, label='训练损失', color='#3498db', linewidth=2)
axes[0,0].plot(val_loss, label='验证损失', color='#e74c3c', linewidth=2)
axes[0,0].set_title('训练/验证损失 (MSE)')
axes[0,0].set_xlabel('Epoch')
axes[0,0].set_ylabel('损失值')
axes[0,0].legend()
axes[0,0].grid(True, alpha=0.3)
# RMSE曲线
axes[0,1].plot(train_rmse, label='训练RMSE', color='#2ecc71', linewidth=2)
axes[0,1].plot(val_rmse, label='验证RMSE', color='#f39c12', linewidth=2)
axes[0,1].set_title('训练/验证RMSE')
axes[0,1].set_xlabel('Epoch')
axes[0,1].set_ylabel('RMSE')
axes[0,1].legend()
axes[0,1].grid(True, alpha=0.3)
# R2曲线
axes[1,0].plot(train_r2, label='训练R²', color='#9b59b6', linewidth=2)
axes[1,0].plot(val_r2, label='验证R²', color='#1abc9c', linewidth=2)
axes[1,0].set_title('训练/验证R²')
axes[1,0].set_xlabel('Epoch')
axes[1,0].set_ylabel('R²')
axes[1,0].legend()
axes[1,0].grid(True, alpha=0.3)
# 综合指标(最后10轮)
epochs = list(range(len(train_loss)))[-10:]
axes[1,1].plot(epochs, train_loss[-10:], label='训练损失', marker='o', color='#3498db')
axes[1,1].plot(epochs, val_loss[-10:], label='验证损失', marker='s', color='#e74c3c')
axes[1,1].plot(epochs, train_rmse[-10:], label='训练RMSE', marker='^', color='#2ecc71')
axes[1,1].plot(epochs, val_rmse[-10:], label='验证RMSE', marker='*', color='#f39c12')
axes[1,1].set_title('最后10轮关键指标')
axes[1,1].set_xlabel('Epoch')
axes[1,1].set_ylabel('指标值')
axes[1,1].legend()
axes[1,1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('models/training_curves.png', dpi=300, bbox_inches='tight')
plt.show()
# 模型评估与可视化
def evaluate_model(model, test_loader, scaler_y):
"""评估模型并可视化预测结果"""
model.eval()
y_true_list = []
y_pred_list = []
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)
# 收集结果
y_true_list.extend(y_batch.cpu().numpy())
y_pred_list.extend(y_pred.cpu().numpy())
# 转换为数组
y_true = np.array(y_true_list).reshape(-1, 1)
y_pred = np.array(y_pred_list).reshape(-1, 1)
# 反标准化(恢复原始负荷值)
y_true_original = scaler_y.inverse_transform(y_true)
y_pred_original = scaler_y.inverse_transform(y_pred)
# 计算最终指标
metrics_calc = MetricsCalculator('cpu')
metrics_calc.update(torch.tensor(y_pred_original), torch.tensor(y_true_original))
final_metrics = metrics_calc.compute()
print("\n=== 最终测试集评估指标(原始尺度)===")
for metric, value in final_metrics.items():
print(f" {metric}: {value:.6f}")
# 可视化预测结果
fig, axes = plt.subplots(2, 1, figsize=(16, 12))
# 整体预测结果(前500个样本)
axes[0].plot(y_true_original[:500], label='真实值', color='#3498db', linewidth=1.5)
axes[0].plot(y_pred_original[:500], label='预测值', color='#e74c3c', linewidth=1.5, alpha=0.8)
axes[0].set_title('电力负荷预测结果对比(前500个样本)')
axes[0].set_xlabel('样本索引')
axes[0].set_ylabel('负荷值 (kW)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 散点图:真实值 vs 预测值
axes[1].scatter(y_true_original, y_pred_original, alpha=0.5, color='#9b59b6', s=5)
# 添加对角线
min_val = min(y_true_original.min(), y_pred_original.min())
max_val = max(y_true_original.max(), y_pred_original.max())
axes[1].plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2)
axes[1].set_title('真实值 vs 预测值散点图')
axes[1].set_xlabel('真实负荷值 (kW)')
axes[1].set_ylabel('预测负荷值 (kW)')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('models/prediction_results.png', dpi=300, bbox_inches='tight')
plt.show()
return final_metrics
# 主函数
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')
scaler_y = joblib.load('data/scaler_y.pkl')
# 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=== 初始化TCN-GRU模型 ===")
n_features = X_train.shape[1] # 特征数
model = TCNGRUModel(
n_features=n_features,
seq_len=CONFIG['seq_len'],
tcn_channels=[64, 128],
tcn_kernel_size=3,
gru_hidden_dim=128,
gru_layers=2,
dropout=0.1
).to(device)
# 4. 定义损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失
optimizer = optim.AdamW(
model.parameters(),
lr=CONFIG['learning_rate'],
weight_decay=CONFIG['weight_decay']
)
# 5. 训练模型
print("\n=== 开始训练模型 ===")
model, training_history = train_model(
model, train_loader, test_loader, criterion, optimizer, CONFIG
)
# 6. 绘制训练曲线
print("\n=== 绘制训练曲线 ===")
plot_training_curves(training_history)
# 7. 加载最佳模型
print("\n=== 加载最佳模型 ===")
checkpoint = torch.load(CONFIG['model_save_path'], map_location=device)
model.load_state_dict(checkpoint['model_state_dict'])
print(f"加载Epoch {checkpoint['epoch']}的最佳模型,验证集RMSE: {checkpoint['best_val_rmse']:.6f}")
# 8. 评估模型
print("\n=== 评估模型性能 ===")
final_metrics = evaluate_model(model, test_loader, scaler_y)
6.2 运行训练脚本
执行训练脚本开始模型训练:
bash
python train.py
训练过程中会输出每轮的训练/验证损失、多维度评估指标,并在训练结束后:
- 绘制训练曲线(损失、RMSE、R²)
- 保存最佳模型到
models/best_model.pth - 评估模型在测试集上的性能
- 可视化预测结果(真实值vs预测值)
七、模型服务化封装
7.1 FastAPI服务封装
创建deploy/app.py文件,实现模型的API服务封装:
python
# deploy/app.py
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
import sys
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from model import TCNGRUModel
# 初始化FastAPI应用
app = FastAPI(
title="TCN-GRU电力负荷预测服务",
description="基于PyTorch的TCN-GRU混合模型电力负荷预测API",
version="1.0.0"
)
# 配置参数
CONFIG = {
'device': torch.device('cuda' if torch.cuda.is_available() else 'cpu'),
'seq_len': 24,
'model_path': '../models/best_model.pth',
'scaler_x_path': '../data/scaler_X.pkl',
'scaler_y_path': '../data/scaler_y.pkl',
'num_imputer_path': '../data/num_imputer.pkl',
'feature_cols_path': '../data/feature_cols.txt'
}
# 全局变量(加载模型和预处理组件)
MODEL = None
SCALER_X = None
SCALER_Y = None
NUM_IMPUTER = None
FEATURE_COLS = None
# 数据模型定义
class LoadPredictionRequest(BaseModel):
"""电力负荷预测请求模型"""
timestamp: str = Field(..., example="2024-01-01 00:00:00", description="预测时间戳(格式:YYYY-MM-DD HH:MM:SS)")
load_history: list[float] = Field(..., min_items=24, max_items=24, description="前24小时负荷值 (kW)")
temperature_history: list[float] = Field(..., min_items=24, max_items=24, description="前24小时温度值 (℃)")
humidity_history: list[float] = Field(..., min_items=24, max_items=24, description="前24小时湿度值 (%)")
wind_speed_history: list[float] = Field(..., min_items=24, max_items=24, description="前24小时风速值 (m/s)")
is_holiday: int = Field(..., ge=0, le=1, example=0, description="是否节假日 (0/1)")
is_weekend: int = Field(..., ge=0, le=1, example=0, description="是否周末 (0/1)")
class Config:
schema_extra = {
"example": {
"timestamp": "2024-01-01 00:00:00",
"load_history": [100.2, 98.8, 97.5, 96.3, 95.1, 94.0, 95.2, 97.5, 100.0, 102.5, 105.0, 106.2,
107.5, 106.3, 105.0, 103.8, 101.5, 99.3, 97.1, 95.9, 94.7, 93.6, 94.8, 96.0],
"temperature_history": [5.2, 4.8, 4.5, 4.3, 4.1, 4.0, 4.2, 4.5, 5.0, 5.5, 6.0, 6.2,
6.5, 6.3, 6.0, 5.8, 5.5, 5.3, 5.1, 4.9, 4.7, 4.6, 4.8, 5.0],
"humidity_history": [60.2, 61.8, 62.5, 63.3, 64.1, 65.0, 64.2, 63.5, 62.0, 61.5, 61.0, 60.2,
59.5, 58.3, 57.0, 56.8, 55.5, 54.3, 53.1, 52.9, 51.7, 50.6, 51.8, 53.0],
"wind_speed_history": [2.2, 2.8, 3.5, 3.3, 3.1, 3.0, 2.2, 2.5, 2.0, 1.5, 1.0, 0.2,
0.5, 1.3, 2.0, 2.8, 3.5, 3.3, 3.1, 2.9, 2.7, 2.6, 2.8, 3.0],
"is_holiday": 1,
"is_weekend": 1
}
}
class PredictionResponse(BaseModel):
"""电力负荷预测响应模型"""
code: int = 200
msg: str = "success"
data: dict = Field(..., description="预测结果数据")
prediction_time: str = Field(..., description="预测执行时间")
# 加载模型和预处理组件
def load_resources():
"""加载模型、标准化器、填充器、特征列等资源,服务启动时执行"""
global MODEL, SCALER_X, SCALER_Y, NUM_IMPUTER, FEATURE_COLS
print(f"开始加载资源,使用设备: {CONFIG['device']}")
# 1. 加载特征列名
if not os.path.exists(CONFIG['feature_cols_path']):
raise FileNotFoundError(f"特征列文件不存在: {CONFIG['feature_cols_path']}")
with open(CONFIG['feature_cols_path'], 'r', encoding='utf-8') as f:
FEATURE_COLS = [line.strip() for line in f.readlines() if line.strip()]
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']}")
if not os.path.exists(CONFIG['num_imputer_path']):
raise FileNotFoundError(f"数值填充器不存在: {CONFIG['num_imputer_path']}")
SCALER_X = joblib.load(CONFIG['scaler_x_path'])
SCALER_Y = joblib.load(CONFIG['scaler_y_path'])
NUM_IMPUTER = joblib.load(CONFIG['num_imputer_path'])
print("成功加载预处理组件(标准化器+填充器)")
# 3. 加载模型
if not os.path.exists(CONFIG['model_path']):
raise FileNotFoundError(f"模型文件不存在: {CONFIG['model_path']}")
# 初始化模型实例
checkpoint = torch.load(CONFIG['model_path'], map_location=CONFIG['device'])
model_config = checkpoint['config']
n_features = len(FEATURE_COLS)
MODEL = TCNGRUModel(
n_features=n_features,
seq_len=model_config['seq_len'],
tcn_channels=[64, 128],
tcn_kernel_size=3,
gru_hidden_dim=128,
gru_layers=2,
dropout=0.1
).to(CONFIG['device'])
# 加载模型权重
MODEL.load_state_dict(checkpoint['model_state_dict'])
MODEL.eval() # 设置为评估模式,关闭Dropout/BatchNorm
print(f"成功加载最佳模型,训练Epoch: {checkpoint['epoch']},最佳验证RMSE: {checkpoint['best_val_rmse']:.6f}")
print("所有资源加载完成,服务就绪!")
def build_prediction_features(request: LoadPredictionRequest):
"""
根据请求参数构建预测特征,与训练阶段特征工程逻辑保持一致
:param request: 预测请求参数
:return: 标准化后的特征序列 (1, seq_len, n_features)
"""
try:
# 解析时间戳
ts = datetime.strptime(request.timestamp, "%Y-%m-%d %H:%M:%S")
except ValueError:
raise HTTPException(status_code=400, detail="时间戳格式错误,必须为YYYY-MM-DD HH:MM:SS")
# 验证历史数据长度(必须为24,与seq_len一致)
history_datas = [
request.load_history,
request.temperature_history,
request.humidity_history,
request.wind_speed_history
]
for name, data in zip(['负荷', '温度', '湿度', '风速'], history_datas):
if len(data) != CONFIG['seq_len']:
raise HTTPException(status_code=400, detail=f"{name}历史数据长度必须为{CONFIG['seq_len']},当前为{len(data)}")
# 构建基础特征字典
features = {}
# 时间特征
features['hour'] = ts.hour
features['day'] = ts.day
features['month'] = ts.month
features['weekday'] = ts.weekday()
features['is_holiday'] = request.is_holiday
features['is_weekend'] = request.is_weekend
features['time_period_num'] = 0 if ts.hour <6 else 1 if ts.hour <12 else 2 if ts.hour <18 else 3
# 滞后特征(基于24小时历史数据)
lag_steps = [1, 6, 12, 24, 48]
for lag in lag_steps:
# 滞后超过24则用最后一个值填充
load_lag_idx = -lag if lag <=24 else -1
temp_lag_idx = -lag if lag <=24 else -1
features[f'load_lag_{lag}'] = request.load_history[load_lag_idx]
features[f'temp_lag_{lag}'] = request.temperature_history[temp_lag_idx]
# 滚动统计特征(窗口6/12/24)
window_sizes = [6, 12, 24]
for window in window_sizes:
win_load = request.load_history[-window:] if window <=24 else request.load_history
win_temp = request.temperature_history[-window:] if window <=24 else request.temperature_history
# 负荷滚动特征
features[f'load_roll_mean_{window}'] = np.mean(win_load)
features[f'load_roll_std_{window}'] = np.std(win_load)
features[f'load_roll_max_{window}'] = np.max(win_load)
# 温度滚动特征
features[f'temp_roll_mean_{window}'] = np.mean(win_temp)
# 差分特征
features['load_diff_1'] = request.load_history[-1] - request.load_history[-2] if len(request.load_history)>=2 else 0
features['load_diff_24'] = request.load_history[-1] - request.load_history[0] if len(request.load_history)>=24 else 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) # 缺失特征填充0,生产环境可优化
if missing_cols:
print(f"警告:缺失特征{missing_cols},已填充0")
# 特征预处理:填充→标准化
feature_array = np.array(feature_vals).reshape(1, -1)
feature_imputed = NUM_IMPUTER.transform(feature_array)
feature_scaled = SCALER_X.transform(feature_imputed)
# 构建序列(seq_len=24,重复特征以匹配输入格式,生产环境可传入连续序列)
feature_seq = np.repeat(feature_scaled, CONFIG['seq_len'], axis=0).reshape(1, CONFIG['seq_len'], -1)
return feature_seq
# 服务启动事件:加载资源
@app.on_event("startup")
async def startup_event():
try:
load_resources()
except Exception as e:
print(f"资源加载失败,服务启动异常: {str(e)}")
raise e
# 服务关闭事件
@app.on_event("shutdown")
async def shutdown_event():
print("服务正在关闭,释放资源...")
global MODEL, SCALER_X, SCALER_Y, NUM_IMPUTER, FEATURE_COLS
MODEL = None
SCALER_X = None
SCALER_Y = None
NUM_IMPUTER = None
FEATURE_COLS = None
print("资源释放完成")
# 健康检查接口
@app.get("/health", summary="服务健康检查", tags=["基础接口"])
async def health_check():
if MODEL is None:
raise HTTPException(status_code=503, detail="服务未就绪,模型未加载")
return {
"status": "healthy",
"device": str(CONFIG['device']),
"model_loaded": True,
"current_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"api_version": "1.0.0"
}
# 核心预测接口
@app.post("/predict", response_model=PredictionResponse, summary="电力负荷单步预测", tags=["预测接口"])
async def predict_load(request: LoadPredictionRequest):
try:
# 1. 构建并预处理特征
feature_seq = build_prediction_features(request)
# 2. 转换为PyTorch张量并移至设备
seq_tensor = torch.tensor(feature_seq, dtype=torch.float32).to(CONFIG['device'])
# 3. 模型推理(关闭梯度计算)
with torch.no_grad():
pred_scaled = MODEL(seq_tensor)
# 4. 反标准化,恢复原始负荷单位(kW)
pred_scaled_np = pred_scaled.cpu().numpy().reshape(-1, 1)
pred_original = SCALER_Y.inverse_transform(pred_scaled_np)[0][0]
# 5. 构建响应数据
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return PredictionResponse(
prediction_time=current_time,
data={
"predict_timestamp": request.timestamp,
"predicted_load_kW": round(float(pred_original), 2),
"seq_len": CONFIG['seq_len'],
"input_valid": True
}
)
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"预测失败:{str(e)}")
# 主函数:启动服务
if __name__ == "__main__":
import uvicorn
uvicorn.run(
app="app:app",
host="0.0.0.0",
port=8000,
reload=False, # 生产环境关闭热重载
workers=1, # 根据云服务器CPU核心数调整,建议1-4
log_level="info"
)
在deploy目录下创建__init__.py(空文件),完成服务代码的目录结构搭建,最终项目整体目录结构如下:
tcn_gru_load_pred/
├── data/ # 数据目录(含原始/预处理/特征文件)
├── models/ # 模型目录(含训练好的模型/训练曲线/指标)
├── deploy/ # 部署目录
│ ├── __init__.py
│ └── app.py # FastAPI服务代码
├── generate_data.py # 模拟数据生成
├── data_analysis.py # 数据探索
├── data_preprocessing.py# 数据预处理&特征工程
├── dataset.py # PyTorch数据集
├── model.py # TCN-GRU模型定义
├── train.py # 模型训练
└── requirements.txt # 项目依赖清单
同时在项目根目录创建requirements.txt,统一管理所有依赖(避免Docker构建时版本冲突):
txt
# 基础依赖
python==3.8.19
torch==2.0.1+cu118
torchvision==0.15.2+cu118
torchaudio==2.0.2+cu118
# 数据处理
pandas==2.0.3
numpy==1.24.3
scikit-learn==1.3.0
scipy==1.10.1
joblib==1.3.2
# 可视化
matplotlib==3.7.2
seaborn==0.12.2
# 模型评估&优化
torchmetrics==1.2.0
tqdm==4.65.0
# 服务部署
fastapi==0.103.1
uvicorn==0.23.2
pydantic==2.4.2
python-multipart==0.0.6
python-dotenv==1.0.0
# 其他
typing-extensions==4.7.1
八、Docker容器化构建
容器化是云端部署的基础,能保证开发环境与生产环境一致,避免依赖冲突、环境配置等问题。本节将编写Dockerfile、构建Docker镜像,并本地测试容器化服务。
8.1 Docker容器化流程
编写Dockerfile
编写.dockerignore
构建Docker镜像
docker build -t name .
本地启动Docker容器
docker run -p 8080:8080 name
本地接口测试
curl或Postman验证
推送镜像到镜像仓库
docker push
云端拉取镜像部署
k8s/docker-compose
8.2 编写Dockerfile
在项目根目录 创建Dockerfile(无后缀),基于Python3.8官方镜像构建,代码详细且带注释,零基础可直接复用:
dockerfile
# 基础镜像:Python3.8-slim(轻量级,适合生产环境)
FROM python:3.8-slim
# 维护者信息(可选)
MAINTAINER tcn_gru_pred <tcn_gru_pred@example.com>
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONUNBUFFERED=1 # 关闭Python输出缓冲,日志实时打印
ENV PYTHONDONTWRITEBYTECODE=1 # 不生成.pyc文件,减少镜像体积
ENV TZ=Asia/Shanghai # 设置时区为上海,避免时间戳偏差
# 安装系统依赖(解决pip安装某些库的编译问题)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
make \
libgomp1 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* # 清理缓存,减少镜像体积
# 复制依赖清单到工作目录
COPY requirements.txt /app/
# 安装Python依赖(使用国内清华源加速,生产环境可根据云服务器地域调整)
RUN pip install --upgrade pip \
&& pip install -i https://pypi.tuna.tsinghua.edu.cn/simple --no-cache-dir -r requirements.txt
# 复制项目所有文件到工作目录
# 注意:通过.dockerignore排除无用文件,减少镜像体积
COPY . /app/
# 创建日志目录(可选)
RUN mkdir -p /app/logs
# 暴露服务端口(与app.py中的8000一致)
EXPOSE 8000
# 启动命令:运行FastAPI服务,使用uvicorn作为服务器
# 生产环境建议使用workers=CPU核心数,这里默认1,云端可修改
CMD ["uvicorn", "deploy.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
8.3 编写.dockerignore
在项目根目录 创建.dockerignore文件,排除无需加入镜像的文件/目录,大幅减少镜像体积,避免敏感文件泄露:
# 虚拟环境
venv/
env/
*.env
# 数据目录(云端可通过挂载卷加载,本地测试可注释)
# data/
# 模型目录(云端可通过挂载卷加载,本地测试可注释)
# models/
# 日志文件
*.log
logs/
# 缓存文件
__pycache__/
*.pyc
*.pyo
*.pyd
.idea/
.vscode/
# 操作系统文件
.DS_Store
Thumbs.db
# 构建缓存
build/
dist/
*.egg-info/
# Git相关
.git/
.gitignore
# 可视化图表
*.png
*.jpg
*.jpeg
# 其他
README.md
LICENSE
8.4 构建Docker镜像
8.4.1 前置条件
- 本地安装Docker(Windows/macOS安装Docker Desktop,Linux安装Docker Engine)
- 启动Docker服务(Docker Desktop点击启动,Linux执行
systemctl start docker) - 进入项目根目录 (终端/命令行切换到
tcn_gru_load_pred/)
8.4.2 构建命令
执行以下命令构建Docker镜像,tcn_gru_load_pred为镜像名,v1.0为标签,可自定义:
bash
# 构建镜像
docker build -t tcn_gru_load_pred:v1.0 .
-t:指定镜像名和标签.:指定Dockerfile所在目录(当前根目录)
构建过程中会自动执行Dockerfile中的步骤,拉取基础镜像、安装依赖、复制项目文件,耐心等待即可(首次构建因拉取镜像/安装依赖耗时稍长,后续构建会缓存)。
8.4.3 验证镜像
构建完成后,执行以下命令查看本地镜像,确认构建成功:
bash
# 查看本地镜像
docker images
输出中会看到tcn_gru_load_pred镜像,标签为v1.0,表示构建成功。
8.5 本地测试Docker容器
构建成功后,本地启动Docker容器,测试服务是否正常运行:
bash
# 启动容器,端口映射:主机8000 → 容器8000
docker run -d -p 8000:8000 --name tcn_gru_pred_container tcn_gru_load_pred:v1.0
-d:后台运行容器-p 8000:8000:端口映射,主机端口:容器端口--name:指定容器名,可自定义- 最后跟镜像名:标签
8.5.1 验证容器运行状态
bash
# 查看运行中的容器
docker ps
输出中会看到tcn_gru_pred_container容器,状态为Up,表示启动成功。
8.5.2 本地接口测试
容器启动后,本地访问http://localhost:8000/docs打开FastAPI自动生成的Swagger UI文档,可直接在网页上测试健康检查 和预测接口:
- 点击
/health→Try it out→Execute,返回healthy表示服务就绪 - 点击
/predict→Try it out,使用示例参数(或自定义)→Execute,即可得到负荷预测结果
8.5.3 停止/删除本地容器(可选)
测试完成后,可执行以下命令停止/删除容器:
bash
# 停止容器
docker stop tcn_gru_pred_container
# 删除容器
docker rm tcn_gru_pred_container
九、Docker云端部署
本地测试通过后,将Docker镜像部署到云服务器 (如阿里云ECS、腾讯云CVM、华为云ECS等),实现公网可访问的电力负荷预测服务。本节以阿里云ECS为例,讲解通用部署步骤,其他云服务器操作基本一致。
9.1 云端部署前置条件
- 拥有一台云服务器(推荐配置:CPU≥2核,内存≥4G,系统为CentOS 7/8或Ubuntu 18.04/20.04,公网IP)
- 云服务器已安装Docker(参考Docker官方安装文档)
- 开放云服务器8000端口(云控制台→安全组→入方向→添加规则,端口8000,授权对象0.0.0.0/0)
- 本地与云服务器通过SSH连接(终端/Putty/Xshell)
9.2 镜像推送与拉取(两种方式)
方式1:本地镜像打包→上传云服务器→加载(适合小镜像,零基础推荐)
bash
# 1. 本地将镜像打包为tar文件
docker save -o tcn_gru_load_pred_v1.0.tar tcn_gru_load_pred:v1.0
# 2. 用scp将tar文件上传到云服务器(替换为自己的云服务器公网IP和用户名)
scp tcn_gru_load_pred_v1.0.tar root@xxx.xxx.xxx.xxx:/root/
# 3. 登录云服务器(SSH),加载镜像
docker load -i /root/tcn_gru_load_pred_v1.0.tar
# 4. 云服务器查看镜像,确认加载成功
docker images
方式2:使用Docker镜像仓库(阿里云镜像仓库/华为云镜像仓库/Docker Hub,适合生产环境)
- 云平台开通镜像仓库服务,创建仓库并命名
- 本地登录镜像仓库:
docker login --username=xxx registry.cn-hangzhou.aliyuncs.com - 给本地镜像打标签:
docker tag tcn_gru_load_pred:v1.0 仓库地址/镜像名:v1.0 - 推送镜像:
docker push 仓库地址/镜像名:v1.0 - 云服务器登录镜像仓库,拉取镜像:
docker pull 仓库地址/镜像名:v1.0
9.3 云服务器启动Docker容器
登录云服务器,执行以下命令启动容器,推荐使用守护式运行+端口映射+数据挂载(生产环境):
bash
# 生产环境启动命令:后台运行+端口映射+数据挂载(模型/数据目录)+自动重启
docker run -d \
-p 8000:8000 \
--name tcn_gru_pred_cloud \
--restart=always \
-v /root/tcn_gru_data:/app/data \
-v /root/tcn_gru_models:/app/models \
tcn_gru_load_pred:v1.0
参数说明:
--restart=always:容器异常退出/服务器重启时,自动重启容器(生产环境必备)-v /root/tcn_gru_data:/app/data:数据挂载,将云服务器的/root/tcn_gru_data目录映射到容器内的/app/data,方便更新数据无需重建镜像-v /root/tcn_gru_models:/app/models:模型挂载,同理,更新模型直接替换云服务器的模型文件即可
验证云端容器状态
bash
# 查看运行中的容器
docker ps
# 查看容器日志,确认服务启动成功
docker logs -f tcn_gru_pred_cloud
日志中输出所有资源加载完成,服务就绪!表示云端服务启动成功。
9.4 云端服务公网访问
- 健康检查:访问
http://云服务器公网IP:8000/health,返回健康状态即表示服务正常 - 接口文档:访问
http://云服务器公网IP:8000/docs,可通过Swagger UI在线测试预测接口 - 程序调用:通过Python/Java/PHP等语言发送POST请求到
http://云服务器公网IP:8000/predict,传入指定参数即可获取预测结果
十、接口测试与实际调用
10.1 浏览器/Postman测试
直接通过云服务器的Swagger UI(http://公网IP:8000/docs)测试,无需编写代码,输入参数后点击执行即可得到结果,适合快速验证。
10.2 Python代码调用
编写Python脚本调用云端预测接口,可直接集成到电力调度系统、数据分析平台等项目中:
python
# call_pred_api.py
import requests
import json
# 云端预测接口地址
API_URL = "http://xxx.xxx.xxx.xxx:8000/predict"
# 请求参数
request_data = {
"timestamp": "2024-01-01 00:00:00",
"load_history": [100.2, 98.8, 97.5, 96.3, 95.1, 94.0, 95.2, 97.5, 100.0, 102.5, 105.0, 106.2,
107.5, 106.3, 105.0, 103.8, 101.5, 99.3, 97.1, 95.9, 94.7, 93.6, 94.8, 96.0],
"temperature_history": [5.2, 4.8, 4.5, 4.3, 4.1, 4.0, 4.2, 4.5, 5.0, 5.5, 6.0, 6.2,
6.5, 6.3, 6.0, 5.8, 5.5, 5.3, 5.1, 4.9, 4.7, 4.6, 4.8, 5.0],
"humidity_history": [60.2, 61.8, 62.5, 63.3, 64.1, 65.0, 64.2, 63.5, 62.0, 61.5, 61.0, 60.2,
59.5, 58.3, 57.0, 56.8, 55.5, 54.3, 53.1, 52.9, 51.7, 50.6, 51.8, 53.0],
"wind_speed_history": [2.2, 2.8, 3.5, 3.3, 3.1, 3.0, 2.2, 2.5, 2.0, 1.5, 1.0, 0.2,
0.5, 1.3, 2.0, 2.8, 3.5, 3.3, 3.1, 2.9, 2.7, 2.6, 2.8, 3.0],
"is_holiday": 1,
"is_weekend": 1
}
# 发送POST请求
try:
response = requests.post(
url=API_URL,
data=json.dumps(request_data),
headers={"Content-Type": "application/json"}
)
# 解析响应
if response.status_code == 200:
result = response.json()
print("预测成功!")
print(f"预测时间: {result['prediction_time']}")
print(f"预测时间戳: {result['data']['predict_timestamp']}")
print(f"预测电力负荷: {result['data']['predicted_load_kW']} kW")
else:
print(f"预测失败,状态码: {response.status_code},原因: {response.text}")
except Exception as e:
print(f"调用接口异常: {str(e)}")
运行脚本,即可得到云端返回的负荷预测结果,实现无侵入式集成。
10.3 常见问题排查
- 访问超时:检查云服务器安全组是否开放8000端口,容器是否正常运行
- 模型加载失败:检查挂载的模型目录是否有
best_model.pth,文件路径是否正确 - 预测参数错误:严格按照接口文档的参数格式传入,历史数据长度必须为24
- 容器重启:执行
docker logs 容器名查看日志,定位异常原因(如内存不足、依赖缺失)
十一、项目优化与扩展建议
11.1 模型优化
- 超参数自动调优:使用Optuna/GridSearchCV优化TCN通道数、GRU隐藏维度、序列长度、学习率等超参数,提升预测精度
- 模型结构改进 :
- 加入注意力机制(Attention),让模型关注关键时序特征
- 替换GRU为LSTM/BLSTM,或使用TCN-LSTM混合模型
- 增加残差连接,解决深层模型梯度消失问题
- 多步预测支持:修改模型输出层,实现未来24/48小时的多步负荷预测(如使用Seq2Seq结构)
- 集成学习:将TCN-GRU与XGBoost/LightGBM/Prophet等模型融合,提升预测稳定性
11.2 工程优化
- 性能优化 :
- 使用TorchScript将模型导出为脚本模式,提升推理速度
- 模型量化(INT8),减少显存占用,提升云端低配服务器的运行效率
- 实现批量预测接口,支持多批次数据同时预测,提升吞吐量
- 部署优化 :
- 使用Nginx做反向代理,实现负载均衡、HTTPS配置、请求限流
- 加入Prometheus+Grafana监控,实时监控容器状态、接口调用量、预测延迟
- 使用K8s(Kubernetes)实现容器编排,支持自动扩缩容、滚动更新
- 数据优化 :
- 接入实时气象数据(天气、降水、风速)、电力市场数据,丰富特征维度
- 实现数据增量更新,模型在线微调,适应负荷规律的变化
- 加入数据清洗的自动化流程,处理异常值、缺失值、时序中断问题
11.3 功能扩展
- 可视化平台:基于Vue/React+ECharts开发前端可视化平台,展示负荷历史数据、实时预测结果、负荷趋势
- 异常检测:结合预测值与实际负荷值的偏差,实现电力负荷异常检测,触发告警
- 离线预测:开发离线预测脚本,支持批量处理历史数据,生成负荷预测报表
- 多场景适配:适配不同场景(居民用电、工业用电、商业用电),训练专属模型,提升场景化预测精度
十二、项目总结
本次实战基于PyTorch实现了TCN-GRU混合模型的电力负荷预测系统 ,完成了从多维数据预处理→特征工程→模型构建→训练验证→FastAPI服务封装→Docker容器化→云端部署的全流程开发,所有代码均可直接运行,零基础用户可按步骤复现并落地。
12.1 核心技术亮点
- 模型融合优势 :结合TCN的局部特征提取+并行计算 和GRU的时序记忆特性,解决了传统单一模型在长时序电力负荷预测中精度低、速度慢的问题
- 工程化落地:全程遵循工业级开发规范,实现了数据预处理的标准化、模型训练的可复现、服务部署的容器化,避免环境依赖问题
- 全流程可扩展:从单步预测到多步预测,从本地部署到云端集群,从单一特征到多维特征,系统各模块解耦,可根据实际需求灵活扩展
- 零基础友好:所有代码带详细注释,步骤拆解清晰,无需复杂的专业知识,即可完成从开发到部署的全流程操作
12.2 核心收获
- 掌握了多维时序数据的预处理和特征工程方法,包括时间特征、滞后特征、滚动统计特征的构建逻辑
- 理解了TCN的因果卷积、残差连接和GRU的门控机制,掌握了混合时序模型的设计与实现
- 学会了PyTorch中自定义Dataset、DataLoader的开发,以及模型训练中的早停、学习率调度、多指标评估
- 掌握了FastAPI的服务封装方法,实现了RESTful API的开发、参数验证、异常处理
- 学会了Docker容器化构建和云端部署的完整流程,解决了开发环境与生产环境的一致性问题
- 掌握了云端服务的测试、集成和问题排查方法,实现了模型从实验室到生产环境的落地
12.3 实际应用价值
本系统可直接应用于智能电网调度、电力企业负荷规划、能源管理平台等实际场景,为电力企业的生产调度、能源分配提供数据支撑;同时,该系统的开发流程可复用至其他时序预测场景(如交通流量预测、股票价格预测、环境监测数据预测等),具有广泛的工程应用价值。
至此,基于PyTorch的TCN-GRU电力负荷预测系统的全流程开发与部署已全部完成,从代码开发到工程落地,形成了一套完整的时序预测解决方案。