終点枕崎

にゃーんo(^・x・^)o

深層学習

お久しぶりです。

最近、岡谷貴之先生の「深層学習」を読みました。楽しいです。

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)

大学1年次でやった微積線形代数がバンバンたくさん出てきて、やっと今年やった数学のありがたみが少しわかった気がします。

理論を学んだら今度は実装をしようと「ゼロから作るDeepLearning」を少し読んだわけですが、どうしても横ベクトルが受け付けられず、結局自前で実装することに決めました。

今回は定番の手書き文字認識をやります。なのでソースコード自体はスッキリしていると思います。今度は畳み込み層にも対応させたいな…

以下詳細について書きますが、初学者ゆえ、間違いやお見苦しい点が多々あるかもしれません。もしお気づきになられたら指摘していただけると幸いです。

学習の大まかな流れとしては(「深層学習」 p50 - p52 参照)、まずは順伝播。各層の総入力Uと出力Zはresultとして記憶しておいて、次の誤差逆伝播法で誤差関数の重さについての勾配を求めるのに使います。

デルタは逐次的に求め、重さに関しての偏微分を全て求めて、勾配法で重みを学習していきます。

出力層のデルタは、出力層の活性化関数と誤差関数の種類によって計算方法が違うので、そこだけ個別に対応します。

手書き文字認識では、MNISTというデータセットが著名らしいので、今回はそれを使用。28 * 28 のモノクロ画像と数字 0〜9 を対応させたデータセットです。

今回、学習に用いたニューラルネットは3層からなり、以下のような構成です。

入力 第1層 第2層 第3層
ユニット数 28 * 28 200 40 10
活性化関数 - ランプ関数 ランプ関数 ソフトマックス関数

分類問題なので、誤差関数は交差エントロピーを使用します。学習係数は学習するにつれ線形に減少させます。

ソースコード

  • test.py
import numpy as np
import neitwork as nw
import copy
import matplotlib.pyplot as plt
import pickle

def read_image(path):
	with open(path, mode = "rb") as f:
		src = f.read()
		num = int.from_bytes(src[4 : 8], "big")
		img_size = int.from_bytes(src[8 : 12], "big") * int.from_bytes(src[12 : 16], "big")
		img = [ x / 255.0 for x in src[16 : 16 + num * img_size]]
		return np.array(img).reshape(-1, img_size).T

def read_label(path):
	with open(path, mode = "rb") as f:
		src = f.read()
		num = int.from_bytes(src[4 : 8], "big")
		L = np.zeros((10, num))
		for i in range(num):
			L[src[8 + i]][i] = 1.0
		return L

def accuracy(N, X, T):
	Y = nw.forward_all(N, X)
	Y = np.argmax(Y, axis = 0)
	T = np.argmax(T, axis = 0)
	return np.sum(Y == T) / X.shape[1]

print("loading...", flush = True)
# 各データセットのバイナリファイルのパス
Xtr = read_image("train-images-idx3-ubyte")
Ttr = read_label("train-labels-idx1-ubyte")
Xts = read_image("t10k-images-idx3-ubyte")
Tts = read_label("t10k-labels-idx1-ubyte")
print("completed.", flush = True)

iter_num = 100
call_learn_num = 50
batch_size = 100
tr_size = Xtr.shape[1]
init_learning_rate = 1.0
goal_learning_rate = 1e-2
learning_rate = lambda j : init_learning_rate - (j / call_learn_num) * (init_learning_rate - goal_learning_rate)

N = nw.make_network(unit_num = [28 * 28, 200, 40, 10], func = [nw.ramp, nw.ramp, nw.soft_max])

train_accuracy_list = []
test_accuracy_list = []

#100イテレーションごとに計測
for j in range(call_learn_num):
	N = nw.learn(N, Xtr, Ttr, nw.cross_entropy_error, lambda Y, T : Y - T, iter_num, batch_size, learning_rate(j))
	train_accuracy = accuracy(N, Xtr, Ttr)
	test_accuracy = accuracy(N, Xts, Tts)
	train_accuracy_list.append(train_accuracy)
	test_accuracy_list.append(test_accuracy)
	print(train_accuracy, test_accuracy, flush = True)

epoch_num = iter_num * batch_size * call_learn_num / tr_size
x_axis = np.linspace(epoch_num / call_learn_num, epoch_num, call_learn_num)
plt.ylim([0, 1])
plt.plot(x_axis, np.array(train_accuracy_list), label = "train accuracy")
plt.plot(x_axis, np.array(test_accuracy_list), label = "test accuracy")
plt.legend()
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.savefig("1.png")

with open("save.pkl", mode = "wb") as f:
	pickle.dump(N, f)
  • neitwork.py
import numpy as np
import copy

class layer:
	def __init__(self, shape):
		self.W = np.empty(shape)
		self.b = np.empty(shape[0])
		self.f = ident

class forward_result:
	def __init__(self, shape):
		self.U = np.empty(shape)
		self.Z = np.empty(shape)

def make_network(unit_num, func, init_std = 0.01):
	ret = []
	layer_num = len(unit_num) - 1
	for i in range(layer_num):
		l = layer((unit_num[i + 1], unit_num[i]))
		l.W = np.random.standard_normal(l.W.shape) * init_std
		l.b = np.zeros(l.b.shape)
		l.f = func[i]
		ret.append(l)

	return ret

def forward_layer(l, X):
	ret = forward_result((l.W.shape[0], X.shape[1]))
	ret.U = np.dot(l.W, X) + np.dot(np.c_[l.b], np.ones(X.shape[1]).reshape(1, X.shape[1]))
	ret.Z = l.f(ret.U)

	return ret

def forward_all(network, X):
	for l in network:
		X = forward_layer(l, X).Z

	return X

def forward_all_with_result(network, X):
	ret = []

	I = forward_result(X.shape)
	I.Z = X
	ret.append(I)

	for l in network:
		ret.append(forward_layer(l, ret[-1].Z))

	return ret

def grad_bp(network, result, delta):
	layer_num = len(network)
	N = result[0].U.shape[1]

	ret = []

	Z = result[-2].Z
	dL = layer(network[-1].W.shape)
	dL.W = np.dot(delta, Z.T) / N
	dL.b = np.dot(delta, np.c_[np.ones(N)]).reshape(delta.shape[0]) / N
	ret.append(dL)

	for i in reversed(range(layer_num - 1)): #今 0-origin で i 層目のデルタを計算する
		W = network[i + 1].W # i + 1 層目のW
		U = result[i + 1].U # i 層目のU
		df = derivative_dict[network[i].f] # i 層目の活性化関数の導関数
		delta = df(U) * np.dot(W.T, delta)
		Z = result[i].Z # i - 1 層目のZ
		dL = layer(network[i].W.shape)
		dL.W = np.dot(delta, Z.T) / N
		dL.b = np.dot(delta, np.c_[np.ones(N)]).reshape(delta.shape[0]) / N
		ret.insert(0, dL)

	return ret

def learn(network, X, T, loss_function, last_delta, iter_num, batch_size, learning_rate):
	N = X.shape[1] # データ数
	layer_num = len(network)

	network = copy.deepcopy(network)

	for i in range(iter_num):
		batch_mask = np.random.choice(N, batch_size)
		x_batch = X.T[batch_mask].T
		t_batch = T.T[batch_mask].T

		result = forward_all_with_result(network, x_batch)
		y_batch = result[-1].Z
		grad = grad_bp(network, result, last_delta(y_batch, t_batch))

		for l in range(layer_num):
			network[l].W -= learning_rate * grad[l].W
			network[l].b -= learning_rate * grad[l].b

	return network

def logistic_sigmoid(x):
	return 1 / (1 + np.exp(-x))
def logistic_sigmoid_d(x):
	return 1 / (np.exp(0.5 * x) + np.exp(-0.5 * x)) ** 2

def tanh(x):
	return np.tanh(x)
def tanh_d(x):
	return 1 / np.cosh(x) ** 2

def ramp(x):
	return np.maximum(0, x)
def ramp_d(x):
	return 1 * (x > 0)

def ident(x):
	return x
def ident_d(x):
	return np.ones_like(x)

def soft_max(a):
	c = np.max(a, axis = 0, keepdims = True)
	exp_a = np.exp(a - c)
	sum_exp_a = np.sum(exp_a, axis = 0, keepdims = True)
	return exp_a / sum_exp_a

def mean_squared_error(y, t):
	return 0.5 * np.sum((y - t) ** 2, axis = 0, keepdims = True)

def cross_entropy_error(y, t):
	delta = 1e-7
	return -np.sum(t * np.log(y + delta), axis = 0, keepdims = True)

derivative_dict = { logistic_sigmoid : logistic_sigmoid_d, tanh : tanh_d, ramp : ramp_d, ident : ident_d }

活性化関数とその1階導関数の対応づけをどのようにするか悩んだのですが、もっといい方法ありそうです。

以下学習結果。
f:id:uchifuji:20180212165521p:plain
テストデータでの正解率は 97.84% 。若干過適合気味なのかな?これ以上時間をかけてもテストデータに対しての正確性は上がらないみたいです。畳み込みニューラルネットなどを使うと99%の大台に乗せることも可能らしいです。ロマンがありますね。

さて、次はいつになるかわかりませんが、畳み込みを実装して画像認識にも挑戦できたらいいなーって思っています。

終わり

参考文献

  • 「深層学習」岡谷 貴之
  • 「ゼロから作るDeepLearning」斎藤 康毅