Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

外部クレートの活用(ndarray入門)

Note

本節のポイント

  • Rustの科学技術計算における標準的で軽量な配列クレートndarrayの基本を学ぶ。
  • ndarrayを用いて、ベクトルや行列といった多次元配列を効率的に作成・操作する方法を習得する。
  • ndarray-linalgが、LAPACK系の線形代数をndarrayに接続するcrateであることを確認する。

本節は、前節「1次元データから多次元データへ」と「多次元配列とメモリレイアウト」で学んだ内容を土台としています。

前の節までで、Rustの標準ライブラリが提供する配列、スライス、Vec<T>、そして多次元データのmemory layoutを確認しました。これらはデータ管理の基本として有用ですが、より高度な科学技術計算、特に多次元配列(ベクトル、行列、テンソル)を多用する線形代数演算には力不足です。

例えば、Vec<Vec<f64>>で「行列」を表現しようとすると、以下のような課題に直面します。

  • メモリ効率: 各行が独立したヒープ割り当てとなり、データがメモリ上で連続的に配置されないため、キャッシュ効率が悪化します。
  • 演算の煩雑さ: 行列の積や転置といった基本的な演算も、手動でループを実装する必要があり、コードが冗長かつエラーが発生しやすくなります。
  • パフォーマンス: 最適化された線形代数ライブラリ(BLASなど)の恩恵を受けにくく、手実装のループでは高いパフォーマンスを得るのが困難です。

このような課題を解決する基本的な選択肢が、ndarray クレートです。 ndarrayは、RustでN次元配列、array view、多次元slicing、効率的な演算を扱うための 標準的で軽量なcrateです。

さらに、固有値分解、特異値分解、線形方程式の求解などの既存LAPACK系の線形代数を ndarray と接続したい場合は、ndarray-linalg が候補になります。 これは ndarrayArrayBase に線形代数機能を提供し、 外部のLAPACK実装を利用するcrateです。

本節では、まずndarrayの基本的な使い方を学び、 必要に応じてndarray-linalgへ進む位置づけを確認します。

ndarrayのセットアップ

まず、ndarrayをプロジェクトに追加します。Cargo.tomlファイルの[dependencies]セクションに以下の行を追記してください。

[dependencies]
ndarray = "0.17"

バージョンは執筆時点のものです。最新版はcrates.ioで確認できます。

ndarray-linalgを使う場合は、LAPACK backendも選ぶ必要があります。 OpenBLAS、Netlib、Intel MKLなどのbackend featureから1つを選ぶ形です。 教材では詳細なbackend構築手順には踏み込みません。 必要になった時点で、公式documentationを確認して、使用環境に合うbackendを選びます。

本書のコード例では、個別の型、関数、traitをuse宣言で明示的にインポートします。 これにより、どの機能がどのcrateから来ているかを追いやすくします。

use ndarray::{arr1, arr2, Array, Array1, Array2, Axis};

1次元配列 (Array1):ベクトルの表現

ndarrayにおける1次元配列Array1<T>は、数学的なベクトルに対応します。

ベクトルの作成

ndarrayには、ベクトルを生成するための多様な方法が用意されています。

use ndarray::{arr1, Array, Array1};

fn main() {
    // スライスからベクトルを作成
    let a: Array1<f64> = Array::from(vec![1.0, 2.0, 3.0]);
    println!("a = {}", a);

    // arr1 関数を使った、より簡潔な作成方法
    let b = arr1(&[4.0, 5.0, 6.0]);
    println!("b = {}", b);

    // 0から9までの連番を持つベクトルを作成
    let c: Array1<f64> = Array::range(0.0, 10.0, 1.0);
    println!("c = {}", c);

    // すべての要素が0.0のベクトルを作成(次元を指定)
    let zeros = Array1::<f64>::zeros(5);
    println!("zeros = {}", zeros);
}

ベクトル演算

ndarrayの最大の利点の一つは、ベクトルや行列に対する数学的な演算が直感的に書ける点です。+, -, *, / といった演算子がオーバーロードされており、要素ごとの演算が可能です。

use ndarray::arr1;

