데이터로부터 학습
신경망의 특징?장점?은 데이터를 보고 학습할 수 있다는 것이다.
그리고 여기에서 사람이 매개변수?같은 것들을 수정해 줄 필요가 없다.
그냥 단순히 데이터를 흘려주기만 하면 되는 것이다.

이렇게 무언가를 분류하는 작업에서 0, 1, 2... 이런 숫자들의 특징을 사람이 알려주는 것이 아닌 직접 특징들을 찾아서 분류할 수 있도록 만든다는 것이다.
손실함수
신경망에서는 결국 값을 학습시켜서 최소의 오차를 만들어야하고, 그 때의 매개변수들을 찾아야 한다.
그렇기위해 사용하는 것이 손실 함수이다.
대표적인 손실함수인 오차제곱합과 교차 엔트로피를 알아보자.
- 오차제곱합
수식은 다음과 같다.

yk는 신경망의 출력
tk는 정답 레이블
k는 차원의 수를 나타낸다.
손글씨 인식을 예로 들면

y는 소프트 맥스 함수에서의 출력이고, t는 정답을 가르킨다.
여기서 답은 1의 위치인 0이고, 신경망은 그 확률을 90%로 예측 한 것을 볼 수 있다.
저 수식을 파이썬 함수로 만들어보면
def sum_squares_error(x, t):
return 0.5 * np.sum((y - t) ** 2)
위와 같다.
이 함수로 저 값의 오차를 계산해보면

위와 같이 나오며, 만약 신경망이 0이 아닌 1로 값을 예측했다면

위와같이 오차가 높아지는 것을 볼 수 있다.
- 교차 엔트로피 오차
다른 손실 함수인 교차 엔트로피 오차이다.
수식은 다음과 같다.

yk는 신경망의 출력
tk는 정답 레이블이다.
그냥 단순하게 정답의 원소만 1이기에, 그 확률에 해당하는 값을 로그로 취해 모두 더한 값의 음수이다.
여기서 yk가 1에 가까울수록 0에 가깝기 때문에, 정답에 해당하는 값을 높은 확률로 예측해야 오차가 줄어든다.
이 수식을 파이썬 함수로 만들면 다음과 같다.
def cross_entropy_error(y, t):
delta = 1e-8
return - np.sum(t * np.log(y + delta))
log0의 경우를 막기 위해, 아주 작은 값인 delta를 살짝 더해주었다.
예시의 값을 한 번 넣어보면

이렇게 정답 확률을 높게 예측 할 수록, 낮은 오차율이 나오는 것을 볼 수 있다.
당연히 학습을 하면, 이렇게 값을 하나하나씩 넣어 오차율을 확인을 하지는 않을거다.
리스트로 무데기로 때려 넣을테니, 그렇게 때려넣은 것에 대한 수식은 다음과 같을 것이다.

n은 데이터가 N개라면, 그 중 n번째 데이터를 의미한다.
마지막에 N으로 나누어서 평균의 손실 함수를 구하는 것이다.
만약 데이터가 너무너무너무 많다면, 이 함수의 결과를 구하는 시간도 굉장히 오래 걸릴 것이다.
그렇기에 이 중 몇개를 무작위로 뽑아, 그 평균만을 구하는 미니 배치의 값을 구하기도 한다.
import numpy as np
rng = np.random.default_rng()
train = rng.random(1000)
batch_mask = np.random.choice(train.shape[0], 20)
print(batch_mask)
print(train[batch_mask])

이렇게 1000개의 값 중에서 무작위로 20개의 값을 추출 가능하다.
이런 손실 함수를 왜 사용하는걸까?
정확도를 높이기 위해서이다.
정확도를 직접 사용하지 않는 이유는, 정확도는 연속적이지 않기 때문이다.
계단함수만 살펴보더라도

정확도가 만약 특정 값에서 바로 1로 올라간다면, 정확도가 0인 구간에서는 파라미터의 값을 키워야 하는지 아니면 높여야 하는지 아예 알 수가 없다.
어느쪽으로 가도 정확도가 높아지지 않기 때문이다.
그렇기에 연속적인 값을 가지는 손실함수를 구하고, 그 값을 미분해서 최대의 정확도를 가지는 구간을 찾으려고 하는 것이다.
수치 미분
파이썬으로 미분 값을 구해보자.
가장 간단하게 만들면 아래와 같을 것이다.
def numerical_diff(
f,
x,
) -> float:
h = 1e-50
return (f(x + h) - f(x)) / h
진짜 이 미세한 차이를 구하는 것인데, 이 방법 말고 중심 차분을 사용해보겠다.
def numerical_diff(
f,
x,
) -> float:
h = 1e-4
return (f(x + h) - f(x - h)) / (2 * h)
위의 파이썬 함수를 사용해서 미분을 해보자.

이 함수를 미분하면 일단

이 함수가 된다는 것은 계산해보면 알 것이고, 이게 파이썬으로 제대로 나오는지 확인해보자.

이 함수의 1에서의 기울기를 계산해보면, 일단 1.3이 나와야 할 것이다.

저 함수를 사용하면 해당 값과 거의 동일하게 값이 나오는 것을 볼 수 있다.
- 편미분
만약 변수가 2개라면 어떻게 해야할까?

