**日期**: 2025-07-18 **主题**: 模型保存逻辑重构与集中化管理 ### 目标 根据 `xz训练模型保存规则.md`,将系统中分散的模型文件保存逻辑统一重构,创建一个集中、健壮且可测试的路径管理系统。 ### 核心成果 1. **创建了 `server/utils/file_save.py` 模块**: 这个新模块现在是系统中处理模型文件保存路径的唯一权威来源。 2. **实现了三种训练模式的路径生成**: 系统现在可以为“按店铺”、“按药品”和“全局”三种训练模式正确生成层级化的、可追溯的目录结构。 3. **集成了智能ID处理**: * 对于包含**多个ID**的训练场景,系统会自动计算一个简短的哈希值作为目录名。 * 对于全局训练中只包含**单个店铺或药品ID**的场景,系统会直接使用该ID作为目录名,增强了路径的可读性。 4. **重构了整个训练流程**: 修改了API层、进程管理层以及所有模型训练器,使它们能够协同使用新的路径管理模块。 5. **添加了自动化测试**: 创建了 `test/test_file_save_logic.py` 脚本,用于验证所有路径生成和版本管理逻辑的正确性。 ### 详细文件修改记录 1. **`server/utils/file_save.py`** * **操作**: 创建 * **内容**: 实现了 `ModelPathManager` 类,包含以下核心方法: * `_hash_ids`: 对ID列表进行排序和哈希。 * `_generate_identifier`: 根据训练模式和参数生成唯一的模型标识符。 * `get_next_version` / `save_version_info`: 线程安全地管理 `versions.json` 文件,实现版本号的获取和更新。 * `get_model_paths`: 作为主入口,协调以上方法,生成包含所有产物路径的字典。 2. **`server/api.py`** * **操作**: 修改 * **位置**: `start_training` 函数 (`/api/training` 端点)。 * **内容**: * 导入并实例化 `ModelPathManager`。 * 在接收到训练请求后,调用 `path_manager.get_model_paths()` 来获取所有路径信息。 * 将获取到的 `path_info` 字典和原始请求参数 `training_params` 一并传递给后台训练任务管理器。 * 修复了因重复传递关键字参数 (`model_type`, `training_mode`) 导致的 `TypeError`。 * 修复了 `except` 块中因未导入 `traceback` 模块导致的 `UnboundLocalError`。 3. **`server/utils/training_process_manager.py`** * **操作**: 修改 * **内容**: * 修改 `submit_task` 方法,使其能接收 `training_params` 和 `path_info` 字典。 * 在 `TrainingTask` 数据类中增加了 `path_info` 字段来存储路径信息。 * 在 `TrainingWorker` 中,将 `path_info` 传递给实际的训练函数。 * 在 `_monitor_results` 方法中,当任务成功完成时,调用 `path_manager.save_version_info` 来更新 `versions.json`,完成版本管理的闭环。 4. **所有训练器文件** (`mlstm_trainer.py`, `kan_trainer.py`, `tcn_trainer.py`, `transformer_trainer.py`) * **操作**: 修改 * **内容**: * 统一修改了主训练函数的签名,增加了 `path_info=None` 参数。 * 移除了所有内部手动构建文件路径的逻辑。 * 所有保存操作(最终模型、检查点、损失曲线图)现在都直接从传入的 `path_info` 字典中获取预先生成好的路径。 * 简化了 `save_checkpoint` 辅助函数,使其也依赖 `path_info`。 5. **`test/test_file_save_logic.py`** * **操作**: 创建 * **内容**: * 编写了一个独立的测试脚本,用于验证 `ModelPathManager` 的所有功能。 * 覆盖了所有训练模式及其子场景(包括单ID和多ID哈希)。 * 测试了版本号的正确递增和 `versions.json` 的写入。 * 修复了测试脚本中因绝对/相对路径不匹配和重复关键字参数导致的多个 `AssertionError` 和 `TypeError`。 --- **日期**: 2025-07-18 (后续修复) **主题**: 修复API层调用路径管理器时的 `TypeError` ### 问题描述 在完成所有重构和测试后,实际运行API时,`POST /api/training` 端点在调用 `path_manager.get_model_paths` 时崩溃,并抛出 `TypeError: get_model_paths() got multiple values for keyword argument 'training_mode'`。 ### 根本原因 这是一个回归错误。在修复测试脚本 `test_file_save_logic.py` 中的类似问题时,我未能将相同的修复逻辑应用回 `server/api.py`。代码在调用 `get_model_paths` 时,既通过关键字参数 `training_mode=...` 明确传递了该参数,又通过 `**data` 将其再次传入,导致了冲突。 ### 解决方案 1. **文件**: `server/api.py` 2. **位置**: `start_training` 函数。 3. **操作**: 修改了对 `get_model_paths` 的调用逻辑。 4. **内容**: ```python # 移除 model_type 和 training_mode 以避免重复关键字参数错误 data_for_path = data.copy() data_for_path.pop('model_type', None) data_for_path.pop('training_mode', None) path_info = path_manager.get_model_paths( training_mode=training_mode, model_type=model_type, **data_for_path # 传递剩余的payload ) ``` 5. **原因**: 在通过 `**` 解包传递参数之前,先从字典副本中移除了所有会被明确指定的关键字参数,从而确保了函数调用签名的正确性。 --- **日期**: 2025-07-18 (最终修复) **主题**: 修复因中间层函数签名未更新导致的 `TypeError` ### 问题描述 在完成所有重构后,实际运行API并触发训练任务时,程序在后台进程中因 `TypeError: train_model() got an unexpected keyword argument 'path_info'` 而崩溃。 ### 根本原因 这是一个典型的“中间人”遗漏错误。我成功地修改了调用链的两端(`api.py` -> `training_process_manager.py` 和 `*_trainer.py`),但忘记了修改它们之间的中间层——`server/core/predictor.py` 中的 `train_model` 方法。`training_process_manager` 尝试将 `path_info` 传递给 `predictor.train_model`,但后者的函数签名中并未包含这个新参数,导致了 `TypeError`。 ### 解决方案 1. **文件**: `server/core/predictor.py` 2. **位置**: `train_model` 函数的定义处。 3. **操作**: 在函数签名中增加了 `path_info=None` 参数。 4. **内容**: ```python def train_model(self, ..., progress_callback=None, path_info=None): # ... ``` 5. **位置**: `train_model` 函数内部,对所有具体训练器(`train_product_model_with_mlstm`, `_with_kan`, etc.)的调用处。 6. **操作**: 在所有调用中,将接收到的 `path_info` 参数透传下去。 7. **内容**: ```python # ... metrics = train_product_model_with_transformer( ..., path_info=path_info ) # ... ``` 8. **原因**: 通过在中间层函数上“打通”`path_info` 参数的传递通道,确保了从API层到最终训练器层的完整数据流,解决了 `TypeError`。 --- **日期**: 2025-07-18 (最终修复) **主题**: 修复“按药品训练-聚合所有店铺”模式下的路径生成错误 ### 问题描述 在实际运行中发现,当进行“按药品训练”并选择“聚合所有店铺”时,生成的模型保存路径中包含了错误的后缀 `_None`,而不是预期的 `_all` (例如 `.../17002608_None/...`)。 ### 根本原因 在 `server/utils/file_save.py` 的 `_generate_identifier` 和 `get_model_paths` 方法中,当 `store_id` 从前端传来为 `None` 时,代码 `scope = store_id if store_id else 'all'` 会因为 `store_id` 是 `None` 而正确地将 `scope` 设为 `'all'`。然而,在 `get_model_paths` 方法中,我错误地使用了 `kwargs.get('store_id', 'all')`,这在 `store_id` 键存在但值为 `None` 时,仍然会返回 `None`,导致了路径拼接错误。 ### 解决方案 1. **文件**: `server/utils/file_save.py` 2. **位置**: `_generate_identifier` 和 `get_model_paths` 方法中处理 `product` 训练模式的部分。 3. **操作**: 将逻辑从 `scope = kwargs.get('store_id', 'all')` 修改为更严谨的 `scope = store_id if store_id is not None else 'all'`。 4. **内容**: ```python # in _generate_identifier scope = store_id if store_id is not None else 'all' # in get_model_paths store_id = kwargs.get('store_id') scope = store_id if store_id is not None else 'all' scope_folder = f"{product_id}_{scope}" ``` 5. **原因**: 这种写法能正确处理 `store_id` 键不存在、或键存在但值为 `None` 的两种情况,确保在这两种情况下 `scope` 都被正确地设置为 `'all'`,从而生成符合规范的路径。 --- **日期**: 2025-07-18 (最终修复) **主题**: 修复 `KeyError: 'price'` 和单ID哈希错误 ### 问题描述 在完成大规模重构后,实际运行时发现了两个隐藏的bug: 1. 在“按店铺训练”模式下,训练因 `KeyError: 'price'` 而失败。 2. 在“按店铺训练”模式下,当只选择一个“指定药品”时,系统仍然错误地对该药品的ID进行了哈希处理,而不是直接使用ID。 ### 根本原因 1. **`KeyError`**: `server/utils/multi_store_data_utils.py` 中的 `get_store_product_sales_data` 函数包含了一个硬编码的列校验,该校验要求 `price` 列必须存在,但这与当前的数据源不符。 2. **哈希错误**: `server/utils/file_save.py` 中的 `get_model_paths` 方法在处理 `store` 训练模式时,没有复用 `_generate_identifier` 中已经写好的单ID判断逻辑,导致了逻辑不一致。 ### 解决方案 1. **修复 `KeyError`**: * **文件**: `server/utils/multi_store_data_utils.py` * **位置**: `get_store_product_sales_data` 函数。 * **操作**: 从 `required_columns` 列表中移除了 `'price'`,根除了这个硬性依赖。 2. **修复哈希逻辑**: * **文件**: `server/utils/file_save.py` * **位置**: `_generate_identifier` 和 `get_model_paths` 方法中处理 `store` 训练模式的部分。 * **操作**: 统一了逻辑,确保在这两个地方都使用了 `scope = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids)` 的判断,从而在只选择一个药品时直接使用其ID。 3. **更新测试**: * **文件**: `test/test_file_save_logic.py` * **操作**: 增加了新的测试用例,专门验证“按店铺训练-单个指定药品”场景下的路径生成是否正确。 --- **日期**: 2025-07-18 (最终修复) **主题**: 修复全局训练范围值不匹配导致的 `ValueError` ### 问题描述 在完成所有重构后,实际运行API并触发“全局训练-所有店铺所有药品”时,程序因 `ValueError: 未知的全局训练范围: all_stores_all_products` 而崩溃。 ### 根本原因 前端传递的 `training_scope` 值为 `all_stores_all_products`,而 `server/utils/file_save.py` 中的 `_generate_identifier` 和 `get_model_paths` 方法只处理了 `all` 这个值,未能兼容前端传递的具体字符串,导致逻辑判断失败。 ### 解决方案 1. **文件**: `server/utils/file_save.py` 2. **位置**: `_generate_identifier` 和 `get_model_paths` 方法中处理 `global` 训练模式的部分。 3. **操作**: 将逻辑判断从 `if training_scope == 'all':` 修改为 `if training_scope in ['all', 'all_stores_all_products']:`。 4. **原因**: 使代码能够同时兼容两种表示“所有范围”的字符串,确保了前端请求的正确处理。 5. **更新测试**: * **文件**: `test/test_file_save_logic.py` * **操作**: 增加了新的测试用例,专门验证 `training_scope` 为 `all_stores_all_products` 时的路径生成是否正确。 --- **日期**: 2025-07-18 (最终优化) **主题**: 优化全局训练自定义模式下的单ID路径生成 ### 问题描述 根据用户反馈,希望在全局训练的“自定义范围”模式下,如果只选择单个店铺和/或单个药品,路径中应直接使用ID而不是哈希值,以增强可读性。 ### 解决方案 1. **文件**: `server/utils/file_save.py` 2. **位置**: `_generate_identifier` 和 `get_model_paths` 方法中处理 `global` 训练模式 `custom` 范围的部分。 3. **操作**: 为 `store_ids` 和 `product_ids` 分别增加了单ID判断逻辑。 4. **内容**: ```python # in _generate_identifier s_id = store_ids[0] if len(store_ids) == 1 else self._hash_ids(store_ids) p_id = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids) scope_part = f"custom_s_{s_id}_p_{p_id}" # in get_model_paths store_ids = kwargs.get('store_ids', []) product_ids = kwargs.get('product_ids', []) s_id = store_ids[0] if len(store_ids) == 1 else self._hash_ids(store_ids) p_id = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids) scope_parts.extend(['custom', s_id, p_id]) ``` 5. **原因**: 使 `custom` 模式下的路径生成逻辑与 `selected_stores` 和 `selected_products` 模式保持一致,在只选择一个ID时优先使用ID本身,提高了路径的可读性和一致性。 6. **更新测试**: * **文件**: `test/test_file_save_logic.py` * **操作**: 增加了新的测试用例,专门验证“全局训练-自定义范围-单ID”场景下的路径生成是否正确。
62 KiB
根目录启动
1:uv venv
2:uv pip install loguru numpy pandas torch matplotlib flask flask_cors flask_socketio flasgger scikit-learn tqdm pytorch_tcn pyarrow
3: uv run .\server\api.py
UI
1:npm install
npm run dev
“预测分析”模块UI重构修改记录
任务目标: 将原有的、通过下拉菜单切换模式的单一预测页面,重构为通过左侧子导航切换模式的多页面布局,使其UI结构与“模型训练”模块保持一致。
后端修复 (2025-07-13)
任务目标: 解决模型训练时因数据文件路径错误导致的数据加载失败问题。
- 核心问题:
server/core/predictor.py
中的PharmacyPredictor
类初始化时,硬编码了错误的默认数据文件路径 ('pharmacy_sales_multi_store.csv'
)。 - 修复方案:
- 修改
server/core/predictor.py
,将默认数据路径更正为'data/timeseries_training_data_sample_10s50p.parquet'
。 - 同步更新了
server/trainers/mlstm_trainer.py
中所有对数据加载函数的调用,确保使用正确的文件路径。
- 修改
- 结果: 彻底解决了在独立训练进程中数据加载失败的问题。
后端修复 (2025-07-13) - 数据流重构
任务目标: 解决因数据处理流程中断导致 sales
和 price
关键特征丢失,从而引发模型训练失败的根本问题。
-
核心问题:
server/core/predictor.py
中的train_model
方法在调用训练器(如train_product_model_with_mlstm
)时,没有将预处理好的数据传递过去。server/trainers/mlstm_trainer.py
因此被迫重新加载和处理数据,但其使用的数据标准化函数standardize_column_names
存在逻辑缺陷,导致关键列丢失。
-
修复方案 (数据流重构):
- 修改
server/trainers/mlstm_trainer.py
:- 重构
train_product_model_with_mlstm
函数,使其能够接收一个预处理好的 DataFrame (product_df
) 作为参数。 - 移除了函数内部所有的数据加载和重复处理逻辑。
- 重构
- 修改
server/core/predictor.py
:- 在
train_model
方法中,将已经加载并处理好的product_data
作为参数,显式传递给train_product_model_with_mlstm
函数。
- 在
- 修改
server/utils/multi_store_data_utils.py
:- 在
standardize_column_names
函数中,使用 Pandas 的rename
方法强制进行列名转换,确保quantity_sold
和unit_price
被可靠地重命名为sales
和price
。
- 在
- 修改
-
结果: 彻底修复了数据处理流程,确保数据只被加载和标准化一次,并被正确传递,从根本上解决了模型训练失败的问题。
第一次重构 (多页面、双栏布局)
- 新增文件:
UI/src/views/prediction/ProductPredictionView.vue
UI/src/views/prediction/StorePredictionView.vue
UI/src/views/prediction/GlobalPredictionView.vue
- 修改文件:
UI/src/router/index.js
: 添加了指向新页面的路由。UI/src/App.vue
: 将“预测分析”修改为包含三个子菜单的父菜单。
第二次重构 (基于用户反馈的单页面布局)
任务目标: 统一三个预测子页面的布局,采用旧的单页面预测样式,并将导航功能与页面内容解耦。
- 修改文件:
UI/src/views/prediction/ProductPredictionView.vue
:- 内容: 使用
UI/src/views/NewPredictionView.vue
的布局进行替换。 - 逻辑: 移除了“模型训练方式”选择器,并将该页面的预测模式硬编码为
product
。
- 内容: 使用
UI/src/views/prediction/StorePredictionView.vue
:- 内容: 使用
UI/src/views/NewPredictionView.vue
的布局进行替换。 - 逻辑: 移除了“模型训练方式”选择器,并将该页面的预测模式硬编码为
store
。
- 内容: 使用
UI/src/views/prediction/GlobalPredictionView.vue
:- 内容: 使用
UI/src/views/NewPredictionView.vue
的布局进行替换。 - 逻辑: 移除了“模型训练方式”及特定目标选择器,并将该页面的预测模式硬编码为
global
。
- 内容: 使用
总结: 通过两次重构,最终实现了使用左侧导航栏切换预测模式,同时右侧内容区域保持统一、简洁的单页面布局,完全符合用户的最终要求。
按药品训练修改
日期: 2025-07-14
文件: server/trainers/mlstm_trainer.py
问题: 模型训练因 KeyError: "['sales', 'price'] not in index"
失败。
分析:
'price'
列在提供的数据中不存在,导致KeyError
。'sales'
列作为历史输入(自回归特征)对于模型训练是必要的。 解决方案: 从mlstm_trainer
的特征列表中移除了不存在的'price'
列,保留了'sales'
列用于自回归。
日期: 2025-07-14 (补充) 文件:
server/trainers/transformer_trainer.py
server/trainers/tcn_trainer.py
server/trainers/kan_trainer.py
问题: 预防性修复。这些文件存在与mlstm_trainer.py
相同的KeyError
隐患。 分析: 经过检查,这些训练器与mlstm_trainer
共享相同的数据处理逻辑,其硬编码的特征列表中都包含了不存在的'price'
列。 解决方案: 统一从所有相关训练器的特征列表中移除了'price'
列,以确保所有模型训练的健壮性。
日期: 2025-07-14 (深度修复)
文件: server/utils/multi_store_data_utils.py
问题: 追踪 KeyError: "['sales'] not in index"
时,发现数据标准化流程存在多个问题。
分析:
- 通过
uv run
读取了.parquet
数据文件,确认了原始列名。 - 发现
standardize_column_names
函数中的重命名映射与原始列名不匹配 (例如quantity_sold
vssales_quantity
)。 - 确认了原始数据中没有
price
列,但代码中存在对它的依赖。 - 函数缺乏一个明确的返回列选择机制,导致
sales
列在数据准备阶段被意外丢弃。 解决方案: - 修正了
rename_map
以正确匹配原始数据列名 (sales_quantity
->sales
,temperature_2m_mean
->temperature
,dayofweek
->weekday
)。 - 移除了对不存在的
price
列的依赖。 - 在函数末尾添加了逻辑,确保返回的
DataFrame
包含所有模型训练所需的标准列(特征 + 目标),保证了数据流的稳定性。 - 原始数据列名:['date', 'store_id', 'product_id', 'sales_quantity', 'sales_amount', 'gross_profit', 'customer_traffic', 'store_name', 'city', 'product_name', 'manufacturer', 'category_l1', 'category_l2', 'category_l3', 'abc_category', 'temperature_2m_mean', 'temperature_2m_max', 'temperature_2m_min', 'year', 'month', 'day', 'dayofweek', 'dayofyear', 'weekofyear', 'is_weekend', 'sl_lag_7', 'sl_lag_14', 'sl_rolling_mean_7', 'sl_rolling_std_7', 'sl_rolling_mean_14', 'sl_rolling_std_14']
日期: 2025-07-14 10:16
主题: 修复模型训练中的 KeyError
及数据流问题 (详细版)
阶段一:修复训练器层 KeyError
- 问题: 模型训练因
KeyError: "['sales', 'price'] not in index"
失败。 - 分析: 训练器硬编码的特征列表中包含了数据源中不存在的
'price'
列。 - 涉及文件:
server/trainers/mlstm_trainer.py
server/trainers/transformer_trainer.py
server/trainers/tcn_trainer.py
server/trainers/kan_trainer.py
- 修改详情:
- 位置: 每个训练器文件中的
features
列表定义处。 - 操作: 修改。
- 内容:
- features = ['sales', 'price', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature'] + features = ['sales', 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature']
- 原因: 移除对不存在的
'price'
列的依赖,解决KeyError
。
- 位置: 每个训练器文件中的
阶段二:修复数据标准化层
- 问题: 修复后出现新错误
KeyError: "['sales'] not in index"
,表明数据标准化流程存在缺陷。 - 分析: 通过
uv run
读取.parquet
文件确认,standardize_column_names
函数中的列名映射错误,且缺少最终列选择机制。 - 涉及文件:
server/utils/multi_store_data_utils.py
- 修改详情:
- 位置:
standardize_column_names
函数,rename_map
字典。- 操作: 修改。
- 内容:
- rename_map = { 'quantity_sold': 'sales', 'unit_price': 'price', 'day_of_week': 'weekday' } + rename_map = { 'sales_quantity': 'sales', 'temperature_2m_mean': 'temperature', 'dayofweek': 'weekday' }
- 原因: 修正键名以匹配数据源的真实列名 (
sales_quantity
,temperature_2m_mean
,dayofweek
)。
- 位置:
standardize_column_names
函数,sales_amount
计算部分。- 操作: 修改 (注释)。
- 内容:
- if 'sales_amount' not in df.columns and 'sales' in df.columns and 'price' in df.columns: - df['sales_amount'] = df['sales'] * df['price'] + # 由于没有price列,sales_amount的计算逻辑需要调整或移除 + # if 'sales_amount' not in df.columns and 'sales' in df.columns and 'price' in df.columns: + # df['sales_amount'] = df['sales'] * df['price']
- 原因: 避免因缺少
'price'
列而导致潜在错误。
- 位置:
standardize_column_names
函数,numeric_columns
列表。- 操作: 删除。
- 内容:
- numeric_columns = ['sales', 'price', 'sales_amount', 'weekday', 'month', 'temperature'] + numeric_columns = ['sales', 'sales_amount', 'weekday', 'month', 'temperature']
- 原因: 从数值类型转换列表中移除不存在的
'price'
列。
- 位置:
standardize_column_names
函数,return
语句前。- 操作: 增加。
- 内容:
+ # 定义模型训练所需的所有列(特征 + 目标) + final_columns = [ + 'date', 'sales', 'product_id', 'product_name', 'store_id', 'store_name', + 'weekday', 'month', 'is_holiday', 'is_weekend', 'is_promotion', 'temperature' + ] + # 筛选出DataFrame中实际存在的列 + existing_columns = [col for col in final_columns if col in df.columns] + # 返回只包含这些必需列的DataFrame + return df[existing_columns]
- 原因: 增加列选择机制,确保函数返回的
DataFrame
结构统一且包含sales
列,从根源上解决KeyError: "['sales'] not in index"
。
- 位置:
阶段三:修复数据流分发层
- 问题:
predictor.py
未将处理好的数据统一传递给所有训练器。 - 分析:
train_model
方法中,只有mlstm
的调用传递了product_df
,其他模型则没有,导致它们重新加载未处理的数据。 - 涉及文件:
server/core/predictor.py
- 修改详情:
- 位置:
train_model
方法中对train_product_model_with_transformer
,_tcn
,_kan
的调用处。 - 操作: 增加。
- 内容: 在函数调用中增加了
product_df=product_data
参数。
(对- model_result, metrics, actual_version = train_product_model_with_transformer(product_id, ...) + model_result, metrics, actual_version = train_product_model_with_transformer(product_id=product_id, product_df=product_data, ...)
tcn
和kan
的调用也做了类似修改) - 原因: 统一数据流,确保所有训练器都使用经过正确预处理的、包含完整信息的
DataFrame
。
- 位置:
阶段四:适配训练器以接收数据
- 问题:
transformer
,tcn
,kan
训练器需要能接收上游传来的数据。 - 分析: 需要修改这三个训练器的函数签名和内部逻辑,使其在接收到
product_df
时跳过数据加载。 - 涉及文件:
server/trainers/transformer_trainer.py
,tcn_trainer.py
,kan_trainer.py
- 修改详情:
- 位置: 每个训练器主函数的定义处。
- 操作: 增加。
- 内容: 在函数参数中增加了
product_df=None
。- def train_product_model_with_transformer(product_id, ...) + def train_product_model_with_transformer(product_id, product_df=None, ...)
- 位置: 每个训练器内部的数据加载逻辑处。
- 操作: 增加。
- 内容: 增加了
if product_df is None:
的判断逻辑,只有在未接收到数据时才执行内部加载。+ if product_df is None: - # 根据训练模式加载数据 - from utils.multi_store_data_utils import load_multi_store_data - ... + # [原有的数据加载逻辑] + else: + # 如果传入了product_df,直接使用 + ...
- 原因: 完成数据流修复的最后一环,使训练器能够灵活地接收外部数据或自行加载,彻底解决问题。
- 位置: 每个训练器主函数的定义处。
日期: 2025-07-14 10:38 主题: 修复因NumPy类型导致的JSON序列化失败问题
阶段五:修复前后端通信层
- 问题: 模型训练成功后,后端向前端发送包含训练指标(metrics)的WebSocket消息或API响应时失败,导致前端状态无法更新为“已完成”。
- 日志错误:
Object of type float32 is not JSON serializable
- 分析: 训练过程产生的评估指标(如
mse
,rmse
)是NumPy的float32
类型。Python标准的json
库无法直接序列化这种类型,导致在通过WebSocket或HTTP API发送数据时出错。 - 涉及文件:
server/utils/training_process_manager.py
- 修改详情:
- 位置: 文件顶部。
- 操作: 增加。
- 内容:
import numpy as np def convert_numpy_types(obj): """递归地将字典/列表中的NumPy类型转换为Python原生类型""" if isinstance(obj, dict): return {k: convert_numpy_types(v) for k, v in obj.items()} elif isinstance(obj, list): return [convert_numpy_types(i) for i in obj] elif isinstance(obj, np.generic): return obj.item() return obj
- 原因: 添加一个通用的辅助函数,用于将包含NumPy类型的数据结构转换为JSON兼容的格式。
- 位置:
_monitor_results
方法内部,调用self.websocket_callback
之前。- 操作: 增加。
- 内容:
+ serializable_task_data = convert_numpy_types(task_data) - self.websocket_callback('training_update', { ... 'metrics': task_data.get('metrics'), ... }) + self.websocket_callback('training_update', { ... 'metrics': serializable_task_data.get('metrics'), ... })
- 原因: 在通过WebSocket发送数据之前,调用
convert_numpy_types
函数对包含训练结果的task_data
进行处理,确保所有float32
等类型都被转换为Python原生的float
,从而解决序列化错误。
- 位置: 文件顶部。
总结: 通过在数据发送前进行类型转换,彻底解决了前后端通信中的序列化问题,确保了训练状态能够被正确地更新到前端。
日期: 2025-07-14 11:04 主题: 根治JSON序列化问题
阶段六:修复API层序列化错误
- 问题: 在修复WebSocket的序列化问题后,发现直接轮询
GET /api/training
接口时,仍然出现Object of type float32 is not JSON serializable
错误。 - 分析: 上一阶段的修复只转换了准备通过WebSocket发送的数据,但没有转换存放在
TrainingProcessManager
内部self.tasks
字典中的数据。因此,当API通过get_all_tasks()
方法读取这个字典时,获取到的仍然是包含NumPy类型的原始数据,导致jsonify
失败。 - 涉及文件:
server/utils/training_process_manager.py
- 修改详情:
- 位置:
_monitor_results
方法,从result_queue
获取数据之后。 - 操作: 调整逻辑。
- 内容:
- with self.lock: - # ... 更新 self.tasks ... - if self.websocket_callback: - serializable_task_data = convert_numpy_types(task_data) - # ... 使用 serializable_task_data 发送消息 ... + # 立即对从队列中取出的数据进行类型转换 + serializable_task_data = convert_numpy_types(task_data) + with self.lock: + # 使用转换后的数据更新任务状态 + for key, value in serializable_task_data.items(): + setattr(self.tasks[task_id], key, value) + # WebSocket通知 - 使用已转换的数据 + if self.websocket_callback: + # ... 使用 serializable_task_data 发送消息 ...
- 原因: 将类型转换的步骤提前,确保存入
self.tasks
的数据已经是JSON兼容的。这样,无论是通过WebSocket推送还是通过API查询,获取到的都是安全的数据,从根源上解决了所有序列化问题。
- 位置:
最终总结: 至此,所有已知的数据流和数据类型问题均已解决。
日期: 2025-07-14 11:15 主题: 修复模型评估中的MAPE计算错误
阶段七:修复评估指标计算
- 问题: 训练
transformer
模型时,日志显示MAPE: nan%
并伴有RuntimeWarning: Mean of empty slice.
。 - 分析:
MAPE
(平均绝对百分比误差) 的计算涉及除以真实值。当测试集中的所有真实销量(y_true
)都为0时,用于避免除零错误的mask
会导致一个空数组被传递给np.mean()
,从而产生nan
和运行时警告。 - 涉及文件:
server/analysis/metrics.py
- 修改详情:
- 位置:
evaluate_model
函数中计算mape
的部分。 - 操作: 增加条件判断。
- 内容:
- mask = y_true != 0 - mape = np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 + mask = y_true != 0 + if np.any(mask): + mape = np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 + else: + # 如果所有真实值都为0,无法计算MAPE,返回0 + mape = 0.0
- 原因: 在计算MAPE之前,先检查是否存在任何非零的真实值。如果不存在,则直接将MAPE设为0,避免了对空数组求平均值,从而解决了
nan
和RuntimeWarning
的问题。
- 位置:
2025-07-14 11:41:修复“按店铺训练”页面店铺列表加载失败问题
问题描述: 在“模型训练” -> “按店铺训练”页面中,“选择店铺”的下拉列表为空,无法加载任何店铺信息。
根本原因:
位于 server/utils/multi_store_data_utils.py
的 standardize_column_names
函数在标准化数据后,错误地移除了包括店铺元数据在内的非训练必需列。这导致调用该函数的 get_available_stores
函数无法获取到完整的店铺信息,最终返回一个空列表。
解决方案: 本着最小改动和保持代码清晰的原则,我进行了以下重构:
- 净化
standardize_column_names
函数:移除了其中所有与列筛选相关的代码,使其只专注于数据标准化这一核心职责。 - 精确应用筛选逻辑:将列筛选的逻辑精确地移动到了
get_store_product_sales_data
和aggregate_multi_store_data
这两个为模型训练准备数据的函数中。这确保了只有在需要为模型准备数据时,才会执行列筛选。 - 增强
get_available_stores
函数:由于load_multi_store_data
现在可以返回所有列,get_available_stores
将能够正常工作。同时,我增强了其代码的健壮性,以优雅地处理数据文件中可能存在的列缺失问题。
代码变更:
- 文件:
server/utils/multi_store_data_utils.py
- 主要改动:
- 从
standardize_column_names
中移除列筛选逻辑。 - 在
get_store_product_sales_data
和aggregate_multi_store_data
中添加列筛选逻辑。 - 重写
get_available_stores
以更健壮地处理数据。
- 从
日期: 2025-07-14 13:00 主题: 修复“按店铺训练-所有药品”模式下的训练失败问题
问题描述
在“模型训练” -> “按店铺训练”页面,当选择“所有药品”进行训练时,后端日志显示 获取店铺产品数据失败: 没有找到店铺 [store_id] 产品 unknown 的销售数据
,导致训练任务失败。
根本原因
- API层:
server/api.py
在处理来自前端的训练请求时,如果product_id
为null
(对应“所有药品”选项),会执行product_id or "unknown"
,错误地将产品ID设置为字符串"unknown"
。 - 预测器层:
server/core/predictor.py
中的train_model
方法接收到无效的product_id="unknown"
后,尝试使用它来获取数据,但数据源中不存在ID为“unknown”的产品,导致数据加载失败。 - 数据工具层:
server/utils/multi_store_data_utils.py
中的aggregate_multi_store_data
函数只支持按产品ID进行全局聚合,不支持按店铺ID聚合其下所有产品的数据。
解决方案 (保留"unknown"字符串)
为了在不改变API层行为的前提下解决问题,采用了在下游处理这个特殊值的策略:
-
修改
server/core/predictor.py
:- 位置:
train_model
方法。 - 操作: 增加了对
product_id == 'unknown'
的特殊处理逻辑。 - 内容:
# 如果product_id是'unknown',则表示为店铺所有商品训练一个聚合模型 if product_id == 'unknown': try: # 使用聚合函数,按店铺聚合 product_data = aggregate_multi_store_data( store_id=store_id, aggregation_method=aggregation_method, file_path=self.data_path ) # 将product_id设置为店铺ID,以便模型保存时使用有意义的标识 product_id = store_id except Exception as e: # ... 错误处理 ... else: # ... 原有的按单个产品获取数据的逻辑 ...
- 原因: 在预测器层面拦截无效的
"unknown"
ID,并将其意图正确地转换为“聚合此店铺的所有产品数据”。同时,将product_id
重新赋值为store_id
,确保了后续模型保存时能使用一个唯一且有意义的名称(如store_01010023_mlstm_v1.pth
)。
- 位置:
-
修改
server/utils/multi_store_data_utils.py
:- 位置:
aggregate_multi_store_data
函数。 - 操作: 重构函数签名和内部逻辑。
- 内容:
def aggregate_multi_store_data(product_id: Optional[str] = None, store_id: Optional[str] = None, aggregation_method: str = 'sum', ...) # ... if store_id: # 店铺聚合:加载该店铺的所有数据 df = load_multi_store_data(file_path, store_id=store_id) # ... elif product_id: # 全局聚合:加载该产品的所有数据 df = load_multi_store_data(file_path, product_id=product_id) # ... else: raise ValueError("必须提供 product_id 或 store_id")
- 原因: 扩展了数据聚合函数的功能,使其能够根据传入的
store_id
参数,加载并聚合特定店铺的所有销售数据,为店铺级别的综合模型训练提供了数据基础。
- 位置:
最终结果: 通过这两处修改,系统现在可以正确处理“按店铺-所有药品”的训练请求。它会聚合该店铺所有产品的销售数据,训练一个综合模型,并以店铺ID为标识来保存该模型,彻底解决了该功能点的训练失败问题。
日期: 2025-07-14 14:19 主题: 修复并发训练中的稳定性和日志错误
阶段八:修复并发训练中的多个错误
- 问题: 在并发执行多个训练任务时,系统出现
JSON序列化错误
、API列表排序错误
和WebSocket连接错误
。 - 分析:
Object of type float32 is not JSON serializable
:training_process_manager.py
在通过WebSocket发送中途的训练进度时,没有对包含NumPyfloat32
类型的metrics
数据进行序列化。'<' not supported between instances of 'str' and 'NoneType'
:api.py
在获取训练任务列表时,对start_time
进行排序,但未处理某些任务的start_time
可能为None
的情况,导致TypeError
。AssertionError: write() before start_response
:api.py
中,当以debug=True
模式运行时,Flask内置的Werkzeug服务器的调试器与Socket.IO的连接管理机制发生冲突。
- 解决方案:
- 文件:
server/utils/training_process_manager.py
- 位置:
_monitor_progress
方法。 - 操作: 在发送
training_progress
事件前,调用convert_numpy_types
函数对progress_data
进行完全序列化。 - 原因: 确保所有通过WebSocket发送的数据(包括中途进度)都是JSON兼容的,彻底解决序列化问题。
- 位置:
- 文件:
server/api.py
- 位置:
get_all_training_tasks
函数。 - 操作: 修改
sorted
函数的key
,使用lambda x: x.get('start_time') or '1970-01-01 00:00:00'
。 - 原因: 为
None
类型的start_time
提供一个有效的默认值,使其可以和字符串类型的日期进行安全比较,解决了排序错误。
- 位置:
- 文件:
server/api.py
- 位置:
socketio.run()
调用处。 - 操作: 增加
allow_unsafe_werkzeug=True if args.debug else False
参数。 - 原因: 这是
Flask-SocketIO
官方推荐的解决方案,用于在调试模式下协调Werkzeug与Socket.IO的事件循环,避免底层WSGI错误。
- 位置:
- 文件:
最终结果: 通过这三项修复,系统的并发稳定性和健壮性得到显著提升,解决了在高并发训练场景下出现的各类错误。
日期: 2025-07-14 14:48 主题: 修复模型评估指标计算错误并优化训练过程
阶段九:修复模型评估与训练优化
- 问题: 所有模型训练完成后,评估指标
R²
始终为0.0,MAPE
始终为0.00%,这表明模型评估或训练过程存在严重问题。 - 分析:
- 核心错误: 在
mlstm_trainer.py
和transformer_trainer.py
中,计算损失函数时,模型输出outputs
的维度是(batch_size, forecast_horizon)
,而目标y_batch
的维度被错误地通过unsqueeze(-1)
修改为(batch_size, forecast_horizon, 1)
。这种维度不匹配导致损失计算错误,模型无法正确学习。 - 优化缺失: 训练过程中缺少学习率调度、梯度裁剪和提前停止等关键的优化策略,影响了训练效率和稳定性。
- 核心错误: 在
- 解决方案:
- 修复维度不匹配 (关键修复):
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 位置: 训练和验证循环中的损失计算部分。
- 操作: 移除了对
y_batch
的unsqueeze(-1)
操作,确保outputs
和y_batch
维度一致。- loss = criterion(outputs, y_batch.unsqueeze(-1)) + loss = criterion(outputs, y_batch.squeeze(-1) if y_batch.dim() == 3 else y_batch)
- 原因: 修正损失函数的输入,使模型能够根据正确的误差进行学习,从而解决评估指标恒为0的问题。
- 文件:
- 增加训练优化策略:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 在两个训练器中增加了以下功能:
- 学习率调度器: 引入
torch.optim.lr_scheduler.ReduceLROnPlateau
,当测试损失停滞时自动降低学习率。 - 梯度裁剪: 在优化器更新前,使用
torch.nn.utils.clip_grad_norm_
对梯度进行裁剪,防止梯度爆炸。 - 提前停止: 增加了
patience
参数,当测试损失连续多个epoch未改善时,提前终止训练,防止过拟合。
- 学习率调度器: 引入
- 文件:
- 原因: 引入这些业界标准的优化技术,可以显著提高训练过程的稳定性、收敛速度和最终的模型性能。
- 修复维度不匹配 (关键修复):
最终结果: 通过修复核心的逻辑错误并引入多项优化措施,模型现在不仅能够正确学习,而且训练过程更加健壮和高效。
日期: 2025-07-14 15:20 主题: 根治模型维度错误并统一数据流 (完整调试过程)
阶段九:错误的修复尝试 (记录备查)
- 问题: 所有模型训练完成后,评估指标
R²
始终为0.0,MAPE
始终为0.00%。 - 初步分析: 怀疑损失函数计算时,
outputs
和y_batch
维度不匹配。 - 错误的假设: 当时错误地认为是
y_batch
的维度有问题,而outputs
的维度是正确的。 - 错误的修复:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 尝试在训练器层面使用
squeeze
调整y_batch
的维度来匹配outputs
。- loss = criterion(outputs, y_batch) + loss = criterion(outputs, y_batch.squeeze(-1) if y_batch.dim() == 3 else y_batch)
- 文件:
- 结果: 此修改导致了新的运行时错误
UserWarning: Using a target size (torch.Size([32, 3])) that is different to the input size (torch.Size([32, 3, 1]))
,证明了修复方向错误,但帮助定位了问题的真正根源。
阶段十:根治维度不匹配问题
- 问题: 深入分析阶段九的错误后,确认了问题的根源。
- 根本原因:
server/models/mlstm_model.py
中的MLSTMTransformer
模型,其forward
方法的最后一层输出了一个多余的维度,导致其输出形状为(B, H, 1)
,而并非期望的(B, H)
。 - 正确的解决方案 (端到端维度一致性):
- 修复模型层 (治本):
- 文件:
server/models/mlstm_model.py
- 位置:
MLSTMTransformer
的forward
方法。 - 操作: 在
output_layer
之后增加.squeeze(-1)
,将模型输出的维度从(B, H, 1)
修正为(B, H)
。- return self.output_layer(decoder_outputs) + return self.output_layer(decoder_outputs).squeeze(-1)
- 文件:
- 净化训练器层 (治标):
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 撤销了阶段九的错误修改,恢复为最直接的损失计算
loss = criterion(outputs, y_batch)
。
- 文件:
- 优化评估逻辑:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 简化了模型评估部分的反归一化逻辑,使其更清晰、更直接地处理
(样本数, 预测步长)
形状的数据。- test_pred_inv = scaler_y.inverse_transform(test_pred.reshape(-1, 1)).flatten() - test_true_inv = scaler_y.inverse_transform(testY.reshape(-1, 1)).flatten() + test_pred_inv = scaler_y.inverse_transform(test_pred) + test_true_inv = scaler_y.inverse_transform(test_true)
- 文件:
- 修复模型层 (治本):
最终结果: 通过记录整个调试过程,我们不仅修复了问题,还理解了其根本原因。通过在模型源头修正维度,并在整个数据流中保持维度一致性,彻底解决了训练失败的问题。代码现在更简洁、健壮,并遵循了良好的设计实践。
日期: 2025-07-14 15:30 主题: 根治模型维度错误并统一数据流 (完整调试过程)
阶段九:错误的修复尝试 (记录备查)
- 问题: 所有模型训练完成后,评估指标
R²
始终为0.0,MAPE
始终为0.00%。 - 初步分析: 怀疑损失函数计算时,
outputs
和y_batch
维度不匹配。 - 错误的假设: 当时错误地认为是
y_batch
的维度有问题,而outputs
的维度是正确的。 - 错误的修复:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 尝试在训练器层面使用
squeeze
调整y_batch
的维度来匹配outputs
。- loss = criterion(outputs, y_batch) + loss = criterion(outputs, y_batch.squeeze(-1) if y_batch.dim() == 3 else y_batch)
- 文件:
- 结果: 此修改导致了新的运行时错误
UserWarning: Using a target size (torch.Size([32, 3])) that is different to the input size (torch.Size([32, 3, 1]))
,证明了修复方向错误,但帮助定位了问题的真正根源。
阶段十:根治维度不匹配问题
- 问题: 深入分析阶段九的错误后,确认了问题的根源在于模型输出维度。
- 根本原因:
server/models/mlstm_model.py
中的MLSTMTransformer
模型,其forward
方法的最后一层输出了一个多余的维度,导致其输出形状为(B, H, 1)
,而并非期望的(B, H)
。 - 正确的解决方案 (端到端维度一致性):
- 修复模型层 (治本):
- 文件:
server/models/mlstm_model.py
- 位置:
MLSTMTransformer
的forward
方法。 - 操作: 在
output_layer
之后增加.squeeze(-1)
,将模型输出的维度从(B, H, 1)
修正为(B, H)
。
- 文件:
- 净化训练器层 (治标):
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 撤销了阶段九的错误修改,恢复为最直接的损失计算
loss = criterion(outputs, y_batch)
。
- 文件:
- 优化评估逻辑:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 操作: 简化了模型评估部分的反归一化逻辑,使其更清晰、更直接地处理
(样本数, 预测步长)
形状的数据。
- 文件:
- 修复模型层 (治本):
阶段十一:最终修复与逻辑统一
- 问题: 在应用阶段十的修复后,训练仍然失败。mLSTM出现维度反转错误 (
target size (B, H, 1)
vsinput size (B, H)
),而Transformer则出现评估错误 ('numpy.ndarray' object has no attribute 'numpy'
)。 - 分析:
- 维度反转根源: 问题的最终根源在
server/utils/data_utils.py
的create_dataset
函数。它在创建目标数据集dataY
时,错误地保留了一个多余的维度,导致y_batch
的形状变为(B, H, 1)
。 - 评估Bug: 在
mlstm_trainer.py
和transformer_trainer.py
的评估部分,代码test_true = testY.numpy()
是错误的,因为testY
已经是Numpy数组。
- 维度反转根源: 问题的最终根源在
- 最终解决方案 (端到端修复):
- 修复数据加载层 (治本):
- 文件:
server/utils/data_utils.py
- 位置:
create_dataset
函数。 - 操作: 修改
dataY.append(y)
为dataY.append(y.flatten())
,从源头上确保y
标签的维度是正确的(B, H)
。
- 文件:
- 修复训练器评估层:
- 文件:
server/trainers/mlstm_trainer.py
,server/trainers/transformer_trainer.py
- 位置: 模型评估部分。
- 操作: 修正
test_true = testY.numpy()
为test_true = testY
,解决了属性错误。
- 文件:
- 修复数据加载层 (治本):
最终结果: 通过记录并分析整个调试过程(阶段九到十一),我们最终定位并修复了从数据加载、模型设计到训练器评估的整个流程中的维度不一致问题。代码现在更加简洁、健壮,并遵循了端到端维度一致的良好设计实践。
日期: 2025-07-14 15:34 主题: 扩展维度修复至Transformer模型
阶段十二:统一所有模型的输出维度
- 问题: 在修复
mLSTM
模型后,Transformer
模型的训练仍然因为完全相同的维度不匹配问题而失败。 - 分析:
server/models/transformer_model.py
中的TimeSeriesTransformer
类也存在与mLSTM
相同的设计缺陷,其forward
方法的输出维度为(B, H, 1)
而非(B, H)
。 - 解决方案:
- 修复Transformer模型层:
- 文件:
server/models/transformer_model.py
- 位置:
TimeSeriesTransformer
的forward
方法。 - 操作: 在
output_layer
之后增加.squeeze(-1)
,将模型输出的维度从(B, H, 1)
修正为(B, H)
。- return self.output_layer(decoder_outputs) + return self.output_layer(decoder_outputs).squeeze(-1)
- 文件:
- 修复Transformer模型层:
最终结果: 通过将维度修复方案应用到所有相关的模型文件,我们确保了整个系统的模型层都遵循了统一的、正确的输出维度标准。至此,所有已知的维度相关问题均已从根源上解决。
日期: 2025-07-14 16:10 主题: 修复“全局模型训练-所有药品”模式下的训练失败问题
问题描述
在“全局模型训练”页面,当选择“所有药品”进行训练时,后端日志显示 聚合全局数据失败: 没有找到产品 unknown 的销售数据
,导致训练任务失败。
根本原因
- API层 (
server/api.py
): 在处理全局训练请求时,如果前端未提供product_id
(对应“所有药品”选项),API层会执行product_id or "unknown"
,错误地将产品ID设置为字符串"unknown"
。 - 预测器层 (
server/core/predictor.py
):train_model
方法接收到无效的product_id="unknown"
后,在training_mode='global'
分支下,直接将其传递给数据聚合函数。 - 数据工具层 (
server/utils/multi_store_data_utils.py
):aggregate_multi_store_data
函数缺少处理“真正”全局聚合(即不按任何特定产品或店铺过滤)的逻辑,当收到product_id="unknown"
时,它会尝试按一个不存在的产品进行过滤,最终导致失败。
解决方案 (遵循现有设计模式)
为了在不影响现有功能的前提下修复此问题,采用了与历史修复类似的、在中间层进行逻辑适配的策略。
-
修改
server/utils/multi_store_data_utils.py
:- 位置:
aggregate_multi_store_data
函数。 - 操作: 扩展了函数功能。
- 内容: 增加了新的逻辑分支。当
product_id
和store_id
参数都为None
时,函数现在会加载所有数据进行聚合,以支持真正的全局模型训练。# ... elif product_id: # 按产品聚合... else: # 真正全局聚合:加载所有数据 df = load_multi_store_data(file_path) if len(df) == 0: raise ValueError("数据文件为空,无法进行全局聚合") grouping_entity = "所有产品"
- 原因: 使数据聚合函数的功能更加完整和健壮,能够服务于真正的全局训练场景,同时不影响其原有的按店铺或按产品的聚合功能。
- 位置:
-
修改
server/core/predictor.py
:- 位置:
train_model
方法,training_mode == 'global'
的逻辑分支内。 - 操作: 增加了对
product_id == 'unknown'
的特殊处理。 - 内容:
if product_id == 'unknown': product_data = aggregate_multi_store_data( product_id=None, # 传递None以触发真正的全局聚合 # ... ) # 将product_id设置为一个有意义的标识符 product_id = 'all_products' else: # ...原有的按单个产品聚合的逻辑...
- 原因: 在核心预测器层面拦截无效的
"unknown"
ID,并将其正确地解释为“聚合所有产品数据”的意图。通过向聚合函数传递product_id=None
来调用新增强的全局聚合功能,并用一个有意义的标识符all_products
来命名模型,确保了后续流程的正确执行。
- 位置:
最终结果: 通过这两处修改,系统现在可以正确处理“全局模型-所有药品”的训练请求,聚合所有产品的销售数据来训练一个通用的全局模型,彻底解决了该功能点的训练失败问题。
日期: 2025-07-14 主题: UI导航栏重构
描述
根据用户请求,对左侧功能导航栏进行了调整。
主要改动
-
删除“数据管理”:
- 从
UI/src/App.vue
的导航菜单中移除了“数据管理”项。 - 从
UI/src/router/index.js
中删除了对应的/data
路由。 - 删除了视图文件
UI/src/views/DataView.vue
。
- 从
-
提升“店铺管理”:
- 将“店铺管理”菜单项在
UI/src/App.vue
中的位置提升,以填补原“数据管理”的位置,使其在导航中更加突出。
- 将“店铺管理”菜单项在
涉及文件
UI/src/App.vue
UI/src/router/index.js
UI/src/views/DataView.vue
(已删除)
按药品模型预测
日期: 2025-07-14 主题: 修复导航菜单高亮问题
描述
修复了首次进入或刷新页面时,左侧导航菜单项与当前路由不匹配导致不高亮的问题。
主要改动
- 文件:
UI/src/App.vue
- 修改:
- 引入
useRoute
和computed
。 - 创建了一个计算属性
activeMenu
,其值动态地等于当前路由的路径 (route.path
)。 - 将
el-menu
组件的:default-active
属性绑定到activeMenu
。
- 引入
结果
确保了导航菜单的高亮状态始终与当前页面的URL保持同步。
日期: 2025-07-15 主题: 修复硬编码文件路径问题,提高项目可移植性
问题描述
项目在从一台计算机迁移到另一台时,由于数据文件路径被硬编码在代码中,导致程序无法找到数据文件而运行失败。
根本原因
多个Python文件(predictor.py
, multi_store_data_utils.py
)中直接写入了相对路径 'data/timeseries_training_data_sample_10s50p.parquet'
作为默认值。这种方式在不同运行环境下(如从根目录运行 vs 从子目录运行)会产生路径解析错误。
解决方案:集中配置,统一管理
-
修改
server/core/config.py
(核心):- 动态计算并定义了一个全局变量
PROJECT_ROOT
,它始终指向项目的根目录。 - 基于
PROJECT_ROOT
,使用os.path.join
创建了一个跨平台的、绝对的默认数据路径DEFAULT_DATA_PATH
和模型保存路径DEFAULT_MODEL_DIR
。 - 这确保了无论从哪个位置执行代码,路径总能被正确解析。
- 动态计算并定义了一个全局变量
-
修改
server/utils/multi_store_data_utils.py
:- 从
server/core/config
导入DEFAULT_DATA_PATH
。 - 将所有数据加载函数的
file_path
参数的默认值从硬编码的字符串改为None
。 - 在函数内部,如果
file_path
为None
,则自动使用导入的DEFAULT_DATA_PATH
。 - 移除了原有的、复杂的、为了猜测正确路径而编写的冗余代码。
- 从
-
修改
server/core/predictor.py
:- 同样从
server/core/config
导入DEFAULT_DATA_PATH
。 - 在初始化
PharmacyPredictor
时,如果未提供数据路径,则使用导入的DEFAULT_DATA_PATH
作为默认值。
- 同样从
最终结果
通过将数据源路径集中到唯一的配置文件中进行管理,彻底解决了因硬编码路径导致的可移植性问题。项目现在可以在任何环境下可靠地运行。
未来如何修改数据源(例如,连接到服务器数据库)
本次重构为将来更换数据源打下了坚实的基础。操作非常简单:
-
定位配置文件: 打开
server/core/config.py
文件。 -
修改数据源定义:
- 当前 (文件):
DEFAULT_DATA_PATH = os.path.join(PROJECT_ROOT, 'data', 'timeseries_training_data_sample_10s50p.parquet')
- 未来 (数据库示例):
您可以将这行替换为数据库连接字符串,或者添加新的数据库配置变量。例如:
# 注释掉或删除旧的文件路径配置 # DEFAULT_DATA_PATH = ... # 新增数据库连接配置 DATABASE_URL = "postgresql://user:password@your_server_ip:5432/your_database_name"
- 当前 (文件):
-
修改数据加载逻辑:
- 定位数据加载函数: 打开
server/utils/multi_store_data_utils.py
。 - 修改
load_multi_store_data
函数:- 引入数据库连接库(如
sqlalchemy
或psycopg2
)。 - 修改函数逻辑,使其使用
config.py
中的DATABASE_URL
来连接数据库,并执行SQL查询来获取数据,而不是读取文件。 - 示例:
from sqlalchemy import create_engine from core.config import DATABASE_URL # 导入新的数据库配置 def load_multi_store_data(...): # ... engine = create_engine(DATABASE_URL) query = "SELECT * FROM sales_data" # 根据需要构建查询 df = pd.read_sql(query, engine) # ... 后续处理逻辑保持不变 ...
- 引入数据库连接库(如
- 定位数据加载函数: 打开
通过以上步骤,您就可以在不改动项目其他任何部分的情况下,轻松地将数据源从本地文件切换到服务器数据库。
日期: 2025-07-15 11:43 主题: 修复因PyTorch版本不兼容导致的训练失败问题
问题描述
在修复了路径和依赖问题后,在某些机器上运行模型训练时,程序因 TypeError: ReduceLROnPlateau.__init__() got an unexpected keyword argument 'verbose'
而崩溃。但在本地开发机上运行正常。
根本原因
此问题是典型的环境不一致导致的兼容性错误。
- PyTorch版本差异: 本地开发环境安装了较旧版本的PyTorch,其学习率调度器
ReduceLROnPlateau
支持verbose
参数(用于在学习率变化时打印日志)。 - 新环境: 在其他计算机或新创建的虚拟环境中,安装了较新版本的PyTorch。在新版本中,
ReduceLROnPlateau
的verbose
参数已被移除。 - 代码问题:
server/trainers/mlstm_trainer.py
和server/trainers/transformer_trainer.py
的代码中,在创建ReduceLROnPlateau
实例时硬编码了verbose=True
参数,导致在新版PyTorch环境下调用时出现TypeError
。
解决方案:移除已弃用的参数
- 全面排查: 检查了项目中所有训练器文件 (
mlstm_trainer.py
,transformer_trainer.py
,kan_trainer.py
,tcn_trainer.py
)。 - 精确定位: 确认只有
mlstm_trainer.py
和transformer_trainer.py
使用了ReduceLROnPlateau
并传递了verbose
参数。 - 执行修复:
- 文件:
server/trainers/mlstm_trainer.py
和server/trainers/transformer_trainer.py
- 位置:
ReduceLROnPlateau
的初始化调用处。 - 操作: 删除了
verbose=True
参数。- scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', ..., verbose=True) + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', ...)
- 原因: 移除这个在新版PyTorch中已不存在的参数,可以从根本上解决
TypeError
,并确保代码在不同版本的PyTorch环境中都能正常运行。此修改不影响学习率调度器的核心功能。
- 文件:
最终结果
通过移除已弃用的 verbose
参数,彻底解决了由于环境差异导致的版本兼容性问题,确保了项目在所有目标机器上都能稳定地执行训练任务。
日期: 2025-07-15 14:05 主题: 仪表盘UI调整
描述
根据用户请求,将仪表盘上的“数据管理”卡片替换为“店铺管理”。
主要改动
- 文件:
UI/src/views/DashboardView.vue
- 修改:
- 在
featureCards
数组中,将原“数据管理”的对象修改为“店铺管理”。 - 更新了卡片的
title
,description
,icon
和path
,使其指向店铺管理页面 (/store-management
)。 - 在脚本中导入了新的
Shop
图标。
- 在
结果
仪表盘现在直接提供到“店铺管理”页面的快捷入口,提高了操作效率。
日期: 2025-07-18 主题: 模型保存逻辑重构与集中化管理
目标
根据 xz训练模型保存规则.md
,将系统中分散的模型文件保存逻辑统一重构,创建一个集中、健壮且可测试的路径管理系统。
核心成果
- 创建了
server/utils/file_save.py
模块: 这个新模块现在是系统中处理模型文件保存路径的唯一权威来源。 - 实现了三种训练模式的路径生成: 系统现在可以为“按店铺”、“按药品”和“全局”三种训练模式正确生成层级化的、可追溯的目录结构。
- 集成了智能ID处理:
- 对于包含多个ID的训练场景,系统会自动计算一个简短的哈希值作为目录名。
- 对于全局训练中只包含单个店铺或药品ID的场景,系统会直接使用该ID作为目录名,增强了路径的可读性。
- 重构了整个训练流程: 修改了API层、进程管理层以及所有模型训练器,使它们能够协同使用新的路径管理模块。
- 添加了自动化测试: 创建了
test/test_file_save_logic.py
脚本,用于验证所有路径生成和版本管理逻辑的正确性。
详细文件修改记录
-
server/utils/file_save.py
- 操作: 创建
- 内容: 实现了
ModelPathManager
类,包含以下核心方法:_hash_ids
: 对ID列表进行排序和哈希。_generate_identifier
: 根据训练模式和参数生成唯一的模型标识符。get_next_version
/save_version_info
: 线程安全地管理versions.json
文件,实现版本号的获取和更新。get_model_paths
: 作为主入口,协调以上方法,生成包含所有产物路径的字典。
-
server/api.py
- 操作: 修改
- 位置:
start_training
函数 (/api/training
端点)。 - 内容:
- 导入并实例化
ModelPathManager
。 - 在接收到训练请求后,调用
path_manager.get_model_paths()
来获取所有路径信息。 - 将获取到的
path_info
字典和原始请求参数training_params
一并传递给后台训练任务管理器。 - 修复了因重复传递关键字参数 (
model_type
,training_mode
) 导致的TypeError
。 - 修复了
except
块中因未导入traceback
模块导致的UnboundLocalError
。
- 导入并实例化
-
server/utils/training_process_manager.py
- 操作: 修改
- 内容:
- 修改
submit_task
方法,使其能接收training_params
和path_info
字典。 - 在
TrainingTask
数据类中增加了path_info
字段来存储路径信息。 - 在
TrainingWorker
中,将path_info
传递给实际的训练函数。 - 在
_monitor_results
方法中,当任务成功完成时,调用path_manager.save_version_info
来更新versions.json
,完成版本管理的闭环。
- 修改
-
所有训练器文件 (
mlstm_trainer.py
,kan_trainer.py
,tcn_trainer.py
,transformer_trainer.py
)- 操作: 修改
- 内容:
- 统一修改了主训练函数的签名,增加了
path_info=None
参数。 - 移除了所有内部手动构建文件路径的逻辑。
- 所有保存操作(最终模型、检查点、损失曲线图)现在都直接从传入的
path_info
字典中获取预先生成好的路径。 - 简化了
save_checkpoint
辅助函数,使其也依赖path_info
。
- 统一修改了主训练函数的签名,增加了
-
test/test_file_save_logic.py
- 操作: 创建
- 内容:
- 编写了一个独立的测试脚本,用于验证
ModelPathManager
的所有功能。 - 覆盖了所有训练模式及其子场景(包括单ID和多ID哈希)。
- 测试了版本号的正确递增和
versions.json
的写入。 - 修复了测试脚本中因绝对/相对路径不匹配和重复关键字参数导致的多个
AssertionError
和TypeError
。
- 编写了一个独立的测试脚本,用于验证
日期: 2025-07-18 (后续修复)
主题: 修复API层调用路径管理器时的 TypeError
问题描述
在完成所有重构和测试后,实际运行API时,POST /api/training
端点在调用 path_manager.get_model_paths
时崩溃,并抛出 TypeError: get_model_paths() got multiple values for keyword argument 'training_mode'
。
根本原因
这是一个回归错误。在修复测试脚本 test_file_save_logic.py
中的类似问题时,我未能将相同的修复逻辑应用回 server/api.py
。代码在调用 get_model_paths
时,既通过关键字参数 training_mode=...
明确传递了该参数,又通过 **data
将其再次传入,导致了冲突。
解决方案
- 文件:
server/api.py
- 位置:
start_training
函数。 - 操作: 修改了对
get_model_paths
的调用逻辑。 - 内容:
# 移除 model_type 和 training_mode 以避免重复关键字参数错误 data_for_path = data.copy() data_for_path.pop('model_type', None) data_for_path.pop('training_mode', None) path_info = path_manager.get_model_paths( training_mode=training_mode, model_type=model_type, **data_for_path # 传递剩余的payload )
- 原因: 在通过
**
解包传递参数之前,先从字典副本中移除了所有会被明确指定的关键字参数,从而确保了函数调用签名的正确性。
日期: 2025-07-18 (最终修复)
主题: 修复因中间层函数签名未更新导致的 TypeError
问题描述
在完成所有重构后,实际运行API并触发训练任务时,程序在后台进程中因 TypeError: train_model() got an unexpected keyword argument 'path_info'
而崩溃。
根本原因
这是一个典型的“中间人”遗漏错误。我成功地修改了调用链的两端(api.py
-> training_process_manager.py
和 *_trainer.py
),但忘记了修改它们之间的中间层——server/core/predictor.py
中的 train_model
方法。training_process_manager
尝试将 path_info
传递给 predictor.train_model
,但后者的函数签名中并未包含这个新参数,导致了 TypeError
。
解决方案
- 文件:
server/core/predictor.py
- 位置:
train_model
函数的定义处。 - 操作: 在函数签名中增加了
path_info=None
参数。 - 内容:
def train_model(self, ..., progress_callback=None, path_info=None): # ...
- 位置:
train_model
函数内部,对所有具体训练器(train_product_model_with_mlstm
,_with_kan
, etc.)的调用处。 - 操作: 在所有调用中,将接收到的
path_info
参数透传下去。 - 内容:
# ... metrics = train_product_model_with_transformer( ..., path_info=path_info ) # ...
- 原因: 通过在中间层函数上“打通”
path_info
参数的传递通道,确保了从API层到最终训练器层的完整数据流,解决了TypeError
。
日期: 2025-07-18 (最终修复) 主题: 修复“按药品训练-聚合所有店铺”模式下的路径生成错误
问题描述
在实际运行中发现,当进行“按药品训练”并选择“聚合所有店铺”时,生成的模型保存路径中包含了错误的后缀 _None
,而不是预期的 _all
(例如 .../17002608_None/...
)。
根本原因
在 server/utils/file_save.py
的 _generate_identifier
和 get_model_paths
方法中,当 store_id
从前端传来为 None
时,代码 scope = store_id if store_id else 'all'
会因为 store_id
是 None
而正确地将 scope
设为 'all'
。然而,在 get_model_paths
方法中,我错误地使用了 kwargs.get('store_id', 'all')
,这在 store_id
键存在但值为 None
时,仍然会返回 None
,导致了路径拼接错误。
解决方案
- 文件:
server/utils/file_save.py
- 位置:
_generate_identifier
和get_model_paths
方法中处理product
训练模式的部分。 - 操作: 将逻辑从
scope = kwargs.get('store_id', 'all')
修改为更严谨的scope = store_id if store_id is not None else 'all'
。 - 内容:
# in _generate_identifier scope = store_id if store_id is not None else 'all' # in get_model_paths store_id = kwargs.get('store_id') scope = store_id if store_id is not None else 'all' scope_folder = f"{product_id}_{scope}"
- 原因: 这种写法能正确处理
store_id
键不存在、或键存在但值为None
的两种情况,确保在这两种情况下scope
都被正确地设置为'all'
,从而生成符合规范的路径。
日期: 2025-07-18 (最终修复)
主题: 修复 KeyError: 'price'
和单ID哈希错误
问题描述
在完成大规模重构后,实际运行时发现了两个隐藏的bug:
- 在“按店铺训练”模式下,训练因
KeyError: 'price'
而失败。 - 在“按店铺训练”模式下,当只选择一个“指定药品”时,系统仍然错误地对该药品的ID进行了哈希处理,而不是直接使用ID。
根本原因
KeyError
:server/utils/multi_store_data_utils.py
中的get_store_product_sales_data
函数包含了一个硬编码的列校验,该校验要求price
列必须存在,但这与当前的数据源不符。- 哈希错误:
server/utils/file_save.py
中的get_model_paths
方法在处理store
训练模式时,没有复用_generate_identifier
中已经写好的单ID判断逻辑,导致了逻辑不一致。
解决方案
- 修复
KeyError
:- 文件:
server/utils/multi_store_data_utils.py
- 位置:
get_store_product_sales_data
函数。 - 操作: 从
required_columns
列表中移除了'price'
,根除了这个硬性依赖。
- 文件:
- 修复哈希逻辑:
- 文件:
server/utils/file_save.py
- 位置:
_generate_identifier
和get_model_paths
方法中处理store
训练模式的部分。 - 操作: 统一了逻辑,确保在这两个地方都使用了
scope = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids)
的判断,从而在只选择一个药品时直接使用其ID。
- 文件:
- 更新测试:
- 文件:
test/test_file_save_logic.py
- 操作: 增加了新的测试用例,专门验证“按店铺训练-单个指定药品”场景下的路径生成是否正确。
- 文件:
日期: 2025-07-18 (最终修复)
主题: 修复全局训练范围值不匹配导致的 ValueError
问题描述
在完成所有重构后,实际运行API并触发“全局训练-所有店铺所有药品”时,程序因 ValueError: 未知的全局训练范围: all_stores_all_products
而崩溃。
根本原因
前端传递的 training_scope
值为 all_stores_all_products
,而 server/utils/file_save.py
中的 _generate_identifier
和 get_model_paths
方法只处理了 all
这个值,未能兼容前端传递的具体字符串,导致逻辑判断失败。
解决方案
- 文件:
server/utils/file_save.py
- 位置:
_generate_identifier
和get_model_paths
方法中处理global
训练模式的部分。 - 操作: 将逻辑判断从
if training_scope == 'all':
修改为if training_scope in ['all', 'all_stores_all_products']:
。 - 原因: 使代码能够同时兼容两种表示“所有范围”的字符串,确保了前端请求的正确处理。
- 更新测试:
- 文件:
test/test_file_save_logic.py
- 操作: 增加了新的测试用例,专门验证
training_scope
为all_stores_all_products
时的路径生成是否正确。
- 文件:
日期: 2025-07-18 (最终优化) 主题: 优化全局训练自定义模式下的单ID路径生成
问题描述
根据用户反馈,希望在全局训练的“自定义范围”模式下,如果只选择单个店铺和/或单个药品,路径中应直接使用ID而不是哈希值,以增强可读性。
解决方案
- 文件:
server/utils/file_save.py
- 位置:
_generate_identifier
和get_model_paths
方法中处理global
训练模式custom
范围的部分。 - 操作: 为
store_ids
和product_ids
分别增加了单ID判断逻辑。 - 内容:
# in _generate_identifier s_id = store_ids[0] if len(store_ids) == 1 else self._hash_ids(store_ids) p_id = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids) scope_part = f"custom_s_{s_id}_p_{p_id}" # in get_model_paths store_ids = kwargs.get('store_ids', []) product_ids = kwargs.get('product_ids', []) s_id = store_ids[0] if len(store_ids) == 1 else self._hash_ids(store_ids) p_id = product_ids[0] if len(product_ids) == 1 else self._hash_ids(product_ids) scope_parts.extend(['custom', s_id, p_id])
- 原因: 使
custom
模式下的路径生成逻辑与selected_stores
和selected_products
模式保持一致,在只选择一个ID时优先使用ID本身,提高了路径的可读性和一致性。 - 更新测试:
- 文件:
test/test_file_save_logic.py
- 操作: 增加了新的测试用例,专门验证“全局训练-自定义范围-单ID”场景下的路径生成是否正确。
- 文件: