● 합성곱 신경망(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()

  • 스프링에서 JSP 위주로 View를 개발하는 것처럼 스프링 부트는 Thymeleaf라는 템플릿 엔진 이용

1. Thymeleaf 기초 문법

 1) 인텔리제이 설정

  • Thymeleaf를 이용하기 위해 html 파일의 네임스페이스에 Thymeleaf를 지정하는 것
  • 네임스페이스를 지정하면 'th:'와 같은 Thymeleaf의 모든 기능을 사용할 수 있게됨
<!-- hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${msg}"></h1>
</body>
</html>
  • 'th:'로 시작하는 기능을 사용할 수는 있지만 Model에 담긴 데이터를 사용할 때 '해당 변수를 찾을 수 없다'는 에러가 발생할 수 있어 인텔리제이의 Setting > Thymeleaf 검색 > Unresolved references in Thymeleaf expression variables' 체크 해제

 

  - Thymeleaf 출력

  • Thymeleaf는 Model로 전달된 데이터를 출력하기 위해 HTML 태그 내에 'th:,,'로 시작하는 속성을 이용하거나 인라인 이용
  • SampleController에서 ex1()을 추가해서 '/ex/ex1'이라는 경로 호출할 때 동작하도록 구성
// SampleController
package org.zerock.b01.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;
import java.util.List;

@Controller
@Log4j2
public class SampleController {

    ...

    @GetMapping("/ex/ex1")
    public void ex1(Model model) {
        List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

        model.addAttribute("list", list);
    }
}
  • templates > ex 디렉토리 생성 > ex1.html 추가하여 결과화면 생성
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h4>[[${list}]]</h4>
  <hr/>
  <h4 th:text="${list}"></h4>
</body>
</html>

 

  - th:with를 이용한 변수 선언

  • Thymeleaf를 이용하는 과정에서 임시로 변수를 선언해야 하는 상황에서 'th:with'를 이용해서 간단히 처리 가능
  • 'th:with'로 만드는 변수는 '변수명 = 값' 형태로, ','를 이용해 여러 개 선언 가능
<!-- hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <h1 th:text="${msg}"></h1>

    <!-- 임시 변수 선언 -->
    <div th:with="num1 = ${10}, num2 = ${20}">
        <h4 th:text="${num1 + num2}"></h4>
    </div>
    
</body>
</html>

 

 2) 반복문과 제어문 처리

  • 크게 두 가지 방법
    • 반복이 필요한 태그에 'th:each'를 적용하는 방법
    • <th:block>이라는 별도의 태그를 이용하는 방법
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <!-- 반복이 필요한 태그에 'th:each'를 적용하는 방법 -->
    <ul>
        <li th:each="str: ${list}" th:text="${str}"></li>
    </ul>
    
    <!-- <th:block>이라는 별도의 태그를 이용하는 방법 -->
    <ul>
        <th:block th:each="str: ${list}">
            <li>[[${str}]]</li>
        </th:block>
    </ul>

</body>
</html>
  • 결과는 동일

 

  - 반복문의 status 변수

  • Thymeleaf는 th:each를 처리할 때 현재 반복문의 내부 상태에 변수를 추가해서 사용할 수 있음
  • 일명 status 변수라고 하며 index / count / size / first / last / odd / even 등을 이용해 자주 사용하는 값 출력 가
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    ...

    <ul>
        <li th:each="str,status: ${list}">
            [[${status.index}]] -- [[${str}]]
        </li>
    </ul>

</body>
</html>

  - th:if / th:unless / th:switch

  • Thymeleaf는 제어문의 형태로 th:if / th:unless / th:switch를 이용할 수 있음
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    ...

    <ul>
        <li th:each="str,status: ${list}">
            <span th:if="${status.odd}">ODD -- [[${str}]]</span>
            <span th:unless="${status.odd}">EVEN -- [[${str}]]</span>
        </li>
    </ul>

</body>
</html>

  • '? ' 사용하여 더 편하게 이항 혹은 삼항 처리 가
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    ...

    <!-- 이항 연산자로 사용 -->
    <ul>
        <li th:each="str,status: ${list}">
            <span th:text="${status.odd}?'ODD ---' + ${str}"></span>
        </li>
    </ul>
    
    <!-- 삼항 연산자로 사용 -->
    <ul>
        <li th:each="str,status: ${list}">
            <span th:text="${status.odd}?'ODD ---' + ${str} : 'EVEN ---' + ${str}"></span>
        </li>
    </ul>
    

</body>
</html>

  • th:switch는 th:case와 같이 사용해서 Switch 문을 처리할 때 사용할 수 있음
