C++随机梯度下降实战:从数学原理到高效实现

1次阅读
没有评论

共计 1819 个字符,预计需要花费 5 分钟才能阅读完成。

image.webp

一、先搞懂这些数学原理

随机梯度下降 (SGD) 的核心是这两个公式:

C++ 随机梯度下降实战:从数学原理到高效实现

  1. 损失函数:对于线性回归,常用均方误差 $J(\theta)=\frac{1}{2m}\sum_{i=1}^m(h_\theta(x^{(i)})-y^{(i)})^2$
  2. 参数更新:$\theta_j := \theta_j – \alpha \frac{\partial}{\partial\theta_j}J(\theta)$

这里的 $\alpha$ 就是学习率,控制着每次更新的步长。小批量梯度下降则是每次随机选取一小部分样本计算梯度,平衡了计算效率和稳定性。

二、Eigen 库 vs 原生数组

通过实测一个 10000×100 的矩阵乘法:

  • Eigen 库用时:12.3ms
  • 原生数组循环:47.8ms

Einer 库快的原因在于:

  1. 自动向量化(SIMD 指令优化)
  2. 延迟计算避免临时变量
  3. 更智能的内存对齐

但原生数组更适合极端内存受限的场景。

三、完整实现代码

#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;
};

四、关键实现技巧

  1. 内存管理:
  2. 使用 Eigen::Map 直接操作现有内存
  3. 对于超大规模数据,采用分块加载

  4. 小批量采样:

    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);
    }

  5. 学习率调整:

  6. 余弦退火:$\alpha_t = \alpha_{min} + \frac{1}{2}(\alpha_{max}-\alpha_{min})(1+\cos(\frac{t\pi}{T}))$
  7. 预热阶段:前 5% 的训练步数线性增加学习率

五、性能优化实测

线程数 每秒迭代次数
1 1250
4 3800
8 4200

注意:超过物理核心数后收益递减

六、生产环境建议

  1. 浮点溢出防护:
  2. 在 softmax 前减去最大值:$x_i – \max(x)$
  3. 对梯度进行裁剪:gradient = gradient.cwiseMin(1.0).cwiseMax(-1.0)

  4. 特征归一化 必须做:

  5. 标准差归一化:$(x-\mu)/\sigma$
  6. 范围归一化:$(x-x_{min})/(x_{max}-x_{min})$

  7. 梯度检查技巧:

  8. 数值梯度:$\frac{f(\theta+\epsilon)-f(\theta-\epsilon)}{2\epsilon}$
  9. 应与解析梯度误差小于 1e-7

七、遇到震荡怎么办?

当损失函数出现剧烈震荡时,建议检查:
1. 学习率是否过大(尝试减半)
2. 批量大小是否过小(适当增加)
3. 特征尺度是否差异过大(检查归一化)
4. 是否缺少动量项(尝试 beta=0.9)

思考题:如果震荡发生在训练后期,可能是什么原因?

正文完
 0
评论(没有评论)