● 합성곱 신경망(Convolutional Neural Networks, CNNs)

  • 이미지 인식, 음성 인식 등에 자주 사용됨,
    특히, 이미지 인식 분야에서 거의 모든 딥러닝 기술에 사용

https://medium.com/@pechyonkin/key-deep-learning-architectures-lenet-5-6fc3c59e6f4

  - 완전 연결 계층과의 차이

  • 완전 연결 계층(Fully-Connected Layer)은 이미지와 같은 데이터의 형상(3차원)을 무시함
  • 모든 입력 데이터를 동긍하게 취급
    즉, 데이터의 특징을 잃어버림
  • 컨볼루션층(convolution layer)은 이미지 픽셀 사이의 관계를 고려
  • 완전 연결 계층은 공간 정보를 손실하지만, 컨볼루션층은 공간 정보를 유지
    • 이미지와 같은 2차원(흑백) 또는 3차원(컬러)의 형상을 유지
    • 공간 정보를 유지하기 때문에 완전 연결 계층에 비해 적은 수의 파라미터를 요구

 

  - 컨볼루션 신경망 구조 예시

https://www.oreilly.com/library/view/neural-network-projects/9781789138900/8e87ad66-6de3-4275-81a4-62b54436bf16.xhtml

 

1. 합성곱 연산

  • 필터(filter) 연산
    • 입력 데이터에 필터를 통한 어떠한 연산을 진행
    • 필터에 대응하는 원소끼리 곱하고, 그 합을 구함
    • 연산이 완료된 결과 데이터를 특징 맵(feature map)이라 부름
  • 필터(filter)
    • 커널(kernel)이라고도 칭함
    • 흔히 사진 어플에서 사용하는 이미지 필터와 비슷한 개념
    • 필터의 사이즈는 거의 항상 홀수
      • 짝수면 패딩이 비대팅이 되어버림
      • 왼쪽, 오른쪽을 다르게 주어야 함
      • 중심 위치가 존재, 즉 구별된 하나의 필셀(중심 픽셀)이 존재
    • 필터의 학습 파라미터 개수는 입력 데이터의 크기와 상관없이 일정,
      따라서, 과적합 방지 가능

http://incredible.ai/artificial-intelligence/2016/06/12/Convolutional-Neural-Networks-Part1.5/

  - 연산 시각화

https://www.researchgate.net/figure/An-example-of-convolution-operation-in-2D-2_fig3_324165524

 

  - 일반적으로 합성곱 연산을 한 후의 데이터 사이즈는

\( \qquad (n-f+1) \times (n-f+1) \)

\( n \): 입력 데이터의 크기

\( f \): 필터(커널)의 크기

  - 예시

  • 입력 데이터의 크기(\( n \))는 5, 필터의 크기(\( k \))는 3이므로, 출력 데이터의 크기는 \( (5-3+1)=3 \)

https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1

 

 

2. 패딩(padding)과 스트라이드(stride)

  • 필터(커널) 사이즈와 함께 입력 이미지와 출력 이미지의 사이즈를 결정하기 위해 사용
  • 사용자가 결정할 수 있음

 

  - 패딩

  • 입력 데이터의 주변을 특정 값으로 채우는 기법
    • 주로 0으로 많이 채움

https://tensorflow.blog/a-guide-to-convolution-arithmetic-for-deep-learning/

  • 출력 데이터의 크기
    \( \qquad (n+2p-f+1) \times (n+2p-f+1) \)
    위 그림에서, 입력 데이터의 크기(\( n \))는 5, 필터의 크기(\( f \))는 4, 패딩값(\( p \))은 2이므로
    출력 데이터의 크기는 \( (5+2 \times 2-4+1)=6 \)

 

  - 'valid'와 'same'

  • 'valid'
    • 패딩을 주지 않음
    • padding=0 (0으로 채워진 테두리가 아니라 패딩을 주지 않는다는 의미)
  • 'same'
    • 패딩을 주어 입력 이미지의 크기와 연산 후의 이미지 크기를 같게 함
    • 만약, 필터(커널)의 크기가 \( k \)이면,
      패딩의 크기는 \( p=\frac{k-1}{2} \)(단, stride=1)

 

  - 스트라이드

  • 필터를 적용하는 간격을 의미
  • 아래는 그림의 간격 2

https://tensorflow.blog/a-guide-to-convolution-arithmetic-for-deep-learning/

  - 출력 데이터의 크기

$$ OH=\frac{H+2P-FH}{S}+1 $$

$$ OW=\frac{W+2P-FW}{S}+1 $$

  • 입력 크기: \( (H, W) \)
  • 필터 크기: \( (FH, FW) \)
  • 출력 크기: \( (OH, OW) \)
  • 패딩, 스트라이드: \( P, S \)
  • (주의)
    • 위 식의 값에서 \( \frac{H+2P-FH}{S} \)또는 \( \frac{W+2P-FW}{S} \)가 정수로 나누어 떨어지는 값이어야 함
    • 만약, 정수로 나누어 떨어지지 않으면
      패딩, 스트라이드 값을 조정하여 정수로 나누어 떨어지게 해야함

 

3. 풀링(Pooling)

  • 필터(커널)의 사이즈 내에서 특정 값을 추출하는 과정

 

  - 맥스 풀링(Max Pooling)

  • 가장 많이 사용되는 방법
  • 출력 데이터의 사이즈 계산은 컨볼루션 연산과 동일

$$ OH=\frac{H+2P-FH}{S}+1 $$

$$ OW=\frac{W+2P-FW}{S}+1 $$

  • 일반적으로 stride=2, kernel_size=2를 통해 특징맵의 크기를 절반으로 줄이는 역할
  • 모델이 물체의 주요한 특징을 학습할 수 있도록 해주며,
    컨볼루션 신경망이 이동 불변성 특징을 가지게 해줌
    • 예를 들어, 아래의 그림에서 초록색 사각형 안에 있는 2와 8의 위치를 바꾼다해도 맥스 풀링 연산은 8을 추출
  • 모델의 파라미더 개수를 줄여주고, 연산 속도를 빠르게 해줌

https://cs231n.github.io/convolutional-networks/

 

  - 평균 풀링 (Avg Pooling)

  • 필터 내에 있는 픽셀값의 평균을 구하는 과정
  • 과거에 많이 사용, 요즘은 잘 사용되지 않음
  • 맥스 풀링과 마찬가지로 stride=1, kernel_size=2를 통해 특징맵의 사이즈를 줄이는 역할

https://www.researchgate.net/figure/Average-pooling-example_fig21_329885401

 

4. 합성곱 연산의 의미

  - 2차원 이미지에 대한 필터 연산 예시

  • 가장 자리 검출(Edge-Detection)
  • 소벨 필터(Sobel Filter)
    • Horizontal: 가로 방향의 미분을 구하는 필터 역할
    • Vertical: 세로 방향의 미분을 구하는 필터 역할

https://www.cloras.com/blog/image-recognition/

  • module import
# %pip install opencv-python
import cv2
import numpy as np
import matplotlib.pyplot as plt
import urllib
import requests
from io import BytesIO
  • util functions
def url_to_image(url, gray = False):
    resp = urllib.request.urlopen(url)
    image = np.asarray(bytearray(resp.read()), dtype = 'uint8')

    if gray == True:
        image = cv2.imdecode(image, cv2.IMREAD_GRAYSCALE)
    else:
        image = cv2.imdecode(image, cv2.IMREAD_COLOR)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    return image

def filtered_image(image, filter, output_size):
    filtered_img = np.zeros((output_size, output_size))
    filter_size = filter.shape[0]

    for i in range(output_size):
        for j in range(output_size):
            # 이미지의 각 픽셀 (i, j)를 돌면서 합성곱 연산을 진행 후, 마지막에 합성곱 연산으로 filtering된 이미지 return
            multiply_values = image[i:(i+filter_size), j:(j+filter_size)] * filter
            sum_value = np.sum(multiply_values)

            if (sum_value > 255):
                sum_value = 255
            
            filtered_img[i, j] = sum_value
    
    return filtered_img
  • 이미지 확인(예시이므로 정사각형 사이즈로 진행)
# 이미지 처리계의 hello world같은 이미지
img_url = "https://upload.wikimedia.org/wikipedia/ko/thumb/2/24/Lenna.png/440px-Lenna.png"
image = url_to_image(img_url, gray = True)
print("image.shape:", image.shape)

plt.imshow(image, cmap = "gray")
plt.show()

image.shape: (440, 440)

  • 필터 연산 적용
vertical_filter = np.array([[1., 2., 1.],
                            [0., 0., 0.,],
                            [-1., -2., -1.]])
horizontal_filter = np.array([[1., 0., -1.],
                              [2., 0., -2.],
                              [1., 0., -1.]])
output_size = int((image.shape[0] - 3) / 1 + 1)
print("output_size:", output_size)

vertical_filtered = filtered_image(image, vertical_filter, output_size)
horizontal_filtered = filtered_image(image, horizontal_filter, output_size)

plt.figure(figsize = (10, 10))
plt.subplot(1, 2, 1)
plt.title("Vertical")
plt.imshow(vertical_filtered, cmap = 'gray')

plt.subplot(1, 2, 2)
plt.title("Horizontal")
plt.imshow(horizontal_filtered, cmap = 'gray')
plt.show()

output_size: 438

  • 이미지 필터를 적용한 최종 결과
# vertical, horizontal 두 개의 필터 연산 결과를 제곱하여 더한 뒤 루트로 제곱근을 구한 연산 시행
sobel_img = np.sqrt(np.square(horizontal_filtered) + np.square(vertical_filtered))

plt.imshow(sobel_img, cmap = 'gray')

 

  - 3차원 데이터의 합성곱 연산

  • 이미지는 3차원으로 구성
    • (가로, 세로, 채널수)
    • 채널: RGB
  • 색상값의 정도에 따라 color 결정

https://www.projectorcentral.com/All-About-Bit-Depth.htm?page=What-Bit-Depth-Looks-Like

  • 이미지 확인
img_url = "https://upload.wikimedia.org/wikipedia/ko/thumb/2/24/Lenna.png/440px-Lenna.png"
# 위에서 흑백으로 출력했을 때와 다르게 'gray = True' 옵션을 제외
image = url_to_image(img_url)
print("image.shape:", image.shape)

# 출력 시에서 "cmap = 'gray'" 옵션을 제외
plt.imshow(image)
plt.show()

image.shape: (440, 440, 3)

# Red Image
image_copy = image.copy()
# 이미지의 shape의 세번째 인덱스는 R, G, B로 된 값이고 현재 3으로 세가지 값 모두 존재
# 아래에서 1, 2를 0으로 만들어 G, B 값을 빼주면 R만 남은 이미지를 만들 수 있음
image_copy[:, :, 1] = 0
image_copy[:, :, 2] = 0
image_red = image_copy

plt.imshow(image_red)
plt.show()

image_copy = image.copy()
# 아래에서 0, 2를 0으로 만들어 R, B 값을 빼주면 G만 남은 이미지를 만들 수 있음
image_copy[:, :, 0] = 0
image_copy[:, :, 2] = 0
image_green = image_copy

plt.imshow(image_green)
plt.show()

image_copy = image.copy()
# 아래에서 0, 1을 0으로 만들어 R, G 값을 빼주면 B만 남은 이미지를 만들 수 있음
image_copy[:, :, 0] = 0
image_copy[:, :, 1] = 0
image_blue = image_copy

plt.imshow(image_blue)
plt.show()

# 한번에 띄우고 흑백 이미지와 비교
fig = plt.figure(figsize = (12, 8))

title_list = ['R', 'G', 'B',
              'R - grayscale', 'G - grayscale', 'B - grayscale']
image_list = [image_red, image_green, image_blue,
              image_red[:, :, 0], image_green[:, :, 1], image_blue[:, :, 2]]

for i, image in enumerate(image_list):
    ax = fig.add_subplot(2, 3, i+1)
    ax.title.set_text("{}".format(title_list[i]))

    if i >= 3:
        plt.imshow(image, cmap = 'gray')
    else:
        plt.imshow(image)

plt.show()

 

  - 연산 과정

  • 각 채널마다 컨볼루션 연산을 적용
    • 3채널을 모두 합쳐서 '하나의 필터'라고 칭함

https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1

  • 각각의 결과를 더함

https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1

  • 더한 결과에 편향을 더함

https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1

  • modules import
# %pip install opencv-python
import cv2
import numpy as np
import matplotlib.pyplot as plt
import urllib
import requests
from io import BytesIO
  • util functions
def url_to_image(url, gray = False):
    resp = urllib.request.urlopen(url)
    image = np.asarray(bytearray(resp.read()), dtype = 'uint8')

    if gray == True:
        image = cv2.imdecode(image, cv2.IMREAD_GRAYSCALE)
    else:
        image = cv2.imdecode(image, cv2.IMREAD_COLOR)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    return image

def conv_op(image, kernel, pad = 0, stride = 1):
    H, W, C = image.shape
    kernel_size = kernel.shape[0]

    out_h = (H + 2*pad - kernel_size) // stride + 1
    out_w = (W + 2*pad - kernel_size) // stride + 1

    filtered_img = np.zeros((out_h, out_w))
    img = np.pad(image, [(pad, pad), (pad, pad), (0, 0)], 'constant')

    for i in range(out_h):
        for j in range(out_w):
            for c in range(C):
                multiply_values = image[i:(i + kernel_size), j:(j + kernel_size), c] * kernel
                sum_value = np.sum(multiply_values)

                filtered_img[i, j] += sum_value
    
    filtered_img = filtered_img.reshape(1, out_h, out_w, -1).transpose(0, 3, 1, 2)

    return filtered_img.astype(np.uint8)
  • 이미지 확인
img_url = "https://upload.wikimedia.org/wikipedia/ko/thumb/2/24/Lenna.png/440px-Lenna.png"
image = url_to_image(img_url)
print("image.shape:", image.shape)

plt.imshow(image)
plt.show()

  • 필터연산 적용
    • 3×3 크기의 3채널 필터 5개
    • (5, 3, 3, 3) → (5개, 3채널, 세로, 가로)
# 예시 1
filter1 = np.random.randn(3, 3, 3)

print(filter1.shape)
print(filter1)

# 출력 결과
(3, 3, 3)
[[[ 1.03527724 -0.91961541  1.12674622]
  [ 0.90570621  2.43452234 -0.58178937]
  [-0.20276794 -0.69609947 -0.22246946]]

 [[-0.19455091  0.96691228  1.18181353]
  [-0.75600052 -2.92070965  0.42929136]
  [-0.43024675  0.61458207 -0.52046698]]

 [[ 0.82826973  0.55922214  0.27557231]
  [-0.47029333 -0.53727015  1.44036126]
  [-0.74869707  1.89852507  1.45523256]]]

(1, 1, 438, 438)

 

# 예시 2
filter2 = np.random.randn(3, 3, 3)

print(filter2.shape)
print(filter2)

# 출력 결과
(3, 3, 3)
[[[ 1.03641458  1.4153158  -0.56486124]
  [-0.1553772  -1.86455138 -0.00522765]
  [ 0.1220599   0.43514984  0.32804735]]

 [[ 0.81778856  1.64887384 -1.29579815]
  [-0.45742362 -0.23823593  1.17207619]
  [ 0.29878226  0.02336725 -0.95649443]]

 [[-0.97517188  0.91275201 -1.00159311]
  [-1.80679889 -0.40762195 -2.10950021]
  [ 1.94690784 -0.80022143 -0.04150088]]]
filtered_img2 = conv_op(image, filter2)
print(filtered_img1.shape)

plt.figure(figsize = (10, 10))
plt.subplot(1, 2, 1)
plt.title("Used Filter")
plt.imshow(filter2, cmap = 'gray')

plt.subplot(1, 2, 2)
plt.title("Result")
plt.imshow(filtered_img2[0, 0, :, :], cmap = 'gray')
plt.show()

(1, 1, 438, 438)

  • 필터연산을 적용한 최종 결과
# 위의 예시 전부 sum
filtered_img = np.stack([filtered_img1, filtered_img2]).sum(axis = 0)
print(filtered_img.shape)

plt.imshow(filtered_img[0, 0, :, :], cmap = 'gray')
plt.show()

(1, 1, 438, 438)

 

  • 전체 과정 한번에 보기
# 5개의 랜덤 필터를 만들고
np.random.seed(222)

fig = plt.figure(figsize = (8, 20))

filter_num = 5
filtered_img = []

for i in range(filter_num):
    ax = fig.add_subplot(5, 2, 2*i+1)
    ax.title.set_text("Filter {}".format(i + 1))

    filter = np.random.randn(3, 3, 3)
    plt.imshow(filter)

    ax = fig.add_subplot(5, 2, 2*i+2)
    ax.title.set_text("Result")

    filtered = conv_op(image, filter)
    filtered_img.append(filtered)
    plt.imshow(filtered[0, 0, :, :], cmap = 'gray')

plt.show()


# 만들어진 필터를 sum하여 컨볼루션 연산 하는 과정을 한번에 작성
filtered_img = np.stack(filtered_img).sum(axis = 0)
print(filtered_img.shape)

plt.imshow(filtered_img[0, 0, :, :], cmap = 'gray')
plt.show()

● MNIST 분류 실습

  • Module Import
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict

 

  • 데이터 로드
np.random.seed(42)
mnist = tf.keras.datasets.mnist
(X_train, y_train), (X_test, y_test) = mnist.load_data()
num_classes = 10

 

  • 데이터 전처리
np.random.seed(42)
mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
num_classes = 10

# 데이터 수가 너무 많으므로 조금 줄이기
x_train = x_train[:10000]
x_test = x_test[:3000]

y_train = y_train[:10000]
y_test = y_test[:3000]

# flatten
x_train, x_test = x_train.reshape(-1, 28*28).astype(np.float32), x_test.reshape(-1, 28*28).astype(np.float32)

x_train = x_train / .255
x_test = x_test / .255

# y는 원-핫 벡터로 변경
y_train = np.eye(num_classes)[y_train]

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)

# 출력 결과
(10000, 784)
(10000, 10)
(3000, 784)
(3000,)

 

  • Hyper Parameter
epochs = 1000
learning_rate = 1e-2
batch_size = 256
train_size = x_train.shape[0]
iter_per_epoch = max(train_size / batch_size, 1)

 

  • Util Functions
def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis = 0)
        y = np.exp(x) / np.sum(np.exp(x), axis = 0)
        return y.T
    
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def mean_squared_error(y, t):
    return 0.5 * np.sum((y - t)**2)

def cross_entropy_error(pred_y, true_y):
    if pred_y.ndim == 1:
        true_y = true_y.reshape(1, true_y.size)
        pred_y = pred_y.reshape(1, pred_y.size)
    
    if true_y.size == pred_y.size:
        true_y = true_y.argmax(axis = 1)
    
    batch_size = pred_y.shape[0]
    return -np.sum(np.log(pred_y[np.arange(batch_size), true_y] + 1e-7)) / batch_size

 

  • Util Classes
  • ReLU
class ReLU:
    def __init__(self):
        self.mask = None

    def forward(self, input_data):
        self.mask = (input_data <= 0)
        out = input_data.copy()
        out[self.mask] = 0

        return out
    
    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout

        return dx

 

  • Sigmoid
class Sigmoid:
    def __init__(self):
        self.out = None
    
    def forward(self, input_data):
        out = 1 / (1 + np.exp(-input_data))
        self.out = out
        return out
    
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.dout
        return dx

 

  • Layer
class Layer:
    def __init__(self, W, b):
        self.W = W
        self.b = b

        self.input_data = None
        self.input_data_shape = None

        self.dW = None
        self.db = None

    def forward(self, input_data):
        self.input_data_shape = input_data.shape

        input_data = input_data.reshape(input_data.shape[0], -1)
        self.input_data = input_data
        out = np.dot(self.input_data, self.W) + self.b

        return out
    
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.input_data.T, dout)
        self.db = np.sum(dout, axis = 0)

        dx = dx.reshape(*self.input_data_shape)
        return dx

 

  • Batch Normalization
class BatchNormalization:
    def __init__(self, gamma, beta, momentum = 0.9, running_mean = None, running_var = None):
        self.gamma = gamma
        self.beta = beta
        self.momentum = momentum
        self.input_shape = None
        
        self.running_mean = running_mean
        self.running_var = running_var
        
        self.batch_size = None
        self.xc = None
        self.std = None
        self.dgamma = None
        self.dbeta = None
    
    def forward(self, input_data, is_train = True):
        self.input_shape = input_data.shape
        if input_data.ndim != 2:
            N, C, H, W = input_data.shape
            input_data = input_data.reshape(N, -1)

        out = self.__forward(input_data, is_train)

        return out.reshape(*self.input_shape)
    
    def __forward(self, input_data, is_train):
        if self.running_mean is None:
            N, D = input_data.shape
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
        
        if is_train:
            mu = input_data.mean(axis = 0)
            xc = input_data - mu
            var = np.mean(xc**2, axis = 0)
            std = np.sqrt(var + 10e-7)
            xn = xc / std

            self.batch_size = input_data.shape[0]
            self.xc = xc
            self.std = std
            self.running_mean = self.momentum * self.running_mean + (1 - self.momentum) * mu 
            self.running_var = self.momentum * self.running_var + (1 - self.momentum) * var
        
        else:
            xc = input_data - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))
        
        out = self.gamma * xn + self.beta
        return out
    
    def backward(self, dout):
        if dout.ndim != 2:
            N, C, H, W = dout.shape
            dout = dout.reshape(N, -1)
        
        dx = self.__backward(dout)

        dx = dx.reshape(*self.input_shape)
        return dx
    
    def __backward(self, dout):
        dbeta = dout.sum(axis = 0)
        dgamma = np.sum(self.xn * dout, axis = 0)
        dxn = self.gamma * dout
        dxc = dxn / self.std
        dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis = 0)
        dvar = 0.5 * dstd / self.std
        dxc += (2.0 / self.batch_size) * self.xc * dvar
        dmu = np.sum(dxc, axis = 0)
        dx = dxc - dmu / self.batch_size

        self.dgamma = dgamma
        self.dbeta = dbeta

        return dx

 

  • Dropout
class Dropout:
    def __init__(self, dropout_ratio = 0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None

    def forward(self, input_data, is_train = True):
        if is_train:
            self.mask = np.random.rand(*input_data.shape) > self.dropout_ratio
            return input_data * self.mask
        else:
            return input_data * (1.0 - self.dropout_ratio)
        
    def backward(self, dout):
        return dout * self.mask

 

  • Softmax
class Softmax:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None

    def forward(self, input_data, t):
        self.t = t
        self.y = softmax(input_data)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss
    
    def backward(self, dout = 1):
        batch_size = self.t.shape[0]

        if self.t.size == self.y.size:
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx

 

  • Model
class MyModel:
    def __init__(self, input_size, hidden_size_list, output_size,
                 activation = 'relu', decay_lambda = 0,
                 use_dropout = False, dropout_ratio = 0.5, use_batchnorm = False):
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size_list = hidden_size_list
        self.hidden_layer_num = len(hidden_size_list)
        self.use_dropout = use_dropout
        self.decay_lambda = decay_lambda
        self.use_batchnorm = use_batchnorm
        self.params = {}

        self.__init_weight(activation)

        activation_layer = {'sigmoid': Sigmoid, 'relu': ReLU}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num + 1):
            self.layers['Layer' + str(idx)] = Layer(self.params['W' + str(idx)],
                                                    self.params['b' + str(idx)])
            if self.use_batchnorm:
                self.params['gamma' + str(idx)] = np.ones(hidden_size_list[idx - 1])
                self.params['beta' + str(idx)] = np.ones(hidden_size_list[idx - 1])
                self.layers['BatchNorm' + str(idx)] = BatchNormalization(self.params['gamma' + str(idx)], self.params['beta' + str(idx)])
            
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()

            if self.use_dropout:
                self.layers['Dropout' + str(idx)] = Dropout(dropout_ratio)
        
        idx = self.hidden_layer_num + 1
        self.layers['Layer' + str(idx)] = Layer(self.params['W' + str(idx)], self.params['b' + str(idx)])
        self.last_layer = Softmax()

    def __init_weight(self, activation):
        all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]

        for idx in range(1, len(all_size_list)):
            scale = None
            if activation.lower() == 'relu':
                scale = np.sqrt(2.0 / all_size_list[idx * 1])
            elif activation.lower() == 'sigmoid':
                scale = np.sqrt(1.0 / all_size_list[idx * 1])
            
            self.params['W' + str(idx)] = scale * np.random.randn(all_size_list[idx - 1], all_size_list[idx])
            self.params['b' + str(idx)] = np.zeros(all_size_list[idx])

    
    def predict(self, x, is_train = False):
        for key, layer in self.layers.items():
            if 'Dropout' in key or 'BatchNorm' in key:
                x = layer.forward(x, is_train)
            else:
                x = layer.forward(x)
            
        return x
    
    def loss(self, x, t, is_train = False):
        y = self.predict(x, is_train)

        weight_decay = 0
        for idx in range(1, self.hidden_layer_num + 2):
            W = self.params['W' + str(idx)]
            # L2 규제 적용
            weight_decay += 0.5 * self.decay_lambda * np.sum(W**2)
        
        return self.last_layer.forward(y, t) + weight_decay
    
    def accuracy(self, x, t):
        y = self.predict(x, is_train = False)
        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
    
    def gradient(self,x, t):
        self.loss(x, t, is_train = True)

        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        # backward이므로 한번 reverser해서 역으로 접근
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
        
        grads = {}
        for idx in range(1, self.hidden_layer_num + 2):
            grads['W' + str(idx)] = self.layers['Layer' + str(idx)].dW + self.decay_lambda * self.params['W' + str(idx)]
            grads['b' + str(idx)] = self.layers['Layer' + str(idx)].db

            if self.use_batchnorm and idx != self.hidden_layer_num + 1:
                grads['gamma' + str(idx)] = self.layers['BatchNorm' + str(idx)].dgamma
                grads['beta' + str(idx)] = self.layers['BatchNorm' + str(idx)].dbeta
        
        return grads

 

  • 모델 생성 및 학습 (1)

  - 사용 기법

  • 학습 데이터 수: 10,000
  • Hidden Layers: 4 [100, 100, 100, 100]
  • SGD
  • EPOCHS: 1000
  • 학습률: 1e-2(0.01)
  • 배치사이즈: 256
  • 드롭아웃: 0.2
  • 배치 정규화
  • 규제화: 0.1
decay_lambda = 0.1
model_1 = MyModel(input_size = 784, hidden_size_list = [256, 100, 64, 32], output_size = 10,
                  decay_lambda = decay_lambda, use_batchnorm = True)

optimizer = SGD(learning_rate = learning_rate)

model_1_train_loss_list = []
model_1_train_acc_list = []
model_1_test_acc_list = []

for epoch in range(epochs):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    y_batch = y_train[batch_mask]

    grads = model_1.gradient(x_batch, y_batch)
    optimizer.update(model_1.params, grads)

    loss = model_1.loss(x_batch, y_batch)
    model_1_train_loss_list.append(loss)

    train_acc = model_1.accuracy(x_train, y_train)
    test_acc = model_1.accuracy(x_test, y_test)
    model_1_train_acc_list.append(train_acc)
    model_1_test_acc_list.append(test_acc)

    if epoch % 50 == 0:
        print("[Model 1]  Epoch: {}  Train Loss: {:.4f}  Train Accuracy: {:.4f}  Test Accuracy: {:.4f}".format(epoch+1, loss, train_acc, test_acc))

# 출력 결과
[Model 1]  Epoch: 1  Train Loss: 137.5669  Train Accuracy: 0.1000  Test Accuracy: 0.1020
[Model 1]  Epoch: 51  Train Loss: 112.5705  Train Accuracy: 0.6919  Test Accuracy: 0.6257
[Model 1]  Epoch: 101  Train Loss: 101.5959  Train Accuracy: 0.7885  Test Accuracy: 0.7303
[Model 1]  Epoch: 151  Train Loss: 91.9510  Train Accuracy: 0.8327  Test Accuracy: 0.7677
[Model 1]  Epoch: 201  Train Loss: 83.1132  Train Accuracy: 0.8590  Test Accuracy: 0.7963
[Model 1]  Epoch: 251  Train Loss: 75.2112  Train Accuracy: 0.8741  Test Accuracy: 0.8127
[Model 1]  Epoch: 301  Train Loss: 68.0901  Train Accuracy: 0.8852  Test Accuracy: 0.8243
[Model 1]  Epoch: 351  Train Loss: 61.6642  Train Accuracy: 0.8969  Test Accuracy: 0.8347
[Model 1]  Epoch: 401  Train Loss: 55.9115  Train Accuracy: 0.9010  Test Accuracy: 0.8450
[Model 1]  Epoch: 451  Train Loss: 50.6766  Train Accuracy: 0.9085  Test Accuracy: 0.8533
[Model 1]  Epoch: 501  Train Loss: 45.8550  Train Accuracy: 0.9132  Test Accuracy: 0.8573
[Model 1]  Epoch: 551  Train Loss: 41.5136  Train Accuracy: 0.9185  Test Accuracy: 0.8613
[Model 1]  Epoch: 601  Train Loss: 37.5357  Train Accuracy: 0.9221  Test Accuracy: 0.8667
[Model 1]  Epoch: 651  Train Loss: 34.0123  Train Accuracy: 0.9255  Test Accuracy: 0.8720
[Model 1]  Epoch: 701  Train Loss: 30.7791  Train Accuracy: 0.9269  Test Accuracy: 0.8747
[Model 1]  Epoch: 751  Train Loss: 27.9667  Train Accuracy: 0.9301  Test Accuracy: 0.8800
[Model 1]  Epoch: 801  Train Loss: 25.3409  Train Accuracy: 0.9313  Test Accuracy: 0.8823
[Model 1]  Epoch: 851  Train Loss: 23.0407  Train Accuracy: 0.9345  Test Accuracy: 0.8830
[Model 1]  Epoch: 901  Train Loss: 20.8816  Train Accuracy: 0.9363  Test Accuracy: 0.8867
[Model 1]  Epoch: 951  Train Loss: 18.8845  Train Accuracy: 0.9387  Test Accuracy: 0.8903
  • 시각화
# 정확도 시각화
x = np.arange(len(model_1_train_acc_list))

plt.plot(x, model_1_train_acc_list, 'bc', label = 'train', markersize = 3)
plt.plot(x, model_1_test_acc_list, 'rv', label = 'test', markersize = 1)
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.grid()
plt.ylim(0, 1.0)
plt.legend()
plt.show()

# 손실함수 시각화
x = np.arange(len(model_1_train_loss_list))

plt.plot(x, model_1_train_loss_list, 'g--', label = 'train', markersize = 3)
plt.xlabel("Epochs")
plt.ylabel('Loss')
plt.grid()
plt.legend()
plt.show()

 

  • 모델 생성 및 학습 (2)

  - 사용 기법

  • 학습 데이터 수: 10,000
  • Hidden Layers: 4 [100, 100, 100, 100]
  • Adam
  • EPOCHS: 1000
  • 학습률: 1e-3(0.001)
  • 배치사이즈: 100
  • 드롭아웃: 0.5
  • 배치 정규화
  • 규제화: 0.15
# 데이터 로드 및 전처리
np.random.seed(42)

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
num_classes = 10

x_train = x_train[:10000]
x_test = x_test[:3000]

y_train = y_train[:10000]
y_test = y_test[:3000]

# flatten
x_train, x_test = x_train.reshape(-1, 28*28).astype(np.float32), x_test.reshape(-1, 28*28).astype(np.float32)

x_train = x_train / .255
x_test = x_test / .255

# y는 원-핫 벡터로 변경
y_train = np.eye(num_classes)[y_train]

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)


# 하이퍼 파라미터
epochs = 1000
learning_rate = 1e-3
batch_size = 100
train_size = x_train.shape[0]
iter_per_epoch = max(train_size / batch_size, 1)

decay_lambda_2 = 0.15
model_2 = MyModel(input_size = 784, hidden_size_list = [100, 100, 100, 100], decay_lambda = decay_lambda_2,
                  output_size = 10, use_dropout = True, dropout_ratio = 0.5, use_batchnorm = True)

optimizer = Adam(learning_rate = learning_rate)

model_2_train_loss_list = []
model_2_train_acc_list = []
model_2_test_acc_list = []


# 모델 생성 및 학습
for epoch in range(epochs):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    y_batch = y_train[batch_mask]

    grads = model_2.gradient(x_batch, y_batch)
    optimizer.update(model_2.params, grads)

    loss = model_2.loss(x_batch, y_batch)
    model_2_train_loss_list.append(loss)

    train_acc = model_2.accuracy(x_train, y_train)
    test_acc = model_2.accuracy(x_test, y_test)
    model_2_train_acc_list.append(train_acc)
    model_2_test_acc_list.append(test_acc)

    if epoch % 50 == 0:
        print("[Model 1]  Epoch: {}  Train Loss: {:.4f}  Train Accuracy: {:.4f}  Test Accuracy: {:.4f}".format(epoch+1, loss, train_acc, test_acc))

# 출력 결과
[Model 1]  Epoch: 1  Train Loss: 189.7545  Train Accuracy: 0.0730  Test Accuracy: 0.0750
[Model 1]  Epoch: 51  Train Loss: 110.1612  Train Accuracy: 0.2698  Test Accuracy: 0.2470
[Model 1]  Epoch: 101  Train Loss: 69.2994  Train Accuracy: 0.5468  Test Accuracy: 0.5150
[Model 1]  Epoch: 151  Train Loss: 44.7758  Train Accuracy: 0.5966  Test Accuracy: 0.5520
[Model 1]  Epoch: 201  Train Loss: 29.6832  Train Accuracy: 0.6948  Test Accuracy: 0.6287
[Model 1]  Epoch: 251  Train Loss: 20.2380  Train Accuracy: 0.7174  Test Accuracy: 0.6733
[Model 1]  Epoch: 301  Train Loss: 14.4343  Train Accuracy: 0.7739  Test Accuracy: 0.7323
[Model 1]  Epoch: 351  Train Loss: 10.3112  Train Accuracy: 0.7837  Test Accuracy: 0.7340
[Model 1]  Epoch: 401  Train Loss: 7.9462  Train Accuracy: 0.8494  Test Accuracy: 0.7950
[Model 1]  Epoch: 451  Train Loss: 6.2215  Train Accuracy: 0.8380  Test Accuracy: 0.7767
[Model 1]  Epoch: 501  Train Loss: 4.9697  Train Accuracy: 0.8574  Test Accuracy: 0.8087
[Model 1]  Epoch: 551  Train Loss: 4.3279  Train Accuracy: 0.8439  Test Accuracy: 0.7980
[Model 1]  Epoch: 601  Train Loss: 3.6755  Train Accuracy: 0.8670  Test Accuracy: 0.8337
[Model 1]  Epoch: 651  Train Loss: 3.1388  Train Accuracy: 0.8588  Test Accuracy: 0.8090
[Model 1]  Epoch: 701  Train Loss: 2.8542  Train Accuracy: 0.8635  Test Accuracy: 0.8040
[Model 1]  Epoch: 751  Train Loss: 2.5575  Train Accuracy: 0.8723  Test Accuracy: 0.8247
[Model 1]  Epoch: 801  Train Loss: 2.3355  Train Accuracy: 0.8722  Test Accuracy: 0.8247
[Model 1]  Epoch: 851  Train Loss: 2.3049  Train Accuracy: 0.8755  Test Accuracy: 0.8163
[Model 1]  Epoch: 901  Train Loss: 2.1523  Train Accuracy: 0.8509  Test Accuracy: 0.8027
  • 시각화
# 정확도 시각화
x = np.arange(len(model_2_train_acc_list))

plt.plot(x, model_2_train_acc_list, 'bo', label = 'train', markersize = 3)
plt.plot(x, model_2_test_acc_list, 'rv', label = 'test', markersize = 1)
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.grid()
plt.ylim(0, 1.0)
plt.legend()
plt.show()

# 손실함수 시각화
x = np.arange(len(model_2_train_loss_list))

plt.plot(x, model_2_train_loss_list, 'g--', label = 'train', markersize = 3)
plt.xlabel("Epochs")
plt.ylabel('Loss')
plt.grid()
plt.legend()
plt.show()

 

  • 모델 생성 및 학습 (3)

  - 사용 기법

  • 학습 데이터 수: 20,000
  • Hidden Layers: 3 [256, 100, 100]
  • Adam
  • EPOCHS: 1000
  • 학습률: 1e-2(0.01)
  • 배치사이즈: 100
  • 배치정규화
# 데이터 로드 및 전처리
np.random.seed(42)

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
num_classes = 10

x_train = x_train[:20000]
x_test = x_test[:3000]

y_train = y_train[:20000]
y_test = y_test[:3000]

# flatten
x_train, x_test = x_train.reshape(-1, 28*28).astype(np.float32), x_test.reshape(-1, 28*28).astype(np.float32)

x_train = x_train / .255
x_test = x_test / .255

# y는 원-핫 벡터로 변경
y_train = np.eye(num_classes)[y_train]


# 하이퍼 파라미터
epochs = 1000
learning_rate = 1e-2
batch_size = 100
train_size = x_train.shape[0]
iter_per_epoch = max(train_size / batch_size, 1)

decay_lambda_3 = 0
model_3 = MyModel(input_size = 784, hidden_size_list = [256, 100, 100], decay_lambda = decay_lambda_3,
                  output_size = 10, use_batchnorm = True)

optimizer = Adam(learning_rate = learning_rate)

model_3_train_loss_list = []
model_3_train_acc_list = []
model_3_test_acc_list = []


# 모델 생성 및 학습
for epoch in range(epochs):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    y_batch = y_train[batch_mask]

    grads = model_3.gradient(x_batch, y_batch)
    optimizer.update(model_3.params, grads)

    loss = model_3.loss(x_batch, y_batch)
    model_3_train_loss_list.append(loss)

    train_acc = model_3.accuracy(x_train, y_train)
    test_acc = model_3.accuracy(x_test, y_test)
    model_3_train_acc_list.append(train_acc)
    model_3_test_acc_list.append(test_acc)

    if epoch % 50 == 0:
        print("[Model 1]  Epoch: {}  Train Loss: {:.4f}  Train Accuracy: {:.4f}  Test Accuracy: {:.4f}".format(epoch+1, loss, train_acc, test_acc))


# 출력 결과
[Model 1]  Epoch: 1  Train Loss: 11.1115  Train Accuracy: 0.2633  Test Accuracy: 0.2520
[Model 1]  Epoch: 51  Train Loss: 0.3368  Train Accuracy: 0.8868  Test Accuracy: 0.8573
[Model 1]  Epoch: 101  Train Loss: 0.3627  Train Accuracy: 0.9221  Test Accuracy: 0.8937
[Model 1]  Epoch: 151  Train Loss: 0.1413  Train Accuracy: 0.9246  Test Accuracy: 0.8897
[Model 1]  Epoch: 201  Train Loss: 0.1724  Train Accuracy: 0.9344  Test Accuracy: 0.8950
[Model 1]  Epoch: 251  Train Loss: 0.2378  Train Accuracy: 0.9447  Test Accuracy: 0.9123
[Model 1]  Epoch: 301  Train Loss: 0.1957  Train Accuracy: 0.9496  Test Accuracy: 0.9133
[Model 1]  Epoch: 351  Train Loss: 0.0789  Train Accuracy: 0.9612  Test Accuracy: 0.9300
[Model 1]  Epoch: 401  Train Loss: 0.1396  Train Accuracy: 0.9544  Test Accuracy: 0.9150
[Model 1]  Epoch: 451  Train Loss: 0.0557  Train Accuracy: 0.9593  Test Accuracy: 0.9223
[Model 1]  Epoch: 501  Train Loss: 0.0462  Train Accuracy: 0.9615  Test Accuracy: 0.9250
[Model 1]  Epoch: 551  Train Loss: 0.0584  Train Accuracy: 0.9661  Test Accuracy: 0.9340
[Model 1]  Epoch: 601  Train Loss: 0.1176  Train Accuracy: 0.9692  Test Accuracy: 0.9323
[Model 1]  Epoch: 651  Train Loss: 0.0956  Train Accuracy: 0.9679  Test Accuracy: 0.9300
[Model 1]  Epoch: 701  Train Loss: 0.0324  Train Accuracy: 0.9703  Test Accuracy: 0.9377
[Model 1]  Epoch: 751  Train Loss: 0.0896  Train Accuracy: 0.9640  Test Accuracy: 0.9317
[Model 1]  Epoch: 801  Train Loss: 0.0107  Train Accuracy: 0.9813  Test Accuracy: 0.9413
[Model 1]  Epoch: 851  Train Loss: 0.1093  Train Accuracy: 0.9795  Test Accuracy: 0.9450
[Model 1]  Epoch: 901  Train Loss: 0.0329  Train Accuracy: 0.9755  Test Accuracy: 0.9353
[Model 1]  Epoch: 951  Train Loss: 0.0891  Train Accuracy: 0.9759  Test Accuracy: 0.9357
  • 시각화
# 정확도 시각화
x = np.arange(len(model_3_train_acc_list))

plt.plot(x, model_3_train_acc_list, 'bo', label = 'train', markersize = 3)
plt.plot(x, model_3_test_acc_list, 'rv', label = 'test', markersize = 1)
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.grid()
plt.ylim(0, 1.0)
plt.legend()
plt.show()

# 손실함수 시각화
x = np.arange(len(model_1_train_loss_list))

plt.plot(x, model_3_train_loss_list, 'g--', label = 'train', markersize = 3)
plt.xlabel("Epochs")
plt.ylabel('Loss')
plt.grid()
plt.legend()
plt.show()

 

  • 세가지 모델 비교
    • 위의 세가지 모델은 전체적으로 학습 데이터 수를 일부로 제한했기 때문에 학습이 잘 안 될 가능성이 높음,
      따라서 여러 학습 기술들을 적용함
x = np.arange(len(model_3_train_acc_list))

plt.plot(x, model_1_train_acc_list, 'b--', label = 'Model 1 train', markersize = 3)
plt.plot(x, model_2_train_acc_list, 'r:', label = 'Model 2 train', markersize = 3)
plt.plot(x, model_3_train_acc_list, 'go', label = 'Model 3 train', markersize = 3)
plt.xlabel("Epochs")
plt.ylabel('Accuracy')
plt.grid()
plt.ylim(0, 1.0)
plt.legend()
plt.show()

1. 딥러닝 학습 기술

  • Optimization(매개변수 갱신(확률적 경사하강법, 모멘텀, AdaGrad, Adam))
  • Weight Decay
  • Batch Normalization
  • 과대적합(Overfitting) / 과소적합(Underfitting)
  • 규제화(Regularization)
  • 드롭아웃(Drop out)
  • 하이퍼 파라미터
    • 학습률(Learning Rate)
    • 학습 횟수
    • 미니배치 크기

 

 

2. 최적화 방법: 매개변수 갱신

  - 확률적 경사하강법(Stochastic Gradient Descent, SGD)

  • 전체를 한번에 계산하지 않고, 확률적으로 일부 샘플을 뽑아 조금씩 나누어 학습을 시키는 과정
  • 반복할 때마다 다루는 데이터의 수가 적기 때문에 한 번에 처리하는 속도는 빠름
  • 한 번 학습할 때 필요한 메모리만 있으면 되므로 매우 큰 데이터셋에 대해서도 학습이 가능
  • 확률적이기 때문에, 배치 경사하강법보다 불안정
  • 손실함수의 최솟값에 이를 때까지 다소 위아래로 요동치면서 이동
  • 따라서, 위와 같은 문제 때문에 미니 배치 경사하강법(mini-batch gradient descent)로 학습을 진행,
    요즘에는 보통 SGD라고 하면 미니 배치 경사하강법을 의미하기도 함
  • 배치 경사하강법
    미니 배치 경사하강법
    확률적 경사하강법
  • \( W \leftarrow W-\gamma \frac{\partial L}{\partial W} \)

    \( \gamma \): 학습률
class SGD:
    def __init__(self, learning_rate = 0.01):
        self.learning_rate = learning_rate

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.learning_rate * grads[key]

optimizer = SGD()

# 계속 optimizer을 update 해주며 동작
for i in range(10000):
    #optimizer.update(params, grads)
  • SGD 단점: 단순하지만, 문제에 따라서 시간이 매우 오래걸림

 

  - 모멘텀(Momentum)

  • 운동량을 의미, 관성과 관련
  • 공이 그릇의 경사면을 따라서 내려가는 듯한 모습
  • 이전의 속도를 유지하려면 성향
    경사하강을 좀 더 유지하려는 성격을 지님
  • 단순히 SGD만 사용하는 것보다 적게 방향이 변함

https://link.springer.com/chapter/10.1007/978-1-4842-4470-8_33\upsilon\leftarrow&nbsp;\alpha&nbsp;\upsilon&nbsp;-\gamma&nbsp;\frac{\partial&nbsp;L}{\partial&nbsp;W}

  • \( \upsilon\leftarrow \alpha \upsilon -\gamma \frac{\partial L}{\partial W}  \)
    \( W \leftarrow W-\upsilon \)

    \( \alpha \): 관성계수
    \( \upsilon \): 속도
    \( \gamma \): 학습률
    \( \frac{\partial L}{\partial W} \): 손실함수에 대한 미분
import numpy as np

class Momentum:
    def __init__(self, learning_rate = 0.01, momentum = 0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.v = None
    
    def update(self, params, grads):
        if self.v is None:
            self.v = {}
            # 처음 알고리즘 시행 시, v를 초기화시킨 후,param의 key를 각각 할당해줌
            for key, val in params.items():
                self.v[key] = np.zeros_like(val)
        
        for key in params.keys():
            self.v[key] = self.momentum * self.v[key] - self.learning_rate * grads[key]
            params[key] += self.v[key]

 

  - AdaGrad(Adaptive Gradient)

  • 가장 가파른 경사를 따라 빠르게 하강하는 방법
  • 적응적 학습률이라고도 함, 학습률을 변화시키며 진행
  • 경사가 급할 때는 빠르게 변화,
    완만할 때는 느리게 변화
  • 간단한 문제에서는 좋을 수 있지만 딥러닝에서는 자주 쓰이지 않음,
    학습률이 너무 감소되어 전여최소값(global minimum)에 도달하기 전에 학습이 빨리 종료될 수 있기 때문
  • \( h\leftarrow h+\frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} \)
    \( W \leftarrow W+\gamma \frac{1}{\sqrt{h}} \frac{\partial L}{\partial W} \)

    \( h \): 기존 기울기를 제곱하여 더한 값
    \( \gamma \): 학습률
    \( \frac{\partial L}{\partial W} \): \( W \)에 대한 미분
  • 과거의 기울기를 제곱하여 계속 더하기 때문에 학습을 진행할수록 갱신 강도가 약해짐(\( \because \frac{1}{\sqrt{h}} \))
class AdaGrad:
    def __init__(self, learning_rate = 0.01):
        self.learning_rate = learning_rate
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
        
        for key in params.keys():
            self.h[key] += grads[key] * grads[key]
            params[key] -= self.learning_rate * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

 

  - RMSProp(Root Mean Square Propagation)

  • AdaGrad를 보완하기 위한 방법으로 등장
  • 합 대신 지수의 평균값을 활용
  • 학습이 안되기 시작하면 학습률이 커져 잘 되게끔하고, 학습률이 너무 크면 학습률을 다시 줄임
  • \( h  \leftarrow   \rho  h + (1 - \rho) \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} \)
    \(  W  \leftarrow  W  + \gamma \frac{\partial L}{\partial W}  / \sqrt{h + \epsilon} \)

    \( h \): 기존 기울기를 제곱하여 업데이트 계수를 곱한 값과 업데이트 계수를 곱한 값을 더해줌
    \( \rho \): 지수 평균의 업데이트 계수
    \( \gamma \): 학습률
    \( \frac{\partial L}{partial W} \): \( W \)에 대한 미분
class RMSProp:
    def __init__(self, learning_rate = 0.01, decay_rate = 0.99):
        self.learning_rate = learning_rate
        self.decay_rate = decay_rate
        self.h = None

    def update(self, params, grads):
        if self.h is None:
            self.h = {}
            for key, val in params.items():
                self.h[key] = np.zeros_like(val)
        
        for key in params.keys():
            self.h[key] *= self.decay_rate
            self.h[key] += (1 - self.decay_rate) * grads[key] * grads[key]
            params[key] -= self.learning_rate * grads[key] / (np.sqrt(self.h[key]) + 1e-7)

 

  - Adam(Adaptive moment estimation)

  • 모멘텀 최적화와 RMSProp의 아이디어를 합친 것
  • 지난 그레디언트의 지수 감소 평균을 따르고(Momentum), 지난 그레디언트 제곱의 지수 감소된 평균(RMSProp)을 따름
  • 가장 많이 사용되는 최적화 방법
  • \( t \leftarrow t + 1 \)
    \( m_t \leftarrow \beta_1 m_{t-1} - (1 - \beta_1)\frac{\partial L}{\partial W} \)
    \( v_t \leftarrow \beta_2 v_{t-1} + (1 - \beta_2) \frac{\partial L}{\partial W} \odot \frac{\partial L}{\partial W} \)
    \( \hat{m_t} \leftarrow \frac{m_t}{1 - \beta_1^t} \)
    \( \hat{v_t} \leftarrow \frac{v_t}{1 - \beta_2^t} \)
    \( W_t \leftarrow W_{t-1} + \gamma \hat{m_t}/\sqrt{\hat{v_t} + \epsilon} \)

    \( \beta \): 지수 평균의 업데이트 계수
    \( \gamma \): 학습률
    \( \beta_{1} \approx 0.9, \beta_{2} \approx 0.999 \)
    \( \frac{\partial L}{\partial W} \): \( W \)에 대한 미분
class Adam:
    def __init__(self, learning_rate = 0.01, beta1 = 0.9, beta2 = 0.999):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.iter = 0
        self.m = None
        self.v = None

    def update(self, params, grads):
        if self.m is None:
            self.m, self.v = {}, {}
            for key, val in params.items():
                self.m[key] = np.zeros_like(val)
                self.v[key] = np.zeros_like(val)
        
        self.iter += 1
        learning_rate_t = self.learning_rate * np.sqrt(1.0 - self.beta2**self.iter) / (1.0 - self.beta1**self.iter)

        for key in params.keys():
            self.m[key] += (1 - self.beta1) * (grads[key] - self.m[key])
            self.v[key] += (1 - self.beta2) * (grads[key]**2 - self.v[key])

            params[key] -= learning_rate_t * self.m[key] / (np.sqrt(self.v[key]) + 1e-7)

 

  - 최적화 방법 비교

https://github.com/ilguyi/optimizers.numpy

 

 

3. 가중치 소실(Gradient Vanishing) - AI 두번째 위기

  • 활성화함수가 Sigmoid 함수일 때, 은닉층의 개수가 늘어날수록 가중치가 역전파되면서 가중치 소실 문제 발생
    • 0~1 사이의 값으로 추력되면서 0 또는 1에 가중치 값이 퍼짐,
      이는 미분값이 점점 0에 가까워짐을 의미
    • ReLU 함수 등장(비선형 함수)
  • 가중치 초기화 문제(은닉층의 활성화값 분포)
    • 가중치의 값이 일부 값으로 치우치게 되면
      활성화 함수를 통과한 값이 치우치게 되고, 표현할 수 있는 신경망의 수가 적어짐
    • 따라서, 활성화값이 골고루 분포되는 것이 중요

https://www.kaggle.com/getting-started/118228

  • 가중치 초기화

  - 아래의 사이트에서 가중치와 기울기가 0일때, 너무 작을 때, 적절할 때, 너무 클 때의 훈련 과정을 볼 수 있음
https://www.deeplearning.ai/ai-notes/initialization/index.html

 

AI Notes: Initializing neural networks - deeplearning.ai

In this post, we'll explain how to initialize neural network parameters effectively. Initialization can have a significant impact on convergence in training deep neural networks...

www.deeplearning.ai

 

  - 초기값: 0(zeros)

  • 학습이 올바르게 진행되지 않음
  • 0으로 설정하면, 오차역전파법에서 모든 가중치 값이 똑같이 갱신됨
import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.zeros((nodes, nodes))
    a = np.dot(x, w)
    z = sigmoid(a)
    activation_values[i] = z

import matplotlib.pyplot as plt

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: 균일분포(Uniform)

  • 활성화 값이 균일하지 않음(활성화함수: sigmoid)
  • 역전파로 전해지는 기울기값이 사라짐
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.uniform(1, 10, (nodes, nodes))
    a = np.dot(x, w)
    z = sigmoid(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: 정규분포(nomalization)

  • 활성화함수를 통과하면 양쪽으로 퍼짐
  • 0과 1에 퍼지면서 기울기 소실문제(gradient vanishing) 발생
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes)
    a = np.dot(x, w)
    z = sigmoid(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 아주 작은 정규분포값으로 가중치 초기화

  • 0과 1로 퍼지지 않고, 한 곳에 치우쳐 짐
  • 해당 신경망이 표현할 수 있는 문제가 제한됨
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

	# 0.01을 곱해 작은 값으로 변경
    w = np.random.randn(nodes, nodes) * 0.01
    a = np.dot(x, w)
    z = sigmoid(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: Xavier(Glorot)

  • 은닉층의 노드의 수가 n이라면 표준편차가 \( \frac{1}{\sqrt{n}} \)인 분포
  • 더 많은 가중치에 역전파가 전달 가능하고, 비교적 많은 문제를 표현할 수 있음
  • 활성화 함수가 선형인 함수일 때 매우 적합
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes) / np.sqrt(nodes)
    a = np.dot(x, w)
    z = sigmoid(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: Xavier(Glorot) - tanh

  • 활성화함: tanh
  • sigmoid 함수보다 더 깔끔한 종모양으로 분포
# sigmoid 대신 tanh 사용
def tanh(x):
    return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes) / np.sqrt(nodes)
    a = np.dot(x, w)
    z = tanh(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  • 비선형 함수에서의 가중치 초기화

  - 초기값: 0(zeros)

  • 활성화함수: ReLU
def ReLU(x):
    return np.maximum(0, x)

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.zeros((nodes, nodes))
    a = np.dot(x, w)
    z = ReLU(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

  • 초기값을 0으로 쓰는 것은 어떤 상황이든 좋지 않음

 

  - 초기값: 정규분포(nomalization)

  • 활성화함수: ReLU
def ReLU(x):
    return np.maximum(0, x)

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes)
    a = np.dot(x, w)
    z = ReLU(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 표준편차: 0.01일 때

def ReLU(x):
    return np.maximum(0, x)

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes) * 0.01
    a = np.dot(x, w)
    z = ReLU(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: Xavier(Glorot)

def ReLU(x):
    return np.maximum(0, x)

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes) / np.sqrt(nodes)
    a = np.dot(x, w)
    z = ReLU(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

  - 초기값: He

  • 표준편차가 \( \sqrt{\frac{2}{n}} \)인 분포
  • 활성화값 분포가 균일하게 분포되어 있음
  • 활성화함수가 ReLU와 같은 비선형함수 일 때 더 적합하다고 알려진 분포
def ReLU(x):
    return np.maximum(0, x)

x = np.random.randn(1000, 50)
nodes = 50
hidden_layers = 6
activation_values = {}

for i in range(hidden_layers):
    if i != 0:
        x = activation_values[i-1]

    w = np.random.randn(nodes, nodes) * np.sqrt(2 / nodes)
    a = np.dot(x, w)
    z = ReLU(a)
    activation_values[i] = z

plt.figure(figsize = (12, 6))
for i, a in activation_values.items():
    plt.subplot(1, len(activation_values), i+1)
    plt.title(str(i+1) + 'th layer')
    plt.hist(a.flatten(), 50, range = (0, 1))
    plt.subplots_adjust(wspace = 0.5, hspace = 0.5)

plt.show()

 

 

4. 배치 정규화(Batch Normalization)

  • 가중치의 활성화값이 적당히 퍼지게끔 '강제'로 적용시키는 것
  • 미니배치 단위로 데이터의 평균이 0, 표준편차가 1로 정규화
  • 학습을 빨리 진행할 수 있음
  • 초기값에 크게 의존하지 않아도 됨
  • 과적합을 방지
  • 보통 Fully-Connected와 활성화함수(비선형) 사이에 놓임

https://www.jeremyjordan.me/batch-normalization/

class BatchNormalization:
    def __init__(self, gamma, beta, momentum = 0.9, running_mean = None, running_var = None):
        self.gamma = gamma
        self.beta = beta
        self.momentum = momentum
        self.input_shape = None
        
        self.running_mean = running_mean
        self.running_var = running_var
        
        self.batch_size = None
        self.xc = None
        self.std = None
        self.dgamma = None
        self.dbeta = None
    
    def forward(self, input_data, is_train = True):
        self.input_shape = input_data.shape
        if input_data.ndim != 2:
            N, C, H, W = input_data.shape
            input_data = input_data.reshape(N, -1)

        out = self.__forward(input_data, is_train)

        return out.reshape(*self.input_shape)
    
    def __forward(self, input_data, is_train):
        if self.running_mean is None:
            N, D = input_data.shape
            self.running_mean = np.zeros(D)
            self.running_var = np.zeros(D)
        
        if is_train:
            mu = input_data.mean(axis = 0)
            xc = input_data - mu
            var = np.mean(xc**2, axis = 0)
            std = np.sqrt(var + 10e-7)
            xn = xc / std

            self.batch_size = input_data.shape[0]
            self.xc = xc
            self.std = std
            self.running_mean = self.momentum * self.running_rate + (1 - self.momentum) * mu 
            self.running_var = self.momentum * self.running_var + (1 - self.momentum) * var
        
        else:
            xc = input_data - self.running_mean
            xn = xc / ((np.sqrt(self.running_var + 10e-7)))
        
        out = self.gamma * xn + self.beta
        return out
    
    def backward(self, dout):
        if dout.ndim != 2:
            N, C, H, W = dout.shape
            dout = dout.reshape(N, -1)
        
        dx = self.__backward(dout)

        dx = dx.reshape(*self.input_shape)
        return dx
    
    def __backward(self, dout):
        dbeta = dout.sum(axis = 0)
        dgamma = np.sum(self.xn * dout, axis = 0)
        dxn = self.gamma * dout
        dxc = dxn / self.std
        dstd = -np.sum((dxn * self.xc) / (self.std * self.std), axis = 0)
        dvar = 0.5 * dstd / self.std
        dxc += (2.0 / self.batch_size) * self.xc * dvar
        dmu = np.sum(dxc, axis = 0)
        dx = dxc - dmu / self.batch_size

        self.dgamma = dgamma
        self.dbeta = dbeta

        return dx

 

 

5. 과대적합(Overfitting) / 과소적합(Underfitting)

https://towardsdatascience.com/underfitting-and-overfitting-in-machine-learning-and-how-to-deal-with-it-6fe4a8a49dbf

  - 과대적합 (Overfitting, 오버피팅)

  • 모델이 학습 데이터에 한에서만 좋은 성능을 보이고 새로운 데이터에는 그렇지 못한 경우
  • 학습 데이터가 매우 적을 경우- 모델이 지나치게 복잡한 경우
  • 학습 횟수가 매우 많을 경우
  • 해결방안
    • 학습 데이터를 다양하게 수집
    • 모델을 단순화(파라미터가 적은 모델을 선택하거나, 학습 데이터의 특성 수를 줄이거나)
    • 정규화(Regularization)을 통한 규칙을 단순화
    • 적정한 하이퍼 파라미터 찾기

 

  - 과소적합 (Underfitting, 언더피팅)

  • 학습 데이터를 충분히 학습하지 않아 성능이 매우 안 좋을 경우
  • 모델이 지나치게 단순한 경우
  • 해결방안
    • 충분한 학습 데이터 수집
    • 보다 더 복잡한 모델
    • 에폭수(epochs)를 늘려 충분히 학습

 

 

6. 규제화(Regularization) - 가중치 감소

  • 과대적합(Overfitting, 오버피팅)을 방지하는 방법 중 하나
  • 과대적합은 가중치의 매개변수 값이 커서 발생하는 경우가 많음,
    이를 방지하기 위해 큰 가중치 값에 큰 규제를 가하는 것
  • 규제란 가중치의 절댓값을 가능한 작게 만드는 것으로,
    가중치의 모든 원소를 0에 가깝게 하여 모든 특성이 출력에 주는 영향을 최소한으로 만드는 것(기울기를 작게 만드는 것)을 의미한다.
    즉, 규제란 과대적합이 되지 않도록 모델을 강제로 제한한다는 의미
  • 적절한 규제값을 찾는 것이 중요.

 

  - L2 규제

  • 가중치의 제곱합
  • 손실함수 일정 값을 더함으로써 과적합을 방지
  • \( \lambda \)값이 크면 가중치 감소가 커지고, 작으면 가하는 규제가 적어짐
  • 더 Robust한 모델을 생성하므로 L1보다 많이 사용됨
  • \(Cost = \frac{1}{n} \sum{^n}_{i=1} {L(y_i, \hat{y_i}) + \frac{\lambda}{2}w^2} \)
    \( L(y_i, \hat{y_i}) \): 기존 Cost Function
# 작동 원리
def loss(X, true_y):
    # weight_decay += 0.5 * weight_decay_lambda * np.sum(W**2)
    # return weight_decay

 

  - L1 규제

  • 가중치의 절대값 합
  • L2 규제와 달리 어떤 가중치는 0이 되는데 이는 모델이 가벼워짐을 의미
  • \( Cost = \frac{1}{n} \sum{^n}_{i=1} {L(y_i, \hat{y_i}) + \frac{\lambda}{2} \left | w \right |} \)
    \( L(y_i, \hat{y_i}) \): 기존 Cost Function
# 작동 원리
def loss(X, true_y):
    # weight_decay += 0.5 * weight_decay_lambda * np.sum(np.abs(W))
    # return weight_decay

 

 

7. 드롭아웃(Dropout)

  • 과적합을 방지하기 위한 방법
  • 학습할 때 사용하는 노드의 수를 전체 노드 중에서 일부만을 사용
  • 보통 ratio_value는 0.5 또는 0.7

https://medium.com/konvergen/understanding-dropout-ddb60c9f98aa

class Dropout:
    def __init__(self, dropout_ratio = 0.5):
        self.dropout_ratio = dropout_ratio
        self.mask = None

    def forward(self, input_data, is_train = True):
        if is_train:
            self.mask = np.random.rand(*input_data.shape) > self.dropout_ratio
            return input_data * self.mask
        else:
            return input_data * (1.0 - self.dropout_ratio)
        
    def backward(self, dout):
        return dout * self.mask

 

 

8. 하이퍼 파라미터(hyper Parameter)

  - 학습률(Learning Rate)

  • 적절한 학습률에 따라 학습 정도가 달라짐
  • 적당한 학습률을 찾는 것이 핵심

 

  - 학습 횟수(Epochs)

  • 학습 횟수를 너무 작게, 또는 너무 크게 지정하면 과소적합 또는 과대적합
  • 몇 번씩 진행하며 최적의 epochs 값을 찾기

 

  - 미니 배치 크기(Mini Batch Size)

  • 미니 배치 학습: 한번 학습할 때 메모리의 부족현상을 막기 위해 전체 데이터의 일부를 여러 번 학습하는 방식
  • 한번 학습할 때마다 얼마만큼의 미니배치 크기를 사용할 지 결정
  • 배치 크기가 작을수록 학습 시간이 많이 소요되고,
    클수록 학습 시간이 적게 소요됨

 

 

9. 검증 데이터(Validation Data)

  • 주어진 데이터를 학습 + 검증 + 테스트 데이터로 구분하여 과적합을 방지
  • 일반적으로 전체 데이터의 2~30%를 테스트 데이터,
    나머지 20% 정도를 검증용 데이터,
    남은 부분을 학습용 데이터로 사용

https://towardsdatascience.com/train-test-split-and-cross-validation-in-python-80b61beca4b6

1. 오차역전파 알고리즘

  • 학습 데이터로 정방향(forward) 연산을 통해 손실함수 값(loss)을 구함
  • 각 layer별로 역전파 학습을 위해 중간값을 저장
  • 손실함수를 학습 파라미터(가중치, 편향)으로 미분하여,
    마지막 layer로부터 앞으로 하나씩 연쇄법칙을 이용하여 미분 각 layer 를 통과할 때마다 저장된 값을 이용
  • 오류(error)를 전달하면서 학습 파라미터를 조금씩 갱신

 

  - 오차역전파 학습의 특징

  • 손실함수를 통한 평가를 한 번만 하고, 연쇄 법칙을 이용한 미분을 활용하기 때문에 학습 소요시간이 매우 단축
  • 미분을 위한 중간값을 모두 저장하기 때문에 메모리를 많이 사용

 

  - 신경망 학습에 있어서 미분 가능성의 중요성

  • 경사하강법(Gradient Descent)에서 손실 함수(cost function)의 최소값 즉, 최적값을 찾기 위한 방법으로 미분 활용
  • 미분을 통해 손실함수의 학습 매개변수(trainable parameter)를 갱신하여 모델의 가중치의 최적값을 찾는 과정

https://www.pinterest.co.kr/pin/424816177350692379/

 

2. 합성 함수의 미분(연쇄법칙, chain rule)

$$ \frac{d}{dx}[f(g(x))]=f'(g(x))g'(x) $$

  • 여러 개 연속으로 사용가능
    \( \frac{\partial f}{\partial x}=\frac{\partial f}{\partial u} \times \frac{\partial u}{\partial m} \times \frac{\partial m}{\partial n} \times \cdots \times \frac{\partial l}{\partial k} \times \frac{\partial k}{\partial g} \times \frac{\partial g}{\partial x} \)
  • 각각에 대해 편미분 적용 가능

https://www.freecodecamp.org/news/demystifying-gradient-descent-and-backpropagation-via-logistic-regression-based-image-classification-9b5526c2ed46/

  • 오차역전파의 직관적 이해
    • 학습을 진행하면서, 즉 손실함수의 최소값을 찾아가는 과정에서 가중치 또는 편향의 변화에 따라 얼마나 영향을 받는지 알 수 있음

 

  - 합성함수 미분 예제

https://medium.com/spidernitt/breaking-down-neural-networks-an-intuitive-approach-to-backpropagation-3b2ff958794c

\( a=-1, \,\, b=3, \,\, c=4,\)

\( x=a+b, \,\, y=b+c, \,\, f=x*y \)일때

 

\( \begin{matrix} \frac{\partial f}{\partial x} &=& y+x\frac{\partial y}{\partial x} \\ &=& (b+c)+(a+b)\times 0 \\ &=& 7 \end{matrix} \)

 

\( \begin{matrix} \frac{\partial f}{\partial y} &=& x+\frac{\partial x}{\partial y}y \\ &=& (a+b)+0 \times (b+c) \\ &=& 2 \end{matrix} \)

 

\( \begin{matrix} \frac{\partial x}{\partial a} &=& 1+a\frac{\partial b}{\partial a} \\ &=& 1 \end{matrix} \)

 

\( \begin{matrix} \frac{\partial y}{\partial c} &=& \frac{\partial b}{\partial c}+1 \\ &=& 1 \end{matrix} \)

 

\( \begin{matrix} \frac{\partial f}{\partial a} &=& \frac{\partial f}{\partial x} \times \frac{\partial x}{\partial a} \\ &=& y \times 1 \\ &=& 7 \times 1 = 7 \end{matrix} \)

 

\( \begin{matrix} \frac{\partial f}{\partial b} &=& \frac{\partial x}{\partial b}y+x\frac{\partial y}{\partial b} \\ &=& 1 \times 7+2 \times 1 = 9 \end{matrix} \)

 

  - 덧셈, 곱셈 계층의 역전파

  • 위 예제를 통해 아래 사항을 알 수 있음
    1. \( z=x+y \) 일 때,
      \( \frac {\partial z}{\partial x}=1, \frac {\partial z}{\partial y}=1 \)
    2. \( t = xy \) 일 때,
      \( \frac {\partial t}{\partial x}=y, \frac {\partial t}{\partial y}=x \)
# 곱셈 연산
class Mul():

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

    def forward(self, x, y):
        self.x = x
        self.y = y
        result = x*y
        return result
    
    def backward(self, dresult):
        dx = dresult * self.y
        dy = dresult * self.x
        return dx, dy

# 덧셈 연산
class Add():
    
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y
        result = x + y
        return result
    
    def backward(self, dresult):
        dx = dresult * 1
        dy = dresult * 1
        return dx, dy

a, b, c = -1, 3, 4
x = Add()
y = Add()
f = Mul()
# forward
x_result = x.forward(a, b)
y_result = y.forward(b, c)

print(x_result)
print(y_result)
print(f.forward(x_result, y_result))

# 출력 결과
2
7
14
# backward
dresult = 1
dx_mul, dy_mul = f.backward(dresult)

da_add, db_add_1 = x.backward(dx_mul)
db_add_2, dc_add = y.backward(dy_mul)

print(dx_mul, dy_mul)
print(da_add)
print(db_add_1 + db_add_2)
print(dc_add)

# 출력 결과
7 2
7
9
2

https://medium.com/spidernitt/breaking-down-neural-networks-an-intuitive-approach-to-backpropagation-3b2ff958794c

 

3. 활성화 함수에서의 역전파

  - 시그모이드(sigmoid) 함수

https://www.geeksforgeeks.org/implement-sigmoid-function-using-numpy/

  • 수식
    \( y=\frac{1}{1+e^{-x}} \) 일 때,
    \( \begin{matrix} y' &=& \left ( \frac{1}{1+e^{-x}} \right )' \\
    &=& \frac{-1}{(1+e^{-x})^{2}} \times (-e^{-x}) \\
    &=& \frac{1}{1+e^{-x}} \times \frac{e^{-x}}{1+e^{-x}} \\
    &=& \frac{1}{1+e^{-x}} \times \left ( 1-\frac{1}{1+e^{-x}} \right ) \\
    &=& y(1-y) \end{matrix} \)
class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        return out
    
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.dout
        return dx

 

   - ReLU 함수

https://machinelearningmastery.com/rectified-linear-activation-function-for-deep-learning-neural-networks/

  • 수식
    /\( y=\left \{ \begin{matrix} x & (x \geq 0) \\ 0 & (x<0) \end{matrix} \right. \)
class ReLU():
    def __init__(self):
        self.out = None
    
    def forward(self, x):
        self.mask = (x < 0)
        out = x.copy()
        # 마스킹 연산자(x<0일때는 전부 0으로 넣어버리기)
        out[x<0] = 0
        return out
    
    def backward(self, dout):
        dout[self.mask] = 0
        dx =dout
        return dx

 

4. 행렬 연산에 대한 역전파

$$ Y=X \bullet W+B $$

  - 순전파(forward)

  • 형상(shape)을 맞춰줘야함
  • 곱셈, 덧셈 계층을 합친 상태
# shape이 맞을 때
import numpy as np

X = np.random.rand(3)
W = np.random.rand(3, 2)
B = np.random.rand(2)

print(X.shape)
print(W.shape)
print(B.shape)

# 출력 결과
(3,)
(3, 2)
(2,)

Y = np.dot(X, W) + B
print(Y.shape)

# 출력 결과
(2,)
# shape이 틀릴 때
import numpy as np

X = np.random.rand(3)
W = np.random.rand(2, 2)
B = np.random.rand(2)

Y = np.dot(X, W) + B
print(Y.shape)

# 출력 결과
ValueError: shapes (3,) and (2,2) not aligned: 3 (dim 0) != 2 (dim 0)

 

  - 역전파(1)

$$ Y=X \bullet W $$

  • \( X \): (2, )
  • \( W \): (2, 3)
  • \( X \bullet W \): (3, )
  • \( \frac{\partial L}{\partial Y} \): (3, )
  • \( \frac{\partial L}{\partial X}=\frac{\partial L}{\partial Y} \bullet W^{T}, (2, ) \)
  • \( \frac{\partial L}{\partial W}=X^{T} \bullet \frac{\partial L}{\partial Y}, (2, 3) \)
# 순전파
X = np.random.rand(2)
W = np.random.rand(2, 3)
Y = np.dot(X, W)

print("X\n{}".format(X))
print("W\n{}".format(W))
print("Y\n{}".format(Y))

# 출력 결과
X
[0.82112094 0.52401537]
W
[[0.98913291 0.3114957  0.74020997]
 [0.0272213  0.29891712 0.30511339]]
Y
[0.82646212 0.41241281 0.76768601]
# 역전파
dL_dY = np.random.randn(3)
dL_dX = np.dot(dL_dY, W.T)
dL_dW = np.dot(X.reshape(-1, 1), dL_dY.reshape(1, -1))

print("dL_dY\n{}".format(dL_dY))
print("dL_dX\n{}".format(dL_dX))
print("dL_dW\n{}".format(dL_dW))

# 출력 결과
dL_dY
[ 2.14017912 -1.88100173 -0.33160328]
dL_dX
[ 1.28554159 -0.60518177]
dL_dW
[[ 1.75734588 -1.5445299  -0.2722864 ]
 [ 1.12148676 -0.98567383 -0.17376522]]

 

  - 역전파(2)

$$ Y=X \bullet W+B $$

  • X, W는 위와 동일
  • B: (3, )
  • \( \frac{\partial L}{\partial B}=\frac{\partial L}{\partial Y}, (3, ) \)
  •  
# 순전파
X = np.random.randn(2)
W = np.random.randn(2, 3)
B = np.random.randn(3)
Y = np.dot(X, W) + B
print(Y)

# 출력 결과
[1.32055282 0.71833361 1.73777915]
# 역전파
dL_dY = np.random.randn(3)
dL_dX = np.dot(dL_dY, W.T)
dL_dW = np.dot(X.reshape(-1, 1), dL_dY.reshape(1, -1))
dL_dB = dL_dY

print("dL_dY\n{}".format(dL_dY))
print("dL_dX\n{}".format(dL_dX))
print("dL_dW\n{}".format(dL_dW))
print("dL_dB\n{}".format(dL_dB))

# 출력 결과
dL_dY
[-0.00997423  0.34937897  1.55598133]
dL_dX
[ 0.9368195  -0.10629718]
dL_dW
[[ 0.00182182 -0.06381513 -0.28420473]
 [ 0.00772224 -0.2704957  -1.20466967]]
dL_dB
[-0.00997423  0.34937897  1.55598133]

 

   - 배치용 행렬 내적 계층

  • N개의 데이터에 대해,

$$ Y=X \bullet W+B $$

    • \( X \): (N, 3)
    • \( W \): (3, 2)
    • \( B \): (2, )
X = np.random.rand(4, 3)
W = np.random.rand(3, 2)
B = np.random.rand(2)

print(X.shape)
print(W.shape)
print(B.shape)

# 출력 결과
(4, 3)
(3, 2)
(2,)

print("X\n{}".format(X))
print("W\n{}".format(W))
print("B\n{}".format(B))

# 출력 결과
X
[[0.5345643  0.82120127 0.38182761]
 [0.07479261 0.99042377 0.50473867]
 [0.47142528 0.72266964 0.44472929]
 [0.16390528 0.94442809 0.78815273]]
W
[[0.90326978 0.75695534]
 [0.24771738 0.05041714]
 [0.5838499  0.60451043]]
B
[0.90558133 0.19752999]
# 순전파
Y = np.dot(X, W) + B
print("Y\n{}".format(Y))
print("Y.shape:", Y.shape)

# 출력 결과
Y
[[1.81479294 0.87439269]
 [1.51317603 0.60919879]
 [1.77007852 0.85965631]
 [1.74774615 0.84566088]]
Y.shape: (4, 2)
# 역전파
dL_dY = np.random.randn(4, 2)
dL_dX = np.dot(dL_dY, W.T)
dL_dW = np.dot(X.T, dL_dY)
dL_dB = np.sum(dL_dY, axis = 0)

print("dL_dY\n{}".format(dL_dY))
print("dL_dX\n{}".format(dL_dX))
print("dL_dW\n{}".format(dL_dW))
print("dL_dB\n{}".format(dL_dB))

# 출력 결과
dL_dY
[[-0.70142117  1.06162232]
 [-0.114932    0.16975345]
 [-1.51024593  0.19728549]
 [ 1.93432977  0.58605845]]
dL_dX
[[ 0.17002814 -0.12023025  0.23223709]
 [ 0.02468118 -0.01991217  0.0355147 ]
 [-1.2148232  -0.36416759 -0.76249579]
 [ 2.1908417   0.50871449  1.48363669]]
dL_dW
[[-0.77847203  0.76926514]
 [ 0.04558715  1.73599575]
 [ 0.52706409  1.04068005]]
dL_dB
[-0.39226933  2.01471971]

 

  • 예제 Layer 생성
class Layer():
    def __init__(self):
        self.W = np.random.randn(3, 2)
        self.b = np.random.randn(2)
        self.x = None
        self.dW = None
        self.sb = None

    def forward(self, x):
        self.x = x
        out = np.dot(x, self.W) + self.b
        return out
    
    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

np.random.seed(111)
layer = Layer()

# 순전파
X = np.random.rand(2, 3)
Y = layer.forward(X)

print(X)

# 출력 결과
[[0.23868214 0.33765619 0.99071246]
 [0.23772645 0.08119266 0.66960024]]
 
 
 # 역전파
 dout = np.random.rand(2, 2)
dout_dx = layer.backward(dout)

print(dout_dx)

# 출력 결과
[[-0.57717814  0.8894841  -1.01146255]
 [-0.5434705   0.86783399 -1.09728643]]

 

5. MNIST 분류 with 역전파

  • Module Import
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from collections import OrderedDict
  • 데이터 로드
np.random.seed(42)

mnist = tf.keras.datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()

num_classes = 10
  • 데이터 전처리
X_train, X_test = X_train.reshape(-1, 28 * 28).astype(np.float32), X_test.reshape(-1, 28 * 28).astype(np.float32)

# 색깔 평탄화
X_train /= .255
X_test /= .255

# 원-핫 벡터 변환
y_train = np.eye(num_classes)[y_train]

# 확인
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

# 출력 결과
(60000, 784)
(60000, 10)
(10000, 784)
(10000,)
  • 하이퍼 파라미터
epochs = 1000
learning_rate = 1e-3
batch_size = 100
train_size=  X_train.shape[0]
  • Util Functions
def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis = 0)
        y = np.exp(x) / np.sum(np.exp(x), axis = 0)
        return y.T
    
    # 오버플로우 방지
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def mean_squared_error(pred_y, true_y):
    return 0.5 * np.sum((pred_y, true_y)**2)

def cross_entropy_error(pred_y, true_y):
    if pred_y.ndim == 1:
        true_y = true_y.reshape(1, true_y.size)
        pred_y = pred_y.reshape(1, pred_y.size)

    # train 데이터가 원-핫 벡터 형태면 정답 레이블의 인덱스 반환
    if true_y.size == pred_y.size:
        true_y = true_y.argmax(axis = 1)
    
    batch_size = pred_y.shape[0]
    return -np.sum(np.log(pred_y[np.arange(batch_size), true_y] + 1e-7)) / batch_size

def softmax_loss(X, true_y):
    # softmax의 결과와 원-핫 벡터의 실제 ture_y 값과 비교하여 그것에 대한 차이를 cross_entrpy_error로 return
    pred_y = softmax(X)
    return cross_entropy_error(pred_y, true_y)
  • Util Classes

  - ReLU

class ReLU():
    def __init__(self):
        self.out = None
    
    def forward(self, x):
        self.mask = (x < 0)
        out = x.copy()
        out[x<0] = 0
        return out
    
    def backward(self, dout):
        dout[self.mask] = 0
        dx = dout
        return dx

  - Sigmoid

class Sigmoid():
    def __init__(self):
        self.out = None

    def forward(self, x):
        out = 1 / (1 + np.exp(-x))
        return out
    
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.dout
        return dx

  - Layer

class Layer():
    def __init__(self, W, b):
        self.W = W
        self.b = B

        self.x = None
        self.origin_x_shape = None

        self.dL_dW = None
        self.dL_db = None
    
    def forward(self, x):
        self.origin_x_shape = x.shape

        x = x.reshape(x.shape[0], -1)
        self.x = x
        out = np.dot(self.x, self.W) + self.b

        return out
    
    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dL_dW = np.dot(self.x.T, dout)
        self.dL_db = np.sum(dout, axis = 0)
        dx = dx.reshpae(self.origin_x_shape)
        return dx

  - SoftMax

class Softmax():
    def __init__(self):
        self.loss = None
        self.y = None
        self.x = None
    
    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss
    
    def backward(self, dout = 1):
        batch_size = self.t.shape[0]

        # 정답 레이블이 원-핫 인코딩 형태일 때
        if self.t.size == self.y.size:
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size
        
        return dx

 

  • 모델 생성 및 학습
class MyModel():
    def __init__(self, input_size, hidden_size_list, output_size, activation = 'relu'):
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size_list = hidden_size_list
        self.hidden_layer_num = len(hidden_size_list)
        self.params = {}

        self.__init_weights(activation)

        activation_layer = {'sigmoid': Sigmoid, 'relu': ReLU}
        self.layers = OrderedDict()
        for idx in range(1, self.hidden_layer_num + 1):
            self.layers['Layer' + str(idx)] = Layer(self.params['W' + str(idx)], self.params['b' + str(idx)])
            self.layers['Activation_function' + str(idx)] = activation_layer[activation]()
        
        idx = self.hidden_layer_num + 1

        self.layers['Layer' + str(idx)] = Layer(self.params['W' + str(idx)], self.params['b' + str(idx)])

        self.last_layer = Softmax()

    
    def __init_weights(self, activation):
        weight_std = None
        # 전체 사이즈 리스트
        all_size_list = [self.input_size] + self.hidden_size_list + [self.output_size]
        for idx in range(1, len(all_size_list)):
            if activation.lower() == 'relu':
                weight_std = np.sqrt(2.0 / self.input_size)
            elif activation.lower() =='sigmoid':
                weight_std = np.sqrt(1.0 / self.input_size)
            
            self.params['W' + str(idx)] = weight_std * np.random.randn(all_size_list[idx-1], all_size_list[idx])
            self.params['b' + str(idx)] = np.random.randn(all_size_list[idx])

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return X
    
    def loss(self, x, true_y):
        pred_y = self.predict(x)

        return self.last_layer.forward(pred_y, true_y)
    
    def accuracy(self, x, true_y):
        pred_y = self.predict(x)
        pred_y = np.argmax(pred_y, axis = 1)

        if true_y.ndim != 1:
            true_y = np.argmax(true_y, axis = 1)
        
        accuracy = np.sum(pred_y == true_y) / float(x.shape[0])
        return accuracy
    
    def gradient(self, x, t):
        self.loss(x, t)

        dout = 1
        dout = self.last_layer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)
        
        grads = {}
        for idx in range(1, self.hidden_layer_num + 2):
            grads['W' + str(idx)] = self.layers['Layer' + str(idx)].dL_dW
            grads['b' + str(idx)] = self.layers['Layer' + str(idx)].dL_db
        return grads

model = MyModel(28*28, [100, 64, 32], 10, activation = 'relu')
# 손실값과 정확도를 저장하는 리스트 생성
train_lost_list = []
train_acc_list = []
test_acc_list = []
for epoch in range(epochs):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = X_train[batch_mask]
    y_batch = y_train[batch_mask]

    grad = model.gradient(x_batch, y_batch)

    for key in model.params.keys():
        model.params[key] -= learning_rate * grad[key]
    
    loss = model.loss(x_batch, y_batch)
    train_lost_list.append(loss)

    if epoch % 50 == 0:
        train_acc = model.accuracy(X_train, y_train)
        test_acc = model.accuracy(X_test, y_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("Epoch: {}, Train Accuracy: {:.4f}, Test Accuracy: {:.4f}".format(epoch, train_acc, test_acc))

# 정확도 시각화
plt.plot(np.arange(1000//50), train_acc_list, 'r--', label = 'train_acc')
plt.plot(np.arange(1000//50), test_acc_list, 'b', label = 'test_acc')

plt.title('Result')
plt.xlabel(loc = 5)
plt.grid()
plt.show()

 

# 손실값 시각화
plt.plot(np.arange(1000), train_lost_list, 'green', label = 'train_loss')
plt.title('train loss')
plt.xlabel('Epochs')
plt.legend(loc = 5)
plt.grid()
plt.show()

1. 단순한 신경망 구현: Logic Gate

# 필요 라이브러리
import numpy as np
import matplotlib.pyplot as plt
# 하이퍼 파라미터

# 몇 번 반복
epochs = 1000
lr = 0.1
# 유틸 함수들
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def mean_squared_error(pred_y, true_y):
    return 0.5 * (np.sum((true_y - pred_y)**2))

def cross_entropy_error(pred_y, true_y):
    if true_y.ndim ==1:
        true_y = true_y.reshape(1, -1)
        pred_y = pred_y.reshape(1, -1)

    delta = 1e-7
    return -np.sum(true_y * np.log(pred_y + delta))

# 배치 사이즈로 각 값들을 나눠줘야 함
def cross_entropy_error_for_batch(pred_y, true_y):
    if true_y.ndim ==1:
        true_y = true_y.reshape(1, -1)
        pred_y = pred_y.reshape(1, -1)

    delta = 1e-7
    batch_size = pred_y.shape[0]
    return -np.sum(true_y * np.log(pred_y + delta)) / batch_size

# 이진 분류일때
def cross_entropy_error_for_bin(pred_y, true_y):
    return 0.5 * np.sum((-true_y * np.log(pred_y) - (1 - true_y) * np.log(1 - pred_y)))

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a

    return y

def differential(f, x):
    eps = 1e-5
    diff_value = np.zeros_like(x)

    for i in range(x.shape[0]):
        temp_val = x[i]

        x[i] = temp_val + eps
        f_h1 = f(x)

        x[i] = temp_val - eps
        f_h2 = f(x)

        diff_value[i] = (f_h1 - f_h2) / (2 * eps)
        x[i] = temp_val
    
    return diff_value
# 신경망
class LogicGateNet():

    def __init__(self):
        def weight_init():
            np.random.seed(1)
            weights = np.random.randn(2)
            bias = np.random.rand(1)

            return weights, bias
        
        self.weights, self.bias = weight_init()

    def predict(self, x):
        W = self.weights.reshape(-1, 1)
        b = self.bias

        pred_y = sigmoid(np.dot(x, W) + b)
        return pred_y
    
    def loss(self, x, true_y):
        pred_y = self.predict(x)
        return cross_entropy_error_for_bin(pred_y, true_y)
    
    def get_gradient(self, x, t):
        def loss_grad(grad):
            return self.loss(x, t)
        
        grad_W = differential(loss_grad, self.weights)
        grad_B = differential(loss_grad, self.bias)

        return grad_W, grad_B

 

 

  - AND 게이트

AND = LogicGateNet()

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y = np.array([[0], [0], [0], [1]])

train_loss_list = list()

for i in range(epochs):
    grad_W, grad_B = AND.get_gradient(X, Y)

    AND.weights -= lr * grad_W
    AND.bias -= lr * grad_B

    loss = AND.loss(X, Y)
    train_loss_list.append(loss)

    if i%100 == 99:
        print("Epoch: {}, Cost: {}, Weights: {}, Bias: {}".format(i+1, loss, AND.weights, AND.bias))
        
# 출력 결과
Epoch: 100, Cost: 0.6886489498071491, Weights: [1.56426876 0.79168393], Bias: [-2.14871589]
Epoch: 200, Cost: 0.4946368603064415, Weights: [2.01360719 1.71241131], Bias: [-3.07894028]
Epoch: 300, Cost: 0.3920165980757418, Weights: [2.42841657 2.29753793], Bias: [-3.79103207]
Epoch: 400, Cost: 0.3257214374791936, Weights: [2.794852   2.73235738], Bias: [-4.37257095]
Epoch: 500, Cost: 0.27863601334755067, Weights: [3.11636193 3.08408364], Bias: [-4.86571237]
Epoch: 600, Cost: 0.24328504683831248, Weights: [3.40015395 3.38235762], Bias: [-5.29433736]
Epoch: 700, Cost: 0.21572536552468008, Weights: [3.65300561 3.64264217], Bias: [-5.67349792]
Epoch: 800, Cost: 0.19363244428365756, Weights: [3.88044124 3.87412053], Bias: [-6.01340133]
Epoch: 900, Cost: 0.1755321312790001, Weights: [4.08680123 4.08279091], Bias: [-6.32133891]
Epoch: 1000, Cost: 0.1604392693330146, Weights: [4.27548114 4.27284863], Bias: [-6.6027234]
  • 반복이 진행될 때마다 손실함수인 Cost가 점점 떨저짐
  • Weight값과 Bias값의 조정 과정도 살펴볼 수 있음
# AND 게이트 테스트
print(AND.predict(X))

# 출력 결과
[[0.00135483]
 [0.08867878]
 [0.08889176]
 [0.87496677]]
  • X값에 대해 실제 Y값은 0, 0, 0, 1임
  • AND 게이트 테스트 결과가 각각 1일 확률이고 마지막만 0.87로 높아 1로 분류, 나머지는 0.1 이하로 낮아 0으로 분류됨

 

  - OR 게이트

OR = LogicGateNet()
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y_2 = np.array([[0], [1], [1], [1]])

train_loss_list = list()

for i in range(epochs):
    grad_W, grad_B = OR.get_gradient(X, Y_2)

    OR.weights -= lr * grad_W
    OR.bias -= lr * grad_B

    loss = OR.loss(X, Y_2)
    train_loss_list.append(loss)

    if i%100 == 99:
        print("Epoch: {}, Cost: {}, Weights: {}, Bias: {}".format(i+1, loss, OR.weights, OR.bias))

# 출력 결과
Epoch: 100, Cost: 0.49580923848195635, Weights: [2.45484353 1.40566594], Bias: [-0.14439625]
Epoch: 200, Cost: 0.3398674231515118, Weights: [2.98631846 2.39448393], Bias: [-0.67661178]
Epoch: 300, Cost: 0.2573360986187996, Weights: [3.45016595 3.08431266], Bias: [-1.03721585]
Epoch: 400, Cost: 0.20630142190075948, Weights: [3.85230067 3.60865952], Bias: [-1.30598633]
Epoch: 500, Cost: 0.1716549922113493, Weights: [4.20195872 4.03000824], Bias: [-1.52060015]
Epoch: 600, Cost: 0.1466501884550824, Weights: [4.50867681 4.38171478], Bias: [-1.6994397]
Epoch: 700, Cost: 0.12779768649454676, Weights: [4.78049264 4.68334611], Bias: [-1.8527641]
Epoch: 800, Cost: 0.11310517185413338, Weights: [5.0237707 4.9472786], Bias: [-1.98691756]
Epoch: 900, Cost: 0.10135180918376233, Weights: [5.24347159 5.18181684], Bias: [-2.10611973]
Epoch: 1000, Cost: 0.09174843008614178, Weights: [5.44346811 5.39279833], Bias: [-2.21332947]
# OR 게이트 테스트
print(OR.predict(X))

# 출력 결과
[[0.09855987]
 [0.9600543 ]
 [0.96195283]
 [0.9998201 ]]

 

 

  - NAND 게이트

NAND = LogicGateNet()

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y_3 = np.array([[1], [1], [1], [0]])

train_loss_list = list()

for i in range(epochs):
    grad_W, grad_B = NAND.get_gradient(X, Y_3)

    NAND.weights -= lr * grad_W
    NAND.bias -= lr * grad_B

    loss = NAND.loss(X, Y_3)
    train_loss_list.append(loss)

    if i%100 == 99:
        print("Epoch: {}, Cost: {}, Weights: {}, Bias: {}".format(i+1, loss, NAND.weights, NAND.bias))

# 출력 결과
Epoch: 100, Cost: 0.7911738653769252, Weights: [-0.48972722 -1.25798774], Bias: [1.74566135]
Epoch: 200, Cost: 0.5430490957885361, Weights: [-1.51545093 -1.80261804], Bias: [2.79151756]
Epoch: 300, Cost: 0.4212591302740578, Weights: [-2.14614496 -2.26642639], Bias: [3.56506179]
Epoch: 400, Cost: 0.3456117101527486, Weights: [-2.607325   -2.66303355], Bias: [4.18521187]
Epoch: 500, Cost: 0.2931298605179329, Weights: [-2.97696333 -3.00501941], Bias: [4.70528682]
Epoch: 600, Cost: 0.2543396786002071, Weights: [-3.28850585 -3.30365261], Bias: [5.1539571]
Epoch: 700, Cost: 0.22443918596775067, Weights: [-3.55912171 -3.56778782], Bias: [5.54869527]
Epoch: 800, Cost: 0.20067626330853877, Weights: [-3.7989077  -3.80411461], Bias: [5.90108417]
Epoch: 900, Cost: 0.18134125517637367, Weights: [-4.01441395 -4.01767547], Bias: [6.21926514]
Epoch: 1000, Cost: 0.1653094408173465, Weights: [-4.21019696 -4.21231432], Bias: [6.50920952]
# NAND 게이트 테스트
print(NAND.predict(X))

# 출력 결과
[[0.99851256]
 [0.90861957]
 [0.90879523]
 [0.12861037]]

 

 

  - XOR 게이트

XOR = LogicGateNet()
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y_4 = np.array([[0], [1], [1], [0]])

train_loss_list = list()

for i in range(epochs):
    grad_W, grad_B = XOR.get_gradient(X, Y_4)

    XOR.weights -= lr * grad_W
    XOR.bias -= lr * grad_B

    loss = XOR.loss(X, Y_4)
    train_loss_list.append(loss)

    if i%100 == 99:
        print("Epoch: {}, Cost: {}, Weights: {}, Bias: {}".format(i+1, loss, XOR.weights, XOR.bias))

# 출력 결과
Epoch: 100, Cost: 1.4026852245456056, Weights: [ 0.47012771 -0.19931523], Bias: [-0.16097708]
Epoch: 200, Cost: 1.3879445622848308, Weights: [ 0.1572739  -0.03387161], Bias: [-0.07321056]
Epoch: 300, Cost: 1.386492030048381, Weights: [0.05525161 0.00089673], Bias: [-0.03330094]
Epoch: 400, Cost: 1.3863236205351948, Weights: [0.02049628 0.00504503], Bias: [-0.01514784]
Epoch: 500, Cost: 1.3862994743646844, Weights: [0.0080051  0.00361297], Bias: [-0.00689034]
Epoch: 600, Cost: 1.3862953430687464, Weights: [0.00326661 0.00201812], Bias: [-0.00313421]
Epoch: 700, Cost: 1.3862945581495083, Weights: [0.00137938 0.00102449], Bias: [-0.00142566]
Epoch: 800, Cost: 1.38629440139037, Weights: [0.00059716 0.00049628], Bias: [-0.00064849]
Epoch: 900, Cost: 1.3862943694120307, Weights: [0.00026303 0.00023435], Bias: [-0.00029498]
Epoch: 1000, Cost: 1.386294362832352, Weights: [0.0001172  0.00010905], Bias: [-0.00013418]
  • Cost를 확인해보면 다른 게이트에 비해 높아 잘 학습이 안된 모습
# XOR 게이트 테스트
print(XOR.predict(X))

# 출력 결과
[[0.49996646]
 [0.49999372]
 [0.49999575]
 [0.50002302]]
  • 테스트 결과도 전부 0.5주변에 머물러 학습이 잘 되지 않음을 확인
  • 2층 신경망으로 구현해야함

 

  - 2층 신경망으로 XOR 게이트 구현(1)

  • 얕은 신경망, Shallow Neural Network
  • 두 논리 게이트(NAND, OR)를 통과하고 AND 게이트로 합쳐서 구현
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y_5 = np.array([[0], [1], [1], [0]])

s1 = NAND.predict(X)
s2 = OR.predict(X)
X_2 = np.array([s1, s2]).T.reshape(-1, 2)

print(AND.predict(X_2))

# 출력 결과
[[0.12870357]
 [0.79966936]
 [0.80108545]
 [0.14420781]]
  • 0, 1, 1, 0에 가깝게 나온 결과

 

  - 2층 신경망으로 XOR 게이트 구현(2)

  • 클래스로 구현
class XORNet():
    def __init__(self):
        np.random.seed(1)

        def weight_init():
            params = {}
            params['w_1'] = np.random.randn(2)
            params['b_1'] = np.random.rand(2)
            params['w_2'] = np.random.randn(2)
            params['b_2'] = np.random.rand(2)
            return params
        
        self.params = weight_init()

    def predict(self, x):
        W_1, W_2 = self.params['w_1'].reshape(-1, 1), self.params['w_2'].reshape(-1, 1)
        B_1, B_2 = self.params['b_1'], self.params['b_2']

        A1 = np.dot(x, W_1) + B_1
        Z1 = sigmoid(A1)
        A2 = np.dot(Z1, W_2) + B_2
        pred_y = sigmoid(A2)

        return pred_y
    
    def loss(self, x, true_y):
        pred_y = self.predict(x)
        return cross_entropy_error_for_bin(pred_y, true_y)
    
    def get_gradient(self, x, t):
        def loss_grad(grad):
            return self.loss(x, t)
        
        grads = {}
        grads['w_1'] = differential(loss_grad, self.params['w_1'])
        grads['b_1'] = differential(loss_grad, self.params['b_1'])
        grads['w_2'] = differential(loss_grad, self.params['w_2'])
        grads['b_2'] = differential(loss_grad, self.params['b_2'])

        return grads

# 하이퍼 파라미터 재조정
lr = 0.3
# 모델 생성 및 학습
XOR = XORNet()
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
Y_5 = np.array([[0], [1], [1], [0]])

train_loss_list = list()

for i in range(epochs):
    grads = XOR.get_gradient(X, Y_5)

    for key in ('w_1', 'b_1', 'w_2', 'b_2'):
        XOR.params[key] -= lr * grads[key]
    
    loss = XOR.loss(X, Y_5)
    train_loss_list.append(loss)

    if i % 100 == 99:
        print("Epoch: {}, Cost: {}".format(i+1, loss))

# 출력 결과
Epoch: 100, Cost: 2.583421249699167
Epoch: 200, Cost: 0.6522444536804384
Epoch: 300, Cost: 0.2505164706195344
Epoch: 400, Cost: 0.14964904919118582
Epoch: 500, Cost: 0.10570445867337958
Epoch: 600, Cost: 0.0814030439804046
Epoch: 700, Cost: 0.06606149912973946
Epoch: 800, Cost: 0.05552519160632019
Epoch: 900, Cost: 0.04785478827730652
Epoch: 1000, Cost: 0.042027122417916646
# XOR 게이트 테스트
print(XOR.predict(X))

# 출력 결과
[[0.00846377]
 [0.98354369]
 [0.99163498]
 [0.0084976]]

 

 

2. 다중 클래스 분류: MNIST Dataset

  • 배치 처리
    • 학습 데이터 전체를 한번에 진행하지 않고,
      일부 데이터(샘플)을 확률적으로 구해서 조금씩 나누어 진행
    • 확률적 경사 하강법(Stochastic Gradient Descent) 또는 미니 배치 학습법(mini-batch learning)이라고 부름

 

# 필요한 라이브러리
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import time
from tqdm.notebook import tqdm

# mnist 데이터
mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

print(x_train.shape)
print(y_train.shape)
print(x_test.shape)
print(y_test.shape)

# 출력 결과
(60000, 28, 28)
(60000,)
(10000, 28, 28)
(10000,)
# 데이터 확인
# x_trian의 첫번째 데이터
img = x_train[0]
print(img.shape)  # (28, 28)
plt.imshow(img, cmap = 'gray')
plt.show()

# y_train의 첫번째 데이터
y_train[0]   # 5

 

  • 데이터 전처리
# 평탄화 함수
def flatten_for_mnist(x):
    temp = np.zeros((x.shape[0], x[0].size))

    for idx, data in enumerate(x):
        temp[idx, :] = data.flatten()

    return temp

# 정규화(색 표현 정규화)
x_train, x_test = x_train / 255.0, x_test / 255.0

# 평탄화
x_train = flatten_for_mnist(x_train)
x_test = flatten_for_mnist(x_test)

print(x_train.shape)
print(x_test.shape)

y_train_ohe = tf.one_hot(y_train, depth = 10).numpy()
y_test_ohe = tf.one_hot(y_test, depth = 10).numpy()

print(y_train_ohe.shape)
print(y_test_ohe.shape)
print(x_train[0].max(), x_test[0].min())
print(y_train_ohe[0])

# 출력 결과
0.00392156862745098 0.0
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

# 전처리 과정을 통해 전체적으로 값이 스케일링됨을 확인

 

  • 하이퍼 파라미터
# 하이퍼 파라미터
epochs = 2
lr = 0.1
batch_size = 100
train_size = x_train.shape[0]

 

  • 사용되는 함수들
# 사용되는 함수들
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def mean_squared_error(pred_y, true_y):
    return 0.5 * (np.sum((true_y - pred_y)**2))

def cross_entropy_error(pred_y, true_y):
    if true_y.ndim ==1:
        true_y = true_y.reshape(1, -1)
        pred_y = pred_y.reshape(1, -1)

    delta = 1e-7
    return -np.sum(true_y * np.log(pred_y + delta))

# 배치 사이즈로 각 값들을 나눠줘야 함
def cross_entropy_error_for_batch(pred_y, true_y):
    if true_y.ndim ==1:
        true_y = true_y.reshape(1, -1)
        pred_y = pred_y.reshape(1, -1)

    delta = 1e-7
    batch_size = pred_y.shape[0]
    return -np.sum(true_y * np.log(pred_y + delta)) / batch_size

# 이진 분류일때
def cross_entropy_error_for_bin(pred_y, true_y):
    return 0.5 * np.sum((-true_y * np.log(pred_y) - (1 - true_y) * np.log(1 - pred_y)))

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a

    return y

def differential_1d(f, x):
    eps = 1e-5
    diff_value = np.zeros_like(x)

    for i in range(x.shape[0]):
        temp_val = x[i]

        x[i] = temp_val + eps
        f_h1 = f(x)

        x[i] = temp_val - eps
        f_h2 = f(x)

        diff_value[i] = (f_h1 - f_h2) / (2 * eps)
        x[i] = temp_val
    
    return diff_value

def differential_2d(f, X):
    if X.ndim == 1:
        return differential_1d(f, x)
    else:
        grad = np.zeros_like(X)

        for idx,  x in enumerate(X):
            grad[idx] = differential_1d(f, x)

        return grad

 

  • 다중분류 클래스 구현
class MyModel():
    def __init__(self):
        np.random.seed(1)

        def weight_init(input_nodes, hidden_nodes, output_units):
            np.random.seed(777)

            params = {}
            params['w_1'] = 0.01 * np.random.randn(input_nodes, hidden_nodes)
            params['b_1'] = np.zeros(hidden_nodes)
            params['w_2'] = 0.01 * np.random.randn(hidden_nodes, output_units)
            params['b_2'] = np.zeros(output_units)
            return params
        
        # 784는 x_train.shape[1], hidden은 임의의 64, output은 0~9까지의 숫자로 10개가 있으므로 10으로 지정
        self.params = weight_init(784, 64, 10)

    def predict(self, x):
        W_1, W_2 = self.params['w_1'], self.params['w_2']
        B_1, B_2 = self.params['b_1'], self.params['b_2']

        A1 = np.dot(x, W_1) + B_1
        Z1 = sigmoid(A1)
        A2 = np.dot(Z1, W_2) + B_2
        pred_y = softmax(A2)

        return pred_y
    
    def loss(self, x, true_y):
        pred_y = self.predict(x)
        return cross_entropy_error_for_bin(pred_y, true_y)
    
    def accuracy(self, x, true_y):
        pred_y = self.predict(x)
        y_argmax = np.argmax(pred_y, axis = 1)
        t_argmax = np.argmax(true_y, axis = 1)
        # 예측값과 실제값이 같은 값들의 합을 전체 수로 나눠 몇개 맞췄는지 비율 계산
        accuracy = np.sum(y_argmax == t_argmax) / float(x.shape[0])

        return accuracy
    
    def get_gradient(self, x, t):
        def loss_grad(grad):
            return self.loss(x, t)
        
        grads = {}
        grads['w_1'] = differential_2d(loss_grad, self.params['w_1'])
        grads['b_1'] = differential_2d(loss_grad, self.params['b_1'])
        grads['w_2'] = differential_2d(loss_grad, self.params['w_2'])
        grads['b_2'] = differential_2d(loss_grad, self.params['b_2'])

        return grads
# 모델 학습
model = MyModel()
train_loss_list = list()
train_acc_list = list()
test_acc_list = list()
iter_per_epoch = max(train_size / batch_size, 1)

start_time = time.time()
for i in tqdm(range(epochs)):

    batch_idx = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_idx]
    y_batch = y_train_ohe[batch_idx]

    grads = model.get_gradient(x_batch, y_batch)
    
    for key in grads.keys():
        model.params[key] -= lr * grads[key]
    
    loss = model.loss(x_batch, y_batch)
    train_loss_list.append(loss)

    train_accuracy = model.accuracy(x_train, y_train_ohe)
    test_accuracy = model.accuracy(x_test, y_test_ohe)
    train_acc_list.appned(train_accuracy)
    test_acc_list.append(test_accuracy)

    print("Epoch: {}, Cost: {}, Train Accuracy: {}, Test Accuracy: {}".format(i+1, loss, train_accuracy, test_accuracy))

end_time = time.time()

print("총 학습 소요시간: {:.3f}s".format(end_time - start_time))
  • 출력 결과

  • 학습이 잘 되지 않은 모습

 

3. 모델의 결과

  • 모델은 학습이 잘 될수도, 잘 안될 수도 있음
  • 만약 학습이 잘 안된다면,
    학습이 잘 되기 위해 어떤 조치를 취해야 하는가?
    • 다양한 학습 관련 기술이 존재

1. 볼록함수(Convex Function)

  • 어떤 지점에서 시작하더라도 최적값(손실함수가 최소로 하는 점)에 도달할 수 있음
  • 1-D Convex Function(1차원 볼록함수)

https://www.researchgate.net/figure/A-strictly-convex-function_fig5_313821095

  • 2-D Convex Function(2차원 볼록함수)

https://www.researchgate.net/figure/Sphere-function-D-2_fig8_275069197

 

2. 비볼록함수(Non-Convex Function)

  • 비볼록함수는 시작점의 위치에 따라 다른 최적값에 도달할 수 있음
  • 1-D Non-Convex Function

https://www.slideserve.com/betha/local-and-global-optima

  • 2-D Non-Convex Function

https://commons.wikimedia.org/wiki/File:Non-Convex_Objective_Function.gif

 

3. 미분과 기울기

  • 스칼라를 벡터로 미분한 것
    \(\frac{df(x)}{dx}=\underset{\triangle x \to 0}{lim}\frac{f(x+\triangle x)-f(x)}{\triangle x}\)

https://ko.wikipedia.org/wiki/%EA%B8%B0%EC%9A%B8%EA%B8%B0_(%EB%B2%A1%ED%84%B0)

  • \(\triangledown f(x)=\left ( \frac{\partial f}{\partial x_{1}}, \frac{\partial f}{\partial x_{2}}, \cdots, \frac{\partial f}{\partial x_{N}} \right )\)
  • 변화가 있는 지점에서는 미분값이 존재하고, 변화가 없는 지점은 미분값이 0
  • 미분값이 클수록 변화량이 크다는 의미

 

4. 경사하강법의 과정

  • 경사하강법은 한 스텝마다의 미분값에 따라 이동하는 방향을 결정
  • \(f(x)\)의 값이 변하지 않을 때까지 반복

$$ x_{n}=x_{n-1}-\eta \frac{\partial f}{\partial x} $$

  • \(\eta\): 학습률(learning rate)
  • 즉, 미분값이 0인 지점을 찾는 방법

https://www.kdnuggets.com/2018/06/intuitive-introduction-gradient-descent.html

  - 2-D 경사하강법

https://gfycat.com/ko/angryinconsequentialdiplodocus

 

5. 경사하강법 구현

  • \(f_{1}(x)=x^{2}
# 손실함수
def f1(x):
    return x**2

# 손실함수를 미분한 식
def df_dx1(x):
    return 2*x 

# 경사하강법 구현
def gradient_descent(f, df_dx, init_x, learning_rate = 0.01, step_num = 100):
    x = init_x
    x_log, y_log = [x], [f(x)]
    for i in range(step_num):
        grad = df_dx(x)
        # 학습률에 미분한 기울기를 곱한 값만큼 x값을 변화시켜 최적값에 도달
        x -= learning_rate * grad

        x_log.append(x)
        y_log.append(f(x))

    return x_log, y_log
# 시각화
import matplotlib.pyplot as plt
import numpy as np

x_init = 5
x_log, y_log = gradient_descent(f1, df_dx1, init_x = x_init)
plt.scatter(x_log, y_log, color = 'red')

x = np.arange(-5, 5, 0.01)
plt.plot(x, f1(x))
plt.grid()
plt.show()

 

  - 비볼록함수에서의 경사하강법

# 손실함수
def f2(x):
    return 0.01*x**4 - 0.3*x**3 - 1.0*x + 10.0

# 손실함수를 미분한 식
def df_dx2(x):
    return 0.04*x**3 - 0.9*x**2 - 1.0

# 시각화
x_init = 2
x_log, y_log = gradient_descent(f2, df_dx2, init_x = x_init)
plt.scatter(x_log, y_log, color = 'red')

x = np.arange(-5, 30, 0.01)
plt.plot(x, f2(x))
plt.xlim(-5, 30)
plt.grid()
plt.show()

 

6. 전역최적값 vs 지역최적값

  • 초기값이 어디냐에 따라 전체 함수의 최솟값이 될 수도 있고, 지역적으로 최솟값일 수도 있음

https://www.kdnuggets.com/2017/06/deep-learning-local-minimum.html

  • \(f_{3}(x)=xsin(x^{2})+1\) 그래프
# 손실함수
def f3(x):
    return x*np.sin(x**2) + 1
    
# 손실함수를 미분한 식
def df_dx3(x):
    return np.sin(x**2) + x*np.cos(x**2)*2*x
    
# 시각화
x_init1 = -0.5
x_log1, y_log1 = gradient_descent(f3, df_dx3, init_x = x_init1)
plt.scatter(x_log1, y_log1, color = 'red')

x_init1 = -0.5
x_log1, y_log1 = gradient_descent(f3, df_dx3, init_x = x_init1)
plt.scatter(x_log1, y_log1, color = 'red')

x_init2 = 1.5
x_log2, y_log2 = gradient_descent(f3, df_dx3, init_x = x_init2)
plt.scatter(x_log2, y_log2, color = 'blue')

x = np.arange(-3, 3, 0.01)
plt.plot(x, f3(x), '--')

plt.scatter(x_init1, f3(x_init1), color = 'red')
plt.text(x_init1 - 1.0, f3(x_init1) + 0.3, "x_init1 ({})".format(x_init1), fontdict = {'size': 13})
plt.scatter(x_init2, f3(x_init2), color = 'blue')
plt.text(x_init2 - 0.7, f3(x_init2) + 0.4, "x_init2 ({})".format(x_init2), fontdict = {'size': 13})

plt.grid()
plt.show()

  • 위의 그래프에서
    • '전역최적값'은 x가 -3보다 조금 큰 부분
    • x가 -0.5에서 시작했을 때 x가 찾아간 '지역최적값'은 -1보다 조금 작은 부분
    • x가 1.5에서 시작했을 때 x가 찾아간 '지역최적값'은 2보다 조금  부분

 

7. 경사하강법 구현(2)

  • 경사하강을 진행하는 도중, 최솟값에 이르면 경사하강법을 종료하는 코드
def gradient_descent2(f ,df_dx, init_x, learning_rate = 0.01, step_num = 100):
    eps = 1e-5
    count = 0

    old_x = init_x
    min_x = old_x
    min_y = f(min_x)

    x_log, y_log = [min_x], [min_y]
    for i in range(step_num):
        grad = df_dx(old_x)
        new_x = old_x - learning_rate * grad
        new_y = f(new_x)
        
        if min_y > new_y:
            min_x = new_x
            min_y = new_y

        if np.abs(old_x - new_x) < eps:
            break

        x_log.append(old_x)
        y_log.append(new_y)

        old_x = new_x
        count += 1

    return x_log, y_log, count
  • \(f_{3}(x)=xsin(x^{2})+1\) 그래프
# 시각화
x_init1 = -2.2
x_log1, y_log1, count1 = gradient_descent2(f3, df_dx3, init_x = x_init1)
plt.scatter(x_log1, y_log1, color = 'red')
print("count:", count1)

x_init2 = -0.5
x_log2, y_log2, count2 = gradient_descent2(f3, df_dx3, init_x = x_init2)
plt.scatter(x_log2, y_log2, color = 'blue')
print("count:", count2)

x_init3 = 1.5
x_log3, y_log3, count3 = gradient_descent2(f3, df_dx3, init_x = x_init3)
plt.scatter(x_log3, y_log3, color = 'green')
print("count:", count3)

x = np.arange(-3, 3, 0.01)
plt.plot(x, f3(x), '--')

plt.scatter(x_init1, f3(x_init1), color = 'red')
plt.text(x_init1 + 0.2, f3(x_init1) + 0.2, "x_init1 ({})".format(x_init1), fontdict = {'size': 13})
plt.scatter(x_init2, f3(x_init2), color = 'blue')
plt.text(x_init2 + 0.1, f3(x_init2) - 0.3, "x_init2 ({})".format(x_init2), fontdict = {'size': 13})
plt.scatter(x_init3, f3(x_init3), color = 'green')
plt.text(x_init3 - 1.0, f3(x_init3) + 0.3, "x_init3 ({})".format(x_init3), fontdict = {'size': 13})

plt.grid()
plt.show()

# 출력 결과
count: 17
count: 100
count: 28

 

8. 학습률(Learning Rate)

  • 학습률 값은 적절히 지정해야 함
  • 너무 크면 발산, 너무작으면 학습이 잘 되지 않음

https://mc.ai/an-introduction-to-gradient-descent-algorithm/

# learning rate가 굉장히 큰 경우(learning_rate = 1.05)
x_init = 10
x_log, y_log, _ = gradient_descent2(f1, df_dx1, init_x = x_init, learning_rate = 1.05)
plt.plot(x_log, y_log, color = 'red')

plt.scatter(x_init, f1(x_init), color = 'green')
plt.text(x_init - 2.2, f1(x_init) - 2, "x_init ({})".format(x_init), fontdict = {'size': 10})
x = np.arange(-50, 30, 0.01)
plt.plot(x, f1(x), '--')
plt.grid()
plt.show()

 

  - 학습률별 경사하강법

lr_list = [0.001, 0.01, 0.1, 1.01]

init_x = 30.0
x = np.arange(-30, 50, 0.01)
fig = plt.figure(figsize = (12,10))

for i, lr in enumerate(lr_list):
    x_log, y_log, count = gradient_descent2(f1, df_dx1, init_x = x_init, learning_rate = lr)
    ax = fig.add_subplot(2, 2, i+1)
    ax.scatter(init_x, f1(init_x), color = 'green')
    ax.plot(x_log, y_log, color = 'red', linewidth = '4')
    ax.plot(x, f1(x), '--')
    ax.grid()
    ax.title.set_text('learning_rate = {}'.format(str(lr)))
    print("init value = {}, count = {}".format(str(lr), str(count)))

plt.show()

 

9. 안장점(Saddle Point)

  • 기울기가 0이지만 극값이 되지 않음
  • 경사하강법은 안장점에서 벗어나지 못함

https://www.pngegg.com/en/png-czdxs

  • \(f_{2}(x)=0.01x^{4}-0.3x^{3}-1.0x+10.0\) 그래프로 확인하기
  • 첫번째 시작점
    • count가 100, 즉 step_num(반복횟수)만큼 루프를 돌았음에도 손실함수의 값이 10 언저리에서 멈춤, 변화 x
    • 이는 학습률 조절 또는 다른 초기값 설정을 통해 수정해야 함
x_init1 = -10.0
x_log1, y_log1, count1 = gradient_descent2(f2, df_dx2, init_x = x_init1)
plt.scatter(x_log1, y_log1, color = 'red')
print("count:", count1)

x_init2 = 5.0
x_log2, y_log2, count2 = gradient_descent2(f2, df_dx2, init_x = x_init2)
plt.scatter(x_log2, y_log2, color = 'blue')
print("count:", count2)

x_init3 = 33.0
x_log3, y_log3, count3 = gradient_descent2(f2, df_dx2, init_x = x_init3)
plt.scatter(x_log3, y_log3, color = 'green')
print("count:", count3)

x = np.arange(-15, 35, 0.01)
plt.plot(x, f2(x), '--')

plt.scatter(x_init1, f2(x_init1), color = 'red')
plt.text(x_init1 + 2, f2(x_init1), "x_init1 ({})".format(x_init1), fontdict = {'size': 13})
plt.scatter(x_init2, f2(x_init2), color = 'blue')
plt.text(x_init2 + 2, f2(x_init2) + 53, "x_init2 ({})".format(x_init2), fontdict = {'size': 13})
plt.scatter(x_init3, f2(x_init3), color = 'green')
plt.text(x_init3 - 18, f2(x_init3), "x_init3 ({})".format(x_init3), fontdict = {'size': 13})

plt.grid()
plt.show()

# 출력 결과
count: 100
count: 82
count: 50

-10.0에서 시작한 빨간점은 안장점에 빠짐

 

  • \(f_{3}(x)=xsin(x^{2})+1\) 그래프로 확인하기
x_init1 = -2.2
x_log1, y_log1, count1 = gradient_descent2(f3, df_dx3, init_x = x_init1)
plt.scatter(x_log1, y_log1, color = 'red')
print("count:", count1)

x_init2 = 1.2
x_log2, y_log2, count2 = gradient_descent2(f3, df_dx3, init_x = x_init2)
plt.scatter(x_log2, y_log2, color = 'blue')
print("count:", count2)

x = np.arange(-3, 3, 0.01)
plt.plot(x, f3(x), '--')

plt.scatter(x_init1, f3(x_init1), color = 'red')
plt.text(x_init1 + 0.2, f3(x_init1) + 0.2, "x_init1 ({})".format(x_init1), fontdict = {'size': 13})
plt.scatter(x_init2, f3(x_init2), color = 'blue')
plt.text(x_init2 - 1.0, f3(x_init2) + 0.3, "x_init2 ({})".format(x_init2), fontdict = {'size': 13})

plt.grid()
plt.show()

# 출력 결과
count: 17
count: 100

1.2에서 시작한 파란점은 안장점에 빠짐

1. 지도 학습 vs 비지도 학습

  • 지도 학습(Supervised Learning)
    • 입력에 대한 정답(Label, Ground Truth)이 존재
    • [입력-정답] 관계를 학습하여 새로운 입력에 대해 정답을 맞추는 과정
  • 비지도 학습(Unsupervised Learning)
    • 정답이 없음
    • 데이터로부터 어떤 알고리즘을 통해 유용한 정보를 추출

 

2. 학습 매개변수(Trainable Parameter)

  • 학습 매개변수: 학습 시, 값이 변화하는 매개변수, 이 매개변수에 따라 학습 알고리즘(모델)이 변함
  • 학습 모델: 입력에 따른 출력을 나타내는 수식
    선형회귀를 예로 들면,
    $$ Y = aX + b $$
    • \(X\): 입력
    • \(Y\): 출력
    • \(a, b\): 학습 매개변수
  • 초기화된 모델로부터 학습이 진행되면서 학습 데이터에 맞는 모델로 학습 파라미터를 수정해 나가는 과정

https://learningstatisticswithr.com/book/regression.html

 

3. 하이퍼 파라미터(Hyper Parameter)

  • 사람이 직접 설정해야하는 매개변수
  • 학습이 되기 전 미리 설정되어 상수취급
    • 손실함수(Cost Function)
    • 학습률(Learnign Rate)
    • 학습 반복 횟수(Epochs)
    • 미니 배치 크기(Batch Size)
    • 은닉층의 노드 개수(Units)
    • 노이즈(Noise)
    • 규제화(Regularization)
    • 가중치 초기화(Weights Initialization)
  • 신경망의 매개변수인 가중치는 학습 알고리에 의해 자동으로 갱

 

4. 손실함수(Loss Function, Cost Function)

  • 학습이 진행되면서 해당 과정이 얼마 잘 되고 있는지 나타내는 지표
  • 손실 함수에 따른 결를 통해 학습 파라미를 조정
  • 최적화 이론에서 최소화하고자 하는 함수
  • 미분 가능한 함수 사용

https://zhuanlan.zhihu.com/p/85540935\

  - 학습의 수학적 의미

https://www.internalpointers.com/post/cost-function-logistic-regression

$$ \widetilde{\theta}=\underset{\theta}{argmin}L(x, y; \theta) $$

  • \(L\): 손실함수
  • \(x\): 학습에 사용되는 데이터의 입력값
  • \(y\): 학습에 사용되는 데이터의 출력값
  • \(\theta\): 학습될 모든 파라미를 모은 벡터
  • \(\widetilde{\theta}\): 추정된 최적의 파라미터
  • 학습에 사용되는 파라미터를 모두 통칭해서 \(\theta\)로 표현 가능, 이러한 \(\theta\)의 최적값을 찾는 것이 학습
  • 학습 데이터의 입력(\(x\))와 \(\theta\)에 따라 나온 예측값이 정답(\y\)와 비교하여 \(\theta\)를 조절해나가는 과정

https://medium.com/@dhartidhami/machine-learning-basics-model-cost-function-and-gradient-descent-79b69ff28091

  • 즉, 최적의 \(theta\)값에 따라 손실함수의 가정 최저점(최소값)을 찾는 과정
  • 손실함수는 지도학습 알고리즘에 반드시 필요

 

5. 원-핫 인코딩

  • 범주형 변수를 표현할 때 사용
  • 가변수(Dummy Variable)이라고도 함
  • 정답인 레이블을 제외하고 0으로 처리

https://medium.com/@michaeldelsole/what-is-one-hot-encoding-and-how-to-do-it-f0ae272f1179

import numpy as np

def convert_one_hot(labels, num_classes):
    one_hot_result = np.zeros((len(labels), num_classes))
    for idx, label in enumerate(labels):
        one_hot_result[idx][label] = 1

    return one_hot_result

x_label = [1, 3, 3, 4, 2, 0, 5, 3, 0]
print(convert_one_hot(x_label, max(x_label) + 1))

# 출력 결과
# 맨 앞에 0부터 시작
[[0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 1. 0. 0.]
 [1. 0. 0. 0. 0. 0.]]
# Keras에서 원-핫 인코딩 하는 법
from keras.utils.np_utils import to_categorical

x_label = [1, 3, 3, 4, 2, 0, 5, 3, 0]
one_hot_label = to_categorical(x_label)
print(one_hot_label)

# 출력 결과
[[0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 1. 0. 0.]
 [1. 0. 0. 0. 0. 0.]]
# sklearn에서 원-핫 인코딩
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder

def convert_one_hot_sklearn(class_label):
    encoder = LabelEncoder()
    encoder.fit(class_label)
    labels = encoder.transform(class_label)

    labels = labels.reshape(-1, 1)

    oh_encoder = OneHotEncoder()
    oh_encoder.fit(labels)
    oh_labels = oh_encoder.transform(labels)

    return oh_labels.toarray()

# 예시 데이터 생성
marvel_labels = ['아이언맨', '캡틴 아메리카', '헐크', '블랙 위도우', '스파이더맨', '앤트맨']
ohe = convert_one_hot_sklearn(marvel_labels)
print(ohe)
print("One hot encoder datatype:", type(ohe))
print("One hot encoder shape:", ohe.shape)
print("-----------------------------")

classes = [3, 2, 1, 3, 0, 4, 5, 3, 0]
ohe = convert_one_hot_sklearn(classes)
print(ohe)
print("One hot encoder datatype:", type(ohe))
print("One hot encoder shape:", ohe.shape)

# 출력 결과
[[0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 1.]
 [1. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]]
One hot encoder datatype: <class 'numpy.ndarray'>
One hot encoder shape: (6, 6)
-----------------------------
[[0. 0. 0. 1. 0. 0.]
 [0. 0. 1. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0.]
 [1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 1. 0. 0.]
 [1. 0. 0. 0. 0. 0.]]
One hot encoder datatype: <class 'numpy.ndarray'>
One hot encoder shape: (9, 6)
# pandas에서 원-핫 인코딩
import pandas as pd

df = pd.DataFrame({'labels': ['아이언맨', '캡틴 아메리카', '헐크', '블랙 위도우', '스파이더맨', '앤트맨']})
ohe_df = pd.get_dummies(df['labels'])
ohe_df

 

6. 평균절대오차(Mean Absolute Error, MAE)

  • 오차가 커져도 손실함수가 일정하게 증가
  • 이상치(Outlier)에 강건함(Robust)
    • 데이터에서 [입력-정답]관계가 적절하지 않은 것이 있을 경우에, 좋은 추정을 하더라도 오차가 발생하는 경우가 발생
    • 그때, 해당 이상치에 해당하는 지점에서 손실 함수의 최소값으로 가는 정도의 영향력이 크지 않음
  • 중간값(Median)과 연관
  • 회귀(Regression)에 많이 사용

https://heartbeat.fritz.ai/5-regression-loss-functions-all-machine-learners-should-know-4fb140e9d4b0

$$ E=\frac{1} {n} \sum_{i=1}^{n} \left |y_{i}-\widetilde{y_{i}} \right | $$

  • \(y_{i}\): 학습 데이터의 \(i\)번째 정답
  • \(\widetilde{y_{i}}\): 학습데이터의 입력으로 추정한 \(i\)번째 출력
def MAE(y, pred_y):
    return np.mean(np.abs((y - pred_y)))
    
import matplotlib.pyplot as plt

y = np.array([-3, -1, -2, 1, 3, -2, 2, 5, 3, 3, -2, -1, 2])
yhat = np.array([-3, -1, -5, 0, 3, -1, 2, 4, 3, 3, -2, -1, -1])
x = list(range(len(y)))

plt.scatter(x, y, color = 'b', label = 'True')
plt.plot(x, yhat, color = 'r', label = 'Pred')
plt.legend()
plt.grid()
plt.show()

# 위 그래프와 같은 y와 y의 예측값을 가질 때 MAE 계산
print(MAE(y, yhat))

# 출력 결과
0.6923076923076923

 

7. 평균제곱오차(Mean Squared Error, MSE)

  • 가장 많이 쓰이는 손실 함수 중 하나
  • 오차가 커질수록 손실함수가 빠르게 증가
    • 정답과 예측한 값의 차이가 클수록 더 많은 패널티를 부여
  • 회귀(Regression)에 쓰임

https://heartbeat.fritz.ai/5-regression-loss-functions-all-machine-learners-should-know-4fb140e9d4b0

$$ E=\frac{1} {n} \sum_{i=1}^{n} (y_{i}-\widetilde{y_{i}})^{2} $$

  • \(y_{i}\): 학습 데이터의 \(i\)번째 정답
  • \(\widetilde{y_{i}}\): 학습데이터의 입력으로 추정한 \(i\)번째 출력
def MSE(y, pred_y):
    return 0.5 * np.sum(np.square(y - pred_y))

print(MSE(y, yhat))

# 출력 결과
10.5

 

  - 손실함수로써의 MAE와 MSE 비교

https://heartbeat.fritz.ai/5-regression-loss-functions-all-machine-learners-should-know-4fb140e9d4b0

 

8. 교차 엔트로피 오차(Cross Entropy Error, CEE)

  • 이진 분류, 다중 클래스 분류
  • 소프트맥스와 원-핫 인코딩 사이의 출력 간 거리를 비교
  • 정답인 클래스에 대해서만 오차를 계산
    • 정답을 맞추면 오차가 0, 틀리면 그 차이가 클수록 오차가 무한히 커짐

$$ E=-\frac{1}{N} \sum_{n} \sum_{i} y_{i} log\widetilde{y_{i}} $$

  • \(y_{i}\): 학습 데이터의 \(i\)번째 정답
  • \(\widetilde{y_{i}}\): 학습데이터의 입력으로 추정한 \(i\)번째 출력
  • \(N\): 전체 데이터의 개수
  • \(i\): 데이터 하나당 클래스 개수
  • \(y=log(x)\)는
    • \(x\)가 0에 가까울수록 \(y\)값은 무한히 커
    • 1에 가까울수록 0에 가까워
  • 정답 레이블(\(y_{i}\))은 원-핫 인코딩으로 정답인 인덱스에만 1이고, 나머지는 모두 0
  • 따라서, 위 수식은 다음과 같이 나타낼 수 있음

$$ E=-log\widetilde{y_{i}} $$

  • 소프트맥스를 통해 나온 신경망 출력이 0.6이라면 \(log0.6 \approx−0.51\)이 되고, 신경망 출력이 0.3이라면 \(−log0.3 \approx −1.2\)이 됨
  • 정답에 가까워질수록 오차값은 작아짐
  • 학습시, 원-핫 인코딩에 의해 정답 인덱스만 살아 남아 비교하지만, 정답이 아닌 인덱스들도 학습에 영향을 미침
    다중 클래스 분류는 소프트맥스(softmax) 함수를 통해 전체 항들을 모두 다루기 때문

https://towardsdatascience.com/understanding-binary-cross-entropy-log-loss-a-visual-explanation-a3ac6025181a

def CEE(y_pred, y_true):
    delta = 1e-7
    return -np.sum(y_true * np.log(y_pred + delta))

y = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0])
yhat = np.array([0.01, 0.1, 0.05, 0.0, 0.1, 0.7, 0.0, 0.03, 0.01, 0.0])
print("yhat 합:", np.sum(yhat))
print(CEE(yhat, y))
print("-----------------")

# yhat의 결과에서 0.03과 0.7 값의 위치를 바꿔 오차를 더 크게 만듦
y = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0 ,0])
yhat = np.array([0.01, 0.1, 0.05, 0.0, 0.1, 0.03, 0.0, 0.7, 0.01, 0.0])
print("yhat 합:", np.sum(yhat))
print(CEE(yhat, y))

# 출력 결과
yhat 합: 1.0
0.3566748010815999
-----------------
yhat 합: 1.1
3.506554563992204

 

  - 이진 분류에서의 교차 크로스 엔트로피(Binary Cross Entropy, BCE)

  • 이진 분류 문제(Binary Classification Problem)에서도 크로스 엔트로피 오차를 손실함수로 사용 가능

$$ E = -\sum_{i=1}^{2} y_{i}log\widetilde{y_{i}} \\ \quad\qquad\qquad\qquad\qquad=-y_{1}log\widetilde{y}_{1}-(1-y_{1})log(1-\widetilde{y}_{1}) \\ \quad(\because y_{2}=1-y_{1}) $$

  • \(y_{i}\): 학습 데이터의 \(i\)번째 정답
  • \(\widetilde{y_{i}}\): 학습데이터의 입력으로 추정한 \(i\)번째 출력
  • 2개의 클래스를 분류하는 문제에서 1번이 정답일 확률이 0.8이고, 실제로 정답이 맞다면 위 식은 다음과 같이 나타낼 수 있음

$$ E = -\sum_{i=1}^{2} y_{i}log\widetilde{y_{i}} \\ \quad\qquad\qquad\qquad\qquad=-1log0.8-(1-1)log(1-0.8) \\ =-log0.8 \\ \approx -0.22 $$

  • 반대로 실제 정답이 2번이었다면, 식은 다음과 같이 나타낼 수 있음

$$ E = -\sum_{i=1}^{2} y_{i}log\widetilde{y_{i}} \\ \quad\qquad\qquad\qquad\qquad=-0log0.8-(1-0)log(1-0.8) \\=-log0.2 \\\approx -1.61 $$

# 2번이 정답
# 2번이 정답일 확률을 0.85로 예측, 맞음
y = np.array([0, 1])
yhat = np.array([0.15, 0.85])
print("yhat 합:", np.sum(yhat))
print(CEE(yhat, y))
print("-----------------")

# 1번이 정답
# 1번이 정답일 확률을 0.15로 예측, 틀림
y = np.array([1, 0])
yhat = np.array([0.15, 0.85])
print("yhat 합:", np.sum(yhat))
print(CEE(yhat, y))

# 출력 결과
yhat 합: 1.0
0.1625188118507231
-----------------
yhat 합: 1.0
1.8971193182194368

1. 퍼셉트론

  • 인공신경망의 한 종류
  • 다수의 입력(\(x_{1}, x_{2}, x_{3}, \cdots, x_{n}\))과 가중치(\(w_{1}, w_{2}, w_{3}, \cdots, w_{n}\))를 곱하여 그 값에 편향(\(bias\))를 더한 값이 어느 임계치 값(\(\theta\))을 초과하면 활성화 함수를 통과한 출력값을 내보냄

https://towardsdatascience.com/rosenblatts-perceptron-the-very-first-neural-network-37a3ec09038a

 

2. 뉴런의 수학적 표현

https://cs231n.github.io/convolutional-networks/

  • 뉴런의 axon(\(x_{0}\))이 synapse를 통과하며 가중치(\(w_{0}\))가 주어짐 → dendrite를 통해 \(w_{0}x_{0}\)이 cell body에 들어감 → 이외에 \(w_{1}x_{1}\), \(w_{2}x_{2}\)등도 cell body에 들어옴
  • cell body에서 값들이 합해지고, 합한 값에 마지막으로 편향을 더해줌 → activation function 과정을 거쳐 output axon으로 출력됨
  • \(y=f(\sum _{i}w_{i}x_{i}+b)\)
  • \(f\): 활성화 함수
    • 임계값(\(\theta\))을 경계로 출력이 바뀜
  • \(b\): 편향
    • 결정 경계선을 원점에서부터 벗어나게 해줌
    • 따로 표현이 없어도 기본적으로 존재한다고 생각
  • \(\sum _{i}w_{i}x_{i}\): 두 벡터의 내적으로 표현 가능
    \(x_{1}w_{1} + x_{2}w_{2} + \cdots + x_{n}w_{n} = w^{T}x\)

 

3. 완전 연결 계층(Fully-Connected Layer) 수학적 표현

  • 모드 노드들이 연결된 구조
  • \(W=[w_{0}, w_{1}, \cdots, w_{M-1}]^{T}\)
    각각의 \(w_{k}\)는 \(N \times 1\) 형태의 벡터
    \(W\)는 \(N \times M\)행렬
    \(b=[b_{0}, b_{1}, \cdots, b_{M-1}]\\
    y_{0}=f(w_{0}^{T}x+b_{0})\\
    y_{1}=f(w_{1}^{T}x+b_{1})\\
    y_{2}=f(w_{2}^{T}x+b_{2})\\
    \cdots\\
    y_{M-1}=f(w_{M-1}^{T}x+b_{M-1})\\
    \to y=f(Wx+b)\)

 

4. 논리 회로

  • 논리 게이트(Logic Gates)
    • AND: 둘 다 1이면 1
    • OR: 둘 중 하나면 1이면 1
    • NOT: 하나가 1이면 다른 하나는 0, 하나가 0이면 다른 하나는 1
    • NAND: 둘 다 1이면 0
    • NOR: 둘 다 0이면 1
  • 다이어그램과 진리표

http://www.schoolphysics.co.uk/age14-16/Electronics/text/Logic_gates/index.html

 

  - AND 게이터

  • 두 입력이 모두 1일 때 1을 출력하는 논리회로

https://www.tutorialspoint.com/computer_logical_organization/logic_gates.htm

def AND(a, b):
    input = np.array([a, b])
    weights = np.array([0.4, 0.4])
    bias = -0.6
    value = np.sum(input * weights) + bias

    if (value <= 0):
        return 0
    else:
        return 1
        
print(AND(0, 0))  # 0
print(AND(0, 1))  # 0
print(AND(1, 0))  # 0
print(AND(1, 1))  # 1
x1 = np.arange(-2, 2, 0.01)
x2 = np.arange(-2, 2, 0.01)
bias = -0.6

y = (-0.4 * x1 - bias) / 0.4

plt.axvline(x = 0)
plt.axhline(y = 0)
plt.plot(x1, y, 'r--')
plt.scatter(0, 0, color = 'orange', marker = 'o', s = 150)
plt.scatter(0, 1, color = 'orange', marker = 'o', s = 150)
plt.scatter(1, 0, color = 'orange', marker = 'o', s = 150)
plt.scatter(1, 1, color = 'black', marker = '^', s = 150)
plt.xlim(-0.5, 1.5)
plt.ylim(-0.5, 1.5)
plt.grid()
plt.show()

  • 빨간선인 임계값을 넘어간 부분이 결과 1을 출

 

  - OR 게이트

  • 두 입력 중 하나라도 1이면 1을 출력하는 논리회로

https://www.tutorialspoint.com/computer_logical_organization/logic_gates.htm

def OR(a, b):
    input = np.array([a, b])
    weights = np.array([0.4, 0.5])
    bias = -0.3
    value = np.sum(input * weights) + bias

    if (value <= 0):
        return 0
    else:
        return 1

print(OR(0, 0))  # 0
print(OR(0, 1))  # 1
print(OR(1, 0))  # 1
print(OR(1, 1))  # 1
x1 = np.arange(-2, 2, 0.01)
x2 = np.arange(-2, 2, 0.01)
bias = -0.3

y = (-0.4 * x1 - bias) / 0.5

plt.axvline(x = 0)
plt.axhline(y = 0)
plt.plot(x1, y, 'r--')
plt.scatter(0, 0, color = 'orange', marker = 'o', s = 150)
plt.scatter(0, 1, color = 'black', marker = '^', s = 150)
plt.scatter(1, 0, color = 'black', marker = '^', s = 150)
plt.scatter(1, 1, color = 'black', marker = '^', s = 150)
plt.xlim(-0.5, 1.5)
plt.ylim(-0.5, 1.5)
plt.grid()
plt.show()

  • 빨간선인 임계값을 넘어간 부분이 결과 1을 출력

 

  - NAND 게이트

  • 두 입력이 모두 1일 때 0을 출력하는 논리회로

https://www.tutorialspoint.com/computer_logical_organization/logic_gates.htm

def NAND(a, b):
    input = np.array([a, b])
    weights = np.array([-0.6, -0.5])
    bias = 0.7
    value = np.sum(input * weights) + bias

    if (value <= 0):
        return 0
    else:
        return 1
        
print(NAND(0, 0))  # 1
print(NAND(0, 1))  # 1
print(NAND(1, 0))  # 1
print(NAND(1, 1))  # 0
x1 = np.arange(-2, 2, 0.01)
x2 = np.arange(-2, 2, 0.01)
bias = 0.7

y = (0.6 * x1 - bias) / -0.5

plt.axvline(x = 0)
plt.axhline(y = 0)
plt.plot(x1, y, 'r--')
plt.scatter(0, 0, color = 'black', marker = '^', s = 150)
plt.scatter(0, 1, color = 'black', marker = '^', s = 150)
plt.scatter(1, 0, color = 'black', marker = '^', s = 150)
plt.scatter(1, 1, color = 'orange', marker = 'o', s = 150)
plt.xlim(-0.5, 1.5)
plt.ylim(-0.5, 1.5)
plt.grid()
plt.show()

  • 빨간선인 임계값을 넘어간 부분이 결과 0을 출력

 

5. XOR 게이트

  • 인공지능 첫번째 겨울, 딥러닝의 첫번째 위기를 초래
  • AND, NAND와 같은 선형 문제는 퍼셉트론으로 해결 가능하지만, XOR은 직선(선형) 하나로는 불가능
  • 다층 퍼셉트론으로 해
  • AND, NAND, OR Gate를 조합

 

6. 다층 퍼셉트론(Multi Layer Perceptron, MLP)

  - 다층 퍼셉트론의 구성

  • 입력층(input layer)
  • 은닉층(hidden layer)
    • 1개 이상 존재
    • 보통 5개 이상 존재하면 Deep Neural Network라고 칭함
  • 출력층(output layer)

https://www.researchgate.net/figure/A-schematic-diagram-of-artificial-neural-network-and-architecture-of-the-feed-forward_fig1_26614896

  • 수식
    • (input layer → hidden layer)
      \(z=f_{L}(W_{L}x+b_{L})\)
    • (hidden layer → output layer)
      \(y=a_{K}(W_{K}z+b_{K})\)

 

  - XOR 게이트

  • 서로 다른 두 값이 입력으로 들어가면 1을 반환

https://www.tutorialspoint.com/computer_logical_organization/logic_gates.htm

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    return y

print(XOR(0, 0))  # 0
print(XOR(0, 1))  # 1
print(XOR(1, 0))  # 1
print(XOR(1, 1))  # 0

 

7. 활성화 함수(Activation Function)

  • 입력 신호의 총합을 출력 신호로 변환하는 함수
  • 활성화 함수에 따라 출력값이 결정
  • 단층, 다층 퍼셉트론 모두 사용
  • 대표적인 활성화 함수
    • Sigmoid
    • ReLU
    • tanh
    • Identify Function
    • Softmax
  • 하나의 layer에서 다음 layer로 넘어갈 때는 항상 활성화 함수를 통과

 

  - 계단 함수(Step Function)

  • \(y=\begin{cases}
     0&(x<0)  \\
     1& (x\geq 0)
    \end{cases}\)

https://www.intmath.com/laplace-transformation/1a-unit-step-functions-definition.php

def step_function(x):
    if x > 0:
        return 1
    else:
        return 0

def step_function_for_numpy(x):
    y = x > 0
    return y.astype(int)


print(step_function(-3))   # 0
print(step_function(5))    # 1

# 넘파이 배열로 입력값을 줄 때 사용
a = np.array([5, 3, -4, 2.0])
print(step_function_for_numpy(a))   # [1 1 0 1]

 

  - 시그모이드 함수(Sigmoid Function)

  • 이진분류(binary Classification)에 주로 사용
    • 마지막 출력층의 활성화 함수로 사용
  • 출력값이  0~1의 값이며, 이는 확률로 표현 가능
    \(y=\frac{1}{1+e^{-x}}\)

https://www.geeksforgeeks.org/implement-sigmoid-function-using-numpy/

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

print(sigmoid(3))   # 0.9525741268224334, 1에 근접
print(sigmoid(-3))  # 0.04742587317756678, 0에 근접

 

  - 시그모이드 함수와 계단 함수 비교

  • 공통점
    • 출력값이 0~1내의 범위
    • 입력값의 정도에 따라 출력값의 정도가 달라짐 즉, 입력이 중요하면(입력값이 크면) 큰 값을 출력
  • 차이점
    계단함수에 비해 시그모이드 함수는
    • 입력에 따라 출력이 연속적으로 변화
    • 출력이 '매끄러움'
      이는 모든 점에서 미분 가능함을 의미
plt.grid()
x = np.arange(-5.0, 5.0, 0.01)
y1 = sigmoid(x)
y2 = step_function_for_numpy(x)
plt.plot(x, y1, 'r--', x, y2, 'b--')
plt.show()

 

  - ReLU(Rectified Linear Unit)

  • 가장 많이 쓰이는 함수 중 하나
  • \(y=\begin{cases}
     0&(x\leq0)  \\
     x& (x> 0)
    \end{cases}\)

https://machinelearningmastery.com/rectified-linear-activation-function-for-deep-learning-neural-networks/

def ReLU(x):
    if x > 0:
        return x
    else:
        return 0

print(ReLU(5))   # 5
print(ReLU(-3))  # 0

 

  - 하이퍼볼릭 탄젠트 함수(Hyperbolic tangent function, tanh)

  • \(y=\frac{e^{x}-e^{-x}}{e^{x}+e^{-x}}

https://www.researchgate.net/figure/Hyperbolic-tangent-activation-function_fig1_326279910

def tanh(x):
    return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))

print(tanh(3))   # 0.9950547536867306
print(tanh(-3))  # -0.9950547536867306

 

  - 항등 함수(Identity Function)

  • 회귀(Regression) 문제에서 주로 사용
    • 출력층의 활성화 함수로 활용
  • \(y=x\)
  • 입력값 그대로 출력하기 때문에 굳이 정의할 필요는 없지만 신경망 중간 레이어 흐름과 통일하기 위해 사용

https://math.info/Algebra/Identity_Function/

def identify_function(x):
    return x

print(identify_function(4))   # 4
print(identify_function(-1))  # -1

X = np.array([2, -3, 0.4])
print(identify_function(X))   # [ 2.  -3.   0.4]

 

  - Softmax

  • 다중 클래스 분류에 사용(Multi Class Classification)
  • 입력값의 영향을 크게 받음
    입력값이 크면 출력값도 큼
  • 출력값을 확률에 대응 가능
  • 출력값의 총합은 1
  • 수식
    \(y_{k}=\frac{exp(a_{k})} {\sum_{i=1}exp(a_{i})}\)

https://medium.com/data-science-bootcamp/understand-the-softmax-function-in-minutes-f3a59641e86d

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    return y

a = np.array([0.3, 0.2, 4.0, -1.2])
print(softmax(a))                 # [0.02348781 0.02125265 0.9500187  0.00524084]
print(np.sum(softmax(a)))    # 1.0

 

  - 소프트맥스 함수 주의점

  • 오버플로우(overflow) 문제
A = np.array([1000, 900, 1050, 500])
print(softmax(A))

# 출력 결과
[nan nan nan  0.]
RuntimeWarning: overflow encountered in exp
  exp_a = np.exp(a)
RuntimeWarning: invalid value encountered in true_divide
  y = exp_a / sum_exp_a
  • 지수 함수(exponential function)을 사용하지 때문에 입력값이 너무 크면 무한대(inf)가 반환됨
  • 개선한 수식(C는 a의 최대값, 스케일링을 조금 하는 것)
    \(y_{k}=\frac {exp(a_{k})} {\sum_{i=1}exp(a_{i})} = \frac {Cexp(a_{k})} {C\sum_{i=1}exp(a_{i})}\\
    \quad = \frac {exp(a_{k} + logC)} {\sum_{i=1}exp(a_{i} + logC)}\\
    \quad = \frac {exp(a_{k}+C'} {\sum_{i=1}exp(a_{i}+C')}\)
def softmax(a):
    C = np.max(a)
    return (np.exp(a - C) / np.sum(np.exp(a - C)))

A = np.array([1000, 900, 1050, 500])
print(softmax(A))

# 출력 결과
[1.92874985e-022 7.17509597e-066 1.00000000e+000 1.37415257e-239]

 

  - 활성화 함수를 비선형 함수로 사용하는 이유

  • 신경망을 깊게 하기 위함
  • 만약 활성화 함수를 선형함수로 하게 되면 은닉층의 개수가 여러개이더라도 의미가 없어짐
  • 만약, \(h(x)=cx\)이고, 3개의 은닉층이 존재한다면
    \(y=h(h(h(x)))\\
    \quad=c \times c \times c \times x\\
    \quad=c^{3}x\)
    이므로 결국 선형 함수가 되어버림

 

  - 그 외의 활성화 함수

  • LeakyReLU
  • \(f_{a}(x)=\begin{cases}
     x& x \geq 0\\
     ax& x<0
    \end{cases}\)

https://knowhowspot.com/technology/ai-and-machine-learning/artificial-neural-network-activation-function/

def LeakyReLU(x):
    a = 0.01
    return np.maximum(a*x, x)
    
x = np.array([0.5, -1.4, 3, 0, 5])
print(LeakyReLU(x))

# 출력 결과
[ 0.5   -0.014  3.     0.     5.   ]

 

  • ELU(Exponential Linear Units)
  • \(f(\alpha, x)=\begin{cases}
     \alpha(e^{x}-1)& x \leq 0\\
     x& x>0
    \end{cases}\)

https://www.researchgate.net/figure/Exponential-Linear-Unit-activation-function-input-output-mapping-The-activation-function_fig1_331794632

def ELU(x):
    alpha = 1.0
    return ( x>= 0) * x + (x < 0) * alpha * (np.exp(x)-1)

print(ELU(4))       # 4.0
print(ELU(-0.5))   # -0.3934693402873666

x = np.array([-2, 0.1, 4])
print(ELU(x))      # [-0.86466472  0.1         4.        ]

 

  - 활성화 함수 참고

  • 일반적인 사용 순서
    1. ELU
    2. LeakyReLU
    3. ReLU
    4. tanh
    5. sigmoid
  • 스탠포드 강의에서 언급한 사용 순서
    1. ReLU
    2. ReLU Family(LeakyReLU, ELU)
    3. sigmoid는 사용 X

 

8. 3층 신경망 구현하기

  • 2 클래스 분류
  • 입력층(input layer)
    • 뉴런수: 3
  • 은닉층(hidden layer)
    • 첫번째 은닉층 뉴런수: 3
    • 두번째 은닉층 뉴런수: 2
  • 출력층(output layer)
    • 뉴런수: 2

 

  - 활성화 함수 정의

def sigmoid(X):
    return 1 / (1 + np.exp(-X))

X = np.array([1.0, 0.5, 0.4])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6], [0.3, 0.5, 0.7]])
B1 = np.array([1, 1, 1])

print(X.shape)    # (3,)
print(W1.shape)   # (3, 3)
print(B1.shape)   # (3,)

A1 = np.dot(X, W1) + B1
Z1 = sigmoid(A1)

print(A1)
print(Z1)

# 출력 결과
[1.32 1.7  2.08]
[0.78918171 0.84553473 0.88894403]
# 두번째 레이어 통과
W2 = np.array([[0.2, 0.4, 0.6], [0.1, 0.3, 0.5], [0.4, 0.6, 0.8]])
B2 = np.array([1, 1, 1])

print(W2.shape)   # (3, 3)
print(B2.shape)   # (3,)

A2 = np.dot(A1, W2) + B2
Z2 = sigmoid(A2)

print(A2)
print(Z2)

# 출력 결과
[2.266 3.286 4.306]
[0.90602176 0.96394539 0.9866921 ]
# 세번째 레이어 통과
W3 = np.array([[0.1, 0.3], [-0.1, -0.5], [0.3, 0.5]])
B3 = np.array([1, 1])

print(W3.shape)   # (3, 2)
print(B3.shape)   # (2,)

A3 = np.dot(A2, W3) + B3
Z3 = sigmoid(A3)

print(A3)
print(Z3)

# 출력 결과
[2.1898 2.1898]
[0.8993298 0.8993298]
# 네번째 레이어 통과
W4 = np.array([[0.1, 0.2], [0.3, 0.5]])
B4 = np.array([1, 1])

print(W4.shape)   # (2, 2)
print(B4.shape)   # (2,)

A4 = np.dot(A3, W4) + B4
Z4 = sigmoid(A4)

print(A4)
print(Z4)

# 출력 결과
[1.87592 2.53286]
[0.86714179 0.92641356]
# 하나의 네트워크로 합치면 다음과 같이 됨
def network():
    network = {}

    # 첫번째 레이어
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6], [0.3, 0.5, 0.7]])
    network['B1'] = np.array([1, 1, 1])

    # 두번째 레이어
    network['W2'] = np.array([[0.2, 0.4, 0.6], [0.1, 0.3, 0.5], [0.4, 0.6, 0.8]])
    network['B2'] = np.array([1, 1, 1])

    # 세번째 레이어
    network['W3'] = np.array([[0.1, 0.3], [-0.1, -0.5], [0.3, 0.5]])
    network['B3'] = np.array([1, 1])

    # 네번째 레이어
    network['W4'] = np.array([[0.1, 0.2], [0.3, 0.5]])
    network['B3'] = np.array([1, 1])

    return network
    

def forward(network, x):
    W1, W2, W3, W4 = network['W1'], network['W2'], network['W3'], network['W4']
    B1, B2, B3, B4 = network['B1'], network['B2'], network['B3'], network['B4']

    A1 = np.dot(x, W1) + B1
    Z1 = sigmoid(A1)

    A2 = np.dot(Z1, W1) + B1
    Z2 = sigmoid(A2)

    A3 = np.dot(Z2, W1) + B1
    Z3 = sigmoid(A3)


    A4 = np.dot(Z3, W1) + B1
    y = sigmoid(A4)

    return y

  - 신경망 추론

net = network()
x = np.array([0.3, 1.3, -2.2])
y = forward(net, x)
print(y)

# 출력 결과
[0.78781193 0.82428264]

+ Recent posts