ひょんなきっかけから機械学習に関する事を、お手伝いさせていただく事になった。自分にできることは何でもする精神で、毎日ベストを尽せるように一生懸命取り組んでいる。周りの方々はとても素晴しい方々で、そして優秀で、私が知らない事をたくさん知っている。私自身は、できる事を粛々とやっていこうと決めて、それを実行している。これまでは周りに助けていただいて何とか持っている球を打ち返す事ができていた。しかし、ここに来て投げられた球を打ち返す事が出来ない事が増えてきた。それは私の知識不足が原因だった。私は、基本的な機械学習の知識を持ち合わせていないため、その時に必要な最低限の知識をその都度調べて対応していた。しかし足りない知識の量が多すぎると、それも出来なくなる。概念をぼんやりとした状態で理解できた気分になることは出来る。ただ、それでは足りない。どんな形式の何のデータが、どこに保持されていて、それがどのような処理によって、どのように変化し、その後どこに保持されるのかといった事が分かる必要がある。分かった気分じゃだめなんだ。そこで機械学習についてのおさらいをする事にした。内容は「ゼロから作るDeep Learning」を読んで勉強した内容だ。
単純パーセプトロンで論理回路を作る
簡単な論理回路を実装する1。
def func(x1, x2):
val = (x1 * 0.5) + (x2 * 0.5) # 0.5は重み
if val <= 0.7: # 0.7は閾値
return 0
else:
return 1
print(func(0, 0))
print(func(0, 1))
print(func(1, 0))
print(func(1, 1))
実行する。
python3 AND.py
0 0 0 1
これはAND論理回路と同じ出力となる。このような機構を単純パーセプトロンと言う2。
この実装を修正して、バイアスとして扱えるようにする。
def func(x1, x2):
w1 = 0.5 # x1用の重み
w2 = 0.5 # x2用の重み
bias = -0.7 # バイアスによって発火しやすさが変わる
val = (
(x1 * w1) +
(x2 * w2) +
bias
)
if val <= 0:
return 0
else:
return 1
print(func(0, 0))
print(func(0, 1))
print(func(1, 0))
print(func(1, 1))
実行する。
python3 AND2.py
0 0 0 1
AND回路、OR回路、NAND回路は重みとバイアスの値を調整するだけで実現できる。重みとバイアスを変更する事で、各回路を実装する。
def curcit(w1, w2, bias):
def _func(x1, x2):
val = (
(x1 * w1) +
(x2 * w2) +
bias
)
if val <= 0:
return 0
else:
return 1
return _func
AND = curcit( 0.5, 0.5, -0.7)
NAND = curcit(-0.5, -0.5, 0.7)
OR = curcit( 0.5, 0.5, -0.2)
print("AND")
print(AND(0, 0))
print(AND(0, 1))
print(AND(1, 0))
print(AND(1, 1))
print("NAND")
print(NAND(0, 0))
print(NAND(0, 1))
print(NAND(1, 0))
print(NAND(1, 1))
print("OR")
print(OR(0, 0))
print(OR(0, 1))
print(OR(1, 0))
print(OR(1, 1))
実行する。
python3 SIMPLE_CURCIT.py
AND 0 0 0 1 NAND 1 1 1 0 OR 0 1 1 1
多層パーセプトロンでXOR回路を作る
XOR回路は単純パーセプトロンでは実現できないが、単純パーセプトロンで実装したAND回路とOR回路を組み合わせる事で実現できる。
この時の入力から見て1つめの回路(NANDとOR)が1層目、その次の回路(AND)が2層目とする。このような単純パーセプトロンが連なり、層を成しているもの全体を、多層パーセプトロンと言う。
def curcit(w1, w2, bias):
def _func(x1, x2):
val = (
(x1 * w1) +
(x2 * w2) +
bias
)
if val <= 0:
return 0
else:
return 1
return _func
AND = curcit( 0.5, 0.5, -0.7)
NAND = curcit(-0.5, -0.5, 0.7)
OR = curcit( 0.5, 0.5, -0.2)
def XOR(x1, x2):
s1 = NAND(x1, x2) # 各変数をノード、データの変換関係をエッジと考えると
s2 = OR(x1, x2) # ネットワークの構造と考える事ができる
return AND(s1, s2) # (ネトワークに見えないかもしれないけど)
print("XOR")
print(XOR(0, 0))
print(XOR(0, 1))
print(XOR(1, 0))
print(XOR(1, 1))
実行する。
python3 XOR.py
XOR 0 1 1 0
これらの単純パーセプトロンの繋りが、人工ニューラルネットワークとなる。
行列と演算でネットワークを表現する
ネットワークと言うと、どこかから線が出ていて、それがどこかに繋っていて、その間をデータが送信されるように思える。実際にそういうケースは多いのだが、人工ニューラルネットワークを表現する時には、利便性から行列を用いて、その計算処理とその結果を、ネットワークとして見立てる。
先程の例のAND回路を考えて見る。AND回路はx1とx2の2つの入力を取る事にしていた。
(x1, x2)
AND回路の場合、x1とx2には0か1が渡される。つまり次のような入力のパターンができる。
x1 | x2 |
---|---|
0 | 0 |
0 | 1 |
1 | 0 |
1 | 1 |
これをnumpyで行列として表現すると次のように4x2の行列として表現できる。
>>> import numpy as np
>>> a = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
>>> a
array([[0, 0],
[0, 1],
[1, 0],
[1, 1]])
aはnumpy.ndarrayという型の値となる。この型の先頭の要素は、長さ2のnumpy.ndarrayを持っている。
>>> type(a[0])
<class 'numpy.ndarray'>
>>> len(a[0])
2
このa[0]の先頭の要素a[0][0]はx1の入力、そん次の要素a[0][1]はx2の入力と考える事にする。
>>> a[0][0] # x1の入力と考える
0
>>> a[0][1] # x2の入力と考える
0
ここで再度、AND回路での引数と重みとの計算をしていたコードを確認する。
val = (
(x1 * w1) +
(x2 * w2) +
bias
)
x1の値にはx1用の重みw1を、x2にはx2用の重みであるw2をかけた後、それら2つ値とバイアスを足している。バイアスの加算は後で行う事とし、それぞれの引数に重みをかけ、結果の総和を取る処理に注目する。これは行列のドット積を計算する事で算出できる。
>>> w = np.array([0.5, 0.5]) # 重み
>>> s = a.dot(w) # ドット積を算出する
>>> s
array([0. , 0.5, 0.5, 1. ])
この値にバイアスを加算する。
>>> v = s + -0.7
>>> v
array([-0.7, -0.2, -0.2, 0.3])
>>>
そして、各値が0と比較し、0以下であれば0、そうでなければ1に変換する。
>>> output = np.array([0 if i <= 0 else 1 for i in v])
>>> output
array([0, 0, 0, 1])
各値がノードとなっており、ノード間でこの行列の計算を含む値の流れをネットワークと見たてている。図にすると次のようになる。
+---------------------------------------------------+
| AND |
| |
| 重み |
| | どの入力用? | 値 | |
| |--------------+-----| |
| | x1用 | 0.5 | |
| | x2用 | 0.5 | |
+-----------------+ | |
|INPUT | | | |
| | | +---------|----------------------------+ |
| | | | | ドット積 | |
| | | | | それぞれかけ算 | |
| | | | v | |
| | | | | x1 | x2 | | |
| | x1 | x2 | | | | |--------+--------| 行ごとに足し算 | |
| |----+----| ------------->| 0*0.5 | 0*0.5 | ---> | 0.0 | | |
| | 0 | 0 | | | | | 0*0.5 | 1*0.5 | | 0.5 | | |
| | 0 | 1 | | | | | 1*0.5 | 0*0.5 | | 0.5 | | |
| | 1 | 0 | | | | | 1*0.5 | 1*0.5 | | 1.0 | | |
| | 1 | 1 | | | | | |
| | | | | | |
+-----------------+ | +-----------------------------|--------+ |
| | |
| ----------------------------|------- |
| | |
| | |
| 足し算 v |
| バイアス --------------> | -0.7 | |
| -0.7 | -0.2 | |
| | -0.2 | |
| | 0.3 | |
| | |
| ----------------------------|------- |
| | |
| | 0以下なら0、 |
| | 0より大きければ1 |
| v |
| | 0 | |
| | 0 | |
| | 0 | |
| | 1 | |
| | |
--------------------------------|-------------------|
|
+----------|------+
|OUTPUT v |
| | 0 | |
| | 0 | |
| | 0 | |
| | 1 | |
+-----------------+
これをよく見るあの図で表現すると、次のようになる。
ここから入力の次元数が増えたり、出力の次元数が増えたり、中間にあたる層が増えたりロジックが複雑化していく。それにより、数値の変遷が複雑になり、ネットワークが複雑になっていく。
行列計算を用いてAND回路を実装する
AND回路を行列を用いて計算できるように、numpyを用いて実装しなおすと次のようになる。
import numpy as np
def AND(x: np.ndarray):
w = np.array([0.5, 0.5])
bias = -0.7
val = x.dot(w) + bias
return np.array([
0 if v <= 0 else 1 for v in val
])
input_data = np.array(
[
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
)
output_data = AND(input_data)
for v in output_data:
print(v)
実行すると同じ結果を得らえる。
python3 AND3.py
0 0 0 1
活性化関数とステップ関数
論理回路の例では、計算の結果が正の数であれば1、そうでなけば0として結果を返していた。論理回路のような単純なものであれば2値で対応可能だが、複雑な問題に対応しようとすると、前の層の出力を入力として次の層に渡す処理を、何層も追加する事になる。それらの値は2値ではない方が好ましい場合もある。この処理を個別に指定できるように関数として切り出す。これは活性化関数と呼ばれている。活性化関数には、よく使われるものとして、ステップ関数、シグモイド関数、ReLU関数がある。
ここではステップ関数を実装する。その他の関数は必要になった時に実装することにする。ステップ関数は引数を1つ取り、その値が正の数であれば1、そうでなければ0を返す。
def step(x):
if x > 0:
return 1
else:
return 0
print(step(0.1)) # => 1
print(step(0.0)) # => 0
print(step(-0.0)) # => 0
print(step(-0.1)) # => 0
実行する。
python3 activation-step.py
1 0 0 0
これを元にAND回路のコードを改良する。step()関数は、数値を受け取る関数と、numpy.ndarrayを受け取る関数の2つを用意した。通常は、numpy.ndarrayを受けとり、numpy.ndarrayを返す方が、次の層に値を渡しやすくなるため都合が良い。
from functools import singledispatch
import numpy as np
@singledispatch
def step(x):
pass
@step.register
def _(x: int) -> int:
if x > 0:
return 1
else:
return 0
@step.register
def _(x: np.ndarray) -> np.ndarray:
"""ndarray用ステップ関数
x > 0で値を比較し、TrueとFalseのndarrayに変換し、その後dtype=intによって整数
に変換して返す。
"""
return np.array(x > 0, dtype=int)
def AND(x: np.ndarray):
w = np.array([0.5, 0.5])
bias = -0.7
val = x.dot(w) + bias
return step(val)
input_data = np.array(
[
[0, 0],
[0, 1],
[1, 0],
[1, 1],
]
)
output_data = AND(input_data)
for v in output_data:
print(v)
実行する。
python3 AND4.py
0 0 0 1
単純パーセプトロンを多層に連ねる
これまでAND回路、OR、NAND回路は単純パーセプトロンで、XOR回路を多層パーセプトロンで実装した。多層パーセプトロンはもっと層を増やす事で、より複雑な問題に対処できる。XOR回路の例は、0層目が入力層、1層目と2層目が重みのある層、3層目が出力層といった構成になっていた。重みがあるのは1層目と2層目だから、このパーセプトロンは2層のパーセプトロンと考える事ができる。重みのある層の数が、そのパーセプトロンの層数となる。 ここでは1層増やして多層のパーセプトロンを作成する事にする。2層目、3層目は重みとバイアスが入力をそのまま返すように調整してあり、あまり意味のある実装になっていないが、多層のパーセプトロンとなっている。
import numpy as np
def step(x: np.ndarray) -> np.ndarray:
"""ステップ関数"""
return np.array(x > 0, dtype=int)
def identity(x: np.ndarray) -> np.ndarray:
"""恒等関数"""
return x
def forward_propagation(x, w, b, activation):
"""順伝播"""
return activation(x.dot(w) + b)
# --------------------
# 入力層
X = np.array([ # 4x2
[0, 0],
[0, 1],
[1, 0],
[1, 1],
])
# 1層目
W1 = np.array([ # 重み 2x1
[0.5],
[0.5],
])
B1 = np.array([-0.7]) # バイアス 1x1
# 2層目
W2 = np.array([ # 重み 1x1
[1.0],
])
B2 = np.array([0.0]) # バイアス 1x1
# 3層目
W3 = np.array([ # 重み 1x1
[1.0],
])
B3 = np.array([0.0]) # バイアス 1x1
# --------------------
A = step(X.dot(W1) + B1) # 1層目
# `
# |
# +-------------+
# |
# v
B = step(np.dot(A, W2) + B2) # 2層目
# `
# |
# +-----------------+
# |
# v
Y = identity(np.dot(B, W2) + B2) # 3層目
for x, y in zip(X, Y):
print(f"{x} => {y}")
実行する。
python3 LAYER3.py
[[0] [0] [0] [1]] [[0] [0] [0] [1]] [0 0] => [0.] [0 1] => [0.] [1 0] => [0.] [1 1] => [1.]
これをよく見るあの図で表現すると、次のようになる。
もっと複雑なネットワークを構築する
ここまでのネットワークは、構造がとてもシンプルだった。単純な問題は、この状態のネットワークでも重みとバイアスを手で調整する事で、解決できるかもしれない。問題が複雑になれば、全く解決できそうにない。重みの値は後で、処理の中で変化するように実装するが、ここではネットワークを複雑にする事を考える事にする。
先程実装したものは入力が2つに対し、1層以降の各層は1つのニューロンだった。しかし、よく見る例では、入力が2つに対し、1層目が3つのニューロンになっている。これは各層の重みの列数によって変化する。重みの行数は、常にその層の入力の列数になる。そうでないとドット積を計算できない。重みの列数は、その層のニューロンの数になる。この数は必要に応じて増やしたり、減らしたりできる。そして、その数が次の層の重みの行数になる。
import numpy as np
def step(x: np.ndarray) -> np.ndarray:
"""ステップ関数"""
return np.array(x > 0, dtype=int)
def identity(x: np.ndarray) -> np.ndarray:
"""恒等関数"""
return x
def forward_propagation(x, w, b, activation):
"""順伝播"""
return activation(x.dot(w) + b)
# --------------------
# 入力層
X = np.array([ # 4x2
[0, 0],
[0, 1],
[1, 0],
[1, 1],
])
# 1層目
W1 = np.array([ # 重み 2x3
[0.5, 0.0, 0.0],
[0.5, 0.0, 0.0],
])
B1 = np.array([-0.7, 0.0, 0.0]) # バイアス 3x1
# 2層目
W2 = np.array([ # 重み 3x3
[1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
])
B2 = np.array([0.0, 0.0, 0.0]) # バイアス 3x1
# 3層目
W3 = np.array([ # 重み 3x3
[1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
[1.0, 1.0, 1.0],
])
B3 = np.array([0.0, 0.0, 0.0]) # バイアス 3x1
# --------------------
A = step(X.dot(W1) + B1) # 1層目
# `
# |
# +-------------+
# |
# v
B = step(np.dot(A, W2) + B2) # 2層目
# `
# |
# +-----------------+
# |
# v
Y = identity(np.dot(B, W2) + B2) # 3層目
for x, y in zip(X, Y):
print(f"{x} => {y}")
実行する。
python3 LAYER3a.py
[0 0] => [0. 0. 0.] [0 1] => [0. 0. 0.] [1 0] => [0. 0. 0.] [1 1] => [3. 3. 3.]
これをよく見るあの図で表現すると、次のようになる。エッジは全て書くとゴチャゴチャになるから省略した。