import pandas as pd import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset import matplotlib # 设置matplotlib后端为Agg,适用于无头服务器环境 matplotlib.use('Agg') import matplotlib.pyplot as plt from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split import os from datetime import datetime import json import torch.serialization # 添加tqdm的安全导入 try: from tqdm import tqdm TQDM_AVAILABLE = True except ImportError: # 如果没有tqdm,创建一个简单的替代品 TQDM_AVAILABLE = False def tqdm(iterable, **kwargs): # 简单的进度指示器 total = len(iterable) if hasattr(iterable, '__len__') else None if total and kwargs.get('desc'): print(f"{kwargs.get('desc')} - 共 {total} 步") return iterable # 添加MinMaxScaler到安全全局列表 torch.serialization.add_safe_globals(['sklearn.preprocessing._data.MinMaxScaler']) # 设置随机种子以便结果可重现 # 导入模型模块 from models.data_utils import PharmacyDataset, create_dataset, evaluate_model from models.transformer_model import TimeSeriesTransformer from models.mlstm_model import MLSTMTransformer from models.kan_model import KANForecaster from models.utils import get_device, to_device, DeviceDataLoader # 解决画图中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 获取设备(GPU或CPU) device = get_device() print(f"使用设备: {device}") # 全局参数设置 look_back = 14 # 使用过去14天数据 T = 7 # 预测未来7天销量 epochs = 50 # 训练轮次 num_features = 8 # 输入特征数 embed_dim = 32 # 嵌入维度 dense_dim = 32 # 隐藏层神经元数 num_heads = 4 # 注意力头数 dropout_rate = 0.1 # 丢弃率 num_blocks = 3 # 编码器解码器数 learn_rate = 0.001 # 学习率 batch_size = 32 # 批大小 # 默认训练函数 - 使用mLSTM作为默认模型 def train_product_model(product_id, epochs=50): """ 默认的产品销售预测模型训练函数,使用mLSTM作为默认模型 Args: product_id: 产品ID epochs: 训练轮次 Returns: model: 训练好的模型 metrics: 模型评估指标 """ return train_product_model_with_mlstm(product_id, epochs) # 使用mLSTM模型训练的函数 def train_product_model_with_mlstm(product_id, epochs=50): # 读取生成的药店销售数据 df = pd.read_excel('pharmacy_sales.xlsx') # 筛选特定产品数据 product_df = df[df['product_id'] == product_id].sort_values('date') product_name = product_df['product_name'].iloc[0] print(f"使用mLSTM模型训练产品 '{product_name}' (ID: {product_id}) 的销售预测模型") print(f"使用设备: {device}") # 创建特征和目标变量 features = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature'] # 预处理数据 X = product_df[features].values y = product_df[['sales']].values # 保持为二维数组 # 归一化数据 scaler_X = MinMaxScaler(feature_range=(0, 1)) scaler_y = MinMaxScaler(feature_range=(0, 1)) X_scaled = scaler_X.fit_transform(X) y_scaled = scaler_y.fit_transform(y) # 划分训练集和测试集(80% 训练,20% 测试) train_size = int(len(X_scaled) * 0.8) X_train, X_test = X_scaled[:train_size], X_scaled[train_size:] y_train, y_test = y_scaled[:train_size], y_scaled[train_size:] # 创建时间序列数据 trainX, trainY = create_dataset(X_train, y_train, look_back, T) testX, testY = create_dataset(X_test, y_test, look_back, T) # 转换为PyTorch的Tensor trainX_tensor = torch.Tensor(trainX) trainY_tensor = torch.Tensor(trainY) testX_tensor = torch.Tensor(testX) testY_tensor = torch.Tensor(testY) # 创建数据加载器 train_dataset = PharmacyDataset(trainX_tensor, trainY_tensor) test_dataset = PharmacyDataset(testX_tensor, testY_tensor) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 将数据加载器包装到设备加载器中 train_loader = DeviceDataLoader(train_loader, device) test_loader = DeviceDataLoader(test_loader, device) # 初始化mLSTM结合Transformer模型 model = MLSTMTransformer( num_features=num_features, hidden_size=128, mlstm_layers=1, embed_dim=embed_dim, dense_dim=dense_dim, num_heads=num_heads, dropout_rate=dropout_rate, num_blocks=num_blocks, output_sequence_length=T ) # 将模型移动到设备上 model = model.to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=learn_rate) # 训练模型 train_losses = [] test_losses = [] for epoch in range(epochs): model.train() epoch_loss = 0 for X_batch, y_batch in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}", leave=False): # 前向传播 outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss += loss.item() # 计算训练损失 train_loss = epoch_loss / len(train_loader) train_losses.append(train_loss) # 在测试集上评估 model.eval() test_loss = 0 with torch.no_grad(): for X_batch, y_batch in test_loader: outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) test_loss += loss.item() test_loss = test_loss / len(test_loader) test_losses.append(test_loss) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}") # 绘制损失曲线 plt.figure(figsize=(10, 5)) plt.plot(train_losses, label='训练损失') plt.plot(test_losses, label='测试损失') plt.title(f'{product_name} - mLSTM模型训练和测试损失') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.grid(True) plt.savefig(f'{product_id}_mlstm_loss_curve.png') # 生成预测 model.eval() with torch.no_grad(): # 将测试数据移动到设备上 testX_device = to_device(testX_tensor, device) y_pred_scaled = model(testX_device).squeeze(-1).cpu().numpy() # 反归一化预测结果 y_pred = scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).reshape(y_pred_scaled.shape) y_true = scaler_y.inverse_transform(testY.reshape(-1, 1)).reshape(testY.shape) # 评估模型 metrics = evaluate_model(y_true.flatten(), y_pred.flatten()) print(f"\n{product_name} mLSTM模型评估指标:") for metric, value in metrics.items(): print(f"{metric}: {value:.4f}") # 绘制预测结果 plt.figure(figsize=(12, 6)) # 获取测试集的实际日期 test_dates = product_df['date'].iloc[train_size + look_back:train_size + look_back + len(y_true)].values # 只绘制最后30天的预测 days_to_plot = min(30, len(y_true)) start_idx = max(0, len(y_true) - days_to_plot) plt.plot(test_dates[start_idx:], y_true[start_idx:, 0], 'b-', label='实际销量') plt.plot(test_dates[start_idx:], y_pred[start_idx:, 0], 'r--', label='mLSTM预测销量') plt.title(f'{product_name} - mLSTM销量预测结果') plt.xlabel('日期') plt.ylabel('销量') plt.legend() plt.grid(True) plt.xticks(rotation=45) plt.tight_layout() plt.savefig(f'{product_id}_mlstm_prediction.png') # 保存预测结果到CSV all_dates = [] num_samples = len(y_true) for i in range(num_samples): start_idx = train_size + i + look_back dates = product_df['date'].iloc[start_idx : start_idx + T] all_dates.extend(dates) # 修正日期长度不匹配的问题 flat_y_true = y_true.flatten() flat_y_pred = y_pred.flatten() min_len = min(len(all_dates), len(flat_y_true)) results_df = pd.DataFrame({ 'date': all_dates[:min_len], 'actual_sales': flat_y_true[:min_len], 'predicted_sales': flat_y_pred[:min_len] }) results_df.to_csv(f'{product_id}_mlstm_prediction_results.csv', index=False) print(f"\nmLSTM预测结果已保存到 {product_id}_mlstm_prediction_results.csv") # 创建models目录和子目录 model_dir = 'models/mlstm' os.makedirs(model_dir, exist_ok=True) # 保存模型 model_path = os.path.join(model_dir, f'{product_id}_model.pt') torch.save({ 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'train_loss': train_losses, 'test_loss': test_losses, 'scaler_X': scaler_X, 'scaler_y': scaler_y, 'features': features, 'look_back': look_back, 'T': T, 'model_type': 'mlstm' }, model_path) print(f"模型已成功保存到 {model_path}") # 保存日志文件 log_path = os.path.join(model_dir, f'{product_id}_log.json') log_data = { 'product_id': product_id, 'product_name': product_name, 'model_type': 'mlstm', 'training_completed_at': datetime.now().isoformat(), 'epochs': epochs, 'metrics': metrics, 'file_path': model_path } with open(log_path, 'w', encoding='utf-8') as f: json.dump(log_data, f, indent=4, ensure_ascii=False) print(f"训练日志已保存到 {log_path}") return model, metrics # 使用KAN模型训练的函数 def train_product_model_with_kan(product_id, epochs=50): # 读取生成的药店销售数据 df = pd.read_excel('pharmacy_sales.xlsx') # 筛选特定产品数据 product_df = df[df['product_id'] == product_id].sort_values('date') product_name = product_df['product_name'].iloc[0] print(f"使用KAN模型训练产品 '{product_name}' (ID: {product_id}) 的销售预测模型") print(f"使用设备: {device}") # 创建特征和目标变量 features = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature'] # 预处理数据 X = product_df[features].values y = product_df[['sales']].values # 保持为二维数组 # 归一化数据 scaler_X = MinMaxScaler(feature_range=(0, 1)) scaler_y = MinMaxScaler(feature_range=(0, 1)) X_scaled = scaler_X.fit_transform(X) y_scaled = scaler_y.fit_transform(y) # 划分训练集和测试集(80% 训练,20% 测试) train_size = int(len(X_scaled) * 0.8) X_train, X_test = X_scaled[:train_size], X_scaled[train_size:] y_train, y_test = y_scaled[:train_size], y_scaled[train_size:] # 创建时间序列数据 trainX, trainY = create_dataset(X_train, y_train, look_back, T) testX, testY = create_dataset(X_test, y_test, look_back, T) # 转换为PyTorch的Tensor trainX_tensor = torch.Tensor(trainX) trainY_tensor = torch.Tensor(trainY) testX_tensor = torch.Tensor(testX) testY_tensor = torch.Tensor(testY) # 创建数据加载器 train_dataset = PharmacyDataset(trainX_tensor, trainY_tensor) test_dataset = PharmacyDataset(testX_tensor, testY_tensor) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 将数据加载器包装到设备加载器中 train_loader = DeviceDataLoader(train_loader, device) test_loader = DeviceDataLoader(test_loader, device) # 初始化KAN模型 model = KANForecaster( input_features=num_features, hidden_sizes=[64, 128, 64], output_size=1, grid_size=5, spline_order=3, dropout_rate=dropout_rate, output_sequence_length=T ) # 将模型移动到设备上 model = model.to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=learn_rate) # 训练模型 train_losses = [] test_losses = [] for epoch in range(epochs): model.train() epoch_loss = 0 for X_batch, y_batch in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}", leave=False): # 前向传播 outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss += loss.item() # 计算训练损失 train_loss = epoch_loss / len(train_loader) train_losses.append(train_loss) # 在测试集上评估 model.eval() test_loss = 0 with torch.no_grad(): for X_batch, y_batch in test_loader: outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) test_loss += loss.item() test_loss = test_loss / len(test_loader) test_losses.append(test_loss) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}") # 绘制损失曲线 plt.figure(figsize=(10, 5)) plt.plot(train_losses, label='训练损失') plt.plot(test_losses, label='测试损失') plt.title(f'{product_name} - KAN模型训练和测试损失') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.grid(True) plt.savefig(f'{product_id}_kan_loss_curve.png') # 生成预测 model.eval() with torch.no_grad(): # 将测试数据移动到设备上 testX_device = to_device(testX_tensor, device) y_pred_scaled = model(testX_device).squeeze(-1).cpu().numpy() # 反归一化预测结果 y_pred = scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).reshape(y_pred_scaled.shape) y_true = scaler_y.inverse_transform(testY.reshape(-1, 1)).reshape(testY.shape) # 评估模型 metrics = evaluate_model(y_true.flatten(), y_pred.flatten()) print(f"\n{product_name} KAN模型评估指标:") for metric, value in metrics.items(): print(f"{metric}: {value:.4f}") # 绘制预测结果 plt.figure(figsize=(12, 6)) # 获取测试集的实际日期 test_dates = product_df['date'].iloc[train_size + look_back:train_size + look_back + len(y_true)].values # 只绘制最后30天的预测 days_to_plot = min(30, len(y_true)) start_idx = max(0, len(y_true) - days_to_plot) plt.plot(test_dates[start_idx:], y_true[start_idx:, 0], 'b-', label='实际销量') plt.plot(test_dates[start_idx:], y_pred[start_idx:, 0], 'r--', label='KAN预测销量') plt.title(f'{product_name} - KAN销量预测结果') plt.xlabel('日期') plt.ylabel('销量') plt.legend() plt.grid(True) plt.xticks(rotation=45) plt.tight_layout() plt.savefig(f'{product_id}_kan_prediction.png') # 保存预测结果到CSV all_dates = [] num_samples = len(y_true) for i in range(num_samples): start_idx = train_size + i + look_back dates = product_df['date'].iloc[start_idx : start_idx + T] all_dates.extend(dates) # 修正日期长度不匹配的问题 flat_y_true = y_true.flatten() flat_y_pred = y_pred.flatten() min_len = min(len(all_dates), len(flat_y_true)) results_df = pd.DataFrame({ 'date': all_dates[:min_len], 'actual_sales': flat_y_true[:min_len], 'predicted_sales': flat_y_pred[:min_len] }) results_df.to_csv(f'{product_id}_kan_prediction_results.csv', index=False) print(f"\nKAN预测结果已保存到 {product_id}_kan_prediction_results.csv") # 创建models目录和子目录 model_dir = 'models/kan' os.makedirs(model_dir, exist_ok=True) # 保存模型 model_path = os.path.join(model_dir, f'{product_id}_model.pt') torch.save({ 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'train_loss': train_losses, 'test_loss': test_losses, 'scaler_X': scaler_X, 'scaler_y': scaler_y, 'features': features, 'look_back': look_back, 'T': T, 'model_type': 'kan' }, model_path) print(f"模型已成功保存到 {model_path}") # 保存日志文件 log_path = os.path.join(model_dir, f'{product_id}_log.json') log_data = { 'product_id': product_id, 'product_name': product_name, 'model_type': 'kan', 'training_completed_at': datetime.now().isoformat(), 'epochs': epochs, 'metrics': metrics, 'file_path': model_path } with open(log_path, 'w', encoding='utf-8') as f: json.dump(log_data, f, indent=4, ensure_ascii=False) print(f"训练日志已保存到 {log_path}") return model, metrics # 使用Transformer模型训练的函数 def train_product_model_with_transformer(product_id, epochs=50): # 读取生成的药店销售数据 df = pd.read_excel('pharmacy_sales.xlsx') # 筛选特定产品数据 product_df = df[df['product_id'] == product_id].sort_values('date') product_name = product_df['product_name'].iloc[0] print(f"使用Transformer模型训练产品 '{product_name}' (ID: {product_id}) 的销售预测模型") print(f"使用设备: {device}") # 创建特征和目标变量 features = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature'] # 预处理数据 X = product_df[features].values y = product_df[['sales']].values # 保持为二维数组 # 归一化数据 scaler_X = MinMaxScaler(feature_range=(0, 1)) scaler_y = MinMaxScaler(feature_range=(0, 1)) X_scaled = scaler_X.fit_transform(X) y_scaled = scaler_y.fit_transform(y) # 划分训练集和测试集(80% 训练,20% 测试) train_size = int(len(X_scaled) * 0.8) X_train, X_test = X_scaled[:train_size], X_scaled[train_size:] y_train, y_test = y_scaled[:train_size], y_scaled[train_size:] # 创建时间序列数据 trainX, trainY = create_dataset(X_train, y_train, look_back, T) testX, testY = create_dataset(X_test, y_test, look_back, T) # 转换为PyTorch的Tensor trainX_tensor = torch.Tensor(trainX) trainY_tensor = torch.Tensor(trainY) testX_tensor = torch.Tensor(testX) testY_tensor = torch.Tensor(testY) # 创建数据加载器 train_dataset = PharmacyDataset(trainX_tensor, trainY_tensor) test_dataset = PharmacyDataset(testX_tensor, testY_tensor) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 将数据加载器包装到设备加载器中 train_loader = DeviceDataLoader(train_loader, device) test_loader = DeviceDataLoader(test_loader, device) # 初始化Transformer模型 model = TimeSeriesTransformer( num_features=num_features, d_model=embed_dim, nhead=num_heads, num_encoder_layers=num_blocks, dim_feedforward=dense_dim, dropout=dropout_rate, output_sequence_length=T ) # 将模型移动到设备上 model = model.to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=learn_rate) # 训练模型 train_losses = [] test_losses = [] for epoch in range(epochs): model.train() epoch_loss = 0 for X_batch, y_batch in tqdm(train_loader, desc=f"Epoch {epoch+1}/{epochs}", leave=False): # 前向传播 outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) # 反向传播和优化 optimizer.zero_grad() loss.backward() optimizer.step() epoch_loss += loss.item() # 计算训练损失 train_loss = epoch_loss / len(train_loader) train_losses.append(train_loss) # 在测试集上评估 model.eval() test_loss = 0 with torch.no_grad(): for X_batch, y_batch in test_loader: outputs = model(X_batch) loss = criterion(outputs.squeeze(-1), y_batch) test_loss += loss.item() test_loss = test_loss / len(test_loader) test_losses.append(test_loss) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}") # 绘制损失曲线 plt.figure(figsize=(10, 5)) plt.plot(train_losses, label='训练损失') plt.plot(test_losses, label='测试损失') plt.title(f'{product_name} - Transformer模型训练和测试损失') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend() plt.grid(True) plt.savefig(f'{product_id}_transformer_loss_curve.png') # 生成预测 model.eval() with torch.no_grad(): # 将测试数据移动到设备上 testX_device = to_device(testX_tensor, device) y_pred_scaled = model(testX_device).squeeze(-1).cpu().numpy() # 反归一化预测结果 y_pred = scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).reshape(y_pred_scaled.shape) y_true = scaler_y.inverse_transform(testY.reshape(-1, 1)).reshape(testY.shape) # 评估模型 metrics = evaluate_model(y_true.flatten(), y_pred.flatten()) print(f"\n{product_name} Transformer模型评估指标:") for metric, value in metrics.items(): print(f"{metric}: {value:.4f}") # 绘制预测结果 plt.figure(figsize=(12, 6)) # 获取测试集的实际日期 test_dates = product_df['date'].iloc[train_size + look_back:train_size + look_back + len(y_true)].values # 只绘制最后30天的预测 days_to_plot = min(30, len(y_true)) start_idx = max(0, len(y_true) - days_to_plot) plt.plot(test_dates[start_idx:], y_true[start_idx:, 0], 'b-', label='实际销量') plt.plot(test_dates[start_idx:], y_pred[start_idx:, 0], 'r--', label='Transformer预测销量') plt.title(f'{product_name} - Transformer销量预测结果') plt.xlabel('日期') plt.ylabel('销量') plt.legend() plt.grid(True) plt.xticks(rotation=45) plt.tight_layout() # 强制重新绘制图表 fig.canvas.draw() # 将预测起始日期和预测时长添加到文件名中 start_date_str = start_date_obj.strftime('%Y%m%d') # 保存和显示图表 forecast_chart = f'{product_id}_transformer_forecast_{start_date_str}_days{T}.png' plt.savefig(forecast_chart) print(f"预测图表已保存为: {forecast_chart}") # 关闭图表以释放内存 plt.close() # 创建预测日期范围 last_date = product_df['date'].iloc[-1] future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=T, freq='D') # 创建预测结果DataFrame - 确保长度一致 # 使用测试集的最后T个预测作为未来预测 future_predictions = y_pred[-1, :T].flatten() # 取最后一个样本的预测序列 # 确保长度匹配 if len(future_dates) != len(future_predictions): # 如果长度不匹配,调整future_predictions的长度 future_predictions = future_predictions[:len(future_dates)] predictions_df = pd.DataFrame({ 'date': future_dates, 'product_id': product_id, 'product_name': product_name, 'predicted_sales': future_predictions }) print(f"\n{product_name} 未来 {T} 天销售预测 (使用Transformer模型):") print(predictions_df[['date', 'predicted_sales']]) # 创建预测结果目录 output_dir = f'predictions/transformer/{product_id}' os.makedirs(output_dir, exist_ok=True) # 可视化预测结果 try: # 1. 创建预测图表 forecast_fig, forecast_ax = plt.subplots(figsize=(12, 6)) # 显示历史数据和预测数据 history_days = 30 # 显示最近30天的历史数据 # 只选择预测起始日期之前30天的历史数据,而不是全部历史数据 history_end_date = start_date_obj - pd.Timedelta(days=1) # 预测起始日期的前一天 history_start_date = history_end_date - pd.Timedelta(days=history_days) # 向前推30天 # 过滤历史数据,只保留这个日期范围内的数据 history_df = product_df[(product_df['date'] >= history_start_date) & (product_df['date'] <= history_end_date)][['date', 'sales']].copy() if history_df.empty: print(f"警告: 在日期范围 {history_start_date} 到 {history_end_date} 内没有历史数据") # 如果没有符合条件的历史数据,就使用最近的数据 history_df = product_df.iloc[-min(history_days, len(product_df)):][['date', 'sales']].copy() print(f"历史数据日期范围: {history_df['date'].min()} 到 {history_df['date'].max()}") print(f"预测数据日期范围: {future_dates.min()} 到 {future_dates.max()}") print(f"预测起始日期: {start_date_obj.strftime('%Y-%m-%d')}") # 绘制历史数据 forecast_ax.plot(history_df['date'].values, history_df['sales'].values, 'b-', label='历史销量') # 绘制预测数据,确保使用future_dates作为x轴 forecast_ax.plot(future_dates, future_predictions, 'r--', label=f'{model_type}预测销量') # 强制X轴从预测起始日期的前30天开始(如果有历史数据)到预测结束日期 date_min = start_date_obj - pd.Timedelta(days=30) date_max = future_dates.max() + pd.Timedelta(days=2) print(f"设置X轴范围: {date_min} 到 {date_max}") forecast_ax.set_xlim(date_min, date_max) forecast_ax.set_title(f'{product_name} - {model_type}销量预测 (从{start_date_obj.strftime("%Y-%m-%d")}开始,预测{T}天)') forecast_ax.set_xlabel('日期') forecast_ax.set_ylabel('销量') forecast_ax.legend() forecast_ax.grid(True) plt.xticks(rotation=45) plt.tight_layout() # 强制重新绘制图表 forecast_fig.canvas.draw() # 将预测起始日期和预测时长添加到文件名中 start_date_str = start_date_obj.strftime('%Y%m%d') # 保存预测图表 forecast_chart = f'{output_dir}/forecast_{start_date_str}_days{T}.png' plt.savefig(forecast_chart) print(f"预测图表已保存为: {forecast_chart}") # 关闭图表以释放内存 plt.close(forecast_fig) # 2. 创建历史趋势图表 try: print("\n开始生成历史趋势图...") history_fig, history_ax = plt.subplots(figsize=(12, 6)) # 获取预测起始日期 current_year = start_date_obj.year current_month = start_date_obj.month current_day = start_date_obj.day print(f"预测起始日期: {start_date_obj}") # 计算同期日期范围(前3天和后3天,共7天) days_before = 3 days_after = 3 date_range_start = start_date_obj - pd.Timedelta(days=days_before) date_range_end = start_date_obj + pd.Timedelta(days=days_after) # 计算去年同期日期范围 last_year_start = date_range_start.replace(year=date_range_start.year-1) last_year_end = date_range_end.replace(year=date_range_end.year-1) # 计算上月同期日期范围 if date_range_start.month > 1: last_month_start = date_range_start.replace(month=date_range_start.month-1) last_month_end = date_range_end.replace(month=date_range_end.month-1) else: # 如果是1月,则转到上一年的12月 last_month_start = date_range_start.replace(year=date_range_start.year-1, month=12) last_month_end = date_range_end.replace(year=date_range_end.year-1, month=12) print(f"当前日期范围: {date_range_start} 到 {date_range_end}") print(f"去年同期范围: {last_year_start} 到 {last_year_end}") print(f"上月同期范围: {last_month_start} 到 {last_month_end}") # 查找对应日期范围的数据 current_period_data = product_df[ (product_df['date'] >= date_range_start) & (product_df['date'] <= date_range_end) ] print(f"当前期间数据点数: {len(current_period_data)}") last_year_period_data = product_df[ (product_df['date'] >= last_year_start) & (product_df['date'] <= last_year_end) ] print(f"去年同期数据点数: {len(last_year_period_data)}") last_month_period_data = product_df[ (product_df['date'] >= last_month_start) & (product_df['date'] <= last_month_end) ] print(f"上月同期数据点数: {len(last_month_period_data)}") # 绘制曲线图 has_data = False if not current_period_data.empty: has_data = True # 确保日期升序排序 current_period_data = current_period_data.sort_values('date') # 生成相对天数(以date_range_start为基准) current_period_data['day_offset'] = (current_period_data['date'] - date_range_start).dt.days print(f"当前期间日期: {current_period_data['date'].tolist()}") print(f"当前期间相对天数: {current_period_data['day_offset'].tolist()}") print(f"当前期间销量: {current_period_data['sales'].tolist()}") history_ax.plot( current_period_data['day_offset'], current_period_data['sales'], 'r-', marker='o', linewidth=2, label=f"当前期间 ({date_range_start.strftime('%Y-%m-%d')} 到 {date_range_end.strftime('%Y-%m-%d')})" ) # 标记预测起始日期 current_center_point = current_period_data[current_period_data['date'] == start_date_obj] if not current_center_point.empty: history_ax.scatter( current_center_point['day_offset'], current_center_point['sales'], color='red', s=100, marker='*', zorder=10, label=f"预测起始日 ({start_date_obj.strftime('%Y-%m-%d')})" ) if not last_year_period_data.empty: has_data = True # 确保日期升序排序 last_year_period_data = last_year_period_data.sort_values('date') last_year_period_data['day_offset'] = (last_year_period_data['date'] - last_year_start).dt.days print(f"去年同期日期: {last_year_period_data['date'].tolist()}") print(f"去年同期相对天数: {last_year_period_data['day_offset'].tolist()}") print(f"去年同期销量: {last_year_period_data['sales'].tolist()}") history_ax.plot( last_year_period_data['day_offset'], last_year_period_data['sales'], 'b-', marker='s', linewidth=2, label=f"去年同期 ({last_year_start.strftime('%Y-%m-%d')} 到 {last_year_end.strftime('%Y-%m-%d')})" ) if not last_month_period_data.empty: has_data = True # 确保日期升序排序 last_month_period_data = last_month_period_data.sort_values('date') last_month_period_data['day_offset'] = (last_month_period_data['date'] - last_month_start).dt.days print(f"上月同期日期: {last_month_period_data['date'].tolist()}") print(f"上月同期相对天数: {last_month_period_data['day_offset'].tolist()}") print(f"上月同期销量: {last_month_period_data['sales'].tolist()}") history_ax.plot( last_month_period_data['day_offset'], last_month_period_data['sales'], 'g-', marker='^', linewidth=2, label=f"上月同期 ({last_month_start.strftime('%Y-%m-%d')} 到 {last_month_end.strftime('%Y-%m-%d')})" ) # 设置X轴标签为相对天数 days_labels = list(range(7)) days_offsets = list(range(7)) day_names = [(date_range_start + pd.Timedelta(days=d)).strftime('%m-%d') for d in range(7)] history_ax.set_xticks(days_offsets) history_ax.set_xticklabels(day_names) # 添加垂直参考线标记预测起始日 history_ax.axvline(x=days_before, color='gray', linestyle='--', alpha=0.5) # 美化图表 history_ax.set_title(f'{product_name} - 同期销量趋势对比 (7天)') history_ax.set_xlabel('日期') history_ax.set_ylabel('销量') history_ax.grid(True, linestyle='--', alpha=0.7) history_ax.legend(loc='best') # 如果所有数据集都为空,显示提示 if not has_data: history_ax.text(0.5, 0.5, '没有找到可比较的同期数据', horizontalalignment='center', verticalalignment='center', transform=history_ax.transAxes, fontsize=14) plt.tight_layout() # 强制重新绘制图表 history_fig.canvas.draw() # 保存历史趋势图表 history_chart = f'{output_dir}/history_{start_date_str}.png' plt.savefig(history_chart) print(f"历史趋势图表已保存为: {history_chart}") # 关闭图表以释放内存 plt.close(history_fig) except Exception as e: import traceback print(f"生成历史趋势图时出错: {e}") traceback.print_exc() history_chart = None # 创建一个包含历史和预测数据的完整DataFrame供CSV导出和API返回 history_df['data_type'] = '历史销量' predictions_df = pd.DataFrame({ 'date': future_dates, 'sales': y_pred, 'data_type': '预测销量', 'product_id': product_id, 'product_name': product_name }) # 合并历史和预测数据 complete_df = pd.concat([ history_df[['date', 'sales', 'data_type']].assign(product_id=product_id, product_name=product_name), predictions_df ]).sort_values('date') except Exception as e: import traceback print(f"生成预测图表时出错: {e}") traceback.print_exc() forecast_chart = None history_chart = None # 出错时仍然创建预测数据 predictions_df = pd.DataFrame({ 'date': future_dates, 'sales': y_pred, 'data_type': '预测销量', 'product_id': product_id, 'product_name': product_name }) complete_df = predictions_df # 保存预测结果到CSV try: forecast_csv = f'{output_dir}/forecast_{start_date_str}_days{T}.csv' complete_df.to_csv(forecast_csv, index=False) print(f"预测结果已保存到: {forecast_csv}") except Exception as e: print(f"保存CSV文件时出错: {e}") forecast_csv = None # 返回文件路径信息和预测数据 result = { 'predictions_df': complete_df, # 返回包含历史数据的完整DataFrame 'chart_path': forecast_chart, 'history_chart_path': history_chart, 'csv_path': forecast_csv } # 保存模型 model_dir = f'models/transformer' os.makedirs(model_dir, exist_ok=True) model_path = f'{model_dir}/{product_id}_model.pt' # 保存模型和相关数据 checkpoint = { 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scaler_X': scaler_X, 'scaler_y': scaler_y, 'metrics': metrics, 'epochs': epochs, 'look_back': look_back, 'T': T, 'features': features } torch.save(checkpoint, model_path) print(f"模型已保存到 {model_path}") # 保存训练日志 log_path = f'{model_dir}/{product_id}_log.json' log_data = { 'product_id': product_id, 'product_name': product_name, 'model_type': 'transformer', 'training_completed_at': datetime.now().isoformat(), 'epochs': epochs, 'metrics': metrics, 'file_path': model_path } with open(log_path, 'w', encoding='utf-8') as f: json.dump(log_data, f, indent=4, ensure_ascii=False) print(f"训练日志已保存到 {log_path}") return model, metrics # 加载模型并进行预测的函数 def load_model_and_predict(product_id, model_type, future_days=7, start_date=None): """ 加载指定类型的模型并进行未来销量预测 Args: product_id: 产品ID model_type: 模型类型,可选 'mlstm', 'kan', 'transformer' future_days: 预测未来天数,默认7天 start_date: 预测起始日期,格式为'YYYY-MM-DD',默认为None表示使用数据集最后日期的下一天 """ print("\n" + "="*80) print(f"加载模型并预测 - 详细调试信息:") print(f"产品ID: {product_id}, 模型类型: {model_type}, 预测天数: {future_days}, 预测起始日期: {start_date}") print("="*80 + "\n") model_path = f'models/{model_type}/{product_id}_model.pt' if not os.path.exists(model_path): print(f"错误: 未找到产品 {product_id} 的 {model_type} 模型文件") return None # 获取设备 device = get_device() print(f"使用设备: {device} 进行预测") # 加载模型和相关数据 checkpoint = torch.load(model_path, map_location=device, weights_only=False) # 读取原始数据以获取最新的记录 df = pd.read_excel('pharmacy_sales.xlsx') product_df = df[df['product_id'] == product_id].sort_values('date') product_name = product_df['product_name'].iloc[0] # 获取最近的look_back天数据 features = checkpoint['features'] look_back = checkpoint['look_back'] T = checkpoint['T'] scaler_X = checkpoint['scaler_X'] scaler_y = checkpoint['scaler_y'] last_data = product_df[features].values[-look_back:] last_data_scaled = scaler_X.transform(last_data) # 创建模型并加载参数 if model_type == 'mlstm': ModelClass = MLSTMTransformer model_params = { 'num_features': len(features), 'hidden_size': 128, 'mlstm_layers': 1, 'embed_dim': embed_dim, 'dense_dim': dense_dim, 'num_heads': num_heads, 'dropout_rate': dropout_rate, 'num_blocks': num_blocks, 'output_sequence_length': T } elif model_type == 'kan': ModelClass = KANForecaster model_params = { 'input_features': len(features), 'hidden_sizes': [64, 128, 64], 'output_size': 1, 'grid_size': 5, 'spline_order': 3, 'dropout_rate': dropout_rate, 'output_sequence_length': T } elif model_type == 'transformer': ModelClass = TimeSeriesTransformer model_params = { 'num_features': len(features), 'd_model': embed_dim, 'nhead': num_heads, 'num_encoder_layers': num_blocks, 'dim_feedforward': dense_dim, 'dropout': dropout_rate, 'output_sequence_length': T } else: print(f"错误: 不支持的模型类型 '{model_type}'") return None model = ModelClass(**model_params) model.load_state_dict(checkpoint['model_state_dict']) # 将模型移动到设备上 model = model.to(device) model.eval() # 准备输入数据 X_input = torch.Tensor(last_data_scaled).unsqueeze(0) # 添加批次维度 X_input = X_input.to(device) # 移动到设备上 # 进行预测 with torch.no_grad(): # 获取模型默认预测长度 default_pred_length = T print(f"模型默认预测长度: {default_pred_length}天") # 使用模型进行预测 - 如果请求的预测天数小于模型默认值,截断结果 if future_days <= default_pred_length: print(f"请求的预测天数 {future_days} 小于等于模型默认值 {default_pred_length},截取需要的部分") y_pred_scaled = model(X_input).squeeze(0).cpu().numpy()[:future_days] else: # 如果请求的预测天数大于模型默认值,需要多次预测并拼接结果 print(f"请求的预测天数 {future_days} 大于模型默认值 {default_pred_length},需要多次预测") y_pred_scaled = model(X_input).squeeze(0).cpu().numpy() # 只取默认预测长度的结果 y_pred_scaled = y_pred_scaled[:min(future_days, default_pred_length)] # 反归一化预测结果 y_pred = scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten() # 创建预测日期范围 last_date = product_df['date'].iloc[-1] if start_date: try: # 使用用户指定的日期作为预测起点 start_date_obj = pd.Timestamp(start_date) print(f"成功解析用户指定的预测起始日期: {start_date_obj.strftime('%Y-%m-%d')}") except Exception as e: # 如果日期格式无效,使用当前日期 start_date_obj = pd.Timestamp.now().normalize() print(f"日期解析错误: {e}") print(f"使用当前日期 {start_date_obj.strftime('%Y-%m-%d')} 作为预测起点") else: # 如果未指定日期,使用数据集最后日期的下一天 start_date_obj = last_date + pd.Timedelta(days=1) print(f"未指定起始日期,使用数据集最后日期 {last_date.strftime('%Y-%m-%d')} 的下一天作为预测起点: {start_date_obj.strftime('%Y-%m-%d')}") future_dates = pd.date_range(start=start_date_obj, periods=future_days, freq='D') print(f"生成预测日期范围: {future_dates[0]} 到 {future_dates[-1]}, 共 {len(future_dates)} 天") # 创建预测结果目录 output_dir = f'predictions/{model_type}/{product_id}' os.makedirs(output_dir, exist_ok=True) # 可视化预测结果 try: # 1. 创建预测图表 forecast_fig, forecast_ax = plt.subplots(figsize=(12, 6)) # 显示历史数据和预测数据 history_days = 30 # 显示最近30天的历史数据 # 只选择预测起始日期之前30天的历史数据,而不是全部历史数据 history_end_date = start_date_obj - pd.Timedelta(days=1) # 预测起始日期的前一天 history_start_date = history_end_date - pd.Timedelta(days=history_days) # 向前推30天 # 过滤历史数据,只保留这个日期范围内的数据 history_df = product_df[(product_df['date'] >= history_start_date) & (product_df['date'] <= history_end_date)][['date', 'sales']].copy() if history_df.empty: print(f"警告: 在日期范围 {history_start_date} 到 {history_end_date} 内没有历史数据") # 如果没有符合条件的历史数据,就使用最近的数据 history_df = product_df.iloc[-min(history_days, len(product_df)):][['date', 'sales']].copy() print(f"历史数据日期范围: {history_df['date'].min()} 到 {history_df['date'].max()}") print(f"预测数据日期范围: {future_dates.min()} 到 {future_dates.max()}") print(f"预测起始日期: {start_date_obj.strftime('%Y-%m-%d')}") # 绘制历史数据 forecast_ax.plot(history_df['date'].values, history_df['sales'].values, 'b-', label='历史销量') # 绘制预测数据,确保使用future_dates作为x轴 forecast_ax.plot(future_dates, y_pred, 'r--', label=f'{model_type}预测销量') # 强制X轴从预测起始日期的前30天开始(如果有历史数据)到预测结束日期 date_min = start_date_obj - pd.Timedelta(days=30) date_max = future_dates.max() + pd.Timedelta(days=2) print(f"设置X轴范围: {date_min} 到 {date_max}") forecast_ax.set_xlim(date_min, date_max) forecast_ax.set_title(f'{product_name} - {model_type}销量预测 (从{start_date_obj.strftime("%Y-%m-%d")}开始,预测{future_days}天)') forecast_ax.set_xlabel('日期') forecast_ax.set_ylabel('销量') forecast_ax.legend() forecast_ax.grid(True) plt.xticks(rotation=45) plt.tight_layout() # 强制重新绘制图表 forecast_fig.canvas.draw() # 将预测起始日期和预测时长添加到文件名中 start_date_str = start_date_obj.strftime('%Y%m%d') # 保存预测图表 forecast_chart = f'{output_dir}/forecast_{start_date_str}_days{future_days}.png' plt.savefig(forecast_chart) print(f"预测图表已保存为: {forecast_chart}") # 关闭图表以释放内存 plt.close(forecast_fig) # 2. 创建历史趋势图表 try: print("\n开始生成历史趋势图...") history_fig, history_ax = plt.subplots(figsize=(12, 6)) # 获取预测起始日期 current_year = start_date_obj.year current_month = start_date_obj.month current_day = start_date_obj.day print(f"预测起始日期: {start_date_obj}") # 计算同期日期范围(前3天和后3天,共7天) days_before = 3 days_after = 3 date_range_start = start_date_obj - pd.Timedelta(days=days_before) date_range_end = start_date_obj + pd.Timedelta(days=days_after) # 计算去年同期日期范围 last_year_start = date_range_start.replace(year=date_range_start.year-1) last_year_end = date_range_end.replace(year=date_range_end.year-1) # 计算上月同期日期范围 if date_range_start.month > 1: last_month_start = date_range_start.replace(month=date_range_start.month-1) last_month_end = date_range_end.replace(month=date_range_end.month-1) else: # 如果是1月,则转到上一年的12月 last_month_start = date_range_start.replace(year=date_range_start.year-1, month=12) last_month_end = date_range_end.replace(year=date_range_end.year-1, month=12) print(f"当前日期范围: {date_range_start} 到 {date_range_end}") print(f"去年同期范围: {last_year_start} 到 {last_year_end}") print(f"上月同期范围: {last_month_start} 到 {last_month_end}") # 查找对应日期范围的数据 current_period_data = product_df[ (product_df['date'] >= date_range_start) & (product_df['date'] <= date_range_end) ] print(f"当前期间数据点数: {len(current_period_data)}") last_year_period_data = product_df[ (product_df['date'] >= last_year_start) & (product_df['date'] <= last_year_end) ] print(f"去年同期数据点数: {len(last_year_period_data)}") last_month_period_data = product_df[ (product_df['date'] >= last_month_start) & (product_df['date'] <= last_month_end) ] print(f"上月同期数据点数: {len(last_month_period_data)}") # 绘制曲线图 has_data = False if not current_period_data.empty: has_data = True # 确保日期升序排序 current_period_data = current_period_data.sort_values('date') # 生成相对天数(以date_range_start为基准) current_period_data['day_offset'] = (current_period_data['date'] - date_range_start).dt.days print(f"当前期间日期: {current_period_data['date'].tolist()}") print(f"当前期间相对天数: {current_period_data['day_offset'].tolist()}") print(f"当前期间销量: {current_period_data['sales'].tolist()}") history_ax.plot( current_period_data['day_offset'], current_period_data['sales'], 'r-', marker='o', linewidth=2, label=f"当前期间 ({date_range_start.strftime('%Y-%m-%d')} 到 {date_range_end.strftime('%Y-%m-%d')})" ) # 标记预测起始日期 current_center_point = current_period_data[current_period_data['date'] == start_date_obj] if not current_center_point.empty: history_ax.scatter( current_center_point['day_offset'], current_center_point['sales'], color='red', s=100, marker='*', zorder=10, label=f"预测起始日 ({start_date_obj.strftime('%Y-%m-%d')})" ) if not last_year_period_data.empty: has_data = True # 确保日期升序排序 last_year_period_data = last_year_period_data.sort_values('date') last_year_period_data['day_offset'] = (last_year_period_data['date'] - last_year_start).dt.days print(f"去年同期日期: {last_year_period_data['date'].tolist()}") print(f"去年同期相对天数: {last_year_period_data['day_offset'].tolist()}") print(f"去年同期销量: {last_year_period_data['sales'].tolist()}") history_ax.plot( last_year_period_data['day_offset'], last_year_period_data['sales'], 'b-', marker='s', linewidth=2, label=f"去年同期 ({last_year_start.strftime('%Y-%m-%d')} 到 {last_year_end.strftime('%Y-%m-%d')})" ) if not last_month_period_data.empty: has_data = True # 确保日期升序排序 last_month_period_data = last_month_period_data.sort_values('date') last_month_period_data['day_offset'] = (last_month_period_data['date'] - last_month_start).dt.days print(f"上月同期日期: {last_month_period_data['date'].tolist()}") print(f"上月同期相对天数: {last_month_period_data['day_offset'].tolist()}") print(f"上月同期销量: {last_month_period_data['sales'].tolist()}") history_ax.plot( last_month_period_data['day_offset'], last_month_period_data['sales'], 'g-', marker='^', linewidth=2, label=f"上月同期 ({last_month_start.strftime('%Y-%m-%d')} 到 {last_month_end.strftime('%Y-%m-%d')})" ) # 设置X轴标签为相对天数 days_labels = list(range(7)) days_offsets = list(range(7)) day_names = [(date_range_start + pd.Timedelta(days=d)).strftime('%m-%d') for d in range(7)] history_ax.set_xticks(days_offsets) history_ax.set_xticklabels(day_names) # 添加垂直参考线标记预测起始日 history_ax.axvline(x=days_before, color='gray', linestyle='--', alpha=0.5) # 美化图表 history_ax.set_title(f'{product_name} - 同期销量趋势对比 (7天)') history_ax.set_xlabel('日期') history_ax.set_ylabel('销量') history_ax.grid(True, linestyle='--', alpha=0.7) history_ax.legend(loc='best') # 如果所有数据集都为空,显示提示 if not has_data: history_ax.text(0.5, 0.5, '没有找到可比较的同期数据', horizontalalignment='center', verticalalignment='center', transform=history_ax.transAxes, fontsize=14) plt.tight_layout() # 强制重新绘制图表 history_fig.canvas.draw() # 保存历史趋势图表 history_chart = f'{output_dir}/history_{start_date_str}.png' plt.savefig(history_chart) print(f"历史趋势图表已保存为: {history_chart}") # 关闭图表以释放内存 plt.close(history_fig) except Exception as e: import traceback print(f"生成历史趋势图时出错: {e}") traceback.print_exc() history_chart = None # 创建一个包含历史和预测数据的完整DataFrame供CSV导出和API返回 history_df['data_type'] = '历史销量' predictions_df = pd.DataFrame({ 'date': future_dates, 'sales': y_pred, 'data_type': '预测销量', 'product_id': product_id, 'product_name': product_name }) # 合并历史和预测数据 complete_df = pd.concat([ history_df[['date', 'sales', 'data_type']].assign(product_id=product_id, product_name=product_name), predictions_df ]).sort_values('date') except Exception as e: import traceback print(f"生成预测图表时出错: {e}") traceback.print_exc() forecast_chart = None history_chart = None # 出错时仍然创建预测数据 predictions_df = pd.DataFrame({ 'date': future_dates, 'sales': y_pred, 'data_type': '预测销量', 'product_id': product_id, 'product_name': product_name }) complete_df = predictions_df # 保存预测结果到CSV try: forecast_csv = f'{output_dir}/forecast_{start_date_str}_days{future_days}.csv' complete_df.to_csv(forecast_csv, index=False) print(f"预测结果已保存到: {forecast_csv}") except Exception as e: print(f"保存CSV文件时出错: {e}") forecast_csv = None # 返回文件路径信息和预测数据 result = { 'predictions_df': complete_df, # 返回包含历史数据的完整DataFrame 'chart_path': forecast_chart, 'history_chart_path': history_chart, 'csv_path': forecast_csv } return result # 特定加载KAN模型并预测的函数 def load_kan_model_and_predict(product_id, future_days=7): """ 加载KAN模型并进行未来销量预测,是load_model_and_predict的简化版本,固定模型类型为'kan' Args: product_id: 产品ID future_days: 预测未来天数,默认7天 Returns: 预测结果字典 """ return load_model_and_predict(product_id, model_type='kan', future_days=future_days) if __name__ == "__main__": # 首先生成测试数据 try: print("正在检查是否存在模拟数据...") df = pd.read_excel('pharmacy_sales.xlsx') print("发现现有数据,跳过数据生成步骤。") except: print("未找到数据,正在生成模拟数据...") import generate_pharmacy_data print("数据生成完成!") # 读取数据获取所有产品ID df = pd.read_excel('pharmacy_sales.xlsx') product_ids = df['product_id'].unique() # 为每个产品训练一个模型 all_metrics = {} for product_id in product_ids: print(f"\n{'='*50}") print(f"开始训练产品 {product_id} 的模型") print(f"{'='*50}") _, metrics = train_product_model(product_id, epochs=epochs) all_metrics[product_id] = metrics # 输出所有产品的评估指标 print("\n所有产品模型评估结果汇总:") for product_id, metrics in all_metrics.items(): product_name = df[df['product_id'] == product_id]['product_name'].iloc[0] print(f"\n{product_name} (ID: {product_id}):") for metric, value in metrics.items(): print(f" {metric}: {value:.4f}") print("\n模型训练和评估完成!")