Pythonで単純パーセプトロンの2値分類問題を機械学習させて解いてみた

2023年10月18日

Photo by Josh Riemer on Unsplash

以前の記事単純パーセプトロンをExcelに実装して2値分類問題を解いてみた。で取り上げた例題をPythonに実装して解いてみました。

 

◆仕事や勉強の息抜きに。。。

2値分類問題の例題

倉庫の中の従業員が熱射病になり易い条件を調べるために、次のようなデータを採りました。

 

グラフにすると次のようになります。

 

このグラフの点線のように、熱射病になる境目の条件をニューラルネットワークで求めてみましょう。

ニューラルネットワークといっても直線で分類できるため、ニューロンが1つしかない単純パーセプトロンで解くことができます。

 

単純パーセプトロンに定式化する

求めたいのは温度と湿度で表される一次式なので、次のように表現できます。

$$温度✕w_0+湿度✕w_1+b=0$$

この式における\(w_0\)、\(w_1\)、\(b\)を、15組ある温度と湿度のデータから求めていきます。

 

単純パーセプトロンをExcelに実装して2値分類問題を解いてみた。で解説したように熱射病になるデータでは\(温度✕w_0+湿度✕w_1+b\)の値はプラスになり、熱射病にならないデータではマイナスになります。

そこで今回は、\(温度✕w_0+湿度✕w_1+b\)の値をソフトサイン関数に入れて、\(-1\)から\(+1\)の値に変換することにします。

ソフトサイン関数については下記の記事で解説していますが、

$$f(x) = x/(1+|x|)$$

で表され、その導関数(微分)は

$$f'(x) = 1/(1+|x|)^2$$

になります。

ソフトプラス関数とソフトサイン関数の逆伝播関数を計算グラフで求める

これを使ってバックプロパゲーション(誤差逆伝播法)をPythonに実装することにします。

本当は活性化関数としてソフトサイン関数を噛ませる必要もないと思いますが、バックプロパゲーションをPythonに実装する練習としてあえて噛ませることにしました。

 

Pythonに実装する

15組の温度と湿度データを入力として、ニューロンを通った後の値(入力の線形和に活性化関数を通した値)が+1(熱射病になる場合)と-1(熱射病にならない場合)に正しく分類されるように重み\(w_0\)、\(w_1\)とバイアス\(b\)の値を機械学習させます。

 

入力を標準化する

まずは各入力と正解値(+1か-1)を対応させたデータを作り、それをNumpyのリストに格納します。

# データをリストに格納
input_data_0 = [[27, 53.2, -1], [28.6, 38.9, -1], [28.3, 37.7, -1], [23.8, 72.7, -1], [27.0, 68.8, 1], [26.3, 52.6, -1], [30.1, 42.6, -1], [23.4, 37.4, -1], [25.4, 36.8, -1], [28.5, 76.9, 1], [24.5, 46.3, -1], [24.9, 37.7, -1], [31.2, 76.3, 1], [31.9, 49.4, 1], [33.0, 71.3, 1]]
input_data = np.array(input_data_0)
temp_data = input_data[:15, 0].copy()
humi_data = input_data[:15, 1].copy()

次にこの2つの入力について、平均が0になるように標準化します。

温度は30前後ですが、湿度は70くらいになることもあるため、両者の大きさを揃えてどちらか一方の影響が大きくなりすぎないようにするためです。

# 標準化(データの平均値を0に変換)
temp_ave = np.average(input_data[:15, 0])
temp_data -= temp_ave
humi_ave = np.average(input_data[:15, 1])
humi_data -= humi_ave

# 標準化後のデータをリストに格納
train_data = []
for i in range(15):
    correct = input_data[:15, 2]
    train_data.append([temp_data[i], humi_data[i], correct[i]])

これでtrain_dataの中に15組の入力データそれに対応する正解値(教師データ)が格納されました。

 

入力値の総和+活性化関数のクラスを定義する

次に、順伝播の計算で繰り返し行う入力の総和と活性化関数の計算を、クラスとして定義しておきます。

その前に、まずはソフトサイン関数を定義しておきます。

# ソフトサイン関数(活性化関数)の定義
def softsign(x):
    return x / (1.0 + np.abs(x))

次に入力値が入ってきたらそれらを合計し、それをソフトサイン関数に通すクラスを定義します。

まずは、このクラスの中で使う変数の初期化を行います。

入力値の総和をinput_sum、それをソフトサイン関数に入れて計算した出力値をoutputという変数とします。

クラスの名前をNeuronとすると次のようにコーディングできます。

# 入力値の和を計算しソフトサイン関数を通すクラスを定義
class Neuron:
def __init__(self):  # 初期設定
    self.input_sum = 0.0
    self.output = 0.0

次に、inpという値(引き数)を受け取るたびに、それらを合計してinput_sumに格納するクラス内関数(メソッド)を定義します。

def set_input(self, inp):
    self.input_sum += inp

次に、その総和をソフトサイン関数に入れて計算された値を変数outputとして返すメソッドを定義します。

def get_output(self):
    self.output = softsign(self.input_sum)
    return self.output

これにより、このNeuronという名前のクラスをインスタンス化した後に何度も呼び出して繰り返し使う準備が整いました。

 

順伝播と逆伝播のクラスを定義する

次に、ニューラルネットワークにおける順伝播逆伝播を計算するクラスを定義します。

順伝播は入力に重み\(w_0\)、\(w_1\)を掛けた線形和にバイアス\(b\)を足し、それを活性化関数に通して推定値を計算することですので、入力1(温度)に\(w_0\)を掛けた値と、入力2(湿度)に\(w_1\)を掛けた値と、バイアス\(b\)を先に定義したクラスのget_outputメソッドに渡せば計算されるはずです。

