65.9K
CodeProject 正在变化。 阅读更多。
Home

Rust 中的简单线性回归

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年12月15日

CPOL

6分钟阅读

viewsIcon

9383

Rust 中的简单线性回归。

引言

作为最早且最简单的机器学习算法之一,实现简单线性回归对于任何机器学习、深度学习和人工智能新手来说,都是一次富有启发性且有益的经历。

在本教程中,我们将使用 Rust 实现简单线性回归。为了真正内化该算法,我们不会使用任何现有的数学或机器学习框架。我们唯一会使用的外部 crate 是 plotlib,它将允许我们以漂亮的 svg 图形可视化结果。
您可以在 https://github.com/phillikus/rust_ml 上找到整个项目的源代码。

要求

  • Rust 编程语言基础知识
  • 基础高中数学/统计学技能

简单线性回归

线性回归算法允许我们根据一组自变量 x0,x1..xn 来预测因变量 y
基于一组现有的 (x, y) 对,我们的目标是创建一个预测函数 y(x)

y(x1..xn) = b0 + b1 * x1 + b2 * x2 + .. + bn * xn

其中 b0 是所谓的截距(当 x==0 时的 y 值),b1..bn 是将应用于输入值的系数。

在简单线性回归的情况下,我们只有一个自变量 x,因此我们可以将上述函数简化为

y(x) = b0 + b1 * x

一旦我们确定了系数和截距,我们就可以通过简单地求解方程来使用它们来预测任何新 x 值。例如,如果 b0 == 5b1 == 2,则 y(4) 可以这样计算:

y(4) = 5 + 2 * 4 = 13

示例数据集

为了测试我们的算法,我编造了以下简单数据集:

[(1, 1), (2, 3), (3, 2), (4, 3), (5, 5)]

这可以在以下散点图中可视化:

我们的目标是画一条直线,使其尽可能接近这些点。然后,我们可以使用这条线来预测任何 x 值的 y

估计截距和系数

为了估计 b0b1,我们可以使用以下统计方程:

b1 = Cov(x, y) / Var(x)
b0 = mean(y) - b1 * mean(x)

其中 Cov 是协方差,Var 是方差,mean 是数组的平均值。
我们可以使用以下公式计算这三者:

mean(x) = sum(x) / length(x)
Var(x) = sum((x - mean(x))^2)
Cov = sum((x[i] - mean(x)) * (y[i] - mean(y)))

要了解有关此算法背后数学原理的更多信息,请查看 简单线性回归

项目结构

在列出所有基础知识后,让我们直接开始编写代码。我将项目结构化如下:

我们的线性回归模型将实现在 regression\linear_regression.rs 中。它将利用 utils\stat.rs,其中包含用于计算上述 meanvariancecovariance 的统计辅助函数。
(macros.rs 模块包含一个用于单元测试的辅助函数,请随时自行查看)。

为了使我们的代码可用作库,我们将 linear_regression 模块引用到 lib.rs 文件中,然后从 main.rs 作为 crate 使用它。

实现平均值、方差和协方差

让我们开始实现上面描述的 3 个数学运算,因为它们构成了我们算法的核心。我们将它们放在 stat.rs 文件中:

pub fn mean(values : &Vec<f32>) -> f32 {
    if values.len() == 0 {
        return 0f32;
    }

    return values.iter().sum::<f32>() / (values.len() as f32);
}

pub fn variance(values : &Vec<f32>) -> f32 {
    if values.len() == 0 {
        return 0f32;
    }

    let mean = mean(values);
    return values.iter()
            .map(|x| f32::powf(x - mean, 2 as f32))
            .sum::<f32>() / values.len() as f32;
}

pub fn covariance(x_values : &Vec<f32>, y_values : &Vec<f32>) -> f32 {
    if x_values.len() != y_values.len() {
        panic!("x_values and y_values must be of equal length.");
    }

    let length : usize = x_values.len();
    
    if length == 0usize {
        return 0f32;
    }

    let mut covariance : f32 = 0f32;
    let mean_x = mean(x_values);
    let mean_y = mean(y_values);

    for i in 0..length {
        covariance += (x_values[i] - mean_x) * (y_values[i] - mean_y)
    }

    return covariance / length as f32;        
}

请注意,当向任一方法提供空的 Vec<f32> 时,我们会返回 0。如果在 covariance 方法中 size(x)!=size(y),代码将中断并 panic。除此之外,代码应该是不言自明的,它只是执行了我们上面定义的计算。

实现简单线性回归

在准备好统计函数后,让我们深入研究实际算法。我们的线性回归模型的接口将如下所示:

pub struct LinearRegression {
    pub coefficient: Option<f32>,
    pub intercept: Option<f32>
}

