人工智能机器学习算法总结--2.逻辑回归算法

1,383 阅读31分钟

温馨提醒:在阅读并理解前文1.线性回归的基础上再看本文会更容易理解

1.定义,目的以及作用

定义:

逻辑回归算法是一种用于解决分类问题的机器学习算法,尽管名字中包含"回归"一词,但实际上逻辑回归是一种分类算法,而不是回归算法。逻辑回归通常用于处理二分类问题(离散值的预测),即将输入数据分为两个类别。它的输出是一个介于0和1之间的概率值表示某个样本属于某个类别的概率

目的:

逻辑回归算法基于逻辑函数(也称为Sigmoid函数)来建模输出,将线性组合的特征与Sigmoid函数结合,从而得到一个在0到1之间的概率值。在训练过程中,逻辑回归通过最大化似然函数或最小化损失函数(比如交叉熵损失函数)来学习模型参数,以便能够对新的样本进行分类

作用:

逻辑回归常用于二分类问题,例如判断邮件是垃圾邮件还是非垃圾邮件、患者是否患有某种疾病等。虽然逻辑回归是一种简单的算法,但在许多实际应用中仍然非常有用,并且可以作为其他更复杂分类算法的基准。

2.算法结构

逻辑回归算法的结构相对简单而直观。让我用文字描述一下逻辑回归算法的基本结构:

  1. 输入层:输入层接收特征数据,每个特征对应输入层的一个节点。
  2. 权重:每个输入特征都有一个对应的权重,用来衡量该特征对最终输出的影响程度。(解释一下。这里所谓的权重其实就是参数θ,有多少个特征就有多少个参数θ,全部的θ组合起来形成一个矩阵)
  3. 线性组合:输入特征和对应的权重进行线性组合,得到加权和(这里利用矩阵进行求和计算)。
  4. 激活函数:线性组合的结果经过激活函数,通常是Sigmoid函数(也称为逻辑函数),将结果映射到[0, 1]之间,表示样本属于正类的概率。
  5. 输出层:输出层输出分类结果,通常以0.5为阈值进行分类,大于0.5为正类,小于0.5为负类。
  6. 损失函数:通过比较模型输出和真实标签,计算损失值,常用的损失函数是交叉熵损失函数。
  7. 优化算法:通过优化算法(如梯度下降)来调整模型参数,使损失函数最小化,从而使模型能够更好地预测样本的类别(这里的损失函数其实就是代价函数,通过不断梯度下降找到代价函数的最小值我们就可以较为准确的预测事物分类的概率,打个比方,通过梯度下降不断优化误差值后,我们就可以通过这个模型更好地预测该用户是否会买某件商品的可能,其中最终的输出结果只有0和1,也就是买与不买)。

总的来说,逻辑回归算法的结构包括输入层、权重、线性组合、激活函数、输出层、损失函数和优化算法。这些组件共同作用,使得逻辑回归能够有效地进行二分类任务的预测。希望这个简要的描述能帮助您更好地理解逻辑回归算法的结构!

3.算法组成One-数据准备

1.读取文件,存储数据

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 引入所需要的库

path = 'ex2data1.txt'
# 文件路径
data = pd.read_csv(path, header=None, names=['Exam1', 'Exam2', 'Admitted'])
# 存储文件数据
data.head()
# 简化文件数据

2.提取与观测数据(注:这里不是重点,只是通过图像的形式能让你有更直观的理解)

positive = data[data['Admitted'].isin([1])]
negative = data[data['Admitted'].isin([0])]

fig, ax = plt.subplots(figsize=(12, 8))
ax.scatter(positive['Exam1'], positive['Exam2'], s=50, c='b', marker='o', label='Admitted')
ax.scatter(negative['Exam1'], negative['Exam2'], s=50, c='r', marker='x', label='Not Admitted')
ax.legend()
ax.set_xlabel('Exam1 Score')
ax.set_ylabel('Exam2 Score')
plt.show()

这段代码是用来创建一个散点图来可视化数据集中的两个特征(Exam1 和 Exam2)和它们与是否被录取的关系。让我逐句解释一下这段代码:

  1. positive = data[data['Admitted'].isin([1])]:这一行代码从数据集中选择所有被录取(Admitted 为1)的数据点,并将它们存储在 positive 变量中。
  2. negative = data[data['Admitted'].isin([0])]:这一行代码从数据集中选择所有未被录取(Admitted 为0)的数据点,并将它们存储在 negative 变量中。
  3. fig, ax = plt.subplots(figsize=(12, 8)):这一行代码创建了一个新的图形(figure)和一个包含单个轴(axes)的 subplot,并设置了图形的大小为 12x8。
  4. ax.scatter(positive['Exam1'], positive['Exam2'], s=50, c='b', marker='o', label='Admitted'):这一行代码绘制了被录取的数据点,使用蓝色圆圈(marker='o')表示,点的大小为 50,横坐标为 Exam1 的值,纵坐标为 Exam2 的值,标签为 'Admitted'。
  5. ax.scatter(negative['Exam1'], negative['Exam2'], s=50, c='r', marker='x', label='Not Admitted'):这一行代码绘制了未被录取的数据点,使用红色叉号(marker='x')表示,点的大小为 50,横坐标为 Exam1 的值,纵坐标为 Exam2 的值,标签为 'Not Admitted'。
  6. ax.legend():这一行代码添加了图例,显示 'Admitted' 和 'Not Admitted' 对应的标签。
  7. ax.set_xlabel('Exam1 Score'):这一行代码设置 x 轴的标签为 'Exam1 Score'。
  8. ax.set_ylabel('Exam2 Score'):这一行代码设置 y 轴的标签为 'Exam2 Score'。
  9. plt.show():最后一行代码显示了绘制好的散点图。

Tips:

  • (1).data['Admitted'].isin([1])

data['Admitted'].isin([1]) 这段代码是 Pandas 中的一种用法,它的作用是检查 DataFrame(数据框)中 'Admitted' 列中的每个元素是否等于 1。具体来说,isin([1]) 方法会返回一个布尔值的 Series,其中每个元素都表示对应位置的元素是否等于 1。如果某个元素等于 1,则对应位置的值为 True,否则为 False。

在这种情况下,这段代码被用来筛选出 'Admitted' 列中值为 1 的行,即被录取的数据点。这样可以方便地从数据集中提取出符合特定条件的子集,进行进一步的分析或可视化操作。

  • (2).ax.scatter()

当我们解析这段代码 ax.scatter(positive['Exam1'], positive['Exam2'], s=50, c='b', marker='o', label='Admitted') 时,我们可以逐个参数来理解它的作用:

  1. positive['Exam1']:这表示在被录取的数据集中,我们要使用 'Exam1' 列的值作为散点图中的 x 值,即被录取学生的第一次考试成绩。
  2. positive['Exam2']:这表示在被录取的数据集中,我们要使用 'Exam2' 列的值作为散点图中的 y 值,即被录取学生的第二次考试成绩。
  3. s=50:这个参数设置了散点的大小为 50,表示被录取的数据点在图中的大小。
  4. c='b':这个参数设置了散点的颜色为蓝色('b' 代表 blue),表示被录取的数据点在图中的颜色。
  5. marker='o':这个参数设置了散点的形状为圆圈('o'),表示被录取的数据点在图中的形状。
  6. label='Admitted':这个参数设置了图例中对应这些散点的标签为 'Admitted',用来区分不同类别的数据点。

综合起来,这段代码的作用是在散点图中绘制被录取的学生数据点,横坐标为第一次考试成绩,纵坐标为第二次考试成绩,点的大小为 50,颜色为蓝色,形状为圆圈,并且在图例中标记为 'Admitted'。这样可以直观地展示被录取学生的分布情况。

  • (3).fig, ax = plt.subplots(figsize=(12, 8))

这段代码 fig, ax = plt.subplots(figsize=(12, 8)) 主要是用来创建一个新的图形(figure)和一个或多个子图(axes)对象。让我来解释一下其中的细节:

  • plt.subplots() 是 Matplotlib 库中用于创建图形和子图的函数。在这里,fig, ax = plt.subplots(figsize=(12, 8)) 创建了一个新的图形和一个子图对象。
  • fig 是代表整个图形的对象,可以包含一个或多个子图。通过 fig 对象,我们可以对整个图形进行操作,比如设置标题、保存图形等。
  • ax 是代表子图(axes)的对象,它是实际用来绘制图形元素的部分。通过 ax 对象,我们可以在子图中添加各种可视化元素,比如线条、散点、文本等。
  • figsize=(12, 8) 这个参数指定了图形的大小,其中 (12, 8) 表示图形的宽度为 12 英寸,高度为 8 英寸。通过设置 figsize,我们可以控制生成的图形的尺寸,以便更好地展示数据或调整布局。

