とあるお兄さんの雑記

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

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

注意書き

始まりは深夜テンションで考えてしまったところです。深夜テンションで「面白そうじゃね?」と考えて途中まで作りましが、日を置いて見返してみると「何バカなこと考えているんだろう。。。」という考えに行きついてしまい、ネタにするのはやめようとも思いました。しかし、さすがに中途半端は良くないなぁと思い、結果的に書くことにしました。

一部深夜テンションで書いている部分があるため、条件が曖昧だったり、論理性に欠けたり、「バカだな、コイツ」と思われるかもしれませんが、ご容赦ください。

そのうえで、続きをお読みになってください。ゴメンナサイ。

始まり

皆さんも、一度は上司や先輩に「乱数作っとけよ」と言われたことがあるかと思います。その場合、皆さんはどうやって乱数を作りますか?

プログラミングができる方はPythonなどを用いて簡単に n桁の乱数を作ることが出来るでしょう。また、コンピュータに頼らず自力で数字を羅列させる方もいるでしょう。

ここで、ふと疑問が湧きます。はたして人がランダムだと思って作った数字の羅列は本当に乱数なのでしょうか?もしかしたら、その人の主観が入っているかもしれません。つまり、人が作り出す数字の羅列コンピュータが作り出す数字の羅列は似ているようで、実は全くの別物かもしれないということです。となると、うまく特徴量を取り出せば、作った乱数が人かコンピュータかを見分けることができるかもしれません。

というわけで、

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

を調べてみました。

環境

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

条件

今回は、10桁の乱数を200個作ってみます。条件としては10桁の乱数なので、 1,000,000,000 ~ 9,999,999,999に制限します(10桁に満たないものは0埋めするなどを考えればいろんな値を取るため、より乱数らしくなります。しかし、深夜テンションのため見逃していました。深夜テンション恐ろしい...)。

人間側で乱数を生成するのは私だけです。本来は「人とコンピュータ」であるため、世界中からランダムに100人選び、それぞれの方に10桁の乱数を10個出してもらう、というような方法が一般的です。ですが、私の場合は友達が少ないのでそこまで集めるのは無理ですし、データが偏る可能性があります。私の数少ない友達にお願いしようとも考えたのですが、絶対めんどくさがってやらない全員忙しいだろうと思いやめました。

というわけで、本来ならダメですが今回の人間側の乱数は私だけになります。

データ収集時の注意点

先ほど、データが偏るという言葉が出てきたのですが、これについて少し考えてみましょう。本来作りたいのは、「人とコンピュータ」の作った乱数を分類できる判定器です。この「人」というのは、お互いの関連が少ない人々(要は見知らぬ人同士)のことを指します。では、なぜそんな人に限るのでしょうか。

例として、内閣総理大臣などに出てくる支持率を考えてみます。



あるところにトーケー王国があり、そこでは国民100万人が住んでいました。このトーケー王国を統べる国王ニコーは重い税金で国民を苦しめ、税金をもとに自分や側近たち、富裕層にあたる1万人の国民は裕福に暮らしていました。

そんなある日、国王ニコーは自分の支持率が気になり、側近に自分の支持率を調べるように指示を出しました。数日後、側近が提出した調査書では 90\%以上の国民が国王ニコーのことを指示するという結果となりました。

ここまで読んで「あれ?」と思われた方もいるかと思います。思い税金で苦しんでいる国民のほとんどが、国王ニコーを支持するわけがありません。なぜこのような結果になってしまったのでしょうか?

国民に圧力がかかっていたのか?側近が数値を都合のいいようにいじったのか?

実は、調査する人をランダムに選んでいない点に問題があります(先の例では選び方についての指示がなかったので、見落とした方もいるかもしれません)。例えば、国民100万人のうち1万人を富裕層にあたる人から選び出したとしたらどうでしょう?富裕層からすれば特に苦しい生活を送っているわけではないため、国王ニコーを支持すると答える人が大勢出てくる可能性があります。

また、国王ニコーの側近である者たちも支持率の調査対象である場合、国王ニコーを支持するはずです。

要は、調査する対象(今回で言えば、国王ニコー)に対して、その関連が強い対象者(今回で言えば、側近や富裕層など)ばかりのデータや、収集したデータ同士の関連が強いデータを集めてしまうと公正な判断ができません。

そのため公正な判断や、一般性を持たせたいのであれば、できるだけ収集するデータは互いに関連が少ないものを集めてくるようにしましょう。

ちなみに、トーケー王国はミーン、メジアン、モードの三人の指導者を中心に国王ニコーに反旗を翻し、革命に成功した結果、トーケー共和国となったそうです(このたとえ話はフィクションですよ)。

データ作成

条件をもとに、データを作成します。ひたすらcsvファイルに10桁の数字200個を書き込んでいくという単純作業。データを書き込んでいる最中は「めんどい...。」、「眠い...。」、「何してんだろ?私は...」とか考えてましたね。