impl LinearRegression {
    pub fn new() -> LinearRegression { .. }
    pub fn fit(&mut self, x_values : &Vec<f32>, y_values : &Vec<f32>) { .. }
    pub fn predict_list(&self, x_values : &Vec<f32>) -> Vec<f32> { .. }
    pub fn predict(&self, x : f32) -> f32 { .. }
    pub fn evaluate(&self, x_test : &Vec<f32>, y_test: &Vec<f32>) -> f32 { ..}

struct 包含两个属性,用于获取我们模型的截距和系数(b0b1)。要初始化它们,我们必须使用 new 函数创建实例,然后调用其 fit 方法(默认情况下,两者都将是 None)。
之后,我们可以使用其他方法进行新预测并评估我们模型的性能(使用 均方根误差)。

让我们逐一介绍这些方法:

pub fn new() -> LinearRegression {
    LinearRegression { coefficient: None, intercept: None }
}

这里没有什么特别之处,我们返回一个包含 None 初始化系数和截距的新 struct 实例。

让我们深入了解 fit 函数。请记住,我们可以使用统计数据来计算 b0b1

b1 = Cov(x, y) / Var(x)
b0 = mean(y) - b1 * mean(x)

翻译成 Rust,我们的代码将如下所示:

pub fn fit(&mut self, x_values : &Vec<f32>, y_values : &Vec<f32>) {
    let b1 = stat::covariance(x_values, y_values) / stat::variance(x_values);
    let b0 = stat::mean(y_values) - b1 * stat::mean(x_values);

    self.intercept = Some(b0);
    self.coefficient = Some(b1);       
}

所有繁重的工作都已在 stat.rs 中实现,因此这段代码看起来非常直接。请记住在文件顶部添加 use utils::stat; 以使其编译。

为了对新值 x 进行预测,我们可以使用上面定义的线性回归方程:

y(x) = b0 + b1 * x

在 Rust 中,这可以轻松实现:

pub fn predict(&self, x : f32) -> f32 {
    if self.coefficient.is_none() || self.intercept.is_none() {
        panic!("fit(..) must be called first");
    }

    let b0 = self.intercept.unwrap();
    let b1 = self.coefficient.unwrap();

    return b0 + b1 * x;
}

我们首先检查截距 (b0) 和系数 (b1) 是否尚未初始化,在这种情况下显示错误消息。否则,我们通过 unwrapping 这两个属性来获取 b0b1,并返回计算 y(x) 的结果。

为了对列表输入进行预测,我添加了一个额外的 predict_list 方法:

pub fn predict_list(&self, x_values : &Vec<f32>) -> Vec<f32> {
    let mut predictions = Vec::new();

    for i in 0..x_values.len() {
        predictions.push(self.predict(x_values[i]));
    }

    return predictions;
}

在这里,我们迭代所有输入元素,预测它们的 y 值,并将它们添加到我们的预测列表中,然后返回该列表。

性能评估

现在只剩下 evaluate 函数,它将告诉我们模型的准确度。如上所述,我们将使用 均方根误差 方法。
我们可以使用以下公式计算均方根误差:

mse = sum((precition[i] - actual[i])^2)
rmse = sqrt(mse)

在 Rust 中,这可以表示为以下函数:

fn root_mean_squared_error(&self, actual : &Vec<f32>, predicted : &Vec<f32>) -> f32 {
    let mut sum_error = 0f32;
    let length = actual.len();

    for i in 0..length {
        sum_error += f32::powf(predicted[i] - actual[i], 2f32);
    }

    let mean_error = sum_error / length as f32;
    return mean_error.sqrt();
}

但是,在此函数之前,我们必须预测测试集的值。我们将在 evaluate 函数中执行此操作,然后返回 root_mean_squared_error 函数的结果:

pub fn evaluate(&self, x_test : &Vec<f32>, y_test: &Vec<f32>) -> f32 {
    if self.coefficient.is_none() || self.intercept.is_none() {
        panic!("fit(..) must be called first");
    }

    let y_predicted = self.predict_list(x_test);
    return self.root_mean_squared_error(y_test, &y_predicted);
}

同样,如果系数或截距未初始化,代码将因错误消息而中断。否则,我们调用 predict_list 方法创建一个预测列表,并将其传递给我们的 rmse 函数。

测试算法

一切准备就绪,是时候测试我们新的线性回归算法了!在 main.rs 文件中,我们可以创建一个 public main() 函数并像这样初始化我们的模型:

let mut model = linear_regression::LinearRegression::new();
let x_values = vec![1f32, 2f32, 3f32, 4f32, 5f32];
let y_values = vec![1f32, 3f32, 2f32, 3f32, 5f32];
model.fit(&x_values, &y_values);

我们可以使用我们创建的方法来显示系数、截距和准确度:

println!("Coefficient: {0}", model.coefficient.unwrap());
println!("Intercept: {0}", model.intercept.unwrap());
println!("Accuracy: {0}", model.evaluate(&x_values, &y_values));

这将打印:

Coefficient: 0.8
Intercept: 0.39999986
Accuracy: 0.69282037

为了进行预测,我们可以使用我们的两种预测方法:

let y_predictions : Vec<f32> = model.predict_list(&x_values);
let y_prediction : f32 = model.predict(4);

可视化结果

为了直观地了解我们的算法性能,而无需进行大量计算,最好通过将预测值绘制到二维坐标系中来获得可视化表示。我们可以使用 plotlib 库来实现此目的:

let plot_actual = Scatter::from_vec(&actual)
    .style(scatter::Style::new()
        .colour("#35C788"));

let plot_prediction = Scatter::from_vec(&y_prediction)
    .style(scatter::Style::new()
        .marker(Marker::Square)
        .colour("#DD3355"));    

let v = View::new()
    .add(&plot_actual)
    .add(&plot_prediction)
    .x_range(-0., 6.)
    .y_range(0., 6.)
    .x_label("x")
    .y_label("y");

Page::single(&v).save("scatter.svg");

别忘了按照 use 指令导入 crate 并添加它们:

extern crate plotlib;

use plotlib::scatter::Scatter;
use plotlib::scatter;
use plotlib::style::{Marker, Point};
use plotlib::view::View;
use plotlib::page::Page;

这将把包含实际值和我们预测的图保存到一个格式精美的 .svg 文件中。

正如你所见,我们的预测与大多数值的实际结果非常吻合。唯一的异常值出现在 x==2x==3,尽管两者仍在可接受的范围内。

结论

这标志着我们对线性回归算法的探索之旅结束了。您可以随意查看源代码并尝试不同的输入值。如果您有任何问题、建议或评论,请告诉我。

© . All rights reserved.