综合起来,这段代码的作用是创建一个新的图形和子图对象,图形的大小为宽度 12 英寸,高度 8 英寸。这样可以为后续的数据可视化操作提供一个基础框架。

3.数据准备与处理

# add ones column,添加一组全1向量
data.insert(0, 'Ones', 1)

# set X(training data) and Y(target variable)
X = data.iloc[:, 0: -1].values
Y = data.iloc[:, -1].values
theta = np.zeros(3)
# 参数矩阵初始化

cost(theta, X, Y)
# 计算初始的代价,这里的cost函数暂且看不懂没关系,下面有解释

gradient(theta, X, Y)
# 进行梯度下降求最合适的参数矩阵,这里的gradient函数暂且看不懂也没关系,下面有解释

# 以上大概就是简单逻辑回归的全部步骤,接下来就是单独对这几个函数进行解释

这段代码是用Python编写的,主要是用于设置训练数据和目标变量。让我逐句解释:

  1. X = data.iloc[:, 0:-1].values: 这行代码是从数据中选择所有行以及除最后一列之外的所有列,然后将其转换为一个NumPy数组。这里假设data是一个DataFrame,.iloc用于通过行和列的位置选择数据(除最后一列之外的所有列)。
  2. Y = data.iloc[:, -1].values: 这行代码是从数据中选择所有行以及最后一列,然后将其转换为一个NumPy数组。这里将最后一列视为目标变量(最后一列,在分类问题中通常是由0或1构成)。
  3. theta = np.zeros(3): 这行代码创建了一个由三个零组成的NumPy数组,用于存储模型的参数。在这里,假设这个数组将用作线性回归模型的参数向量,其中包括截距项和特征的系数。

这些代码片段一起构成了准备线性回归模型所需的数据和参数的过程。

注意:

当数据全部准备好后X,Y,θ的值分别是:

  • X:(100,3)-->是一个100行3列的多维数组
  • Y:(100,)--> (100,)表示一个包含100个元素的一维数组,也可以称为一维向量。(通常默认情况下会被视为行向量而不是列向量)
  • θ:(3,)-->一个含有3个元素的数组,通常默认为行向量

一定要注意的是,这里的X,Y,θ本质上都是数组而不是矩阵,但其运算和使用的时候与数组几乎并无差别,所以为了易于接受理解,本文我全采用矩阵的口吻,至于为什么是数组,因为在Python中,如果你使用的是NumPy库中的数组(numpy array),数组之间是可以进行乘法操作的。当你对两个NumPy数组进行乘法操作时,它们会执行元素级别的乘法,也就是对应位置的元素相乘。 例如,如果有两个NumPy数组 array1 和 array2,你可以通过 array1 * array2 来实现元素级别的乘法操作。这意味着它们的形状必须相容,例如两个一维数组的长度必须相同,或者对于多维数组,对应维度的大小必须一致。 总的来说,在Python中,如果你使用NumPy库中的数组,你可以对数组之间进行乘法操作。这种元素级别的乘法可以在很多数据处理和科学计算的情况下非常有用。

4.算法组成Two-Sigmoid函数(等价于线性回归中的假设函数)

定义

 Sigmoid函数是一种常用的激活函数,也称为逻辑函数,通常用于二分类问题中。别看它叫逻辑回归,其实和逻辑和回归问题没多大关系,逻辑是因为音译的原因,回归的话大概率是因为借助了回归的思想原理。

 在逻辑回归中,Sigmoid函数扮演着关键的角色。逻辑回归是一种广泛应用于分类问题的机器学习算法,它的目标是将输入特征映射到一个介于0和1之间的概率值表示某个样本属于正类的概率。Sigmoid函数的作用是将逻辑回归模型的输出转换为这种概率值。

Sigmoid函数具有S形状曲线,当z趋近于正无穷大时,Sigmoid函数的值趋近于1;当z趋近于负无穷大时,Sigmoid函数的值趋近于0。这种性质使得Sigmoid函数在将实数映射到概率值时非常有效。

v2-d2107611d56e80155da1098b9fca2346_b.jpg

具体来说,逻辑回归模型会对输入特征进行加权求和,并将结果输入到Sigmoid函数中,该函数将实数值映射到0到1之间。这样,对于给定的输入特征,逻辑回归模型会输出一个介于0和1之间的概率值,表示该样本属于正类的可能性。

通过Sigmoid函数的作用,逻辑回归模型可以进行二分类预测,将连续的实数输出转换为概率值,并根据设定的阈值来判断样本属于哪个类别。这种机制使得逻辑回归成为处理二分类问题的一种强大工具。