fn main() {
    let a = arr1(&[1.0, 2.0, 3.0]);
    let b = arr1(&[4.0, 5.0, 6.0]);

    // ベクトルの加算
    println!("a + b = {}", &a + &b);

    // ベクトルの減算
    println!("a - b = {}", &a - &b);

    // スカラー倍
    println!("a * 2.0 = {}", &a * 2.0);

    // 要素ごとの積
    println!("a * b (element-wise) = {}", &a * &b);

    // 内積 (dot product)
    let dot_product = a.dot(&b);
    println!("a . b = {}", dot_product);
}

Note

参照と所有権 &a + &bのように、演算時にベクトルの参照(&)を渡している点に注目してください。ndarrayの多くの二項演算は参照を受け取るように実装されています。これにより、演算後も元のデータabの所有権が維持され、続けて使用することができます。もしa + bと記述すると、aの所有権が移動してしまい、以降aは使えなくなります。

2次元配列 (Array2):行列の表現

ndarrayにおける2次元配列Array2<T>は、行列に対応します。

行列の作成

行列もベクトルと同様、様々な方法で作成できます。 ndarray のowned arrayは、標準では row-major、つまり同じ行の隣接要素が メモリ上で連続する配置です。これはC-orderとも呼ばれます。 Array2::from_shape_vec((rows, cols), data) に渡す data も、この順序で解釈されます。

use ndarray::{arr2, Array2};

fn main() {
    // arr2 関数で行列を作成
    // 各内部配列が「行」に対応する
    let m = arr2(&[[1.0, 2.0, 3.0],
                   [4.0, 5.0, 6.0]]);
    println!("m =\n{}", m);

    // from_shape_vecで行列を作成
    // (行数, 列数)のタプルと、データを格納したVecを渡す
    let shape = (3, 2);
    let data = vec![1, 2, 3, 4, 5, 6];
    let m_from_vec = Array2::from_shape_vec(shape, data)
        .expect("Incompatible shape");
    println!("m_from_vec =\n{}", m_from_vec);

    // 3x3のゼロ行列
    let zeros = Array2::<f64>::zeros((3, 3));
    println!("zeros =\n{}", zeros);

    // 2x2の単位行列
    let eye = Array2::<f64>::eye(2);
    println!("eye =\n{}", eye);
}

Note

layoutとstride

ndarray では、配列がshapeとstrideを持ちます。 owned arrayを普通に作る場合は row-major と考えてよいですが、 slice、transpose、view ではstrideが変わります。 性能や外部ライブラリとの接続が重要な場合は、shapeだけでなくstrideも確認します。

行列演算

行列同士の加減算やスカラー倍もベクトルと同様に直感的に記述できます。そして、dotメソッドを用いることで、行列積や行列とベクトルの積を計算できます。

use ndarray::{arr1, arr2};

fn main() {
    let m1 = arr2(&[[1.0, 2.0],
                    [3.0, 4.0]]);

    let m2 = arr2(&[[5.0, 6.0],
                    [7.0, 8.0]]);

    // 行列の加算
    println!("m1 + m2 =\n{}", &m1 + &m2);

    // 行列のスカラー倍
    println!("m1 * 3.0 =\n{}", &m1 * 3.0);

    // 行列積
    let matrix_product = m1.dot(&m2);
    println!("m1 * m2 (matrix product) =\n{}", matrix_product);

    // 行列とベクトルの積
    let v = arr1(&[10.0, 20.0]);
    let matrix_vector_product = m1.dot(&v);
    println!("m1 * v =\n{}", matrix_vector_product);
}

ndarray 自体でも、基本的な行列積や要素ごとの演算を扱えます。 一方、分解、固有値、特異値分解、線形方程式の求解など、 LAPACK系の機能が必要な場合は ndarray-linalg を使います。 ndarray-linalg はbackend featureと外部libraryの設定に依存するため、 小さい演習で不用意に必須化しません。

スライシング

ndarrayの特に強力な機能がスライシングです。これにより、配列の一部を効率的に、かつ柔軟に抜き出すことができます。スライシングはNumPyのそれに非常によく似ており、s![]マクロを用いて行います。

use ndarray::{arr2, s};

fn main() {
    let m = arr2(&[[ 1,  2,  3,  4],
                   [ 5,  6,  7,  8],
                   [ 9, 10, 11, 12]]);
    println!("m =\n{}", m);

    // 0番目の行を取得
    let row0 = m.slice(s![0, ..]);
    println!("Row 0: {}", row0); // [1, 2, 3, 4]

    // 1番目の列を取得
    let col1 = m.slice(s![.., 1]);
    println!("Column 1: {}", col1); // [2, 6, 10]

    // 部分行列を取得
    // 0-1行目、1-2列目を抜き出す
    let sub_matrix = m.slice(s![0..2, 1..3]);
    println!("Sub-matrix (0..2, 1..3) =\n{}", sub_matrix);
    // [[2, 3],
    //  [6, 7]]

    // スライスは元のデータのビュー(参照)
    // スライスを介して元のデータを変更することも可能
    let mut m_mut = m.clone();
    let mut sub_view = m_mut.slice_mut(s![0..2, 1..3]);
    sub_view.fill(0); // 部分行列を0で埋める
    println!("Modified m_mut =\n{}", m_mut);
}

Important

スライスはビューである

slice()メソッドが返すArrayView型は、元の配列データの所有権を持たない「ビュー(参照)」です。これにより、巨大な行列の一部を操作する際に、不要なデータコピーを回避し、高いメモリ効率とパフォーマンスを実現します。

ブロードキャスト

ブロードキャストは、形状(shape)が異なる配列間の演算を可能にする強力な機能です。ndarrayは、不足している次元を自動的に「引き伸ばし」て、形状を一致させてから演算を実行します。

例えば、行列のすべての要素に同じ値を加算する、あるいは行列の各行に同じベクトルを加算するといった操作が簡単に行えます。

use ndarray::{arr1, arr2, Axis};

fn main() {
    let m = arr2(&[[1.0, 2.0],
                   [3.0, 4.0]]);

    let v = arr1(&[10.0, 20.0]);

    // 行列の各行にベクトルvを加算する
    // vは (2,) -> (1, 2) にブロードキャストされ、
    // それがさらに (2, 2) に引き伸ばされてmと加算される
    let result_row = &m + &v;
    println!("m + v (row-wise broadcast) =\n{}", result_row);

    // 行列の各列にベクトルを加算する場合
    // ベクトルを列ベクトルとして扱うために次元を追加する必要がある
    // v.view().insert_axis(Axis(1)) は vの形状を (2,) から (2, 1) に変える
    // view() を使うことで v の所有権を保持したままビューを操作できる
    let result_col = &m + &v.view().insert_axis(Axis(1));
    println!("m + v (column-wise broadcast) =\n{}", result_col);
}

実行結果:

m + v (row-wise broadcast) =
[[11, 22],
 [13, 24]]
m + v (column-wise broadcast) =
[[11, 12],
 [23, 24]]

ndarrayとRust標準型の連携

ndarrayは標準ライブラリの型とスムーズに連携できます。

  • ArrayからVecへ: to_vec()
  • Arrayからスライスへ: as_slice()
  • スライスからArrayViewへ: ArrayView::from()

これにより、既存のRustコードにndarrayを段階的に導入したり、ndarrayで計算した結果を他のライブラリに渡したりすることが容易になります。

その他の高度な機能

ndarrayは本節で紹介した以外にも、以下のような多数の機能を提供します。

  • 高階関数: map, fold, zipなど、配列を効率的に操作するメソッド。
  • 軸に沿った操作: sum, mean, max, minなどを特定の軸(行方向や列方向)に沿って計算。
  • LAPACK系の線形代数: ndarray-linalgクレートと組み合わせることで、分解、固有値問題、特異値分解(SVD)、線形方程式の求解などを扱えます。

まとめ

本節では、Rustの科学技術計算でよく使われる軽量な配列クレートndarrayの基本的な使い方を学びました。

  • Array1(ベクトル)とArray2(行列)の作成方法と、それらの間の基本的な演算(四則演算、内積、行列積)を習得しました。
  • s![]マクロを用いた柔軟なスライシング機能により、配列の一部を効率的に参照・操作できることを見ました。
  • ブロードキャスト機能により、形状の異なる配列間でも直感的な演算が可能であることを学びました。

ndarrayは、Rustで多次元配列を扱う標準的な軽量crateです。 LAPACK系の線形代数が必要な場合は ndarray-linalg も確認します。

参照:

Last change: , commit: d65f323