easy_ml/linear_regression.rs
1/*!
2Linear regression examples
3
4[Overview](https://en.wikipedia.org/wiki/Linear_regression).
5
6# Linear regression to fit a polynomial line
7The code below is a method for [minimising the sum of squares error](https://en.wikipedia.org/wiki/Least_squares)
8in choosing weights to learn to predict a polynomial ine. The method creates a design matrix
9from the inputs x, expanding each row from \[x\] to [1, x, x^2] to allow the model
10to represent the non linear relationship between x and y. To model more complex
11x and f(x), more complex basis functions are needed (ie to model a n-degree polynomial
12you will probably need n + 1 polynomial basis functions from x^0 to x^N).
13
14This example does not include any methods to prevent overfitting. In practise you
15may want to use some kind of [regularisation](https://en.wikipedia.org/wiki/Regularization_(mathematics))
16and or holding back some data for verification to stop updating the the model when it starts
17performing worse on unseen data.
18
19## Matrix APIs
20
21```
22use easy_ml::matrices::Matrix;
23
24// first create some data to fit a curve to
25let x: Matrix<f32> = Matrix::column(
26 vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0]
27);
28// we are going to fit a polynomial curve of x^2, but add the sin of x to each value for
29// y to create some deteriministic 'noise'.
30let y = x.map(|x| x.powi(2) + x.sin());
31println!("{:?}", &y);
32
33// Now we create a quadratic basis where each row is [1, x, x^2]
34let mut X = x.clone();
35// insert the 1 values as the first column, so that each row becomes [1, x]
36X.insert_column(0, 1.0);
37// insert another column with x^2 as the last, so that each row becomes [1, x, x^2]
38X.insert_column_with(2, x.column_iter(0).map(|x| x * x));
39println!("{:?}", &X);
40
41// now we compute the weights that give the lowest error for y - (X * w)
42// by w = inv(X^T * X) * (X^T * y)
43// Note the use of referencing X, w, and y so we don't move them into
44// a computation.
45// Because we're doing linear regression and creating the matrix we take the inverse
46// of in a particular way we don't check if the inverse exists here, but in general
47// for arbitary matrices you cannot assume that an inverse exists.
48let w = (X.transpose() * &X).inverse().unwrap() * (X.transpose() * &y);
49// now predict y using the learned weights
50let predictions = &X * &w;
51// compute the error for each y and predicted y
52let errors = &y - &predictions;
53// multiply each error by itself to get the squared error
54// and sum into a unit matrix by taking the inner prouct
55// then divide by the number of rows to get mean squared error
56let mean_squared_error = (errors.transpose() * &errors).get(0, 0) / x.rows() as f32;
57
58println!("MSE: {}", mean_squared_error);
59assert!(mean_squared_error > 0.41);
60assert!(mean_squared_error < 0.42);
61println!("Predicted y values:\n{:?}", &predictions);
62println!("Actual y values:\n{:?}", &y);
63
64// now we have a model we can predict outside the range we trained the weights on
65let test_x: Matrix<f32> = Matrix::column(vec![-3.0, -1.0, 0.5, 2.5, 13.0, 14.0]);
66let test_y = test_x.map(|x| x.powi(2) + x.sin());
67let mut test_X = test_x.clone();
68test_X.insert_column(0, 1.0);
69test_X.insert_column_with(2, test_x.column_iter(0).map(|x| x * x));
70
71// unsurprisingly the model has generalised quite well but
72// did better on the training data
73println!("Unseen x values:\n{:?}", test_x);
74println!("Unseen y predictions:\n{:?}", &test_X * &w);
75println!("Unseen y actual values:\n{:?}", test_y);
76let errors = &test_y - (&test_X * &w);
77let mean_squared_error = (errors.transpose() * &errors).get(0, 0) / test_x.rows() as f32;
78println!("MSE on unseen values: {}", mean_squared_error);
79assert!(mean_squared_error < 1.0);
80assert!(mean_squared_error > 0.99);
81```
82
83## Tensor APIs
84
85```
86use easy_ml::tensors::Tensor;
87use easy_ml::tensors::views::{TensorView, TensorStack};
88
89// first create some data to fit a curve to
90let x: Tensor<f32, 1> = Tensor::from(
91 [("row", 13)],
92 vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0]
93);
94// we are going to fit a polynomial curve of x^2, but add the sin of x to each value for
95// y to create some deteriministic 'noise'.
96let y = x.map(|x| x.powi(2) + x.sin());
97println!("{:?}", &y);
98
99// Now we create a quadratic basis where each row is [1, x, x^2]
100let mut X = TensorView::from(
101 TensorStack::<_, (_, _, _), 1>::from(
102 (
103 x.map(|x| 1.0), // the first column will be all 1.0s
104 x.clone(), // the second column will be x
105 x.map(|x| x * x) // the last column will be x^2
106 ),
107 // we want our X to have a shape of [("row", 13), ("column", 3)]
108 // so we insert the columns after the row dimension
109 (1, "column")
110 )
111);
112println!("{:?}", &X);
113
114// now we compute the weights that give the lowest error for y - (X * w)
115// by w = inv(X^T * X) * (X^T * y)
116// Note the use of referencing X, w, and y so we don't move them into
117// a computation.
118// Because we're doing linear regression and creating the matrix we take the inverse
119// of in a particular way we don't check if the inverse exists here, but in general
120// for arbitary matrices you cannot assume that an inverse exists.
121let w = (X.transpose(["column", "row"]) * &X).inverse().unwrap() *
122 (X.transpose(["column", "row"]) * y.expand([(1, "column")]));
123assert_eq!(w.shape(), [("row", 3), ("column", 1)]);
124// now predict y using the learned weights
125let predictions = &X * &w;
126// compute the error for each y and predicted y
127let errors = y.expand([(1, "column")]) - &predictions;
128// multiply each error by itself to get the squared error
129// and sum into a unit matrix by taking the inner prouct
130// then divide by the number of rows to get mean squared error
131let rows = errors.shape()[0].1;
132let mean_squared_error = (errors.transpose(["column", "row"]) * &errors).first() / rows as f32;
133
134println!("MSE: {}", mean_squared_error);
135assert!(mean_squared_error > 0.41);
136assert!(mean_squared_error < 0.42);
137println!("Predicted y values:\n{:?}", &predictions);
138println!("Actual y values:\n{:?}", &y);
139
140// now we have a model we can predict outside the range we trained the weights on
141let test_x: Tensor<f32, 1> = Tensor::from(
142 [("row", 6)],
143 vec![-3.0, -1.0, 0.5, 2.5, 13.0, 14.0]
144);
145let test_y = test_x.map(|x| x.powi(2) + x.sin());
146let test_X = TensorView::from(
147 TensorStack::<_, (_, _, _), 1>::from(
148 (
149 test_x.map(|x| 1.0),
150 test_x.clone(),
151 test_x.map(|x| x * x)
152 ),
153 (1, "column")
154 )
155);
156
157// unsurprisingly the model has generalised quite well but
158// did better on the training data
159println!("Unseen x values:\n{:?}", test_x);
160println!("Unseen y predictions:\n{:?}", &test_X * &w);
161println!("Unseen y actual values:\n{:?}", test_y);
162let errors = test_y.expand([(1, "column")]) - (&test_X * &w);
163let rows = errors.shape()[0].1;
164let mean_squared_error = (errors.transpose(["column", "row"]) * &errors).first() / rows as f32;
165println!("MSE on unseen values: {}", mean_squared_error);
166assert!(mean_squared_error < 1.0);
167assert!(mean_squared_error > 0.99);
168```
169
170*/