sigmoid.png

代码复现

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

这里的z就是θ的转置乘以特征矩阵x,也就是θ^(T)X,在这段代码中的话没有对z进行拓展和细节化处理,可以增加程序以及函数的灵活性

5.算法组成Three-代价函数

引入:既然我们知道了拟合函数,知道了我们最终的目的就是通过这个函数更好去预测一个介于0~1的概率值,那么我们就需要找到一组合适的参数来最大限度地减少其中预测值和真实值的总误差(虽然真实值不是0就是1,但是预测值不一定总是整数的1或0),此时我们就能够通过构造代价函数找出总误差值最小的那组系数。

定义以及解释:

参照前文线性回归代价函数的公式,我们发现在逻辑回归中多了取对数的操作,也就是log(θ),为什么呢?这里我只能粗略的解释,总之最终的目的是为了能够使我们最终得出来的预测值更加准确精确,如果你想搞懂的话,推荐一篇讲的还不错的文章 用人话讲明白逻辑回归Logistic regression

其次,我们发现该代价函数分成了两段,这又是为什么?

逻辑回归中的代价函数通常是由两部分组成的,这种设计有其特定的意义和作用。在逻辑回归中,常用的代价函数是交叉熵损失函数(Cross-Entropy Loss)。这个损失函数在逻辑回归中被广泛使用,因为它能很好地衡量模型输出与实际标签之间的差异。

交叉熵损失函数的两段分别是针对正类别和负类别的情况。

这种两段式的设计使得代价函数能够更好地对模型的预测进行惩罚,当模型的预测与实际标签不一致时,会产生较大的损失。通过这种设计,模型在训练过程中能够更好地学习正确的分类边界,提高分类的准确性。

总的来说,逻辑回归中代价函数分成两段的设计有助于模型更好地学习和调整参数,使得模型能够更准确地预测样本的分类情况。这种代价函数的设计有助于提高模型的性能和泛化能力(提醒一下,有些知识点或者结论不要求一定要搞懂,因为一定要搞懂的话就需要伴随着数学的推理过程,理解与明晰有些时候可能更为重要)。 cost.png

代码复现

def cost(theta, X, Y):
    first = Y * np.log(sigmoid(X@theta.T))
    second = (1 - Y) * np.log(1 - sigmoid(X@theta.T))
    return -1 * np.mean(first + second)

通过这个函数,我们就能求出对于某组参数值θ所对应的代价值,然后通过比较每组θ值的代价值大小来选出最合适,误差最小,拟合效果最好的θ参数矩阵。那么问题来了,我们怎么更新θ参数矩阵呢?便是梯度下降了。

算法组成Four-梯度下降

gradient.png

代码复现:

方法一

# 计算步长
def gradient(theta, X, Y):
    return (1/len(X) * X.T @ (sigmoid(X @ theta.T) - Y))
    
# 使用Scipy.optimize.fmin_tnc优化函数拟合最优的θ
import scipy.optimize as opt
result = opt.fmin_tnc(func=cost, x0=theta, fprime=gradient, args=(X, Y))

result     #(array([-25.16131862, 0.20623159, 0.20147149]), 36, 0)
# 这里是为了输出θ,更直观的看看
cost(result[0], X, Y)

scipy.optimize.fmin_tnc是SciPy库中的一个函数,用于执行无约束最小化(优化)问题的优化算法。TNC 代表 Truncated Newton Conjugate-Gradient,是一种基于牛顿共轭梯度方法的优化算法。这个函数的主要作用是寻找使得给定目标函数最小化的参数值。在优化过程中,它使用梯度信息来迭代地调整参数值,以便找到使目标函数最小化的最优解。

scipy.optimize.fmin_tnc函数通常需要提供以下几个参数:

  • 目标函数(通常是一个 Python 函数)
  • 初始参数值
  • 梯度函数(可选)
  • 其他优化参数,如容差值、最大迭代次数等

通过调用 scipy.optimize.fmin_tnc 函数,可以方便地在 Python 中进行数值优化,寻找目标函数的最小值。这个函数在处理大规模的无约束优化问题时非常有用,尤其是当目标函数具有复杂的形式或者无法通过解析方法求解时。

方法二:

# 传统梯度下降法
theta = np.matrix(theta)
X = np.matrix(X)
Y = np.matrix(Y)
    