クラスの名称をNeuralNetworkとし、その中で順伝播を計算するメソッドをcommitとすると次のようにコーディングできます。

# 順伝播と逆伝播を計算するクラスを定義
class NeuralNetwork:
    def __init__(self):  # 初期設定
        # 各層の宣言
        self.input_layer = [0.0, 0.0]  # 入力を0に初期化
        self.output_layer = [Neuron()]  # 出力はニューロンの計算結果

    def commit(self, input_data):  # 順伝播の計算
        # 入出力のセット
        self.input_layer[0] = input_data[0]
        self.input_layer[1] = input_data[1]
        self.output_layer[0].reset()

        # 入力の線形和を計算後、ソフトサイン関数を通す
        self.output_layer[0].set_input(self.input_layer[0] * self.w_mo[0][0])
        self.output_layer[0].set_input(self.input_layer[1] * self.w_mo[0][1])
        self.output_layer[0].set_input(self.b_o[0])
        return self.output_layer[0].get_output()

逆伝播については少し難しいですが、順伝播で計算した結果を再利用します。

逆伝播の計算式は

\(w_0 = 前回のw_0 – (順伝播で計算された出力 – 正解値) * 活性化関数の導関数*入力1*学習率\)

\(w_1 = 前回のw_1 – (順伝播で計算された出力 – 正解値) * 活性化関数の導関数*入力2*学習率\)

\(b = 前回のb – (順伝播で計算された出力 – 正解値) * 活性化関数の導関数*学習率\)

で計算されます。

これらの式の中で「順伝播で計算された出力」とは先に計算した順伝播の計算結果そのものです。

また、活性化関数(ソフトサイン関数)の導関数は\(1/(1+|x|)^2\)ですが、この中の変数xは順伝播の計算の途中で計算される活性化関数を通す前の値です。

これは先のクラスの中の変数であるinput_sumに入っているので、それを使えばよいでしょう。

逆伝播を計算するメソッドをtrainとすると、次のようにコーディングできます。

    def train(self, correct):  # 逆伝播の計算
        k = 0.01  # 学習率
        output_o = self.output_layer[0].output  # 順伝播の出力
        input_i = self.output_layer[0].input_sum  # 順伝播の途中結果(入力の線形和)
        delta_o = (output_o - correct) / (1.0 + np.abs(input_i))**2  # δOの計算


        # 重みとバイアスの更新
        self.w_mo[0][0] -= k * delta_o * self.input_layer[0]
        self.w_mo[0][1] -= k * delta_o * self.input_layer[1]
        self.b_o[0] -= k * delta_o

\((順伝播で計算された出力 – 正解値) * 活性化関数の導関数\)は通称\(δ_o\)と呼ばれているので、コードの中ではそのように書いています。

 

散布図で表示する

以上により、15組の入力データを学習させて重みを更新することができます。

次に、その更新された重みにより、どのように分類されるようになったかを散布図で表示するための関数を作ります。

そのために、まず熱中症として分類される入力と熱中症として分類されない入力それぞれを格納するための空のリストを作ります。

# 散布図表示関数の定義
def show_graph(epoch):
    print("繰り返し回数:", epoch)
    no_predicted = [[], []]  # 熱中症 Noの入力を入れる空のリスト
    yes_predicted = [[], []]  # 熱中症 Yesの入力を入れる空のリスト

次に、順伝播の計算結果(推定値)の正負を判断し正であれば熱中症Yesのリストに、負であれば熱中症Noのリストに入れます。

この時、標準化したデータに平均値を足して元の温度や湿度のデータに戻します。

for data in train_data:
    if neural_network.commit(data) < 0:  # 正負の判断
        no_predicted[0].append(data[0]+temp_ave)  # データを入れる時に標準化データを元に戻す
        no_predicted[1].append(data[1]+humi_ave)  # データを入れる時に標準化データを元に戻す
    else:
        yes_predicted[0].append(data[0]+temp_ave)  # データを入れる時に標準化データを元に戻す
        yes_predicted[1].append(data[1]+humi_ave)  # データを入れる時に標準化データを元に戻す

あとはmatplotlibを使って散布図を描きます。

# 分類結果をグラフ表示
plt.scatter(no_predicted[0], no_predicted[1], label="No")
plt.scatter(yes_predicted[0], yes_predicted[1], label="Yes")
plt.legend()
plt.xlabel("Temperature (degree)")
plt.ylabel("Humidity (%)")
plt.show()

 

繰り返し学習させる

これで機械学習させる部品は整いましたので、15組の入力データを読み込みながら順伝播と逆伝播を繰り返すことを何回も繰り返します。

それにより重みが適切な値に更新されていくはずです。

ここでは300回繰り返しますが、毎回15組の入力データの順番をシャッフルさせます。

そして、1回目、50回目、100回目、200回目、300回目の散布図を表示させます。

重みの初期値は適当にw_0とw_1を\(-1\)、bを\(1\)としました。

# 学習させながら途中結果を表示
for t in range(0, 300):
    random.shuffle(train_data)
    for data in train_data:
        neural_network.commit(data[:2])  # 順伝播を実行
        neural_network.train(data[2])  # 逆伝播を実行
    if t+1 in [1, 50, 100, 200, 300]:
        show_graph(t+1)

すると結果は次のようになりました。

 

 

このように、最初はほぼ真逆だった分類が回数を重ねるたびに更新され、300回目では正しい分類になったことが確認できました。