top
おさだのホームページ

2次元の簡単な線形分離モデルを自作し、
人工知能(深層学習)の構成単位であるパーセプトロンについて少し理解する

はじめに
巷で”人工知能(AI)”と言えばもっぱら深層学習を指すようになり、ユーザーの趣向に合わせた広告表示や指紋認証、天気予報、企業戦略、工場での品質管理などその応用分野は多岐に渡る。 それと同時に、深層学習が社会に出回っていくにつれて実際にそれらに触れる機会が増え、SFの世界にあった人間のように生活したり会話したりする”人工知能”の姿はだいぶ薄れたと思う。 今回はそんな深層学習の基礎的研究である「パーセプトロン」について具体的なコードを中心に解説し、深層学習を理解することの一助になれば良いなと思う。

andサンプル
(AND素子の学習の様子)
このページでは上の画像のような線形分離モデルを作成する。

1. パーセプトロンとは

深層学習(特にニューラルネットワーク)は人間の脳細胞の構造を再現し、人間が赤ちゃんから大人になるまでに様々なものを学習していくことと同じように、データから法則性を学習していくプログラムである。 人間の脳細胞はニューロンと呼ばれ、各ニューロンがネットワーク上に繋がってシナプスという電気信号を伝え合い、最終的にシナプスがどこに到達したかによって人間は様々に思考している。 それと似たように深層学習の構成単位はパーセプトロンと呼ばれ、各パーセプトロンがネットワーク上に繋がって数値を伝え合い、最終的に数値がどうなっているかによって様々な出力を実現している。

ではまず、ニューロンを数式として表す。
ニューロンは下の画像のような構造をしている。



ニューロンは各シナプスの入力値の合計が一定値を超えると次のニューロンへシナプスを出す仕組みになっている。 ここで、シナプスの入力をX1, X2, X3, •••とし、出力をY、しきい値をZとすると、

 
Xの合計 = a*X1 + b*X2 + c*X3 + ••• + 定数

if( Xの合計 > Z ){
    Y = 1
}else{
    Y = 0
}

シナプスの出力( Y )


上記のアルゴリズムで表すことができる。このようなアルゴリズムによって動くものをパーセプトロンと呼び、たくさんのパーセプトロンがネットワーク上に繋がったものをニューラルネットワークと呼ぶ。


これ以降でAND素子とOR素子のパーセプトロンを作成していく。その理由は、全ての電子回路はこのAND素子、OR素子、そしてNOT素子で構成されていて、ニューラルネットワークもその例外ではないからである。 NOT素子の作成は簡単であるので今回は紹介しない。


2. AND素子の学習

AND演算とは入力が全て1であった時のみ1を出力し、それ以外の時は0を出力するものである。また、記号は「 ∧ 」で表される。
よって、期待される結果は
    0 ∧ 0 = 0
    1 ∧ 0 = 0
    0 ∧ 1 = 0
    1 ∧ 1 = 1
である。

まずはいきなりAND素子の学習コードをそのまま紹介する。


2-1. AND素子の学習コード

入力はx, yの二つ。
copy
learn_and.py
import random

# ANDのパーセプトロンの定義
def _and_(x, y, coe_x, coe_y, intercept):
    return 1 if(coe_x*x + coe_y*y + intercept > 0) else 0

# coe_x:xの係数、coe_y:yの係数、intercept:z軸の切片、LEARNING_RATE:一回の学習で変動させる数値、LIMIT:実行回数
coe_x = random.random()
coe_y = random.random()
intercept = random.random()
LEARNING_RATE = 0.01
LIMIT = 1000

# 学習
for i in range(LIMIT):
    x = random.randint(0, 1)
    y = random.randint(0, 1)
    if(_and_(x, y, coe_x, coe_y, intercept)):
        if not x or not y: # x = 0 または y = 0
            intercept -= LEARNING_RATE
            if not x and y: # x,y = 0,1
                coe_y -= LEARNING_RATE
            if x and not y : # x,y = 1,0
                coe_x -= LEARNING_RATE
    else:
        if x and y:
            coe_x += LEARNING_RATE
            coe_y += LEARNING_RATE
            intercept += LEARNING_RATE

