Rustを用いたテスト駆動開発でパーセプトロン実装する

2022-10-26

イントロダクション

今季のアニメは豊作です。どれを見ても面白いという異常事態です。虚無を感じるようなクソアニメ、抑揚の無いクソアニメが今のところなさそうです。 あとAmazon Prime Videoに子供が生まれてから見に行けなかったけど見たかった映画が入ってきていい感じですね。 劇場版 艦これも入ってましたね。 アニメはなぜか如月を沈めて後半パートで祭りをするという落差が当時話題になりましたね。

今日は、テスト駆動本を一通り読み切ったので入門していきます。 題材としてはパーセプトロン実装をやっていきます。

パーセプトロン

1950年代にローゼンブラットという方が考案したアルゴリズムです。このアルゴリズムが近年流行っているニューラルネットワークの元になっています。

パーセプトロンを数式で表すとこのようになります。

$$ f(x)=\begin{cases} 0 & (w_1x_1 + w_2x_2 <= \theta) \\ 1 & (w_1x_1 + w_2x_2 > \theta) \end{cases} $$

$\theta:閾値$

入力$x_1$や$x_2$に対して重み$w_1$や$w_2$を乗算して上げその総和の値で出力を$0$や$1$を切り替えます。

院生の頃にゲーム情報学という授業の課題で作成した朧気な記憶をもとに書いていきます。 テストコード各練習にはちょうど良い量ですしね…。

まずはこれに使うAND,OR,NAND回路を模擬したプログラムを書いていきます。

AND

先にコード書いてしまうのはテスト駆動的には誤りのようですが…とりあえず

fn and(x1: f64,x2: f64)->u32{
    let w1: f64=0.5;
    let w2: f64=0.5;
    let theta:f64 = 0.7;
    let tmp  = x1*w1 + x2*w2;
    if tmp <= theta{
        return 0;   
    }else{
        return 1;
    }
}

NAND

AND回路の閾値を反転させたらNANDになります。

fn nand(x1: f64,x2: f64)->u32{
    let w1: f64=-0.5;
    let w2: f64=-0.5;
    let theta:f64 = -0.7;
    let tmp  = x1*w1 + x2*w2;
    if tmp <= theta{
        return 0;   
    }else{
        return 1;
    }
}

RustによるUnit test

テスト駆動開発という本を読み終えて実際に試してみないとわからんなという部分もあったのでそれを実践してみようと思い立ちました。 本を買って積み上げる生活が長くなってきたので消化していこうと思います。

間違えたテスト結果を得る

まずは間違えたテストを書いてみるとFAILEDが得られます。 assert_eq!の引数にて、右と左が一致するとテストがOKとなります。

#[test]
    fn test_func_and() {
        assert_eq!(and(0.0,0.0), 0);
        assert_eq!(and(1.0,0.0), 0);
        assert_eq!(and(0.0,1.0), 0);
        assert_eq!(and(1.0,1.0), 0);
    }


#[test]
    fn test_func_nand() {
        assert_eq!(nand(0.0,0.0), 0);
        assert_eq!(nand(1.0,0.0), 0);
        assert_eq!(nand(0.0,1.0), 0);
        assert_eq!(nand(1.0,1.0), 0);
    }

実行はこうします。

cargo test

実施するとこのような結果が得られます。

   Compiling DeepZeroLib v0.1.0 (D:\src\Rust\DeepZero\DeepZeroLib)
warning: crate `DeepZeroLib` should have a snake case name
  |
  = note: `#[warn(non_snake_case)]` on by default
  = help: convert the identifier to snake case: `deep_zero_lib`

warning: `DeepZeroLib` (bin "DeepZeroLib" test) generated 1 warning
    Finished test [unoptimized + debuginfo] target(s) in 0.27s
     Running unittests src\main.rs (target\debug\deps\DeepZeroLib-23e46e3a4c248662.exe)

