文章目录
- 逻辑回归原理
- 什么是逻辑回归
- Sigmoid函数
- 逻辑回归的损失函数
- 损失函数变换过程:从极大似然估计理解逻辑回归的损失函数
- 损失函数变换过程:从交叉熵的角度理解逻辑回归的损失函数
- 逻辑回归损失函数求解
- 逻辑斯特回归为什么要对特征进行离散化
- 逻辑回归应用
- 优缺点(特点)
- 一般应用场景
- 对于过拟合和欠拟合等优化方案
- 基于逻辑回归的分类示例
- 手动实现逻辑回归
- 使用sklearn逻辑回归模型
逻辑回归原理
什么是逻辑回归
注意,本文里的y_pred指的是y预测值
逻辑回归是用来做分类算法的,大家都熟悉线性回归,一般形式是y_pred=aX+b,y的取值范围是[-∞, +∞],有这么多取值,怎么进行分类呢?不用担心,伟大的数学家已经为我们找到了一个方法。
也就是把Y的结果带入一个非线性变换的Sigmoid函数中,即可得到(0,1)之间取值范围的数S,S可以把它看成是一个概率值,如果我们设置概率阈值为0.5,那么S大于0.5可以看成是正样本,小于0.5看成是负样本,就可以进行分类了。
再简短说下:
- 为啥叫逻辑回归:创造者名叫逻辑斯谛(Logistic )
- 线性回归的输出是[-∞, +∞],通过Sigmoid函数转换成(0,1)之间的值
- 分类模型
- 输出目标:(0,1)之间的值
- 模型:直观上看就是线性模型加入sigmoid函数:y_pred = sigmoid(W0 + W1*X1+…+WnXn)
Sigmoid函数
公式:
其中 z 是 wx + b,(其实就是线性模型中的预测值y,只不过这里习惯写成 z ),扩展开来 z = w0+w1x1…+wnxn
图像:
性质:
- 单调性。必须单调性,保持和原线性模型输出的结果单调性的一致性。因为Sigmoid函数只是把结果压缩到(0,1)而已,并不改变结果值的走向
- 两边梯度趋于饱和,即导数趋于0(作为激活函数在神经网络中的弊端)
- 1/2处的导数最大
- 设f(x)=sigmoid(x),f(x)导数为f(x)(1-f(x))
- 不以原点为中心(作为激活函数在神经网络中的弊端)
逻辑回归的损失函数
任何一个模型都应该有个损失函数,就是描述预测值与实际值的差值的函数。
先上损失函数(这里假设样本服从伯努利分布(0-1分布)):
损失函数变换过程:从极大似然估计理解逻辑回归的损失函数
逻辑回归的损失函数别名1:负对数似然损失
假设样本服从伯努利分布(0-1分布):
小结一下
- 1)样本服从什么分布:伯努利(0-1)
- 2)损失函数的由来:伯努利分布的极大似然估计
- 这里怎么理解呢:回到P(Y|X),这表示的是在X情况下Y的概率,在0-1分布的式子里,当然希望预测值越大越好,放到整个样本来描述就是,当x1,x2…xn情况下预测值的情况就是联合概率P(Y|x1,x2,x3…xn),就是所有概率值连乘。并且,我们仍然是希望预测值越大越好,即希望概率值连乘的结果越大越好,上述式子就能看明白了吧! 这就是极大似然函数,也是我们的目标函数。
- 为啥加log:log是单调性的,能够和原式子保持一致的结果单调性,不会影响式子求解。连乘法加log变成加法,方便求解
- 为啥加负号:一般求极小值更容易有优化方法,方便求解
损失函数变换过程:从交叉熵的角度理解逻辑回归的损失函数
逻辑回归的损失函数别名2:交叉熵损失
参考网上文章叙述
先引入一个概念:KL散度:衡量两个概率分布的差异
逻辑回归模型最后的计算结果(通过sigmoid或softmax函数)是各个分类的概率(可以看做是各个分类的概率分布)。那么假设真实的概率分布是,估计得到的概率分布是, 这两个概率分布的距离如何去衡量?
在信息论中,「相对熵」,也就是KL散度,可以衡量两个概率分布的差异性。具体公式为:
因为交叉熵越大,KL散度越大,也可以用交叉熵来衡量两个概率分布之间的距离,所以逻辑回归使用交叉熵作为逻辑回归的损失函数。
逻辑回归损失函数求解
一个模型在一个特定样本分布上只有一个损失函数
为啥逻辑回归要用梯度下降?因为参数项太多了,无法直接求导得出最优解
先明确几个概念:
- 1)梯度下降属于优化算法来求解逻辑回归的损失函数,牛顿法也是属于优化算法,都是迭代算法
- 2)LR中,梯度下降求解是参数W
- 3)梯度下降中的“梯度”针对的是损失函数loss
这里就是使用梯度下降来求解逻辑回归的损失函数的参数
梯度的定义:
图示梯度下降样例:如图,当我要往山底走时(求最小值);我的方向四面八方,参考的东西(特征)很多,比如树的稀疏程度、草的茂盛程度、动物出没情况等等;你不清楚啥情况的时候,不知道往哪个方向,那么梯度公式就告诉你当前这一步你尝试往gradient(w)这个方向走a步(阿尔法符号)看看(梯度–>方向),每走完一步又继续确认方向再往前试探的走,直到走到谷底(求到最小值)。
LR中梯度法的推导:
推导流程来源于网络,但是意思能表示清楚
最后,带入梯度下降公式:这也就是LR的w参数关系式:
逻辑斯特回归为什么要对特征进行离散化
- 引入非线性:逻辑回归属于广义线性模型,表达能力受限;单变量离散化为N个后,每个变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合; 离散特征的增加和减少都很容易,易于模型的快速迭代;
- 速度快:稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;
- 鲁棒性:离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;
- 方便交叉与特征组合:离散化后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;
- 稳定性:特征离散化后,模型会更稳定,比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻处的样本会刚好相反,所以怎么划分区间是门学问;
- 简化模型:特征离散化以后,起到了简化了逻辑回归模型的作用,降低了模型过拟合的风险。
逻辑回归应用
优缺点(特点)
- 轻量级训练快:因为是线性的套了个sigmoid,故计算很快。
- 易存储易扩展:稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展
- 鲁棒性:离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;
- LR的可解释性强:参数代表每个特征对输出的影响,可解释性强
- 因为结果是概率,可以做ranking model
- 解决过拟合的方法很多,如L1、L2正则化
- 具有参数记忆从而进行特征选择的用途
- 缺点:因为它本质上是一个线性的分类器,所以处理不好特征之间相关的情况,即更喜欢特征独立的样本数据
- 缺点:特征空间很大时,性能不好。
- 缺点:容易欠拟合,精度不高
一般应用场景
- 模型的baseline
- CTR预估/推荐系统的learning to rank/各种分类场景。
- 某搜索引擎厂的广告CTR预估基线版是LR。
- 某电商搜索排序/广告CTR预估基线版是LR。
- 某电商的购物搭配推荐用了大量LR。
- 某现在一天广告赚1000w+的新闻app排序基线是LR
对于过拟合和欠拟合等优化方案
- 梯度下降实际上就是一种优化方案,同类的还有牛顿法,这里不展开了
- 对于过拟合和欠拟合通常使用正则化方式处理,对于正则化与逻辑回归的优化过程,后面单独开一篇详细描述
基于逻辑回归的分类示例
本示例代码取用sklearn的鸢尾花数据集,并取用标签结果为0,1的数据集来测试分类
手动实现逻辑回归
from math import exp
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
# data
def create_data():
# 加载鸢尾花数据集(sklearn中的数据集)
iris = load_iris()
# 通过feature_names构造dataFrame
df = pd.DataFrame(iris.data, columns=iris.feature_names)
# 把iris的结果放到dataFrame的label属性中
df['label'] = iris.target
# 声明dataFrame的新列项
df.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'label']
# 切取dataFrame前100行,index为0,1,-1的列
data = np.array(df.iloc[:100, [0,1,-1]])
# data[:,:2]切取所有行和前两列 ;data[:,-1]切取所有行和index=-1的那列
return data[:,:2], data[:,-1]
class LogisticReressionClassifier:
def __init__(self, max_iter=200, learning_rate=0.01):
self.max_iter = max_iter
self.learning_rate = learning_rate
# 定义sigmoid函数
def sigmoid(self, x):
return 1 / (1 + exp(-x))
def data_matrix(self, X):
data_mat = []
for d in X:
# 把d所有值放到数组里并append到data_mat里。这里为啥第一项是1.0?因为我们的公式w0+w1x1+....wnxn,公式中没有x0,实际上第一项x0等同于1.0
data_mat.append([1.0, *d])
return data_mat
def fit(self, X, y):
# 获得数据矩阵,也就是我们公式中的x集合
data_mat = self.data_matrix(X) # m*n
# 初始化参数w,即公式中的w0,w1,w2。为啥这里初始化全为0?因为训练过程中会逐渐根据损失函数调整参数w
self.weights = np.zeros((len(data_mat[0]), 1), dtype=np.float32)
# 迭代的根据损失函数优化参数w的取值
for iter_ in range(self.max_iter):
for i in range(len(X)):
# 计算逻辑回归的结果
result = self.sigmoid(np.dot(data_mat[i], self.weights))
# 计算损失值
error = y[i] - result
# 根据损失函数更新参数w,其中np.transpose表示矩阵转置
self.weights += self.learning_rate * error * np.transpose([data_mat[i]])
print('LogisticRegression Model(learning_rate={},max_iter={})'.format(
self.learning_rate, self.max_iter))
# 模型得分
def score(self, X_test, y_test):
right = 0
X_test = self.data_matrix(X_test)
# zip是方便for取到每个x,y
for x, y in zip(X_test, y_test):
# 根据训练出来的w参数对测试数据集进行校验真实结果
result = np.dot(x, self.weights)
# 由于数据集只取了前100行,故y的值只有0,1,用来分类
if (result > 0 and y == 1) or (result < 0 and y == 0):
right += 1
return right / len(X_test)
# ====================================开始训练============
# 调用函数获得样本集
X, y = create_data()
# 划分数据集为训练、测试数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
lr_clf = LogisticReressionClassifier()
# 训练
lr_clf.fit(X_train, y_train)
lr_clf.score(X_test, y_test)
# score结果打印:0.9666666666666667
画图展示
# 画图展示参数划分结果
x_ponits = np.arange(4, 8)
y_ = -(lr_clf.weights[1]*x_ponits + lr_clf.weights[0])/lr_clf.weights[2]
plt.plot(x_ponits, y_)
#lr_clf.show_graph()
plt.scatter(X[:50,0],X[:50,1], label='0')
plt.scatter(X[50:,0],X[50:,1], label='1')
plt.legend()
使用sklearn逻辑回归模型
# 使用SKlearn里的模型
from sklearn.linear_model import LogisticRegression
# 设置迭代次数为200得到模型
clf = LogisticRegression(max_iter=200)
# 训练(沿用手动实现中的划分数据集)
clf.fit(X_train, y_train)
# 查看得分(沿用手动实现中的划分数据集)
clf.score(X_test, y_test)
# 打印参数w;coef_和intercept_都是模型参数,即为w;intercept_为w0,coef_为w1到wn
print(clf.coef_, clf.intercept_)
score为:1.0
打印的参数w为:[[ 2.71974676 -2.60054221]] [-6.86475951]
画图展示:
# 图像化展示分类效果
x_ponits = np.arange(4, 8)
y_ = -(clf.coef_[0][0]*x_ponits + clf.intercept_)/clf.coef_[0][1]
plt.plot(x_ponits, y_)
plt.plot(X[:50, 0], X[:50, 1], 'bo', color='blue', label='0')
plt.plot(X[50:, 0], X[50:, 1], 'bo', color='orange', label='1')
plt.xlabel('sepal length')
plt.ylabel('sepal width')
plt.legend()
以上,若有表述错误的地方,希望指出,谢谢!