逻辑回归算法 0基础小白也能懂(附代码)

逻辑回归算法 0基础小白也能懂(附代码)

原文链接

啥是逻辑回归算法

逻辑回归(Logistic Regression)是一种广泛用于分类任务的统计模型,特别适用于二元分类问题。尽管名称中带有“回归”,但逻辑回归主要用于分类。逻辑回归算法包含以下几个关键部分:线性回归与分类,Sigmoid 函数与决策边界,梯度下降与优化,正则化与缓解过拟合

线性回归与分类

分类问题和回归问题有一定的相似性,都是通过对数据集的学习来对未知结果进行预测,区别在于输出值不同。

分类问题的输出值是离散值(如垃圾邮件和正常邮件)。
回归问题的输出值是连续值(例如房子的价格)。

既然分类问题和回归问题有一定的相似性,那么我们能不能在回归的基础上进行分类呢?

可以使用『线性回归+阈值』解决分类问题,也就是根据回归的函数按照一个阈值来一分为二,但是单纯地通过将线性拟合的输出值与某一个阈值进行比较,这种方法用于分类非常不稳定呢。

但是当我们单纯地使用线性回归来解决分类问题时,输出是一个未限定范围的连续值。这样做存在几个问题:

1.范围不固定:线性回归的输出可能是任何实数(比如 -∞ 到 +∞),这使得我们无法确定一个合理的阈值来判断输出 属于哪个类别。例如,可能得到一个极大的正值或负值,难以直接映射为0或1。

2.阈值选择困难:为了将连续值映射到离散类别(例如 0 和 1),我们通常需要一个判定阈值。如果线性回归的输出值不在一个固定的范围内,选择一个合适的阈值会变得非常困难,尤其是在处理极端值时。

我们是否可以把这个结果映射到一个固定大小的区间内(比如\((0,1)\)),进而判断。

Sigmoid 函数与决策边界

当然可以,这就是逻辑回归做的事情,而其中用于对连续值压缩变换的函数叫做 Sigmoid 函数(也称 Logistic 函数,S函数)。

\(S(x)=\frac{1}{1+e^{-x}}\)输出在\((0,1)\)之间。

上面见到了 Sigmoid 函数,下面我们来讲讲它和线性拟合的结合,如何能够完成分类问题,并且得到清晰可解释的分类器判定「决策边界」。决策边界就是分类器对于样本进行区分的边界。

那么,逻辑回归是怎么得到决策边界的,它与 Sigmoid 函数又有什么关系呢?文中讲到了两类决策边界,线性和非线性。

在这两张图中,右边的是我们的S函数,它将通过函数计算后的结果映射到了\((0,1)\)区间,那比如这里要做的就是二分类,就找0.5的那一条线,也就是在原函数中能将数据分成两组的函数,改变\(\theta_0,\theta_1,\theta_2\)之类的参数,比如线性参数的例子里就是-3,1,1,以这几个参数做出来的函数就是决策边界,0.5则是判断边界。

梯度下降与优化

损失函数

前一部分的例子中,我们手动取了一些参数\(\theta_0,\theta_1,\theta_2\)的取值,最后得到了决策边界。但大家显然可以看到,取不同的参数时,可以得到不同的决策边界。

哪一条决策边界是最好的呢?我们需要定义一个能量化衡量模型好坏的函数——损失函数(有时候也叫做「目标函数」或者「代价函数」)。我们的目标是使得损失函数最小化。总不能一直手动去取参数。

我们如何衡量预测值和标准答案之间的差异呢?最简单直接的方式是数学中的均方误差\(MSE=\frac{1}{m}\sum_{i=1}^{m} (f(x_i)-y_i)^2\)

均方误差损失(MSE)在回归问题损失定义与优化中广泛应用,但是在逻辑回归问题中不太适用。 因为Sigmoid 函数的变换使得我们最终得到损失函数曲线如下图所示,是非常不光滑凹凸不平的.

我们希望损失函数如下的凸函数。凸优化问题中,局部最优解同时也是全局最优解

逻辑回归模型场景下,我们会改用对数损失函数(二元交叉熵损失),这个损失函数同样能很好地衡量参数好坏,又能保证凸函数的特性。对数损失函数的公式如下:

\(J(\theta)=-\frac{1}{m} [\sum_{i=1}^{m}y^{(i)}\log{h_{\theta}(x^{(i)})-(1-y^{(i)})\log{(1-h_{\theta}(x^{(i)})})}]\)

其中\(h_{\theta}(x^{(i)})\)表示的可以说是根据这个h函数预测的样本值,\(y^{(i)}\)表示样本取值,在其为正样本时取值为1,负样本时取值为0,正负样本就是我们之前说的分的那两类,比如正常邮件是正样本,垃圾邮件是负样本,我们分这两种情况来看看:

\(y^{(i)}=0\):样本为负样本时,若\(h_{\theta}(x^{(i)})\)接近1(预测值为正样本),那么\(-\log{(1-h_\theta (x))}\)值就很大,也就是对应的惩罚也越大
\(y^{(i)}=1\):样本为正样本时,若\(h_{\theta}(x^{(i)})\)接近0(预测值为负样本),那么\(-\log{(h_\theta (x))}\)值就很大,也就是对应的惩罚也越大
通过惩罚大小就可以判断出参数好坏了。

梯度下降

损失函数可以用于衡量模型参数好坏,但我们还需要一些优化方法找到最佳的参数(使得当前的损失函数值最小)。最常见的算法之一是「梯度下降法」,逐步迭代减小损失函数(在凸函数场景下非常容易使用)。如同下山,找准方向(斜率),每次迈进一小步,直至山底。

梯度下降(Gradient Descent)法,是一个一阶最优化算法,通常也称为最速下降法。要使用梯度下降法找到一个函数的局部极小值,必须向函数上当前点对应梯度(或者是近似梯度)的反方向的规定步长距离点进行迭代搜索。

上图中,\(\alpha\)称为学习率(learning rate),直观的意义是,在函数向极小值方向前进时每步所走的步长。太大一般会错过极小值,太小会导致迭代次数过多。

进一步学习梯度算法

正则化与缓解过拟合

曲线3就是过拟合,学的太死板了也就是,那咋办呢?

过拟合的一种处理方式是正则化,我们通过对损失函数添加正则化项,可以约束参数的搜索空间,从而保证拟合的决策边界并不会抖动非常厉害。如下图为对数损失函数中加入正则化项(这里是一个L2正则化项)

在损失函数后面的那一项就是L2正则化项,L2 正则化可以看作是对参数空间内欧氏距离的惩罚。其几何效果是缩小所有参数,使它们更加接近原点,从而降低模型复杂度,会惩罚较大的参数值,但不会直接将它们缩减为零。这种“平滑”的惩罚方式促使模型将所有特征的权重均衡分配,从而减少过拟合。

我们依然可以采用梯度下降对加正则化项的损失函数进行优化。

代码实现

下面使用了scikit-learn提供的乳腺癌数据集作为示例数据集

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.datasets import load_breast_cancer

# 加载数据集
data = load_breast_cancer()
X = data.data
y = data.target

# 拆分数据集为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 对特征进行标准化处理
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# 初始化并训练逻辑回归模型
model = LogisticRegression()
model.fit(X_train, y_train)

# 使用模型进行预测
y_pred = model.predict(X_test)

# 计算并打印准确率
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.2f}")

# 打印混淆矩阵
conf_matrix = confusion_matrix(y_test, y_pred)
print("Confusion Matrix:")
print(conf_matrix)

# 打印分类报告
class_report = classification_report(y_test, y_pred)
print("Classification Report:")
print(class_report)

结果如下

Accuracy: 0.98
Confusion Matrix:
[[ 62   1]
 [  2 106]]
Classification Report:
              precision    recall  f1-score   support

           0       0.97      0.98      0.98        63
           1       0.99      0.98      0.99       108

    accuracy                           0.98       171
   macro avg       0.98      0.98      0.98       171
weighted avg       0.98      0.98      0.98       171

我们再用python构建一个原生的算法模型,用到了交叉损失函数和正则化

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler


# Sigmoid 函数,将线性组合映射到 [0, 1] 区间,用于预测概率
def sigmoid(z):
    return 1 / (1 + np.exp(-z))


# 初始化模型参数,权重设为0,偏置设为0
def initialize_params(dim):
    weights = np.zeros((dim, 1))  # 权重向量,初始为全零
    bias = 0  # 偏置,初始为零
    return weights, bias


# 前向传播与反向传播,计算预测值、损失函数值以及梯度
def propagate(weights, bias, X, Y, regularization_strength):
    m = X.shape[1]  # 样本数量

    # 前向传播:计算预测值
    A = sigmoid(np.dot(weights.T, X) + bias)  # 线性组合后通过Sigmoid函数得到预测值

    # 计算对数损失函数(带正则化项)
    cost = -1 / m * np.sum(Y * np.log(A) + (1 - Y) * np.log(1 - A)) + (regularization_strength / (2 * m)) * np.sum(
        np.square(weights))

    # 反向传播:计算权重和偏置的梯度(包括正则化项)
    dw = 1 / m * np.dot(X, (A - Y).T) + (regularization_strength / m) * weights  # 计算权重的梯度(带正则化项)
    db = 1 / m * np.sum(A - Y)  # 计算偏置的梯度(偏置不正则化)

    gradients = {"dw": dw, "db": db}  # 将梯度存储在字典中

    return gradients, cost


# 梯度下降优化,更新权重和偏置
def optimize(weights, bias, X, Y, num_iterations, learning_rate, regularization_strength):
    costs = []  # 记录每100次迭代的损失值

    for i in range(num_iterations):
        # 计算梯度和损失
        gradients, cost = propagate(weights, bias, X, Y, regularization_strength)

        # 获取梯度值
        dw = gradients["dw"]
        db = gradients["db"]

        # 更新权重和偏置
        weights -= learning_rate * dw  # 权重更新
        bias -= learning_rate * db  # 偏置更新

        # 每100次迭代打印一次损失
        if i % 100 == 0:
            costs.append(cost)
            print(f"Cost after iteration {i}: {cost}")

    params = {"weights": weights, "bias": bias}  # 存储最终的权重和偏置
    gradients = {"dw": dw, "db": db}  # 存储最终的梯度

    return params, gradients, costs


# 预测函数,将训练好的模型应用于新的数据
def predict(weights, bias, X):
    m = X.shape[1]  # 样本数量
    Y_prediction = np.zeros((1, m))  # 初始化预测结果矩阵
    A = sigmoid(np.dot(weights.T, X) + bias)  # 计算预测的概率值

    for i in range(A.shape[1]):
        # 将概率值转为0或1的分类结果
        Y_prediction[0, i] = 1 if A[0, i] > 0.5 else 0

    return Y_prediction


# 将以上各部分组合成一个完整的逻辑回归模型
def model(X_train, Y_train, num_iterations=2000, learning_rate=0.5, regularization_strength=0.1):
    dim = X_train.shape[0]  # 特征数量
    weights, bias = initialize_params(dim)  # 初始化权重和偏置

    # 通过梯度下降优化权重和偏置
    params, gradients, costs = optimize(weights, bias, X_train, Y_train, num_iterations, learning_rate,
                                        regularization_strength)

    # 获取优化后的权重和偏置
    weights = params["weights"]
    bias = params["bias"]

    return weights, bias, costs


# 加载乳腺癌数据集
data = load_breast_cancer()
X = data.data
y = data.target

# 将 y 进行 reshape 使其成为二维数组
y = y.reshape(1, y.shape[0])

# 拆分数据集为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y.T, test_size=0.2, random_state=42)

# 转置 X_train 和 X_test 以符合我们实现的逻辑回归模型的输入要求
X_train = X_train.T
X_test = X_test.T
y_train = y_train.T
y_test = y_test.T

# 对特征进行标准化处理
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train.T).T
X_test = scaler.transform(X_test.T).T

# 训练逻辑回归模型(带正则化)
weights, bias, costs = model(X_train, y_train, num_iterations=2000, learning_rate=0.01, regularization_strength=0.1)

# 使用训练好的模型进行预测
Y_prediction_train = predict(weights, bias, X_train)
Y_prediction_test = predict(weights, bias, X_test)

# 打印训练集和测试集上的准确率
print(f"Train accuracy: {100 - np.mean(np.abs(Y_prediction_train - y_train)) * 100}%")
print(f"Test accuracy: {100 - np.mean(np.abs(Y_prediction_test - y_test)) * 100}%")

下面是结果

Cost after iteration 0: 0.6931471805599453
Cost after iteration 100: 0.2543244526519929
Cost after iteration 200: 0.1918342038605185
Cost after iteration 300: 0.16348680375028227
Cost after iteration 400: 0.14665006862221902
Cost after iteration 500: 0.13525053919817978
Cost after iteration 600: 0.12689843726947214
Cost after iteration 700: 0.12044835639062768
Cost after iteration 800: 0.11527696015953119
Cost after iteration 900: 0.1110131308144327
Cost after iteration 1000: 0.10742059509404249
Cost after iteration 1100: 0.1043409206676583
Cost after iteration 1200: 0.10166339647566097
Cost after iteration 1300: 0.0993080193536485
Cost after iteration 1400: 0.09721534841672455
Cost after iteration 1500: 0.0953401769323753
Cost after iteration 1600: 0.09364743315495462
Cost after iteration 1700: 0.09210943796116201
Cost after iteration 1800: 0.09070401842998121
Cost after iteration 1900: 0.08941317838842508
Train accuracy: 98.24175824175825%
Test accuracy: 99.12280701754386%
posted @ 2024-08-27 15:16  Mephostopheles  阅读(144)  评论(0编辑  收藏  举报