# 結果の出力
print(f"0 \u2227 0 = {_and_(0, 0, coe_x, coe_y, intercept)}")
print(f"1 \u2227 0 = {_and_(1, 0, coe_x, coe_y, intercept)}")
print(f"0 \u2227 1 = {_and_(0, 1, coe_x, coe_y, intercept)}")
print(f"1 \u2227 1 = {_and_(1, 1, coe_x, coe_y, intercept)}")

実行結果
 
0 ∧ 0 = 0
1 ∧ 0 = 0
0 ∧ 1 = 0
1 ∧ 1 = 1

期待通りの出力なので、学習成功である。


2-2. 解説

まずはAND素子のパーセプトロンを定義する。

copy
 
def _and_(x, y, coe_x, coe_y, intercept):
    return 1 if(coe_x*x + coe_y*y + intercept > 0) else 0

先程紹介したアルゴリズム通り (係数*入力)の合計 > しきい値 の判定となるように、

if( coe_x*x + coe_y*y + 切片 > 0 ){
    return 1
}else{
    return 0
}

※ coe : coefficient(係数)

とした。




次に変数、定数の定義である。各変数の初期値は乱数で決める。

copy
 
coe_x = random.random()
coe_y = random.random()
intercept = random.random()
LEARNING_RATE = 0.01
LIMIT = 1000

xの係数 : coe_x = random.random()

yの係数 : coe_y = random.random()

z軸の切片: intercept = random.random()

一回の学習で各変数に加えたり引いたりする数(この数値が小さければ小さいほど正確な学習となる) : 
LEARNING_RATE = 0.01

学習の繰り返し回数(この数値が大きければ大きいほど正確な学習となる) : 
LIMIT = 1000




そしてLIMITの値の分だけ学習をする。

copy
 
for i in range(LIMIT):

    # xとyを、0か1のどちらかにランダムで決める。

    x = random.randint(0, 1)
    y = random.randint(0, 1)

    # そして、AND素子のパーセプトロンにxとyを通し、その結果から係数や切片の値を変更していく。

    if(_and_(x, y, coe_x, coe_y, intercept)):
    # もし _and_(x, y) == 1 の時

        if not x or not y:
        # もし x == 0 または y == 0 ならば

            intercept -= LEARNING_RATE
            # zの切片を下げる

            if not x and y:
            # もし x == 0, y == 1 ならば

                coe_y -= LEARNING_RATE
                yの係数を下げる

            if x and not y:
            # もし x == 1, y == 0 ならば

                coe_x -= LEARNING_RATE
                xの係数を下げる

    else:
    # もし _and_(x, y) == 0 の時

        if x and y:
        # もし x == 1, y == 1 ならば

            coe_x += LEARNING_RATE
            coe_y += LEARNING_RATE
            intercept += LEARNING_RATE
            # 全変数を上げる


2-3. 学習の様子を可視化する

グラフに描画して学習の様子を可視化してみる。下の折り畳み欄にコードを記載したが読みとばしていただいても構わない。

copy
learn_and.py (可視化込み)
展開
折り畳む
import random
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import os

# 作業ディレクトリの指定。自分の好きなところに設定してください。
os.chdir("../images")


# AND素子のパーセプトロンの定義
def _and_(x, y, coe_x, coe_y, intercept):
    return 1 if(coe_x*x + coe_y*y + intercept > 0) else 0

# 同じ要素が続いているかどうかの判定
def conse(ps):
    result = [1]
    for i in range(1, len(ps)):
        result.append(0 if (ps[i-1] == ps[i]) else 1)
    return result

# x,yの二変数関数からxの一変数関数への変換
def linear_func(x, coe_x, coe_y, intercept):
    return (-1*coe_x*x - intercept)/coe_y

coe_x = random.random()
coe_y = random.random()
intercept = random.random()
LEARNING_RATE = 0.01
LIMIT = 1000

params = {"coe_x":[coe_x], "coe_y":[coe_y], "intercept":[intercept]}

