import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Lambda
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import MinMaxScaler
import os
import time
import warnings
warnings.filterwarnings('ignore', category=UserWarning, module='tensorflow')
mpl.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
mpl.rcParams['axes.unicode_minus'] = False # 关键修复:使用 ASCII 减号
# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
class PINNModel(tf.keras.Model):
def __init__(self, num_layers=4, hidden_units=32, **kwargs):
super(PINNModel, self).__init__(**kwargs)
self.dense_layers = [Dense(hidden_units, activation='tanh')
for _ in range(num_layers)]
self.final_layer = Dense(1, activation='linear')
# 添加带约束的物理参数
self.k_raw = tf.Variable(0.001, trainable=True, dtype=tf.float32, name='k_raw')
self.k = tf.math.sigmoid(self.k_raw) * 0.5 # 约束在0-0.5之间
def call(self, inputs):
t, h, dt = inputs
# 添加特征交互项
interaction = tf.concat([t * dt, h * dt, t * h], axis=1)
# 将时间、水位和时间步长作为输入特征
x = tf.concat([t, h, dt, interaction], axis=1)
for layer in self.dense_layers:
x = layer(x)
return self.final_layer(x)
def physics_loss(self, t, h_current, dt):
"""计算物理损失(基于离散渗流方程)"""
# 预测下一时刻的水位
h_next_pred = self([t, h_current, dt])
# 改进的物理方程:指数衰减模型
# h_{t+1} = h_t * exp(-k * dt)
residual = h_next_pred - (h_current * tf.exp(-self.k * dt))
return tf.reduce_mean(tf.square(residual))
class DamSeepageModel:
def __init__(self, root):
self.root = root
self.root.title("大坝渗流预测模型(PINNs)")
self.root.geometry("1200x800")
# 初始化数据
self.train_df = None # 训练集
self.test_df = None # 测试集
self.model = None
self.scaler = MinMaxScaler(feature_range=(0, 1))
self.evaluation_metrics = {}
# 添加历史记录字典
self.history_records = {}
self.current_history_key = None
# 创建主界面
self.create_widgets()
def create_widgets(self):
# 创建主框架
main_frame = ttk.Frame(self.root, padding=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# 左侧控制面板
control_frame = ttk.LabelFrame(main_frame, text="模型控制", padding=10)
control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
# ====== 新增历史记录UI ======
# 在控制面板中添加历史记录部分
history_frame = ttk.LabelFrame(control_frame, text="历史训练记录", padding=10)
history_frame.pack(fill=tk.X, pady=10)
# 历史记录选择框
ttk.Label(history_frame, text="选择记录:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.history_var = tk.StringVar()
self.history_combobox = ttk.Combobox(
history_frame,
textvariable=self.history_var,
width=25,
state='readonly'
)
self.history_combobox.grid(row=0, column=1, padx=5)
self.history_combobox.bind('<<ComboboxSelected>>', self.load_history_record)
# 历史记录操作按钮
btn_frame = ttk.Frame(history_frame)
btn_frame.grid(row=0, column=2, padx=5)
ttk.Button(btn_frame, text="添加当前", command=self.save_current_as_history).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="删除", command=self.delete_history_record).pack(side=tk.LEFT, padx=2)
# 文件选择部分
file_frame = ttk.LabelFrame(control_frame, text="数据文件", padding=10)
file_frame.pack(fill=tk.X, pady=5)
# 训练集选择
ttk.Label(file_frame, text="训练集:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.train_file_var = tk.StringVar()
ttk.Entry(file_frame, textvariable=self.train_file_var, width=30, state='readonly').grid(row=0, column=1,
padx=5)
ttk.Button(file_frame, text="选择文件", command=lambda: self.select_file("train")).grid(row=0, column=2)
# 测试集选择
ttk.Label(file_frame, text="测试集:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.test_file_var = tk.StringVar()
ttk.Entry(file_frame, textvariable=self.test_file_var, width=30, state='readonly').grid(row=1, column=1, padx=5)
ttk.Button(file_frame, text="选择文件", command=lambda: self.select_file("test")).grid(row=1, column=2)
# PINNs参数设置
param_frame = ttk.LabelFrame(control_frame, text="PINNs参数", padding=10)
param_frame.pack(fill=tk.X, pady=10)
# 验证集切分比例
ttk.Label(param_frame, text="验证集比例:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.split_ratio_var = tk.DoubleVar(value=0.2)
ttk.Spinbox(param_frame, from_=0, to=1, increment=0.05,
textvariable=self.split_ratio_var, width=10).grid(row=0, column=1, padx=5)
# 隐藏层数量
ttk.Label(param_frame, text="网络层数:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.num_layers_var = tk.IntVar(value=4)
ttk.Spinbox(param_frame, from_=2, to=8, increment=1,
textvariable=self.num_layers_var, width=10).grid(row=1, column=1, padx=5)
# 每层神经元数量
ttk.Label(param_frame, text="神经元数/层:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.hidden_units_var = tk.IntVar(value=32)
ttk.Spinbox(param_frame, from_=16, to=128, increment=4,
textvariable=self.hidden_units_var, width=10).grid(row=2, column=1, padx=5)
# 训练轮次
ttk.Label(param_frame, text="训练轮次:").grid(row=3, column=0, sticky=tk.W, pady=5)
self.epochs_var = tk.IntVar(value=500)
ttk.Spinbox(param_frame, from_=100, to=2000, increment=100,
textvariable=self.epochs_var, width=10).grid(row=3, column=1, padx=5)
# 物理损失权重
ttk.Label(param_frame, text="物理损失权重:").grid(row=4, column=0, sticky=tk.W, pady=5)
self.physics_weight_var = tk.DoubleVar(value=0.5)
ttk.Spinbox(param_frame, from_=0.1, to=1.0, increment=0.1,
textvariable=self.physics_weight_var, width=10).grid(row=4, column=1, padx=5)
# 控制按钮
btn_frame = ttk.Frame(control_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="训练模型", command=self.train_model).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="预测结果", command=self.predict).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="保存结果", command=self.save_results).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="重置", command=self.reset).pack(side=tk.RIGHT, padx=5)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(control_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(fill=tk.X, side=tk.BOTTOM)
# 右侧结果显示区域
result_frame = ttk.Frame(main_frame)
result_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建标签页
self.notebook = ttk.Notebook(result_frame)
self.notebook.pack(fill=tk.BOTH, expand=True)
# 损失曲线标签页
self.loss_frame = ttk.Frame(self.notebook)
self.notebook.add(self.loss_frame, text="训练损失")
# 预测结果标签页
self.prediction_frame = ttk.Frame(self.notebook)
self.notebook.add(self.prediction_frame, text="预测结果")
# 指标显示
self.metrics_var = tk.StringVar()
metrics_label = ttk.Label(
self.prediction_frame,
textvariable=self.metrics_var,
font=('TkDefaultFont', 10, 'bold'),
relief='ridge',
padding=5
)
metrics_label.pack(fill=tk.X, padx=5, pady=5)
# 初始化绘图区域
self.fig, self.ax = plt.subplots(figsize=(10, 6))
self.canvas = FigureCanvasTkAgg(self.fig, master=self.prediction_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 损失曲线画布
self.loss_fig, self.loss_ax = plt.subplots(figsize=(10, 4))
self.loss_canvas = FigureCanvasTkAgg(self.loss_fig, master=self.loss_frame)
self.loss_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def save_current_as_history(self):
"""将当前训练状态保存为历史记录"""
if not hasattr(self, 'train_history') or not hasattr(self, 'predictions'):
messagebox.showwarning("警告", "没有可保存的训练记录")
return
# 生成唯一键(时间戳)
timestamp = time.strftime("%Y%m%d-%H%M%S")
key = f"记录-{timestamp}"
# 保存历史记录
self.history_records[key] = {
'train_df': self.train_df.copy(),
'test_df': self.test_df.copy(),
'train_history': self.train_history.copy(),
'predictions': self.predictions.copy(),
'actual_values': self.actual_values.copy(),
'test_time': self.test_time.copy(),
'evaluation_metrics': self.evaluation_metrics.copy(),
'scaler': self.scaler,
'model_params': {
'num_layers': self.num_layers_var.get(),
'hidden_units': self.hidden_units_var.get(),
'epochs': self.epochs_var.get(),
'physics_weight': self.physics_weight_var.get(),
'split_ratio': self.split_ratio_var.get()
},
'file_paths': {
'train': self.train_file_var.get(),
'test': self.test_file_var.get()
}
}
# 更新下拉框
self.update_history_combobox()
self.history_var.set(key)
self.status_var.set(f"已保存当前训练为历史记录: {key}")
def update_history_combobox(self):
"""更新历史记录下拉框选项"""
records = list(self.history_records.keys())
self.history_combobox['values'] = records
def load_history_record(self, event=None):
"""加载选中的历史记录"""
key = self.history_var.get()
if not key or key not in self.history_records:
return
record = self.history_records[key]
self.current_history_key = key
# 恢复数据集
self.train_df = record['train_df'].copy()
self.test_df = record['test_df'].copy()
# 恢复模型参数设置
params = record['model_params']
self.num_layers_var.set(params['num_layers'])
self.hidden_units_var.set(params['hidden_units'])
self.epochs_var.set(params['epochs'])
self.physics_weight_var.set(params['physics_weight'])
self.split_ratio_var.set(params['split_ratio'])
# 恢复文件路径显示
files = record['file_paths']
self.train_file_var.set(files['train'])
self.test_file_var.set(files['test'])
# 恢复训练历史
self.train_history = record['train_history'].copy()
# 恢复预测结果
self.predictions = record['predictions'].copy()
self.actual_values = record['actual_values'].copy()
self.test_time = record['test_time'].copy()
self.evaluation_metrics = record['evaluation_metrics'].copy()
# 更新状态
self.status_var.set(f"已加载历史记录: {key}")
# 显示预测结果
self.show_prediction_results()
# 显示损失曲线
self.show_loss_history()
def show_prediction_results(self):
"""显示历史记录的预测结果"""
if not hasattr(self, 'predictions'):
return
# 清除现有图表
self.ax.clear()
# 绘制结果
self.ax.plot(self.test_time, self.actual_values, 'b-', label='真实值')
self.ax.plot(self.test_time, self.predictions, 'r--', label='预测值')
self.ax.set_title(f'大坝渗流水位预测结果(历史记录: {self.current_history_key})')
self.ax.set_xlabel('时间')
self.ax.set_ylabel('测压管水位', rotation=0)
self.ax.legend()
# 设置时间轴格式
import matplotlib.dates as mdates
self.ax.xaxis.set_major_locator(mdates.YearLocator())
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
self.ax.xaxis.set_minor_locator(mdates.MonthLocator(interval=2))
self.ax.grid(which='minor', axis='x', linestyle='--', color='gray', alpha=0.3)
self.ax.grid(which='major', axis='y', linestyle='-', color='lightgray', alpha=0.5)
self.ax.tick_params(axis='x', which='major', rotation=0, labelsize=10)
self.ax.tick_params(axis='x', which='minor', length=3)
# 显示评估指标
metrics_text = (
f"MSE: {self.evaluation_metrics['MSE']:.4f} | "
f"RMSE: {self.evaluation_metrics['RMSE']:.4f} | "
f"MAE: {self.evaluation_metrics['MAE']:.4f} | "
f"MAPE: {self.evaluation_metrics['MAPE']:.2f}% | "
f"R²: {self.evaluation_metrics['R2']:.4f}"
)
self.metrics_var.set(metrics_text)
# 在图表上添加指标
self.ax.text(
0.5, 1.08, metrics_text,
transform=self.ax.transAxes,
ha='center', fontsize=10,
bbox=dict(facecolor='white', alpha=0.8)
)
# 调整布局
plt.tight_layout(pad=2.0)
self.canvas.draw()
def show_loss_history(self):
"""显示历史记录的损失曲线"""
if not hasattr(self, 'train_history') or 'train_data_loss' not in self.train_history:
return
# 修复:清除现有图表
self.loss_ax.clear() # 修正此行
# 绘制损失曲线
epochs_range = range(1, len(self.train_history['train_data_loss']) + 1)
self.loss_ax.plot(epochs_range, self.train_history['train_data_loss'], 'b-', label='训练数据损失')
if 'physics_loss' in self.train_history:
self.loss_ax.plot(epochs_range, self.train_history['physics_loss'], 'r--', label='物理损失')
if 'valid_data_loss' in self.train_history:
self.loss_ax.plot(epochs_range, self.train_history['valid_data_loss'], 'g-.', label='验证数据损失')
self.loss_ax.set_title(f'PINNs训练损失曲线(历史记录: {self.current_history_key})')
self.loss_ax.set_xlabel('轮次')
self.loss_ax.set_ylabel('损失', rotation=0)
self.loss_ax.legend()
self.loss_ax.grid(True, alpha=0.3)
self.loss_ax.set_yscale('log')
self.loss_canvas.draw()
def delete_history_record(self):
"""删除选中的历史记录"""
key = self.history_var.get()
if not key or key not in self.history_records:
return
# 确认删除
if not messagebox.askyesno("确认删除", f"确定要删除历史记录 '{key}' 吗?"):
return
# 删除记录
del self.history_records[key]
# 更新下拉框
self.update_history_combobox()
# 清空选择
self.history_var.set('')
self.status_var.set(f"已删除历史记录: {key}")
def select_file(self, file_type):
"""选择Excel文件并计算时间步长"""
try:
file_path = filedialog.askopenfilename(
title=f"选择{file_type}集Excel文件",
filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
)
if not file_path:
return
df = pd.read_excel(file_path)
# 验证必需列是否存在
required_cols = ['year', 'month', 'day', '水位']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
messagebox.showerror("列名错误", f"缺少必需列: {', '.join(missing_cols)}")
return
# 时间特征处理
time_features = ['year', 'month', 'day']
missing_time_features = [feat for feat in time_features if feat not in df.columns]
if missing_time_features:
messagebox.showerror("列名错误",
f"Excel文件缺少预处理后的时间特征列: {', '.join(missing_time_features)}")
return
# 创建时间戳列 (增强兼容性)
time_cols = ['year', 'month', 'day']
if 'hour' in df.columns: time_cols.append('hour')
if 'minute' in df.columns: time_cols.append('minute')
if 'second' in df.columns: time_cols.append('second')
# 填充缺失的时间单位
for col in ['hour', 'minute', 'second']:
if col not in df.columns:
df[col] = 0
df['datetime'] = pd.to_datetime(df[time_cols])
# 设置时间索引
df = df.set_index('datetime')
# 计算相对时间(天)
df['days'] = (df.index - df.index[0]).days
# 新增:计算时间步长dt(单位:天)
df['dt'] = df['days'].diff()
# 处理时间步长异常值
if len(df) > 1:
# 计算有效时间步长(排除<=0的值)
valid_dt = df['dt'][df['dt'] > 0]
if len(valid_dt) > 0:
avg_dt = valid_dt.mean()
else:
avg_dt = 1.0
else:
avg_dt = 1.0
# 替换非正值
df.loc[df['dt'] <= 0, 'dt'] = avg_dt
# 填充缺失值
df['dt'] = df['dt'].fillna(avg_dt)
# 保存数据
if file_type == "train":
self.train_df = df
self.train_file_var.set(os.path.basename(file_path))
self.status_var.set(f"已加载训练集: {len(self.train_df)}条数据")
else:
self.test_df = df
self.test_file_var.set(os.path.basename(file_path))
self.status_var.set(f"已加载测试集: {len(self.test_df)}条数据")
except Exception as e :
error_msg = f"文件读取失败: {str(e)}\n\n请确保:\n1. 文件不是打开状态\n2. 文件格式正确\n3. 包含必需的时间和水位列"
messagebox.showerror("文件错误", error_msg)
def calculate_metrics(self, y_true, y_pred):
"""计算评估指标"""
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_true, y_pred)
non_zero_idx = np.where(y_true != 0)[0]
if len(non_zero_idx) > 0:
mape = np.mean(np.abs((y_true[non_zero_idx] - y_pred[non_zero_idx]) / y_true[non_zero_idx])) * 100
else:
mape = float('nan')
r2 = r2_score(y_true, y_pred)
return {
'MSE': mse,
'RMSE': rmse,
'MAE': mae,
'MAPE': mape,
'R2': r2
}
def train_model(self):
"""训练PINNs模型(带早停机制+训练指标监控,无指标绘图)"""
if self.train_df is None:
messagebox.showwarning("警告", "请先选择训练集文件")
return
try:
self.status_var.set("正在预处理数据...")
self.root.update()
# 从训练集中切分训练子集和验证子集(时间顺序切分)
split_ratio = 1 - self.split_ratio_var.get()
split_idx = int(len(self.train_df) * split_ratio)
train_subset = self.train_df.iloc[:split_idx]
valid_subset = self.train_df.iloc[split_idx:]
# 检查数据量是否足够
if len(train_subset) < 2 or len(valid_subset) < 2:
messagebox.showerror("数据错误", "训练集数据量不足(至少需要2个时间步)")
return
# 数据预处理(训练子集拟合scaler,验证子集用相同scaler)
train_subset_scaled = self.scaler.fit_transform(train_subset[['水位']])
valid_subset_scaled = self.scaler.transform(valid_subset[['水位']])
# 准备训练数据(原始值用于指标计算)
t_train = train_subset['days'].values[1:].reshape(-1, 1).astype(np.float32)
h_train = train_subset_scaled[:-1].astype(np.float32)
dt_train = train_subset['dt'].values[1:].reshape(-1, 1).astype(np.float32) # 时间步长
h_next_train_scaled = train_subset_scaled[1:].astype(np.float32) # 归一化后的标签
h_next_train_true = train_subset['水位'].values[1:].reshape(-1, 1) # 原始真实值(反归一化前)
# 准备验证数据(原始值用于指标计算)
t_valid = valid_subset['days'].values[1:].reshape(-1, 1).astype(np.float32)
h_valid = valid_subset_scaled[:-1].astype(np.float32)
dt_valid = valid_subset['dt'].values[1:].reshape(-1, 1).astype(np.float32) # 时间步长
h_next_valid_scaled = valid_subset_scaled[1:].astype(np.float32) # 归一化后的标签
h_next_valid_true = valid_subset['水位'].values[1:].reshape(-1, 1) # 原始真实值
# 创建模型和优化器
self.model = PINNModel(
num_layers=self.num_layers_var.get(),
hidden_units=self.hidden_units_var.get()
)
optimizer = Adam(learning_rate=0.001)
# 构建训练/验证数据集(现在包含时间步长dt)
train_dataset = tf.data.Dataset.from_tensor_slices(((t_train, h_train, dt_train), h_next_train_scaled))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(32)
valid_dataset = tf.data.Dataset.from_tensor_slices(((t_valid, h_valid, dt_valid), h_next_valid_scaled))
valid_dataset = valid_dataset.batch(32) # 验证集无需shuffle
# 损失记录(新增指标记录)
train_data_loss_history = []
physics_loss_history = []
valid_data_loss_history = []
# 新增:训练集和验证集的指标历史(MSE, RMSE等)
train_metrics_history = [] # 每个元素是字典(如{'MSE':..., 'RMSE':...})
valid_metrics_history = []
# 早停机制参数
patience = int(self.epochs_var.get() / 3)
min_delta = 1e-4
best_valid_loss = float('inf')
wait = 0
best_epoch = 0
best_weights = None
start_time = time.time()
# 自定义训练循环(新增指标计算)
for epoch in range(self.epochs_var.get()):
# 训练阶段
epoch_train_data_loss = []
epoch_physics_loss = []
# 收集训练预测值(归一化后)
train_pred_scaled = []
for step, ((t_batch, h_batch, dt_batch), h_next_batch) in enumerate(train_dataset):
with tf.GradientTape() as tape:
# 预测下一时刻水位
h_pred = self.model([t_batch, h_batch, dt_batch])
data_loss = tf.reduce_mean(tf.square(h_next_batch - h_pred))
# 动态调整物理损失权重
current_physics_weight = tf.minimum(
self.physics_weight_var.get() * (1.0 + epoch / self.epochs_var.get()),
0.8
)
# 计算物理损失(传入时间步长dt)
physics_loss = self.model.physics_loss(t_batch, h_batch, dt_batch)
loss = data_loss + current_physics_weight * physics_loss
grads = tape.gradient(loss, self.model.trainable_variables)
optimizer.apply_gradients(zip(grads, self.model.trainable_variables))
epoch_train_data_loss.append(data_loss.numpy())
epoch_physics_loss.append(physics_loss.numpy())
train_pred_scaled.append(h_pred.numpy()) # 保存训练预测值(归一化)
# 合并训练预测值(归一化后)
train_pred_scaled = np.concatenate(train_pred_scaled, axis=0)
# 反归一化得到原始预测值
train_pred_true = self.scaler.inverse_transform(train_pred_scaled)
# 计算训练集指标(使用原始真实值和预测值)
train_metrics = self.calculate_metrics(
y_true=h_next_train_true.flatten(),
y_pred=train_pred_true.flatten()
)
train_metrics_history.append(train_metrics)
# 验证阶段
epoch_valid_data_loss = []
valid_pred_scaled = []
for ((t_v_batch, h_v_batch, dt_v_batch), h_v_next_batch) in valid_dataset:
h_v_pred = self.model([t_v_batch, h_v_batch, dt_v_batch])
valid_data_loss = tf.reduce_mean(tf.square(h_v_next_batch - h_v_pred))
epoch_valid_data_loss.append(valid_data_loss.numpy())
valid_pred_scaled.append(h_v_pred.numpy()) # 保存验证预测值(归一化)
# 合并验证预测值(归一化后)
valid_pred_scaled = np.concatenate(valid_pred_scaled, axis=0)
# 反归一化得到原始预测值
valid_pred_true = self.scaler.inverse_transform(valid_pred_scaled)
# 计算验证集指标(使用原始真实值和预测值)
valid_metrics = self.calculate_metrics(
y_true=h_next_valid_true.flatten(),
y_pred=valid_pred_true.flatten()
)
valid_metrics_history.append(valid_metrics)
# 计算平均损失
avg_train_data_loss = np.mean(epoch_train_data_loss)
avg_physics_loss = np.mean(epoch_physics_loss)
avg_valid_data_loss = np.mean(epoch_valid_data_loss)
# 记录损失
train_data_loss_history.append(avg_train_data_loss)
physics_loss_history.append(avg_physics_loss)
valid_data_loss_history.append(avg_valid_data_loss)
# 早停机制逻辑(与原代码一致)
current_valid_loss = avg_valid_data_loss
if current_valid_loss < best_valid_loss - min_delta:
best_valid_loss = current_valid_loss
best_epoch = epoch + 1
wait = 0
best_weights = self.model.get_weights()
else:
wait += 1
if wait >= patience:
self.status_var.set(f"触发早停!最佳轮次: {best_epoch},最佳验证损失: {best_valid_loss:.4f}")
if best_weights is not None:
self.model.set_weights(best_weights)
break
# 更新状态(新增指标显示)
if epoch % 10 == 0:
# 提取当前训练/验证的关键指标(如RMSE)
train_rmse = train_metrics['RMSE']
valid_rmse = valid_metrics['RMSE']
train_r2 = train_metrics['R2']
valid_r2 = valid_metrics['R2']
k_value = self.model.k.numpy()
elapsed = time.time() - start_time
self.status_var.set(
f"训练中 | 轮次: {epoch + 1}/{self.epochs_var.get()} | "
f"训练RMSE: {train_rmse:.4f} | 验证RMSE: {valid_rmse:.4f} | "
f"训练R²: {train_r2:.4f} | 验证R²: {valid_r2:.4f} | "
f"k: {k_value:.6f} | 时间: {elapsed:.1f}秒 | 早停等待: {wait}/{patience}"
)
self.root.update()
# 绘制损失曲线(仅保留原始损失曲线)
self.loss_ax.clear()
epochs_range = range(1, len(train_data_loss_history) + 1)
self.loss_ax.plot(epochs_range, train_data_loss_history, 'b-', label='训练数据损失')
self.loss_ax.plot(epochs_range, physics_loss_history, 'r--', label='物理损失')
self.loss_ax.plot(epochs_range, valid_data_loss_history, 'g-.', label='验证数据损失')
self.loss_ax.set_title('PINNs训练与验证损失')
self.loss_ax.set_xlabel('轮次')
self.loss_ax.set_ylabel('损失', rotation=0)
self.loss_ax.legend()
self.loss_ax.grid(True, alpha=0.3)
self.loss_ax.set_yscale('log')
self.loss_canvas.draw()
# 训练完成提示(保留指标总结)
elapsed = time.time() - start_time
if wait >= patience:
completion_msg = (
f"早停触发 | 最佳轮次: {best_epoch} | 最佳验证损失: {best_valid_loss:.4f} | "
f"最佳验证RMSE: {valid_metrics_history[best_epoch - 1]['RMSE']:.4f} | "
f"总时间: {elapsed:.1f}秒"
)
else:
completion_msg = (
f"训练完成 | 总轮次: {self.epochs_var.get()} | "
f"最终训练RMSE: {train_metrics_history[-1]['RMSE']:.4f} | "
f"最终验证RMSE: {valid_metrics_history[-1]['RMSE']:.4f} | "
f"最终训练R²: {train_metrics_history[-1]['R2']:.4f} | "
f"最终验证R²: {valid_metrics_history[-1]['R2']:.4f} | "
f"总时间: {elapsed:.1f}秒"
)
# 在训练循环结束后,保存训练历史
self.train_history = {
'train_data_loss': train_data_loss_history,
'physics_loss': physics_loss_history,
'valid_data_loss': valid_data_loss_history,
'train_metrics': train_metrics_history,
'valid_metrics': valid_metrics_history
}
self.status_var.set(completion_msg)
messagebox.showinfo("训练完成", f"PINNs模型训练成功完成!\n{completion_msg}")
except Exception as e:
messagebox.showerror("训练错误", f"模型训练失败:\n{str(e)}")
self.status_var.set("训练失败")
def predict(self):
"""使用PINNs模型进行递归预测(自回归预测)"""
if self.model is None:
messagebox.showwarning("警告", "请先训练模型")
return
if self.test_df is None:
messagebox.showwarning("警告", "请先选择测试集文件")
return
try:
self.status_var.set("正在生成预测...")
self.root.update()
# 预处理测试数据
test_scaled = self.scaler.transform(self.test_df[['水位']])
# 准备时间特征
t_test = self.test_df['days'].values.reshape(-1, 1).astype(np.float32)
dt_test = self.test_df['dt'].values.reshape(-1, 1).astype(np.float32) # 时间步长
# 递归预测(自回归)带误差修正
n = len(t_test)
# 初始化预测序列(归一化),第一个点使用真实值
predicted_scaled = np.zeros((n, 1), dtype=np.float32)
predicted_scaled[0] = test_scaled[0] # 第一个点使用真实值
# 误差累积修正因子
error_correction_factor = 0.3
# 从第二个时间点开始预测
for i in range(1, n):
# 使用上一个时间点的特征
t_prev = t_test[i - 1:i] # 上一个时间点的时间
h_prev = predicted_scaled[i - 1:i] # 上一个时间点的水位(预测值)
dt_i = dt_test[i:i + 1] # 当前时间步长
# 预测当前时间点的水位
h_pred = self.model([t_prev, h_prev, dt_i])
# 误差修正:混合真实值和预测值
if i < n - 1 and i % 5 == 0: # 每5步校正一次
correction = test_scaled[i] - h_pred.numpy()[0][0]
predicted_scaled[i] = h_pred.numpy()[0][0] + error_correction_factor * correction
else:
predicted_scaled[i] = h_pred.numpy()[0][0]
# 反归一化
predictions = self.scaler.inverse_transform(predicted_scaled)
actual_values = self.scaler.inverse_transform(test_scaled)
# 创建时间索引
test_time = self.test_df.index
# 清除现有图表
self.ax.clear()
# 绘制结果
self.ax.plot(test_time, actual_values, 'b-', label='真实值')
self.ax.plot(test_time, predictions, 'r--', label='预测值')
self.ax.set_title('大坝渗流水位预测结果(PINNs)')
self.ax.set_xlabel('时间')
self.ax.set_ylabel('测压管水位', rotation=0)
self.ax.legend()
# 优化时间轴刻度
import matplotlib.dates as mdates
self.ax.xaxis.set_major_locator(mdates.YearLocator())
self.ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
self.ax.xaxis.set_minor_locator(mdates.MonthLocator(interval=2))
self.ax.grid(which='minor', axis='x', linestyle=':', color='gray', alpha=0.3)
self.ax.grid(which='major', axis='y', linestyle='-', color='lightgray', alpha=0.5)
self.ax.tick_params(axis='x', which='major', rotation=0, labelsize=9)
self.ax.tick_params(axis='x', which='minor', length=2)
# 计算评估指标(排除第一个点)
eval_actual = actual_values[1:].flatten()
eval_pred = predictions[1:].flatten()
self.evaluation_metrics = self.calculate_metrics(eval_actual, eval_pred)
metrics_text = (
f"MSE: {self.evaluation_metrics['MSE']:.4f} | "
f"RMSE: {self.evaluation_metrics['RMSE']:.4f} | "
f"MAE: {self.evaluation_metrics['MAE']:.4f} | "
f"MAPE: {self.evaluation_metrics['MAPE']:.2f}% | "
f"R²: {self.evaluation_metrics['R2']:.4f}"
)
self.metrics_var.set(metrics_text)
# 在图表上添加指标
self.ax.text(
0.5, 1.05, metrics_text,
transform=self.ax.transAxes,
ha='center', fontsize=9,
bbox=dict(facecolor='white', alpha=0.8)
)
# 调整布局
plt.tight_layout(pad=2.0)
self.canvas.draw()
# 保存预测结果
self.predictions = predictions
self.actual_values = actual_values
self.test_time = test_time
self.status_var.set("预测完成,结果已显示")
except Exception as e:
messagebox.showerror("预测错误", f"预测失败:\n{str(e)}")
self.status_var.set("预测失败")
# 记录详细错误信息
import traceback
traceback.print_exc()
def save_results(self):
"""保存预测结果和训练历史数据"""
if not hasattr(self, 'predictions') or not hasattr(self, 'train_history'):
messagebox.showwarning("警告", "请先生成预测结果并完成训练")
return
# 选择保存路径
save_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")],
title="保存结果"
)
if not save_path:
return
try:
# 1. 创建预测结果DataFrame
result_df = pd.DataFrame({
'时间': self.test_time,
'实际水位': self.actual_values.flatten(),
'预测水位': self.predictions.flatten()
})
# 2. 创建评估指标DataFrame
metrics_df = pd.DataFrame([self.evaluation_metrics])
# 3. 创建训练历史DataFrame
history_data = {
'轮次': list(range(1, len(self.train_history['train_data_loss']) + 1)),
'训练数据损失': self.train_history['train_data_loss'],
'物理损失': self.train_history['physics_loss'],
'验证数据损失': self.train_history['valid_data_loss']
}
# 添加训练集指标
for metric in ['MSE', 'RMSE', 'MAE', 'MAPE', 'R2']:
history_data[f'训练集_{metric}'] = [item[metric] for item in self.train_history['train_metrics']]
# 添加验证集指标
for metric in ['MSE', 'RMSE', 'MAE', 'MAPE', 'R2']:
history_data[f'验证集_{metric}'] = [item[metric] for item in self.train_history['valid_metrics']]
history_df = pd.DataFrame(history_data)
# 保存到Excel
with pd.ExcelWriter(save_path) as writer:
result_df.to_excel(writer, sheet_name='预测结果', index=False)
metrics_df.to_excel(writer, sheet_name='评估指标', index=False)
history_df.to_excel(writer, sheet_name='训练历史', index=False)
# 保存图表
chart_path = os.path.splitext(save_path)[0] + "_chart.png"
self.fig.savefig(chart_path, dpi=300)
# 保存损失曲线图
loss_path = os.path.splitext(save_path)[0] + "_loss.png"
self.loss_fig.savefig(loss_path, dpi=300)
self.status_var.set(f"结果已保存至: {os.path.basename(save_path)}")
messagebox.showinfo("保存成功",
f"预测结果和图表已保存至:\n"
f"主文件: {save_path}\n"
f"预测图表: {chart_path}\n"
f"损失曲线: {loss_path}")
except Exception as e:
messagebox.showerror("保存错误", f"保存结果失败:\n{str(e)}")
def reset(self):
"""重置程序状态"""
self.train_df = None
self.test_df = None
self.model = None
self.train_file_var.set("")
self.test_file_var.set("")
# 清除训练历史
if hasattr(self, 'train_history'):
del self.train_history
# 清除图表
if hasattr(self, 'ax'):
self.ax.clear()
if hasattr(self, 'loss_ax'):
self.loss_ax.clear()
# 重绘画布
if hasattr(self, 'canvas'):
self.canvas.draw()
if hasattr(self, 'loss_canvas'):
self.loss_canvas.draw()
# 清除状态
self.status_var.set("已重置,请选择新数据")
# 清除预测结果
if hasattr(self, 'predictions'):
del self.predictions
# 清除指标文本
if hasattr(self, 'metrics_var'):
self.metrics_var.set("")
messagebox.showinfo("重置", "程序已重置,可以开始新的分析")
if __name__ == "__main__":
root = tk.Tk()
app = DamSeepageModel(root)
root.mainloop()
帮我对t,h,dt等特征量进行归一化处理
最新发布