简介:这是一份基于双向堆叠LSTM的电力负荷预测系统Java完整项目,专为计算机相关专业学生、毕业设计及课程设计人群打造,可用于毕业论文实现与负荷预测算法入门。系统采用堆叠式双向LSTM构建预测模型,配套JavaFX图形界面展示预测结果,帮助读者理解时序预测与深度学习在Java工程中的落地方式。压缩包共90个文件,含20个Java源码、23个class编译文件、12个jar依赖库、24张png运行截图,以及fxml界面描述、XML配置与README说明文档,整体仅14.48MB,结构清晰便于直接导入IDE运行。项目代码均已测试成功,作者称答辩平均分达96分,并支持下载后私聊远程教学,适合需要快速跑通毕设或深入学习LSTM预测思路的读者。目前已有161人学习下载。
1. Java里跑双向堆叠LSTM做电力负荷预测,到底图什么?
电力负荷预测是电网调度、电力交易和需求响应的基础工作。很多做电力信息化的工程师,需要在 Java 后端系统里直接内置一套负荷预测能力,而现成的开源方案大多集中在 Python 生态,部署时总要在旁边多养一个 Python 服务,维护成本一下子上来了。基于 Java 开发一套双向堆叠 LSTM 电力负荷预测系统源码,本质就是用 Deeplearning4j 在 JVM 里完成数据清洗、时序样本构造、双向堆叠 LSTM 建模、训练与预测的完整闭环,并把模型文件、归一化参数、运行文档一并整理成可持续维护的源码包。这篇文章适合手上有历史负荷数据、想把深度学习预测直接跑进生产系统的团队和个人开发者。下面这套流程是我实际做过多次的方案,重点讲清楚模型怎么搭、参数怎么调、坑在哪里。
2. 双向堆叠LSTM在负荷预测里解决什么?原理与选型
2.1 负荷序列里最难的不是趋势,而是“模式切换”
电力负荷数据看着像一条波浪线,但真实业务里真正的挑战在于模式切换:工作日用电爬坡、周末整体下降,夏季空调在下午拉出峰值,冬季采暖在凌晨推高负荷。传统统计模型 ARIMA 对平稳序列效果还可以,但遇到模式切换时要反复重新拟合,很难跟上突变。LSTM 的门控机制允许网络保留跨时间跨度的信息——遗忘门决定丢哪些旧状态,输入门决定把哪些新信息写进记忆,输出门控制暴露多少给下一层。这让它比传统 RNN 更擅长处理“大部分时间有规律、关键时刻突变”的负荷序列。
另外,负荷数据不是独立同分布的采样,而是严格按时间顺序产生的依赖序列。今天下午两点的负荷和昨天下午两点的负荷相关,也和前三天同一时刻的负荷相关。这种时间依赖正是 LSTM 这类循环结构的用武之地。你在做预测系统时,不需要把全部历史数据塞进模型,只需要让网络在训练中自己学会“该记多久、该忘什么”。
2.2 为什么是双向:前后的负荷相互印证
普通 LSTM 的信息流只有一个方向,从序列开头向结尾传播。也就是说,预测 t 时刻时,它只见过 0 到 t-1 的信息。但负荷数据里,后面一段往往能给当前段提供很强的判断依据。最典型的例子是夜间低谷:只看过去两小时负荷一路往下走,模型很难判断现在是不是已经到底了;如果能同时看到后半夜到凌晨负荷开始爬升,就能更准确判断当前正处在低谷区,并且合理预测接下来的上升拐点。
双向 LSTM 的实现思路是:把原始序列正向送入一个 LSTM,同时把反转后的序列送入另一个 LSTM,然后在每个时间步把两个方向的隐藏状态拼接起来。这样每个时刻的输出同时包含“从前面看到现在”和“从后面回看现在”两路信息。虽然预测时我们拿不到真正的未来,但训练阶段对每个时间步都提供了完整上下文,模型可以学到负荷序列里那种“前后呼应的节奏感”,推理时再靠这种节奏感来补足早期的信息盲区。
2.3 为什么要堆叠:不同时间尺度特征的组合
单层双向 LSTM 能感知短窗口内的变化,但如果只靠一层,模型输出离原始输入太近,对“周期性”这类跨时间尺度的特征不够敏感。堆叠两层后,底层直接接触原始负荷序列,学到的是最近一两小时的趋势、爬坡速率这类局部特征;顶层输入是底层的隐藏状态序列,时间跨度更长,模型可以组合出日周期、周周期这样的高层模式。这和卷积神经网络里浅层学边缘、深层学形状的思路是相通的。
在负荷预测项目中,两层到三层双向堆叠是工程上比较常见的区间。两层结构已经在大多数数据集上表现出足够强的拟合能力;三层可以再往上提一点精度,但训练时间和过拟合风险都会增加。要不要加第三层,不是看训练集损失,而是看验证集 MAPE 是否真的在降。这一点我在第 5 章的避坑里会展开说。
2.4 Java场景下的选型:DL4J为什么是常见选择
如果模型层跑在 Python 上,Java 业务层就得通过 HTTP 或 RPC 调用,每次预测多一次跨进程通信,还得维护一套 Python 环境、一套模型管理接口。把深度学习直接放进 JVM,DL4J(Deeplearning4j)是使用最多的方案。它的底层是 ND4J 多维数组计算库,API 层提供 MultiLayerNetwork 和 ComputationGraph 两种网络容器,层类型里内置了 LSTM,也提供了 Bidirectional 包装层用来实现双向结构。
选 DL4J 还有一层现实考量:模型训练完直接序列化保存为 zip 文件,Java 服务启动时加载一次,后续推理在进程内完成,单次预测延迟能压到毫秒级。这对改造现有电力业务系统非常友好——不需要额外部署模型服务,不需要引入新的中间件,只把一个 jar 包加进依赖,就能在原有工程里把预测能力跑起来。JDK 8 以上的环境都能用,和 Spring Boot 这类常见框架也能无缝集成。
3. 先把数据变成网络能吃的样本:CSV读取、归一化与滑动窗口
3.1 数据格式与特征设计
原始数据通常是一张 CSV,最少包含“时间戳”和“负荷”两列。负荷单位可能是 kW 也可能是 MW,建议先统一单位再处理,网络本身不关心绝对量级,关键在于归一化和窗口对齐保持一致。如果业务方有条件提供温度、湿度或节假日标记,可以在预处理阶段把这些信息提出来。
常见做法是从时间戳里提取小时、星期、节假日三个特征。小时的周期性可以用 sin/cos 编码,星期同理,节假日直接用 0/1 标记。特征维度从 1 扩到 5 或更多,模型输入就不再是单条负荷曲线,而是多个特征在时间轴上的并行序列。第一版项目我一般建议先用“单负荷”跑通全流程,后续再加特征。单特征版本能出合理结果,说明数据管线没问题,再加温度、节假日时排查起来也容易定位是哪一步出了问题。
3.2 Min-Max归一化:代码与边界
LSTM 的门控单元用 sigmoid 和 tanh 作为激活函数,输入数值一旦跨度太大,梯度传播会异常。归一化是数据管线里不能省的一步。我常用 Min-Max 归一化,把数值压到 [0, 1] 区间:
import java.util.Arrays; public class MinMaxScaler { private double min; private double max; public void fit(double[] values) { this.min = Arrays.stream(values).min().orElse(0.0); this.max = Arrays.stream(values).max().orElse(1.0); } public double normalize(double value) { return (value - min) / (max - min + 1e-8); } public double[] normalize(double[] values) { double[] out = new double[values.length]; for (int i = 0; i < values.length; i++) { out[i] = normalize(values[i]); } return out; } public double[] inverse(double[] values) { double[] out = new double[values.length]; for (int i = 0; i < values.length; i++) { out[i] = values[i] * (max - min + 1e-8) + min; } return out; } }这段代码里有几个细节:fit到底该在哪个数据集上调用,后面避坑里专门讲,原则是只用训练集来求 min 和 max;1e-8是为了防止某段数据全为同一个值时 max-min 等于 0 导致除零异常。inverse方法是预测后把结果还原回真实量纲用的,很多人在这个环节翻车,预测值一直在 0 到 1 之间打转,最后才发现没有逆归一化。
3.3 滑动窗口:把时间序列切成监督学习样本
神经网络不像 ARIMA 那样直接吃整段序列,它需要把数据切成“特征窗口”和“标签窗口”。窗口大小选取原则是覆盖负荷的主要周期:小时级数据至少覆盖 24 小时,想看到周周期就取 168。我一般先用 72 或 120 试跑,效果好再提高;窗口越大训练越慢,但信息也更充分。
import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.factory.Nd4j; import org.nd4j.linalg.primitives.Pair; public class SequenceDatasetBuilder { /** * 构建训练样本 * @param data 归一化后的负荷序列 * @param windowSize 输入窗口长度(小时数) * @param horizon 预测未来第几步 */ public static Pair<INDArray, INDArray> build(double[] data, int windowSize, int horizon) { int n = data.length; int sampleCount = n - windowSize - horizon + 1; if (sampleCount <= 0) { throw new IllegalArgumentException("数据长度不足以生成样本"); } // DL4J RNN 输入形状: [样本数, 特征数, 序列长度] INDArray features = Nd4j.create(sampleCount, 1, windowSize); // 输出与输入序列对齐: [样本数, 输出数, 序列长度] // 序列上每个位置 t 的标签是 t + horizon 时刻的真实负荷 INDArray labels = Nd4j.create(sampleCount, 1, windowSize); for (int s = 0; s < sampleCount; s++) { for (int t = 0; t < windowSize; t++) { features.putScalar(new int[]{s, 0, t}, data[s + t]); labels.putScalar(new int[]{s, 0, t}, data[s + t + horizon]); } } return Pair.of(features, labels); } }解释一下这个对齐方式。DL4J 的 RnnOutputLayer 要求输出序列长度和输入序列长度一致,所以我把每个时间步都作为监督点:输入第 t 个位置的值是data[s + t],对应的标签是data[s + t + horizon]。你预测 “未来 1 小时”就设 horizon=1,预测 “未来 24 小时”就设 horizon=24。这样每个样本里 windowSize 个位置都有监督信号,训练信息非常充分,预测时取模型输出序列的最后一个位置即可。
3.4 训练集、验证集、测试集切分
样本生成完成后,切分数据集有一个铁律:不能随机打乱。时序数据和图像数据不一样,随机打乱会把未来的信息混进训练集,模型学到的“预测能力”实际是“偷看答案”。我习惯按时间顺序切:前 70% 训练,中间 15% 验证,最后 15% 测试。
int trainEnd = (int) Math.floor(totalNorm.length * 0.7); int valEnd = (int) Math.floor(totalNorm.length * 0.85); double[] trainNorm = Arrays.copyOfRange(totalNorm, 0, trainEnd); double[] valNorm = Arrays.copyOfRange(totalNorm, trainEnd, valEnd); double[] testNorm = Arrays.copyOfRange(totalNorm, valEnd, totalNorm.length);这里要注意一个细节:MinMaxScaler只允许在 trainNorm 上调用fit,valNorm 和 testNorm 直接复用同一组 min/max 做变换。如果对三段数据分别求 min/max,测试集的分布信息就通过归一化参数泄漏到了模型里,最终评估指标会虚高,换到真实场景立刻现原形。数据跨越多个季度时,我建议用最近 12 个月的数据做训练,而不是拿三年前的数据来凑样本量,毕竟负荷模式会随着产业结构和用电习惯变化。
4. 用DL4J构建双向堆叠LSTM:核心配置与训练流程
4.1 网络结构:各层维度和参数怎么定
双向堆叠 LSTM 的标准结构分三层:第一层双向 LSTM 抓局部时序特征,第二层双向 LSTM 在更高抽象层级上组合特征,最后接一个 RnnOutputLayer 做数值回归输出。隐藏单元数的选择逻辑是,数据量大时取 64 和 32,数据量小或只是快速验证可行性时取 32 和 16。
有个很容易踩的维度坑:Bidirectional 包装层会把正向和反向两个 LSTM 的输出拼接起来,所以它的实际输出维度是隐藏单元数的两倍。第一层设了 nOut=64,输出给下一层的是 128 维,第二层的 nIn 必须写成 128。如果不注意这个 2 倍关系,模型初始化时就会报维度不匹配。下表是完整的结构参数:
| 层序号 | 类型 | 输入维度 | 输出维度 | 说明 |
|---|---|---|---|---|
| 0 | Bidirectional LSTM | 1 | 64 | 双向,隐藏单元 64 |
| 1 | Bidirectional LSTM | 128 | 32 | 注意输入是 2×64 |
| 2 | RnnOutputLayer | 64 | 1 | 2×32,线性输出做回归 |
4.2 构建代码与参数说明
import org.deeplearning4j.nn.conf.MultiLayerConfiguration; import org.deeplearning4j.nn.conf.NeuralNetConfiguration; import org.deeplearning4j.nn.conf.InputType; import org.deeplearning4j.nn.conf.layers.LSTM; import org.deeplearning4j.nn.conf.layers.RnnOutputLayer; import org.deeplearning4j.nn.conf.layers.recurrent.Bidirectional; import org.deeplearning4j.nn.multilayer.MultiLayerNetwork; import org.deeplearning4j.nn.weights.WeightInit; import org.nd4j.linalg.activations.Activation; import org.nd4j.linalg.learning.config.Adam; import org.nd4j.linalg.lossfunctions.LossFunctions; public class BidirectionalStackedLstmBuilder { public static MultiLayerNetwork build(int inputSize, int learningRate) { MultiLayerConfiguration config = new NeuralNetConfiguration.Builder() .seed(12345L) .weightInit(WeightInit.XAVIER) .updater(new Adam(learningRate)) .list() // 第一层双向 LSTM .layer(0, new Bidirectional( new LSTM.Builder() .nIn(inputSize) .nOut(64) .activation(Activation.TANH) .build())) // 第二层双向 LSTM,输入维度必须是 2*64 .layer(1, new Bidirectional( new LSTM.Builder() .nIn(128) .nOut(32) .activation(Activation.TANH) .build())) // 回归输出层,输出维度 = 2*32 .layer(2, new RnnOutputLayer.Builder(LossFunctions.LossFunction.MSE) .nIn(64) .nOut(1) .activation(Activation.IDENTITY) .build()) .setInputType(InputType.recurrent(inputSize, 1)) .build(); MultiLayerNetwork model = new MultiLayerNetwork(config); model.init(); return model; } }这里要重点说三个参数。weightInit(WeightInit.XAVIER)是个好习惯,Xavier 初始化在 LSTM 这种带 tanh 激活的结构里收敛更稳定,比默认初始化省去很多调参时间。Activation.TANH是 LSTM 隐藏层的标准选择,不要随便换成 ReLU——ReLU 在 LSTM 中容易出现数值爆炸。输出层用Activation.IDENTITY线性激活,因为回归输出的数值范围不该被限制在 [0,1] 这类区间里,网络应该自由输出任意实数。
Bidirectional包装层的详细用法在不同 DL4J 版本里包路径略有差异,IDE 里提示找不到类时,搜索一下recurrent.Bidirectional或conf.layers.Bidirectional就能定位。里面传的 LSTM 层只配置一次,库内部会自动创建正向和反向的实例并处理拼接逻辑,不需要手动写复制序列的操作。
4.3 训练循环:早停策略与损失监控
数据准备好了,模型结构也搭好了,接下来是训练环节。训练直接调用model.fit即可,但我强烈建议手动写训练循环,因为只有手动循环才能做早停和验证集评估。DL4J 的MultiLayerNetwork.fit虽然方便,但内部不暴露验证集逻辑。
import org.deeplearning4j.nn.multilayer.MultiLayerNetwork; import org.nd4j.linalg.api.ndarray.INDArray; public class Trainer { public static MultiLayerNetwork train(MultiLayerNetwork model, INDArray trainFeatures, INDArray trainLabels, INDArray valFeatures, INDArray valLabels, int maxEpochs) { double bestValLoss = Double.MAX_VALUE; int patience = 0; for (int epoch = 1; epoch <= maxEpochs; epoch++) { model.fit(trainFeatures, trainLabels); double trainLoss = model.score(); double valLoss = computeMse(model, valFeatures, valLabels); System.out.printf("epoch=%d trainLoss=%.4f valLoss=%.4f%n", epoch, trainLoss, valLoss); if (valLoss < bestValLoss) { bestValLoss = valLoss; patience = 0; } else { patience++; if (patience >= 5) { System.out.println("early stop at epoch " + epoch); break; } } } return model; } private static double computeMse(MultiLayerNetwork model, INDArray features, INDArray labels) { INDArray output = model.output(features, false); return output.sub(labels).mul(output.sub(labels)).meanNumber().doubleValue(); } }早停的 patience 取值要结合验证集波动来看。负荷数据包含节假日、极端天气等异常点,验证集损失曲线天然有波动,patience 取 5 到 10 比较稳妥。取 3 会过早停止,模型还没学到周期模式就停了;取 20 又太长,后半程几乎在过拟合数据噪声。我一般先用 patience=5 跑一遍,看验证集曲线如果还有明显下降趋势,再调大到 10 重训。
maxEpochs 我习惯设 50,但配合早停通常跑 15 到 30 轮就停了。如果你发现 50 轮还没触发早停,说明验证集一直在改善,那可以把 patience 调大继续跑,但也要警惕是不是验证集太小导致噪声被当成信号。
4.4 模型保存、加载与“文档说明”的存档结构
训练完成后,模型必须序列化保存,下次直接用,不用重新训练。DL4J 提供ModelSerializer:
import org.deeplearning4j.util.ModelSerializer; // 保存模型到磁盘 ModelSerializer.writeModel(model, "model/lstm_forecast_v1.zip", true); // 加载模型 MultiLayerNetwork restored = ModelSerializer.restoreMultiLayerNetwork( "model/lstm_forecast_v1.zip");模型文件只是系统的一部分,更重要的是把配套参数一起存档。归一化器的 min/max、训练时的 windowSize、horizon、隐藏单元数、最佳 epoch、验证集 MAPE,这些信息全部要落到文件里,否则三个月后回头看这个模型,你根本不知道它当时是怎么训出来的。我现在每个模型目录固定放这套文件:
model/ ├── lstm_forecast_v1.zip # DL4J 序列化模型 ├── scaler_min_max.json # 归一化参数 ├── model_config.json # windowSize, horizon, 隐藏单元, 最佳 epoch └── training_loss.csv # 训练时的 loss 记录这份存档结构就是标题里“文档说明”的落地形态。一个可维护的预测系统,源码只占一半,另一半是能让别人接手的数据说明、模型记录和运行步骤。我见过太多项目只留了一个 zip 模型文件,换个人接手时完全不知道归一化区间是多少,预测结果错得离谱。
5. 避坑:双向堆叠LSTM电力负荷预测里最容易翻车的5个点
5.1 预测曲线整体滞后,模型在“抄作业”
现象:模型在测试集上画出来的预测曲线,整体比真实负荷曲线晚一两个小时,形状高度相似,但就是有肉眼可见的偏移。误差指标看着还行,但调度人员拿到这种预测根本不敢用。
原因:小时级负荷数据连续性很强,t 时刻的负荷和 t-1 时刻的负荷高度相关。当 horizon=1 时,模型发现最简单的降损失方案是“复制最近一个值”——上一小时多少,这一小时几乎也是多少,损失就已经很低了。它根本没有去学日周期、周周期,而是走了捷径。
解决:三个手段叠加使用。第一个是把 horizon 调大,用 4 小时或 24 小时替代 1 小时,强制模型必须往前看更远。第二个是加入“小时”“星期几”这些时间特征,模型想准确预测就必须依赖这些特征而不是复制。第三个是用差分序列代替原始序列,把相邻时刻的差值作为预测目标,从数据层面消掉自相关性。
5.2 归一化时把未来信息泄漏进了训练集
现象:训练集和验证集的损失都低得离谱,RMSE 甚至小于 0.01,你觉得模型完美了。但拿出真实历史数据做“模拟上线”,预测结果一塌糊涂,完全不像测试集那么准。
原因:这是数据泄漏的典型表现。很多人写代码时先对整个数据集求 min/max,再切分训练验证测试。测试集的最大值、最小值已经通过归一化参数传给了模型。模型在做“开卷考试”,自然得分高。还有一个隐蔽版本:用测试集数据做过缺失值填充,同样属于泄漏。
解决:严格按“先切分,再 fit”的顺序。MinMaxScaler只允许在训练集上调用 fit,验证集和测试集复用同一组 min/max。回看第 3.4 节的代码,scaler.fit(trainNorm)之后再用scaler.normalize处理其他集合。训练阶段不要碰测试集,包括查看测试集统计值做参数调整也不行。
5.3 训练时损失函数变成 NaN,模型直接崩溃
现象:训练到第 5 轮左右,控制台打印的 loss 突然变成 NaN,之后再怎么调都回不来。检查数据和模型配置都没发现明显错误。
原因:最常见的是学习率太大,Adam 的步长跨度过大导致梯度爆炸;其次是输入数据里存在 NaN 或 Infinity,比如 CSV 中某个时间点为空,Java 解析时直接把 null 转成了 0 或 NaN;还有一个可能是个别样本归一化后仍然出现极端尖峰,比如数据记录错误导致的瞬间超大负荷值。
解决:先查数据源的质量,逐列看有没有空值和异常尖峰。然后检查归一化结果,确认数值落在 [0,1] 区间内。最后把学习率从 0.001 降到 0.0001 重训一次。如果问题依旧,把 Adam 的 epsilon 参数从默认值调整到 1e-8 附近的显式值来提升数值稳定性。逐个排查,基本能定位到是数据问题还是超参问题。
5.4 堆叠过深,验证集误差不降反升
现象:想当然地从两层双向 LSTM 加到三层,训练集损失继续下降,看起来很漂亮,但验证集 MAPE 反而上涨了 10% 以上。
原因:双向结构本身就把参数翻了一倍,再加一层意味着参数量进一步膨胀。负荷数据的复杂程度有限——24 小时周期、7 天周期、季节性趋势,再加一些外部扰动,两层双向 LSTM 的表达能力已经接近上限。第三层开始,模型学的是训练集里的噪声和偶然性,而不是普遍规律,过拟合就发生了。
解决:把堆叠层数锁定在两层,把精力放在数据质量和特征工程上。同时给每层 LSTM 加 dropout,DL4J 里在 LSTM.Builder 上加.dropOut(0.2),可以在不破坏序列依赖的前提下抑制过拟合。如果两层还过拟合,先降隐藏单元数,再考虑加 dropout,而不是加层数。
5.5 训练与推理张量形状不一致,预测值错位
现象:模型训练完成后,调用model.output做预测时抛维度不匹配异常,或者不报错但预测值明显不对。检查半天才发现是输入数据形状的问题。
原因:训练时特征张量是[batchSize, 1, windowSize]的三维结构,但推理时直接拿一维数组或二维数组喂给了模型。DL4J 对 RNN 的输入有严格形状要求,少一个维度它不会帮你自动补。还有一种情况是训练时的 batch 大小和预测时不同,某些旧版本的内部状态没有正确重置。
解决:在推理入口统一做形状转换,写成一个公共方法:
INDArray input = Nd4j.create(new double[]{...}); // 一维原始数据 INDArray reshape = input.reshape(1, 1, windowSize); // 转成 [1, 1, windowSize] INDArray output = model.output(reshape); double predictValue = output.getDouble(0, 0, windowSize - 1);取输出时永远是最后一个时间步的位置,对应的是整个输入窗口之后那个时刻的预测值。我在这个过程踩过不止一次坑,后来固定用上述三行代码模板,不再手写形状转换。
6. 从模型到服务:验证、部署与持续打磨
6.1 上线前用连续时段验证,指标看 MAPE
模型训练完,别急着上线。测试集上随机抽几天看看效果是不够的,我习惯连续抽一周或一个月的完整预测结果来评估。指标改用 MAPE 而不是 MSE,MAPE 把误差变成了百分比,调度人员能直观理解误差水平。MAPE 在 5% 以内算优秀,8% 以内可接受,超过 10% 就需要继续调参或检查数据质量了。
public static double mape(double[] actual, double[] predicted) { double sum = 0.0; for (int i = 0; i < actual.length; i++) { sum += Math.abs((actual[i] - predicted[i]) / actual[i]); } return sum / actual.length * 100; }6.2 把模型接进 Spring Boot 服务
模型验证通过后,把它封装成 REST 接口最简单。服务启动时加载一次模型,后续请求直接复用,不需要每次重新读文件。关键点是归一化器要和模型一起加载,否则预测值量纲就是错的。
@RestController public class ForecastController { private final MultiLayerNetwork model; private final MinMaxScaler scaler; public ForecastController() throws IOException { this.model = ModelSerializer.restoreMultiLayerNetwork( "model/lstm_forecast_v1.zip"); // scaler 从 scaler_min_max.json 读取并填充 } @PostMapping("/forecast") public Map<String, Object> forecast(@RequestBody double[] lastWindow) { INDArray input = Nd4j.create(lastWindow).reshape(1, 1, lastWindow.length); INDArray output = model.output(input); double value = output.getDouble(0, 0, lastWindow.length - 1); return Map.of("load", scaler.inverse(new double[]{value})[0]); } }6.3 源码文档和维护节奏
系统源码的“文档说明”部分,我建议至少包含四块内容:README 写运行步骤和环境要求;data 目录说明数据字段和单位;model 目录记录模型参数和训练日志;docs 目录画模块调用关系。维护节奏上,每周跑一次真实预测对比,每月用最近 6 个月数据重训一次模型。负荷预测不是训一次就完事的模型,季节变化和用电结构调整都会让模型逐渐失效。
我做第一版这个系统时,最深刻的教训是忘记把归一化参数随模型一起保存,上线后预测结果直接错了一个数量级,排查了很久才发现是逆归一化用了错误的 min/max。从那以后,模型文件和 scaler 参数永远打包存放,并写进存档清单。这套流程你按步骤走下来,基本能避开我踩过的大部分坑。希望帮到你。
本文还有配套的精品资源,点击获取