🤖

機械学習でFizzBuzzを実現する

2021/01/17に公開

元ネタ

https://twitter.com/kazoo04/status/1350703632715444224

実行結果

GoogleColaboratoryで動かした結果は以下です
https://colab.research.google.com/drive/1DC7EqTsblJwwgXFnkChLA-VJZnn207cD?usp=sharing

教師データの作成

学習用の教師データを生成して確認します。
本来は教師データは外部から提供されるはずだが、今回はそれがないので自分で生成する。

import numpy as np

def fizzbuzz(n:int):
  if n % 15 == 0:
    return "FizzBuzz"
  if n % 5 == 0:
    return "buzz"
  if n % 3 == 0:
    return "Fizz"
  return n

def generate_sample_data(size=1000):
  feature = np.random.randint(0, np.iinfo(np.int32).max, size)
  label = list(map(fizzbuzz, feature))

  return feature, label

generate_sample_data(10)
(array([1825835839,   73640530,  858314129, 1561484482, 1684633482,
        1809287540, 1782686014, 1740432349, 1389025035, 1186629944]),
 [1825835839,
  'buzz',
  858314129,
  1561484482,
  'Fizz',
  'buzz',
  1782686014,
  1740432349,
  'FizzBuzz',
  1186629944])
original_feature, original_label = generate_sample_data(100000)
original_feature[:10], original_label[:10]
(array([1478497585, 1861887592, 1055858028, 1948518472, 1953683988,
        1341279806, 1127796401,   26103186, 1388074938, 1541044962]),
 ['Buzz',
  1861887592,
  'Fizz',
  1948518472,
  'Fizz',
  1341279806,
  1127796401,
  'Fizz',
  'Fizz',
  'Fizz'])

数値ラベルの変換

このままだと数値が取り扱いづらいので、数値ラベルを変換する関数を用意する

# 数値ラベルの変換
def convert_label(label):
  if type(label) is str:
    return label
  else:
    return "num"

# 数値ラベルの逆変換
def reverse_convert_label(n, label):
  if label == "num":
    return n
  else:
    return label

converted_label = list(map(convert_label, original_label))
converted_label[:10]
['Buzz', 'num', 'Fizz', 'num', 'Fizz', 'num', 'num', 'Fizz', 'Fizz', 'Fizz']

RandomForestで学習して予測する

ひとまずRandomForestでクラス分類器を学習させてみて、混同行列を確認してみる。

import sklearn.model_selection
import sklearn.ensemble
import sklearn.metrics 

x_train, x_test, y_train, y_test =  sklearn.model_selection.train_test_split(
    original_feature.reshape((original_feature.shape[0], 1)), # 学習器の入力のために reshape
    converted_label,
    test_size=0.5, random_state=42)

rf_model = sklearn.ensemble.RandomForestClassifier(
    n_estimators = 300,
    max_depth=5
)

rf_model.fit(x_train, y_train)
y_pred = rf_model.predict(x_test)

cm = sklearn.metrics.confusion_matrix(y_test, y_pred)

print(cm)
[[    0     0     0  6583]
 [    0     1     0 13350]
 [    0     0     0  3356]
 [    0     4     0 26706]]

混同行列から予測がまったくできておらず、ほぼすべてが数値であると予測されている。

特徴量を加工する

特徴量をそのまま入力することはだめだということが分かったので、特徴量を変換することを試みる。fizzbuzzなる問題は何やら倍数が関係しているらしいので、元の値を残しつつ剰余演算をした特徴量を追加するようにしてみる。feature engineeringってやつよ。

# 剰余特徴量の生成
def convert_feature(n):
  return [n] + [n % i for i in range(1, 20)] 

