🐖

Rustを始めました

に公開2

Rustを始めた

この春からRustを始めました。(まだ一月経ってないかな)
「そのうち技術ブログなるものを書こう」と思っていて、「そのうち」っていつくるんだろう?って感じだったのですが、「技術ブログは自分用の公開メモだ」のような言説を見かけて、「ならば」と思い、書き残すことにしました。

あとはZenn見ているとすごい記事ばっかりで「私のようなものの記事など…」と思ってしまうので、「他の入門者のためにハードルを下げよう!」というのもあり。


Rust初心者のRustへの雑感

私はほぼほぼPHPしか経験がないので、Rustのあれこれを学ぶのがとても楽しかったです。

所有権とか参照とかも、オライリーの『プログラミングRust』を読むと
「なるほどね。これはスタックでこっちはヒープで。ふーん」
という感じで、特に問題なく読み進めました。

「要するに! どこに値が置いてあって、それをどこから参照しているかを管理してるってことでしょ? なんだ。そんなに難しくないじゃん!」

…すいませんでした。

実際にコードを書いてみると、エラーの山でした…。

でもエラー表示がかなり丁寧で、大体はエラーに教わり、なんとか進めることができました。
(本当はこの辺りの試行錯誤を残しておくべきだったんでしょうが…。思い出せることがあれば、またの機会に追記します。)


とりあえずの現状報告

まだ日が浅く、とりあえずようやく簡単なものなら所有権を扱ったり、コードを書けるようになったので、進捗として今書けるコードを置いておきます。

こちらはテスト関数。今回はmatmulとtransposeを実装したので、まずはテスト関数を書きました。

#[cfg(test)]
mod test {

    use super::*;

    #[test]
    fn transpose_test() {
        let tensor = Tensor::new(vec![0., 1., 2., 3., 4., 5.], vec![1, 2, 3, 1]);
        let tensor_transosed = tensor.transpose();
        assert_eq!(tensor_transosed.data, vec![0., 3., 1., 4., 2., 5.]);
    }

    #[test]
    fn matmul() {
        let l_tensor = Tensor::new(vec![1., 2., 3., 4., 5., 6.], vec![1, 2, 3, 1]);
        let r_tensor = Tensor::new(vec![1., 0., 0., 1., 1., 0.], vec![1, 3, 2, 1]);

        let res = l_tensor.matmul(r_tensor);
        assert_eq!(res.data, vec![4., 2., 10., 5.]);
        println!("from zero 8: {:?}", res.data);

        let l_tensor2 = Tensor::new(vec![5., 1., 2., 4.], vec![1, 2, 2, 1]);
        let r_tensor2 = Tensor::new(vec![2., 3.], vec![1, 2, 1, 1]);
        let res2 = l_tensor2.matmul(r_tensor2);
        println!("from zero 8: {:?}", res2.data);
        assert_eq!(res2.data, vec![13., 16.]);
    }
}

こちらがtransposeとmatmulです。

impl Tensor {
    fn new(data: Vec<f64>, shape: Vec<usize>) -> Tensor {
        Tensor { data, shape }
    }
    fn transpose(&self) -> Tensor {
        let h = self.shape[1];
        let w = self.shape[2];
        let mut data_transposed = vec![0. as f64; h * w];
        for i in 0.. self.data.len() {
            let reminder = i % w;
            let ans = i / w;
            data_transposed[reminder * h + ans] = self.data[i];
        } 

        Tensor::new(data_transposed, vec![1, w, h, 1])
    }

    fn matmul(&self, r_matrix: Tensor) -> Tensor {
        let r_h = r_matrix.shape[1];
        let r_w = r_matrix.shape[2];
        let r_matrix_transposed = r_matrix.transpose();
        let l_h = self.shape[1];
        let l_w = self.shape[2];

        let res: Vec<f64> = self.data.par_chunks(l_w)
            .flat_map(|l_row|{
                r_matrix_transposed.data.par_chunks(r_h)
    
                    .map(move|r_col|{
                        r_col.iter()
                            .zip(l_row)
                            .map(|(r, l)|{
                                r * l
                            }).sum::<f64>()
                    })

            }).collect();
        Tensor::new(res, vec![1, l_h, r_w, 1])
    }
}

