機器學習演算法實現解析——liblbfgs之L-BFGS演算法
在博文“優化演算法——擬牛頓法之L-BFGS演算法”中,已經對L-BFGS的演算法原理做了詳細的介紹,本文主要就開原始碼liblbfgs重新回顧L-BFGS的演算法原理以及具體的實現過程,在L-BFGS演算法中包含了處理L1正則的OWL-QN演算法,對於OWL-QN演算法的詳細原理,可以參見博文“優化演算法——OWL-QN”。
1、liblbfgs簡介
liblbfgs是L-BFGS演算法的C語言實現,用於求解非線性優化問題。
liblbfgs的主頁:http://www.chokkan.org/software/liblbfgs/
下載連結(見上面的主頁連結):
https://github.com/downloads/chokkan/liblbfgs/liblbfgs-1.10.tar.gz
https://github.com/chokkan/liblbfgs
用於Windows平臺
2、liblbfgs原始碼解析
2.1、主要的結構
在liblbfgs中,主要的程式碼包括
liblbfgs-1.10/include/lbfgs.h
:標頭檔案liblbfgs-1.10/lib/lbfgs.c
:具體的實現liblbfgs-1.10/lib/arithmetic_ansi.h
(另兩個arithmetic_sse_double.h
和arithmetic_sse_float.h
是兩個彙編編寫的等價形式):相當於一些工具liblbfgs-1.10/sample/sample.c
2.2、工具arithmetic_ansi.h
在arithmetic_ansi.h
檔案中,主要是對向量(vector)的一些操作,這部分的程式程式碼比較簡單,在這裡簡單對沒個函式的作用進行描述,包括:
- 申請size大小的空間,同時對其進行初始化
void* vecalloc(size_t size)
- 1
- 釋放空間
void vecfree(void *memblock)
- 1
- 將向量x中的值設定為c
void vecset(lbfgsfloatval_t *x, const lbfgsfloatval_t c, const int n)
- 1
- 將向量x中的值複製到向量y中
void veccpy(lbfgsfloatval_t *y, const lbfgsfloatval_t *x, const int n)
- 1
- 取向量x中的每個值的負數,將其放到向量y中
void vecncpy(lbfgsfloatval_t *y, const lbfgsfloatval_t *x, const int n)
- 1
- 對向量y中的每個元素增加向量x中對應元素的c倍
void vecadd(lbfgsfloatval_t *y, const lbfgsfloatval_t *x, const lbfgsfloatval_t c, const int n)
- 1
- 計算向量x和向量y的差
void vecdiff(lbfgsfloatval_t *z, const lbfgsfloatval_t *x, const lbfgsfloatval_t *y, const int n)
- 1
- 向量與常數的積
void vecscale(lbfgsfloatval_t *y, const lbfgsfloatval_t c, const int n)
- 1
- 向量的外積
void vecmul(lbfgsfloatval_t *y, const lbfgsfloatval_t *x, const int n)
- 1
- 向量的點積
void vecdot(lbfgsfloatval_t* s, const lbfgsfloatval_t *x, const lbfgsfloatval_t *y, const int n)
- 1
- 向量的點積的開方
void vec2norm(lbfgsfloatval_t* s, const lbfgsfloatval_t *x, const int n)
- 1
- 向量的點積的開方的倒數
void vec2norminv(lbfgsfloatval_t* s, const lbfgsfloatval_t *x, const int n)
- 1
2.3、L-BFGS演算法的主要函式
在liblbfgs中,有很多利用匯編語言優化的程式碼,這裡暫且不考慮這些優化的程式碼,對於這些優化的程式碼,作者提供了基本的實現方式。
2.3.1、為變數分配和回收記憶體空間
函式lbfgs_malloc
是為優化函式中的變數分配記憶體空間的函式,其具體形式為:
// 為變數分配空間
lbfgsfloatval_t* lbfgs_malloc(int n)
{
// 涉及到彙編的一些知識,暫且不考慮
#if defined(USE_SSE) && (defined(__SSE__) || defined(__SSE2__))
n = round_out_variables(n);
#endif/*defined(USE_SSE)*/
// 分配n個大小的記憶體空間
return (lbfgsfloatval_t*)vecalloc(sizeof(lbfgsfloatval_t) * n);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
函式lbfgs_malloc
用於為變數分配長度為n的記憶體空間,其中,lbfgsfloatval_t
為定義的變數的型別,其定義為float
或者是double
型別:
#if LBFGS_FLOAT == 32
typedef float lbfgsfloatval_t;
#elif LBFGS_FLOAT == 64
typedef double lbfgsfloatval_t;
#else
#error "libLBFGS supports single (float; LBFGS_FLOAT = 32) or double (double; LBFGS_FLOAT=64) precision only."
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
與記憶體分配對應的是記憶體的回收,其具體形式為:
// 釋放變數的空間
void lbfgs_free(lbfgsfloatval_t *x)
{
vecfree(x);
}
- 1
- 2
- 3
- 4
- 5
2.3.2、L-BFGS中引數的初始化
函式lbfgs_parameter_init
提供了L-BFGS預設引數的初始化方法。
其實在L-BFGS的演算法過程中也回提供預設的引數的方法,所以該方法有點多餘。
// 預設初始化lbfgs的引數
void lbfgs_parameter_init(lbfgs_parameter_t *param)
{
memcpy(param, &_defparam, sizeof(*param));// 使用預設的引數初始化
}
- 1
- 2
- 3
- 4
- 5
函式lbfgs_parameter_init
將預設引數_defparam
複製到引數param
中,lbfgs_parameter_t
是L-BFGS引數的結構體,其具體的程式碼如下所示:
作者在這部分程式碼中的註釋寫得特別詳細,從這些註釋中可以學習到很多調參時的重要資訊。
2.3.3、L-BFGS演算法的核心過程概述
函式lbfgs
是優化演算法的核心過程,其函式的引數為:
int n,// 變數的個數
lbfgsfloatval_t *x,// 變數
lbfgsfloatval_t *ptr_fx,// 目標函式值
lbfgs_evaluate_t proc_evaluate,// 計算目標函式值和梯度的回撥函式
lbfgs_progress_t proc_progress,// 處理計算過程的回撥函式
void *instance,// 資料
lbfgs_parameter_t *_param// L-BFGS的引數
- 1
- 2
- 3
- 4
- 5
- 6
- 7
該函式通過呼叫兩個函式proc_evaluate
和proc_progress
用於計算具體的函式以及處理計算過程中的一些工作。
在函式lbfgs
中,其基本的過程為:
2.3.4、引數的宣告和初始化
在初始化階段涉及到很多引數的宣告,接下來將詳細介紹每一個引數的作用。
2.3.5、迴圈求解的過程
迴圈的求解過程從for
迴圈開始,在for
迴圈中,首先需要利用線搜尋策略進行最優的步長選擇,其具體的過程如下所示:
/* Store the current position and gradient vectors. */
// 儲存當前的變數值和梯度值
veccpy(xp, x, n);// 將當前的變數值複製到向量xp中
veccpy(gp, g, n);// 將當前的梯度值複製到向量gp中
/* Search for an optimal step. */
// 線搜尋策略,搜尋最優步長
if (param.orthantwise_c == 0.) {// 無L1正則
ls = linesearch(n, x, &fx, g, d, &step, xp, gp, w, &cd, ¶m);// gp是梯度
} else {// 包含L1正則
ls = linesearch(n, x, &fx, g, d, &step, xp, pg, w, &cd, ¶m);// pg是偽梯度
// 計算偽梯度
owlqn_pseudo_gradient(
pg, x, g, n,
param.orthantwise_c, param.orthantwise_start, param.orthantwise_end
);
}
if (ls < 0) {// 已達到終止條件
// 由於線上搜尋的過程中更新了向量x和向量g,因此從xp和gp中恢復變數值和梯度值
/* Revert to the previous point. */
veccpy(x, xp, n);
veccpy(g, gp, n);
ret = ls;
goto lbfgs_exit;// 釋放資源
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
由於線上搜尋的過程中會對變數x以及梯度的向量g修改,因此在進行線搜尋之前,先將變數x以及梯度g儲存到另外的向量中。引數param.orthantwise_c
表示的是L1正則的引數,若為0則不使用L1正則,即使用L-BFGS演算法;若不為0,則使用L1正則,即使用OWL-QN演算法。
關於線搜尋的具體方法,可以參見2.3.6。
對於owlqn_pseudo_gradient
函式,可以參見2.3.4
在OWL-QN中,由於在某些點處不存在導數,因此使用偽梯度代替L-BFGS中的梯度。
2.3.6、迴圈的終止條件
在選擇了最優步長過程中,會同時對變數進行更新,第二步即是判斷此時的更新是否滿足終止條件,終止條件分為以下三類:
- 是否收斂
vec2norm(&xnorm, x, n);// 平方和的開方
if (param.orthantwise_c == 0.) {// 非L1正則
vec2norm(&gnorm, g, n);
} else {// L1正則
vec2norm(&gnorm, pg, n);
}
- 1
- 2
- 3
- 4
- 5
- 6
// 判斷是否收斂
if (xnorm < 1.0) xnorm = 1.0;
if (gnorm / xnorm <= param.epsilon) {
/* Convergence. */
ret = LBFGS_SUCCESS;
break;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
收斂的判斷方法為:
|g(x)|max(1,|x|)<ε|g(x)|max(1,|x|)<ε
如果上式滿足,則表示已滿足收斂條件。
- 目標函式值是否有足夠大的下降(最小問題)
if (pf != NULL) {// 終止條件
/* We don't test the stopping criterion while k < past. */
// k為迭代次數,只考慮past>k的情況,past是指只保留past次的值
if (param.past <= k) {
/* Compute the relative improvement from the past. */
// 計算函式減小的比例
rate = (pf[k % param.past] - fx) / fx;
/* The stopping criterion. */
// 下降比例是否滿足條件
if (rate < param.delta) {
ret = LBFGS_STOP;
break;
}
}
/* Store the current value of the objective function. */
// 更新新的目標函式值
pf[k % param.past] = fx;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
在pf
中,儲存了param.past
次的目標函式值。計算的方法為:
fo−fnfn<Δfo−fnfn<Δ
- 是否達到最大的迭代次數
// 已達到最大的迭代次數
if (param.max_iterations != 0 && param.max_iterations < k+1) {
/* Maximum number of iterations. */
ret = LBFGSERR_MAXIMUMITERATION;
break;
}
- 1
- 2
- 3
- 4
- 5
- 6
如果沒有滿足終止的條件,那麼需要擬合新的Hessian矩陣。
2.3.7、擬合Hessian矩陣
在BFGS演算法(優化演算法——擬牛頓法之BFGS演算法)中,其Hessian矩陣為:
Hk+1=(I−skyTkyTksk)THk(I−yksTkyTksk)+sksTkyTkskHk+1=(I−skykTykTsk)THk(I−ykskTykTsk)+skskTykTsk
其中,yk=▽f(xk+1)−▽f(xk)yk=▽f(xk+1)−▽f(xk)步向量即可。其具體的計算方法為:
L-BFGS的具體原理可以參見“優化演算法——擬牛頓法之L-BFGS演算法”。
在上述過程中,第一個迴圈計算出倒數第mm代時的下降方向,第二個階段利用上面計算出的方法迭代計算出當前的下降方向。
根據上述的流程,開始擬合Hessian矩陣:
- 計算向量序列{sk}{sk}
// 更新s向量和y向量
it = &lm[end];// 初始時,end為0
vecdiff(it->s, x, xp, n);// x - xp,xp為上一代的值,x為當前的值
vecdiff(it->y, g, gp, n);// g - gp,gp為上一代的值,g為當前的值
- 1
- 2
- 3
- 4
- 兩個迴圈
// 通過擬牛頓法計算Hessian矩陣
// L-BFGS的兩個迴圈
j = end;
for (i = 0;i < bound;++i) {
j = (j + m - 1) % m; /* if (--j == -1) j = m-1; */
it = &lm[j];
/* \alpha_{j} = \rho_{j} s^{t}_{j} \cdot q_{k+1}. */
vecdot(&it->alpha, it->s, d, n);// 計算alpha
it->alpha /= it->ys;// 乘以rho
/* q_{i} = q_{i+1} - \alpha_{i} y_{i}. */
vecadd(d, it->y, -it->alpha, n);// 重新修正d
}
vecscale(d, ys / yy, n);
for (i = 0;i < bound;++i) {
it = &lm[j];
/* \beta_{j} = \rho_{j} y^t_{j} \cdot \gamma_{i}. */
vecdot(&beta, it->y, d, n);
beta /= it->ys;// 乘以rho
/* \gamma_{i+1} = \gamma_{i} + (\alpha_{j} - \beta_{j}) s_{j}. */
vecadd(d, it->s, it->alpha - beta, n);
j = (j + 1) % m; /* if (++j == m) j = 0; */
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
其中,ys和yy的計算方法如下所示:
vecdot(&ys, it->y, it->s, n);// 計算點積
vecdot(&yy, it->y, it->y, n);
it->ys = ys;
- 1
- 2
- 3
bound和end的計算方法如下所示:
bound = (m <= k) ? m : k;// 判斷是否有足夠的m代
++k;
end = (end + 1) % m;
- 1
- 2
- 3
2.3.8、線搜尋策略
在liblbfgs中涉及到大量的線搜尋的策略,線搜尋的策略主要作用是找到最優的步長。我們將在下一個主題中進行詳細的分析。
補充
1、回撥函式
回撥函式就是一種利用函式指標進行函式呼叫的過程。回撥函式的好處是具體的計算過程以函式指標的形式傳遞給其呼叫處,這樣可以較方便地對呼叫函式進行擴充套件。
假設有個print_result
函式,需要輸出兩個int型數的和,那麼直接寫即可,如果需要改成差,那麼得重新修改;如果在print_result
函式的引數中傳入一個函式指標,具體的計算過程在該函式中實現,那麼就可以在不改變print_result
函式的條件下對功能進行擴充,如下面的例子:
- frame.h
#include <stdio.h>
void print_result(int (*get_result)(int, int), int a, int b){
printf("the final result is : %d\n", get_result(a, b));
}
- 1
- 2
- 3
- 4
- 5
- 6
- process.cc
#include <stdio.h>
#include "frame.h"
int add(int a, int b){
return a + b;
}
int sub(int a, int b){
return a - b;
}
int mul(int a, int b){
return a * b;
}
int max(int a, int b){
return (a>b?a:b);
}
int main(){
int a = 1;
int b = 2;
print_result(add, a, b);
print_result(sub, a, b);
print_result(mul, a, b);
print_result(max, a, b);
return 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28