<!-- ex1.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    ...
	
    <!-- 3으로 나눈 나머지의 결과에 따라 각각의 결과를 출력 -->
    <ul>
        <li th:each="str,status: ${list}">
            <th:block th:switch="${status.index % 3}">
                <span th:case="0">0</span>
                <span th:case="1">1</span>
                <span th:case="2">2</span>
            </th:block>
        </li>
    </ul>

</body>
</html>

 

 3) Thymeleaf 링크 처리

  • Thymeleaf에서는 @로 링크를 작성하면 링크 처리됨
<a th:href="@{/hello}">Go to /hello</a>

 

  - 링크의 쿼리 스트링 처리

  • 링크를 'key=value'의 형태로 필요한 파라미터를 처리해야 할 때 상당히 편리
  • 쿼리 스트링은 '()'를 이용해서 파라미터의 이름과 값을 지정
<a th:href="@{/hello(name='AAA', age=16)}">Go to /hello</a>

  • 한글이나 공백에 대한 URL 인코딩 처리가 자동으로 이루어짐
<a th:href="@{/hello(name='한글 처리', age=16)}">Go to /hello</a>
  • 링크를 만드는 값이 배열과 같이 여러 개일 때는 자동으로 같은 이름의 파라미터로 처리
<a th:href="@{/hello(types=${{'AA', 'BB', 'CC'}}, age=16)}">Go to /hello</a>

 

 

2. Thymeleaf의 특별한 기능들

 1) 인라인 처리

  • Thymeleaf에서 상황에 따라 동일한 데이터를 다르게 출력해 주는 인라인 기능은 자바스크립트를 사용할 때 편리한 기능
  • 다양한 종류의 데이터를 Model로 담아서 전달하는 메서드를 SampleController에 추가
// SampleController
package org.zerock.b01.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Controller
@Log4j2
public class SampleController {

    ...
    
    class SampleDTO {
        private String p1,p2,p3;
        
        public String getP1() {
            return p1;
        }
        public String getP2() {
            return p2;
        }
        public String getP3() {
            return p3;
        }
    }
    
    @GetMapping(("/ex/ex2"))
    public void ex2(Model model) {
        log.info("ex/ex2................");
        
        List<String>strList = IntStream.range(1,10)
                .mapToObj(i -> "Data"+i)
                .collect(Collectors.toList());
        
        model.addAttribute("list", strList);
        
        Map<String, String> map = new HashMap<>();
        map.put("A","AAAA");
        map.put("B","BBBB");
        
        model.addAttribute("map", map);
        
        SampleDTO sampleDTO = new SampleDTO();
        sampleDTO.p1 = "Value -- p1";
        sampleDTO.p2 = "Value -- p2";
        sampleDTO.p3 = "Value -- p3";
        
        model.addAttribute("dto", sampleDTO);
    }
}
  • ex2 화면 구성
<!-- ex2.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

  <div th:text="${list}"></div>
  <div th:text="${map}"></div>
  <div th:text="${dto}"></div>

  <script th:inline="javascript">
    const list = [[${list}]]
    const map = [[${map}]]
    const dto = [[${dto}]]
    
    console.log(list)
    console.log(map)
    console.log(dto)
  </script>

</body>
</html>
  • html은 기존처럼 출력되고, <script> 부분은 자바스크립트에 맞는 문법으로 만들어지는 것을 확인

 

 2) Thymeleaf의 레이아웃 기능

  • <th:block>을 이용하면 레이아웃을 만들고 특정한 페이지에서 필요한 부분만 작성하는 방식으로 개발 가능
  • 별도의 라이브러리 필요하므로 build.gradle에서 추가
// build.gradle
dependencies {
	
    ...

	implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'
}

 

  • templates > layout 폴더 생성 > 레이아웃을 위한 layout1.html 작성
<!-- layout1.html -->
<!DOCTYPE html>
<!-- thymeleaf의 layout 작성을 위한 네임스페이스 지정 -->
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Layout Page</title>
</head>
<body>

<div>
  <h3>Sample Layout Header</h3>
</div>

<!-- layout:fragment 속성을 이용하면 나중에 다른 파일에서 해당 부분만 개발 가능 -->
<div layout:fragment="content">
  <p>Page content goes here</p>
</div>

<div>
  <h3>Sample Layout Footer</h3>
</div>

<th:block layout:fragment="script">
  
</th:block>

</body>
</html>

 

  • Samplecontroller에 레이아웃 예제를 위한 ex3()을 추가
// SampleController
package org.zerock.b01.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@Controller
@Log4j2
public class SampleController {

    ...

    @GetMapping("/ex/ex3")
    public void ex3(Model model) {
        model.addAttribute("arr", new String[]{"AAA", "BBB", "CCC"});
    }
}

 

  • layout1.html에서 layout:fragment 속성으로 처리했던 content와 script부분을 다른 파일인 ex3에서 개발할 수 있음
<!-- ex3 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">
    <h1>ex3.html</h1>
</div>

<!-- ex3 -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">
    <h1>ex3.html</h1>
</div>

<script layout:fragment="script" th:inline="javascript">
	const arr = [[${arr}]]
</script>

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. 기존 개발에서 달라진 점들

  • 스프링에서 스프링 부트로 넘어오는 일은 기존의 코드나 개념이 그대로 유지됨(현실적으로 새로운 개념 필요 x)
  • 설정과 관련해 직접 필요한 라이브러리를 기존 build.gradle 파일에 추가하는 설정이 자동으로 처리됨
  • 톰캣이 내장된 상태로 프로젝트가 생성되기 때문에 WAS의 추가 설정이 필요 x
  • Bean 설정은 XML을 대신에 자바 설정으로 이용하는 것으로 약간 변경됨
  • 스프링 MVC에서는 JSP, 스프링 부트에서는 Thymeleaf라는 템플릿 엔진 활용
  • 최근 스프링 부트는 화면을 구성하지 않고 데이터만을 제공하는 API 서버 형태를 이용하기도 함
  • 스프링 부트에서도 MyBatis를 이용할 수 있지만, [자바 웹 개발 워크북]에서는 JPA 이용
    • JPA 이용 시 객체지향으로 구성된 객체들을 데이터베이스에 반영할 수 있고, 이를 자동으로 처리할 수 있으므로 별도의 SQL 개발 없이도 개발 가능

 

 1) 스프링 부트의 프로젝트 생성 방식

  • Spring Initializr를 이용한 자동 생성 또는
    Maven이나 Gradle을 이용한 직접 생성
  • 스프링 부트는 거의 모든 개발에 Spring Initializr를 이용(프로젝트의 기본 템플릿 구조를 만들어 주기 때문)

 

 

2. Spring Initailizr를 이용한 프로젝트 생성

  • New Project 생성

  • Dependencies에서 다음 항목들 추가
    • Spring Boot DevTools
    • Lombok
    • Spring Web
    • Thymeleaf
    • Spring Data JPA
    • MariaDB Driver

 

 1) 프로젝트의 실행

  • 스프링 부트의 프로젝트는 서버를 내장한 상태에서 만들어지기 때문에 스프링만을 이용할 때와 달리 별도의 WAS 설정 필요 x
  • main()의 실행을 통해 프로젝트 실행
  • 프로젝트 초기화할 때 실행 메뉴에 'B01Application이라는 이름으로 실행 메뉴가 구성됨

  • main() 실행 시, 다음과 같은 로그 출력

  • 실행 결과는 실패

  • 스프링 부트가 자동 설정을 통해 인식한 Spring Data JPA를 실행할 때 DB와 관련된 설정을 찾을 수 없어 발생
  • 에러가 발생하긴 했지만 아무 설정 없이 자동으로 데이터베이스 관련 설정을 이용함
    → 라이브러리만으로 설정을 인식하려는 특성을 자동 설정(auto configuration)이라고 함

 

  • 스프링 부트 설정은 프로젝트 생성 시 만들어진 application.properties 파일을 이용하거나 application.yml 파일 이용
  • 파일 설정을 피하고 싶다면 @Configuration이 있는 클래스 파일을 만들어서 필요한 설정을 추가
  • 대부분의 스프링을 지원하는 IDE에서 application.properties 파일에 들어갈 수 있는 내용을 쉽게 완성해 주는 기능 제공
  • application.properties 파일에 데이터베이스 설정을 다음과 같이 추가
// application.properties

spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/webdb
spring.datasource.username=webuser
spring.datasource.password=yong1998
  • 다시 실행하면 8080포트로 톰캣이 실행됨

 

  • HikariCP 라이브러리를 가져오거나 HikariConfig 객체를 구성하는 등의 모든 과정이 생략됨

 

 2) 편의성을 높이는 설정

  - 자동 리로딩 설정

  • 웹 개발 시 코드 수정하고 다시 deploy 하는 과정을 자동으로 설정 가능
  • Edit Configuration > Modify options > On 'Update' action / On frame deactivation > Update classes and resources

 

  - Lombok을 테스트 환경에서도 사용하기

  • 스프링 부트는 체크박스를 선택하는 것만으로 Lombok 라이브러리 추가가 가능하지만 테스트 환경에서는 설정이 빠져 있음
  • build.gradle 파일 내 dependencies 항목에 test 관련 설정을 조정
