spark-liblinear是一个可用于大规模LR的分布式库,其核心迭代算法为信赖域牛顿法(Trust-region Newton method 简称TRON)。可能很多人并没有听说过这种算法,也不知为何要使用它,它与那些常见的SGD、L-BFGS等优化方法有何不同,下面让我们一探究竟。
1、从LR模型说起
LR模型常被用于二分类问题,给定数据
x
\textbf x
x 和权重
(
w
,
b
)
(\textbf w, b)
(w,b),可用如下概率公式来表示:
(1)
P
(
y
=
±
1
∣
x,w
)
=
1
1
+
exp
(
−
y
(
w
T
x
+
b
)
)
P(y=\pm 1|\textbf {x,w}) = \frac{1}{1+\exp(-y(\textbf w^T\textbf x+b))}\tag{1}
P(y=±1∣x,w)=1+exp(−y(wTx+b))1(1)
这里
y
y
y 是 label 标签。对于训练样本
x
i
,
i
=
1
,
.
.
.
,
l
\textbf x_i, i=1,...,l
xi,i=1,...,l,label w为
y
i
∈
{
−
1
,
+
1
}
y_i\in \{-1, +1\}
yi∈{−1,+1},通常最小化负对数似然来对参数
(
w
,
b
)
(\textbf w, b)
(w,b)进行估计:
(2)
min
w
,
b
∑
i
=
1
l
log
(
1
+
e
−
y
i
(
w
T
x
i
+
b
)
)
\min_{\textbf w, b}\quad\sum_{i=1}^l\log(1+e^{-y_i(\textbf w^T\textbf x_i+b)})\tag{2}
w,bmini=1∑llog(1+e−yi(wTxi+b))(2).
为了推导方便,我们一般将标量
b
b
b作为样本
x
\textbf x
x的一个维度考虑:
(3)
[
x
i
T
,
1
]
→
x
i
T
[
w
T
,
b
]
→
w
T
[\textbf x_i^T, 1]\to \textbf x_i^T\qquad [\textbf w^{T}, b]\to \textbf w^T\tag{3}
[xiT,1]→xiT[wT,b]→wT(3)
同时为了获得更好的泛化能力,我们将加入L2正则化项
w
T
w
/
2
\textbf w^T \textbf w/2
wTw/2,因此本文将考虑解决如下正则化LR问题:
(4) min w f ( w ) ≡ 1 2 w T w + C ∑ i = 1 l log ( 1 + e − y i w T x i ) \min_{\textbf w}f(\textbf w) \equiv\frac{1}{2}\textbf{w}^T\textbf w+C\sum^l_{i=1}\log(1+e^{-y_i\textbf w^T\textbf x_i}) \tag{4} wminf(w)≡21wTw+Ci=1∑llog(1+e−yiwTxi)(4)
2、牛顿法&截断牛顿法
为了解决问题(4),我们首先考虑牛顿法,它是一种二阶优化算法,需要计算
f
(
w
)
f(\textbf w)
f(w)的梯度与Hessian矩阵:
(5)
∇
f
(
w
)
=
w
+
C
∑
i
=
1
l
(
σ
(
y
i
w
T
x
i
)
−
1
)
y
i
x
i
∇
2
f
(
w
)
=
I
+
C
X
T
D
X
,
\nabla f(\textbf w) = \textbf w + C\sum^l_{i=1}(\sigma(y_i\textbf w^T\textbf x_i)-1)y_i\textbf x_i\\ \nabla^2f(\textbf w)=I + CX^TDX, \tag 5
∇f(w)=w+Ci=1∑l(σ(yiwTxi)−1)yixi∇2f(w)=I+CXTDX,(5)
牛顿法更新权重
w
\textbf w
w:
w
k
+
1
=
w
k
+
s
k
\textbf w^{k+1}=\textbf w^k+\textbf s^k
wk+1=wk+sk, 其中
s
k
\textbf s^k
sk是牛顿方向, 是如下线性系统的解:
(6)
∇
2
f
(
w
k
)
s
k
=
−
∇
f
(
w
k
)
\nabla^2f(\textbf w^k)\textbf s^k=-\nabla f(\textbf w^k) \tag 6
∇2f(wk)sk=−∇f(wk)(6)
但是,使用如上更新规则时会遇到两个问题:
- 序列 w k \textbf w^k wk 可能无法收敛到最优解,甚至不能确定函数的值会降低。
- 我们假设 X X X足够稀疏,但是 X T D X X^TDX XTDX会仍是稠密的,当数据规模很大时,Hessian矩阵难以存储。
我们可以通过调整牛顿方向的长度来解决第一个问题,当前的技术主要有两种:线搜索与置信域方法。我们可以这么理解线搜索方法就像年轻人的梦想:沿着搜索方向,勇往直前的寻求下降而不计后果。与之截然相反的是,信赖域方法则像四十而不惑的人,回首朝气蓬勃的年轻时代,首先考虑到的是在当前模型下走一步所产生的预期,随后通过函数值是否下降或者超过预期的程度来调整模型状态。
对于第二个问题,同样有两类方法可供选择:直接法与迭代法。迭代法主要是计算Hessian矩阵与向量
s
\textbf s
s的乘积:
(7)
∇
2
f
(
w
)
s
=
(
I
+
C
X
T
D
X
s
)
=
s
+
C
⋅
X
T
(
D
(
X
s
)
)
\nabla^2f(\textbf w) \textbf s=(I + CX^TDX\textbf s)=\textbf s+C\cdot X^T(D(X\textbf s)) \tag 7
∇2f(w)s=(I+CXTDXs)=s+C⋅XT(D(Xs))(7)
来避免对矩阵的直接存储,这种方法一般比直接计算法高效得多。在众多迭代算法中,共轭梯度法是使用最多的。因此我们看到对于求解问题(4),外层是牛顿法进行迭代,内层是共轭梯度法迭代来求牛顿方向。然而,使用共轭梯度法会遇到迭代时间过长的问题,因此为了节省时间,我们一般在外层迭代时使用“近似”的牛顿方向,这种技术称为:截断牛顿法。
3、Trust Region Newton Method
首先让我们看看什么是信赖域方法:
在每次迭代时强制性地要求新的迭代点与当前的迭代点之间的距离不超过某一控制量. 引入控制步长是因为传统的线搜索方法常常由于步长过大而导致算法失败,特别是当问题是病态时. 控制步长实质上等价于在以当前迭代点为中心的一个邻域内对一个近似于原问题的简单模型求极值. 这种技巧可理解为只在一个邻域内对近似模型信赖,所以此邻域被称为信赖域(trust region)。
通过将置信域方法与截断牛顿法相结合,精细地设置内部共轭梯度法的迭代停止条件来挑选出好的牛顿方向,从而可以保证算法收敛到全局最小值,我们将之称为置信域牛顿法(Trust Region Newton Method)。接下来我们将通过公式来进一步阐述TRON算法。
首先,在利用tron算法最小化
f
(
w
)
f(\textbf w)
f(w)时,第k步迭代存在一个大小为
Δ
k
\Delta_k
Δk的置信区间,此时用二阶泰勒展开:
(5)
q
k
(
s
)
=
∇
(
w
k
)
T
s
+
1
2
s
T
∇
2
f
(
w
k
)
s
q_k(\textbf s)=\nabla(\textbf w^k)^T\textbf s+\frac{1}{2}\textbf s^T\nabla^2f(\textbf w^k)\textbf s \tag 5
qk(s)=∇(wk)Ts+21sT∇2f(wk)s(5)
来作为
f
(
w
k
+
s
)
−
f
(
w
k
)
f(\textbf w^k+\textbf s)-f(\textbf w^k)
f(wk+s)−f(wk)的近似值。接下来,我们需要寻找步长
s
k
\textbf s^k
sk来近似最小化
q
k
(
s
)
q_k(\textbf s)
qk(s)同时满足
∣
∣
s
∣
∣
≤
Δ
k
||\textbf s||\le \Delta_k
∣∣s∣∣≤Δk。然后根据真实模型中loss的减少值与二阶近似模型中预期的减少值的比值
ρ
k
\rho_k
ρk,来更新
w
k
、
Δ
k
\textbf w^k、\Delta_k
wk、Δk:
(8)
ρ
k
=
f
(
w
k
+
s
)
−
f
(
w
k
)
q
k
(
s
k
)
\rho_k=\frac{f(\textbf w^k+\textbf s)-f(\textbf w^k)}{q_k(\textbf s^k)} \tag 8
ρk=qk(sk)f(wk+s)−f(wk)(8)
w
k
\textbf w^k
wk的更新规则如下,其中
η
0
>
0
\eta_0>0
η0>0是需要设置的参数
(9) w k + 1 = { w k + s k ρ k > η 0 w k ρ k ≤ η 0 \textbf w^{k+1}=\begin{cases} \textbf w^k+\textbf s^k& \rho_k>\eta_0\\ \textbf w^k &\rho_k\le \eta_0 & \end{cases} \tag 9 wk+1={wk+skwkρk>η0ρk≤η0(9)
Δ
k
\Delta_k
Δk的更新规则如下,其中参数
η
1
<
η
2
<
1
,
0
<
σ
1
<
σ
2
<
1
<
σ
3
\eta_1<\eta_2<1, 0<\sigma_1<\sigma_2<1<\sigma_3
η1<η2<1,0<σ1<σ2<1<σ3
(10)
{
Δ
k
+
1
∈
[
σ
1
min
{
∣
∣
s
k
∣
∣
,
Δ
k
}
,
σ
2
Δ
k
]
ρ
k
≤
η
1
Δ
k
+
1
∈
[
σ
1
Δ
k
,
σ
3
Δ
k
]
ρ
k
∈
(
η
1
,
η
2
)
Δ
k
+
1
∈
[
Δ
k
,
σ
3
Δ
k
]
ρ
k
≥
η
2
\begin{cases} \Delta_{k+1}\in[\sigma_1\min\{||\textbf s^k||, \Delta_k\}, \sigma_2\Delta_k]&& \rho_k\le\eta_1\\ \Delta_{k+1}\in[\sigma_1\Delta_k, \sigma_3\Delta_k] && \rho_k\in(\eta_1,\eta_2)\\ \Delta_{k+1}\in[\Delta_k, \sigma_3\Delta_k] && \rho_k\ge\eta_2 \\ \end{cases} \tag {10}
⎩⎪⎨⎪⎧Δk+1∈[σ1min{∣∣sk∣∣,Δk},σ2Δk]Δk+1∈[σ1Δk,σ3Δk]Δk+1∈[Δk,σ3Δk]ρk≤η1ρk∈(η1,η2)ρk≥η2(10)
因此,我们给出完整的tron算法如下:
其中子问题(11)由如下的共轭梯度法来近似求解:
4、分布式的spark-liblinear
了解tron算法之后,让我们再来看看在spark-liblinear中是如何做分布式的以及代码实现中的一些技巧。
在spark-liblinear中通过将梯度与Hessian矩阵的计算公式改写为分量形式,以此便于分布式计算。
(14)
f
(
w
)
=
1
2
w
T
w
+
C
∑
k
=
1
p
f
k
(
w
)
,
∇
f
(
w
)
=
w
+
C
∑
k
=
1
p
∇
f
k
(
w
)
,
∇
2
f
(
w
)
v
=
v
+
C
∑
k
=
1
p
∇
2
f
k
(
w
)
v
f(\textbf w)=\frac{1}{2}\textbf w^T\textbf w+C\sum^p_{k=1}f_k(\textbf w), \tag{14}\\ \nabla f(\textbf w)=\textbf w+C\sum^p_{k=1}\nabla f_k(\textbf w) ,\\ \nabla^2 f(\textbf w) \textbf v = \textbf v+C\sum^p_{k=1}\nabla^2 f_k(\textbf w)\textbf v
f(w)=21wTw+Ck=1∑pfk(w),∇f(w)=w+Ck=1∑p∇fk(w),∇2f(w)v=v+Ck=1∑p∇2fk(w)v(14)
这里:
(15)
f
k
(
w
)
≡
e
k
T
log
(
σ
(
Y
k
X
k
w
)
)
,
∇
f
k
(
w
)
≡
(
Y
k
X
k
)
T
(
σ
(
Y
k
X
k
w
)
−
1
−
e
k
)
,
∇
2
f
k
(
w
)
v
≡
X
k
T
(
D
k
(
X
k
v
)
)
,
D
k
≡
d
i
a
g
(
(
σ
(
Y
k
X
k
w
)
−
e
k
)
/
σ
(
Y
k
X
k
w
)
2
)
f_k(\textbf w)\equiv e^T_k\log(\sigma(Y_kX_k\textbf w)),\\ \ \\ \nabla f_k(\textbf w)\equiv(Y_kX_k)^T(\sigma(Y_kX_k\textbf w)^{-1}-e_k),\\ \ \\ \nabla^2f_k(\textbf w)\textbf v\equiv X^T_k(D_k(X_k\textbf v)), \\ \ \\ D_k\equiv diag\big((\sigma(Y_kX_k\textbf w)-e_k)/\sigma(Y_kX_k\textbf w)^2\big) \tag{15}
fk(w)≡ekTlog(σ(YkXkw)), ∇fk(w)≡(YkXk)T(σ(YkXkw)−1−ek), ∇2fk(w)v≡XkT(Dk(Xkv)), Dk≡diag((σ(YkXkw)−ek)/σ(YkXkw)2)(15)
对于计算(14),每个分量的计算只需要与之相关的数据分量 X k 、 Y k X_k、Y_k Xk、Yk,因此可以做分布式计算。
spark-liblinear 是基于scala语言开发的,为了降低开发scala程序的开销,需要涉及到一些复杂的设计问题。为实现高效的计算、通信和内存使用,需要分析以下不同的实现问题。
1、Loop Structure
算法中主要的计算量在于:矩阵与向量相乘, 需要用到循环结构。可以考虑三种方式: for-generator、for-range、while。 通过实验发现while 最高效。for表达式 在编译时先转译成高阶运算,开销较大
while(i < p.index.length)
{
z += p.value(i) * wB(p.index(i))
i += 1
}
z = (1.0 / (1.0 + exp(-p.y * z)) - 1.0) * p.y
i = 0
while(i < p.index.length)
{
grad(p.index(i)) += z * p.value(i)
i += 1
}
2、Data Encapsulation
将数据表示成稀疏矩阵,我们只存储非零值处的索引与值。文中使用两个Array来存储实例对象的索引与特征值,来减少内存开销与调用时间。
class DataPoint(val index : Array[Int], val value : Array[Double], val y : Double) extends Serializable
3、Using mapPartitions Rather Than map
map的输入变换函数是应用于RDD中每个元素,而mapPartitions的输入函数是应用于每个分区。使用MapPartitions操作之后,一个task仅仅会执行一次function,function一次接收所有的partition数据。只要执行一次就可以了,性能比较高。 在计算函数值、梯度、hessian以及数据转换时用到了mapPartitions
4、not to cache σ ( Y k X k w ) \sigma(Y_kX_k\textbf w) σ(YkXkw)
对中间量的缓存是单机版liblinear使用的策略,对于数据量巨大的情形下,分布式下的带来的额外通信会抵消掉缓存带来的计算时间节省,因此不做缓存
5、 Using Broadcast Variables
算法迭代过程中,每个分区都使用来自master的相同信息的w与s,可以通过使用广播变量来减少通信间的开销
6、 The Cost of the reduce Function
使用coalesce函数在分区的输出发送到主服务器之前,将它们局部地组合在一起,减少通信间开销。
5、spark-liblinear性能调优
spark-liblinear库的使用十分简单方便,可以直接在spark-shell里调用jar包进行训练预测。
关于用spark-liblinear时的遇到的一些性能问题,可以参考如下:
另外关于spark-liblinear里的参数 -N 如何设置:
1、-N 用于将数据重新分区,使用coalesce函数使分区数减少,这个过程不会发生shuffle。
2、当样本量或特征量非常大时,运行spark-liblinear会出现这样的错误:
ERROR scheduler.TaskSetManager: Total size of serialized results of 342 tasks (8.3 GB) is bigger than spark.driver.maxResultSize (2.0 GB)。这是由于它在计算梯度时将所需计算的vector全部拉到driver上进行,会导致程序爆掉。通过合理的设置 -N 可以有效地避免这种错误。
3、例如原始数据分区3000,设置 -N 1600。在计算梯度时,会先在executor上把1600个task hash分成40*40,先计算40个梯度累加的结果,再将这40个拉到driver上进行运算。一般建议 -N 设置成 executor-num * core 的 1-2倍,且不超过原始分区数。
文章参考:
1、C.-J. Lin, R. C. Weng, and S. S. Keerthi, “Trust region Newton method for large-scale logistic regression,” Journal of Machine Learning Research, vol. 9, pp. 627–650, 2008.
[Online]. Available: [http://www.csie.ntu.edu.tw/ ∼ cjlin/papers/logistic.pdf]
2、Large-scale Logistic Regression and Linear Support Vector Machines Using Spark