[機械学習] SmartCoreでペンギンの分類をやってみる[Rust]

2021.07.27

Introduction

機械学習フレームワークといえば、Tensorflowやscikit-learnが有名ですが、
RustでもlinfaSmartCoreといった機械学習フレームワークが存在します。

どちらもアクティブに開発されているフレームワークですが、
本稿では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もさわってみようかと思います。

References