Categories
Machine Learning & AI

Rapid Machine Learning Prototyping in Rust

Rust? Yes, Rust. Not even I, the one writing this article, am familiar with the language, if we don’t count this past week. So why am I writing about it with such limited knowledge?

Well, first of all, it’s my blog, and I can write whatever I want. Secondly, the Linfa library looks pretty sick! So I figured I’d give it a shot and document the process.

What is Linfa?

Scikit-learn of the Rust language.

If you want to read more: Linfa is basically a library that lets you build machine learning models quickly by default, because it’s written entirely in Rust, it’s also safe by default.

The Rust compiler helps a lot when errors happen; it’s honestly way easier to deal with than scikit-learn’s cryptic errors. Plus, you can achieve parallelization using Rayon, which is super handy.

So yeah, basically the scikit-learn of the Rust world, but with some cool new features. Let’s just dive in; I’m already bored of this introduction.

Linear Regression with Linfa

I chose linear regression for my first introduction to Linfa because it’s easy to understand. Honestly, that’s the only reason I have.

Ready your Rust environment, create a new project, and add these dependencies to your Cargo.toml file:

[dependencies]
linfa = "0.6.1"
linfa-linear = "0.6.1"
ndarray = "0.15"
ndarray-rand = "0.14"
rand = "0.8"

Now that we’re ready, let’s first talk about what the heck linear regression actually is before diving into code. We should understand the concept first, otherwise, I don’t know how we’ll use it efficiently in real life.

Linear regression is basically a mathematical equation: y = ax + b. Here, y is the result (dependent variable), x is the input (independent variable), and a and b are constants (coefficients) that the model learns.

In simple terms, the model tries to find the best values of a and b so that for given x values, the predicted y is as close as possible to the real data points.

You can read my article on correlation for more information on how ML models use correlation to find patterns in data and predict future events. Now, let’s move on to the code, I’m getting bored of definitions anyway.

Implementing
use linfa::prelude::*;
use linfa_linear::LinearRegression;
use ndarray::{Array, Array2, Axis};
use ndarray_rand::rand_distr::Uniform;
use ndarray_rand::RandomExt;

First, we import the necessary crates and modules.

fn main() -> Result<(), Box<dyn std::error::Error>> {
  
}

This is the start of the main function, which is the entry point of every Rust program. Yes, I know you probably already know this, but I want to explain: the return type Result with an empty tuple means the function will return Ok if everything goes well, or an error wrapped in a Box if something goes wrong.

fn main() -> Result<(), Box<dyn std::error::Error>> {
      let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
}

We need a dataset, but unfortunately, I’m too lazy to find one, so firstly I created a random array with 100 x values and 1 feature.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
    
    // y must be 1D: shape (100,)
    let mut y = x.column(0).mapv(|x_val| 2.0 * x_val + 3.0);
    y += &Array::random(100, Uniform::new(-1.0, 1.0));
}

Now we need some noise, this means data that doesn’t perfectly match the pattern. For example, if the average height of girls is 1.60 meters, you might still find a girl in the dataset who is 1.98 meters tall. You can think of it like that.

I added noise to the data to simulate real-world randomness, but the underlying pattern in the random x values still follows the equation y = 2x + 3.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
    
    // y must be 1D: shape (100,)
    let mut y = x.column(0).mapv(|x_val| 2.0 * x_val + 3.0);
    y += &Array::random(100, Uniform::new(-1.0, 1.0));
    
    let dataset = Dataset::new(x, y);
}

This line creates a new dataset by combining the input data x (features) and the target data y (labels). The Dataset new function packages these arrays together into a format that Linfa’s models can understand and work with.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
    
    // y must be 1D: shape (100,)
    let mut y = x.column(0).mapv(|x_val| 2.0 * x_val + 3.0);
    y += &Array::random(100, Uniform::new(-1.0, 1.0));
    
    let dataset = Dataset::new(x, y);
    let model = LinearRegression::default().fit(&dataset)?;
}

We trained our model using the dataset. However, I skipped the step of splitting the dataset into training and testing sets to keep things simple.

In real projects, it is important to test your model with unseen data to ensure that there is no overfitting (which is not a concern in this case).

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
    
    // y must be 1D: shape (100,)
    let mut y = x.column(0).mapv(|x_val| 2.0 * x_val + 3.0);
    y += &Array::random(100, Uniform::new(-1.0, 1.0));
    
    let dataset = Dataset::new(x, y);
    
    let model = LinearRegression::default().fit(&dataset)?;
    let predictions = model.predict(&dataset);
}

This line uses the trained model to make predictions based on the input data in the dataset. The predict function takes the features from the dataset and outputs the model’s estimated target values.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let x: Array2<f64> = Array::random((100, 1), Uniform::new(0., 10.));
    
    // y must be 1D: shape (100,)
    let mut y = x.column(0).mapv(|x_val| 2.0 * x_val + 3.0);
    y += &Array::random(100, Uniform::new(-1.0, 1.0));
    
    let dataset = Dataset::new(x, y);
    
    let model = LinearRegression::default().fit(&dataset)?;
    let predictions = model.predict(&dataset);
    
    for i in 0..10 {
        let real = dataset.targets()[i];
        let predicted = predictions[i];
        println!("Real: {:.2}, Predicted: {:.2}", real, predicted);
    }

    Ok(())
}

I don’t think I need to explain the last part, I just took the first 10 rows from the predictions and real data and printed them to the console for comparison.

Results

I love seeing results because it makes me feel like I did something important, but in reality, we just built a simple linear regression model with random data, not even real data.

But it’s all for education and fun!

$ cargo run

Real: 5.43, Predicted: 5.71
Real: 15.32, Predicted: 15.27
Real: 10.32, Predicted: 10.87
Real: 14.62, Predicted: 13.92
Real: 11.45, Predicted: 10.72
Real: 14.08, Predicted: 14.23
Real: 14.88, Predicted: 14.39
Real: 15.91, Predicted: 15.97
Real: 8.59, Predicted: 7.64
Real: 2.67, Predicted: 3.05

Seeing pairs like “Real: 5.43, Predicted: 5.71” shows how closely the model estimates actual results.

Small differences between these numbers indicate the model successfully captured the underlying pattern in the data.

Mission accomplished!

Conclusion

I wanted to try something new, and honestly, it was more fun than I expected. Maybe I’ll write more content about Rust, who knows, not even me.

Anyway, I can easily say that Linfa is a great alternative to scikit-learn if you already use Rust in your workflow. If you don’t, it’s still worth considering because Rust is gaining popularity and it’s fun to work with.

You don’t need to worry about performance or memory safety with Rust, so if you like how it works, I think you should give Linfa a shot for your next machine learning project.

I’ll never bite, I’ll never bite the pain.

Leave a Reply

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