Tensorのshapeは本来なら、batch/height/width/channelを置きたいのですが、現在はbatch,channelは決め打ちで1としています。

最初は素朴なtransposeとmatmulを書いたのですが、少しでも処理を早くしたいと思って、この形にしました。
matmulは右行列を転置したものを掛けることでキャッシュ効率を上げることができたと思います。
また、並列処理ができるといいなと思い、初クレートとして、rayonを使用しました。
このあたりの速度比較としてbenchをとってみたいと思っているのですが、そちらはまだ手付かずです。

まだわかっていないこと

エラーが親切なので、動いてはいますが、実際のところ、moveがわかっていません!
とりあえずまだ途中の本を読もうと思います。

        let res: Vec<f64> = self.data.par_chunks(l_w)
            .flat_map(|l_row|{
                //なぜこのr_matrix_transposedはmoveしなくても借用されているのに 
                r_matrix_transposed.data.par_chunks(r_h)
                    //ここではl_rowはmoveしないといけないのか・・・ 
                    .map(move|r_col|{
                        r_col.iter()
                            .zip(l_row)
                            .map(|(r, l)|{
                                r * l
                            }).sum::<f64>()
                    })

            }).collect();

今後の課題

課題はいっぱいあるんですが(transposeもmatmulもまだ最適化できるはずですし・・・)、とりあえず、優先順位としては以下の通りです。
・moveを理解したい。
・生存期間を扱うようなものを書きたい('strのものは一度書いてみたことがある。コンパイラに助けてもらいつつ)
・benchとってみたい
・このプロジェクトをなんとか(?)形にしたい。
・技術ブログというやつを書けるようになりたい(しばらくは備忘録だけ残していきたいです)

Rustのすきなところ

最後になりますが、一ヶ月ほどRustに取り組んでみて、思うRustの好きなところを書き残しておきたいです。
私はPHPから来たので、今まで特に速度が気になるようなエッジケースを取り扱ったこともなく、「Rustのおかげでこんなことができた!」とかそういうのはないと思います。RustとPHPでは得意な分野が全然違うと思うので、比較して「こう!」っていうのがありません。

だから、Rustの機能面で語れることがほとんどなく…。
でも、Rustが楽しいなって思えるのは以下のことがあるからです。

いままで知らなかった低レベルのことを学ぶことができる。

今までは特に考えなくてよかったようなことを学ぶのはとても楽しいです。
「あ、そういうことだったのか」とRustを学ぶとアハ体験ができます。

エラー解消するの楽しい。

解消した時にドーパミンが出ます。

以上です。

とりあえず、今後もRustを続けて、備忘録を残していきたいと思います。
そのうち役にたつ記事も書きたいです。

** 公開しようと思ってカテゴリ選ぼうと思ったら、ハードル高いやん…。知見…?学習方法…?

Discussion

SiketyanSiketyan

まだわかっていないこと

コード全容が見えていないので想像を含みますが,匿名関数にキャプチャされる変数が (不変) 参照かどうかによるものだと思います.前者の r_matrix_transposed については par_chunks メソッドの呼び出しに (UFCS 表記を使って) その不変参照を使っています.従って匿名関数にキャプチャされるのも不変参照であり,これはムーブしなくてもコピーすることができます.一方で l_row はその実体を使っているため,キャプチャするときにムーブが必要になります.

nium_devnium_dev

ありがとうございます!
100パーセント理解できたかはちょっと怪しいのですが、理解が進んだと思います。
(UFCSという言葉も初めて知りました。ありがとうございます!)