// build.gradle
...

dependencies {
	
    ...
	
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

...

 

  - 로그 레벨의 설정

  • 스프링 부트는 기본적으로 Log4j2가 추가되어 있어 라이브러리를 추가하지 않아고 됨
  • application.properties 파일을 이용해서 간단하게 로그 설정을 추가 가능
// application.properties

...

logging.level.org.springframework=info
logging.level.org.zerock=debug

 

  - 인텔리제이의 DataSource 설정

  • DataSource를 설정해두면 나중에 엔티티 클래스의 생성이나 기타 클래스의 생성과 설정 시에 도움이 됨
  • MariaDB를 설정

 

  - 테스트 환경과 의존성 주입 테스트

  • 스프링에는 'spring-test-xxx' 라이브러리를 추가해야 하고, JUnit 등도 직접 추가해야하지만,
    스프링 부트는 프로젝트 생성 시 이미 테스트 관련 설정이 완료되고 테스트 코드도 생성되어 있음

  • 테스트 코드 실행을 위해 DataSourceTests를 작성해서 HikariCP의 테스트와 Lombok 확인
// DataSourceTests
package org.zerock.b01;

import lombok.Cleanup;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
@Log4j2
public class DataSourcetests {

    // DataSource는 application.properties에 설정된 DataSource 관련 설정을 통해 생성된 bean
    // 이에 대한 별도 설정없이 스프링에서 바로 사용 가능    
    @Autowired
    private DataSource dataSource;
    
    @Test
    public void testConnection() throws SQLException {
        
        @Cleanup
        Connection con = dataSource.getConnection();
        
        log.info(con);
        Assertions.assertNotNull(con);
    }
}
  • 테스트 환경에서도 @Log4j2 어노테이션을 통해 테스트 환경에서 Lombok을 사용할 수 있음을 확인

 

  - Spring Data JPA를 위한 설정

  • application.properties에 다음 내용 추가
//application.properties

...

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
  • spring.jpa.hibernate.ddl-auto 속성은 프로젝트 실행 시 DDL 문을 어떻게 처리할 것인지 명시
  • 속성값은 다음과 같이 명시
속성값 의미
none DDL을 하지 않음
create-drop 실행할 때 DDL을 실행하고 종료 시에 만들어진 테이블 등을 모두 삭제
create 실행할 때마다 새롭게 테이블을 생성
update 기존과 다르게 변경된 부분이 있을 때는 새로 생성
validate 변경된 부분만 알려주고 종료
  • update 속성값의 경우, 테이블이 없을 때는 자동으로 생성하고 변경이 필요할 때는 alter table이 실행됨,
    테이블 뿐만 아니라 인덱스나 외래키 등도 자동으로 처리

 

  • spring.jpa.properties.hibernate.format_sql 속성은 실제로 실행되는 SQL을 포맷팅하여 알아보기 쉽게 출력
  • sprinf.jpa.show-sql은 JPA가 실행하는 SQL을 같이 출력

 

 

3. 스프링 부트에서 웹 개발

  • controller나 화면을 개발하는 것은 유사하지만 web.xml이나 servlet-context.xml과 같은 웹 관련 설정 파일들이 없기 때문에 이를 대신하는 클래스를 작성해 준다는 점이 다름

 

 1) 컨트롤러와 Thymeleaf 만들기

  • 프로젝트에 우선 controller라는 패키지 생성 > SampleController 클래스 생성
// SampleController
package org.zerock.b01.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@Log4j2
public class SampleController {
    
    @GetMapping("/hello")
    public void hello(Model model) {
        log.info("hello.................");
        
        model.addAttribute("msg", "HELLO WORLD");
    }
}
  • 프로젝트 생성 시 만들어져 있는 templates 폴더에 hello.html 작성

<!-- hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
	<h1 th:text="${msg}"></h1>
</body>
</html>
  • 실행 결과

 

 2) JSON 데이터 만들기

  • controller 패키지에 SampleJSONComtroller라는 클래스 작성
// SampleJSONController
package org.zerock.b01.controller;

import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Log4j2
public class sampleJSONController {
    
    @GetMapping("/helloArr")
    public String[] helloArr() {
        log.info("helloArr.................");
        
        return new String[]{"AAA", "BBB", "CCC"};
    }
}
  • 브라우저에 'localhost:8080/helloArr' 경로를 호출하면 배열이 그대로 출력

  • 응답 헤더를 확인해보면 서버에서 해당 데이터는 'application/json'이라는 것을 전송함

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에서 시작한 파란점은 안장점에 빠짐

+ Recent posts