机器学习之LR算法理论和实战(实战篇)

1. python 原生实现

这里的原生实现异常粗糙(没有正则项,随机梯度上升),就是上一篇 原理篇 的代码实现,数据集直接来自sklearn iris(3分类问题),另外,手工提出了0,1两类,仅做了两类iris的分类。
对于 (h(X) = w_0 + w_1 x_1 + w_2 x_2 + ... + W_m x_m) = (W^T X)
其中 (W =[w_0,w_1,...w_m] , X = [1,x_1,x_2,...,X_m]) 我习惯于把截距(w_0) 也合到向量中去了。

import numpy as np
from sklearn.datasets import load_iris

"""
加载数据
"""
def load_dataSet():
    X, Y = load_iris(return_X_y=True)
    X_data, Y_data = X[:100,:], Y[:100]   ## 只要 0,1 两类,做两分类
    Z = np.concatenate((X_data, Y_data.reshape(X_data.shape[0], 1)), axis=1)
    np.random.shuffle(Z)
    #分割数据为训练集和测试集
    X_train, Y_train = Z[:75,0:-1], Z[:75,-1:]
    X_test, Y_test = Z[75:, :-1], Z[75:, -1:]
    return X_train,Y_train,X_test,Y_test

"""
模型训练
"""

def sigmoid(W,X):
    return 1.0 / (np.exp( - np.dot(W.T,X)) + 1)

def LRModel(W,X,Y,epoch,alpha):
    for i in range(0,epoch):
        Yhat = sigmoid(W, X)
        J = np.dot(Y, np.log(Yhat.T + 0.001)) + np.dot((1 - Y), np.log(1 - Yhat.T +0.001)) ## 这里我手工加了一个0.001,防止处理log0的情况
        W = W + (alpha * (np.dot((Y - Yhat),X.T)).T)
    return W

"""
模型预测,计算各个指标
"""
def predict(X_test,Y_test,Wf):
    Ypre = sigmoid(Wf, X_test)
    print(Ypre)
    Ypre = np.where(Ypre > 0.5, 1., 0.)
    print(Ypre)
    print(Y_test)
    acc = Ypre - Y_test
    print(acc)

if __name__ == "__main__":
    # 初始化W
    W0 = np.random.randn(5,1) # 列向量
    print(W0)
    X_train,Y_train,X_test,Y_test = load_dataSet()
    X_train = np.concatenate((np.ones((X_train.shape[0], 1)), X_train),axis=1)
    X_test = np.concatenate((np.ones((X_test.shape[0], 1)), X_test), axis=1)
    Wf = LRModel(W0,X_train.T,Y_train.T,1000,0.05)
    print("最终学习的参数为:")
    print(Wf)
    predict(X_test.T,Y_test.T,Wf)
  • 结果
[[-0.32794377]
 [-0.16341301]
 [-0.46871745]
 [-0.58777365]
 [ 0.72274566]]
最终学习的参数为:
[[-0.92044644]
 [-0.86818604]
 [-3.75098037]
 [ 5.22856039]
 [ 3.17813294]]
[[9.99919845e-01 9.99939370e-01 9.99912569e-01 9.99983816e-01
  9.97146748e-01 6.61803146e-05 3.19672831e-06 9.99976409e-01
  9.99999553e-01 9.99837974e-01 9.99827795e-01 4.69817201e-04
  1.65549104e-05 9.99878991e-01 9.99982895e-01 1.66078700e-04
  9.88524297e-01 1.12949239e-05 9.99600934e-01 6.22679950e-05
  9.99989180e-01 1.44838053e-04 4.05583288e-05 3.13544364e-04
  9.99987482e-01]]
[[1. 1. 1. 1. 1. 0. 0. 1. 1. 1. 1. 0. 0. 1. 1. 0. 1. 0. 1. 0. 1. 0. 0. 0.
  1.]]
[[1. 1. 1. 1. 1. 0. 0. 1. 1. 1. 1. 0. 0. 1. 1. 0. 1. 0. 1. 0. 1. 0. 0. 0.
  1.]]
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
  0.]]   ## acc,可以看到在测试集上完全预测正确

2. scikit-learn代码实现