converted_feature = list(map(convert_feature, original_feature))
converted_feature[:10]
[[1419735139, 0, 1, 1, 3, 4, 1, 4, 3, 7, 9, 9, 7, 4, 11, 4, 3, 12, 7, 1],
 [293031021, 0, 1, 0, 1, 1, 3, 3, 5, 3, 1, 8, 9, 10, 3, 6, 13, 15, 3, 6],
 [1144011144, 0, 0, 0, 0, 4, 0, 3, 0, 3, 4, 1, 0, 3, 10, 9, 8, 3, 12, 16],
 [228800883, 0, 1, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 12, 3, 3, 3, 8, 3, 14],
 [853094197, 0, 1, 1, 1, 2, 1, 4, 5, 1, 7, 10, 1, 7, 11, 7, 5, 10, 1, 11],
 [1781579905, 0, 1, 1, 1, 0, 1, 0, 1, 7, 5, 6, 1, 1, 7, 10, 1, 16, 7, 8],
 [1858463710, 0, 0, 1, 2, 0, 4, 5, 6, 7, 0, 4, 10, 12, 12, 10, 14, 12, 16, 9],
 [1008809478, 0, 0, 0, 2, 3, 0, 5, 6, 0, 8, 6, 6, 1, 12, 3, 6, 0, 0, 13],
 [573881766, 0, 0, 0, 2, 1, 0, 3, 6, 6, 6, 7, 6, 3, 10, 6, 6, 16, 6, 9],
 [302839948, 0, 0, 1, 0, 3, 4, 5, 4, 1, 8, 4, 4, 8, 12, 13, 12, 10, 10, 12]]

加工した特徴量を使って、FizzBuzzを予測する

それでは、もう一度RandomForestで分類して混同行列を確認してみる。

x_train, x_test, y_train, y_test =  sklearn.model_selection.train_test_split(
    converted_feature,
    converted_label,
    test_size=0.5, random_state=42)
rf_model = sklearn.ensemble.RandomForestClassifier(
    n_estimators = 300,
    max_depth=5
)

rf_model.fit(x_train, y_train)
y_pred = rf_model.predict(x_test)

cm = sklearn.metrics.confusion_matrix(y_test, y_pred)

print(cm)
[[ 6583     0     0     0]
 [    0 13351     0     0]
 [    0     0  3356     0]
 [    0     0     0 26710]]

混同行列が対角に並んだので、精度100%を実現できた。

FizzBuzzをClass化する

convert関数を毎回適用するのは大変なので、FizzBuzzクラスを作っていく。今回はとりあえずRandomForestを使ったが、他の学習器も使えるように、学習器のクラスとパラメータを引数にとるようにしておく。

class FizzBuzz():
  def __init__(self, model_cls, param):
    self.model = model_cls(**param)

  @staticmethod
  def _convert_label(label):
    if type(label) is str:
      return label
    else:
      return "num"

  @staticmethod
  def _reverse_convert_label(n, label):
    if label == "num":
      return n
    else:
      return label

  @staticmethod
  def _convert_feature(n):
    return [n] + [n % i for i in range(1, 20)]

  @staticmethod
  def _fizzbuzz(n:int):
    if n % 15 == 0:
      return "FizzBuzz"
    if n % 5 == 0:
      return "buzz"
    if n % 3 == 0:
      return "Fizz"
    return n

  @staticmethod
  def generate_sample_data(size=1000):
    feature = np.random.randint(0, np.iinfo(np.int32).max, size)
    label = list(map(FizzBuzz._fizzbuzz, feature))

    return feature, label

  def fit(self, x, y):
    converted_x = list(map(self._convert_feature, x))
    converted_y = list(map(self._convert_label, y))
    
    self.model.fit(converted_x, converted_y)
    
  def predict(self, x):
    converted_x = list(map(self._convert_feature, x))
    converted_y_pred = self.model.predict(converted_x)
    y_pred = list(map(
        lambda x: self._reverse_convert_label(x[0], x[1]),
        zip(x, converted_y_pred)))
    return y_pred

作ったクラスを学習させていく

model = FizzBuzz(
    sklearn.ensemble.RandomForestClassifier,
    {
      "n_estimators": 300,
      "max_depth": 5
    }
)

