とあるお兄さんの雑記

基本的に技術系の内容を書きますが、何を書くかは私の気分です。

人がつくるランダムな数字の羅列は乱数になり得るか(タイトル詐欺注意)vol.2

この記事は、タイトルにある通り、

 n桁の乱数を m個作るとき、人が作った乱数ととコンピュータが生成した乱数を見分けることは可能か」

を深夜テンションで思いつき、実際にやってみた結果を述べる記事です。
ちなみに、vol.1はこちら

vol.1の振り返り

  1. ことの始まり
  2. 環境、条件の確認
  3. データの作成
  4. 分類1回目
  5. 分類2回目~データ増加~

ざっくりこんな感じでしたね。これを踏まえたうえで、vol.2に進みます。

環境

Windows 10   
Python: 3.7.3  
jupyter lab: 0.35.4  
pandas: 0.25.3  
numpy: 1.17.0  
scikit-learn: 0.23.1  
matplotlib: 3.1.1

条件

  1. 乱数の範囲は 1,000,000,000 ~ 9,999,999,999、データの個数はそれぞれ1000個
  2. 人側のサンプルは私のみ
  3. コンピュータ側はプログラムで作成

分類3回目~新しい特徴量を考えよう~

今までは、何の加工も施していないデータでしか学習を行っていませんでした。そこで、新しい特徴量を考えてみましょう。

この特徴量を考えるところは皆さんの工夫次第では、学習器の性能が悪くなったり良くなったりします。しっかり考えて有望な特徴量を考えるようにしましょう。

新しい特徴量1つ目

今回は隣の位の数字との差の絶対値をとり、それを合計した特徴量 sum\_absを考えてみましょう。

(なぜ隣の位の数字との差を取って、それを合計した特徴量を考えたかですが、この発想はテレビからヒントを得ています。そのテレビで取り上げられていた内容では、1~6の目のあるサイコロを振り、出た目を記録します。一方、人側は1から6のうち、一つを選び書き出します。これをそれぞれ同じ回数行ったとき、出た目の前後との差の絶対値を取ると、サイコロの場合と人が書き出した場合で違いが出てくる、というものでした。こっちをやって、ブログのネタにすればよかったかも...。)

以下、特徴量 sum\_absの作成です。

data["sum_abs"] = 0

for i in range(len(data)):
    num = data["data"][i]
    sum = 0#各桁の差の合計値
    a = num % 10#10で割った時の余りを代入(1の位)
    b = 0#いったん0を代入
    
    while num > 10:#numが10よりも大きい場合
        num = num // 10#10で割って小数点を切り捨て
        b = a#直前のaの値を代入
        a = num % 10#10で割った時の余りを代入
        sub = abs(b-a)#差の絶対値を計算    
        sum = sum + sub
        
    data["sum_abs"][i] = sum

dataというのはvol.1で作った、人の乱数とコンピュータの乱数を結合したものです。
このプログラムがどう動作しているか詳しく解説します。

まず、dataですが、

data label
4280387012 0
2095513148 0
... ...
5960120934 1
7761929872 1

となっています。

はじめに、

data["sum_abs"] = 0

でdataは次のようになります。

data label sum_abs
4280387012 0 0
2095513148 0 0
... ... ...
5960120934 1 0
7761929872 1 0



次に

for i in range(len(data)):

でdataのデータ数2000に対してfor文内の処理を行います。

    num = data["data"][i]

ここでは、変数のnumにdataのdata列にあたるものを取ってきて代入しています。1番最初のデータは4280387012ですので、numにはこの値が入ります。

変数 現在入っている数字
num 4280387012



    sum = 0#各桁の差の合計値
    a = num % 10#10で割った時の余りを代入(1の位)
    b = 0#いったん0を代入

sum = 0で各桁の差の合計値を計算するための変数sumを初期化します。

次の変数aではnum%10として、10で割った時の余りを変数aに入れます。今回のnumは4280387012が入っているので、このnumの10で割った余り2が入ります。これで1の位を取ってくることが出来ました。

次の変数bはここではいったん0を入れて初期化しておきます。

変数 現在入っている数字
num 4280387012
sum 0
a 2
b 0
    while num > 10:#numが10よりも大きい場合