scikit-learn中LR的损失函数函数和我们想象的不一样,一方面,他加入了正则项,另一方面,scikit-learn中(y_i)取 1 或者 -1,故他构造的损失函数形式与我们常规的构造不同,(其实,不管是机器学习,还是深度学习中,在得出目标函数后,我们都要想办法把(y_i)加入目标函数中,得到了损失函数,所以可以灵活构造哈

[hat{y_i} = P(y_i| x_i;W) = frac{1}{e^{-x_i^T W + c} + 1} (1) ]

其中(hat{y_i})为预测值,W为列向量,c为标量

[(hat{y_i} = 1) = P(y_i = 1| x_i;W) = frac{1}{e^{y_i(-x_i^TW + c)} + 1} = frac{1}{e^{-x_i^T W + c} + 1} (2) ]

[(hat{y_i} = -1) = P(y_i = -1| x_i;W) = 1 - P(y_i = 1| x_i;W) = 1 - frac{1}{e^{-x_i^T W + c} + 1} = frac{1}{e^{x_i^TW + c} + 1} = frac{1}{e^{y_i(-x_i^T W + c)} + 1} (3) ]

综上(0)(1)(2),sklearn LR的损失函数可定义为:

[J(W,c) = mathop{argmax}_{W,c} prod_i^m frac{1}{e^{y_i(-x_i^T W + c)} + 1} ]

即为等价于:

[J(W,c) = mathop{argmax}_{W,c}( - sum_{i}^{m} log(e^{y_i(-x_i^T W + c)} + 1) ) ]

即为:

[J(W,c) = mathop{argmin}_{W,c} sum_{i}^{m} log(exp(-y_i(x_i^T W + c)) + 1) ]

引入(L_2)正则化:

[J(W,c) = mathop{argmin}_{W,c} sum_{i}^{m} log(exp(-y_i(x_i^T W + c)) + 1) + frac{1}{2} lambda W^T W (4) ]

其中 (lambda)是超参且必须为正浮点数,需要手工调参
(4)式求出来的参数W,c的值和下式无异

[J(W,c) = mathop{argmin}_{W,c} lambda sum_{i}^{m} log(exp(-y_i(x_i^T W + c)) + 1) + frac{1}{2} W^T W (5) ]

对于无异的解释:

另外除了L2正则化外,还有L1,Elastic-Net 正则化

  • L1正则化:

[min_{W,c} |w|_1 + C sum_{i=1}^n log(exp(- y_i (X_i^T w + c)) + 1) ]

  • Elastic-Net正则化:

[min_{w, c} frac{1 - ho}{2}w^T w + ho |w|_1 + C sum_{i=1}^n log(exp(- y_i (X_i^T w + c)) + 1) ]

简单来讲,L1正则化偏向于起到特征选择作用,L2正则化偏向于起到防止过拟合的作用,Elastic-Net综合了L1,L2两种正则化的作用
另外说一句,由于水平有限,鄙人仅关注于sklearn的API的使用,各个参数的含义,不管追究sklearn中算法的具体实现,原因参考如下:https://www.zhihu.com/question/37217348/answer/71147129 [Glenn Qian的回答]

sklearn API

  • sklearn.linear_model.LogisticRegression 参数介绍
class sklearn.linear_model.LogisticRegression(penalty='l2', *, dual=False, tol=0.0001, C=1.0, fit_intercept=True, intercept_scaling=1, 
class_weight=None, random_state=None, solver='lbfgs', max_iter=100, multi_class='auto',
verbose=0, warm_start=False, n_jobs=None, l1_ratio=None)
  • penalty{‘l1’, ‘l2’, ‘elasticnet’, ‘none’}, default=’l2’
    penalty表示选择的正则化,可选L1,L2,elasticnet, none,默认是l2

  • dual: bool, default=False
    当使用solver='liblinear',l2正则化时,才有dual = true

  • tol: float, default=1e-4
    停止训练的条件,当迭代前后的函数差值小于等于tol时就停止

  • C: float, default=1.0
    正则化强度的倒数,必须为正浮点数。越小的值表示越强的正则化

  • fit_intercept: bool, default=True
    指定是否应将常量(也称为偏差或截距)添加到决策函数

  • intercept_scaling: float, default=1
    仅在使用solver = 'liblinear' 且fit_intercept = True时有用。
    *intercept_scaling: float, default=1

  • class_weight: dict or ‘balanced’, default=None
    以字典或者'balanced'形式给出模型的参数。如果没有给出,所有类的权重都应该是1。“balanced” 模式使用y的值自动调整权值与输入数据中的类频率成反比,如n_samples / (n_classes * np.bincount(y))。
    注意,如果指定了sample_weight,那么这些权重将与sample_weight相乘(通过fit方法传递)。
    0.17版本新增:class_weight= 'balanced'

  • random_state: int, RandomState instance, default=None
    在solver= 'sag','saga'或 'liblinear' 才需要有random_state设置。

  • solver: {‘newton-cg’, ‘lbfgs’, ‘liblinear’, ‘sag’, ‘saga’}, default=’lbfgs’
    求解器solver可以使用 'newton-cg','lbfgs','liblinear','sag','sage'算法来更新参数,其中sage 表示 Stochastic Average Gradient descent。选择solver的一般原则如下:

  1. 对于小型数据集,选择'liblinear',大型数据集,可选择‘sag’,'saga';
  2. ‘newton-cg’, ‘lbfgs’, ‘sag’ and ‘saga’ 应用在L2正则化或没有正则化的地方;
  3. ‘liblinear’ 和‘saga’ 也可以应用在L1正则化出现的地方;
  4. ‘saga’支持‘elasticnet’正则化;
  5. ‘liblinear’不支持penalty='none';
  6. 对于多分类问题,只有“newton-cg”, “sag”, “saga”和“lbfgs”直接处理多分类问题。而"liblinear"多分类问题转化成多个二分类问题(one-versus-rest,又称ovr)
  • max_iter: int, default=100
    使求解器solver收敛所需的最大迭代次数
  • multi_class: {‘auto’, ‘ovr’, ‘multinomial’}, default=’auto’
  • multi_class: {‘auto’, ‘ovr’, ‘multinomial’}, default=’auto’
    如果选择的选项是'ovr',那么将多分类转化成多个二分类问题。对于“multinomial”损失最小化是多项式损失适合整个概率分布,即使当数据是二进制。当求解器= ' liblinear '时,'多项'不可用。auto选择' ovr '如果数据是二进制的,或者如果solver= ' liblinear ',否则选择'多项'。
  • verbose: int, default=0
    记录训练的日志,int类型。默认为0。就是不输出训练过程,1的时候偶尔输出结果
  • warm_start: bool, default=False
    热启动参数,bool类型。默认为False。如果为True,则下一次训练是以重新上一次的调用作为初始化
  • n_jobs : int, default: 1
    用cpu的几个核来跑程序,-1表示使用所有处理器。
  • l1_ratio: float, default=None
    当 0 <= l1_ratio <= 1 且 penalty = 'Elasticnet'时, 使用的才是'elasticnet'正则化。设置l1_ratio=0 相当于使用penalty='l2',而设置l1_ratio=1 相当于使用penalty='l1'。


参考文献

[1] https://www.cnblogs.com/wjq-Law/p/9779657.html (参数的一些解释)
[2] https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression