最小二乘法 (OLS, Ordinary Least Squares)
- 公式:
-
推导: 暂无
-
计算:
- 斜率
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{n+1} = x{n} - * f'(x) * D - 差值:
dv = |x{n+1} - x{n}|
- 迭代:
/*
推导:
梯度下降的目的是为了更新向量 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)
- 公式:
- 计算:
- 偏导数 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
- 偏导数 k:
/*
推导:
批量梯度下降实际上是采用 最小二乘法 + 梯度下降 的方式去拟合函数
传统最小二乘法采用偏微分的方式做函数拟合, 而梯度下降求出的是局部最小值而不是整体最小值
拟合直线: 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)
- 公式:
小批量梯度下降 (MBGD, Mini-Batch Gradient Descent)
- 公式
/*
推导:
随机梯度下降相当于省略了 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)
}
算法结果测试
以传统 OLS 算法为标准, 在数据集样本不多的情况下, BGD 和 OLS 的结果基本趋于稳定, BGD, MBGD 和 SGD 在 “学习率” 设定正确的情况下, 基本上也都会趋于稳定, 所以设置 “学习率” 让其收敛应该是一个很重要的问题 (当然也有可能是算法实现出现了问题 orz
暂时没办法想明白的问题 = =
- 梯度下降的 “学习率” 的收敛问题, 为什么梯度下降时学习率一般设置为 0.01? 试验表明学习率过大很容易变动不收敛, 梯度下降收敛的边界情况是什么? 是否和数据集有关?
(GD 和 BGD 都会存在不收敛的情况
2. “学习率” 根据当前梯度进行动态条件目前看是属于一个优化的问题, 考虑算法中常用的归一化方案是否和梯度下降学习率的收敛有关