변수가 2개이기 때문에, x0과 x1 중 어떤 변수에 대해서 미분해야 하는지를 정해야 한다고 한다.
그러고 해당 점에서의 x0, x1에 대한 미분값을 각각 구해야 한다고 한다.
import numpy as np
def numerical_gradient(f, x) -> np.ndarray:
grad = np.zeros_like(x)
h = 1e-4
# 원소의 값뿐만 아니라 인덱스(i)도 필요하므로 range를 사용합니다.
for i in range(x.size):
# 원본 값을 잠시 보관해둡니다.
tmp_val = x[i]
# 1. f(x + h) 계산 : i번째 원소만 h를 더하고 배열 전체를 f에 전달
x[i] = tmp_val + h
res1 = f(x)
# 2. f(x - h) 계산 : i번째 원소만 h를 빼고 배열 전체를 f에 전달
x[i] = tmp_val - h
res2 = f(x)
# 기울기(편미분) 계산 후 grad 배열의 i번째 자리에 저장
grad[i] = (res1 - res2) / (2 * h)
# 다음 원소 계산을 위해 원본 값으로 복원해둡니다.
x[i] = tmp_val
return grad
이렇게 기울기를 구하는 함수를 만들고, 테스트로 값을 넣어보면

아래와 같이 잘 나오는 것을 볼 수 있다.
- 경사 하강법
이제 기울기를 구할 수 있게 되었다.
그 다음은 기울기를 잘 이용해서 최솟값을 구하기이다.
함수가 극솟값, 극대값을 가지는 부분의 기울기는 0이다.
특정위치에서 일정거리만큼 계속 이동하며, 기울기를 구해가고 그 기울기를 점차 줄이는 방법인 경사법을 사용할 것이다.
경사법은 수식으로는 다음과 같다.


eta는 학습마다 갱신하는 양을 나타내며, 보통 신경망에서는 학습률이라고 한다.
일단 이 수식을 이용하여 간단한 경사하강법을 만들어보자.
def gradient_descent(
func,
init_x: float,
learning_rate: float,
step_num:int,
):
x = init_x
for i in range(step_num):
grad = numerical_gradient(func, x)
x -= learning_rate * grad
return x
init_x는 출발할 x의 초기값
learning_rate는 학습률
step_num은 경사법을 몇번 반복 할 것인가에 해당한다.
기울기에 학습률을 곱해서 x를 수정하고, 그 작업을 step_num번 반복한다.
저 함수를 사용해서 위에 보았던

이 함수의 최솟값을 구해보자.
init_x = np.array([4., 4.])
res = gradient_descent(
func = func2,
init_x = init_x,
learning_rate = 0.1,
step_num = 100,
)
4, 4에서 시작을 하며 해당 함수에 그대로 넣어보니

최솟값인 0, 0에 굉장히 근접한 값이 나온다.
학습 알고리즘 구현하기
이렇게 기울기를 계속 수정하는 단계를 반복하면서 신경망을 학습시킨다.
MNIST 데이터를 사용해서 신경망을 학습시켜보자.
다음과 같은 클래스를 만들었다.
class TwoLayerNetwork:
def __init__(
self,
input_size,
hidden_size,
output_size,
weight_init_std=0.01,
):
self.params = {}
self.params['weight1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['weight2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['b2'] = np.zeros(output_size)
def predict(
self,
x,
):
w1, w2 = self.params['weight1'], self.params['weight2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, w2) + b2
y = softmax(a2)
return y
def loss(
self,
x,
t,
):
y = self.predict(x)
return cross_entropy_error(y, t)
def accuracy(
self,
x,
t,
):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
def numerical_gradient(
self,
x,
w,
):
loss_w = lambda X: self.loss(x, t)
grads = {}
grads['weight1'] = numerical_gradient(loss_w, self.params['weight1'])
grads['b1'] = numerical_gradient(loss_w, self.params['b1'])
grads['weight2'] = numerical_gradient(loss_w, self.params['weight2'])
grads['b2'] = numerical_gradient(loss_w, self.params['b2'])
return grads
일단 2layer의 신경망을 만들었다.
여기에 존재하는 함수들을 하나씩 설명해보면
predict: 해당 신경망을 통해 데이터의 결과를 예측한다.
loss: 손실함수의 값을 구한다.
accuracy: 정확도를 구한다.
numerical_gradient: 가중치 매개변수의 기울기를 구한다.
일단 인스턴스를 생성해보자.
network = TwoLayerNetwork(
input_size=784,
hidden_size=100,
output_size=10,
)
이렇게 신경망을 만들고,
import numpy as np
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
max_cnt = 15
train_size = x_train.shape[0]
batch_size = 10
lr = 0.1
network = TwoLayerNetwork(
input_size = 784,
hidden_size=50,
output_size = 10,
)
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
for index in range(max_cnt):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
grad = network.numerical_gradient(x_batch, t_batch)
for key in ('weight1', 'b1', 'weight2', 'b2'):
network.params[key] -= lr * grad[key]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
print(f"반복 {index:3d}회째 Loss: {train_loss_list[index]:.4f}")
if index % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print(f"반복 {index:3d}회째 Train Acc: {train_acc:.4f}, Test Acc: {test_acc:.4f}")
이렇게 신경망의 가중치를 업데이트하면서 손실함수의 값을 줄여 나간다.
사실 이 정도로의 학습으로는 정확도가 높지는 않지만, 아무래도 너무 시간이 오래 걸리기에 학습을 조금만 진행해보았다.