【シェルコマンド x 機械学習】awkで機械学習で使える高速データ処理 〜 ガウス分布ノイズの生成方法


2020/12/08

近年では深層学習を用いたAIの発展を背景に、学習の決めてとなる訓練データを膨大かつ高速で処理しなければならないというニーズの高まりを感じます。

特にAwkはテキスト処理に特化した一種のスクリプト言語で、c言語のような記述でスクリプトが組めて、その処理もとても高速であり、昨今のAI熱を受けて再評価に兆しがあるようです。

ということで今回はAwkを中心にして人工知能の訓練などの下処理にも使えるテクニックを意識しながら、ガウス分布ノイズのデータセット生成の方法を解説します。


はじめに

線型回帰解析などで実験データがない場合や、自作の関数で何点かサンプルする場合に、通常はpythonならnumpyを使ってnp.arangeを使って等間隔区切りの配列を使ってラベル値を作ったり、他の言語でもこれ相当のことが出来ると思います。

ただ、かなり演算の実装が難しい特殊関数などはそれ相応の数値計算ライブラリを使わないと歯が立たないのは仕方ないことです。

ですが、大したことのない基礎的な数学関数だけしか使わないのに、
numpyだのscipyだのを読み込んでまで計算させる必要があるのか、と思ってしまうことがあります。

そんなことを思ってしまった人は、一度立ち止まってこう考えるべきかもしれません。

「...それはAwkで十分ではないのか?」

もしその答えがYesだったなら、shellを立ち上げてawkを打つだけで既に必要な開発環境を手に入れていることになっているでしょう。

まさにインストールレス。

ほとんどの環境で、即時使うことが可能です。

さて、以降の内容ではAwkを使ってシェルスクリプトでもダミー実験データを生成する方法をやってみます。

ちなみに、今回の内容の類似記事として、
以前書いたこちら記事でもデータ処理のコツを紹介しています。

そちらの記事はawkだけでなくsedやsort、wgetなどの他のコマンドもバランス良く要所要所で使っています。


Awkで使える基礎的な数学関数

Awkが使える組み込みの関数は決して多くありません。

ただ三角関数やランダム関数が使えるので、発想次第でほとんどの陽関数なら自作できると思います。

関数名

備考

int(x)

整数への丸めを行う関数。四捨五入ではないので注意

sqrt(x)

xの正の平方根を返す。

exp(x)

自然対数eのx乗を返す。xが範囲外であればエラー

log(x)

xが正の場合にはxの自然対数を返す。xが範囲外であればエラー

sin(x)

xの正弦を返す。xの単位はラジアンに注意

cos(x)

xの余弦を返す。xの単位はラジアンに注意

atan2(y, x)

y/xのアークタンジェントを返す。返り値はラジアン

rand()

0から1の範囲の乱数を返す。ただし0と1は含まれない

srand(x)

シード値xから乱数を生成して返す。同一のシード値からは同一の乱数が返ることに注意

乱数を使った簡単なサイコロの目を出すだけのプログラム例を以下に挙げておきます。

            
            $ awk -v seed="${RANDOM}" '
    function roll(n) { return 1 + int(rand() * n) }
    BEGIN{
        srand(seed);
        print(roll(6))
    }
'
#実行する度に違う目が出る
5
        
なお、このプログラムではroll関数内でrand関数を使っており、短期間には(内部時刻から参照された)同一のシード値が設定されます。

なので、何度実行しても同一の返り値が得られるようにも思えますが、rollを呼び出す直前に
srand(seed)によって変更されたシード値がそれ以降で呼び出されるrand()にも影響を与え、結果として実行する度に結果が違うところに注意してください。


等間隔の配列を作成する

numpyでいうところのnp.arange関数をAwkで作成します。

            
            $ awk '
    #start(下限)とstop(上限)の範囲をstep間隔に刻んだ配列を返す
    function arange(start, stop, step) {
        count = 0;
        x = start;
        while (x <= stop) {
            varr[count] = x;
            count++;
            x = x + step;
        }
    }
    BEGIN {
        #例として0から6までを0.5間隔で刻んだ配列を計算
        arange(0, 6, 0.5);
        arr_len = length(varr);
        for (i = 0; i < arr_len; i++) {
            print i, varr[i];
        }
    }