parameters = int(theta.ravel().shape[1])
grad = np.zeros(parameters)
print(X.shape, theta.shape, (theta.T).shape, (X*theta.T).shape)
error = sigmoid(X * theta.T) - Y
for i in range(parameters):
   term = np.multiply(error, X[:, i])
   grad[i] = np.sum(term) / len(X)
       
return grad

让我逐句来解释一下:

  1. theta = np.matrix(theta): 将参数 theta 转换为 NumPy 矩阵。
  2. X = np.matrix(X): 将特征矩阵 X 转换为 NumPy 矩阵。
  3. Y = np.matrix(Y): 将标签矩阵 Y 转换为 NumPy 矩阵。
  4. parameters = int(theta.ravel().shape[1]): 计算参数的数量。
  5. grad = np.zeros(parameters): 创建一个全零数组,用于存储梯度。
  6. print(X.shape, theta.shape, (theta.T).shape, (X*theta.T).shape): 打印出各个矩阵的形状,用于调试和验证维度匹配。
  7. error = sigmoid(X * theta.T) - Y: 计算预测值与实际值之间的误差。
  8. for i in range(parameters):: 开始一个循环,遍历每个参数。
  9. term = np.multiply(error, X[:, i]): 计算误差与特征矩阵的乘积。
  10. grad[i] = np.sum(term) / len(X): 计算梯度的第 i 个元素。
  11. return grad: 返回计算得到的梯度数组。

这段代码的目的是计算逻辑回归模型的梯度,用于更新参数以最小化损失函数。

方法三:

# 使用Scipy.optimize.minimize拟合最优的theta
res = opt.minimize(fun=cost, x0=np.array(theta), args=(X, np.array(Y)), method='Newton-CG', jac=gradient)
res

#结果: fun: 0.20349770451259855 
#    jac: array([1.62947970e-05, 1.11339134e-03, 1.07609314e-03]) 
#    message: 'Optimization terminated successfully.' 
#    nfev: 71 
#    nhev: 0 
#    nit: 28 
#    njev: 242 
#    status: 0 
#    success: True 
#    x: array([-25.16576744, 0.20626712, 0.20150754])   (这里就是我们想要的theta矩阵)

cost(res.x, X, Y)

scipy.optimize.minimize 是 SciPy 库中一个非常强大的函数,用于解决多种数学优化问题,包括最小化或最大化标量函数、约束优化、全局优化等。这个函数提供了一个统一的接口,可以通过指定不同的方法和选项来解决各种优化问题。

使用 scipy.optimize.minimize 可以通过指定目标函数、初始猜测值、优化方法、约束条件等参数来寻找目标函数的最小值或最大值。它支持多种优化算法,如共轭梯度法、BFGS 算法、L-BFGS-B 算法等,以及处理约束优化问题的方法。

一般来说,scipy.optimize.minimize 函数需要提供以下几个参数:

  • 目标函数
  • 初始参数值
  • 优化方法(如 BFGS、Nelder-Mead、COBYLA 等)
  • 约束条件(可选)
  • 其他优化参数,如容差值、最大迭代次数等

通过调用 scipy.optimize.minimize 函数,可以在 Python 中方便地进行各种数学优化问题的求解,无论是简单的无约束优化问题还是复杂的带约束优化问题。这个函数在科学计算、机器学习、工程优化等领域都有广泛的应用。

方法一和方法三是借助优化函数库,方法二是传统的梯度下降

好了,到这里我们应该就能得到一个拟合效果相对较好的假设函数了,接下来就是使用该函数进行预测分析并且计算校验一下该模型的准确率

算法组成Five-预测分析

因为逻辑回归不同于线性回归对连续值的预测,所计算出来的值就是结果值,逻辑回归主要还是分类问题,需要将计算所得的结果进行适当处理以求得到最终的结果

模型使用与预测:

def predict(theta, X):
    probability = sigmoid(X @ theta.T)
    return [1 if x >= 0.5 else 0 for x in probability]

X为特征矩阵,theta为之前求出来的较合适的θ参数组,这里的输出值也就是每一个样本的分类结果(对应0或1两种)

算法组成Six-模型准确率

那么我们既然利用模型预测出了对应结果,不妨看看我们的模型的预测结果的准确率是多少(这里说一下,之前的预测不是真的"预测",需要我们同时也拿到Y值真实结果,只有这样子才能判断我们训练出来的模型准不准确)

模型准确率计算:

theta_min = np.matrix(result[0])
predictions = predict(theta_min, X)
correct = [1 if a^b == 0 else 0 for (a,b) in zip(predictions, Y)]
accuracy = (sum(correct) / len(correct))
print('accuracy = {0:.0f}%'.format(accuracy*100))

