机器学习算法学习笔记 (1)

254 阅读4分钟

最小二乘法 (OLS, Ordinary Least Squares)

  • 公式:

image.png

image.png

  • 推导: 暂无

  • 计算:

    • 斜率 b = SUM(X[i] * Y[i] - AVER(X) * AVER(Y)) / SUM(X[i]^2 - AVER(X)^2)
    • 偏移 a = AVER(Y) - k * AVER(X)
    • 回归函数 f(x) = bx + a
/*
样本: 
[[1,3],[2,4],[3,7],[4,8],[5,6]]
推导:
最小二乘法是通过设置拟合直线 
-> f(x) = k * x + b
得出数据中每一项的误差(预测结果与输入数据)进行求和, 得出整体差值
D(X, Y) = SUM( (Y{i} - f(X{i}))^2 )
通过对 差值D 的函数求偏导数, 另其结果为 0 求出偏导数极值点
得出拟合函数中 k 与 b 的值
*/
// 设 X = [x], Y = y
// 则 k = SUM(X[i] * Y[i] - AVER(X) * AVER(Y)) / SUM(X[i]^2 - AVER(X)^2)
// b = AVER(Y) - k * AVER(X)
// f(x) = k * x + b
function OLS(S) { // 样本 S, 第一列为 X, 第二列为 Y
    const X = S.map(s => s[0]) // 样本 X
    const Y = S.map(s => s[1]) // 样本 Y
    const XA = AVER(X)
    const YA = AVER(Y)
    const SUM_S = SUM(S, ([xi, yi]) => xi * yi - XA * YA)
    const SUM_X = SUM(X, xi => Math.pow(xi, 2) - Math.pow(XA, 2))
    const k = SUM_S / SUM_X
    const b = YA - k * XA
    return (x) => k * x + b // f(x) = a * x + b
}

function SUM(X, F) {
    return X.reduce((a, b) => a + (F ? F(b) : b), 0)
}
function AVER(X, F) {
    return SUM(X, F) / X.length
}

梯度下降(GD, Gradient Descent)

梯度下降求区域极小值


/* 
推导:
梯度下降的目的是为了更新向量 X 使得更新后 f(X) 的值趋向于最小
通过使用微分来比较切点斜率的方式, 将 f(X) 的比较转换为更为简单的向量 X 的比较
(泰勒展开) -> f(x) = f(x{0}) + f'(x{0}) * (x - x{0}) + A // A: 无穷小量, f'(x{0}) 为斜率
(变换) -> f(x) - f(x{0}) = f'(x{0}) * (x - x{0})
(矩阵变换) 设 X = [x], X{0} = [x{0}] // 将推导拓展至多纬度
(变量带入) -> f(X) - f(X{0}) = (X - X{0}) * f'(X{0}) *  D // D: 增量 delta, f'(X{0}) 为梯度方向
因为 `X - X{0} = 向量 - 向量 = 向量 = 标量 * 单位向量` 设 单位向量为 V, 标量为 1 (即忽略标量)
    -> f(X) - f(X{0}) = V * f'(X{0}) *  D 
因为期望 V * f'(X{0}) * D < 0 
    // 我理解的是, 通过求向量所有轴上的偏微分形成一个新的函数, 再将函数求导根据相关曲率变换求取最大值 (梯度最大
    // 方向导数/求取最大梯度: https://zhuanlan.zhihu.com/p/24913912
    -> V = -(f‘(X{0}) / |f’(X{0})|) // V 是单位向量, 所以除以模, 反方向下降最快, 加负号
(带入) -> V = X - X{0} = -(f‘(X{0}) / |f’(X{0})|) * D // D 为标量
(化简标量) -> X - X{0} = - f'(X{0}) * D1 // D1 为新的标量, 即 D1 = D / |f'(X{0})|
(推导结果) -> X = X{0} - f'(X{0}) * D
*/ 
// x{n+1} = x{n} - f'(x) * D
function GD(x, d) { 
    let xn = f(x) // x{n+1}
    let dv = 9999999999 // x{n+1} - x{n}
    let i = 0
    while (dv > 0.000000001 && i < 9999) {
        i++
        x = xn
        xn = x - _f(x) * d // x{n+1} = x{n} - f'(x{n}) * D
        dv = Math.abs(xn - x)
    }
    return f(xn)
}

function f(x) { // f(x) = x^2
    return Math.pow(x, 2)
    // return Math.cos(x)
}
function _f(x) { // f'(x) = 2x
    return x * 2
    // return -Math.sin(x)
}

批量梯度下降 (BGD)

  • 公式:

image.png

image.png

image.png

  • 计算:
    • 偏导数 k: h{k}' = - SUM( Y{i} - h(X{i}) ) * X{i} / n
    • 偏导数 b: h{b}' = - SUM( Y{i} - h(X{i}) ) / n
    • 迭代 k: k{n+1} = k{n} - h{k}' * D
    • 迭代 b: b{n+1} = b{n} - h{b}' * D
    • 回归函数: h(x) = k * x + b
