2018年1月14日日曜日

rustでndarrayを利用して行列を扱う方法


背景

rustとは、速度、安全性、並列性を重視して作られたプログラミング言語です。
ndarrayとは多次元配列を扱うためのライブラリです。

ndarrayで行列を扱うにあたり、データの作成や扱い方に戸惑ったので、備忘録としてメモを残します。

全体像

  1. 使ったもの
  2. 行列を作成
  3. ベクトルから行列を作成
  4. 行列からベクトルを作成
  5. 行列の一部を抽出
  6. 行列の転置
  7. 行列+行列
  8. 行列-行列
  9. 行列×行列
  10. 行列○行列
  11. 列の置換
  12. 行列を関数の引数として渡す
  13. まとめ

使ったもの

rustのをインストールしたPC

rustのインストールページに従ってインストールしました。

動作環境はこちらです。
ubuntu: 17.10
rustc: rustc 1.22.1 (05e2e1c41 2017-11-22)

ndarrayを呼び出せるプロジェクト

実行可能なプロジェクト(下記のコマンドだとrust_ndarray_practice)をつくり、それの依存としてndarrayを設定しました。

プロジェクトの作成
cargo new rust_ndarray_practice --bin

依存関係の記述
Cargo.tmol
[dependencies]
ndarray = "0.11"

プロジェクトのディレクトリ内で下記のコマンドを実行すると、プログラムを動かせます。
cargo run


行列を作成

2次元の行列はarr2で作れます。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix = arr2(&[[1, 2, 3],
                        [4, 5, 6],
                        [7, 8, 9]]);
    println!("{:?}", matrix);
}

実行するとこうなります。
[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

実行するとこうなります。
Are they same?: true

ベクトルから行列を作成

from_vecinto_shapeを利用します。
extern crate ndarray;
use ndarray::{Array, arr2};

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let vector = vec!(1, 2, 3, 4, 5, 6, 7, 8, 9);
    let matrix2 = Array::from_vec(vector).into_shape((3,3)).unwrap();
    let result = matrix1 == matrix2;
    println!("Are they same?: {:?}", result);
}

行列からベクトルを作成

shape, into_shape, to_vecを利用します。
to_vecは1次元の行列からしか呼べ無さそうなので、shapeで形を取得し、要素の長さの1次元行列に変換した後、to_vecを読んでいます。
shapeを使わずにvecに変換する方法をご存じでしたら、教えてもらえると嬉しいです。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let vector1 = vec!(1, 2, 3, 4, 5, 6, 7, 8, 9);
    let (w, h) = {
        let shape = matrix.shape();
        (shape[1], shape[0])
    };
    let vector2 = matrix.into_shape(h*w).unwrap().to_vec();
    let result = vector1 == vector2;
    println!("Are they same?: {:?}", result);
}

実行するとこうなります。
Are they same?: true

行列の一部を抽出

slices!を使うと、抽出できます。
s!はsliceの範囲を表現するマクロです。
開始と終わり+1を..でつなげたレンジを、次元と同じ数与えます。
#[macro_use(s)]
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[2, 3],
                         [5, 6]]);
    let matrix3 = matrix1.slice(s![0..2, 1..3]);
    let result = matrix2 == matrix3;
    println!("Are they same?: {:?}", result);
}

実行するとこうなります。
Are they same?: true

行列の転置

転置(transpose)とは、行と列を入れ替える操作です。
reversed_axesという関数を利用すると、行列を転置できます。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix = arr2(&[[1, 2, 3],
                        [4, 5, 6],
                        [7, 8, 9]]);
    let result = matrix.reversed_axes();
    println!("{:?}", result);
}

実行するとこうなります。
[[1, 4, 7],
 [2, 5, 8],
 [3, 6, 9]] shape=[3, 3], strides=[1, 3], layout=F (0x2)

行列+行列

行列の足し算はaddでできます。
std::ops::Addをuseとして記述する必要がありました。
extern crate ndarray;
use ndarray::{arr2};
use std::ops::Add;

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[1, 0, 0],
                         [0, 1, 0],
                         [0, 0, 2]]);
    let result = matrix1.add(&matrix2);
    println!("{:?}", result);
}

実行するとこうなります。
[[2, 2, 3],
 [4, 6, 6],
 [7, 8, 11]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

行列-行列

行列の引き算はsubでできます。
std::ops::Subをuseとして記述する必要がありました。
extern crate ndarray;
use ndarray::{arr2};
use std::ops::Sub;

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[1, 0, 0],
                         [0, 1, 0],
                         [0, 0, 2]]);
    let result = matrix1.sub(&matrix2);
    println!("{:?}", result);
}

