この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
Introduction
機械学習フレームワークといえば、Tensorflowやscikit-learnが有名ですが、
RustでもlinfaやSmartCoreといった機械学習フレームワークが存在します。
どちらもアクティブに開発されているフレームワークですが、
本稿ではSmartCoreをつかって機械学習してみましょう。
What's my objective?
SmartCoreをつかって機械学習モデルを構築してみます。
対象となるデータですが、今回は「アヤメ(iris)の分類に飽きたらコレ」といわれる、
ペンギンの分類を行います。
これは南極のパーマー群島に住む数種類のペンギン、
- アデリーペンギン(Adelie Penguin)
- ヒゲペンギン(Chinstrap Penguin)
- ジェンツーペンギン(Gentoo Penguin)
について、くちばしやヒレのサイズからペンギンの種類を推論させます。
また、データフレームライブラリにはPolarsを使います。
SmartCore?
SmartCoreはRustの機械学習フレームワークです。
線形代数やらオプティマイザがひととおり用意されており、
いろいろな機械学習アルゴリズムをサポートしています。
ここでもいわれているように、
将来的にRustでMLするときの標準になりそうなフレームワークみたいです。
私としては「sklearn意識度が高い」ってところが良いです。
Polars?
Apache Arrowsをベースにしたデータフレームライブラリです。
PythonとRustで提供されているみたいです。
PythonでデータフレームライブラリといえばPandasですが、
Rustではこれくらいしかない?
参考:
Rustのデータフレームcrateのpolarsとpandasの比較
Environment
- OS : MacOS 10.15.7
- Rust : 1.52.1
Create Rust Example
CargoでProject作成
Cargoでプロジェクトの雛形を作成します。
% cargo new smartcore-example
対象となるペンギンのデータをKaggleからダウンロードします。
ファイルを解凍してpenguins_size.csvをさきほど作成したプロジェクトに移動させておきましょう。
ここのcsvファイルを加工してトレーニングデータとテストデータを作成します。
ちなみにcsvファイル(penguins_size.csv)の内容は↓のようになってます。
species | island | culmen_length_mm | culmen_depth_mm | flipper_length_mm | body_mass_g | sex |
---|---|---|---|---|---|---|
Adelie | Torgersen | 39.1 | 18.7 | 181 | 3750 | MALE |
Adelie | Torgersen | 39.5 | 17.4 | 186 | 3800 | FEMALE |
Adelie | Torgersen | 40.3 | 18 | 195 | 3250 | FEMALE |
Chinstrap | Dream | 50.8 | 19 | 210 | 4100 | MALE |
Chinstrap | Dream | 50.2 | 18.7 | 198 | 3775 | FEMALE |
Gentoo | Biscoe | 46.1 | 13.2 | 211 | 4500 | FEMALE |
Gentoo | Biscoe | 50 | 16.3 | 230 | 5700 | MALE |
polarsとSmartCoreのCrateをCargo.tomlに設定。
・・・
[dependencies]
polars = "0.14.7"
polars-core = {version = "0.14.7", features=["ndarray"]}
smartcore = { version = "0.2.0", default-features = false, features=["nalgebra-bindings", "ndarray-bindings", "datasets"]}
CSVファイルのread
Rustのコードを書いていきます。
まずはcsvファイルをreadします。
下の関数を定義してCSVファイルをDataFrameとして読み込みます。
※ソースコード全文はgistに記載
//CSVファイルを読み込んでDataFrameを返す
fn read_csv_with_schema<P: AsRef<Path>>(path: P) -> PolarResult<DataFrame> {
let schema = Schema::new(vec![
Field::new("species", DataType::Utf8),
Field::new("island", DataType::Utf8),
Field::new("culmen_length_mm", DataType::Float64),
Field::new("culmen_depth_mm", DataType::Float64),
Field::new("flipper_length_mm", DataType::Float64),
Field::new("body_mass_g", DataType::Float64),
Field::new("sex", DataType::Utf8)
]);
let file = File::open(path).expect("Cannot open file.");
CsvReader::new(file)
.with_schema(Arc::new(schema))
.has_header(true)
.with_ignore_parser_errors(true) //エラーが出ても処理継続
.finish()
}
Schemaを定義することで、任意の型で各Seriesを定義することがきます。
また、対象のCSVには不正な値を持つデータが存在するので、
with_ignore_parser_errorsを設定して、エラー無視でloadします。
不正なデータを削除
drop_nullsを使えばnullを含むデータを削除できます。
pandasでdf.dropna(how='any')みたいにしてる感じです。
//不正データdrop
let df: DataFrame = ・・・
let df2 = df.drop_nulls(None).unwrap();
DataFrameをfeatureとtargetに分割
DataFrameからselectで必要なSeriesを取得し、タプルで返します。
//featureとtargetに分割
fn get_feature_target(df: &DataFrame) -> (PolarResult<DataFrame>, PolarResult<DataFrame>) {
let features = df.select(vec![
"culmen_length_mm",
"culmen_depth_mm",
"flipper_length_mm",
"body_mass_g",
]);
let target = df.select("species");
(features, target)
}
features変換
polarsのDataFrameをSmartCoreのDenseMatrixに変換します。
ほとんどここにある内容まんまです。
pub fn convert_features_to_matrix(df: &DataFrame) -> Result<DenseMatrix<f64>> {
let nrows = df.height();
let ncols = df.width();
let features_res = df.to_ndarray::<Float64Type>().unwrap();
let mut xmatrix: DenseMatrix<f64> = BaseMatrix::zeros(nrows, ncols);
let mut col: u32 = 0;
let mut row: u32 = 0;
for val in features_res.iter() {
let m_row = usize::try_from(row).unwrap();
let m_col = usize::try_from(col).unwrap();
xmatrix.set(m_row, m_col, *val);
if m_col == ncols - 1 {
row += 1;
col = 0;
} else {
col += 1;
}
}
Ok(xmatrix)
}
Labelエンコーディング
speciesのペンギン名を数値にreplaceします。
(これはもっとまともな方法がある気がする)
//speciesのLabelエンコーディング用function
fn str_to_num(str_val: &Series) -> Series {
str_val
.utf8()
.unwrap()
.into_iter()
.map(|opt_name: Option<&str>| {
opt_name.map(|name: &str| {
match name {
"Adelie" => 1,
"Chinstrap" => 2,
"Gentoo" => 3,
_ => panic!("Problem species str to num"),
}
})
})
.collect::<UInt32Chunked>()
.into_series()
}
//speciesのLabelエンコーディング
let target_array = target
.unwrap()
.apply("species", str_to_num)
.unwrap()
.to_ndarray::<Float64Type>()
.unwrap();
// create a vec type and populate with y values
let mut y: Vec<f64> = Vec::new();
for val in target_array.iter() {
y.push(*val);
}
学習データとテストデータに分割
smartcoreのtrain_test_splitつかってデータシャッフル&データ分割します。
//データ分割
let (x_train, x_test, y_train, y_test) = train_test_split(&xmatrix.unwrap(), &y, 0.3, true);
学習&推論
そしてトレーニング&テストデータで推論して、
平均二乗誤差と正解率を取得しています。
//学習
let reg = LogisticRegression::fit(&x_train, &y_train, Default::default()).unwrap();
//予測
let preds = reg.predict(&x_test).unwrap();
let mse = mean_squared_error(&y_test, &preds);
println!("MSE: {}", mse);
println!("accuracy : {}", accuracy(&y_test, &preds));
runで実行してみましょう。
無事に推論できてるみたいです。
% cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/smartcore-example`
MSE: 0.00980392156862745
accuracy : 0.9901960784313726
すべてのコード
ソースコード全文はgistにあります。
Appendix : Python Example
SmartCore + Polarsで実装する前、scikit-learnで試したコード。
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Perceptron
from sklearn.metrics import accuracy_score
import pandas as pd
import numpy as np
# read csv
data = pd.read_csv(f"./penguins_size.csv")
# drop NaN
data = data.dropna(how='any')
# replace str to num
target_names = {'Adelie':0,'Chinstrap':1,'Gentoo':2}
data['species'] = data['species'].map(target_names)
X = data.iloc[:,2:6]
y = data.species
# split data
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=1, stratify=y)
# scale
sc = StandardScaler()
sc.fit(X_train)
X_train_std = sc.transform(X_train)
X_test_std = sc.transform(X_test)
# fit & predicate
ppn = Perceptron(eta0=0.1, random_state=1)
ppn.fit(X_train_std, y_train)
y_pred = ppn.predict(X_test_std)
print('Misclassified examples: %d' % (y_test != y_pred).sum())
print('Accuracy: %.3f' % accuracy_score(y_test, y_pred))
Summary
今回はRustで機械学習ということで、SmartCoreをつかってペンギン分類をおこなってみました。
たしかにSmartCoreはscikit-learnライクでなんとなく使い方はわかる感じです。
それより、Polarを使ったデータ前処理がけっこう手間取ってしまい、
Python+Pandasの手軽さを感じました。
(なれればもっと使いやすく感じるかも?)
次はlinfaもさわってみようかと思います。