从零开始构建逻辑回归模型
逻辑回归模型是针对线性可分问题的一种易于实现而且性能优异的分类模型。我们将分别使用Numpy和TensorFlow实现逻辑回归模型训练和预测过程,并且探讨在大规模分布式系统中的工程实现。
从零构建
首先,我们通过Numpy构建一个逻辑回归模型。
我们定义shape如下:
\(X\):(n,m)
\(Y\):(1,m)
\(w\):(n,1)
\(b\):(1)
其中\(n\)代表特征维数,\(m\)代表样本个数。
对于逻辑回归二分类模型,其损失函数如下:
对\(\theta\)求导得\(\theta_j\)的更新方式是:
所以,在代码中,\(\theta\)的更新方式为:
dw = np.dot(X,(A-Y).T)/m
各个函数作用如下:
- sigmoid(x):激活函数实现
- initialization(dim):零值初始化w以及b
- propagate(w,b,X,Y):前向传播得到梯度以及代价函数值
- optimize(w,b,X,Y,learning_rate,epochs,print_cost=False):反向传播更新参数
- predict(w,b,X):得到预测值
- model(X_train,Y_train,X_test,Y_test,epochs=200,learning_rate=0.01,print_cost=False):建模
import numpy as np
def sigmoid(x):
return 1/(1+np.exp(-x))
def initialization(dim):
w = np.zeros((dim,1))
b = 0
return w,b
def propagate(w,b,X,Y):
m = X.shape[1]
A = sigmoid(np.dot(w.T,X)+b)
cost = -1*np.sum(Y*np.log(A)+(1-Y)*np.log(1-A))/m
dw = np.dot(X,(A-Y).T)/m
db = np.sum(A-Y)/m
grads = {'dw':dw,'db':db}
return grads,cost
def optimize(w,b,X,Y,learning_rate,epochs,print_cost=False):
costs = []
for epoch in range(epochs):
grads,cost = propagate(w,b,X,Y)
dw = grads['dw']
db = grads['db']
w -= learning_rate * dw
b -= learning_rate * db
if epochs % 100 == 0:
costs.append(cost)
if print_cost:
print('epochs:%i;cost:%f'%(epoch,cost))
params = {'w':w,'b':b}
return params,costs
def predict(w,b,X):
predictions = sigmoid(np.dot(w.T,X)+b)
return (predictions>0.5).astype(int)
def model(X_train,Y_train,X_test,Y_test,epochs=200,learning_rate=0.01,print_cost=False):
dim = X_train.shape[0]
w,b = initialization(dim)
params, costs = optimize(w,b,X_train,Y_train,learning_rate,epochs,print_cost)
w,b = params['w'],params['b']
Y_predictions = predict(w,b,X_test)
print('Test Acc:{}%'.format(100-np.mean(abs(Y_predictions-Y_test))*100))
if __name__ == '__main__':
n = 20 #特征维度
m = 200 #样本数目
X_train = np.random.random((n,m))
Y_train = np.random.randint(0,2,size=(1,m))
X_test = np.random.random((n,10))
Y_test = np.random.randint(0,2,size=(1,10))
model(X_train,Y_train,X_test,Y_test,epochs=200,learning_rate=0.01,print_cost=False)
TensorFlow版本
下面,我们实现下 TensorFlow版本的逻辑回归模型。
这里采用了mnist数据集,将每张图片\(28*28\)像素作为特征,使用\(softmax\)作为激活函数,最终我们的损失函数定义只有一行代码:
cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(pred)))
下面,是具体的构建过程:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('/tmp/data',one_hot=True)
learning_rate = 0.1
training_steps = 100
display_steps = training_steps//10
batch_size =64
X = tf.placeholder(tf.float32,[None,28*28])
y = tf.placeholder(tf.float32,[None,10])
W = tf.Variable(tf.zeros([784,10]))
b = tf.Variable(tf.zeros([10]))
pred = tf.nn.softmax(tf.add(tf.matmul(X,W),b))
cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(pred)))
optimizer = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cost)
init = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in range(training_steps):
avg_cost = 0.
total_batch = int(mnist.train.num_examples/batch_size)
for i in range(total_batch):
batch_x,batch_y = mnist.train.next_batch(batch_size)
_,c = sess.run([optimizer,cost],feed_dict={X:batch_x,y:batch_y})
avg_cost += c/total_batch
if (epoch+1) % display_steps == 0:
print('The epoch:%i,The cost:%9f'%(epoch+1,avg_cost))
print('Finished')
correct_prediction = tf.equal(tf.argmax(pred,1),tf.argmax(y,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
print('acc:',sess.run(accuracy,feed_dict={X: mnist.test.images[:3000], y: mnist.test.labels[:3000]}))
分布式实现
实际情况中,由于受到单机处理能力和效率的限制,在利用大规模样本数据进行训练的时候往往需要将求解LR问题的过程进行并行化,本文从并行化的角度讨论LR的实现。
逻辑回归问题的求解方法主要有三种:
- 梯度下降法
- 牛顿法(Newton Methods):牛顿法是在当前参数\(\theta\)下,利用二次泰勒展开近似目标函数,然后利用该近似函数来求解目标函数的下降方向。该算法需要计算海森矩阵,因此算法需要花费大量的时间,迭代时间较长。牛顿法要求Hession矩阵是正定的,但在实际问题中,很难保证是正定的。
- 拟牛顿法(Quasi-Newton Methods):使用近似算法,计算海森矩阵,从而降低算法每次迭代的时间,提高算法运行的效率。在拟牛顿算法中较为经典的算法有两种:BFGS算法和L-BFGS算法。BFGS算法是利用原有的所有历史计算结果,近似计算海森矩阵,虽然提高了整个算法的效率,但是由于需要保存大量历史结果,因此该算法受到内存的大小的局限,限制了算法的应用范围;而L-BFGS则是正是针对BFGS消耗内存较大的特点,为了解决空间复杂度的问题,只保存有限的计算结果,大大降低了算法对于内存的依赖。L-BFGS在特征量大时比BFGS实用,可以非常容易用map/reduce实现分布式求解,mapper求部分数据上的梯度,reducer求和并更新参数。它与梯度法实现复杂一点的地方在,它需要保存前几次的模型,才能计算当前迭代的更新值。
由逻辑回归问题的求解方法中可以看出,无论是梯度下降法、牛顿法、拟牛顿法,计算梯度都是其最基本的步骤,并且L-BFGS通过两步循环计算牛顿方向的方法,避免了计算海森矩阵。因此逻辑回归的并行化最主要的就是对目标函数梯度计算的并行化。从梯度更新公式中可以看出,目标函数的梯度向量计算中只需要进行向量间的点乘和相加,可以很容易将每个迭代过程拆分成相互独立的计算步骤,由不同的节点进行独立计算,然后归并计算结果。
将M个样本的标签构成一个M维的标签向量,M个N维特征向量构成一个M*N的样本矩。其中特征矩阵每一行为一个特征向量(M行),列为特征维度(N列)。
如果将样本矩阵按行划分,将样本特征向量分布到不同的计算节点,由各计算节点完成自己所负责样本的点乘与求和计算,然后将计算结果进行归并,则实现了“按行并行的LR”。按行并行的LR解决了样本数量的问题,但是实际情况中会存在针对高维特征向量进行逻辑回归的场景(如广告系统中的特征维度高达上亿),仅仅按行进行并行处理,无法满足这类场景的需求,因此还需要按列将高维的特征向量拆分成若干小的向量进行求解。
数据分割
假设所有计算节点排列成m行n列(m*n个计算节点),按行将样本进行划分,每个计算节点分配M/m个样本特征向量和分类标签;按列对特征向量进行切分,每个节点上的特征向量分配N/n维特征。如图所示,同一样本的特征对应节点的行号相同,不同样本相同维度的特征对应节点的列号相同。
一个样本的特征向量被拆分到同一行不同列的节点中,即:
其中\(X_{r,k}\)表示第\(r\)行的第\(k\) 个向量,\(X_{(r,c),k}\)表示\(X_{r,k}\)在第\(c\)列节点上的分量。同样的,用\(W_c\)表示特征向量\(W\)在第\(c\)列节点上的分量,即:
并行计算
观察目标函数的梯度计算公式,其依赖于两个计算结果:特征权重向量\(W_t\)和特征向量\(X_j\)的点乘,标量\((y_n-t_n)\)和特征向量\(X_j\)的相乘。其中\(y_n = \sigma(x)={1\over 1+e^{-XW}}\)可以将目标函数的梯度计算分成两个并行化计算步骤和两个结果归并步骤:
- 各节点并行计算点乘,计算\(d_{(r,c),k,t}=W_{c,t}^T X_{(r,c),k}\),其中\(k=1,2,\cdots,M/m\),\(d_{(r,c),k,t}\)表示第\(t\)次迭代中节点\((r,c)\)上的第\(k\)个特征向量与特征权重分量的点乘,\(W_{c,t}\)为第\(t\)次迭代中特征权重向量在第\(c\)列节点上的分量。
- 对行号相同的节点归并点乘结果:
计算得到的点乘结果需要返回到该行所有计算节点中,如图所示。
- 各节点独立算标量与特征向量相乘:
\(G_{(r,c),t}\)可以理解为由第\(r\)行节点上部分样本计算出的目标函数梯度向量在第\(c\)列节点上的分量。
- 对列号相同的节点进行归并:
\(G_{c,t}\)就是目标函数的梯度向量\(G_t\)在第\(c\)列节点上的分量,对其进行归并得到目标函数的梯度向量:
这个过程如图所示。
综合上述步骤,并行LR的计算流程如图所示。并行LR实际上就是在求解损失函数最优解的过程中,针对寻找损失函数下降方向中的梯度方向计算作了并行化处理,而在利用梯度确定下降方向的过程中也可以采用并行化(如L-BFGS中的两步循环法求牛顿方向)。
小结
1、按行并行。即将样本拆分到不同的机器上去。其实很简单,重新看梯度计算的公式:
\(\frac{1}{N}\sum_{n=1}^N(y_n-t_n)x_n\),其中\(y_n=\frac{1}{1+e^{-W^Tx_n}}\)。比如我们按行将其均分到两台机器上去,则分布式的计算梯度,只不过是每台机器都计算出各自的梯度,然后 归并求和再求其平均。为什么可以这么做呢?因为公式\(\frac{1}{N}\sum_{n=1}^N(y_n-t_n)x_n\)只与上一个时刻的以及当前样本有关。所以就可以并行计算了。
2、按列并行。按列并行的意思就是将同一样本的特征也分布到不同的机器中去。上面的公式为针对整个\(W\),如果我们只是针对某个分量\(W_j\),可得到对应的梯度计算公式\(\frac{1}{N}\sum_{n=1}^N (y_n-t_n)x_{n,j}\)即不再是乘以整个\(x_n\),而是乘以\(x_n\)对应的分量\(x_{n,j}\),此时可以发现,梯度计算公式仅与\(x_n\)中的特征有关系,我们就可以将特征分布到不同的计算上,分别计算\(W_j\)对应的梯度,最后归并为整体的\(W\),再按行归并到整体的梯度更新。