共计 1819 个字符,预计需要花费 5 分钟才能阅读完成。
一、先搞懂这些数学原理
随机梯度下降 (SGD) 的核心是这两个公式:

- 损失函数:对于线性回归,常用均方误差 $J(\theta)=\frac{1}{2m}\sum_{i=1}^m(h_\theta(x^{(i)})-y^{(i)})^2$
- 参数更新:$\theta_j := \theta_j – \alpha \frac{\partial}{\partial\theta_j}J(\theta)$
这里的 $\alpha$ 就是学习率,控制着每次更新的步长。小批量梯度下降则是每次随机选取一小部分样本计算梯度,平衡了计算效率和稳定性。
二、Eigen 库 vs 原生数组
通过实测一个 10000×100 的矩阵乘法:
- Eigen 库用时:12.3ms
- 原生数组循环:47.8ms
Einer 库快的原因在于:
- 自动向量化(SIMD 指令优化)
- 延迟计算避免临时变量
- 更智能的内存对齐
但原生数组更适合极端内存受限的场景。
三、完整实现代码
#include <Eigen/Dense>
#include <random>
class SGDOptimizer {
public:
SGDOptimizer(double lr, int batch_size)
: learning_rate(lr), batch_size(batch_size) {}
void update(Eigen::MatrixXd& weights,
const Eigen::MatrixXd& gradients) {
// 防御性检查
assert(!gradients.hasNaN());
assert(weights.rows() == gradients.rows());
// 带动量项的参数更新
if (momentum_buffer.size() == 0) {
momentum_buffer = Eigen::MatrixXd::Zero(gradients.rows(), gradients.cols());
}
momentum_buffer = beta * momentum_buffer
+ (1-beta) * gradients;
weights -= learning_rate * momentum_buffer;
// 学习率衰减
learning_rate *= 0.995;
}
private:
double learning_rate;
int batch_size;
double beta = 0.9; // 动量系数
Eigen::MatrixXd momentum_buffer;
};
四、关键实现技巧
- 内存管理:
- 使用 Eigen::Map 直接操作现有内存
-
对于超大规模数据,采用分块加载
-
小批量采样:
std::vector<int> get_mini_batch(int total_samples) { static std::random_device rd; static std::mt19937 gen(rd()); std::shuffle(indices.begin(), indices.end(), gen); return std::vector<int>(indices.begin(), indices.begin() + batch_size); } -
学习率调整:
- 余弦退火:$\alpha_t = \alpha_{min} + \frac{1}{2}(\alpha_{max}-\alpha_{min})(1+\cos(\frac{t\pi}{T}))$
- 预热阶段:前 5% 的训练步数线性增加学习率
五、性能优化实测
| 线程数 | 每秒迭代次数 |
|---|---|
| 1 | 1250 |
| 4 | 3800 |
| 8 | 4200 |
注意:超过物理核心数后收益递减
六、生产环境建议
- 浮点溢出防护:
- 在 softmax 前减去最大值:$x_i – \max(x)$
-
对梯度进行裁剪:
gradient = gradient.cwiseMin(1.0).cwiseMax(-1.0) -
特征归一化 必须做:
- 标准差归一化:$(x-\mu)/\sigma$
-
范围归一化:$(x-x_{min})/(x_{max}-x_{min})$
-
梯度检查技巧:
- 数值梯度:$\frac{f(\theta+\epsilon)-f(\theta-\epsilon)}{2\epsilon}$
- 应与解析梯度误差小于 1e-7
七、遇到震荡怎么办?
当损失函数出现剧烈震荡时,建议检查:
1. 学习率是否过大(尝试减半)
2. 批量大小是否过小(适当增加)
3. 特征尺度是否差异过大(检查归一化)
4. 是否缺少动量项(尝试 beta=0.9)
思考题:如果震荡发生在训练后期,可能是什么原因?
正文完
