机器学习 逻辑回归之二分类
- 一、前言
- 二、sigmoid函数
- 三、假设函数
- 四、代价函数
- 五、梯度下降
- 六、二分类原生代码实现
- 6.1 生成模拟数据
- 6.2 添加前置与数据分割
- 6.3 迭代训练
- 6.4 验证数据
- 七、sklearn代码实现
一、前言
前面一系列文章介绍了线性回归,接下来介绍逻辑回归。
线性回归的结果可能是一个区间范围内的任意值,例如预测房价,结果可能是100,100.5,100.6等。
逻辑回归用于解决分类问题,例如识别一张图片是否有猫,识别手写数字是几、判断一件商品是什么类型等,预测结果是逻辑值,0或1(2,3,…n),是或否。
二分类属于只有两种结果的逻辑回归。
逻辑回归与线性回归的区别,类似于数字电路与模拟电路的区别。
比如说,数字电路的电压抽象成0
和1
,例如最高电压为5V
,设定0~2.5V
为0
,而2.5~5V
为1
;
而模拟电路则可以是0~5V
的任意值,比如温度传感器的结果,用0和1来表示温度显然是不够的,那么我们可以通过0~5V
的结果映射成0~50℃
来知道具体的温度(可以是小数点后n位)。
二、sigmoid函数
sigmoid
函数也叫Logistic
函数,它可以将一个实数映射到(0,1)的区间。
函数表示如下
其导数为
具体请查看作者的另一篇文章《sigmoid函数及其图像绘制》
我们把之前的预测值进行sigmoid
函数变换,则得到0~1的值,你可以把它看作是预测结果为True
的概率。
比如,一张图片包含猫,则y=1,如果预测结果为0.9212,那么大概有92%的概率包含猫,18%的概率不包含猫(当然可能不是严格意义上的概率)。
在验证数据时,得到的预测结果是0~1的小数,我们可以假定大于0.5
则为True
,否则为False
。
三、假设函数
引入sigmoid
函数之后,假设函数就成了
四、代价函数
对于线性回归模型,我们定义的代价函数是所有模型误差的平方和。理论上来说,我们也可以对逻辑回归模型沿用这个定义。但是问题在于,当我们将代入到这样定义的代价函数中时,我们得到的代价函数将是一个非凸函数。这意味着我们的代价函数有许多局部最小值,这将影响梯度下降算法寻找全局最小值。
(PS:关于凸函数的证明在此不做讲解,读者有兴趣可以自行查找相关资料。此处贴一篇文章便于扩展思路《凸函数》)
所以,我们需要改变代价函数模型。
我们重新定义逻辑回归的代价函数为: ,其中
由于没有平方求导,所以我们不用直接用即可。
我们使用代码画出与 之间的关系:
当时,,代码及函数图像如下:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(-10, 10, 0.1) #起点,终点,间距
h = sigmoid(x)
#y=1时
Cost=-np.log(h) #以自然数e为底数
plt.plot(h, Cost)
plt.show()
如上图所示,在的情况下:h=1时,误差Cost=0;h越小,误差越大;当h趋近于0时,误差趋近,很好地反映了预测偏离的代价。
当时:
#y=0时
Cost=-np.log(1-h) #以自然数e为底数
plt.plot(h, Cost)
plt.show()
如上图所示,在的情况下:h=0时,误差Cost=0;h越大,误差越大;当h趋近于1时,误差趋近,也很好地反映了预测偏离的代价。
将构建的 简化如下:
可以将和分别代入,即可得到之前的函数。
代入代价函数可得到:
五、梯度下降
得到上述代价函数后,我们就可以跟线性回归一样,用梯度下降法来求出代价函数最小时的参数了。
对中的分别求偏导,求导相关知识可参考《导数与偏导》。
我们在这里对中的任意值,进行求导推理。
从上面可知,
现在我们要进行链式求导,用到的求导公式有:
,我们把看作变量,其他的都看作常数,求导可得
前面有提到simoid
函数,其导数为
有
那么:
公式推导参考文章《逻辑回归的梯度下降公式详细推导过程》
重复以下计算:
称之为学习率
,控制梯度下降时每一段走的距离。
一直迭代计算,直到变化很小为止,代表已经达到最小值
或者局部最小值
了,此时确定下来。
最终批量梯度下降的矩阵运算写法是
矩阵运算推导过程参考《机器学习 多变量线性回归》梯度下降章节。
六、二分类原生代码实现
6.1 生成模拟数据
具体可以参考另一篇文章《sklearn 使用make_classification生成分类样本数据》
from sklearn.datasets import make_classification
import matplotlib.pyplot as plt
X, Y = make_classification(n_samples=100,n_features=2,n_informative=2,n_clusters_per_class=1,n_classes=2,n_redundant=0,random_state=1)
XT=X.T
plt.scatter(
XT[0], #x坐标
XT[1], #y坐标
c=Y
)
plt.show()
print(X.shape,Y.shape)
print(X)
print(Y)
(100, 2) (100,)
[[-1.04948638 0.8786438 ]
[ 0.8780991 0.89551051]
[ 0.95928819 1.03967316]
[-1.18013412 1.12062155]
[-0.90731836 1.06040861]
[-0.62754626 1.37661405]
[ 0.82178321 1.18947778]
[-1.20376927 0.78801845]
[ 0.77151441 0.345716 ]
[-0.493857 1.66752297]
[ 1.17456965 0.69211449]
[ 0.82798737 0.84806927]
[ 0.89189141 0.87699465]
[ 0.90394545 0.35413361]
[ 0.94459507 1.12641981]
[ 0.9317172 0.78344054]
[-0.96853969 1.27991386]
[-1.09230828 0.92686981]
[-0.68243019 1.4468218 ]
[-0.76045809 0.61363671]
[-1.1146902 1.13545112]
[ 1.32970299 1.58463774]
[-1.39003042 0.85771953]
[ 1.01313574 1.16777676]
...
[-0.54245992 1.34740825]]
[1 0 0 1 1 1 0 1 0 1 0 0 0 0 0 0 1 1 1 1 1 0 1 0 0 1 0 0 0 1 1 1 1 0 0 1 0
0 0 0 1 1 0 0 1 0 1 1 0 1 1 0 1 1 1 0 0 0 1 1 0 0 1 0 0 1 0 0 0 1 0 0 0 1
1 1 0 1 1 1 0 0 0 1 1 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1]
6.2 添加前置与数据分割
添加x0=1的前置,然后将数据分割成训练数据与验证数据。
import numpy as np
np.set_printoptions(suppress=True) #numpy不使用科学计数法
from sklearn.model_selection import train_test_split
#添加前置 x0=1
temp = np.ones([X.shape[0],X.shape[1]+1])
temp[:,1:] = X #第[0]行到最后一行的(第[1]列到最后一列)赋值为X
X = temp
# 将数据分割为训练和验证数据,都有特征和预测目标值
# 分割基于随机数生成器。为random_state参数提供一个数值可以保证每次得到相同的分割
train_X, val_X, train_y, val_y = train_test_split(X, Y, random_state = 0)
print(train_X.shape, val_X.shape, train_y.shape, val_y.shape)
(75, 3) (25, 3) (75,) (25,)
6.3 迭代训练
#损失函数
def loss(preY,trainY):
return -1/m*(trainY*np.log(preY)+(1-trainY)*np.log(1-preY)).sum()
def sigmoid(x):
return 1/(1+np.exp(-x))
learn_rate=0.5 #学习率
m=train_X.shape[0]
n=train_X.shape[1] #由于添加了前置,这里的n等于文章中的n+1
w=np.zeros([1,n]) #初始化参数w,1行n列
# train_y=train_y.reshape([1,m])
count=0 #迭代次数
plt_epoch=[]
plt_loss=[]
#迭代
for i in range(10000):
preY=sigmoid(w.dot(train_X.T)) #预测值,1行m列,由于数据集是m行n列,这里的train_X.T就是文章中的x
w_C=(learn_rate/m)*(preY-train_y).dot(train_X)
if count%100==0:
#每迭代100次则输出误差值
ls=loss(preY,train_y)
print('epoch:',count,'loss:',ls)
plt_epoch.append(count)
plt_loss.append(ls)
#若w变化不大,则暂停迭代,模型训练完成
if (np.abs(w_C)<0.001).all():
print('最终w变化量:',w_C)
break
count+=1
w-=w_C
print('迭代次数:',count)
print('w权重:',w)
#绘制迭代次数与损失函数的关系
plt.plot(plt_epoch,plt_loss)
epoch: 0 loss: 0.6931471805599454
epoch: 100 loss: 0.02806346493668348
epoch: 200 loss: 0.01497424185854577
epoch: 300 loss: 0.010341734055737531
epoch: 400 loss: 0.007945436620121295
epoch: 500 loss: 0.006473035961682875
epoch: 600 loss: 0.005473323387074791
epoch: 700 loss: 0.0047485829736239145
epoch: 800 loss: 0.004198225319392855
epoch: 900 loss: 0.0037655642147051117
epoch: 1000 loss: 0.003416173435881908
epoch: 1100 loss: 0.0031279137308657568
epoch: 1200 loss: 0.002885887831874862
最终w变化量: [[ 0.00000628 0.00099987 -0.00010276]]
迭代次数: 1262
w权重: [[ 0.09543459 -6.92496003 0.34671816]]
训练迭代次数与代价函数计算结果的关系
6.4 验证数据
preY=np.where(sigmoid(w.dot(val_X.T))>0.5,1,0) #由于数据集是m行n列,这里的val_X.T就是文章中的x
print('预测:',preY)
print('实际:',val_y)
print('误差:',preY-val_y)
预测: [[0 0 0 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 0 0 0 1 1 0 0]]
实际: [0 0 0 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 0 0 0 1 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 0 0]]
七、sklearn代码实现
from sklearn.linear_model import LogisticRegression
import joblib
#初始化模型
lr_model = LogisticRegression()
#训练
lr_model.fit(train_X, train_y)
#保持模型
joblib.dump(lr_model, './LogisticRegression_Binary.model')
#加载模型,在实际应用中直接加载已训练好的模型
lr_model = joblib.load('./LogisticRegression_Binary.model')
#预测
y_pred = lr_model.predict(val_X)
print("Prediction on test set:", y_pred)
print("Score on test set:", lr_model.score(val_X, val_y))
运行结果
Prediction on test set: [0 0 0 0 1 0 1 1 1 0 1 0 1 0 1 1 1 0 0 0 0 1 1 0 0]
Score on test set: 1.0