2021人工智能领域新星创作者,带你从入门到精通,该博客每天更新,逐渐完善机器学习各个知识体系的文章,帮助大家更高效学习。
一、概述
对于我们的机器学习算法来说,一般有分类模型、回归模型等,对于分类模型又分为线性模型和非线性模型,那么什么是线性模型和非线性模型呢?
如果对于一维变量来说,y = 2 x + 1 y=2x+1y=2x+1 这就是一个线性方程,y = 2 ∗ x 2 + 1 y=2*x^2+1y=2∗x2+1 这显然就是非线性方程,对于分类问题,所有的数据都分布在一个高维空间内,我们的目标就是找到一个超平面来将我们的数据进行区分,(超平面就是高纬度空间中的面),但是在显示生活中我们的数据分布往往很复杂,很难找到一个面进行分割,举个例子针对二维平面分布很多个点,我们目标是找到一条直线将我们的点进行区分,如下图,很容易找到一条直线进行分割
但有时数据分布很杂乱,由单独一条直线是无法分割的,比如下面这幅图,我们单纯使用直线是无法区分的,需要用到曲线才能够满足要求,这也就对应着线性可分和线性不可分,线性模型在一定程度上精度不是特别高,但是如果我们引入非线性模型,它能够捕捉更多的特征信息,拟合效果也会更好,但是这往往也会导致另外一个问题就是过拟合。
二、逻辑回归是什么
1.概述
其实逻辑回归的本质就是最简单的回归函数加上个非线性约束,y = w 1 ∗ x 1 + w 2 ∗ x 2 + b y=w1*x1+w2*x2+by=w1∗x1+w2∗x2+b ,这是最简单的回归函数,由于我们是要做分类问题,如果采用上面这种函数方式,无法精确获得每个类别所属的概率值,无法清晰定位阈值,没有可比性,因为对于回归函数我们的值域很广,所以我们就需要将上述的结果转换到一个我们可以比较大小的区间,一般我们的概率值为0-1,所以我们就需要找到一种函数将上面的y映射到0-1区间,这是就引入了我们经常听说的sigmoid函数,其实它就是一个逻辑函数。
那么说了这么多,我们逻辑回归的概念就是最经典的回归方程最外面套用了一个逻辑函数,最经常使用的是sigmoid函数,这样的就是一个逻辑回归模型。
2.逻辑回归的原理
2.1 sigmoid概率映射
为了叙述方便,我们之后所以的假设都为二分类(0、1),其实多分类就是做个推广,我们上面说到了我们会通过我们构造的模型函数将数据最终映射到0-1之间,这样数据所属的每个分类就会存在一个概率,我们利用该概率的大小进行比较,如果我们的我们的回归值大于0,那么我们的概率就会大于0.5,从下面的图可以很容易看出,也就是我们每个数据分到哪个类就看 y = w . T ∗ x y=w.T*xy=w.T∗x 的结果是大于0还是小于0。
2.2 概率计算
为了求数据所属分类的概率,引入两个概率公式,如下:
p ( y = 1 ∣ x , w ) = 1 1 + e − w T x p(y=1|x,w)=\frac{1}{1+e^{-w^Tx}}p(y=1∣x,w)=1+e−wTx1
p ( y = 0 ∣ x , w ) = 1 1 + e w T x = 1 − p ( y = 1 ∣ x , w ) = 1 − 1 1 + e − w T x p(y=0|x,w)=\frac{1}{1+e^{w^Tx}}=1-p(y=1|x,w)=1-\frac{1}{1+e^{-w^Tx}}p(y=0∣x,w)=1+ewTx1=1−p(y=1∣x,w)=1−1+e−wTx1
这里采用的都是后验概率,就是我们已经知道了样本和模型符合的参数,去求y的概率
为了表达方便再引入个表达式:
h ( x ) = s i g m o i d ( x ) = 1 1 + e − w . T x h(x)=sigmoid(x)=\frac{1}{1+e^{-w.Tx}}h(x)=sigmoid(x)=1+e−w.Tx1
2.3 极大似然估计
由于我们要求取最优的模型,所以需要构造个优化函数,这里常常使用极大似然估计,极大似然的意思就是再某个条件下,我们所有样本发生的概率最大,假设样本特征为 X = x 1 , x 2 , . . . , x n X={x_1,x_2,...,x_n}X=x1,x2,...,xn ,标签分类为 y = y 1 , y 2 , . . . , y n y={y_1,y_2,...,y_n}y=y1,y2,...,yn ,那么我们的极大似然函数就为:
L ( w ∣ x , y ) = ∏ i = 1 m p ( y ( i ) ∣ x ( i ) ; w ) , 其 中 y 为 0 或 1 L(w|x,y)=\prod_{i=1}^mp(y^{(i)}|x^{(i)};w),其中y为0或1L(w∣x,y)=i=1∏mp(y(i)∣x(i);w),其中y为0或1
为了计算极大似然方便,我们将上面的两个后验概率合并简化成为一个表达式:
p ( y ∣ x , w ) = h ( x ) y ( 1 − h ( x ) ) 1 − y p(y|x,w)=h(x)^y(1-h(x))^{1-y}p(y∣x,w)=h(x)y(1−h(x))1−y
从该式子可以看出,当y=1时,我们的概率就为sigmoid函数,如果y=0时,概率就为1-sigmoid,与上面的是对应的,把这个式子带入我们的似然函数,就变成了:
L ( w ∣ x , y ) = ∏ i = 1 m h ( x ( i ) ) y ( i ) ( 1 − h ( x ( i ) ) ) y ( i ) L(w|x,y)=\prod_{i=1}^mh(x^{(i)})^{y^{(i)}}(1-h(x^{(i)}))^{y^{(i)}}L(w∣x,y)=i=1∏mh(x(i))y(i)(1−h(x(i)))y(i)
对于这里其实已经可以了,但是在实际中我们往往使用对数似然函数,什么是对数似然,就是将上面的似然函数取对数,log L ( w ∣ x , y ) \log L(w|x,y)logL(w∣x,y) ,我们为什么这样做呢,因为上面的式子代表多个样本数据的概率相乘,而且概率一般是0-1之间,如果样本数据特别多,会造成计算结果下溢,而且计算机对于加法和乘法,更偏爱加法,所以我们可以利用对数的性质,将乘法转换为加法,转换后的式子就变成了:
l ( w ) = l o g ( L ( w ∣ x , y ) ) = ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) l(w)=log(L(w|x,y))=\sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))l(w)=log(L(w∣x,y))=i=1∑my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))
3.逻辑回归的本质
假设一组数据他们的特征为3个,那么我们需要构建一个回归方程,即:
y = w 1 ∗ x 1 + w 2 ∗ x 2 + w 3 ∗ x 3 + b y=w1*x1+w2*x2+w3*x3+by=w1∗x1+w2∗x2+w3∗x3+b
- w:每个w分别代表对应特征的权重,也可以理解为贡献率
- b:偏置,控制模型的偏移位置
如果不引入偏置的话,我么的回归方程是过原点的,有些问题不能够区分,举个例子:
对于上面这幅图,显然过原点的直线不能够完全区分所有数据,如果我们的直线可以再向上平移一点,那么就可以将两类样本完全区分
这就是引入偏置b的作用,从图也可以观察到,增加偏置b并不会改变模型的线性关系,增加后还是个线性模型,只不过是位置不一样,有些地方把上面的这条直线叫做决策边界,作为区分不同数据样本的边界。
4.损失函数
说了这么多的原理,那么到底应该如何求得这组拟合数据得权值呢?构造模型的目标就是找到一组最优的参数w来拟合直线进而达到区分样本数据的能力。
这里引入一种损失函数,它可以理解为模型的一种损失,是个大于0的函数,损失值越小,模型效果越好,对于多分类问题有交叉熵,而对于回归问题有MSE均方误差,也就是说要找到一种函数来进行评估模型的好坏,不知各位还记不记得上面我们构造了一个对数似然函数,我们当初的目标是让这个对数似然值越大越好,因为它代表在某组权值下,模型结果发生的概率越大,但是它是越大越好,而我们的损失函数是越小越好,怎么办呢?很简单,加个负号不就可以了,结果我们的cost function就变成了 − l o g ( L ( w ∣ x , y ) ) -log(L(w|x,y))−log(L(w∣x,y)) 。
这里有人可能有疑问,损失函数是固定的吗?一定要用似然函数作为损失函数吗?不一定,因为损失函数本质就是构造一种方程,一个指标来进行评估你模型的好坏程度,这个完全可以自定义,你比如说,对于回归问题经常使用MSE,即预测值与真实值之间的距离作为评估指标,那么你完全可以使用距离的平方,4次方作为指标,都可以进行评估,只不过是不同数据不同环境效果可能不太一样,选取针对于自己数据评估有效的损失函数就可以,不是说一定要用什么。
5.权值求解
那么根据损失函数怎么求解最优的权值呢?权值函数是越小越好,所以有人想到我们只需要对目标函数求导,求其最小值点对应的参数不就行了,理论上是可以的,但是由于在实际中,我们的数据特征维度高,而且损失函数方程较为复杂,直接求解较为困难,而且存在解不唯一的情况,这里就和线代中的秩相关,什么时候无解,什么时候唯一解,什么时候多组解。
这时引入了一种新的求解办法:梯度下降法,它就是不断更新权重w,使距离最小值的点不断拉近,该方法的原理就不细讲了
之后,我们的参数更新公式就变成了:
w t + 1 = w t − λ ∂ J ∂ w w_{t+1}=w_t-\lambda \frac{\partial J}{\partial w}wt+1=wt−λ∂w∂J
- w t + 1 w_{t+1}wt+1:对应更行后的参数值,一般是一个列向量
- w t w_twt:更新前的参数值
- λ \lambdaλ :学习率,控制参数更新的步长
- ∂ J ∂ w \frac{\partial J}{\partial w}∂w∂J :参数w的梯度,即导数
那么我们之后的目标就是计算出损失函数对于参数w的梯度,即:
l ( w ) = l o g ( L ( w ∣ x , y ) ) = ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) l(w)=log(L(w|x,y))=\sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))l(w)=log(L(w∣x,y))=i=1∑my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))
w ′ = ∂ J ∂ w = ∂ ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) ∂ w w'=\frac{\partial J}{\partial w}=\frac{\partial \sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))}{\partial w}w′=∂w∂J=∂w∂∑i=1my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))
化简后得:
w ′ = ∑ i = 1 m ( h ( x ( i ) ) − y ( i ) ) ∗ x w'=\sum_{i=1}^{m}(h(x^{(i)})-y^{(i)})*xw′=i=1∑m(h(x(i))−y(i))∗x
补充一点,可以有的人见到该地方需要有个 1 m \frac{1}{m}m1 ,这里为什么没有呢?是因为我的损失函数计算的是所有样本的损失,而不是计算平均每个样本的损失,所以没有,如果需要可以自己加上。
那么我们的梯度更新公式就变成了:
w t + 1 = w t − λ ∑ i = 1 m ( h ( x ( i ) ) − y ( i ) ) ∗ x w_{t+1}=w_t-\lambda \sum_{i=1}^{m}(h(x^{(i)})-y^{(i)})*xwt+1=wt−λi=1∑m(h(x(i))−y(i))∗x
注意一点,上面的所有公式都是矩阵或者列向量之间的运算,不是我们常见的标量的运算,需要时刻注意形状。
三、正则项
1.概述
正则项其实就是一个多项式,它会加在损失函数的后面,它的目的就是为了防止模型过拟合,提高模型的泛化能力,有时也叫做抗干扰能力,我们最经常使用的就是L1正则项和L2正则项,下面我们分别讲述一下这两个正则项的原理。
2.L1正则项
对于线性回归模型来说,加入了L1正则项的叫做Lasso回归,它的损失函数为:
J ( w ) = − 1 m ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) + α ∣ ∣ w ∣ ∣ 1 J(w)=-\frac{1}{m}\sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))+\alpha||w||_1J(w)=−m1i=1∑my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))+α∣∣w∣∣1
它就是在后面加入了参数的L1范式,就是参数向量w的各个元素的绝对值之和,前面的 α \alphaα 代表惩罚系数,即正则化的程度,它的值越大,正则化越严重,模型的泛化能力越强,但是该值过大也会影响模型的拟合效果不足,所以需要按照实际情况进行修整。
对于L1正则化来说,它通常会拟合出一组稀疏的参数向量,也就是回归方程中的大多数参数w都为0,这有一个好处就是方便我们进行特征的选择,之前我们说过参数w可以代表该特征的贡献率,如果参数为0,那么说明该特征没有什么贡献,删掉该特征也没有什么较大的影响。
那么为什么要生成稀疏向量呢?你想我们显示中很多数据的特征都非常大,所以需要筛选掉一部分,刚才也说了,如果该特征对应的权重为0或者非常小,也就是说该特征的贡献非常小,即便删除掉也不会对模型产生较大的影响,此时我们就可以专注特征为非零值的特征。
那么怎么求解带有L1正则项的损失函数呢?其实可以这样理解,损失函数就是在原来的基础上加个L1正则项,也就是加入个约束条件,现在就代表在这个约束条件下,求原来损失函数的最小值。
从上图可以看到原来损失函数的等高线为彩色的,L1正则项就是中间那个菱形,我们约束后的极值点就是两个图形的交点,我们以二维为例,上图观察到两个图形的交点在w2轴上面,即此时w1=0,这也就说明为什么L1正则项会导致权值稀疏,因为L1正则项的图形是尖的,存在突出点,显然该凸点相对于每条边上的点更容易与原损失函数相交。
如果我们的惩罚系数越大,那么惩罚程度就越大,从图中怎么解释呢?如果 α \alphaα 越大,那么该图形的斜率就越大,该菱形也就会越靠近原点,单位参数值是我们的y值就会很大,所以会瘦高,那么此时的交点就会更加靠近原点,即参数值会更加的小,模型的抗扰动能力就越强,为什么呢?
因为模型的参数越小,即便数据变化程度大,也不会对y值有什么影响,因为参数很小,乘积加和后变化的也很小,尽管泛化能力较强,但此时参数点离原损失函数的最优点越来越远,也就说明模型的精度会有所下降,所以需要调整惩罚系数,在抗干扰能力和精度方面做个权衡。
3.L2正则项
对于线性回归模型来说,加入了L2正则项的叫做Ridge回归(岭回归),它的损失函数为:
J ( w ) = − 1 m ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) + α ∣ ∣ w ∣ ∣ 2 2 J(w)=-\frac{1}{m}\sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))+\alpha||w||_2^2J(w)=−m1i=1∑my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))+α∣∣w∣∣22
它就是在后面加入了参数的L2范式,就是参数向量w的各个元素的平方和,再开根号。
从该图也可以看出L2显然不会引起权值稀疏的问题,与菱形来说,它的棱角被磨平,所以坐标轴相交的几率变小了,它会将我们的权值缩小,抗扰动能力增大。
我们一般认为,模型参数值小的模型比较简单,能够使用于不同的数据,也在一定程度上避免了过拟合现象,假想现有一个回归方程,它的权重系数非常大,一旦我们的数据偏移一点点,都会对结果造成较大的影响,如果参数较小,即便数据有较大的变动,也不会对结果产生较大的影响。
那为什么L2正则项可以获得很小的参数?
我们原来的损失函数为:
J ( w ) = − 1 m ∑ i = 1 m y ( i ) l o g ( h ( x ( i ) ) ) + 1 − y ( i ) l o g ( 1 − h ( x ( i ) ) ) J(w)=-\frac{1}{m}\sum_{i=1}^my^{(i)}log(h(x^{(i)}))+1-y^{(i)}log(1-h(x^{(i)}))J(w)=−m1i=1∑my(i)log(h(x(i)))+1−y(i)log(1−h(x(i)))
它对应的梯度更新公式为:
w t + 1 = w t − λ w ′ w_{t+1}=w_t-\lambda w'wt+1=wt−λw′
但是如果加上L2正则项后,我们的梯度更新公式就变成了:
w t + 1 = w t ∗ ( 1 − α λ m ) − λ w ′ w_{t+1}=w_t*(1-\alpha \frac{\lambda}{m})-\lambda w'wt+1=wt∗(1−αmλ)−λw′
其中 λ \lambdaλ 代表学习率,α \alphaα 代表惩罚系数,可以看到如果惩罚系数越大,前面的 w t w_twt 就越小,因为乘以一个0-1之间的数会导致越来越小,而两次减的都是 λ w ′ \lambda w'λw′ ,所以惩罚系数越大,权重w衰减的就越快。
同理,这里惩罚系数越大,我们上面图中的圆也就会越小,最后求得的参数值也就会越小。
四、代码实现
""" * Created with PyCharm * 作者: 阿光 * 日期: 2021/8/2 * 时间: 19:04 * 描述: 利用Numpy实现逻辑回归 """ import numpy as np import pandas as pd from sklearn.linear_model import LogisticRegression class MyLogisticRegression: def __init__(self, max_iter=1000, learning_rate=0.03, penalty=None, C=1, optimizer='gradient'): self.epochs = max_iter # 最大迭代次数 self.learning_rate = learning_rate # 学习率 self.penalty = penalty # 正则方式 self.C = C # 惩罚系数 self.optimizer = optimizer # 目标函数优化方式 gradient:梯度下降 newton:牛顿法 self.w = None self.b = None # 模型训练 def fit(self, X, y): """ * 描述: 训练模型 * 参数: X:数据集(m,n) y:标签 (m,1) * 返回值: """ X = X.T # 将X进行转置,因为模型在推导过程中是以列向量假设的 y = y.reshape(1, -1) # sigmoid函数 def sigmoid(w, b, x): res = np.zeros((1, x.shape[1])) for i in range(x.shape[1]): # 这里循环是为了判断每个值的正负,进而选择不同的方式进行计算,防止指数计算溢出 inx = w.T @ x[:, i] + b res[0, i] = 1 / (1 + np.exp(-inx)) if inx >= 0 else np.exp(inx) / (1 + np.exp(inx)) return res # 随机初始化w、b def init_params(): w = np.random.rand(X.shape[0], 1) # 随机初始化w,(n,1) b = np.random.rand(1) # 标量b return w, b # 正向传播 def propagate(w, b): h_x = sigmoid(w, b, X) # 采用L1正则化 if self.penalty == 'l1': loss_function = y.T @ np.log(h_x + 1e-10) + (1 - y).T @ np.log(1 - h_x + 1e-10) + self.C * np.sum( np.abs(w)) dw = X @ (h_x - y).T - self.C * np.sign(w) db = np.sum(y - h_x) dw2 = -X @ h_x.T @ (1 - h_x) @ X.T db2 = np.sum(h_x * h_x - h_x) # 采用L2正则化 elif self.penalty == 'l2': loss_function = y.T @ np.log(h_x + 1e-10) + (1 - y).T @ np.log( 1 - h_x + 1e-10) + 1 / 2 * self.C * w.T @ w dw = X @ (h_x - y).T - self.C * w db = np.sum(y - h_x) dw2 = -X @ h_x.T @ (1 - h_x) @ X.T db2 = np.sum(h_x * h_x - h_x) # 极大似然损失函数,不使用正则项 else: loss_function = y.T @ np.log(h_x + 1e-10) + (1 - y).T @ np.log(1 - h_x + 1e-10) # 参数的梯度 dw = X @ (h_x - y).T db = np.sum(y - h_x) dw2 = -X @ h_x.T @ (1 - h_x) @ X.T db2 = np.sum(h_x * h_x - h_x) return loss_function, dw, db, dw2, db2 # 优化目标函数 def optimize(w, b): if self.optimizer == 'gradient': # 进行梯度下降 for epoch in range(self.epochs): loss, dw, db, dw2, db2 = propagate(w, b) w = w - self.learning_rate * dw b = b - self.learning_rate * db # if epoch % 100 == 0: # print("迭代的次数: %d , 误差值: %f" % (epoch,-loss)) else: # 进行牛顿法迭代 for epoch in range(self.epochs): loss, dw, db, dw2, db2 = propagate(w, b) H_X_1 = np.linalg.inv(dw2) w = w - H_X_1 @ dw b = b - db / db2 return w, b w, b = init_params() w, b = optimize(w, b) # 保存最终的解 self.w = w self.b = b # 预测数据 def predict(self, X): """ * 描述: 预测测试集的结果 * 参数: X:测试数据集 * 返回值: X对应的标签 """ def sigmoid(w, b, x): res = np.zeros((1, x.shape[1])) for i in range(x.shape[1]): inx = w.T @ x[:, i] + b res[0, i] = 1 / (1 + np.exp(-inx)) if inx >= 0 else np.exp(inx) / (1 + np.exp(inx)) return res h_x = sigmoid(self.w, self.b, X) # 定于(1,m)形状的array,用于保存结果 y_pred = np.zeros((1, X.shape[1])) # 判断每个样本的正类概率,如果大于0为1,否则为0 for i in range(X.shape[1]): y_pred[0, i] = 1 if h_x[0, i] > 0.5 else 0 return y_pred # 模型精度 def score(self, X, y): """ * 描述: 获取模型的评分 * 参数: X:数据集 y:标签 * 返回值: 模型的打分结果 """ X = X.T y = y.reshape(1, -1) y_pred = self.predict(X) return ((y_pred == y).sum()) / y.shape[1] # 获取模型参数 def get_params(self): return self.w, self.b if __name__ == '__main__': data = pd.read_csv('../../data/data.csv', header=None) X = data.iloc[:, :-1].values y = data.iloc[:, -1].values clf = LogisticRegression(solver='liblinear') clf.fit(X, y) print('sklearn:', clf.score(X, y)) # print(clf.coef_) # print(clf.intercept_) clf = MyLogisticRegression(learning_rate=1, optimizer='gradient') clf.fit(X, y) print('自实现:', clf.score(X, y)) clf = MyLogisticRegression(penalty='l1', learning_rate=1) clf.fit(X, y) print('使用l1正则化:', clf.score(X, y)) clf = MyLogisticRegression(penalty='l1', C=10, learning_rate=1) clf.fit(X, y) print('使用l1正则化,C=10:', clf.score(X, y)) clf = MyLogisticRegression(penalty='l2', learning_rate=1) clf.fit(X, y) print('使用l2正则化:', clf.score(X, y)) clf = MyLogisticRegression(penalty='l2', C=10, learning_rate=1) clf.fit(X, y) print('使用l2正则化,C=10:', clf.score(X, y)) # w_, b_ = clf.get_params() # print("自实现拟合的参数", w_, b_)