当你执行这段代码时,它做了以下几件事情:

  1. theta_min = np.matrix(result[0]):这行代码将优化结果 result 中的第一个元素(通常是优化得到的参数向量)转换为 NumPy 矩阵,并赋值给 theta_min。这个参数向量通常代表着优化后的最优参数值(这里的result[0]对应的上面梯度下降的第一个方法所求出来的result)。
  2. predictions = predict(theta_min, X):这行代码调用一个名为 predict 的函数,传入 theta_min 和输入数据 X,然后生成预测值 predictions。这个步骤用最优参数对输入数据进行预测。
  3. correct = [1 if a^b == 0 else 0 for (a,b) in zip(predictions, Y)]:这行代码通过遍历 predictions 和实际标签 Y,创建一个列表 correct。在这个列表中,如果预测值和实际标签相同,则对应位置为1,否则为0。这样可以判断每个样本的预测是否正确。
  4. accuracy = (sum(correct) / len(correct)):这行代码计算了正确预测的样本数量,并除以总样本数量,得到准确率 accuracy。这是通过将 correct 列表中的1的总和除以总样本数来计算的。
  5. print('accuracy = {0:.0f}%'.format(accuracy*100)):最后一行代码将准确率以百分比的形式打印出来。通过格式化字符串,将准确率乘以100后保留整数部分,并输出到控制台,显示最终的准确率结果。

这段代码主要用于评估一个模型的准确率,通过比较模型预测结果和实际标签,然后计算出准确率并输出。

tips:

(1)

zip(predictions, Y) 是 Python 中的一个内置函数,它接受多个可迭代对象作为参数,并返回一个将这些可迭代对象中对应位置的元素打包成元组的迭代器。

在这种情况下,zip(predictions, Y) 将两个可迭代对象 predictions 和 Y 中对应位置的元素一一配对,形成一个迭代器。例如,如果 predictions 是 [0, 1, 0]Y 是 [1, 1, 0],那么 zip(predictions, Y) 将生成一个迭代器,其中第一个元素是 (0, 1),第二个元素是 (1, 1),第三个元素是 (0, 0)

在上面提到的代码中,zip(predictions, Y) 的作用是将模型的预测值 predictions 和实际标签 Y 对应位置的元素配对,这样可以方便地同时遍历这两个列表,进行比较和操作。

(2)

这行代码中的 1 if a^b == 0 else 0 是一个条件表达式,也称为三元运算符。它的作用是根据条件 a^b == 0 的真假来返回不同的值。在这里,a^b 表示 a 和 b 的异或运算,即按位异或操作。(在很多编程语言中,包括Python,^ 符号通常用来表示按位异或运算,而不是乘方运算。在Python中,^ 运算符被用于执行位运算,对两个数的二进制位进行异或操作。如果你想进行乘方运算,应该使用 ** 运算符,而不是 ^。所以在这种情况下,a^b 表示的是 a 和 b 的按位异或运算,而不是乘方运算。如果 a 和 b 相等,那么 a 和 b 的每一位都是相同的,因此它们的异或操作结果为 0。换句话说,如果 a 和 b 相等,那么 a^b 的值将为 0。而如果 a 和 b 不相等,那么它们的二进制表示中至少有一位是不同的。在这种情况下,进行异或操作会使得对应的位为 1。所以,如果 a 和 b 不相等,a^b 的值将不为 0。)

如果 a^b 的结果等于 0,那么条件 a^b == 0 就为真,此时该条件表达式会返回 1;否则,即 a^b != 0,条件表达式返回 0。

因此,整个列表推导式 correct = [1 if a^b == 0 else 0 for (a,b) in zip(predictions, Y)] 的作用是根据 predictions 和 Y 中对应位置的元素进行异或运算,如果结果为 0,则对应位置的 correct 列表中的元素为 1,否则为 0。这样可以根据模型的预测值和实际标签的异或结果来判断预测是否正确,并将结果存储在 correct 列表中。

拓展

1.那么以上就是基础的模型框架了,如果你想对该模型了解的更深一点,那么还有查准率,召回率,调和平均数这几个数值可以调

# support标签中出现的次数
# precision查准率,recall召回率,f1-score调和平均数
from sklearn.metrics import classification_report
print(classification_report(Y, predictions))

关于这里的这三个率是什么意思我后面会讲。

2.决策边界:

coef = -res.x / res.x[2]
x = np.arange(30, 100, 0.5)
y = coef[0] + coef[1] * x

