swift算法:线性回归

@高效码农  August 8, 2023

线性回归

线性回归是一种创建两个(或多个)变量之间关系模型的技术。

例如,假设我们计划出售一辆汽车。我们不确定要多少钱。所以我们看看最近的广告中其他汽车的要价。我们可以考虑很多变量 - 例如:品牌、型号、发动机尺寸。为了简化我们的任务,我们仅收集有关汽车的车龄和价格的数据:

车龄(年)价格(英镑)
10500
8400
37,000
38,500
211,000
110,500

我们的车已经有4年了。我们如何根据此表中的数据为我们的汽车定价?

让我们首先查看绘制的数据:

图1

我们可以想象一条穿过该图上的点的直线。(在这种情况下)它不会精确地穿过每个点,但我们可以放置这条线,使其尽可能接近所有点。

换句话说,我们想让直线到每个点的距离尽可能小。这通常是通过最小化从线到每个点的距离的平方来完成的。

我们可以用两个变量来描述这条直线:

  1. 它与 y 轴相交的点即全新汽车的预测价格。这就是拦截
  2. 线的斜率- 即对于每一年,价格变化多少

这是我们的线的方程式:

carPrice = slope * carAge + intercept

我们如何找到截距和斜率的最佳值?让我们看看两种不同的方法来做到这一点。

迭代方法

一种方法是从截距和斜率的一些任意值开始。我们计算出对这些值进行哪些小的更改以使我们的线更接近数据点。然后我们重复多次。最终我们的线将接近最佳位置。

首先让我们设置数据结构。我们将使用两个 Swift 数组来表示汽车年龄和汽车价格:

let carAge: \[Double\] \= \[10, 8, 3, 3, 2, 1\]
let carPrice: \[Double\] \= \[500, 400, 7000, 8500, 11000, 10500\]

这就是我们表示直线的方式:

var intercept \= 0.0
var slope \= 0.0
func predictedCarPrice(\_ carAge: Double) \-> Double {
    return intercept + slope \* carAge
}

现在我们来看看执行迭代的代码:

let numberOfCarAdvertsWeSaw \= carPrice.count
let numberOfIterations \= 100
let alpha \= 0.0001

for \_ in 1...numberOfIterations {
    for i in 0..<numberOfCarAdvertsWeSaw {
        let difference \= carPrice\[i\] \- predictedCarPrice(carAge\[i\])
        intercept += alpha \* difference
        slope += alpha \* difference \* carAge\[i\]
    }
}

alpha是决定每次迭代我们距离正确解决方案有多近的一个因素。如果这个因子太大,那么我们的程序将无法收敛到正确的解决方案。

该程序循环遍历每个数据点(每个车龄和汽车价格)。对于每个数据点,它会调整截距和斜率,使它们更接近正确值。代码中用于调整截距和斜率的方程基于朝这些变量最大减少的方向移动。这就是梯度下降

我们想要最小化线和点之间距离的平方。我们定义一个函数J来表示这个距离——为了简单起见,我们在这里只考虑一个点。该函数J与 成正比((slope * carAge + intercept) - carPrice)) ^ 2

为了朝着最大减少的方向移动,我们对该函数相对于斜率求偏导数,对于截距也类似。我们将这些导数乘以因子 alpha,然后使用它们来调整每次迭代的斜率和截距值。

看代码,直观上是有道理的——当前预测的汽车价格和实际的汽车价格差异越大,并且 的值越大,alpha对截距和斜率的调整就越大。

可能需要大量迭代才能接近理想值。让我们看看随着迭代次数的增加截距和斜率如何变化:

迭代截距一辆 4 年车龄的汽车的预测价值
0000
2000年4112-1133659
60008564-7645507
1000010517-10496318
1400011374-11756673
1800011750-12306829

这是以图表形式显示的相同数据。图表上的每条蓝线代表上表中的一行。

图2

经过 18,000 次迭代后,这条线看起来越来越接近我们期望的(仅通过观察)最佳拟合的正确线。此外,每增加 2,000 次迭代,对最终结果的影响就会越来越小 - 截距和斜率的值会收敛到正确的值。

封闭式解决方案

还有另一种方法可以计算最佳拟合线,而无需进行多次迭代。我们可以求解描述最小二乘最小化的方程,并直接计算出截距和斜率。

首先我们需要一些辅助函数。这个计算 Double 数组的平均值:

func average(\_ input: \[Double\]) \-> Double {
    return input.reduce(0, +) / Double(input.count)
}

我们使用reduceSwift 函数对数组的所有元素求和,然后除以元素数量。这给了我们平均值。

我们还需要能够将数组中的每个元素与另一个数组中的相应元素相乘,以创建一个新数组。这是一个可以执行此操作的函数:

func multiply(\_ a: \[Double\], \_ b: \[Double\]) \-> \[Double\] {
    return zip(a,b).map(\*)
}

我们正在使用该map函数来乘以每个元素。

最后,将线拟合到数据的函数:

func linearRegression(\_ xs: \[Double\], \_ ys: \[Double\]) \-> (Double) \-> Double {
    let sum1 \= average(multiply(ys, xs)) \- average(xs) \* average(ys)
    let sum2 \= average(multiply(xs, xs)) \- pow(average(xs), 2)
    let slope \= sum1 / sum2
    let intercept \= average(ys) \- slope \* average(xs)
    return { x in intercept + slope \* x }
}

该函数将两个双精度数组作为参数,并返回一个最佳拟合线的函数。计算斜率和截距的公式可以从我们对函数 的定义中导出J。让我们看看该行的输出如何适合我们的数据:

图3

使用这条线,我们可以预测我们 4 年车龄的汽车的价格为 6952 英镑。

概括

我们已经看到了在 Swift 中实现简单线性回归的两种不同方法。一个明显的问题是:为什么要费心使用迭代方法呢?

嗯,我们发现的线与数据并不完全吻合。一方面,该图表在高车龄时包含一些负值!可能我们需要花钱请人拖走一辆非常旧的汽车……但实际上这些负值只是表明我们没有非常准确地模拟现实生活情况。汽车车龄和汽车价格之间的关系不是线性的,而是其他函数。我们还知道,汽车的价格不仅与车龄有关,还与汽车的品牌、型号和发动机尺寸等其他因素有关。我们需要使用额外的变量来描述这些其他因素。

事实证明,在一些更复杂的模型中,迭代方法是唯一可行或有效的方法。当数据数组非常大并且数据值可能稀疏时,也会发生这种情况。



评论已关闭