스물여덟번째 수업 | 인공신경망 | 신경망 학습 | 오차역전파법

Faithful Dev·2024년 10월 28일
0

강명호 강사님

신경망 학습

신경망 기울기 평가

import numpy as np
from dataset.mnist import load_mnist

# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
  • load_mnist: MNIST 데이터셋을 로드하는 함수. 학습용과 테스트용 데이터셋을 반환한다.
  • normalize=True: 데이터를 0~1 범위로 정규화.
  • one_hot_label=True: 레이블을 원핫 인코딩으로 변환.
train_loss_list = []
train_acc_list = []
test_acc_list = []
  • train_loss_list: 학습 과정에서의 손실 값을 저장하는 리스트.
  • train_acc_list: 각 에포크마다 기록되는 훈련 데이터 정확도를 저장하는 리스트.
  • test_acc_list: 각 에포크마다 기록되는 테스트 데이터 정확도를 저장하는 리스트.
# 하이퍼파라미터
iters_num = 5 # 반복 횟수를 적절히 설정
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning_rate = 0.1
  • iters_num: 경사하강법의 반복 횟수.
  • train_size: 학습 데이터의 총 크기.
  • batch_size: 한 번의 학습에 사용할 미니배치 데이터의 크기.
  • learning_rate: 매개변수를 업데이트할 때 사용하는 학습률.
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
  • TwoLayerNet: 2층 신경망 클래스를 초기화.
iter_per_epoch = max(train_size / batch_size, 1)
  • iter_per_epoch: 한 에포크는 전체 학습 데이터를 한 번 모두 사용하는 것을 의미한다. 한 에포크가 끝날 때마다 훈련 정확도테스트 정확도를 계산할 수 있다.
    - train_size / batch_size: 전체 데이터를 미니배치로 나눈 횟수.
for i in range(iters_num):
    # 미니배치 획득
    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 ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    # 1에폭당 정확도 계산
    if i % 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('train acc, test acc | ' + str(train_acc,) + ', ' + str(test_acc))

오차역전파법

계산 그래프

이 부분은 도저히 강사님이 보여주신 사과 그림들을 말로 설명할 자신이 없어서 팻스. 기억이 안나면 강의자료 찾아보기.

연쇄법칙 (Chain Rule)

여러 개의 함수가 합성된 경우 그 함수들의 미분을 계산할 때 사용하는 방법. 각 함수의 미분을 순차적으로 곱하는 방식이다. 계산 그래프의 역전파는 이 연쇄법칙을 사용해 각 노드의 미분을 차례로 계산한다.

단순 계층 구현

곱셈 연산

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

클래스가 생성될 때 xy 변수를 초기화한다. 이 변수들은 곱셈에 사용할 두 입력 값을 저장하는 데 사용된다. None으로 초기화하는 이유는 아직 값을 받지 않았기 때문.

    def forward(self, x, y):
        self.x = x
        self.y = y
        out = x * y

        return out

순전파 과정.

  • x, y 두 입력값을 받아서 이들을 곱한 out을 계산한다.
  • 입력값 x, y를 각각 클래스 내부 변수 self.x, self.y에 저장한다.
    def backward(self, dout):
        dx = dout * self.y # x와 y를 바꾼다.
        dy = dout * self.x

        return dx, dy

역전파 과정.

  • dout: 출력 값에 대한 미분값. 이 값은 이전 계층으로부터 전달된다.
  • z=x\*yz = x\*y 라면, 이를 x와 y에 대해 미분할 때
    - dzdx=y\frac{dz}{dx} = y : 출력에 대한 입력 x의 기울기는 y에 의해 결정된다.
    • dzdy=x\frac{dz}{dy} = x : 출력에 대한 입력 y의 기울기는 x에 의해 결정된다.
  • 따라서 dxdout * y, dydout * x가 된다.
  • dx, dy: x, y에 대한 미분값.

덧셈 연산

class AddLayer:
    def __init__(self):
        pass

이 클래스는 덧셈 연산만 다루므로 따로 변수를 저장할 필요가 없다. 그래서 __init__ 메서드 안에 아무것도 없고 pass로 넘어간다. 특별히 초기화할 것이 없을 때 이렇게 사용한다.

    def forward(self, x, y):
        out = x + y

        return out

순전파에서 덧셈 연산 처리.

  • 두 입력값 x,y를 받아서 더한 결과를 out에 저장하고 반환.
  • 덧셈 연산이므로 그냥 x + y로 간단히 계산할 수 있다.
    def backward(self, dout):
        dx = dout * 1
        dy = dout * 1

        return dx, dy

역전파.

  • dout: 출력에 대한 미분값.
  • z=x+yz = x + y라면 이를 각각 x와 y에 대해 미분할 때
    - dzdx=1\frac{dz}{dx} = 1
    • dzdy=1\frac{dz}{dy} = 1
  • dx, dy: dout에 1을 곱한 값
  • 덧셈 연산에서는 각각의 입력값에 동일한 기울기가 전달되므로 dx = dout, dy = dout이 된다.

ReLU

class Relu :
    def __init__(self) :
        self.mask = None
  • ReLU의 중요한 특징은 입력이 0 이하일 때 출력을 0으로 만든다는 것!
    def forward(self, x) :
        self.mask = (x <= 0)
        out = x.copy()
        out[self.mask] = 0

        return out
  • 순전파에서 ReLU 함수의 동작을 구현한다.
  • self.mask = (x <= 0): 입력 값 x가 0 이하인 위치를 True, 그 외에는 False로 저장한다.
  • 입력 값을 복사한 out에서 maskTrue인 위치에 해당하는 값을 0으로 만든다. 즉 0 이하인 값을 모두 0으로 만들어준다.
  • 최종적으로 out을 반환.
    def backward(self, dout) :
        dout[self.mask] = 0
        dx = dout

        return dx
  • 역전파를 처리.
  • 순전파 때 저장했던 mask를 사용해서 0 이하였던 입력값들에 해당하는 미분값을 0으로 만든다.
  • self.maskTrue였던 위치(입력값이 0 이하였던 위치)의 dout을 0으로 바꾼다. (ReLU 함수의 특징 때문에 0 이하였던 입력값들이 기울기가 0이 되기 때문)
  • 수정된 doutdx로 반환.

Affine / Softmax 계층 구현하기

Affine 계층

주로 신경망의 완전연결층에서 사용된다. Affine 계층은 입력 데이터를 가중치와 곱하고 편향을 더해서 출력하는 역할을 한다.

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None
  • 클래스가 생성될 때 가중치와 편향을 초기화한다.
  • x: 입력 데이터를 저장하기 위한 변수.
  • dW, db: 가중치와 편향에 대한 미분값(기울기).
    def forward(self, x):
        self.x = x
        out = np.dot(x, self.W) + self.b

        return out
  • 순전파에서는 입력 데이터 x를 가중치 W와 곱하고 편향 b를 더해 out을 계산한다.
    - np.dot(x, self.W): 입력 x와 가중치 W를 행렬 곱셈으로 연산한다.
    • + self.b: 계산된 결과에 편향 더해주기.
  • 입력 xself.x에 저장해둔다.
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

        return dx
  • 역전파에서는 순전파에서 계산된 출력에 대한 미분값인 dout을 받아 입력 x, 가중치W, 편향 b에 대한 미분값을 각각 계산한다.
    - dx = np.dot(dout, self.W.T): dx는 입력 x에 대한 미분. doutW의 전치행렬(W.T)을 곱해 dx를 계산한다.
    • self.dW = np.dot(self.x.T, dout): dW는 가중치 W에 대한 미분. self.x의 전치행렬(self.x.T)과 dout을 곱해 계산한다.
    • self.db = np.sum(dout, axis=0): db는 편향 b에 대한 미분으로 dout의 각 원소를 행 방향으로 합산해서 구한다. 이는 편향이 모든 출력에 고르게 영향을 미치기 때문에 가능하다.
  • 계산된 dx를 반환한다.

Soft-with-Loss 계층

class SoftmaxWithLoss :
    def __init__(self) :
        self.loss = None # 손실함수
        self.y = None # softmax의 출력
        self.t = None # 정답브레이블(원-핫 인코딩 형태) 
  • loss: 손실 함수 값(예측과 실제 정답 간의 오차)을 저장.
  • y: 순전파에서 소프트맥스의 출력을 저장하는 변수.
  • t: 정답 레이블을 저장하는 변수.
    def forward(self, x, t):
       self.t = t
       self.y = softmax(x)
       self.loss = cross_entropy_error(self.y, self.t)

       return self.loss
  • 순전파에서는 신경망의 출력 x를 받아서 손실 값을 계산한다.
    - self.y = softmax(x): 입력 x를 소프트맥스 함수에 통과시켜 각 클래스에 대한 확률로 변환한다. (소프트맥스: 신경망의 출력 값을 0과 1 사이의 확률 값으로 정규화)
    - self.loss = cross_entropy_error(self.y, self.t): 소프트맥스 결과 y와 정답 레이블 t를 사용해 교차 엔트로피 오차를 계산한다. 이 오차 값은 예측한 확률 분포가 정답 분포와 얼마나 다른지를 나타낸다.
  • 손실 값 self.loss를 반환한다.
    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size

        return dx
  • 역전파에서는 손실값에 대한 미분을 계산한다.
    - batch_size = self.t.shape[0]: 입력 데이터의 배치 크기를 구한다.
    • dx = (self.y - self.t) / batch_size: 소프트맥스의 출력 y와 정답 레이블 t의 차이를 구한 후 배치 크기로 나누어 손실에 대한 입력 x에 대한 미분 dx를 구한다.
  • dx를 반환하여 손실에 대한 입력 x의 미분값을 전달한다.

오차역전파법 구현

2층 신경망 구현

from collections import OrderedDict

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        # 가중치 초기화
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)
        
        # 계층 생성
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()
  • 초기화에서 신경망의 가중치와 편향을 설정하고 각 계층을 생성한다.
    - self.params: 신경망의 가중치와 편향을 저장.
    • W1, b1, W2, b2: 각 계층의 가중치와 편향.
  • 가중치는 표준 편차가 0.01인 정규분포로 초기화되고, 편향은 0으로 설정된다.
  • 신경망의 각 계층을 OrderedDict에 저장해서 순서대로 순전파 및 역전파가 가능하도록 한다.
    - Affine1, Affine2: Affine 계층
    • Relu: ReLU 활성화 함수 계층
    • lastLayer: SoftmaxWithLoss 계층으로, 손실을 계산하는 마지막 계층이다.
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
  • 순전파 예측을 수행하여 입력 데이터를 통해 나온 최종 출력(예측값)을 반환한다.
  • self.layers에 저장된 계층을 순서대로 통과하여 x는 다음 계층으로 전달된다.
    # x: 입력 데이터, t: 정답 레이블
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
  • 손실 함수 값을 계산.
    - predict 메서드를 통해 예측값 y를 얻는다.
    • 예측값 y와 정답 레이블 tlastLayerSoftmaxWithLoss 계층의 forward 메서드에 전달해 손실을 계산한다.
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
  • 정확도를 계산하는 메서드로 예측값과 실제 정답 레이블이 얼마나 일치하는지를 확인한다.
    - np.argmax(y, axis=1): 예측값 중에서 가장 높은 확률을 가진 클래스의 인덱스를 가져온다.
    • 예측과 실제 정답 레이블이 일치하는 비율을 계산하여 반환한다.
    # x: 입력 데이터, t: 정답 레이블
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])        
        return grads
  • numerical_gradient: 수치 미분을 통해 각 가중치와 편향의 기울기를 계산.
    - 손실 함수 loss_W를 정의한 후 numerical_gradient 함수를 이용해 W1, b1, W2, b2에 대한 기울기를 계산하여 저장한다.
    • 계산은 정확하지만 속도가 느려 큰 신경망에서는 역전파 기울기 계산을 주로 사용한다.
    def gradient(self, x, t):
        # 순전파
        self.loss(x, t)

        # 역전파
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads
  • gradient: 역전파를 통해 각 가중치와 편향에 대한 기울기를 효율적으로 계산한다.
    - self.loss(x, t)를 호출하여 순전파를 수행해 손실을 계산한다.
    • 마지막 SoftmaxWithLoss 계층부터 역전파를 시작하여 dout 값을 거꾸로 전달하면서 각 계층의 backward 메서드를 호출해 기울기를 계산한다.
    • 마지막으로 Affine 계층의 dW, db 값을 grads 딕셔너리에 저장하여 반환한다.
from dataset.mnist import load_mnist

# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

load_mnist() 함수로 MNIST 데이터를 불러오며 normalize=True 옵션으로 입력 데이터를 0~1 사이 값으로 정규화하고 one_hot_label=True 옵션으로 정답 레이블을 원-핫 인코딩 형태로 변환한다.

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

TwoLayerNet 클래스 객체 network를 생성한다. MNIST 데이터는 각 이미지가 28*28 크기이므로 입력 크기는 784, 은닉층 노드수는 50, 출력층 노드 수는 10으로 설정.

x_batch = x_train[:3]
t_batch = t_train[:3]

학습 데이터 중 처음 3개의 데이터(x_batch)와 정답 레이블(t_batch)을 미니 배치로 선택해 기울기 계산에 사용한다.

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)
  • grad_numerical: numerical_gradient 메서드를 통해 수치 미분으로 계산한 기울기 값.
  • grad_backprop: gradient 메서드를 통해 역전파로 계산한 기울기 값.
    두 기울기는 이론적으로 매우 유사해야 한다. 수치 미분은 계산량이 많고 느리지만 정확하고, 역전파는 속도가 빠르기 때문에 신경망 학습에 주로 사용된다.
# 각 가중치의 차이의 절대값을 구한 후 그 절대값들의 평균을 산출함
for key in grad_numerical.keys():
    diff = np.average(np.abs(grad_backprop[key] - grad_numerical[key]))
    print(key + ': ' + str(diff))

각 가중치에 대해 두 기울기의 차이(절대값)를 평균해서 출력한다. 차이가 작을수록 역전파가 제대로 구현되었다는 의미.

MNIST 숫자 인식 학습

from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
  • load_mnist(): 학습 데이터와 테스트 데이터를 불러온다.
  • normalize=True: 입력 데이터를 0~1 사이로 정규화.
  • one_hot_label=True: 레이블을 원-핫 인코딩 형식으로 변환.
iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
  • iters_num: 전체 반복 횟수. 총 10,000번 반복.
  • batch_size: 미니배치 크기. 한 번에 100개의 데이터를 신경망에 입력.
  • learning_rate: 학습률. 기울기를 얼마나 크게 적용할지 조절.
train_loss_list = []
train_acc_list = []
test_acc_list = []
  • 학습 중 손실 함수 값과 정확도를 기록할 리스트들.
  • train_loss_list: 각 반복마다 손실 함수 값을 기록.
  • train_acc_list, test_acc_list: 각 epoch 마다 학습 정확도와 테스트 정확도를 기록.
iter_per_epoch = max(train_size / batch_size, 1)

한 epoch에 몇 번의 반복이 필요한지를 계산해 매 epoch 마다 정확도를 측정한다.

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    grad = network.gradient(x_batch, t_batch) # 오차역전파법
  • 학습은 iters_num 만큼 반복된다.
  • batch_mask를 사용해 학습 데이터에서 무작위로 batch_size만큼의 인덱스를 선택해 미니배치를 만든다.
  • network.gradient()를 호출해 미니배치에 대한 기울기를 계산한다.
    # 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
  • 각 가중치와 편향에 대해 기울기 grad[key]를 학습률만큼 곱해 기존 값에서 뺀다.
  • 이를 통해 신경망이 예측 오차를 줄이도록 가중치가 갱신된다.
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
  • 현재 미니배치에 대한 손실 함수 값을 계산한고, 이를 train_loss_list에 추가해 기록한다.
    if i % 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(train_acc, test_acc)
  • iter_per_epoch 횟수마다 학습 데이터와 테스트 데이터에 대한 정확도를 계산한다.
  • 이를 train_acc_listtest_acc_list에 저장하고, 학습 정확도와 테스트 정확도를 출력하여 모델 성능 변화를 확인한다.
profile
Turning Vision into Reality.

0개의 댓글