Любой практикующий специалист по данным знаком с такими библиотеками, как Scikit-learn, NumPy и SciPy. Но как насчет аналогичных библиотек в Rust, есть ли какие-нибудь хорошие ящики, которые можно успешно использовать для создания приложений машинного обучения с использованием этого невероятно быстрого и эффективного с точки зрения памяти языка программирования? В моем первом сообщении в блоге, посвященном машинному обучению в Rust, я покажу, как можно реализовать линейный регрессор с нуля. Я также продемонстрирую некоторые полезные инструменты, которые вы можете использовать для реализации многих других алгоритмов статистического обучения на чистом Rust.

В конце концов, машинное обучение несложно, если у вас под рукой правильный набор инструментов, а обучение на основе данных в Rust имеет свои уникальные преимущества, которых нет в других языках программирования!

Часть I. Повторный курс по машинному обучению

Чтобы следовать моему коду, вам не нужен какой-либо предыдущий опыт машинного обучения, но нам нужно потратить некоторое время, чтобы обсудить некоторые важные идеи, лежащие в основе статистического моделирования. Если вы знакомы с этими концепциями, можете пропустить этот раздел.

Во-первых, давайте ответим на вопрос, что такое статистическое обучение? Допустим, мы наблюдаем количественный ответ Y и некоторые атрибуты, x ₁, x ₂,…, x m. которые соответствуют этому ответу.

Например, предположим, что у вас есть дом с определенной ценой, в котором 4 комнаты и 2 ванные комнаты. Атрибуты дома, такие как количество комнат и ванных комнат, называются предикторами, а цена дома называется целевой или ответной. Мы думаем, что между предсказателями и ценой на жилье может быть какая-то связь. Это отношение можно записать в виде функции, которую мы называем моделью.

Поскольку эта функция не отображает это соотношение в точности, всегда есть некоторая неприводимая ошибка, которую, как мы надеемся, можно игнорировать в большинстве случаев.

Обычно у вас есть не один образец, а много. Например, вместо одного дома вы можете получить набор данных с несколькими домами, каждый со своим собственным набором предикторов и ценой. Когда мы складываем наши предикторы вертикально, один поверх другого, мы получаем матрицу X с предикторами, а также вектор-столбец Y. Эта матрица и вектор-столбец становятся нашим набором данных.

В этом примере у нас есть набор данных с атрибутами дома и ценами, собранный Службой переписи населения США в районе Бостона, Массачусетс. Этот набор данных имеет 13 числовых и категориальных атрибутов. Мы хотим спрогнозировать цену дома по этим атрибутам.

Мы используем статистические модели для решения проблем, и есть несколько способов классифицировать эти проблемы. Например, если ваша цель Y является количественной (другими словами, числовой), вы можете построить регрессионную модель, которая прогнозирует это значение из X. Когда Y категоричен, ваш единственный вариант - это классификатор, который может предсказать класс Y на основе X.

Тестирование моделей является неотъемлемой частью процесса статистического моделирования, потому что не все модели идеальны, и вы должны понимать, насколько ваши прогнозы далеки от истинных значений, чтобы устанавливать правильные ожидания и правильно управлять рисками. Один из распространенных методов оценки неснижаемой ошибки - разделение данных на обучающие и тестовые наборы данных. Вы используете обучающий набор данных, чтобы соответствовать вашей модели, и вы используете тестовый набор данных, чтобы оценить ошибку, сравнивая предсказанные значения с истинными значениями.

Итак, как выбрать модель, отражающую взаимосвязь между X и Y? Какие параметры у этой модели? Как мы оцениваем эти параметры?

Это ключевые вопросы, которые вы задаете себе при разработке статистической модели. Когда вы решаете задачу машинного обучения, первое, что вы пытаетесь понять, - это то, к какой категории относится эта проблема: можете ли вы проводить обучение с учителем? Вашему клиенту нужен классификатор или регрессор? Какой существующий алгоритм лучше подходит для моих данных?

Ответ на все эти вопросы будет зависеть от вашей проблемы и ваших данных, но вам понадобятся библиотеки с широким спектром алгоритмов, которые вы можете использовать для моделирования отношений между Y и X. Такие языки, как Python и R, предоставляют обширную экосистему инструментов. которые могут использоваться инженерами для разработки и реализации статистических моделей. В большинстве этих инструментов используются методы, разработанные в четырех разделах математики, которые вы можете увидеть на рисунке ниже. Хорошая новость заключается в том, что в Rust вы найдете зрелые библиотеки, которые предоставляют методы как минимум в двух из этих важнейших ветвей: оптимизации и линейной алгебре.

В этом и следующих сообщениях блога я сосредоточусь на одном конкретном подходе к контролируемому обучению, линейных моделях, который предполагает наличие примерно линейной зависимости между X и Y . Я буду использовать эту простую технику моделирования, чтобы продемонстрировать ящики Rust, которые вы также можете использовать для реализации других алгоритмов машинного обучения. Кроме того, линейные модели популярны в машинном обучении и обладают множеством полезных свойств, таких как отличная интерпретируемость, быстрая реализация и большие возможности обобщения.