/*
推导:
批量梯度下降实际上是采用 最小二乘法 + 梯度下降 的方式去拟合函数
传统最小二乘法采用偏微分的方式做函数拟合, 而梯度下降求出的是局部最小值而不是整体最小值
拟合直线: h(x) = k * x + b
差值函数: D(X, Y) = SUM( (Y{i} - h(X{i}))^2 ) / 2 * n 
   一般采用 均方误差 函数, 另外由于梯度下降需要求导, 为了方便求导会多除以 2
梯度下降: x{n+1} = x{n} - f'(x) * k
针对拟合函数中的参数 k, b 求偏导数获得梯度 // 理论上应该可以将参数列表作为向量进行求导
   -> h{k}' = - SUM( Y{i} - h(X{i}) ) * X{i} / n
   -> h{b}' = - SUM( Y{i} - h(X{i}) ) / n
推导结果
   -> k{n+1} = k{n} + h{k}' * D
   -> b{n+1} = b{n} + h{b}' * D
*/
// k{n+1} = k{n} - h{k}' * D
// b{n+1} = b{n} - h{b}' * D
// h(x) = k * x + b
function BGD(S, D) {
   let i = 0
   let k = 0
   let b = 0

   function f(x) {
       return k * x + b
   }
   function _fk() {
       return - SUM(S, ([xi, yi]) => (yi - f(xi)) * xi) / S.length
   }
   function _fb() {
       return - SUM(S, ([xi, yi]) => yi - f(xi)) / S.length
   }

   let kn = k
   let bn = b
   let dv = 9999999

   while (dv > 0.00000001 && i < 9999) {
       i++
       k = kn
       b = bn
       kn = k - _fk() * D
       bn = b - _fb() * D
       dv = Math.abs(kn - k) + Math.abs(bn - b)
   }
   return (x) => k * x + b
}

function SUM(X, F) {
   return X.reduce((a, b) => a + (F ? F(b) : b), 0)
}

随机梯度下降(SGD, Stochastic Gradient Descent)

  • 公式:

image.png

小批量梯度下降 (MBGD, Mini-Batch Gradient Descent)

  • 公式

image.png

/*
推导:
随机梯度下降相当于省略了 BGD 中对数据集求均值对步骤, 即 
h{k}' = - SUM( Y{i} - h(X{i}) ) * X{i} / n
    -> h{k}' = - ( Y{i} - h(X{i}) * X{i} )
h{b}' = - SUM( Y{i} - h(X{i}) ) / n
    -> h{b}' = - ( Y{i} - h(X{i}) )
小批量梯度下降相当于求均值时取了数据集中的子集, 即
h{k}' = - SUM( Y{i} - h(X{i}) ) * X{i} 
    -> h{k}' = - STO{n}( Y{i} - h(X{i}) * X{i} ) 
h{b}' = - SUM( Y{i} - h(X{i}) )
    -> h{b}' = - STO{n}( Y{i} - h(X{i}) )
关于为什么随机梯度下降可以收敛: https://www.zhihu.com/question/27012077/answer/501362912
*/
// h(x) = k * x + b
function SGD(S, D) {
    return MBGD(S, D)
}
function MBGD(S, D, N) {
    let i = 0
    let k = 0
    let b = 0

    function f(x) {
        return k * x + b
    }
    function _fk() {
        return - STO(S, ([xi, yi]) => (yi - f(xi)) * xi, N)
    }
    function _fb() {
        return - STO(S, ([xi, yi]) => yi - f(xi), N)
    }

    let kn = k
    let bn = b
    let dv = 9999999

    while (dv > 0.00000001 && i < 9999) {
        i++
        k = kn
        b = bn
        kn = k - _fk() * D
        bn = b - _fb() * D
        dv = Math.abs(kn - k) + Math.abs(bn - b)
    }
    return (x) => k * x + b
}

// 如果 N 为 1 的话, 即为随机梯度下降, SGD 相当于小批量梯度下降的一个特例
function STO(X, F, N = 1) {
    const P = [...X]
    const Q = []
    for(let i = 0; i < N; i++) {
        const idx = Math.floor(Math.random() * P.length)
        const item = P.splice(idx, 1)
        Q.push(...item)
    }
    return SUM(Q, F) / Q.length
}

function SUM(X, F) {
    return X.reduce((a, b) => a + (F ? F(b) : b), 0)
}

算法结果测试

image.png

以传统 OLS 算法为标准, 在数据集样本不多的情况下, BGD 和 OLS 的结果基本趋于稳定, BGD, MBGD 和 SGD 在 “学习率” 设定正确的情况下, 基本上也都会趋于稳定, 所以设置 “学习率” 让其收敛应该是一个很重要的问题 (当然也有可能是算法实现出现了问题 orz

暂时没办法想明白的问题 = =

  1. 梯度下降的 “学习率” 的收敛问题, 为什么梯度下降时学习率一般设置为 0.01? 试验表明学习率过大很容易变动不收敛, 梯度下降收敛的边界情况是什么? 是否和数据集有关?

image.png (GD 和 BGD 都会存在不收敛的情况 2. “学习率” 根据当前梯度进行动态条件目前看是属于一个优化的问题, 考虑算法中常用的归一化方案是否和梯度下降学习率的收敛有关

image.png