🐶

第1回【C言語】「ディープラーニングと物理学」を読み始める

2024/04/20に公開

目的

ここでは「ディープラーニングと物理学」(田中章詞、富谷昭夫、橋本幸士著)を読み進める上で私が「コーディングして再現できるかなあ」「これは皆にも役立つんじゃないか?」といったことをまとめたものであり、記念すべきZenn第1回目の投稿です。はじめは次節からはじまるように25ページの式(2.1.4)について考えます。

25ページの式(2.1.4)

この節では教師あり学習を定式化することを目的としており次のような思考実験が行われます。

次のような2つのサイコロを用意しましょう。

  • サイコロAは確率1/6ですべての目がでる
  • サイコロBは確率16のみが出る

この上で次の操作を考えます。

  1. 確率1/2ABを選び、それぞれd=01でラベルする。
  2. サイコロを振り、出た目をsとする。
  3. (d, s)を記録する。

ここでデータの総数、すなわち試行回数をN_{\rm att}(d, s)の組みが現れた回数をN(x, d)とします。
この試行によりそれぞれの実現値に対する確率\hat{P}(d, s)が次のようにして計算できるでしょう:

\hat{P}(d, s) = \frac{N(d, s)}{N_{\rm att}}

これを実際にC言語で試してみました。次の節では実際のコードを示します。

C言語による再現

全体像

まずは全体像を提示しておきます。私の環境ではclang version 12.0.5でC言語をコンパイルしています。

/* 
* === TITLE: On p.24 in DEEP LEARNING AND PHYSICS ===
*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// Maximum and minimum number of dice
#define DICE_NUM_MAX 6
#define DICE_NUM_MIN 1
#define NUM_OF_ATTEMPTS 1000

// prototype declaration
void DICE_A(int *dice_num);
void DICE_A(int *dice_num);
void DOUBLE_DICE(int *d, int *s);

// main function
int main (void) { 
    int i, d, s, ds[2][DICE_NUM_MAX+1] = {0};
    int j, k; // used as index for array

    srand((unsigned)time(NULL)); // To vary a seed, we should introduce this.

    // count realizations
    for(i = 0; i <= NUM_OF_ATTEMPTS; i++) {
        DOUBLE_DICE(&d, &s);
        fprintf(fp, "%d %d\n", d, s);
        for(j = 0; j <= 1; j++) {
            for (k = DICE_NUM_MIN; k <= DICE_NUM_MAX; k++) {
                if(j == d && k == s) {
                    ds[d][s]++;
                }
            }
        }
    }
    
    double prob_ds[2][NUM_OF_ATTEMPTS+1];
    for(j = 0; j <= 1; j++) {
        for (k = DICE_NUM_MIN; k <= DICE_NUM_MAX; k++) {
            prob_ds[j][k] = ds[j][k]/(double)NUM_OF_ATTEMPTS;
        }
    }

    printf("    d=0 \t d=1\n");
    printf("-----------------------------\n");
    for(k = DICE_NUM_MIN; k <= DICE_NUM_MAX; k++) {
        printf("%d | %f \t %f\n", k ,prob_ds[0][k], prob_ds[1][k]);
    }

    return 0;
    
}

/* === On DICE_A() and DICE_B() ===
*
* DICE_A() takes numbers 1 to 6 randomly, while DICE_B() always takes a number 6.
*/ 
void DICE_A(int *dice_num) {
    /*
    * *srand(time(NULL));
    * Reason why we use time(NULL) is that it can always vary as time goas, 
    * and thus, rand() can be more randomized.
    * If you would like to know it, see e.g., https://9cguide.appspot.com/21-02.html
    */
    *dice_num = DICE_NUM_MIN +  rand() % DICE_NUM_MAX;
}

void DICE_B(int *dice_num) {
    *dice_num = 6;
}
/*-----*/

void DOUBLE_DICE(int *d, int *s) {
    /* 
    * We choose a dice A or B which we will use.
    * d = 1 -> Dice A 
    * d = 0 -> Dice B
    */ 
    *d = rand() % 2;
    int dice_num;
    if (*d == 0) {
        DICE_A(&dice_num);
        *s = dice_num;
    }else if (*d == 1){
        DICE_B(&dice_num);
        *s = dice_num;
    }else{
        printf("Something wrong.\n");
        exit(1);
    }
}

サイコロの試行回数

// Maximum and minimum number of dice
#define DICE_NUM_MAX 6
#define DICE_NUM_MIN 1
#define NUM_OF_ATTEMPTS 1000

のNUM_OF_ATTEMPTSを変化させることができます。

関数DOUBLE_DICEについて

関数void DOUBLE_DICE(*int, *int)は確率的にえられるdxを与える関数です。
これはさらに2つの関数void DICE_A(*int)とvoid DICE_B(*int)によって構成されております。
DICE_Aではランダムに1から6までの目が現れるはずなので、rand()関数でそれを表現しております。

原理ついてはまだ把握していないのですが、rand()関数は擬似乱数を生成するため完全な乱数ではありません。シード値が等しければ同じ乱数を生成してしまうため、それを回避しよりランダムなふるまいを手にいれるために、srand()を使用します。

srand((unsigned)time(NULL))の引数に対して現在時刻をいれることで毎回の計算ごとにシード値を変化させることができます。

出た目のカウント

// count realizations
    for(i = 0; i <= NUM_OF_ATTEMPTS; i++) {
        DOUBLE_DICE(&d, &s);
        fprintf(fp, "%d %d\n", d, s);
        for(j = 0; j <= 1; j++) {
            for (k = DICE_NUM_MIN; k <= DICE_NUM_MAX; k++) {
                if(j == d && k == s) {
                    ds[d][s]++;
                }
            }
        }
    }

ここでDOUBLE_DICEから得られるdと出る目sをiがインクリメントされるごとに、それが出た回数としてds[k][j]に記憶させます。

確率の計算

double prob_ds[2][NUM_OF_ATTEMPTS+1];
    for(j = 0; j <= 1; j++) {
        for (k = DICE_NUM_MIN; k <= DICE_NUM_MAX; k++) {
            prob_ds[j][k] = ds[j][k]/(double)NUM_OF_ATTEMPTS;
        }
    }
\hat{P}(d, s) = \frac{N(d, s)}{N_{\rm att}}

に従って計算します。

最後にこの系の確率分布がprintf()で表示されるようになっています。

Reference

田中章詞、富谷昭夫、橋本幸士「ディープラーニングと物理学」(2019)講談社
https://bookclub.kodansha.co.jp/product?item=0000318303

Discussion