В линейной модели, когда у нас есть только один предиктор x1, мы моделируем взаимосвязь между X и Y как линию, которая пересекает ось Y в некоторой точке бета 0 и имеет наклон бета 1.

Когда у нас есть 2 предиктора, наша линия становится плоскостью, которая определяется двумя параметрами, β1 и β2, и пересекает оси Y в точке β0. Когда вы подбираете линейную модель к своим данным, ваша цель - найти значения для параметров модели, β.

Часть II. Линейная регрессия в Rust

Давайте посмотрим, как мы впишем линейную модель в Python. Чтобы продемонстрировать регрессию, я буду использовать The Boston Housing Dataset, и я хочу спрогнозировать цену дома на основе характеристик дома.

import numpy as np
from sklearn.datasets import load_boston

boston = load_boston()

Нашим первым шагом было бы отделить предикторы, X от целевого Y, и разделить наш набор данных на тестовый и обучающий наборы.

import pandas as pd
from sklearn.model_selection import train_test_split

bos = pd.DataFrame(boston.data)
bos.columns = boston.feature_names
bos['PRICE'] = boston.target

X = bos.drop('PRICE', axis=1).values
y = bos.PRICE.values

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2)   
X_train.shape, X_test.shape, y_train.shape, y_test.shape

Теперь мы готовы установить линейный регрессор для обучающей части X и Y. Линейный регрессор определен в пакете Scikit Learn. Этот пакет предоставляет большой выбор традиционных алгоритмов машинного обучения и представляет собой библиотеку для машинного обучения, которая широко используется в отрасли. С помощью Scikit Learn мы можем поместить линейный регрессор в наши данные в одну строку.

from sklearn.linear_model import LinearRegression
lr = LinearRegression().fit(X_train, y_train)

Чтобы оценить производительность нашего регрессора, мы будем использовать среднюю абсолютную ошибку, которая вычисляет среднюю абсолютную разницу между прогнозируемыми и истинными значениями.

Мы вычисляем среднюю абсолютную ошибку, используя тестовый набор данных, и получаем 3,4

from sklearn.metrics import mean_absolute_error
print(mean_absolute_error(y_test, lr.predict(X_test)))
3.447122276148069

Мы также можем распечатать параметры нашей модели.

print("coef: {}, intercept: {}".format(lr.coef_, lr.intercept_))
coef: [-1.09345106e-01  5.14771860e-02 -1.35771811e-02  3.11370157e+00
 -1.53107579e+01  3.54796861e+00 -4.78707187e-03 -1.49453646e+00
  3.17255788e-01 -1.20148597e-02 -9.28169726e-01  1.03065117e-02
 -5.58857305e-01], intercept: 37.03977851074369

Код, который я продемонстрировал, использует несколько пакетов Python, но на сегодняшний день наиболее заметным из них является NumPy. NumPy является основой большинства библиотек для машинного обучения и научных вычислений на Python, поскольку NumPy предоставляет набор инструментов для управления n-мерными массивами, а также процедурами линейной алгебры. Scikit Learn в значительной степени полагается на NumPy для вычисления параметров модели, когда вы вызываете методы соответствия и прогнозирования.

Нам понадобится аналогичная библиотека, если я хочу обучать и запускать наши статистические модели на Rust. К счастью, в Rust есть как минимум два отличных ящика, которые мы можем использовать для управления n-мерными массивами: nalgebra и ndarray. Хотя эти библиотеки предоставляют немного разные функции, мы могли бы использовать их взаимозаменяемо для наших целей. Одно из важных различий между этими двумя библиотеками заключается в том, что nalgebra поддерживает только 2D и 1D массивы, а ndarray поддерживает массивы произвольного размера. Другое отличие состоит в том, что вам не нужны серверные части BLAS и LAPACK для решения линейных уравнений и разложения матриц при использовании алгебры.

Мы начинаем с импорта структур и признаков nalgebra, в которых определены функции управления данными.

use nalgebra::{DMatrix, DVector, Scalar};

Читаем файл с набором данных Boston Housing. К сожалению, в nalgebra нет функции чтения данных из файла CSV, но ее нетрудно реализовать самостоятельно.

use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;
use std::str::FromStr;

fn parse_csv<N, R>(input: R) -> Result<DMatrix<N>, Box<dyn std::error::Error>>
  where N: FromStr + Scalar,
        N::Err: std::error::Error,
        R: BufRead
{
  // initialize an empty vector to fill with numbers
  let mut data = Vec::new();

  // initialize the number of rows to zero; we'll increment this
  // every time we encounter a newline in the input
  let mut rows = 0;

  // for each line in the input,
  for line in input.lines() {
    // increment the number of rows
    rows += 1;
    // iterate over the items in the row, separated by commas
    for datum in line?.split_terminator(",") {
      // trim the whitespace from the item, parse it, and push it to
      // the data array
      data.push(N::from_str(datum.trim())?);
    }
  }

  // The number of items divided by the number of rows equals the
  // number of columns.
  let cols = data.len() / rows;

  // Construct a `DMatrix` from the data in the vector.
  Ok(DMatrix::from_row_slice(rows, cols, &data[..]))
}

