Press "Enter" to skip to content

使用NumPy从零开始的线性回归

使用NumPy从零开始的线性回归 四海 第1张

 

动机

 

线性回归是机器学习中最基本的工具之一。它用于找到最适合我们数据的一条直线。尽管它只能处理简单的直线模式,但理解背后的数学可以帮助我们理解梯度下降和损失最小化方法。这对于所有机器学习和深度学习任务中使用的更复杂模型非常重要。

在本文中,我们将亲自动手使用NumPy从头开始构建线性回归。我们将从基础开始,而不是使用Scikit-Learn提供的抽象实现。

 

数据集

 

我们使用Scikit-Learn方法生成一个虚拟数据集。目前我们只使用一个变量,但实现将是通用的,可以训练任意数量的特征。

Scikit-Learn提供的make_regression方法生成具有添加高斯噪声的随机线性回归数据集。

X, y = datasets.make_regression(
        n_samples=500, n_features=1, noise=15, random_state=4)

 

我们生成了500个随机值,每个值只有一个特征。因此,X的形状为(500, 1),每个独立的X值都对应一个y值。因此,y的形状也为(500, )。

可视化后,数据集如下所示:

  使用NumPy从零开始的线性回归 四海 第2张  

我们的目标是找到一条最佳拟合线,使其通过数据中心,最小化预测值和原始y值之间的平均差异。

 

直觉

 

线性直线的一般方程为:

y = m*X + b

X是数值型的单值。这里的m和b表示斜率和y截距(或偏置)。它们是未知的,这些值的变化可以生成不同的直线。在机器学习中,X取决于数据,y值也是如此。我们只能控制m和b,它们作为我们的模型参数。我们的目标是找到这两个参数的最优值,生成最小化预测和实际y值之间差异的直线。

这个概念可以扩展到X是多维的情况。在这种情况下,m的数量将等于数据中的维数。例如,如果我们的数据有三个不同的特征,我们将有三个不同的m值,称为权重。

方程现在变为:

y = w1*X1 + w2*X2 + w3*X3 + b

这可以扩展到任意数量的特征。

但是我们如何知道偏置和权重的最优值呢?实际上我们不知道。但是我们可以使用梯度下降算法来迭代地找出它们。我们从随机值开始,经过多次微小的调整,直到接近最优值。

首先,让我们初始化线性回归模型,稍后我们将详细介绍优化过程。

 

初始化线性回归类

 

import numpy as np


class LinearRegression:
    def __init__(self, lr: int = 0.01, n_iters: int = 1000) -> None:
        self.lr = lr
        self.n_iters = n_iters
        self.weights = None
        self.bias = None

 

我们使用学习率和迭代次数作为超参数,稍后将对其进行解释。权重和偏置设置为None,因为权重参数的数量取决于数据中的输入特征。我们目前还没有访问数据,所以暂时将它们初始化为None。

 

拟合方法

 

在拟合方法中,我们提供了数据及其相关值。现在我们可以使用这些数据来初始化我们的权重,然后训练模型以找到最优权重。

def fit(self, X, y):
        num_samples, num_features = X.shape     # X的形状为[N, f]
        self.weights = np.random.rand(num_features)  # W的形状为[f, 1]
        self.bias = 0

 

独立特征X将是一个形状为(num_samples, num_features)的NumPy数组。在我们的例子中,X的形状是(500, 1)。我们的数据中的每一行都有一个相应的目标值,所以y的形状也是(500,)或(num_samples)。

我们提取这个并根据输入特征的数量随机初始化权重。所以现在我们的权重也是一个形状为(num_features, )的NumPy数组。偏差是一个初始化为零的单个值。

 

预测Y值

 

我们使用上面讨论的线性方程来计算预测的y值。然而,我们可以采用向量化的方法进行更快的计算,而不是迭代地求和所有值。考虑到权重和X值都是NumPy数组,我们可以使用矩阵乘法来进行预测。

X的形状为(num_samples, num_features),权重的形状为(num_features, )。我们希望预测的形状为(num_samples, ),与原始的y值相匹配。因此,我们可以将X与权重相乘,即(num_samples, num_features) x (num_features, ),以获得形状为(num_samples, )的预测值。

偏差值在每个预测值的末尾添加。这可以简单地在一行中实现。

# y_pred的形状应该是N, 1
y_pred = np.dot(X, self.weights) + self.bias

 

然而,这些预测是否正确?显然不是。我们使用随机初始化的权重和偏差值,所以预测也是随机的。

我们如何获得最优值?梯度下降。

 

损失函数和梯度下降

 