fig, ax = plt.subplots(figsize=(12, 8))
ax.scatter(positive['Exam1'], positive['Exam2'], s=50, c='b', marker='o', label='Admitted')
ax.scatter(negative['Exam1'], negative['Exam2'], s=50, c='r', marker='x', label='Not Admitted')
ax.plot(x, y, label='Decision Boundary', c='grey')
ax.legend()
ax.set_xlabel('Exam1 Score')
ax.set_ylabel('Exam2 Score')
plt.show()

屏幕截图 2024-03-28 170714.png

我们就是根据该样本点落在这条拟合线的左右两边来判断出最终结果的

3.特征映射

def feature_mapping(x, y, power, as_ndarray=False):
    data = {'f{0}{1}'.format(i-p, p): np.power(x, i-p) * np.power(y, p)
                for i in range(0, power+1)
                for p in range(0, i+1)
           }
    if as_ndarray:
        return pd.DataFrame(data).values
    else:
        return pd.DataFrame(data)

这段代码是一个Python函数,名为feature_mapping,用于进行特征映射。让我逐句解释它:

  1. def feature_mapping(x, y, power, as_ndarray=False):

    • 这是定义函数的行,函数名为feature_mapping,接受四个参数:x,y,power,以及一个可选的参数as_ndarray,用于指示是否将结果转换为NumPy数组。
  2. data = {'f{0}{1}'.format(i-p, p): np.power(x, i-p) * np.power(y, p) for i in range(0, power+1) for p in range(0, i+1) }

    • 这一行是一个字典推导式,用于生成特征映射后的数据。它会生成一个字典,其中键为'f{0}{1}'.format(i-p, p),值为x的(i-p)次方乘以y的p次方,i和p的取值范围由range函数确定。
  3. if as_ndarray:

    • 这行代码检查是否需要将结果转换为NumPy数组。
  4. return pd.DataFrame(data).values

    • 如果as_ndarray为True,函数将返回data字典转换为DataFrame后的值数组。
  5. else:

    • 如果as_ndarray为False,即默认情况下,函数将返回data字典转换为DataFrame后的DataFrame对象。

这段代码主要完成了一个特征映射的过程,可以将输入的x和y映射到更高维度的特征空间中。希望这个解释能够帮助你理解这段代码的功能。如果有任何其他问题或需要更多解释,请随时告诉我。

当考虑一个简单的特征映射示例时,我们可以想象一个二维数据集,其中包含一些数据点,它们无法通过一条直线进行良好的分类。这时,我们可以利用特征映射将这些数据点映射到一个更高维的空间,以便更好地进行分类。

假设我们有一个二维数据集,包括两个特征:x1和x2。在二维空间中,这些数据点可能无法通过一条直线很好地分开两个类别。但是,如果我们将这些二维数据点通过特征映射映射到三维空间,我们可以添加一个新的特征
x3​=x12​+x22​
. 这样,我们就将二维数据映射到了一个更高维的三维空间。

在这个新的三维空间中,数据点可能会呈现出线性可分的特性,即存在一个平面可以很好地将两个类别分开。通过这种方式,我们利用特征映射将原始数据转换为一个更适合分类的形式。

这个简单的例子展示了特征映射如何可以帮助我们处理原始数据,并使得数据更容易分类。

简单例子:

想象一下你正在准备烤蛋糕。在配方中,你有两种原料:面粉和砂糖。现在,你想要根据这两种原料的不同比例来判断蛋糕的口感是甜蜜还是偏向面粉味。

在原始的二维空间中,你只能看到面粉和砂糖的比例,无法准确判断口感。但是,通过特征映射,你可以引入一个新的特征,比如面粉的平方加上砂糖的平方,这样就将二维空间映射到了三维空间。

在这个新的三维空间中,你可以更好地区分不同口感的蛋糕。比如,如果面粉和砂糖的平方和较大,可能代表口感更甜蜜;反之,如果平方和较小,可能代表口感更偏向面粉味。

通过这种特征映射,你在更高维度的空间中可以更好地理解和分类不同口感的蛋糕,就像在二维空间中无法做到的那样。

好了,现在我们对大致模型已经有了基本的概念,但大家有没有考虑过这样一个问题,当我们经过过多次数的迭代,使得到的θ参数矩阵越来越准确,拟合效果越来越好,输出值越来越接近真实值,那么当最终拟合的数据与真实值完全相等的时候,拟合的数据确实是100%准确了,但是这样不但会造成拟合的函数扭曲,形状怪异,还会大大减少拟合函数的预测作用和泛化能力,这就是所谓的过拟合问题了。同样的,欠拟合问题也是如此,都是因为一个合适的迭代次数的问题,那么我们在遇到如此问题的时候该怎么处理呢?那就是,正则化!