train_feature, train_label = FizzBuzz.generate_sample_data(10000)
model.fit(train_feature, train_label)

1から100までのFizzBuzzを確認していく

for i in range(1, 101):
  y_pred = model.predict([i])[0] # arrayを受け取ってarrayを返す関数なので、こうする
  y_truth = model._fizzbuzz(i)
  print(i, y_pred, y_truth, y_pred == y_truth)
1 1 1 True
2 2 2 True
3 Fizz Fizz True
4 4 4 True
5 buzz buzz True
6 Fizz Fizz True
7 7 7 True
8 8 8 True
9 Fizz Fizz True
10 buzz buzz True
11 11 11 True
12 Fizz Fizz True
13 13 13 True
14 14 14 True
15 FizzBuzz FizzBuzz True
16 16 16 True
17 17 17 True
18 Fizz Fizz True
19 19 19 True
20 buzz buzz True
21 Fizz Fizz True
22 22 22 True
23 23 23 True
24 Fizz Fizz True
25 buzz buzz True
26 26 26 True
27 Fizz Fizz True
28 28 28 True
29 29 29 True
30 FizzBuzz FizzBuzz True
31 31 31 True
32 32 32 True
33 Fizz Fizz True
34 34 34 True
35 buzz buzz True
36 Fizz Fizz True
37 37 37 True
38 38 38 True
39 Fizz Fizz True
40 buzz buzz True
41 41 41 True
42 Fizz Fizz True
43 43 43 True
44 44 44 True
45 FizzBuzz FizzBuzz True
46 46 46 True
47 47 47 True
48 Fizz Fizz True
49 49 49 True
50 buzz buzz True
51 Fizz Fizz True
52 52 52 True
53 53 53 True
54 Fizz Fizz True
55 buzz buzz True
56 56 56 True
57 Fizz Fizz True
58 58 58 True
59 59 59 True
60 FizzBuzz FizzBuzz True
61 61 61 True
62 62 62 True
63 Fizz Fizz True
64 64 64 True
65 buzz buzz True
66 Fizz Fizz True
67 67 67 True
68 68 68 True
69 Fizz Fizz True
70 buzz buzz True
71 71 71 True
72 Fizz Fizz True
73 73 73 True
74 74 74 True
75 FizzBuzz FizzBuzz True
76 76 76 True
77 77 77 True
78 Fizz Fizz True
79 79 79 True
80 buzz buzz True
81 Fizz Fizz True
82 82 82 True
83 83 83 True
84 Fizz Fizz True
85 buzz buzz True
86 86 86 True
87 Fizz Fizz True
88 88 88 True
89 89 89 True
90 FizzBuzz FizzBuzz True
91 91 91 True
92 92 92 True
93 Fizz Fizz True
94 94 94 True
95 buzz buzz True
96 Fizz Fizz True
97 97 97 True
98 98 98 True
99 Fizz Fizz True
100 buzz buzz True

ひとまず、1から100までの間では、すべて一致しているようなので大丈夫でそうである。
任意のnについて成立するかどうかはわからないが、おそらく大丈夫だろう。知らんけど。

多層パーセプトロンを使ってFizzBuzzをしてみる

それでは利用する学習器をRandomForestから、多層パーセプトロンに変えて実行してみましょう。

import sklearn.neural_network

model = FizzBuzz(
    sklearn.neural_network.MLPClassifier,
    {
      "max_iter": 1000
    }
)

train_feature, train_label = FizzBuzz.generate_sample_data(10000)
model.fit(train_feature, train_label)
for i in range(1, 101):
  y_pred = model.predict([i])[0] # arrayを受け取ってarrayを返す関数なので、こうする
  y_truth = model._fizzbuzz(i)
  print(i, y_pred, y_truth, y_pred == y_truth)
