动机
线性回归是机器学习中最基本的工具之一。它用于找到最适合我们数据的一条直线。尽管它只能处理简单的直线模式,但理解背后的数学可以帮助我们理解梯度下降和损失最小化方法。这对于所有机器学习和深度学习任务中使用的更复杂模型非常重要。
在本文中,我们将亲自动手使用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, )。
可视化后,数据集如下所示:
我们的目标是找到一条最佳拟合线,使其通过数据中心,最小化预测值和原始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)用于比较实数。方程如下:
我们只关心值之间的绝对差异。高于原始值的预测和低于原始值的预测一样糟糕。所以我们将目标值和预测之间的差异平方,将负差异转换为正差异。此外,这惩罚了目标和预测之间的较大差异,因为较大的差异平方将更多地贡献给最终损失。为了使我们的预测尽可能接近原始目标,我们现在试图最小化这个函数。当梯度为零时,损失函数将达到最小值。由于只能优化权重和偏差值,我们对MSE函数相对于权重和偏差值进行偏导数。
然后,我们根据梯度值优化权重。
我们对每个权重值进行梯度,并将它们移动到梯度的相反方向。这将损失推向最小值。根据图像,梯度是正的,所以我们减小权重。这将使J(W)或损失向最小值推进。因此,优化方程如下所示:
学习率(或alpha)控制图像中显示的增量步骤。我们只对值进行小的改变,以稳定地向最小值移动。
实现
如果我们使用基本的代数运算简化导数方程,这将非常简单实现。
对于导数,我们使用两行代码来实现:
# 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
结果
使用随机初始化的权重和偏置,我们的预测结果如下:
图片作者:Muhammad Arham 权重和偏置非常接近0,因此我们得到了一条水平线。经过1000次迭代训练模型后,我们得到了以下结果:
图片作者: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应用,这些应用在全球排行榜上名列前茅。他对于构建和优化智能系统的机器学习模型感兴趣,并且坚信不断改进。