実行するとこうなります。
[[0, 2, 3],
 [4, 4, 6],
 [7, 8, 7]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

行列×行列

行列の掛け算はdotでできます。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[1, 0, 0],
                         [0, 1, 0],
                         [0, 0, 2]]);
    let result = matrix1.dot(&matrix2);
    println!("{:?}", result);
}

実行するとこうなります。
[[1, 2, 6],
 [4, 5, 12],
 [7, 8, 18]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

行列○行列

行列の要素同士の掛け算はmulでできます。
std::ops::Mulをuseとして記述する必要がありました。
extern crate ndarray;
use ndarray::{arr2};
use std::ops::Mul;

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[1, 0, 0],
                         [0, 1, 0],
                         [0, 0, 2]]);
    let result = matrix1.mul(&matrix2);
    println!("{:?}", result);
}

実行するとこうなります。
[[1, 0, 0],
 [0, 5, 0],
 [0, 0, 18]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

列の置換

ndarrayには行や列の置換(permute、replace)を行ってくれる関数は無さそうでした。
しかし、下記のどちらかの処理で行を置き換えられることが分かりました。

入れ替えたい列の要素を1つずつswapで入れ替える。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let mut matrix = arr2(&[[1, 2, 3],
                            [4, 5, 6],
                            [7, 8, 9]]);
    matrix.swap(s![0, 0..3], s![2, 0..3]);
    for i in 0..2 {
        matrix.swap((i, 0), (i, 2));
    }
    println!("{:?}", matrix);
}

置換のための行列を掛け合わせる。
extern crate ndarray;
use ndarray::{arr2};

fn main() {
    let matrix1 = arr2(&[[1, 2, 3],
                         [4, 5, 6],
                         [7, 8, 9]]);
    let matrix2 = arr2(&[[0, 0, 1],
                         [0, 1, 0],
                         [1, 0, 0]]);
    let result = matrix1.dot(&matrix2);
    println!("{:?}", result);
}

実行するとこうなります。
[[3, 2, 1],
 [6, 5, 4],
 [9, 8, 7]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

行列を関数の引数として渡す

Array2に要素の型を記述したものが、2次元行列の型になります。
0~255の値を使っているので、今回はu8型にしました。
extern crate ndarray;
use ndarray::{arr2, Array2};

fn truncate_and_replace(matrix1: &Array2) -> Array2 {
    let matrix1 = matrix1.clone();
    let matrix2 = arr2(&[[0, 0, 1],
                         [0, 1, 0],
                         [1, 0, 0]]);
    matrix1.reversed_axes().dot(&matrix2)
}

fn main() {
    let matrix = arr2(&[[1, 2, 3],
                        [4, 5, 6],
                        [7, 8, 9]]);
    let result = truncate_and_replace(&matrix);
    println!("{:?}", result);
}

実行するとこうなります。
[[7, 4, 1],
 [8, 5, 2],
 [9, 6, 3]] shape=[3, 3], strides=[3, 1], layout=C (0x1)

まとめ

ndarrayを利用して、rustで行列の操作ができました。

行列の基本変形としてwikipediaでも紹介されている行や列の交換に関する操作が、関数の機能として見つけられなかったのが疑問です。
該当する機能を知っている方が居れば、共有してもらえると嬉しいです。

ndarrayの他にもnalgebraという行列を操作に関するライブラリあるので試してみましたが、次元の他に行列のサイズも型に含まれるため、型による束縛が強いと個人的に感じました。
そのため、ndarrayの使い方だけ備忘録としてまとめてみました。

何かの参考になれば嬉しいです。

参考

概要理解の参考にしました。
Rustによる数値計算: 線形代数編

ndarrayのドキュメントです。
Crate ndarray

ndarrayのサンプルプログラムです。
rust-ndarray/examples

今回の記事の内容は advent of code 2017 day21 を解くのに利用しました。
自分の回答です。
ndarray版
nalgebra版

更新情報

2018.02.11
下記の項目を追加しました。
  • ベクトルから行列を作成
  • 行列からベクトルを作成
  • 行列の一部を抽出
  • 行列+行列
  • 行列-行列
  • 行列○行列
swapを使った列の置換方法を追加しました。

0 件のコメント :