ここではwhileでnumが10よりも大きい間while内の処理を行うようにしています。

        num = num // 10#10で割って小数点を切り捨て
        b = a#直前のaの値を代入
        a = num % 10#10で割った時の余りを代入

num//10で10で割って小数点を切り捨てた値をnumに代入します。numは4280387012ですね。これを10で割って小数点を切り捨てるので、numには428038701が入ります。

次にbには直前のaの値を代入します。現在aは2の値が入っているので、bには2が入ります。

次にaには、num % 10で10で割った時の余りを代入します。現在のnumは428038701ですから、aに入る値は1になります。

ここまでを整理すると、現在もともとのnumの値の1の位をbが、10の位をaが保持しています。

変数 現在入っている数字
num 428038701
sum 0
a 1
b 2
        sub = abs(b-a)#差の絶対値を計算    
        sum = sum + sub

ここで、やっとsumの出番です。隣り合う位の差を計算し、それをsumに加算します。

sub = abs(b-a)で差の絶対値を計算し、いったんsubという変数に代入します。そして次の行で、sumの現在の値(0)にsubを足し合わせて、sumに代入します。

変数 現在入っている数字
num 428038701
sum 1
a 1
b 2

この後whileに戻り、num>10でwhile文内の処理を繰り返すかどうかを判定します。

もちろん、現在のnumは10よりも大きいので、while文内を処理することになります。処理結果は、

変数 現在入っている数字
whileの判定
num = 428038701 > 10 ? yes
while文内の処理後 ...
num 42803870
sum 1
a 1
b 1

となります。さて、これをnumが10より小さくなるまで見ていくとしましょう。

変数 現在入っている数字
whileの判定
num = 42803870 > 10 ? yes
while文内の処理後 ...
num 4280387
sum 2
a 0
b 1
変数 現在入っている数字
whileの判定
num = 4280387 > 10 ? yes
while文内の処理後 ...
num 428038
sum 9
a 7
b 0
変数 現在入っている数字
whileの判定
num = 428038 > 10 ? yes
while文内の処理後 ...
num 42803
sum 10
a 8
b 7
変数 現在入っている数字
whileの判定
num = 42803 > 10 ? yes
while文内の処理後 ...
num 4280
sum 15
a 3
b 8
変数 現在入っている数字
whileの判定
num = 4280 > 10 ? yes
while文内の処理後 ...
num 428
sum 18
a 0
b 3
変数 現在入っている数字
whileの判定
num = 428 > 10 ? yes
while文内の処理後 ...
num 42
sum 26
a 8
b 0
変数 現在入っている数字
whileの判定
num = 42 > 10 ? yes
while文内の処理後 ...
num 4
sum 34
a 4
b 2
変数 現在入っている数字
whileの判定
num = 4 > 10 ? no
while文内の処理後 ...
num 4
sum 34
a 4
b 2



ここで、初めてwhileの条件式がno(false)になりました。というわけで、whileの処理を抜け、最後の処理に行きます。

    data["sum_abs"][i] = sum

data["sum_abs"][i] = sumで、dataの sum\_abs列にsumの値を入れています。すると、dataは

data label sum_abs
4280387012 0 34
2095513148 0 0
... ... ...
5960120934 1 0
7761929872 1 0

となります。

これをdata全体に行います。これで特徴量の1つ目ができました。

新しい特徴量2つ目

新しい特徴量2つ目はシンプルです。numに0, 1, 2, 3, 4, 5, 6, 7, 8, 9の値がそれぞれ何個あるかを数えていきます。こうすると、いきなり特徴量が10個も増えてしまうので、特徴量2つ目というのは語弊がありますが、許してください。

やり方は以下のような感じです。

data["is_zero"] = 0
data["is_one"] = 0
data["is_two"] = 0
data["is_three"] = 0
data["is_four"] = 0
data["is_five"] = 0
data["is_six"] = 0
data["is_seven"] = 0
data["is_eight"] = 0
data["is_nine"] = 0

for i in range(len(data)):
    num = data["data"][i]
    
    while num > 1:#numが1よりも大きい場合
        a = num % 10#10で割った時の余りを代入
        if a == 0:
            data["is_zero"][i] = data["is_zero"][i] + 1
        elif a == 1:
            data["is_one"][i] = data["is_one"][i] + 1
        elif a == 2:
            data["is_two"][i] = data["is_two"][i] + 1
        elif a == 3:
            data["is_three"][i] = data["is_three"][i] + 1
        elif a == 4:
            data["is_four"][i] = data["is_four"][i] + 1
        elif a == 5:
            data["is_five"][i] = data["is_five"][i] + 1
        elif a == 6:
            data["is_six"][i] = data["is_six"][i] + 1
        elif a == 7:
            data["is_seven"][i] = data["is_seven"][i] + 1
        elif a == 8:
            data["is_eight"][i] = data["is_eight"][i] + 1
        else:
            data["is_nine"][i] = data["is_nine"][i] + 1
            
        num = num // 10

最初の方で、0から9の値が何個あるのかという特徴量を作成し、あとは条件に沿って0ならばis_zeroの値を1つ加算、1ならばis_oneの値を1つ加算、......、9ならばis_nineの値を1つ加算としていきます。

結果、2つの特徴量を持ったdataの形は以下のようになります。

data label sum_abs is_zero is_one is_two is_three is_four is_five is_six is_seven is_eight is_nine
4280387012 0 34 2 1 2 1 1 0 0 1 1 0
2095513148 0 30 1 2 1 1 1 2 0 0 1 1
... ... ... ... ... ... ... ... ... ... ... ... ...
5960120934 1 33 2 1 1 1 1 1 1 0 0 2
7761929872 1 35 0 1 2 0 0 0 1 3 1 2

学習

以下のように書いて学習を行います。分類1回目、2回目と同じでランダムフォレストを使います。

from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score
from sklearn.ensemble import RandomForestClassifier

X = data.drop(['label'], axis = 1)
Y = data['label']
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.2, random_state = 0) # 80%のデータを学習データに、20%を検証データにする

rfc = RandomForestClassifier(n_estimators = 100)
rfc.fit(X_train, Y_train)
Y_pred = rfc.predict(X_test)

print('confusion matrix = \n', confusion_matrix(y_true=Y_test, y_pred=Y_pred))#混合行列
print('accuracy = ', accuracy_score(y_true=Y_test, y_pred=Y_pred))#正解率
print('precision = ', precision_score(y_true=Y_test, y_pred=Y_pred))#適合率
print('recall = ', recall_score(y_true=Y_test, y_pred=Y_pred))#再現率
print('f1 score = ', f1_score(y_true=Y_test, y_pred=Y_pred))#F値



以下、結果です。

confusion matrix = 
 [[171  29]
 [ 54 146]]
accuracy =  0.7925
precision =  0.8342857142857143
recall =  0.73
f1 score =  0.7786666666666667

分類2回目では7割前半だったものが、再現率を除いて7割後半から8割に上がっており、性能はよくなっているようです。



ここまで急に上がると気になるのは、どの特徴量が分類に有望だったかです。

ランダムフォレストにはどの特徴量がどれだけ分類に貢献したかを可視化してくれるものがあり、今回はmatplotlibで可視化してみます。

import matplotlib.pyplot as plt

#特徴量の重要度
feature = rfc.feature_importances_
#特徴量の名前
label = X.columns[0:]
#特徴量の重要度順(降順)
indices = np.argsort(feature)[::1]

# プロット
x = range(len(feature))
y = feature[indices]
y_label = label[indices]
plt.barh(x, y, align = 'center')
plt.yticks(x, y_label)
plt.xlabel("importance_num")
plt.ylabel("label")
plt.show()

以下、結果です。

f:id:kurasher:20200627020444p:plain

画像をみると、sum_absが分類に一番貢献しているようですね。また、is_nineis_zeroが3番目と4番目のところに来ており、どうやら0と9で判断できる要素があるようです。

このようにデータの羅列だけでは分からないものも、可視化することで判断材料が増え、意思決定を促すことが可能になります。



話を戻して、モデルの性能は前回よりも上がりました。次回ではさらにデータを増やす、特徴量を増やす方法に加えて、さらに性能を上げることが出来ないかを調べてみます。

次は最終回のvol.3に続きます。