1 1 1 True
2 2 2 True
3 3 Fizz False
4 4 4 True
5 5 buzz False
6 6 Fizz False
7 7 7 True
8 8 8 True
9 9 Fizz False
10 10 buzz False
11 11 11 True
12 12 Fizz False
13 13 13 True
14 14 14 True
15 15 FizzBuzz False
16 16 16 True
17 17 17 True
18 18 Fizz False
19 19 19 True
20 20 buzz False
21 21 Fizz False
22 22 22 True
23 23 23 True
24 24 Fizz False
25 25 buzz False
26 26 26 True
27 27 Fizz False
28 28 28 True
29 29 29 True
30 30 FizzBuzz False
31 31 31 True
32 32 32 True
33 33 Fizz False
34 34 34 True
35 35 buzz False
36 36 Fizz False
37 37 37 True
38 38 38 True
39 39 Fizz False
40 40 buzz False
41 41 41 True
42 42 Fizz False
43 43 43 True
44 44 44 True
45 45 FizzBuzz False
46 46 46 True
47 47 47 True
48 48 Fizz False
49 49 49 True
50 50 buzz False
51 51 Fizz False
52 52 52 True
53 53 53 True
54 54 Fizz False
55 55 buzz False
56 56 56 True
57 57 Fizz False
58 58 58 True
59 59 59 True
60 60 FizzBuzz False
61 61 61 True
62 62 62 True
63 63 Fizz False
64 64 64 True
65 65 buzz False
66 66 Fizz False
67 67 67 True
68 68 68 True
69 69 Fizz False
70 70 buzz False
71 71 71 True
72 72 Fizz False
73 73 73 True
74 74 74 True
75 75 FizzBuzz False
76 76 76 True
77 77 77 True
78 78 Fizz False
79 79 79 True
80 80 buzz False
81 81 Fizz False
82 82 82 True
83 83 83 True
84 84 Fizz False
85 85 buzz False
86 86 86 True
87 87 Fizz False
88 88 88 True
89 89 89 True
90 90 FizzBuzz False
91 91 91 True
92 92 92 True
93 93 Fizz False
94 94 94 True
95 95 buzz False
96 96 Fizz False
97 97 97 True
98 98 98 True
99 99 Fizz False
100 100 buzz False

全滅である。数値のところは数値として予測しているが、そもそもすべて数値として予測してしまっているので、全滅と言って過言はないだろう。

多層パーセプトロンを利用したところ、正しい結果は得られなかった。これは学習が足りないか、層の数が不足しているのだと考えられる。

というのは冗談で、まじめな話、特徴量を変換した際に、元の値を保持してしまっており、元の値をいれて多層パーセプトロンが動作した結果、元の値のスケールが大きすぎて、全結合で全体が引きずられてしまって、まともに学習ができていないのだと思われる。

特徴量を変換する際に元の値を残さないようにすると、正しく予測することができる。

class FizzBuzz2():
  def __init__(self, model_cls, param):
    self.model = model_cls(**param)

  @staticmethod
  def _convert_label(label):
    if type(label) is str:
      return label
    else:
      return "num"

  @staticmethod
  def _reverse_convert_label(n, label):
    if label == "num":
      return n
    else:
      return label

  @staticmethod
  def _convert_feature(n):
    # return [n] + [n % i for i in range(1, 20)]
    return [n % i for i in range(1, 20)] # ここが変わった

  @staticmethod
  def _fizzbuzz(n:int):
    if n % 15 == 0:
      return "FizzBuzz"
    if n % 5 == 0:
      return "buzz"
    if n % 3 == 0:
      return "Fizz"
    return n

  @staticmethod
  def generate_sample_data(size=1000):
    feature = np.random.randint(0, np.iinfo(np.int32).max, size)
    label = list(map(FizzBuzz._fizzbuzz, feature))

    return feature, label

  def fit(self, x, y):
    converted_x = list(map(self._convert_feature, x))
    converted_y = list(map(self._convert_label, y))
    
    self.model.fit(converted_x, converted_y)
    
  def predict(self, x):
    converted_x = list(map(self._convert_feature, x))
    converted_y_pred = self.model.predict(converted_x)
    y_pred = list(map(
        lambda x: self._reverse_convert_label(x[0], x[1]),
        zip(x, converted_y_pred)))
    return y_pred