とか考えているうちにできたのが下の画像です。

f:id:kurasher:20200611200716p:plain

あとはこのcsvファイルを読み込めばOKです。

import pandas as pd
human = pd.read_csv("random_human.csv", encoding="UTF-8")



一方、コンピュータ側で10桁の乱数を作るのは簡単です。

import random
machine = [random.randrange(10**9,10**10) for i in range(200)]



また、今回の問題は作った乱数が人かコンピュータかを見分けるため、分類にあたります。また、分類するものは人とコンピュータの2種類であるため、2クラス分類となります。

ここでは、人(私)が作った乱数側のクラスにラベル1を、コンピュータが作った乱数側のクラスにラベル0を付与します。

人側は上の画像のB列にあるようにすでに1を付与しています。コンピュータ側は、以下のようにしてラベル0を付与します。

machine = pd.DataFrame(machine,  columns=['data'])
machine["label"] = 0



で、これを出力すると、

f:id:kurasher:20200611200410p:plain

となります。

いい感じにできましたね。

あとはデータを結合します。

data = pd.concat([machine, human])

これで、人とコンピュータ合わせて400個の乱数データが出来上がりました。

分類:1回目

適当なモデルで分類してみましょう。今回は私が研究室にいたときに使っていたランダムフォレストで分類してみます。

ここでは、scikit-learnライブラリのmodel_selectionからデータを学習データと検証データに分割します。

また、構築した学習モデルの性能を評価するライブラリと、機械学習モデルの一つであるランダムフォレスト分類器のライブラリを準備します。

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



次に、上で結合させたデータを乱数データとそのラベルに分けます。このうち、乱数データの8割を学習データとし、残りの2割を検証データとしています。検証データは学習させたモデルの性能を測定するためのデータです。これで、作ったモデルの性能を評価し汎用的かどうかを判断します。

X = data[['data']]
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%を検証データにする



ここからやっと学習の項目です。

まずは、学習用モデルをインスタンスを作成します。今回はランダムフォレストなので、RandomForestClassifier()となります。また、引数のn_estimatorsは決定木のことで、今回はデフォルトの100としています。他にも、引数があるので、興味のある方は調べてみてはいかがでしょうか。

インスタンスの作成が終わったら、.fitを用いて学習データで学習を行います。

最後に、検証データを用いて、.predictで予測を行います。

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値

ここで混合行列というのは、実際のクラスが0、1のデータに対し、クラス0、1に分類されたデータの個数を要素とする 2 \times 2の行列です。今回の例では、クラス0はコンピュータ側の乱数、クラス1は私が作成した乱数を表します。

今回の行列の中身は

  1. 1行1列目の要素は、実際にクラス0で正しくクラス0に分類されたデータ数(真陰性(TN: True Negative))
  2. 1行2列目の要素は、実際にクラス0だが誤ってクラス1に分類されたデータ数(偽陽性(FP: False Positive))
  3. 2行1列目の要素は、実際にはクラス1だが誤ってクラス0に分類されたデータ数(偽陰性(FN: False Negative))
  4. 2行2列目の要素は、実際にはクラス1で正しくクラス1に分類されたデータ数(真陽性(TP: True Positive))

となっています。

(場合によっては、ここに紹介しているような混合行列になることもあります。位置に注意しましょう。)

結果は・・・

confusion matrix =   
 [[29 17]
 [12 22]]
accuracy =  0.6375  
precision =  0.5641025641025641  
recall =  0.6470588235294118  
f1 score =  0.6027397260273972  

となりました。

・・・全体的に低いですね。改善の余地がありそうです。

ちなみに、混合行列辺りの内容はこちらの記事に詳しく書いてあります。参考にどうぞ。

【入門者向け】機械学習の分類問題評価指標解説(正解率・適合率・再現率など)

分類:2回目~データを増やそう~

分類1回目の結果はいまいちでした。そこで、今回は単純にデータを増やしてみることにします。他の条件は変えません。

今度は、データを合わせて2000個用意し、そのうち、8割を学習用データ、2割を検証データとしています。人側のデータ1000個はすべて私が作りました。仕事の合間を縫いながら「一体私は何をやっているんだろう・・・?」と思いました、はい。

さて、1回目と違い、結果はどうなるでしょうか?

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
from sklearn.tree import DecisionTreeClassifier


X = data[['data']]
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 = 
 [[140  60]
 [ 54 146]]
accuracy =  0.715
precision =  0.7087378640776699
recall =  0.73
f1 score =  0.7192118226600985

先ほどと比べて全体的に上がりましたね。しかし、7割程度しか当たっていません。一応データを増やしていけば精度が上がることが予想できますが、これ以上データを増やす気はありません(めんどくさい)。

一旦このあたりで区切りましょう。次はvol.2に続きます。