running 2 tests
test test_func_nand ... FAILED
test test_func_and ... FAILED

failures:

---- test_func_nand stdout ----
thread 'test_func_nand' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `0`', src\main.rs:43:9

---- test_func_and stdout ----
thread 'test_func_and' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `0`', src\main.rs:37:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

正しいテスト結果を得る

出力結果を正しい値に設定します。 ANDとNANDの真理値表と同じものになるように設定します。

#[test]
    fn test_func_and() {
        assert_eq!(and(0.0,0.0), 0);
        assert_eq!(and(1.0,0.0), 0);
        assert_eq!(and(0.0,1.0), 0);
        assert_eq!(and(1.0,1.0), 1);
    }


#[test]
    fn test_func_nand() {
        assert_eq!(nand(0.0,0.0), 1);
        assert_eq!(nand(1.0,0.0), 1);
        assert_eq!(nand(0.0,1.0), 1);
        assert_eq!(nand(1.0,1.0), 0);
    }
cargo test
running 2 tests
test test_func_and ... ok
test test_func_nand ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

OKとなりGreenバーが点灯します。この状態をキーブするようにテストを追加して関数を作って、テストと実コードを修正し続けて開発するようなイメージです。

OR回路を実装する

まずはテストコードを書きましょう。ORゲートの真理値表に合わせたテストを書いてみます。

#[test]
    fn test_func_or() {
        assert_eq!(or(0.0,0.0), 0);
        assert_eq!(or(1.0,0.0), 1);
        assert_eq!(or(0.0,1.0), 1);
        assert_eq!(or(1.0,1.0), 1);
    }

NANDゲートをコピペして実行します。

fn or(x1: f64,x2: f64)->u32{
    let w1: f64=0.5;
    let w2: f64=0.5;
    let theta:f64 = 0.5;
    let tmp  = x1*w1 + x2*w2;
    if tmp <= theta{
        return 0;   
    }else{
        return 1;
    }
}

実行して、FAILEDになることを確認できました。

cargo test
warning: crate `DeepZeroLib` should have a snake case name
  |
  = note: `#[warn(non_snake_case)]` on by default
  = help: convert the identifier to snake case: `deep_zero_lib`

warning: `DeepZeroLib` (bin "DeepZeroLib" test) generated 1 warning
    Finished test [unoptimized + debuginfo] target(s) in 0.02s
     Running unittests src\main.rs (target\debug\deps\DeepZeroLib-23e46e3a4c248662.exe)

running 3 tests
test test_func_and ... ok
test test_func_nand ... ok
test test_func_or ... FAILED

failures:

パラメータを調整してORゲートを実現します。 Thetaの値は、w1とw2の値より小さくなる正の値を設定することで実現できます。

    let w1: f64=0.5;
    let w2: f64=0.5;
    let theta:f64 = 0.4;

まとめ

実際に副業で営業かけて実装して納品してみてわかったのですが、納品物の成果物の作り込みをどこまでやるのかは自然言語だけだと結構ブレがあります。 理想を言えば、どんな機能を実現したいかがあってその機能が実現した状態を定義して上げる必要があります。 様々な言い方がありますが製造現場だと良品条件と呼ばれていたりするようです。

抵抗などの電子部品なら基準値の±n%以内の誤差などと決まっています。 ソフトウェア開発でも似たような話で、必要な要件を並べてその要件ごとに何を行えば満たしていると言えるのかという評価内容を事前に決めて置くとスムーズなのではないかとしみじみ感じます。

相手がごちゃごちゃ言うて来たとしても「おいおい契約の範囲はここからここまで。ちゃんと事前に合意した要件は満たしてるじゃないっすか甘えてんすか??」とコチラも強気に出れるというものです。

こういう作り込みのところが決まってないと、不具合が多いときやスケジュールから遅れている開発の場合結構な割合で揉める印象です。 反省しかないのですが、予めこういうテストなり達成条件みたいなものは決めて行きたいですね。