2.2 逻辑回归-part1

0x00 Abstract

  • 回归:假设现在有一些数据点,我们用一条直线对这些点进行拟合(这条直线称为最佳拟合直线),这个拟合的过程就叫做回归
  • 线性回归:假设因变量和自变量之间是线性关系(拟合出一条直线)。
  • 逻辑回归和线性回归都是广义的线性回归模型的特例
  • 线性回归只能用于回归问题,逻辑回归则用于分类问题(可由二分类推广至多分类)。
  • 线性回归使用最小二乘法作为参数估计方法,逻辑回归使用极大似然法作为参数估计方法。
  • 逻辑回归去除 Sigmoid 函数就是线性回归,可以说线性回归是逻辑回归的理论基础。逻辑回归通过 Sigmoid 函数引入了非线性因素,得以解决分类问题。

0x01 LR 逻辑回归的理论

以一个例子开始:《信用舆情预测》

学习输入到输出的映射:- x:输入(个人的信息) - y:输出(预测逾期风险) - 标签为 1 代表逾期。

1. 建模

定义一个条件概率:,相当于用模型来捕获 之间的关系。

如果令 ,由于这样一个线性回归的式子的输出(因变量)在一个实数范围内,并不能满足二分类的需求(输出 0~1 的区间),因此需要一个激活函数来将最后的输出控制到 (0,1) 区间。这个激活函数就是 Sigmoid 函数。

Sigmoid 函数:

Sigmoid 函数通常记作

因此如果将 作为自变量带入到 ,那么就可以得到一个因变量(输出)在 (0,1) 区间的函数。

补充: 通常在公式中如果出现带有转置符号的参数,如 ,一般默认为一个列向量的转置,即行向量 若参数不带有转置符号,如 ,一般默认一个列向量。

如下,以刚开始的例子,需要对最下面一行的人做信用预测。将他的数据带入逻辑回归公式,就可以得到预测结果。对 或者 做预测都一样,因为

如上,对于二分类问题可以合并到一条公式来表示。

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 的求导

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 正则化的功能相近,能够实现对模型权重的约束。

L1 与 L2 正则化

从公式角度理解: 如图,最上面两条公式:L1 正则化添加了惩罚项 是正则化系数,当 越大时,正则化约束越强。代表 w 的 L1 范数。L2 正则化则是添加了 w 的 L2 范数的二次项作为惩罚项。

如果最小化损失函数,当左边原损失函数尽可能小时,右边添加的惩罚项也会尽可能小。

从图的角度理解: 再看下面两张图。二维坐标上,每个点代表 的取值。像等高线一样的曲线上,每条曲线代表着——该曲线上所有的 参数点带入损失函数的值相等。更直观的理解:以最外圈的红线为例,沿着红线向右变大,就相应变小,那么自然可以做到损失函数相等。补充:最外圈的红线,其实也是损失函数最小的地方。

既然红色曲线上这么多参数点的损失函数都是一样的,那我们自然希望取到一个最合适的参数点:都不会过大,尽可能接近 0。

L1 正则化的曲线是一个菱形,与损失函数相交的点在 y 轴。也就是说,此时 的取值为 0。因此 L1 正则化会使得特征变得稀疏,起到了筛选特征,减小模型复杂度的作用。 因为 Loss 的最小值一般在坐标轴上取到,这时候说明其中有一个特征的权重变成 0 了,从而起到了特征稀疏化的作用。

L2 正则化会使得参数尽可能的小,从而防止模型过拟合。还有一个优点是处处可导,而 L1 正则化曲线的四个角上是不可导的。

3. 总结

梳理一下,正则化有多种方式,包括 L0(向量中非零元素个数),L1(向量中元素绝对值只和),L2(向量的模)。但是 L0 范数的求解是个 NP 完全问题,而 L1 也能实现稀疏并且比 L0 有更好的优化求解特性,因此 L1 被广泛应用

L2 范数指向量中各元素求平方和后开根号的值,可以令 w 各元素尽可能接近 0。虽然不如 L1 范数更彻底的降低模型复杂度(使特征稀疏),但是可以防止过拟合,而且处处可微,降低了计算难度。

Conclusion

学习了逻辑回归的理论基础,并且在简单数据集上实践了,用梯度下降法来训练逻辑回归分类器。最后还学习了 L1 与 L2 正则化,并且探究了这两个正则化如何防止了模型过拟合。

References

逻辑回归: 浅析机器学习:线性回归 & 逻辑回归 - 知乎 一步步教你轻松学逻辑回归模型算法 - 伏草惟存

正则化: 正则化方法一篇就够了 L1范数与L2范数的区别 - 知乎 机器学习必知必会:正则化 - 知乎

稀疏矩阵: Python稀疏矩阵详解 - 知乎