let file = File::open("../data/creditcard.csv").unwrap();
let bos: DMatrix<f64> = parse_csv(BufReader::new(file)).unwrap(); 
println!("{}", bos.rows(0, 5));

Вот как выглядят наши данные, когда они представлены в виде двухмерной матрицы. Крайний левый столбец - это цена дома, а остальные столбцы - атрибуты дома.

Мы отделяем предикторы от цены с помощью методов манипулирования матрицей, определенных в алгебре.

let x = bos.columns(0, 13).into_owned();
let y = bos.column(13).into_owned();

Чтобы разделить данные на обучающие / тестовые наборы, нам нужно использовать функцию, определенную в другом крейте Rust, Smartcore.

use smartcore::model_selection::train_test_split;
let (x_train, x_test, y_train, y_test) = train_test_split(&x, &y.transpose(), 0.2);

Итак, как мы можем вписать линейную регрессию в Rust? Из линейной алгебры мы знаем, что один из способов решения системы линейных уравнений

получается обратным к A

Если вы сравните формулу, представляющую линейную регрессию, с этим выражением, вы увидите, что все, что нам нужно сделать, чтобы найти параметры линейной регрессии, - это взять обратное X

Одна из проблем с прямым решением этого уравнения заключается в том, что для высоких и тонких матриц, где у вас больше строк, чем столбцов, точных решений, скорее всего, не будет. Итак, один из приемов, который мы можем использовать для поиска приближенного решения, - это преобразовать X в квадратную матрицу, умножив наше уравнение на X, транспонированное с обеих сторон. Теперь решение можно найти, взяв обратное к X транспонирование X

Посмотрим, получим ли мы те же значения параметров, если применим эту формулу в Rust. Определим A и b

let a = x_train.clone().insert_column(13, 1.0).into_owned();
let b = y_train.clone().transpose();

Когда мы применяем формулу из предыдущего слайда к нашим данным, мы получаем аналогичные коэффициенты и перехват!

let x = (a.transpose() * &a).try_inverse().unwrap() * &a.transpose() * &b;
let coeff = x.rows(0, 13);
let intercept = x[(13, 0)];
println!("coeff: {}, intercept: {}", coeff, intercept);

Обратите внимание, что коэффициенты и точка пересечения очень близки к значениям, полученным в Python. Причина, по которой числа не совпадают, заключается в том, что наш разделение на поезд / тест в Rust отличается от разделения на поезд / тест в Python из-за рандомизации.

Одной из проблем этого подхода к оценке параметров модели является обратная матрица, которая требует больших вычислительных ресурсов и нестабильна в числовом отношении. Альтернативный подход - использовать разложение матрицы, чтобы избежать этой операции. Разложение матрицы разбивает матрицу на составляющие элементы. Есть много методов разложения, но в этом примере я буду использовать QR-разложение, которое может разбить нашу матрицу A на матрицы Q и R.

Пройдя через весь процесс вывода, можно найти коэффициенты, используя матрицы Q и R, используя эту формулу.

Этот подход по-прежнему включает обращение матрицы, но в данном случае только на более простой матрице R.

В Rust и ndarray, и nalgebra также предоставляют несколько методов разложения матриц. Вот как мы разложим матрицу и решим уравнение в Rust с помощью алгебры.

let qr = a.qr();
let (q, r) = (qr.q().transpose(), qr.r());        
let x = r.try_inverse().unwrap() * &q * &b;
let coeff = x.rows(0, 13);
let intercept = x[(13, 0)];
println!("coeff: {}, intercept: {}", coeff, intercept);

У нас есть те же значения для коэффициентов и снова точка пересечения! Теперь, когда у нас есть параметры линейного регрессора, как мы можем оценить цены на жилье? Мы просто умножаем наши оценки параметров β hat на X!

let y_hat = (x_test * &coeff).add_scalar(intercept);
println!("mae: {}", mean_absolute_error(&y_test, &y_hat.transpose()));
mae: 3.1081864959381837

Средняя абсолютная ошибка очень близка к ошибке, вычисленной нами в Python. Опять же, причина, по которой у нас нет того же числа, заключается в том, что наше разделение на поезд / тест в Rust не такое же, как в Python.

Заключение

Тот факт, что мы смогли подогнать линейный регрессор к набору данных Boston Housing на Rust, примечателен. Что делает наш результат еще лучше, так это то, что наш алгоритм достиг той же производительности, что и реализация Scikit Learn, и нам пришлось написать очень мало кода для оценки параметров нашей модели. Большую часть работы выполняет один из ящиков линейной алгебры, предоставляемый экосистемой Rust. Хорошей новостью является то, что многие другие базовые алгоритмы машинного обучения полагаются на процедуры линейной алгебры для оценки параметров. Вы будете использовать тот же набор инструментов, если хотите реализовать в Rust такие алгоритмы, как PCA, SVD и Ridge Regression.

В моем следующем посте, посвященном машинному обучению в Rust, я покажу вам некоторые дополнительные инструменты, которые имеют решающее значение, если вы хотите подогнать статистические модели к своим данным. Будьте на связи!

Источники