
1. 从“炼丹”到“调参”为什么这一周的内容是深度学习的分水岭很多人学深度学习前面几周还能跟得上到了第三周就开始发懵。原因很简单前两周讲的是“网络怎么搭”这一周讲的是“网络怎么才能跑得好”。搭网络像搭积木照着图拼就行调参像炒菜火候、调料、下锅顺序差一点味道就完全不对。而这一周要解决的恰恰是深度学习里最“玄学”也最考验功力的部分——超参数调试、Batch正则化和程序框架。我自己刚开始做项目的时候最怕的就是训练不收敛。模型结构明明没问题数据也清洗过了但loss就是下不去或者训练集准确率很高、验证集一塌糊涂。后来才明白问题往往不在模型本身而在那些“训练之前就要定好”的参数上——学习率、batch size、动量系数、网络层数、每层神经元个数还有正则化项的系数。这些东西统称为超参数它们不是通过训练学出来的而是靠人在训练之前设定、训练之中调整的。这一周的内容之所以关键是因为它把“能跑通”和“跑得好”之间的那道墙给拆了。你会学到怎么系统地搜索超参数而不是靠瞎猜会学到Batch Normalization批归一化为什么能让网络更稳、更快、对初始化不那么敏感还会接触到TensorFlow这类程序框架知道怎么把前面手写的那些公式变成几行工程代码。对于已经写过一点神经网络、但训练效果总是不理想的人来说这一周就是分水岭。学明白了你调模型的速度会快一个量级学不明白就只能继续在“改一个参数跑一次”的循环里耗时间。我写这篇笔记不是照搬课程PPT而是把我自己踩过的坑、试过的参数组合、以及那些文档里不会写的经验按“为什么这么选、怎么操作、出问题怎么查”的逻辑重新整理一遍。你如果正在被超参数折磨或者对Batch Normalization只知道概念不会用那这篇内容应该能帮你省下不少试错时间。2. 超参数调试别再用网格瞎搜了先搞清楚谁最重要2.1 超参数不是平等的先分优先级再谈搜索策略很多人一上来就列一张大表把学习率、batch size、层数、每层单元数、正则化系数、动量、学习率衰减率全列上然后开始网格搜索。结果跑了几十组发现效果提升微乎其微。问题出在超参数的重要性差异极大把低优先级的参数和高优先级的参数放在同一维度搜索纯属浪费算力。我的经验是按下面这个优先级来分优先级超参数影响程度调试建议最高学习率 α直接决定是否收敛、收敛快慢先粗调再精调优先于其他所有参数高动量 β通常0.9、学习率衰减策略影响收敛稳定性和最终精度学习率定好后调中隐藏层数、每层神经元数影响模型容量根据数据复杂度选不宜频繁调中Batch size影响训练速度和泛化受显存限制通常取2的幂低正则化系数 λ、dropout率影响过拟合程度最后微调为什么学习率排第一因为学习率错了其他参数再对也没用。学习率太大loss会震荡甚至发散学习率太小训练慢到你想砸键盘。我试过一个图像分类任务其他参数完全一样学习率从0.01改成0.001验证集准确率差了将近8个百分点。所以我的习惯是先把学习率调到一个能让loss稳定下降的值再动其他参数。2.2 随机搜索为什么比网格搜索更划算网格搜索听起来很规整但在高维空间里效率极低。假设你有5个超参数每个取10个候选值网格搜索要跑10^5100000组。而实际上很多超参数对结果的影响很小你花大量算力去遍历它们的组合不如把算力集中在少数关键参数上。随机搜索的逻辑是从每个超参数的分布中随机采样而不是遍历所有组合。有研究表明在同样的计算预算下随机搜索找到好参数的概率比网格搜索高得多。原因很简单当你不知道哪个参数更重要时随机采样能让每个参数都有机会被充分探索而网格搜索在低重要性参数上浪费了太多组合。我自己的做法是先对学习率做随机搜索范围取[0.0001, 1]按对数尺度采样因为学习率跨数量级变化。比如从10^-4到10^0之间随机取而不是均匀取。对数尺度很重要因为0.001和0.01之间的差异远比0.5和0.51之间的差异有意义。# 学习率的对数随机采样示例 import numpy as np def sample_learning_rate(): exponent np.random.uniform(-4, 0) # 10^-4 到 10^0 return 10 ** exponent for i in range(5): print(f第{i1}次采样学习率: {sample_learning_rate():.6f})跑几次你会看到采样值在0.0001到1之间分布但更偏向小值区域这符合实际调参的经验——好的学习率往往在0.001到0.01之间。2.3 为超参数选择合适的尺度不是所有参数都适合均匀分布除了学习率其他超参数也有尺度问题。比如动量β通常取值在0.9附近0.9和0.9001的差异可能不大但0.9和0.99的差异就很大。所以对β应该用1-β来做采样尺度。也就是说不直接采样β而是采样1-β范围取[0.001, 0.1]然后β1-(1-β)。这样采样出来的β会集中在0.9、0.99、0.999这些有意义的区域。再比如隐藏层单元数通常取2的幂像64、128、256、512。这种离散参数用随机搜索时直接从候选集合里随机选就行不用做连续采样。注意超参数搜索不是一次性的而是“粗调-精调”的循环。先在大范围内随机搜找到表现好的区域后再在这个区域附近缩小范围精调。我一般会做两到三轮第一轮跑20组左右第二轮在最好的3组附近再跑10组。3. Batch Normalization让深层网络不再“脾气古怪”3.1 为什么需要Batch Normalization内部协变量偏移的直观解释训练深层网络时一个很头疼的问题是前面层的参数一变后面层的输入分布就跟着变。后面层得不断适应这种变化训练效率自然低。这个问题叫内部协变量偏移Internal Covariate Shift。你可以把它想象成你在教一个人打球但每次他抬手的时候篮筐的高度都在变他根本没法形成稳定的肌肉记忆。Batch NormalizationBN的思路很直接在每一层激活函数之前把该层的输入强行拉回均值为0、方差为1的分布。这样后面层收到的输入就稳定了不用再“追着”前面层的变化跑。具体来说对一个mini-batch的数据BN做四步计算这个batch的均值 μ计算这个batch的方差 σ²归一化x_norm (x - μ) / sqrt(σ² ε)缩放和平移y γ * x_norm β其中γ和β是可学习的参数让网络自己决定要不要恢复原来的分布。ε是个很小的数比如1e-8防止除零。我一开始不理解为什么还要γ和β直接归一化不就行了吗后来想明白了如果每层都强行归一化网络的表达能力会受限。比如sigmoid激活函数在0附近近似线性归一化后所有值都挤在0附近非线性能力就丢了。加上γ和β网络可以学到“我需要多大的方差、什么样的均值”既稳定了训练又保留了表达能力。3.2 BN在训练和测试时的差异别在测试时用batch统计量BN在训练时用当前batch的均值和方差但测试时怎么办测试时可能只有一个样本没法算batch统计量。所以BN在训练时会维护一个移动平均的均值和方差测试时直接用这个移动平均。具体操作是训练时每跑一个batch就更新一次移动平均# 移动平均更新示例 running_mean momentum * running_mean (1 - momentum) * batch_mean running_var momentum * running_var (1 - momentum) * batch_var这里的momentum通常取0.9或0.99。测试时直接用running_mean和running_var做归一化不再计算当前batch的统计量。注意如果你在测试时忘了切换模式还用batch统计量结果会非常不稳定。尤其是batch size很小时测试结果会剧烈波动。我踩过这个坑当时测试准确率忽高忽低查了半天才发现是BN模式没切对。3.3 BN为什么能加速训练允许更大的学习率BN最直接的好处是允许使用更大的学习率。没有BN时学习率稍大一点梯度就会爆炸或消失有了BN每层的输入分布稳定梯度也更稳定学习率可以调大几倍甚至十倍。这意味着训练轮数可以大幅减少。另外BN还有轻微的正则化效果。因为每个batch的均值和方差都有噪声相当于给网络加了噪声能减少过拟合。所以用了BN之后有时可以适当降低dropout率甚至不用dropout。我实测过一个5层全连接网络在MNIST上训练。不用BN时学习率0.01就震荡训练到收敛要30个epoch用了BN后学习率0.1也能稳定下降15个epoch就收敛了验证集准确率还高了0.5个百分点。这个差距在深层网络上会更明显。3.4 BN的实现细节放在激活函数之前还是之后关于BN放在激活函数之前还是之后课程里讲的是放在激活函数之前即先BN再ReLU。但实际工程中两种做法都有。我自己的习惯是放在激活函数之前因为这样归一化的是线性变换的输出更符合“稳定输入分布”的初衷。另外BN层通常不接在输入层而是从第一个隐藏层开始。输入层的数据一般会做标准化预处理不需要再用BN。# PyTorch中BN的使用示例 import torch.nn as nn model nn.Sequential( nn.Linear(784, 256), nn.BatchNorm1d(256), # BN放在线性层之后激活函数之前 nn.ReLU(), nn.Linear(256, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Linear(128, 10) )注意BatchNorm1d用于全连接层BatchNorm2d用于卷积层。参数是上一层的输出通道数或特征数。4. 程序框架从手写公式到工程化实现4.1 为什么需要程序框架手写反向传播的痛前几周我们手写了前向传播和反向传播虽然能跑但有几个致命问题一是代码量大改一个结构就要重写很多地方二是容易出错尤其是链式法则求导符号一多就乱三是没法利用GPU加速训练大模型时慢得无法接受。程序框架如TensorFlow、PyTorch解决了这些问题。你只需要定义前向传播框架会自动做反向传播你只需要把数据放到GPU上框架会自动并行计算。这就像从手写汇编变成用高级语言效率提升不是一点半点。课程里主要讲了TensorFlow但我想说现在PyTorch在研究和工程中更流行两者思路相通。我下面会以TensorFlow为主同时补充PyTorch的对应写法方便你对照。4.2 TensorFlow的核心概念计算图、会话、占位符TensorFlow 1.x的核心是计算图。你先定义好所有的计算节点变量、操作但此时并不执行然后创建一个会话Session在会话里运行图才会真正计算。这种“先定义、后执行”的模式叫声明式编程。import tensorflow as tf # 定义计算图 X tf.placeholder(tf.float32, [None, 784]) # 占位符运行时喂数据 W tf.Variable(tf.zeros([784, 10])) # 变量训练中更新 b tf.Variable(tf.zeros([10])) y tf.nn.softmax(tf.matmul(X, W) b) # 定义损失和优化器 y_ tf.placeholder(tf.float32, [None, 10]) cross_entropy tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices[1])) train_step tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy) # 在会话中执行 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for i in range(1000): batch_xs, batch_ys mnist.train.next_batch(100) sess.run(train_step, feed_dict{X: batch_xs, y_: batch_ys})这段代码里tf.placeholder是占位符相当于函数的参数运行时才传入tf.Variable是变量训练过程中会更新tf.Session是会话负责执行计算图。注意TensorFlow 2.x已经默认启用Eager Execution不再需要手动建会话写法更接近PyTorch。如果你用的是2.x上面的tf.Session写法会报错。我建议新手直接学2.x或PyTorch1.x的图模式虽然理解原理有帮助但实际项目里已经很少用了。4.3 用框架实现一个完整模型从数据到训练下面我用TensorFlow 2.x的Keras接口写一个完整的MNIST分类模型包含BN层和dropout你可以直接复制运行。import tensorflow as tf from tensorflow.keras import layers, models # 1. 加载和预处理数据 (x_train, y_train), (x_test, y_test) tf.keras.datasets.mnist.load_data() x_train x_train.reshape(-1, 784).astype(float32) / 255.0 x_test x_test.reshape(-1, 784).astype(float32) / 255.0 y_train tf.keras.utils.to_categorical(y_train, 10) y_test tf.keras.utils.to_categorical(y_test, 10) # 2. 定义模型 model models.Sequential([ layers.Dense(256, input_shape(784,)), layers.BatchNormalization(), layers.Activation(relu), layers.Dropout(0.3), layers.Dense(128), layers.BatchNormalization(), layers.Activation(relu), layers.Dropout(0.3), layers.Dense(10, activationsoftmax) ]) # 3. 编译模型 model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losscategorical_crossentropy, metrics[accuracy] ) # 4. 训练 history model.fit( x_train, y_train, batch_size128, epochs20, validation_split0.1, verbose1 ) # 5. 评估 test_loss, test_acc model.evaluate(x_test, y_test) print(f测试集准确率: {test_acc:.4f})这个模型里BN层放在Dense之后、激活函数之前Dropout放在激活函数之后。学习率用Adam的默认值0.001batch size取128。跑20个epoch测试集准确率通常能到98%以上。4.4 框架选型TensorFlow还是PyTorch这个问题没有标准答案但我可以给你一些实际对比对比维度TensorFlowPyTorch学习曲线1.x较陡2.x平缓平缓接近Python原生调试体验图模式难调试Eager模式改善动态图调试直观部署生态TensorFlow Serving、TFLite成熟TorchServe、ONNX支持完善研究社区工业界用得多学术界用得多代码风格声明式为主命令式为主我自己的选择是做研究用PyTorch做部署用TensorFlow。但如果你刚开始学选哪个都行核心概念是相通的。把其中一个用熟另一个半天就能上手。5. 常见问题与排查技巧实录5.1 训练不收敛先查学习率再查数据训练不收敛是最常见的问题表现是loss不下降、震荡或变成NaN。我的排查顺序是学习率太大这是最常见的原因。把学习率缩小10倍试试如果loss开始下降说明之前太大了。数据没归一化输入数据范围差异大会导致梯度不稳定。把输入缩放到[0,1]或标准化到均值0、方差1。标签有问题检查标签是否和输入对应有没有错位。网络太深没有BN的深层网络容易梯度消失加BN或换ReLU激活。我踩过的坑有一次loss一直是NaN查了半天发现是学习率设成了1.0而数据没归一化。改成0.001并归一化后立刻正常了。5.2 BN层报错batch size太小或模式不对BN在batch size很小时比如1或2会报错或效果很差因为算出来的均值和方差噪声太大。如果显存不够只能用很小的batch可以考虑用Group Normalization或Layer Normalization替代。另一个常见错误是测试时忘了切换模式。在PyTorch里测试前要调用model.eval()训练前调用model.train()。在TensorFlow的Keras里model.fit和model.evaluate会自动切换不用手动管。5.3 超参数搜索太慢用并行和早停超参数搜索很耗算力我一般用两个技巧加速并行搜索同时跑多组参数用不同的GPU或不同的进程。TensorFlow和PyTorch都支持多GPU训练。早停如果一组参数在几个epoch后loss明显比之前差直接停掉不用跑完。我通常设patience3连续3个epoch验证集loss不下降就停。5.4 常见问题速查表问题现象可能原因解决方法loss震荡不下降学习率太大缩小学习率10倍loss变成NaN学习率太大或数据未归一化缩小学习率归一化数据训练集准确率高、验证集低过拟合加dropout、加正则化、加数据训练集准确率也低欠拟合增加网络容量、减小正则化BN层效果差batch size太小增大batch size或换归一化方法测试结果不稳定BN模式未切换测试时用eval模式训练速度慢未用GPU或batch size太小检查GPU适当增大batch size5.5 独家避坑技巧学习率预热和余弦退火这两个技巧课程里没细讲但实际项目里非常有用。学习率预热是指在训练最开始几个epoch把学习率从很小的值逐渐增加到设定值。这样可以避免训练初期因参数随机初始化导致的梯度爆炸。余弦退火是指训练后期把学习率按余弦曲线降到接近0让模型更精细地收敛。# 余弦退火学习率调度示例 import tensorflow as tf initial_learning_rate 0.001 decay_steps 1000 alpha 0.0 lr_schedule tf.keras.optimizers.schedules.CosineDecay( initial_learning_rate, decay_steps, alpha ) optimizer tf.keras.optimizers.Adam(learning_ratelr_schedule)我实测下来加了余弦退火后验证集准确率能再提升0.3到0.5个百分点而且训练后期loss更平滑。6. 我个人的调参流程与经验总结说了这么多原理和技巧最后分享一下我自己的调参流程。每次拿到一个新任务我会按这个顺序走数据预处理归一化、划分训练验证测试集。这一步不做后面全白搭。搭一个简单模型先不加BN、不加dropout用默认参数跑通确认数据管道没问题。调学习率用随机搜索找学习率范围[1e-4, 1e-1]对数采样。找到能让loss稳定下降的值。加BN和dropout在每层加BN在全连接层后加dropout0.2到0.5。观察验证集准确率变化。调网络结构增加层数或每层单元数直到验证集准确率不再提升。微调正则化调整dropout率和L2正则化系数控制过拟合。学习率调度加余弦退火或阶梯衰减做最后精调。这个流程走下来通常两三天能调出一个不错的模型。最忌讳的是一上来就搞复杂结构结果连baseline都跑不过。另外我强烈建议你养成记录实验的习惯。每次调参改了哪些参数、结果如何都记下来。我用的是一个简单的表格列包括实验编号、学习率、batch size、网络结构、BN有无、dropout率、验证集准确率、备注。跑了几十组之后你会慢慢对哪些参数敏感、哪些不敏感形成直觉。这种直觉比任何教程都值钱。最后说一个心态问题调参确实有“玄学”成分但绝不是纯运气。理解每个参数背后的原理知道它为什么影响训练你就能从“瞎试”变成“有方向地试”。这一周的内容本质上就是给你一张调参的地图。地图不能保证你一次走到终点但至少能让你不迷路。