model = FizzBuzz2(
    sklearn.neural_network.MLPClassifier,
    {
      "max_iter": 1000
    }
)

train_feature, train_label = FizzBuzz.generate_sample_data(10000)
model.fit(train_feature, train_label)

for i in range(1, 101):
  y_pred = model.predict([i])[0] # arrayを受け取ってarrayを返す関数なので、こうする
  y_truth = model._fizzbuzz(i)
  print(i, y_pred, y_truth, y_pred == y_truth)
1 1 1 True
2 2 2 True
3 Fizz Fizz True
4 4 4 True
5 buzz buzz True
6 Fizz Fizz True
7 7 7 True
8 8 8 True
9 Fizz Fizz True
10 buzz buzz True
11 11 11 True
12 Fizz Fizz True
13 13 13 True
14 14 14 True
15 FizzBuzz FizzBuzz True
16 16 16 True
17 17 17 True
18 Fizz Fizz True
19 19 19 True
20 buzz buzz True
21 Fizz Fizz True
22 22 22 True
23 23 23 True
24 Fizz Fizz True
25 buzz buzz True
26 26 26 True
27 Fizz Fizz True
28 28 28 True
29 29 29 True
30 FizzBuzz FizzBuzz True
31 31 31 True
32 32 32 True
33 Fizz Fizz True
34 34 34 True
35 buzz buzz True
36 Fizz Fizz True
37 37 37 True
38 38 38 True
39 Fizz Fizz True
40 buzz buzz True
41 41 41 True
42 Fizz Fizz True
43 43 43 True
44 44 44 True
45 FizzBuzz FizzBuzz True
46 46 46 True
47 47 47 True
48 Fizz Fizz True
49 49 49 True
50 buzz buzz True
51 Fizz Fizz True
52 52 52 True
53 53 53 True
54 Fizz Fizz True
55 buzz buzz True
56 56 56 True
57 Fizz Fizz True
58 58 58 True
59 59 59 True
60 FizzBuzz FizzBuzz True
61 61 61 True
62 62 62 True
63 Fizz Fizz True
64 64 64 True
65 buzz buzz True
66 Fizz Fizz True
67 67 67 True
68 68 68 True
69 Fizz Fizz True
70 buzz buzz True
71 71 71 True
72 Fizz Fizz True
73 73 73 True
74 74 74 True
75 FizzBuzz FizzBuzz True
76 76 76 True
77 77 77 True
78 Fizz Fizz True
79 79 79 True
80 buzz buzz True
81 Fizz Fizz True
82 82 82 True
83 83 83 True
84 Fizz Fizz True
85 buzz buzz True
86 86 86 True
87 Fizz Fizz True
88 88 88 True
89 89 89 True
90 FizzBuzz FizzBuzz True
91 91 91 True
92 92 92 True
93 Fizz Fizz True
94 94 94 True
95 buzz buzz True
96 Fizz Fizz True
97 97 97 True
98 98 98 True
99 Fizz Fizz True
100 buzz buzz True

というわけで、多層パーセプトロンでFizzBuzzは実現できたので、ディープラーニングでFizzBuzzもちゃんとできそうである。(だれかやって)

おわりに

機械学習でFizzBuzzはめんどくさい。そして任意のnでFizzBuzzの出力が正しく行えるかどうかを保証できないのがつらい。

多層パーセプトロンで予測が失敗したのは割と面白かった。こういうのでもちゃんと動くと思ってたんだけど、全結合だから生データを突っ込むと値が吹っ飛ぶのだなぁ。

これで顧客が「ディープラーニングで予測したとプレスリリースを書きたいので、ディープラーニングを使って作ってください!!」と要望してきても大丈夫だぞ。

まぁ、すでにTensorFlowでFizzBuzzはあるんだけどね
https://joelgrus.com/2016/05/23/fizz-buzz-in-tensorflow/

Discussion