# 学習
for i in range(LIMIT):
    x = random.randint(0, 1)
    y = random.randint(0, 1)
    if(_and_(x, y, coe_x, coe_y, intercept)):
        if not x or not y: # x,y = ?,0 or 0,?
            intercept -= LEARNING_RATE
            if not x and y: # x,y = 0,1
                coe_y -= LEARNING_RATE
            if x and not y: # x,y = 1,0
                coe_x -= LEARNING_RATE
    else:
        if x and y:
            coe_x += LEARNING_RATE
            coe_y += LEARNING_RATE
            intercept += LEARNING_RATE

    params["coe_x"].append(coe_x)
    params["coe_y"].append(coe_y)
    params["intercept"].append(intercept)

# グラフに描画する配列の定義
params_shaped = {"coe_x":[], "coe_y":[], "intercept":[]}
coe_x_conse = conse(params["coe_x"])
coe_y_conse = conse(params["coe_y"])
coe_i_conse = conse(params["intercept"])

# 重複する要素を排除して先程の配列に格納
for i in range(len(coe_i_conse)):
    if (coe_x_conse[i] or coe_y_conse[i] or coe_i_conse[i]):
        params_shaped["coe_x"].append(params["coe_x"][i])
        params_shaped["coe_y"].append(params["coe_y"][i])
        params_shaped["intercept"].append(params["intercept"][i])


# 結果の出力
print(f"0 \u2227 0 = {_and_(0, 0, coe_x, coe_y, intercept)}")
print(f"1 \u2227 0 = {_and_(1, 0, coe_x, coe_y, intercept)}")
print(f"0 \u2227 1 = {_and_(0, 1, coe_x, coe_y, intercept)}")
print(f"1 \u2227 1 = {_and_(1, 1, coe_x, coe_y, intercept)}")


# グラフの描画領域の定義
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
plt.xlim(-1,2)
plt.ylim(-1,2)
images = []
xs, ys = [], []
xs2, ys2 = [], []

# グラフの描画
for p in range(len(params_shaped["intercept"])):
    image = ax.plot([-1, 2],
                    [linear_func(-1, params_shaped["coe_x"][p], params_shaped["coe_y"][p], params_shaped["intercept"][p]),
                     linear_func(2, params_shaped["coe_x"][p], params_shaped["coe_y"][p], params_shaped["intercept"][p])],
                    c="#cc11cc")
    xs.append(1)
    ys.append(1)
    point = ax.scatter(xs, ys, c="#cc1111")
    xs2.append([0, 1, 0])
    ys2.append([0, 0, 1])
    point2 = ax.scatter(xs2, ys2, c="#616161")
    images.append(image + [point] + [point2])

ADD_PLOT = 10
LEN_PARAMS = len(params_shaped["intercept"])-1
for a in range(ADD_PLOT):
    image = ax.plot([-1, 2],
                    [linear_func(-1, params_shaped["coe_x"][LEN_PARAMS], params_shaped["coe_y"][LEN_PARAMS], params_shaped["intercept"][LEN_PARAMS]),
                     linear_func(2, params_shaped["coe_x"][LEN_PARAMS], params_shaped["coe_y"][LEN_PARAMS], params_shaped["intercept"][LEN_PARAMS])],
                    c="#ff1111")
    xs.append(1)
    ys.append(1)
    point = ax.scatter(xs, ys, c="#cc1111")
    xs2.append([0, 1, 0])
    ys2.append([0, 0, 1])
    point2 = ax.scatter(xs2, ys2, c="#616161")
    images.append(image + [point] + [point2])

anime = animation.ArtistAnimation(fig, images, interval=100, repeat_delay=100)
anime.save("and_learn.gif", writer="pillow")
plt.show()


学習0 学習1

面白い学習をしたもの↓
学習2 学習3 学習4 学習5


上の画像たちを見ていただくと、0と判定する点(黒)と1と判定する点(赤)を一本の直線で分離できるまで学習をしていることがわかる。


3. OR素子の学習