现在我们既有预测值又有目标y值,我们可以找到两者之间的差异。均方误差(MSE)用于比较实数。方程如下:

  使用NumPy从零开始的线性回归 四海 第3张  

我们只关心值之间的绝对差异。高于原始值的预测和低于原始值的预测一样糟糕。所以我们将目标值和预测之间的差异平方,将负差异转换为正差异。此外,这惩罚了目标和预测之间的较大差异,因为较大的差异平方将更多地贡献给最终损失。为了使我们的预测尽可能接近原始目标,我们现在试图最小化这个函数。当梯度为零时,损失函数将达到最小值。由于只能优化权重和偏差值,我们对MSE函数相对于权重和偏差值进行偏导数。

  使用NumPy从零开始的线性回归 四海 第4张  

然后,我们根据梯度值优化权重。

  使用NumPy从零开始的线性回归 四海 第5张  

我们对每个权重值进行梯度,并将它们移动到梯度的相反方向。这将损失推向最小值。根据图像,梯度是正的,所以我们减小权重。这将使J(W)或损失向最小值推进。因此,优化方程如下所示:

  使用NumPy从零开始的线性回归 四海 第6张  

学习率(或alpha)控制图像中显示的增量步骤。我们只对值进行小的改变,以稳定地向最小值移动。

 

实现

 

如果我们使用基本的代数运算简化导数方程,这将非常简单实现。

  使用NumPy从零开始的线性回归 四海 第7张  

对于导数,我们使用两行代码来实现:

# X -> [ N, f ]
# y_pred -> [ N ]
# dw -> [ f ]
dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
db = (1 / num_samples) * np.sum(y_pred - y)

 

dw的形状再次为(num_features, ),因此我们为每个权重都有一个单独的导数值。我们分别优化它们。db有一个单独的值。

现在,为了优化这些值,我们使用基本的减法将值移动到梯度的相反方向。

self.weights = self.weights - self.lr * dw
self.bias = self.bias - self.lr * db

 

同样,这只是一个单步操作。我们只对随机初始化的值进行了一个小的改变。我们现在重复执行相同的步骤,以便收敛到一个最小值。

完整的循环如下所示:

for i in range(self.n_iters):

            # y_pred的形状应该是N, 1
            y_pred = np.dot(X, self.weights) + self.bias

            # X -> [N,f]
            # y_pred -> [N]
            # dw -> [f]
            dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
            db = (1 / num_samples) * np.sum(y_pred - y)

            self.weights = self.weights - self.lr * dw
            self.bias = self.bias - self.lr * db

 

预测

 

我们的预测方式与训练时相同。然而,现在我们拥有了最优的权重和偏置值。预测值现在应该接近原始值。

def predict(self, X):
        return np.dot(X, self.weights) + self.bias

 

结果

 

使用随机初始化的权重和偏置,我们的预测结果如下:

  使用NumPy从零开始的线性回归 四海 第8张 图片作者:Muhammad Arham   权重和偏置非常接近0,因此我们得到了一条水平线。经过1000次迭代训练模型后,我们得到了以下结果:

  使用NumPy从零开始的线性回归 四海 第9张 图片作者:Muhammad Arham  

预测线通过我们的数据中心,似乎是可能的最佳拟合线。

 

结论

 

您已经从头实现了线性回归。完整的代码也可以在GitHub上找到。

import numpy as np


class LinearRegression:
    def __init__(self, lr: int = 0.01, n_iters: int = 1000) -> None:
        self.lr = lr
        self.n_iters = n_iters
        self.weights = None
        self.bias = None

    def fit(self, X, y):
        num_samples, num_features = X.shape     # X形状[N, f]
        self.weights = np.random.rand(num_features)  # W形状[f, 1]
        self.bias = 0

        for i in range(self.n_iters):

            # y_pred的形状应该是N, 1
            y_pred = np.dot(X, self.weights) + self.bias

            # X -> [N,f]
            # y_pred -> [N]
            # dw -> [f]
            dw = (1 / num_samples) * np.dot(X.T, y_pred - y)
            db = (1 / num_samples) * np.sum(y_pred - y)

            self.weights = self.weights - self.lr * dw
            self.bias = self.bias - self.lr * db

        return self

    def predict(self, X):
        return np.dot(X, self.weights) + self.bias

    Muhammad Arham 是一名在计算机视觉和自然语言处理领域工作的深度学习工程师。他曾在Vyro.AI工作,负责部署和优化多个生成型AI应用,这些应用在全球排行榜上名列前茅。他对于构建和优化智能系统的机器学习模型感兴趣,并且坚信不断改进。

Leave a Reply

Your email address will not be published. Required fields are marked *