Rust 中的简单线性回归





5.00/5 (3投票s)
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 == 5
且 b1 == 2
,则 y(4)
可以这样计算:
y(4) = 5 + 2 * 4 = 13
示例数据集
为了测试我们的算法,我编造了以下简单数据集:
[(1, 1), (2, 3), (3, 2), (4, 3), (5, 5)]
这可以在以下散点图中可视化:
我们的目标是画一条直线,使其尽可能接近这些点。然后,我们可以使用这条线来预测任何 x
值的 y
。
估计截距和系数
为了估计 b0
和 b1
,我们可以使用以下统计方程:
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,其中包含用于计算上述 mean
、variance
和 covariance
的统计辅助函数。
(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
包含两个属性,用于获取我们模型的截距和系数(b0
和 b1
)。要初始化它们,我们必须使用 new
函数创建实例,然后调用其 fit
方法(默认情况下,两者都将是 None
)。
之后,我们可以使用其他方法进行新预测并评估我们模型的性能(使用 均方根误差)。
让我们逐一介绍这些方法:
pub fn new() -> LinearRegression {
LinearRegression { coefficient: None, intercept: None }
}
这里没有什么特别之处,我们返回一个包含 None
初始化系数和截距的新 struct
实例。
让我们深入了解 fit
函数。请记住,我们可以使用统计数据来计算 b0
和 b1
:
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 这两个属性来获取 b0
和 b1
,并返回计算 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==2
和 x==3
,尽管两者仍在可接受的范围内。
结论
这标志着我们对线性回归算法的探索之旅结束了。您可以随意查看源代码并尝试不同的输入值。如果您有任何问题、建议或评论,请告诉我。