OR演算とは入力のいずれかが1であった時に1を出力し、それ以外の時は0を出力するものである。また、記号は「 ∨ 」で表される。
よって、期待される結果は
    0 ∨ 0 = 0
    1 ∨ 0 = 1
    0 ∨ 1 = 1
    1 ∨ 1 = 1
である。

それではまたコードを紹介する。


3-1. OR素子の学習コード

入力はx, yの二つ。

copy
learn_or.py
import random

# OR素子のパーセプトロンの定義
def _or_(x, y, coe_x, coe_y, intercept):
    return 1 if(coe_x*x + coe_y*y + intercept > 0) else 0

coe_x = random.random()
coe_y = random.random()
intercept = random.random()
LEARNING_RATE = 0.01
LIMIT = 1000

# 学習
for i in range(LIMIT):
    x = random.randint(0, 1)
    y = random.randint(0, 1)
    if(_or_(x, y, coe_x, coe_y, intercept)):
        if not x and not y: # x,y = 0,0
            intercept -= LEARNING_RATE
    else:
        if x and y: # x,y = 1,1
            coe_x -= LEARNING_RATE
            coe_y -= LEARNING_RATE
            intercept -= LEARNING_RATE
        elif x: # x = 1
            coe_x -= LEARNING_RATE
            intercept -= LEARNING_RATE
        elif y: # y = 1
            coe_y -= LEARNING_RATE
            intercept -= LEARNING_RATE

# 結果の出力
print(f"0 \u2228 0 = {_or_(0, 0, coe_x, coe_y, intercept)}")
print(f"1 \u2228 0 = {_or_(1, 0, coe_x, coe_y, intercept)}")
print(f"0 \u2228 1 = {_or_(0, 1, coe_x, coe_y, intercept)}")
print(f"1 \u2228 1 = {_or_(1, 1, coe_x, coe_y, intercept)}")

実行結果
 
0 ∨ 0 = 0
1 ∨ 0 = 1
0 ∨ 1 = 1
1 ∨ 1 = 1

これも期待通りの結果なので学習成功である。



3-2. 学習の様子を可視化する

これについてもグラフに描画して学習の様子を可視化してみる。下の折り畳み欄にコードを記載したが読みとばしていただいても構わない。

copy
learn_or.py (可視化込み)
展開
折り畳む
import random
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import os

# 作業ディレクトリの指定。自分の好きなところに設定してください。
os.chdir("../images")


# OR素子のパーセプトロンの定義
def _or_(x, y, coe_x, coe_y, intercept):
    return 1 if(coe_x*x + coe_y*y + intercept > 0) else 0


# 同じ要素が続いているかどうかの判定
def conse(ps):
    result = [1]
    for i in range(1, len(ps)):
        result.append(0 if (ps[i-1] == ps[i]) else 1)
    return result


# x,yの二変数関数からxの一変数関数への変換
def linear_func(x, coe_x, coe_y, intercept):
    return (-1*coe_x*x - intercept)/coe_y

coe_x = random.random()
coe_y = random.random()
intercept = random.random()
LEARNING_RATE = 0.01
LIMIT = 1000

params = {"coe_x":[coe_x], "coe_y":[coe_y], "intercept":[intercept]}


# 学習
for i in range(LIMIT):
    x = random.randint(0, 1)
    y = random.randint(0, 1)
    if(_or_(x, y, coe_x, coe_y, intercept)):
        if not x and not y: # x,y = 0,0
            intercept -= LEARNING_RATE
    else:
        if x and y:
            coe_x -= LEARNING_RATE
            coe_y -= LEARNING_RATE
            intercept -= LEARNING_RATE
        elif x:
            coe_x -= LEARNING_RATE
            intercept -= LEARNING_RATE
        elif y:
            coe_y -= LEARNING_RATE
            intercept -= LEARNING_RATE

    params["coe_x"].append(coe_x)
    params["coe_y"].append(coe_y)
    params["intercept"].append(intercept)


# グラフに描画する配列の定義
params_shaped = {"coe_x":[], "coe_y":[], "intercept":[]}
coe_x_conse = conse(params["coe_x"])
coe_y_conse = conse(params["coe_y"])
coe_i_conse = conse(params["intercept"])