算法惩戒制度-正则化

1.代价函数正则化

regularized_cost.png

在这里我们对代价函数加上正则化 代码复现:

# 特征映射与数据准备
theta = np.zeros(data.shape[1])
X = feature_mapping(x1, x2, power=6, as_ndarray=True)
X.shape, Y.shape, theta.shape

# Sigmoid激活函数
def sigmoid(z):
    return 1 / (1 + np.exp(-z))
    
# 基础代价函数
def cost(theta, X, Y):
    first = Y * np.log(sigmoid(X@theta.T))
    second = (1 - Y) * np.log(1 - sigmoid(X@theta.T))
    return -1 * np.mean(first + second)
    
# 正则化代价函数
def regularized_cost(theta, X, Y, l=1):
    theta_1n = theta[1:]
    regularized_term = l / (2 * len(X)) * np.power(theta_1n, 2).sum()
    return cost(theta, X, Y) + regularized_term
    
# 运行正则化代价函数
cost(theta, X, Y)
regularized_cost(theta, X, Y, l=1)

regularized_term = l / (2 * len(X)) * np.power(theta_1n, 2).sum()代码解释(其他部分请看前面):

  • regularized_term: 这是要计算的正则化项的结果,即正则化项的值。
  • l: 这是正则化参数(lambda),用于控制正则化的强度。
  • len(X): 这里是X的长度,表示训练样本的数量。
  • np.power(theta_1n, 2): 这部分计算了参数向量theta的每一个元素的平方。
  • .sum(): 这是对上述平方值进行求和操作。

综合起来,这行代码的作用是:

  1. 首先,计算了参数向量theta的第一个元素的平方。
  2. 然后,将这个平方值求和。
  3. 接着,将这个求和值乘以正则化参数lambda除以2倍训练样本数量len(X)。

这个计算过程是正则化项的一部分,用于在训练模型时惩罚模型复杂度,防止过拟合。正则化项通常将模型参数的平方和加入到损失函数中,以确保模型不会过度依赖于任何一个特征,从而提高模型的泛化能力

2.正则化梯度

regularized_gradient.png

代码复现:

# 基础梯度下降
def gradient(theta, X, Y):
    return (1/len(X) * X.T @ (sigmoid(X @ theta.T) - Y))
    
# 正则化梯度下降
def regularized_gradient(theta, X, Y, l=1):
    theta_1n = theta[1:]
    regularized_theta = l / len(X) * theta_1n
#     regularized_theta[0] = 0
    regularized_term = np.concatenate([np.array([0]), regularized_theta])
    
    return  gradient(theta, X, Y) + regularized_term
#     return  gradient(theta, X, Y) + regularized_theta

gradient(theta, X, Y)

regularized_gradient(theta, X, Y)

regularized_term = np.concatenate([np.array([0]), regularized_theta])代码解释:

这段代码中,regularized_term = np.concatenate([np.array([0]), regularized_theta]) 的目的是创建一个新的参数向量 regularized_term,其中包含了一个额外的零值作为第一个元素,然后将 regularized_theta 中的所有元素依次添加到新向量中。

让我来详细解释一下这行代码的作用:

  • np.array([0]): 这部分代码创建了一个包含单个零值的NumPy数组。这个零值将作为新参数向量 regularized_term 的第一个元素。
  • regularized_theta: 这是经过正则化处理后的参数向量,其中包含了要添加到新向量中的所有元素。
  • np.concatenate(): 这是NumPy函数,用于连接(concatenate)数组。在这里,它将上述两个数组连接起来,将第一个数组 [0] 作为第一个元素,然后将 regularized_theta 中的所有元素依次添加到新向量中。

因此,整体来说,这行代码的作用是创建一个新的参数向量 regularized_term,其中包含了一个额外的零值作为第一个元素,然后将 regularized_theta 中的所有参数依次添加到新向量中。这种操作通常用于在某些机器学习算法中对参数向量进行处理或扩展。

通过这两个梯度下降,只要我们选择较合适的λ值,就能起到对欠拟合和过拟合很好的约束作用!

以上就是第二部分对于逻辑回归算法的全部了,如果大家有疑惑或者文章有问题,欢迎交流