2.2 逻辑回归-part1
0x00 Abstract
- 回归:假设现在有一些数据点,我们用一条直线对这些点进行拟合(这条直线称为最佳拟合直线),这个拟合的过程就叫做回归。
- 线性回归:假设因变量和自变量之间是线性关系(拟合出一条直线)。
- 逻辑回归和线性回归都是广义的线性回归模型的特例。
- 线性回归只能用于回归问题,逻辑回归则用于分类问题(可由二分类推广至多分类)。
- 线性回归使用最小二乘法作为参数估计方法,逻辑回归使用极大似然法作为参数估计方法。
- 逻辑回归去除 Sigmoid 函数就是线性回归,可以说线性回归是逻辑回归的理论基础。逻辑回归通过 Sigmoid 函数引入了非线性因素,得以解决分类问题。
0x01 LR 逻辑回归的理论
以一个例子开始:《信用舆情预测》
学习输入到输出的映射:
1. 建模
定义一个条件概率:
如果令
Sigmoid 函数:
Sigmoid 函数通常记作
因此如果将
即
补充: 通常在公式中如果出现带有转置符号的参数,如
,一般默认为一个列向量的转置,即行向量 若参数不带有转置符号,如 ,一般默认一个列向量。
如下,以刚开始的例子,需要对最下面一行的人做信用预测。将他的数据带入逻辑回归公式,就可以得到预测结果。对
如上,对于二分类问题可以合并到一条公式来表示。
2. 目标函数
目标函数是数据集中所有样本预测结果的正确概率的连乘。
那么最大化目标函数,就意味着,尽可能使更多的样本预测结果正确,使得所有的预测都是对的。或者说,去寻找一条完美的决策边界,将正负样本准确无误地分到两侧。也就是找到最合适的
这里也反应了回归的本质。线性回归是用一条直线尽可能的去拟合数据中的点 (x,y),而逻辑回归的目的,也是去拟合出一条直线(决策边界),使得正负样本可以尽可能准确地被划分在边界两侧。
3. 最大化目标函数
求解最大化目标函数的过程中,引入 log 将连乘变为相加,并且避免精度越界。取负号,将求解最大值问题转化为求解最小值。
最后将二分类问题公式代入。得到了形如交叉熵损失函数的公式。
至此,有了优化的目标:最小化这个交叉熵函数。如何实现?那就要依赖下面将要介绍的优化算法,如梯度下降法等。
4. 优化算法
- GD (Gradient Descent)
- 假设 f(w) 是关于 w 的凸函数,要求解该函数的最小值。那么梯度下降法的公式如下。
- 以一定的步长
向梯度 增长的反方向移动,直到函数值变化极小时停止。 - 因为梯度指向函数增长最快的方向,如果想要求得最小值,那么很自然的,就应该向梯度的反方向移动 w。一般将梯度记为
或者 。 - 梯度下降用于求最小值,梯度上升用于求最大值。
- SGD (Stochastic Gradient Descent)
- 与 GD 的区别在于:GD 每次迭代需要对整个 epoch 的数据算 Loss,然后更新 w;SGD 则是取数据中的一个样本计算 Loss 然后更新 w。
- 因此 GD 运算量大,占用内存多,时间长,但可以找到最优解;SGD 运算量少,更快,但不一定会找到最优解。
- Mini-batch Gradient Descent
- 是对 GD 与 SGD 折中的方法。每次取一个小 batch 做梯度更新。
一般使用随机梯度下降 SGD。
4.1. 梯度下降法 GD
- 梯度下降求解
- 对 w 求导
- 上面三行是对目标函数的分解
- 下面两行是对目标函数中 w 的求导
- 有趣的是,在中括号中间的两项,左项其实就是等同第一行的预测值,右项即已知的真实值。
- 对 b 求导
- 上面三行也是对目标函数的分解
- 下面两行是对目标函数中 b 的求导
- 对 w 求导
4.2. 随机梯度下降 SGD
0x02 实践1:LR 在简单数据集上的分类案例
REPO:Dive-into-NLP/2.ml-and-dl-foundation/2.2.logistic-regression at main · 1nnoh/Dive-into-NLP · GitHub
1. 案例描述
在一个简单的数据集上,采用梯度下降法找到 Logistic 回归分类器在此数据集上的最佳回归系数。
1.1. 开发流程
数据采集: 可以使用任何方法 数据预处理: 由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳 数据分析: 画出决策边界 训练算法: 使用梯度下降找到最佳参数 测试算法: 使用 Logistic 回归进行分类 使用算法: 对简单数据集进行分类
1.2. 数据采集
该案例采用 100 行的数据集文本。其中前两列是特征 1 和特征 2,第三列是对应的类别标签。(两列特征并无实际含义,可以理解为特征 1 为身高,特征 2 为体重。类别判断性别为男或女。)
testSet.txt
-0.017612 -14.053064 0.0 -1.395634 -4.662541 1.0 -0.752157 -6.53862 0.0 ......
2. 梯度下降训练算法
import numpy as np
''' sigmoid跳跃函数 '''
def sigmoid(inX):
return 1.0 / (np.exp(-inX) + 1)
'''加载数据集和类标签'''
def loaddata(filename):
# dataMat 为原始数据, labelMat 为原始数据的标签
datamat, labelmat = [], []
fr = open(filename)
for line in fr.readlines():
linearr = line.strip().split('\t')
datamat.append([1.0, float(linearr[0]), float(linearr[1])])
# 为了方便计算(将 b 放入矩阵一起运算),在第一列添加一个 1.0 作为 x0
# w^T x + b = [w1 w2]^T * [x1 x2] + b = [b w1 w2]^T * [1.0 x1 x2]
labelmat.append(float(linearr[-1]))
return datamat, labelmat
''' 梯度下降法,得到的最佳回归系数 '''
def gd(data, label):
datamat = np.mat(data) # 转换为 NumPy 矩阵
labelmat = np.mat(label).transpose() # 首先将数组转换为 NumPy 矩阵,然后再将行向量转置为列向量
# 转化为矩阵[[0,1,1,1,0,1.....]],并转置[[0],[1],[1].....]
# transpose() 行列转置函数
# 将行向量转化为列向量 => 矩阵的转置
# m->数据量,样本数 n->特征数
m,n = datamat.shape # 矩阵的行数和列数
# print(m,n)
weight = np.ones((n,1)) # 初始化回归系数 w^T [1 1 1]
iters = 200 # 迭代次数
learn_rate = 0.001 # 步长
for i in range(iters):
# 因为根据公式,每次更新 w 需要对所有的数据 xi 做一个误差求和,
# 然后乘以步长 learn_rate。所以这里都是对整个数据集矩阵做运算。
# 所以说内存里要将所有数据都存放进来做运算
# 也正是这个原因,GD 的运算速度会慢于 SGD。
# SGD 每次迭代只随机取数据集中的一个样本。
# wx:
gradient = datamat*np.mat(weight) # 矩阵乘法
# sigmoid(w^T*x+b):
out = sigmoid(gradient) # 获得预测值
# sigmoid(w^T*x+b) - yi:
errors = labelmat - out # labelmat 为真实值,相减得到误差
# 更新回归系数 w^T:
weight = weight + learn_rate * datamat.T * errors
# 最后一项:
# 0.001* (3*m)*(m*1) 即 步长*grad(w^Txi+b)*xi 的求和,得到 3*1 的列向量。
# 得到的是 m 列的所有数据 x0,x1,x2 的偏移量求和。
return weight.getA() # 矩阵转为数组
if __name__ == '__main__':
datamat,labelmat = loaddata('testSet.txt')
weight = gd(datamat,labelmat)
print(weight)
代码分析: gd(data, label)
函数的两个参数是数据加载返回的特征集和标签类集合。对数据集进行 mat 矩阵化转化,而类标签集矩阵化之后转置,便于行列式的计算。然后设定步长,和迭代次数。整个特征矩阵与回归系数相乘再求 sigmoid 值,最后返回更新得到的回归系数的值。运行结果如下:
[[2.88492031]
[0.39158333]
[0.45880875]]
总结: 梯度下降法在每次更新梯度(回归系数)时都需要遍历整个数据集,该方法在处理有 100 个样本(仅有两个特征)的数据集时尚可,但如果数据集有上亿个样本,成千上万的特征,那么该方法的计算复杂度就会非常高。
针对这种情况,一种改进方法就是每次仅仅针对一个样本点来更新回归系数,该方法称为随机梯度下降 SGD。
3. 随机梯度下降训练算法
import random
import numpy as np
import matplotlib.pyplot as plt
''' sigmoid 跳跃函数 '''
def sigmoid(inX):
return 1.0 / (np.exp(-inX) + 1)
''' 加载数据集和类标签 '''
def loaddata(filename):
# dataMat 为原始数据, labelMat 为原始数据的标签
datamat, labelmat = [], []
fr = open(filename)
for line in fr.readlines():
linearr = line.strip().split('\t')
datamat.append([1.0, float(linearr[0]), float(linearr[1])])
# 为了方便计算(将 b 放入矩阵一起运算),在第一列添加一个 1.0 作为 x0
# w^T x + b = [w1 w2]^T * [x1 x2] + b = [b w1 w2]^T * [1.0 x1 x2]
labelmat.append(float(linearr[-1]))
return datamat, labelmat
''' 随机梯度下降法 得到的最佳回归系数 '''
def sgd(data,label):
datamat = np.mat(data)
labelmat = np.mat(label).transpose()
m, n = datamat.shape
weight = np.ones((n,1)) # 创建与列数相同的矩阵的系数矩阵
iters = 200
for i in range(iters):
dataindex = list(range(m)) # 返回 [0, 1, 2, ..., m] 的列表作为 index
for j in range(m):
learn_rate=4/(i+j+1)+0.01
# 随着轮数的增加,学习率(或步长)逐渐变小
randinx = int(random.uniform(0,len(dataindex))) # 随机取一个 index
# random.uniform(x, y) 随机生成一个实数,它在[x,y]范围内
out = sigmoid(datamat[randinx]*weight) # 计算预测值
error = labelmat[randinx] - out # 计算预测值与真实值的误差
weight = weight + learn_rate * datamat[randinx].T * error # 更新 w
del(dataindex[randinx])
# 学完一个样本,删除掉一个
return weight.getA()
''' 数据可视化展示 '''
def plotBestFit(dataArr, labelMat, weights):
n = np.shape(dataArr)[0]
xcord1, xcord2, ycord1, ycord2 = [],[],[],[]
for i in range(n):
if int(labelMat[i]) == 1:
xcord1.append(dataArr[i, 1])
ycord1.append(dataArr[i, 2])
else:
xcord2.append(dataArr[i, 1])
ycord2.append(dataArr[i, 2])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')
ax.scatter(xcord2, ycord2, s=30, c='green')
x = np.arange(-3.0, 3.0, 0.1)
"""
dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
w0*x0+w1*x1+w2*x2=f(x)
x0最开始就设置为1, x2就是我们画图的y值,而f(x)被我们磨合误差给算到w0,w1,w2身上去了
所以: w0+w1*x+w2*y=0 => y = (-w0-w1*x)/w2
"""
y = (-weights[0] - weights[1] * x) / weights[2]
ax.plot(x, y)
plt.xlabel('X')
plt.ylabel('Y')
plt.show()
if __name__ == '__main__':
# 1.加载数据
datamat, labelmat = loaddata('testSet.txt')
# 2.训练模型,f(x)=a1*x1+b2*x2+..+nn*xn 中 (a1,b2, .., nn).T 的矩阵值
weight = sgd(datamat, labelmat)
print(weight)
# 3.数据可视化
dataArr = np.array(datamat)
plotBestFit(dataArr, labelmat, weight)
代码分析: 加载数据以及 sigmoid 函数与 GD 算法是一样的。区别在于 sgd(data,label)
函数。梯度下降在每次更新数据集时都需要遍历整个数据集,计算复杂度较高;随机梯度下降一次只用一个样本点来更新回归系数。
- 这里还对学习率 lr 做了优化,每次迭代时,lr 都会调整,随着迭代次数不断减小,但不会为 0,因为公式中有一个常数项。
- 通过
randinx
随机选择样本来更新梯度。每次随机从dataindex
列表选择一个 index 作为randinx
,更新后则从dataindex
列表删除该 index。直到所有样本都被选取后,算是进行了一次 iteration。
0x03 正则化
机器学习中经常会在损失函数中加入正则项,称之为正则化(Regularization)。正则化是用来防止模型过拟合的一种手段。下面先介绍什么是过拟合与欠拟合,然后再引入正则化。
1. 欠拟合 & 过拟合
- 欠拟合:没有学习充分。
- 过拟合:过度学习,将噪声也学习了进去;过度拟合样本,泛化性差(比如过度拟合训练集,但迁移测试集时效果很差)。
小结:欠拟合是比较容易解决的,比如增加训练轮数,让模型多学一些,或者让模型复杂些,增强学习能力。而过拟合是一个关键问题,为了保证模型的泛化性,防止过拟合,有许多的方法。正则化是其中一种常用的手段。
2. 正则化
目的:防止过拟合,提高泛化性,防止模型只在训练集上有效、在测试集上不够有效。 原理:在损失函数中加上某些规则(限制),缩小解空间,从而减少求出过拟合解的可能性。
- 用一个例子来解释一下为什么需要正则化?
- 例子:还是以上面的 LR 实践的情景作为案例——做性别的二分类(男或女)。有两个特征:身高 ——
,体重 —— 。将这两个特征放入 LR 模型,就是: , 是激活函数 Sigmoid, 分别是两个特征的权重,也是模型要学习的两个参数。 - 过拟合:把班级 1 作为训练集,班级 2 作为测试集。假如班级 1 中,恰好所有男生身高都大于 1.8m,而女生身高都小于 1.7m,也就是说仅通过身高这一个特征,就可以对训练集正确分类。那就很可能学习到如:
=70, =0.4 的参数。甚至 会更大,因为在当前班级 1 中,只通过身高判断就可以了。即使 不需要这么大也能正确判断。此时,如果在测试集(班级 2)预测,而班级 2 里的男女生身高是正常分布的,那么使用这样的一组参数 —— =70, =0.4 来预测,极大概率是会判断错误的(因为相当于是只考虑身高特征)。这就是过拟合,或者说不具备泛化性。 - 正则化的作用:给损失函数加上正则化后,缩小了参数的解空间,让某些关键特征的权重参数不会增长的过大,仅仅够用就可以了。比如得到
=0.7, =0.4 这样一对参数解,明显是比之前过拟合的参数更加合理。 - 如何实现正则化:有 L1 正则化,L2 正则化,Dropout 正则化以及 BN,LN 等等。
- L1 正则化:在损失函数中加入一次惩罚项(一次的 L1 范数)。L1 范数是指向量中各个元素绝对值之和。L1 范数可以进行特征选择,即让特征的权重系数变为 0。
- L2 正则化:在损失函数中加入二次惩罚项(二次的 L2 范数)。L2 范数是指向量各元素的平方和然后求平方根。L2 范数可以防止过拟合,提升模型的泛化能力,让模型权重尽可能接近 0。
- Dropout 正则化:在训练过程中,每次都随机失活一部分神经元,神经元失活时权重即为 0。这样可以保证模型不会过度依赖某几个神经元(给予他过高的权重)。当这几个神经元失活时,就必须通过其他的特征来完成预测。和 L2 正则化的功能相近,能够实现对模型权重的约束。
- 例子:还是以上面的 LR 实践的情景作为案例——做性别的二分类(男或女)。有两个特征:身高 ——
L1 与 L2 正则化
从公式角度理解: 如图,最上面两条公式:L1 正则化添加了惩罚项
如果最小化损失函数,当左边原损失函数尽可能小时,右边添加的惩罚项也会尽可能小。
从图的角度理解: 再看下面两张图。二维坐标上,每个点代表
既然红色曲线上这么多参数点的损失函数都是一样的,那我们自然希望取到一个最合适的参数点:
L1 正则化的曲线是一个菱形,与损失函数相交的点在 y 轴。也就是说,此时
L2 正则化会使得参数尽可能的小,从而防止模型过拟合。还有一个优点是处处可导,而 L1 正则化曲线的四个角上是不可导的。
3. 总结
梳理一下,正则化有多种方式,包括 L0(向量中非零元素个数),L1(向量中元素绝对值只和),L2(向量的模)。但是 L0 范数的求解是个 NP 完全问题,而 L1 也能实现稀疏并且比 L0 有更好的优化求解特性,因此 L1 被广泛应用。
L2 范数指向量中各元素求平方和后开根号的值,可以令 w 各元素尽可能接近 0。虽然不如 L1 范数更彻底的降低模型复杂度(使特征稀疏),但是可以防止过拟合,而且处处可微,降低了计算难度。
Conclusion
学习了逻辑回归的理论基础,并且在简单数据集上实践了,用梯度下降法来训练逻辑回归分类器。最后还学习了 L1 与 L2 正则化,并且探究了这两个正则化如何防止了模型过拟合。
References
逻辑回归: 浅析机器学习:线性回归 & 逻辑回归 - 知乎 一步步教你轻松学逻辑回归模型算法 - 伏草惟存
正则化: 正则化方法一篇就够了 L1范数与L2范数的区别 - 知乎 机器学习必知必会:正则化 - 知乎
稀疏矩阵: Python稀疏矩阵详解 - 知乎