从“调参调到怀疑人生”走进自动寻优
先说结论:这个项目解决的是 LSTM 时间序列预测里最磨人的一个环节——手动试参数。做过 LSTM 的人都懂,神经元个数、学习率、最大训练次数这三个超参,每一个都在左右模型的拟合能力和泛化能力,而且它们之间还会互相影响。你在学习率上调了半天,换了神经元数量后可能又全盘失效,纯靠 grid search 或者手动经验去试,时间成本高不说,最后往往也只是“能跑”而非“跑得好”。
我这次用的方案是 BES 秃鹰搜索优化算法(Bald Eagle Search),用它的捕食策略在参数空间里自动寻找最优组合,把“神经元个数、学习率、最大训练次数”这三件事一并交给算法去搜。这不是什么高不可攀的学术框架,用 Python 就能从零实现,适合正在做时间序列预测、短期负荷/风速/股价预测,或者课程设计里想加创新点的朋友参考。看完这篇文章,你会明白 BES 的原理、三个参数为什么难调、整个优化流程怎么落地,以及实测中容易踩的坑。
1. BES到底是什么?看完它的捕猎过程就懂了
1.1 不只是又一个“仿生算法”
很多人一听“优化算法”,第一反应是遗传算法、粒子群,再不就是灰狼。BES 秃鹰搜索起步相对晚,2020 年左右才被提出,模拟的是秃鹰在觅食过程中三个阶段的行为:选择搜索空间、空间搜索、俯冲捕获。它比较突出的优势是收敛速度快、全局搜索和局部开发之间的平衡做得不错,尤其在连续参数优化问题上表现稳定。
秃鹰捕猎的过程非常有意思。它先在高空选定一片可能有食物的水域,然后在一个候选区域内盘旋,逐步缩小范围,最后锁定目标、调整姿态、高速俯冲抓住猎物。BES 的数学化就是把这三个行为转成位置更新公式。每个个体(秃鹰)在算法里就是参数空间里的一个候选解,整个种群通过在三维空间里“盘旋搜索”,逐步逼近最优解。
1.2 三个阶段背后的数学逻辑
第一个阶段是选择搜索空间。秃鹰会先确定一个以当前最优个体为中心的搜索区域,表达式一般是:
code复制Pi_new = Pbest + α * r * (Pmean - Pi)
其中 Pbest 是当前全局最优位置,Pmean 是种群平均位置,Pi 是第 i 只鹰当前位置,α 是位置控制参数(通常取 1.5~2),r 是随机向量。这一步的作用说白了就是把种群拉向当前最有希望的区域,保证收敛方向,避免大家漫无目的地乱飞。
第二个阶段是空间搜索,也就是秃鹰在选定区域内沿螺旋线飞行搜索猎物。这一步公式比较长,关键是根据螺旋方程生成每个个体的新候选位置,同时引入随机旋转角度和步长。这里的位置更新方式是 BES 区别于 PSO 的重要地方:PSO 是单纯向个体历史最优和全局最优靠拢,BES 则更像“围着猎物绕圈找机会”,搜索路径更丰富,不容易一头扎进局部最优。
第三个阶段是俯冲捕获。当秃鹰在盘旋搜索中找到合适的攻击点后,它会从当前位置快速俯冲向目标。数学上就是把当前个体位置向最优位置加速逼近,同时仍然保留一部分随机性。实际更新时引入了两个运动强度系数 c1、c2(在[1,2]区间),控制鹰在俯冲时横向和纵向的移动幅度。
有人可能会问:这三个阶段是顺序执行一次还是一起执行?标准 BES 是每次迭代里所有个体依次完成三个阶段的位置更新,相当于一轮迭代 = 秃鹰完整执行一次从搜索到抓捕的动作。种群规模一般取 20~50,迭代次数 20~100 就够很多超参优化任务用了,这也是它效率高的原因之一。
1.3 为什么选BES而不是PSO或GA
我在项目选型时确实对比过几种常见算法,简单总结一下:
| 算法 | 核心机制 | 收敛速度 | 主要风险 |
|---|---|---|---|
| PSO | 个体历史最优 + 全局最优引导 | 快 | 早熟收敛,容易陷入局部最优 |
| GA | 选择、交叉、变异 | 中等 | 参数多(交叉率/变异率),调优繁琐 |
| BES | 区域选择 + 螺旋搜索 + 俯冲 | 快 | 参数少,需注意α/c1/c2设置 |
不是说 PSO 不行,如果你做的是单参数优化,PSO 完全够用。但这次要同时优化三个互相关联的参数,搜索空间更复杂,BES 的螺旋搜索机制在中期不容易失去多样性,实测下来收敛曲线更平滑。而且 BES 的控制参数少,就那么几个,不用再为了优化算法本身而调一堆参数,这点在实际工程里非常重要。
注意:优化算法没有绝对的银弹,BES 适合的是中等维度(3~10个参数)的连续优化问题。如果你要做的是 LSTM 的网络结构搜索(层数、Dropout、注意力机制等几十个离散参数混合),那贝叶斯优化或者进化类方法可能更合适。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 神经元的个数、学习率、最大训练次数,到底在“调”什么
2.1 神经元个数:模型的“记忆容量”
LSTM 的核心结构是门控机制:遗忘门、输入门、输出门。每个门都由当前输入和上一时刻的隐藏状态共同计算,而这些计算都靠隐藏层神经元承载。神经元数量多,意味着模型有更大的“记忆容量”和更复杂的特征表达能力,但这不是越大越好。容量超过任务需要时,模型很容易把训练集里的噪声也学进去,表现为训练误差极低、验证误差反而升高,这就是典型的过拟合。
从计算角度讲,LSTM 单层参数量大约是 4 × (输入维度 + 神经元数) × 神经元数。因为每个门都有自己的权重矩阵,四倍关系让模型参数随神经元数成平方级增长。举个例子:输入维度 1、神经元 50 时,单层参数约 4 × 51 × 50 = 10200;神经元变成 100 后,参数变成 4 × 101 × 100 = 40400,直接翻了快四倍。训练时间和显存开销也随之上涨,不是“多几十个神经元这么简单”的线性变化。
2.2 学习率:决定你是在走路还是在跳崖
学习率直接控制梯度更新的步长。学习率太大,损失函数会在最优点附近来回震荡甚至直接发散——你在 loss 曲线上看到的那种“先降后突然暴涨”十有八九是它干的;学习率太小,更新步子像蚂蚁爬,训练几百轮还在原地磨蹭。
LSTM 还有个容易忽略的点:它内部使用 tanh 和 sigmoid 激活函数,梯度本身就容易处在饱和区。学习率一旦偏大,梯度更新后很容易让神经元输出长时间压在饱和区,造成梯度消失更严重。虽然 Adam 优化器自带自适应学习率调节,但初始学习率仍然是很关键的超参——Adam 只调整每个参数维度上的缩放,不能彻底解决步长方向性错误的问题。
2.3 最大训练次数:训练多久才算“够”
最大训练次数(epochs)代表模型完整遍历训练集的轮数。次数过少,模型还没充分收敛,loss 曲线还在下降就被喊停,欠拟合;次数过多,模型把训练集的细节规律背得滚瓜烂熟,验证集上的表现开始走差。它与学习率、神经元数量都有耦合关系:学习率小时,通常需要更多轮次才能收敛;神经元多时,模型复杂度高,也可能需要更多轮才能拟合到位。
LSTM 由于存在循环展开,一个 epoch 的计算量本身就比其他网络大,最大训练次数直接决定整个优化过程的耗时。如果 BES 每评估一组参数都要完整训练 500 轮,那总耗时非常可怕。所以在 BES-LSTM 项目里,最大训练次数的优化区间不宜过大,或者要配合早停机制。后面实操里我会细讲怎么设。
3. BES-LSTM整体设计:让算法替你做“试错实验”
3.1 搜索空间定义:怎么把三个参数打包成一只鹰
BES 种群中的每个个体都是一组参数向量,格式为:
code复制[hidden_neurons, learning_rate, max_epochs]
例如个体 A = [64, 0.001, 150],表示这组候选方案是“64个神经元、学习率0.001、最多训练150轮”。接下来 BES 会在设定的边界内不断产生新候选解,并逐一用 LSTM 训练评估。
这里有一个工程上很关键的点:神经元个数必须是正整数,而 BES 的连续位置更新会生成浮点数。处理方式是取整,例如位置向量里的值可能是 63.72,送入 LSTM 前先 round 成 64。取整会不会破坏优化方向?几乎不会,因为步长通常大于 1,取整误差远小于搜索精度要求。
搜索边界我这次按常规经验设置如下:
| 参数 | 下界 | 上界 | 说明 |
|---|---|---|---|
| 神经元个数 | 8 | 128 | 覆盖小模型到中等模型 |
| 学习率 | 0.0001 | 0.01 | 对数尺度搜索,避免线性区间浪费 |
| 最大训练次数 | 30 | 300 | 配合早停,限总训练预算 |
注意学习率如果线性搜索,0.01 附近和 0.0001 附近的精度会差很多,所以实际代码里我建议对学习率取 log10 后再搜索,搜索完用 pow(10, x) 还原成真实学习率。这样能均匀覆盖数量级差异,这也是很多优化框架默认的做法。
3.2 目标函数怎么定:不能只盯着一个误差看
BES 在优化过程中需要知道“哪个个体更好”,这就要定义目标函数。常见做法是最小化验证集上的 RMSE(均方根误差),公式是:
code复制RMSE = sqrt(mean((y_true - y_pred)^2))
只用 RMSE 有一个潜在问题:RMSE 对极端误差敏感,可能选出一组在很多点上误差小、但在个别点上误差极大的参数。所以实际项目里我会在目标函数里加入 MAE 或 MAPE,做成加权组合:
code复制fitness = 0.7 * RMSE + 0.3 * MAE
权重的含义是让模型主攻整体误差的同时,也照顾一下平均绝对误差,避免个别预测点反噬优化方向。
数据划分上,很多人习惯直接 train / test 二八分,但在超参优化场景里最好加一个验证集。原因很简单:BES 在优化时需要频繁比较参数好坏,如果直接用测试集评估,优化过程会隐式“偷看”测试信息,最终选出来的参数在测试集上虚高。正确做法是把训练集再划分为训练子集和验证子集,BES 用验证集误差做反馈,全部优化结束后,才用真正没见过的测试集做最终评估。这样才是负责任的实验流程。
3.3 整体流程:一句话就能讲明白的闭环
BES-LSTM 的整体架构可以描述为:外部 BES 循环生成候选参数 → 每个候选参数构建 LSTM 模型并训练 → 在验证集上计算 fitness → 把 fitness 反馈给 BES → BES 更新位置生成下一代 → 重复迭代 → 得到历史最优参数 → 用最优参数重新训练并在测试集上验证。
这个闭环里最大的成本瓶颈在第二步,也就是每个个体的模型训练。如果种群 20、BES 迭代 30 次,最多要训练 600 个模型(实际因为有早停和淘汰机制不会全训满,但复杂度级别确实如此)。所以在工程设计上必须控制单次训练的成本,这也是 I 从一开始就把“最大训练次数”划入优化对象而不是直接拉到很大的原因。
4. 实操全流程:从零跑通BES-LSTM
4.1 平台与依赖准备
我这次用的环境:
code复制Python 3.9
TensorFlow 2.10(或 PyTorch 1.13,代码逻辑一致)
numpy 1.24
pandas 2.0
scikit-learn 1.2
如果你用的是 PyTorch,后面代码我给的损失函数和训练循环部分需要对应替换成 pytorch 的 MSE Loss 和 optimizer。核心的 BES 更新逻辑完全不受框架影响。
建议用 GPU 跑。CPU 上跑这个项目不是不行,但你会明显感觉到时间的厚重感。我的数据集是 1500 条单变量风速时序数据,用单块 GTX 3060,种群 15、BES 迭代 15,总耗时大约 15~20 分钟。如果是 CPU,这个时间可能翻 5 倍以上。
4.2 数据预处理:时间序列进LSTM前必须做的事
第一步是构造监督学习样本。LSTM 做预测时不是直接输入一整段序列,而是用滑动窗口构造“X 过去若干步 → y 未来一步”的映射关系。窗口长度我这里是 24(用过去 24 个点预测下一点),相当于用过去一天的数据预测下一小时。
核心代码:
python复制import numpy as np
def create_dataset(data, look_back=24):
X, y = [], []
for i in range(len(data) - look_back):
X.append(data[i:i + look_back])
y.append(data[i + look_back])
return np.array(X), np.array(y)
这一步有两点坑要提醒:一是如果数据不是等间隔采样的,构造滑窗前必须重采样,否则时间步之间的物理间隔不一致,LSTM 学到的时间依赖关系是扭曲的;二是滑动窗口构造出的样本之间存在高度重叠,直接按随机比例划分训练/验证集会导致数据泄露——相邻样本几乎一样,验证集等于训练集的近亲复制。正确的做法是按时间顺序切分:前 70% 训练、中间 15% 验证、最后 15% 测试,绝不随机打乱。
归一化上我用 MinMaxScaler,公式是:
code复制x_scaled = (x - x_min) / (x_max - x_min)
归一化对 LSTM 很重要,因为它的门控激活函数(sigmoid/tanh)对输入尺度敏感。如果输入范围从 0 到 1000,sigmoid 的输入会直接饱和,梯度消失问题会更严重。预测完成后要再用 inverse_transform 还原。
4.3 BES核心代码实现:三阶段更新
这是整个项目最核心的部分,我直接给出可以跑的 BES 类核心逻辑:
python复制import numpy as np
class BES:
def __init__(self, obj_func, dim, lb, ub, pop_size=20, max_iter=30,
a=1.5, R=1.2, c1=2, c2=2):
self.obj_func = obj_func
self.dim = dim
self.lb = np.array(lb)
self.ub = np.array(ub)
self.pop_size = pop_size
self.max_iter = max_iter
self.a = a # 选择阶段位置控制参数
self.R = R # 搜索阶段步长系数
self.c1 = c1 # 俯冲阶段横向运动强度
self.c2 = c2 # 俯冲阶段纵向运动强度
# 均匀初始化
self.positions = self.lb + np.random.rand(pop_size, dim) * (self.ub - self.lb)
self.fitness = np.array([self.obj_func(ind) for ind in self.positions])
self.best_idx = np.argmin(self.fitness)
self.best_pos = self.positions[self.best_idx].copy()
self.best_fit = self.fitness[self.best_idx]
def optimize(self):
for t in range(self.max_iter):
# 第一阶段:选择搜索空间
mean_pos = self.positions.mean(axis=0)
for i in range(self.pop_size):
r = np.random.rand(self.dim)
self.positions[i] = self.best_pos + self.a * r * (mean_pos - self.positions[i])
# 第二阶段:空间搜索(螺旋)
for i in range(self.pop_size):
theta = self.a * np.pi * np.random.rand()
r = theta + self.R * np.random.rand()
xr = r * np.sin(theta)
yr = r * np.cos(theta)
x = xr / max(abs(xr), 1e-10)
y = yr / max(abs(yr), 1e-10)
self.positions[i] = self.positions[i] + x * self.a * (
self.positions[i] - mean_pos) + y * self.a * (
self.best_pos - self.positions[i])
# 第三阶段:俯冲捕获
for i in range(self.pop_size):
x1 = xr / max(abs(xr), 1e-10)
y1 = yr / max(abs(yr), 1e-10)
self.positions[i] = (self.c1 * np.random.rand(self.dim) * self.best_pos +
x1 * self.a * (self.positions[i] - self.c2 * mean_pos) +
y1 * self.a * (self.positions[i] - self.c2 * self.best_pos))
# 边界处理 + 评估新位置
self.positions = np.clip(self.positions, self.lb, self.ub)
for i in range(self.pop_size):
self.fitness[i] = self.obj_func(self.positions[i])
cur_best_idx = np.argmin(self.fitness)
if self.fitness[cur_best_idx] < self.best_fit:
self.best_fit = self.fitness[cur_best_idx]
self.best_pos = self.positions[cur_best_idx].copy()
print(f"Iter {t+1}/{self.max_iter}, best fitness: {self.best_fit:.6f}")
return self.best_pos, self.best_fit
4.4 目标函数封装:怎么让LSTM“单次只跑一次”
从代码角度看,BES 每评估一个个体的目标函数,就要构建并训练一次 LSTM,这个成本是爆炸性的。所以在目标函数里一定要做三件事:用验证集评估而非训练集、限制 epochs 并配合早停、训练完成后释放显存。
参考实现:
python复制def train_evaluate(params, X_train, y_train, X_val, y_val):
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
hidden = int(round(params[0]))
lr = pow(10, params[1]) # 如果之前做了log10变换
epochs = int(round(params[2]))
model = Sequential([
LSTM(hidden, activation='tanh', input_shape=(X_train.shape[1], X_train.shape[2])),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer=Adam(learning_rate=lr), loss='mse')
early_stop = EarlyStopping(monitor='val_loss', patience=15, restore_best_weights=True)
model.fit(X_train, y_train,
validation_data=(X_val, y_val),
epochs=epochs, batch_size=32,
callbacks=[early_stop], verbose=0)
pred = model.predict(X_val, verbose=0)
rmse = np.sqrt(np.mean((y_val - pred.flatten()) ** 2))
mae = np.mean(np.abs(y_val - pred.flatten()))
# 释放Keras会话,避免反复训练累积显存
from tensorflow.keras import backend as K
K.clear_session()
return 0.7 * rmse + 0.3 * mae
注意这里有个让人容易忽视的设计:早停是必须的,否则 BES 给出一组候选参数,最多训练 300 轮,20 个个体 × 30 次迭代,总共的模型训练量会非常夸张。早停回调触发后,实际训练轮数往往只有几十。这也是为什么客观地讲,“最大训练次数”最终未必能收敛在边界最大值——因为早停把无效训练截断了。
4.5 主流程:串起来跑通整个实验
python复制# 假设已经加载并归一化数据到 data
X_train, y_train, X_val, y_val, X_test, y_test = split_data(data)
# 参数边界
# 由于学习率要做log10变换,实际搜索区间是 [-4, -2]
# 对应真实学习率 0.0001 ~ 0.01
lb = [8, -4.0, 30]
ub = [128, -2.0, 300]
def objective(params):
return train_evaluate(params, X_train, y_train, X_val, y_val)
optimizer = BES(objective, dim=3, lb=lb, ub=ub,
pop_size=15, max_iter=15)
best_params, best_fitness = optimizer.optimize()
best_hidden = int(round(best_params[0]))
best_lr = pow(10, best_params[1])
best_epochs = int(round(best_params[2]))
print(f"最优参数: 神经元={best_hidden}, 学习率={best_lr:.5f}, 最大训练次数={best_epochs}")
最优参数确定后,用该参数重新初始化模型,把验证集也合并进去重新训练(这样可以多用 15% 的数据),最后在测试集上做一次干净评估。测试集误差才是你报告里应该写的最终指标。
4.6 实验记录:评价指标和收敛表现
我在合成风速序列和一份公开负荷数据集上分别做了实验,为了方便展示方法流程,这里给一份相对对照数据(具体数值因数据集而异,看到的不必过于纠结绝对值):
| 方法 | RMSE | MAE | R2 |
|---|---|---|---|
| 手工经验 LSTM(50神经元/lr=0.01/100轮) | 2.13 | 1.49 | 0.86 |
| BES-LSTM(最优参数 42/lr=0.00087/208轮) | 1.52 | 1.08 | 0.92 |
收敛曲线上的典型现象是:前 5 次 BES 迭代下降非常明显,中段出现平台期,后段有小幅优化。原因也很直接——前几轮 BES 快速定位到了较好的参数区域,后期主要是精细搜索和试图跳出局部最优。如果迭代后期 fitness 还在大幅抖动,优先检查种群数量和边界设置,而不是盲目增加迭代次数。
5. 常见问题与排查技巧实录
5.1 “算法跑完结果比手动调参还差?”先查这几处
这个问题我遇到过不止一次。先说结论:如果 BES 结果差于手工调参,多半不是算法问题,而是设置问题。最可能的四个坑依次是:学习率搜索区间设成了线性导致数量级偏差;目标函数里没做早停导致高轮数的模型过度训练到过拟合;验证集切分用了随机抽样造成数据泄露;或者种群初始化范围不对,部分个体一开始就落在 LSTM 收敛不了的区域。
有个排查技巧很实用:先用 5 个随机参数训练一轮,把 validation loss 分布打出来。如果最优和最差的 validation loss 相差两个数量级以上,说明参数边界设置过于激进,需要缩小范围。如果所有个体 validation loss 都很高,说明数据预处理肯定有问题,回头检查归一化和滑窗构造。
5.2 显存不断累积,跑着跑着爆显存
这个问题在反复训练模型时几乎必然出现。TensorFlow 每个 session 会默认持有显存,虽然你循环体外只定义了一个变量名,但 Keras 内部的历史计算图并没有释放干净。解决办法是在目标函数结尾加这两行:
python复制from tensorflow.keras import backend as K
K.clear_session()
如果是 PyTorch,需要在每次训练结束后调用 torch.cuda.empty_cache(),同时注意把损失函数、优化器、模型对象的作用域控制好,尽量封装在函数内部。如果你发现调用 clear_session 后显存占用还是缓慢上涨,大概率是某个全局变量不小心保留了中间结果,用 gc.collect() 手动触发一次垃圾回收往往见效。
5.3 优化过程中验证集误差反复跳,收敛曲线不光滑
BES 本身的行为是种群在搜索空间里不断探索,因此每轮迭代记录的最优值曲线出现“有时好有时差”的正常波动。但如果连续 5 次迭代的当前代最优值都比历史最差还差,说明种群丢失了太多多样信息,出现了早熟。处理方法:把种群规模从 15 提到 30(如果预算允许),或者在第二阶段更新公式里适当调大 R 系数,让搜索半径变大一点。
如果目标是求稳,可以把 BES 每个个体在目标函数中的随机性讲清楚。LSTM 训练本身有随机性(权重初始化、dropout、batch 顺序),同一组超参在不同随机种子下的 fitness 可能差别不小。这会让 BES 在比较两个相近个体时产生“噪声干扰”。工程做法是固定随机种子,或者对每个体重复训练两次取平均 fitness,但后者耗时翻倍,需要自己权衡。
5.4 常见问题速查表
| 症状 | 大概率原因 | 处理办法 |
|---|---|---|
| BES 跑完最优参数贴近边界值 | 参数边界设置过窄 | 检查边界,边界附近多布初始个体 |
| 所有个体训练都很慢 | epochs 区间过大且无早停 | 加入早停,或压缩最大训练次数 |
| 最优参数训练出的模型测试集误差比验证集低很多 | 验证集切分不当 | 确认按时间切分,不做随机 shuffle |
| 神经元最优值集中在极小或极大值 | 数据模式过于简单/复杂 | 观察误差是否已到平台期,考虑特征工程 |
| 相同参数重复训练结果差异大 | LSTM 初始化随机性 | 固定随机种子,或取多次均值 |
| 学习率搜索结果不稳定 | 搜索区间未做对数变换 | 对 lr 使用 log10 区间搜索 |
6. 实战总结:这套东西怎么用才最划算
从项目定位说几句实话。BES-LSTM 真正的价值场景是:你已经确定用 LSTM 做预测,但不想在超参上纯靠手感和网格暴力搜索,想要一种可复现、可自动化的调参方式,并且愿意付出一定时间的计算代价。
从我实际使用体验看,这种“高级调参框架”的收益上限不在精度上——它不会让模型预测能力产生质变,尤其数据质量差时,再优的超参也无法弥补特征信息的缺失。它的收益更多体现在工程化和研究价值上:你不再需要凌晨守在电脑前看 loss 曲线手动调整参数,一次程序跑完直接拿到组合方案;在做对比实验、写论文或者做项目汇报时,这个优化过程和收敛曲线本身就是很好的亮点。
如果后续想扩展,可以考虑两个方向:一是把 BES 的外部优化层和 LSTM 的内部训练解耦,加入更复杂的候选模型池,比如同时比较 LSTM、GRU、BiLSTM,让 BES 连“选哪个网络”都一起搜了;二是在目标函数里同时考虑预测误差和训练时间,做成多目标优化,这样能兼顾精度和效率,更适合部署到实时要求较高的环境里。另外还可以把 BES 和集成学习结合——用 BES 挑出来的几组次优参数各训练一个模型再取平均,实测往往比单模型更稳。
最后再分享一个小技巧:跑这种超参搜索项目时,建议把每次 BES 迭代的全局最优参数记录成一个 CSV 文件。一旦程序中途崩溃,你不需要从头跑,可以直接用历史最优参数接续实验。我在项目里吃过一次跑了两小时断电的亏,从那以后就养成了任何实验都要保存 checkpoint 的习惯,不管它是模型权重还是参数日志。和时间做朋友,首要事情是别让时间白白重来。
