逻辑回归推导过程
逻辑回归是一种名为“回归”的线性分类器,其本质是由线性回归变化而来。想理解逻辑回归,先回顾以下线性回归。线性回归写作一个人人熟悉的方程:
θ被统称为模型的参数,其中θ被称为截距。θ1—θn被称为系数。我们可以用矩阵表示这个方程,其中x和θ都可以被看作是一个列矩阵:
线性回归的任务,就是构造一个预测函数z来映射输入的特征矩阵x和标签值y的线性关系,而构造预测函数的核心
就是找出模型的参数:θ的转置矩阵和θ0,著名的最小二乘法就是用来求解线性回归中参数的数学方法。
通过函数z,线性回归使用输入的特征矩阵X来输出一组连续型的标签值y_pred,以完成各种预测连续型变量的任务
(比如预测产品销量,预测股价等等)。那如果我们的标签是离散型变量,尤其是,如果是满足0-1分布的离散型
变量,我们要怎么办呢?我们可以通过引入联系函数(link function),将线性回归方程z变换为g(z),并且令g(z)的值
分布在(0,1)之间,且当g(z)接近0时样本的标签为类别0,当g(z)接近1时样本的标签为类别1,这样就得到了一个分
类模型。而这个联系函数对于逻辑回归来说,就是Sigmoid函数:
Sigmoid函数的公式和性质:Sigmoid函数是一个S型的函数,当自变量z趋近正无穷时,因变量g(z)趋近于1,而当z趋近负无穷时,g(z)趋近于0,它能够将任何实数映射到(0,1)区间,使其可用于将任意值函数转换为更适合二分类的函数。因为这个性质,Sigmoid函数也被当作是归一化的一种方法,与我们之前学过的MinMaxSclaer同理,是属于数据预处理中的“缩放”功能,可以将数据压缩到[0,1]之内。区别在于,MinMaxScaler归一化之后,是可以取到0和1的(最大值归一化后就是1,最小值归一化后就是0),但Sigmoid函数只是无限趋近于0和1。
将z带入Sigmoid,得到二元逻辑回归的一般形式:
而g(z)就是我们逻辑回归返回的标签值。此时,y(x)的取值都在[0,1]之间,因此 y(x)和1-y(x)相加必然为1。令y(x)除以1-y(x)可以得到形似几率得y(x)/(1-y(x)),在此基础上取对数,得:
不难发现,g(z)的形似几率取对数的本质其实就是我们的线性回归z,我们实际上是在对线性回归模型的预测结果取对数几率来让其的结果无限逼近0和1。因此,其对应的模型被称为”对数几率回归“(logistic Regression),也就是我们的逻辑回归,这个名为“回归”却是用来做分类工作的分类器。
逻辑回归有着基于训练数据求解参数θ的需求,并且希望训练出来的模型能够尽可能地拟合训练数据,即模型在训练集上的预测准确率越靠近100%越好。因此,我们使用”损失函数“这个评估指标,来衡量参数θ的优劣,即这一组参数能否使模型在训练集上表现优异。
关键概念:损失函数,衡量参数θ的优劣的评估指标,用来求解最优参数的工具损失函数小,模型在训练集上表现优异,拟合充分,参数优秀损失函数大,模型在训练集上表现差劲,拟合不足,参数糟糕我们追求,能够让损失函数最小化的参数组合。注意:没有”求解参数“需求的模型没有损失函数,比如KNN,决策树。 损失函数只适用于单个训练样本,所以引入了成本函数来。损失函数是衡量单一训练样例的效果。成本函数用于衡量参数w和b的效果。
我们要找到一组w和b是成本函数J最小。
推导过程:
既然是最大似然,我们的目标当然是要最大化似然概率:
对于二分类问题有:
用一个式子表示上面这个分段的函数(写成相乘的形式):
如果用hθ(xi)表示p0,1 - hθ(xi)表示p1,将max函数换成min,则得到最终形式:
由于我们追求成本函数的最小值,让模型在训练集上表现最优,可能会引发另一个问题:如果模型在训练集上表示优秀,却在测试集上表现糟糕,模型就会过拟合。虽然逻辑回归和线性回归是天生欠拟合的模型,但我们还是需要控制过拟合的技术来帮助我们调整模型,对逻辑回归中过拟合的控制,通过正则化来实现。
逻辑回归的类
linear_model.LogisticRegression
正则化重要参数
正则化是用来防止模型过拟合的过程,常用的有L1正则化和L2正则化两种选项,分别通过在损失函数后加上参数向量θ的L1范式和L2范式的倍数来实现。这个增加的范式,被称为“正则项”,也被称为”惩罚项”。损失函数改变,基于损失函数的最优化来求解的参数取值必然改变,我们以此来调节模型拟合的程度。其中L1范数表现为参数向量中的每个参数的绝对值之和,L2范数表现为参数向量中的每个参数的平方和的开方值。
其中J(θ)是我们之前提过的损失函数,C是用来控制正则化程度的超参数,n是方程中特征的总数,也是方程中参数的总数。j代表每个参数。在这里,J要大于等于1,因为我们的参数向量θ中,第一个参数是θ0,使我们的截距。它通常不参与正则化。上面的式子还有另一种写法,本质是一样的。
L1正则化和L2正则化虽然都可以控制过拟合,但它们的效果并不相同。当正则化强度逐渐增大(即C逐渐变小),参数 θ的取值会逐渐变小,但L1正则化会将参数压缩为0,L2正则化只会让参数尽量小,不会取到0。在L1正则化在逐渐加强的过程中,携带信息量小的、对模型贡献不大的特征的参数,会比携带大量信息的、对模型有巨大贡献的特征的参数更快地变成0,所以L1正则化本质是一个特征选择的过程,掌管了参数的“稀疏性”。L1正则化越强,参数向量中就越多的参数为0,参数就越稀疏,选出来的特征就越少,以此来防止过拟合。因此,如果特征量很大,数据维度很高,我们会倾向于使用L1正则化。由于L1正则化的这个性质,逻辑回归的特征选择可以由Embedded嵌入法来完成。相对的,L2正则化在加强的过程中,会尽量让每个特征对模型都有一些小的贡献,但携带信息少,对模型贡献不大的特征的参数会非常接近于0。通常来说,如果我们的主要目的只是为了防止过拟合,选择L2正则化就足够了。但是如果选择L2正则化后还是过拟合,模型在未知数据集上的效果表现很差,就可以考虑L1正则化。
multi_class
输入”ovr”, “multinomial”, “auto”来告知模型,我们要处理的分类问题的类型。默认是”ovr”。
‘ovr’(one-vs-rest):表示分类问题是二分类,或让模型使用”一对多”的形式来处理多分类问题。
‘multinomial’:表示处理多分类问题,这种输入在参数solver是’liblinear’时不可用。
“auto”:表示会根据数据的分类情况和其他参数来确定模型要处理的分类问题的类型。比如说,如果数据是二分类,或者solver的取值为”liblinear”,”auto”会默认选择”ovr”。反之,则会选择”nultinomial”。
注意:默认值将在0.22版本中从”ovr”更改为”auto”。
1 | from sklearn.datasets import load_iris |
solver
对于小数据集,‘liblinear’是一个不错的选择,而’sag’和’saga’对于大数据集来说更快。对于多类问题,只有’newton-cg’,‘sag’,’saga’和’lbfgs’处理多项损失。‘newton-cg’,’lbfgs’和’sag’只处理L2 penalty,而’liblinear’和’saga’处理L1 penalty。
参数列表
属性
接口
逻辑回归的特征工程
高效的embedded嵌入法
我们已经说明了,由于L1正则化会使得部分特征对应的参数为0,因此L1正则化可以用来做特征选择,结合嵌入法的模块SelectFromModel,我们可以很容易就筛选出让模型十分高效的特征。注意,此时我们的目的是,尽量保留原数据上的信息,让模型在降维后的数据上的拟合效果保持优秀,因此我们不考虑训练集测试集的问题,把所有的数据都放入模型进行降维。
1 | from sklearn.linear_model import LogisticRegression as LR |
看看结果,特征数量被减小到个位数,并且模型的效果却没有下降太多,如果我们要求不高,在这里其实就可以停下了。但是,能否让模型的拟合效果更好呢?在这里,我们有两种调整方式:
1)调节SelectFromModel这个类中的参数threshold,这是嵌入法的阈值,表示删除所有参数的绝对值低于这个阈值的特征。现在threshold默认为None,所以SelectFromModel只根据L1正则化的结果来选择了特征,即选择了所有L1正则化后参数不为0的特征。我们此时,只要调整threshold的值(画出threshold的学习曲线),就可以观察不同的threshold下模型的效果如何变化。一旦调整threshold,就不是在使用L1正则化选择特征,而是使用模型的属性.coef_中生成的各个特征的系数来选择。coef_虽然返回的是特征的系数,但是系数的大小和决策树中的feature_ importances_以及降维算法中的可解释性方差explained_vairance_概念相似,其实都是衡量特征的重要程度和贡献度的,因此SelectFromModel中的参数threshold可以设置为coef_的阈值,即可以剔除系数小于threshold中输入的数字的所有特征。
1 | fullx = [] |
这种方法其实是比较无效的,大家可以用学习曲线来跑一跑:当threshold越来越大,被删除的特征越来越多,模型的效果也越来越差。
2)第二种调整方法,是调逻辑回归的类LR_,通过画C的学习曲线来实现:
1 | fullx = [] |
系数累加法
系数累加法的原理非常简单。在PCA中,我们通过绘制累积可解释方差贡献率曲线来选择超参数,在逻辑回归中我们可以使用系数coef_来这样做,并且我们选择特征个数的逻辑也是类似的:找出曲线由锐利变平滑的转折点,转折点之前被累加的特征都是我们需要的,转折点之后的我们都不需要。不过这种方法相对比较麻烦,因为我们要先对特征系数进行从大到小的排序,还要确保我们知道排序后的每个系数对应的原始特征的位置,才能够正确找出那些重要的特征。如果要使用这样的方法,不如直接使用嵌入法来得方便。
包装法
相对的,包装法可以直接设定我们需要的特征个数。
梯度下降
之前提到过,逻辑回归的数学目的是求解能够让模型最优化的参数θ的值,即求解能够让损失函数最小化的的值,而这个求解过程,对于二元逻辑回归来说,有多种方法可以选择,最常见的有梯度下降法(Gradient Descent),坐标轴下降法(Coordinate Descent),牛顿法(Newton-Raphson method)等,每种方法都涉及复杂的数学原理,但这些计算在执行的任务其实是类似的。 成本函数就是1/m的损失函数之和。损失函数可以衡量算法的结果,成本函数可以看出参数w和b在训练集上的效果。
单个样本的梯度下降
其中,σ就是sigmoid函数:
假设某个样本的特征只有 x1 和 x2 ,即(2,1)的列向量: x = [x1, x2]^T, w 是一个(2,1)的列向量, 那么 根据 z = w^T*X + b 可以得到下面的图,其中w1, w2, b 是未知的参数,之后我们会通过训练获得一组最佳的取值。
首先,让我们计算损失函数的导数 dL(a,y)/da 和 da/dz
不难得到:
da / dz = a * (1 - a); ①
da/dz=e^(-z)/(1+e^(-z))^2=(1+e^(-z)-1)/((1+e^(-z))^2)=1/(1+e^(-z))-1/(1+e^(-z))^2=a-a^2=a(1-a)
dL / da = - y / a + (1 - y) / (1 - a); ②
根据链式求导法则,从图的右边向左推,得
dz = dL / dz = ② * ① = a - y;
再进一步:
dw1 = dL / dw1 = ( dL / dz ) ( dz / dw1) = dz x1 = ( a - y ) * x1
dw2 = dL / dw2 = ( dL / dz ) ( dz / dw2) = dz x2 = ( a - y ) * x2
db = dL / db = ( dL / dz ) ( dz / db) = dz 1 = ( a - y )
然后更新w1, w2, b 的值即可:
w1 = w1 - dw1 * α (α 是学习率),
w2 = w2 - dw2 * α,
b = b - α * db;
这样我们就完成了单个样本的参数更新
多个样本的梯度下降
先回顾一下成本函数 J(w, b) 的含义, 它是m个样本损失函数求和后的平均值. 注意到公式里有上角标 i ,表示这是第 i 个样本的数据.
既然 成本函数 J 是 所有样本的 L 累加后的平均值, 那么 J 对 w1 的导数 等价于 各个 L 对 w1 的导数求和后的平均值, 同理, 那么 J 对 w2, b 的导数 也是 各个L 对 w2, b 的导数求和后的平均值.
于是得出各个参数更新过程的伪代码:
1 | # 初始化变量 |
这样, m 个样本的各个参数也就能得到调整了. 但是这部分代码, 仅仅更新了这些参数一次, 需要多次执行, 才能让 J 沿着梯度下降到最低点。这部分代码使用的 for 循环会造成执行效率不高, 如果我们可以把部分数据向量化, 利用矩阵乘法代替 for 循环可以大幅度提高程序执行效率。
在梯度下降过程中,应该有两个for循环。第一个大的for循环代表特征数量,第二个for嵌在第一个for循环里,代表样本数量。两个显式for循环会大大减慢运行速度。为了加快速度,可以通过向量化。初始化的时候用np.zeros初始化成零向量,利用np.dot实现ω*x。
向量化我们对代码的改动:
① 使用 numpy 生成 (n_x, 1) 的列矩阵存放w1, w2, w3..wn , 并初始化为0
② 利用矩阵的加法,将第 i 个样本 x^(i) 的每个特征全部乘以 dz^(i) 并与 列向量 dw 相加
③ 利用 numpy 的特性, dw /= m 可以使得 dw 中每个元素都除以 m
去掉内层for循环后的伪代码:
1 | Z = np.dot(w^T, X) + b |
这样就完成了m个样本的梯度下降的一次迭代。然而,我们需要多次迭代才能使得成本函数 J 到达最低点,因此 我们仍然需要使用 for 循环,外层for循环暂时还没有办法将其去掉。
步长的概念与解惑:
许多博客和教材在描述步长的时候,声称它是”梯度下降中每一步沿梯度的反方向前进的长度“,”沿着最陡峭最易下山的位置走的那一步的长度“或者”梯度下降中每一步损失函数减小的量“,甚至有说,步长是二维平面著名的求导三角形中的”斜边“或者“对边”的。这些说法都是错误的
请看下图,A(θa,J(θa))就是小球最初的位置,B(θb,J(θb))就是一次滚动后小球移动到的位置。从A到B的方向就是梯度向量的反方向,指向损失函数在A点下降最快的方向。而梯度向量的大小是点A在图像上对θ求导后的结果,也是点A切线方向的斜率,橙色角的tan结果,记作d。 梯度下降每走一步,损失函数减小的量,是损失函数在θ变化之后的取值的变化,写作J(θb)-J(θa)。梯度下降每走一步,参数变量的变化,写作θa-θb,根据我们参数向量的迭代公式,就是我们的步长梯度向量的大小。记作α\d,这是三角形的邻边。梯度下降中每走一步,也就是三角形中的斜边。
所以,步长不是任何物理距离,它甚至不是梯度下降过程中任何距离的直接变化,它是梯度向量的大小d上的一个比例,影响着参数向量θ每次迭代后改变的部分。
不难发现,既然参数迭代是靠梯度向量的大小d * 步长α来实现的,而J(θ)的降低又是靠调节θ来实现的,所以步长可以调节损失函数下降的速率。在损失函数降低的方向上,步长越长,θ的变动就越大。相对的,步长如果很短,θ的每次变动就很小。具体地说,如果步长太大,损失函数下降得就非常快,需要的迭代次数就很少,但梯度下降过程可能跳过损失函数的最低点,无法获取最优值。而步长太小,虽然函数会逐渐逼近我们需要的最低点,但迭代的速度却很缓慢,迭代次数就需要很多。
在彩色图中,小球在进入深蓝色区域后,并没有直接找到某个点,而是在深蓝色区域中来回震荡了数次才停下,这种”震荡“其实就是因为我们设置的步长太大的缘故。但是在我们开始梯度下降之前,我们并不知道什么样的步长才合适,但梯度下降一定要在某个时候停止才可以,否则模型可能会无限地迭代下去。因此,在sklearn当中,我们设置参数max_iter最大迭代次数来代替步长,帮助我们控制模型的迭代速度并适时地让模型停下。max_iter越大,代表步长越小,模型迭代时间越长,反之,则代表步长设置很大,模型迭代时间很短。 迭代结束,获取到J(θ)的最小值后,我们就可以找出这个最小值θ对应的参数向量 ,逻辑回归的预测函数也就可以根据这个参数向量θ来建立了。
接下来的是max_iter的学习曲线coding:
1 | from sklearn.linear_model import LogisticRegression as LR |
运行上面的代码。会弹出红色警告。因为max_iter中限制的步数已经走完了,逻辑回归却还没找到损失函数的最小值。参数θ还没有收敛,sklearn就会弹出警告:
当参数solver=”liblinear”:
当参数solver=“sag”:
虽然写法看起来略有不同,但其实都是一个含义,这是在提醒我们:参数没有收敛,请增大max_iter中输入的数字。但我们不一定要听sklearn的。max_iter很大,意味着步长小,模型运行得会更加缓慢。虽然我们在梯度下降中追求的是损失函数的最小值,但这也可能意味着我们的模型会过拟合(在训练集上表现得太好,在测试集上却不一定),因此,如果在max_iter报红条的情况下,模型的训练和预测效果都已经不错了,那我们就不需要再增大max_iter中的数目了,毕竟一切都以模型的预测效果为基准——只要最终的预测效果好,运行又快,那就一切都好,无所谓是否报红色警告了
coding
1 | #建立两个逻辑回归,L1正则化和L2正则化。效果一目了然 |