# 重複する要素を排除して先程の配列に格納
for i in range(len(coe_i_conse)):
    if (coe_x_conse[i] or coe_y_conse[i] or coe_i_conse[i]):
        params_shaped["coe_x"].append(params["coe_x"][i])
        params_shaped["coe_y"].append(params["coe_y"][i])
        params_shaped["intercept"].append(params["intercept"][i])


# 結果の出力
print(f"0 \u2228 0 = {_or_(0, 0, coe_x, coe_y, intercept)}")
print(f"1 \u2228 0 = {_or_(1, 0, coe_x, coe_y, intercept)}")
print(f"0 \u2228 1 = {_or_(0, 1, coe_x, coe_y, intercept)}")
print(f"1 \u2228 1 = {_or_(1, 1, coe_x, coe_y, intercept)}")


# グラフの描画領域の定義
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
plt.xlim(-1,2)
plt.ylim(-1,2)
images = []
xs, ys = [], []
xs2, ys2 = [], []


# グラフの描画
for p in range(len(params_shaped["intercept"])):
    image = ax.plot([-1, 2],
                    [linear_func(-1, params_shaped["coe_x"][p], params_shaped["coe_y"][p], params_shaped["intercept"][p]),
                     linear_func(2, params_shaped["coe_x"][p], params_shaped["coe_y"][p], params_shaped["intercept"][p])],
                    c="#cc11cc")
    xs.append(0)
    ys.append(0)
    point = ax.scatter(xs, ys, c="#616161")
    xs2.append([1, 1, 0])
    ys2.append([1, 0, 1])
    point2 = ax.scatter(xs2, ys2, c="#cc1111")
    images.append(image + [point] + [point2])

ADD_PLOT = 10
LEN_PARAMS = len(params_shaped["intercept"])-1
for a in range(ADD_PLOT):
    image = ax.plot([-1, 2],
                    [linear_func(-1, params_shaped["coe_x"][LEN_PARAMS], params_shaped["coe_y"][LEN_PARAMS], params_shaped["intercept"][LEN_PARAMS]),
                     linear_func(2, params_shaped["coe_x"][LEN_PARAMS], params_shaped["coe_y"][LEN_PARAMS], params_shaped["intercept"][LEN_PARAMS])],
                    c="#ff1111")
    xs.append(0)
    ys.append(0)
    point = ax.scatter(xs, ys, c="#616161")
    xs2.append([1, 1, 0])
    ys2.append([1, 0, 1])
    point2 = ax.scatter(xs2, ys2, c="#cc1111")
    images.append(image + [point] + [point2])

anime = animation.ArtistAnimation(fig, images, interval=100, repeat_delay=100)
anime.save("or_learn_.gif", writer="pillow")
plt.show()

学習0 学習1 学習2

条件分岐が単純なのでAND素子の学習を比べると面白みに欠けるが、しっかり線形分離できていることがわかる。



4. 線形分離モデルと非線形分離モデル

これまで紹介したものは線形分離モデルであるが、これの適用が困難であるものが存在する。 例えばXOR素子を学習させようとした際、期待される結果は
    0 ⊻ 0 = 0
    1 ⊻ 0 = 1
    0 ⊻ 1 = 1
    1 ⊻ 1 = 0
である。

これらの点を 0=黒、1=赤 でグラフに描画すると、

xor

こうなるが、これを一本の直線で分離することは不可能である。(脳トレ的な問題ではなく、本当にできない)
しかし、直線を2本使ったり、曲線を使ったりすれば分離できる。

xor_2_line xor_not_linear
(手書きのイメージ図)

また、現在「深層学習」と呼ばれるものはこの非線形分離モデルを中心としたものである。 このモデルはパーセプトロンをはじめとするニューロンを階層に分けて学習させることで実装できる。 実際に、XOR素子の学習は入力を受け取る2つのシグモイドニューロンと、その2つをまとめる1つのパーセプトロンで実装可能である。 これにはシグモイド関数や微分、行列計算の利用が不可欠であり複雑になるので、気が向いたら別の記事にしてまとめようと思う。