'
#👇実行後
0 0
1 0.5
2 1
3 1.5
4 2
5 2.5
6 3
7 3.5
8 4
9 4.5
10 5
11 5.5
12 6
        

正規分布関数

ガウス分布とは以下のような関数を指します。

f(x)=12πσ2exp((xμ)22σ2)\displaystyle{ f(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp \biggl( -\frac{(x - \mu)^2}{2 \sigma^2} \biggr) }Eq. (1)

また確率変数
xx(1次元)が正規分布はN(μ,σ2)N(\mu ,\sigma^2)と表現します。

さて、これをAwkで関数を定義し、計算できるように実装してみます。

            
            $ awk '
    #start(下限)とstop(上限)の範囲をstep間隔に刻んだ配列を返す
    function gauss(val, mu, sigma) {
        PI = 3.14159265359; #👈円周率は適当な精度で...
        a_part = 1.0 / sqrt(2.0 * PI * sigma^2);
        b_part = -1.0 * (val - mu)^2 / (2 * sigma^2);
        return a_part * exp(b_part)
    }
    BEGIN {
        print gauss(0, 0, 1)
    }
'
#👇実行後
0.398942
        
(一点しか確認してませんが...)良さげな値が得られていますので、検証はひとまずおいておいて次の内容に進みます。


正規分布に基づく乱数(正規乱数)の生成

正規分布に基づく乱数、つまりはガウス分布ノイズ(=ホワイトノイズ)を作成するためには、単なる乱数から少し捻りを加えないといけません。

有名どころのアルゴリズムで、
ボックス-ミュラー法があります。

とても簡潔なアルゴリズムですので、Awkでも難なく実装できると思います。

            
            $ awk -v seed="${RANDOM}" '
    function box_muller(mu, sigma, arr_len_) {
        PI = 3.14159265359; #👈円周率は適当な精度で...
        count = 0;
        while(count < arr_len_) {
            x1 = rand();
            x2 = rand();

            #👇cosかsinかどちらかを選択
            tmp_rand_val = sqrt(-2 * log(x1)) * cos(2 * PI * x2);
            # tmp_rand_val = sqrt(-2 * log(x1)) * sin(2 * PI * x2);

            gauss_arr[count] = mu + sigma*tmp_rand_val;
            count++;
        }
    }
    BEGIN{
        #👇シード値をリセット(Awkの場合には重要)
        srand(seed);
        #標準正規分布N(0,1)に従う乱数10点の配列を計算
        box_muller(0, 1, 10);
        arr_len = length(gauss_arr);
        for (i = 0; i < arr_len; i++) {
            print i, gauss_arr[i];
        }
    }
'
#実行
0 1.28016
1 -0.435245
2 -0.962477
3 -1.56287
4 0.710585
5 -0.512278
6 0.4468
7 -1.43556
8 0.0096908
9 1.02674
        
なおボックス=ミュラー法は標準正規分布N(0,1)N(0, 1)に従う乱数です。

今回では、線形変換により平均
μ\mu、分散σ2\sigma^2の正規分布N(μ,σ2)N(\mu, \sigma^2)の分布に従う乱数を発生させるように拡張しています。

話を本題に戻すと、このコードが生成するを正規乱数は、ガウス分布ノイズと等価です。

言い換えるとノイズのとる値が正規分布に従えばガウス分布ノイズであることを確かめることができます。

ガウス分布ノイズの検証

あらかた材料は揃いましたので、最後に標準正規分布(理論値)と、正規乱数で100万回分サンプルした度数分布を正規化して重ねてみます。

まず
N(0,1)N(0,1)分布の理論値を以下のスクリプトで生成します。

            
            $ awk -v inp_mu=0.0 -v inp_sigma=1.0 '
    function arange(start, stop, step) {
        count = 0;
        x = start;
        while (x <= stop) {
            varr[count] = x;
            count++;
            x = x + step;
        }
    }
    function gauss(val, mu, sigma) {
        PI = 3.14159265359;
        a_part = 1.0 / sqrt(2.0 * PI * sigma^2);
        b_part = -1.0 * (val - mu)^2 / (2 * sigma^2);
        return a_part * exp(b_part)
    }
    BEGIN {
        OFS=","
        arange(-4, 4, 0.1);
        arr_len = length(varr);
        for (i = 0; i < arr_len; i++) {
            print i, varr[i], gauss(varr[i], inp_mu, inp_sigma);
        }
    }
' | awk -F "," '
    BEGIN {
        OFS=",";
        count = 0;
    }
    {
        xarr[count] = $2;
        yarr[count] = $3;
        if(ymax<$3) ymax=$3;
        count++;
    }
    END {
        xarr_len = length(xarr);
        for (i = 0; i < xarr_len; i++) {
            print i, xarr[i], yarr[i] / ymax
        }
    }
' > norm_dist_strict.csv
        
さらに、N(0,1)N(0,1)の正規乱数を100万回繰り返して得られた度数分布のデータセットは以下のスクリプトで生成します。

            
            $ awk -v seed="${RANDOM}" -v iter=1000000 -v inp_mu=0.0 -v inp_sigma=1.0 '
    function box_muller(mu, sigma, arr_len_) {
        PI = 3.14159265359;
        count = 0;
        while(count < arr_len_) {
            x1 = rand();
            x2 = rand();
            tmp_rand_val = sqrt(-2 * log(x1)) * cos(2 * PI * x2);
            gauss_arr[count] = mu + sigma * tmp_rand_val;
            count++;
        }
    }
    BEGIN{
        srand(seed); #シード値をリセット
        box_muller(inp_mu, inp_sigma, iter);
        arr_len = length(gauss_arr);
        for (i = 0; i < arr_len; i++) {
            print gauss_arr[i];
        }
    }
' | awk -v binsize=0.2 '
    BEGIN {
        OFS=","
    }
    {
        if(binsize <= 0) exit

        if($1 < 0) {
            frequency[int($1 / binsize) - 1] ++;
        } else {
            frequency[int($1 / binsize)] ++;
        }
    }
    END {
        for(i in frequency) {
            print (i + 0.5) * binsize, frequency[i] | "sort -n";
        }
    }
' | awk -F "," '
    BEGIN {
        OFS=",";
        count = 0;
    }
    {
        xarr[count] = $1;
        yarr[count] = $2;
        if(ymax<$2) ymax=$2;
        count++;
    }
    END {
        xarr_len = length(xarr);
        for (i = 0; i < xarr_len; i++) {
            print i, xarr[i], yarr[i] / ymax
        }
    }
' > norm_dist_exp.csv
        
以上で、生成された理論式のデータセット(norm_dist_strict.csv)を実線、正規乱数から得られた度数分布のデータセット(norm_dist_exp.csv)を散布図にして描画したグラフは以下です。

合同会社タコスキングダム|蛸壺の技術ブログ

関数形だけみるとほぼ一致しており、正規乱数から得られた値がガウス分布ノイズであることが確認できました。


まとめ

正規分布は機械学習においてもいろんな場面で顔を出す重要な関数の一つです。

数学的な理解を深めつつ、計算のアルゴリズムも確実に身につけておきたい内容だと思います。

今回の内容は見ての通りで、Awkを駆使することで正規分布のみならず色々な確率密度関数に基づいたデータセットが生成することも可能です。

特に機械学習のプログラムは様々な知識が入り乱れる複合テーマです。

データの下処理周りはシェルスクリプトにお任せし、解析はtensorflowなどの機械学習用ライブラリで、描画・視覚化はchart.jsなどのグラフィックユーティリティで、各作業を区分けして行うとそれぞれのブログラムの棲み分けができてプロジェクトの管理がしやすくなると思います。

参考サイト

Pythonでガウス分布を持つノイズの作り方と調整方法

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。