지능적인 기계를 만드는 법에 대한 영감을 얻으려면 뇌 구조를 살펴보는 것이 합리적일 것이다. 이는 인공 신경망 artificial neural network 를 촉발시킨 근원
인공 신경망은 딥러닝의 핵심, 복잡하고 대규모 머신러닝 문제를 다루는 데 적합하다.
10.1 생물학적 뉴런에서 인공 뉴런까지
- 신경망을 훈련하기 위한 데이터가 많아졌다.
- 하드웨어가 크게 발전
- 훈련 알고리즘이 향상
- 일부 인공 신경망의 이론상 제한이 문제가 되지 않는다고 밝혀짐
10.1.1 생물학적 뉴런
뉴런 세포는 핵을 포함하는 새포체 cell body 와 복잡한 구성 요소로 이루어져 있다. 수상돌기 dendrite 라는 나뭇가지 모양의 돌기와 축삭 돌기 axon 라는 아주 긴 돌기가 있다. 축삭돌기의 끝은 축삭끝가지 telodendria 라는 여러 가지로 나뉘고, 이 가지 끝은 시냅스 말단 synaptic terminals 이라는 미세한 구조, 다른 뉴런의 수상돌기나 세포체에 연결, 생물학적 뉴런은 활동 전위 또는 신호라고 부르는 짧은 전기 자극을 만든다. 이 신호는 축삭돌기를 따라 이동하며 시냅스가 신경전달물질이라는 화학적 신호를 발생
10.1.2 뉴런을 사용한 논리 연산
인공 뉴런 artificial neuron 모델은 하나 이상의 이진 입력과 이진 출력 하나를 가진다. 인공 뉴런은 단순히 입력이 일정 개수만큼 활성화되었을 때 출력을 내보낸다.
여기서는 적어도 입력이 둘은 준비되어야 뉴런이 활성화된다고 가정했다.
- 왼쪽 첫 번째 네트워크는 항등 함수, A가 활성화되면 C도 활성화
- 두 번째 네트워크는 논리 곱 연산을 수행 A와 B 모두 활성화 될 때만 C가 활성화
- 논리 합 연산 수행 A와 B 중 하나가 활성화 되면 C가 활성화
- 어떤 입력이 뉴런의 활성화를 억제할 수 있다고 가정, A가 활성화 B가 비활성화 될 때 C가 활성화 A가 항상 활성화되어 있다면 B에 대한논리 부정 연산이 된다. 즉, 뉴런 B가 비활성화될 때 뉴런 C가 활성화되고, 또는 정반대로 뉴런 B가 활성화될 때 뉴런 C가 비활성화된다.
10.1.3 퍼셉트론
퍼셉트론 perceptron 은 가장 간단한 인공 신경망 구조 인공 뉴런을 기반으로 입력과 출력이 숫자이고, 각각의 입력 연결은 가중치와 연관되어 있다. 입력의 가중치 합을 계산한 뒤 계산된 합에 계단 함수 step function 를 적용하여 결과를 출력한다.
퍼셉트론에서 가장 널리 사용되는 계단 함수는 헤비사이드 계단 함수 Heaviside step function 이다.
하나의 TLU는 간단한 선형 이진 분류 문제에 사용할 수 있다. 입력의 선형 조합을 계산해 그 결과가 임곗값을 넘으면 양성 클래스를 출력, 그렇지 않으면 음성 클래스 출력, TLU를 훈련한다는 것은 최적의 가중치를 찾는다는 뜻
퍼셉트론은 층이 하나뿐인 TLU으로 구성, 각 TLU는 모든 입력에 연결 한 층에 있는 모든 뉴런이 이전 층의 모든 뉴런과 연결되어 있을 때 이를 완전 연결 층 fullyconnectedlayer 또는 밀집층 dense layer 라고 부른다. 퍼셉트론의 입력은 입력 뉴런 input neuron 이라 불리는 특별한 통과 뉴런에 주입, 이 뉴런은 어떤 입력이 주입되는 그냥 출력으로 통과시킴, 입력층은 모두 입력 뉴런으로 구성된다. 보통 거기에 편향 특성이 더해진다.
전형적으로 이 편향 특성은 항상 1을 출력하는 특별한 종류의 뉴런인 편향 뉴런 bias neuron 으로 표현된다.
입력 두 개와 출력 세 개로 구성된 퍼셉트론이다. 이 퍼셉트론은 샘플을 세 개의 다른 이진 클래스로 동시에 분류할 수 있으므로 다중 레이블 분류기다.
선형 대수학을 통해 한 번에 여러 샘플에 대해 인공 뉴런 층의 출력을 효율적으로 계산할 수 있다.
- 이전과 마찬가지로 X는 입력 특성의 행렬을 나타낸다. 이 행렬의 행은 샘플, 열은 특성이다.
- 가중치 행렬 W는 편향 뉴런을 제외한 모든 연결 가중치를 포함한다. 이 행렬의 행은 입력 뉴런에 해당하고 열은 출력층에 있는 인공 뉴런에 해당한다.
- 편향 벡터 b는 편향 뉴런과 인공 뉴런 사이의 모든 연결 가중치를 포함한다. 인공 뉴런마다 하나의 편향값이 있다.
- 활성화 함수 activation function이라고 부른다. 인공 뉴런이 TLU인 경우 이 함수는 계단 함수이다.
그렇다면 퍼셉트론은 어떻게 훈련될까? '서로 활성화된느 세포가 서로 연결된다' 즉, 두 뉴런이 동시에 활성화될 때마다 이들 사이의 연결 가중치가 증가하는 경향이 있다. 이 규칙은 헤브의 규칙 헤브의 학습으로 알려져있다.
퍼셉트론은 네트워크가 예측할 때 만드는 오차를 반영하도록 조금 변형된 규칙을 사용하여 훈련된다.
퍼셉트론 학습 규칙은 오차가 감소되도록 연결을 강화시킨다. 퍼셉트론에 한 번에 한 개의 샘플이 주입되면 각 샘플에 대해 예측이 만들어진다. 잘못된 예측을 하는 모든 출력 뉴런에 대해 올바른 예측을 만들 수 있도록 입력에 연결된 가중치를 강화시킨다.
- w_i,j 는 i 번째 입력 뉴런과 j 번째 출력 사이를연결하는 가중치이다.
- x_i 는 현재 훈련 샘플의 i 번째 뉴런의 입력값이다.
- y^_ j 는 현재 훈련 샘플의 j 번째 출력 뉴런의 출력값이다.
- y_ j 는 현재 훈련 샘플의 j 번째 출력 뉴런의 타깃값이다.
- n은 학습률이다.
각 출력 뉴런의 결정 경계는 선형이므로 퍼셉트론도 복잡한 패턴을 학습하지 못한다. 하지만 훈련 샘플이 선형적으로 구분될 수 있다면 이 알고리즘이 정답에 수렴한다는 것을 증명, 이를 퍼셉트론 수렴 이론 perceptron convergence theorem이라고 한다.
사이킷런은 하나의 TLU 네트워크를 구현한 Perceptron 클래스를 제공한다. 붓꽃 데이터셋을 사용
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import Perceptron
iris = load_iris()
X = iris.data[:, (2, 3)] # 꽃잎 길이, 꽃잎 너비
y = (iris.target == 0).astype(np.int)
per_clf = Perceptron(max_iter=1000, tol=1e-3, random_state=42)
per_clf.fit(X, y)
y_pred = per_clf.predict([[2, 0.5]])
y_pred
>>>
array([1])
학습 알고리즘이 확률적 경사 하강법과 비슷하지만, 퍼셉트론은 클래스 확률을 제공하지 않으며 고정된 임곗값을 기준으로 예측을 만든다 이런 이유로 퍼셉트론보다 로지스틱 회귀가 선호된다.
퍼셉트론을 여러 개 쌓아올리면 일부 제약을 줄일 수 있다. 이런 인공 신경망을 다층 퍼셉트론 MLP라고 한다. 다층 퍼셉트론은 XOR문제를 풀 수 있다. 입력이 0,0이나 1,1 일 때는 네트워크가 0을 출력고 입력이 0,1 이나 1,0 일 때는 1을 출력한다.
10.1.4 다층 퍼셉트론과 역전파
다층 퍼셉트론은 입력층 input layer 하나와 은닉층 hidden layer 이라 불리는 하나 이상의 TLU 층과 마지막 출력층 output layer 으로 구성된다. 입력층과 가까운 층을 보통 하위층 lower layer 이라 부르고 출력에 가까운 층을 상위층 upper layer 이라 부른다. 출력층을 제외하고 모든 층은 편향 뉴런을 포함하며 다음 층과 완전히 연결되어 있다.
신호는 입력에서 출력으로 한 방향으로만 흐른다. 이 구조는 피드포워드 신경망 feedforward neural network FNN 에 속한다.
은닉층을 여러 개 쌓아 올린 인공 신경망을 심층 신경망 Deep Neural Network DNN 라 한다. 딥러닝은 심층 신경망을 연구하는 분야며, 조금 더 일반적으로 연산이 연속하여 길게 연결된 모델을 연구한다.
다층 퍼셉틀노을 훈련할 방법 중 역전파 backpropagation 훈련 알고리즘은 효율적인 기법으로 그레이디언트를 자동으로 계산하는 경사 하강법이다. 네트워크를 두 번 통과하는 것만으로 이 역전파 알고리즘은 모든 모델 파라미터에 대한 네트워크 오차의 그레이디언트를 계산할 수 있다. 다른 말로 오차를 감소시키기 위해 각 연결 가중치와 편향값이 어떻게 바뀌어야 할 지 알 수 있다.
그레이디언트를 구하고 나면 평범한 경사 하강법을 수행한다. 전체 과정은 네트워크가 어떤 해결책으로 수렴될 때까지 반복한다.
자동으로 그레이디언트를 계산하는 것을 자동 미분 automatic diffrenentiation 이라 부른다. 역전파에서 사용하는 기법은 후진 모드 자동 미분 reverse-mode autodiff 이 기법은 빠르고 정확하며 미분할 함수가 변수(연결 가중치)가 많고 출력이 적은 경우 잘 맞는다.
예를 들어 한 번에 하나의 미니배치씩 진행하여 전체 훈련 세트를 처리한다. 이 과정을 여러 번 반복한다. 각 반복을 에포크 epoch 라고 부른다.
각 미니배치는 네트워크의 입력층으로 전달되어 첫 번째 은닉층으로 보내진다. 그 다음 해당 층에 있는 모든 뉴런의 출력을 계산한다. 이 결과는 다음 층으로 전달된다. 이런식으로 마지막 층인 출력층의 출력을 계산할 때까지 계속된다. 이것이 정방향 계산 forward pass 이다. 역방향 계산을 위해 중간 계산값을 모두 저장하는 것 외에는 예측을 만드는 것과 정확히 같다.
그 다음 알고리즘이 네트워크의 출력 오차를 측정한다. (즉, 손실 함수를 사용하여 기대하는 출력과 네트워크의 실제 출력을 비교하고 오차 측정 값을 반환한다.)
이제 각 출력 연결이 이 오차에 기여하는 정도를 계산한다. 연쇄 법칙 chain rule 을 적용하면 이 단계를 빠르고 정확하게 수행할 수 있다.
이 알고리즘은 또 다시 연쇄 법칙을 사용하여 이전 층의 연결 가중치가 이 오차의 기여 정도에 얼마나 기여했는지 측정한다. 이렇게 입력층에 도달할 때까지 역방향으로 계속된다. 앞서 설명한 것처럼 이런 역방향 단계는 오차 그레이디언트를 거꾸로 전파함으로써 효율적으로 네트워크에 있는 모든 연결 가중치에 대한 오차 그레이디언트를 측정한다.
마지막으로 알고리즘은 경사 하강법을 수행하여 방금 계산한 오차 그레이디언트를 사용해 네트워크에 있는 모든 연결 가중치를 수정한다.
다시 한 번 요약하자면 각 훈련 샘플에 대해 역전파 알고리즘이 먼저 예측을 만들고 정방향 계산 오차를 측정한다. 그런 다음 역방향으로 각 층을 거치면서 각 연결이 오차에 기여한 정도를 계산한다 역방향 계산, 마지막으로 이 오차가 감소하도록 가중치를 조정한다.
은닉층의 연결 가중치를 랜덤하게 초기화하는 것이 중요하다. 그렇지 않으면 모델이 마치 뉴런이 하나인 것처럼 작동할 것이다. 가중치를 랜덤하게 초기화하여 대칭성을 깨트리고, 역전파가 전체 뉴런을 다양하게 훈련할 수 있도록한다.
위 알고리즘을 잘 작동하고나 논문 저자들은 다층 퍼셉트론 구조에 중요한 변화를 주었다. 계단 함수를 로지스틱 함수로 바꾼 것이다. 계단 함수는 수평선밖에 없어 계산할 그레이디언트가 없다, 반면 로지스틱 함수는 어디서든지 0이 아닌 그레이디언트가 잘 정의되어 있다. 로지스틱 함수뿐만 아니라 다른 활성화 함수와도 사용될 수 있다.
- 하이퍼볼릭 탄젠트 함수 (쌍곡 탄젠트 함수) : -1 ~ 1사이의 출력
- ReLU 함수 : 출력에 최댓값이 없다는 점에 경사 하강법에 있는 문제를 일부 완화해준다. 출력이 0보다 작을 경우 도함수는 0이다.
활성화 함수와 해당 도함수의 그래프, 왜 활성화 함수가 필요할까? 선형 변환을 여러 개 연결해도 얻을 수 있는 것은 선형 변환뿐이다. 따라서 층 사이에 비선형성을 추가하지 않으면 하나의 층과 동일하다.
반대로 비선형 활성화 함수가 있는 충분히 큰 심층 신경망은 이론적으로 어떤 연속 함수도 근사할 수 있다.
10.1.5 회귀를 위한 다층 퍼셉트론
다층 퍼셉트론은 회귀 작업에 사용할 수 있다. 값 하나를 예측하는 데 출력 뉴런이 하나만 필요하다. 이 뉴런의 출력이 예측된 값이다. 다변량 회귀에서는 출력 차원마다 출력 뉴런이 하나씩 필요하다.
일반적으로 회귀용 다층 퍼셉트론을 만들 때 출력 뉴런에 활성화 함수를 사용하지 않고 어떤 범위의 값도 출력되도록 한다. 출력이 양수여야 한다면 출력층에 ReLU 활성화 함수를 사용할 수 있다.
마지막으로 어떤 범위 안의 값을 예측하고 싶다면 로지스틱 함수나 하이퍼볼릭 탄젠트 함수를 사용하고 레이블의 스케일을 적절한 범위로 조정할 수 있다.
훈련에 사용하는 손실 함수는 전형적으로 평균 제곱 오차이다. 훈련 세트에 이상치가 많을경우 평균 절댓값 오차를 사용할 수 있거나 둘을 조합한 후버 손실을 사용할 수 있다.
10.1.6 분류를 위한 다층 퍼셉트론
다층 퍼셉트론은 분류 작업에도 사용할 수 있다. 이진 분류 문제에서는 로지스틱 활성화 함수를 가진 하나의 출력 뉴런만 필요하다. 출력은 0과 1 사이의 실수이다. 이를 양성 클래스에 대한 예측 확률로 해석할 수 있다.
다층 퍼셉트론은 다중 레이블 이진 분류 문제를 쉽게 처리할 수 있다. 각 레이블에 해당하는 출력 뉴런을 하나씩 둘 수 있다. 각 출력 뉴런의 합은 1이 될 필요는 없다.
각 샘플이 3개 이상의 클래스 중 한 클래스에만 속할 수 있다면, 클래스마다 하나의 출력 뉴런이 필요하고 출력층에는 소프트맥스 활성화 함수를 사용한다. 모든 예측 확률을 0과 1 사이로 만들고 그 합이 1이 되도록 만든다. 이를 다중 분류 라고 한다.
확률 분폴르 예측해야 하므로 손실 함수에는 일반적으로 크로스 엔트로피 손실을 선택하는 것이 좋다.
10.2 케라스로 다층 퍼셉트론 구현하기
10.2.2 시퀀셜 API를 사용하여 이미지 분류기 만들기
시퀀셜 API를 사용하여 모델 만들기
model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28,28]))
model.add(keras.layers.Dense(300, activation="relu"))
model.add(keras.layers.Dense(100, activation="relu"))
model.add(keras.layers.Dense(10, activation="softmax"))
- 첫 번째 라인은 Sequential 모델 생성, 이 모델은 가장 간단한 케라스 신경망 모델 순서대로 연결된 층을 일렬로 쌓아서 구성한다. 이를 시퀀셜 Sequential API 라고 부른다.
- 그 다음 첫 번재 층을 만들고 모델에 추가한다. Flatten 층은 입력 이미지를 1D 배열로 변환한다. 즉, 입력 데이터 X를 받으면 X.reshape(-1,28*28)을 계산한다. 이 층은 어떤 모델 파라미터도 가지지 않고 전처리를 수행한다. 모델의 첫 번재 층이므로 input_shape를 지정해야 한다. 여기에는 배치 크기를 제외하고 샘플의 크기만 써야 한다.
- 그 다음 뉴런 300 개를 가진 Dense 은닉층 추가, ReLU 활성화 함수 사용, Dense 층마다 각자 가중치 행렬을 관리, 이 행렬에는 뉴런의 입력 사이의 모든 연결 가중치가 포함, 또한 편향도 벡터로 관리
- 다음 뉴런 100개를 가진 두 번째 Dense 은닉층을 추가 ReLU 활성화 함수를 사용한다.
- 마지막으로 클래스마다 하나씩 10개의 츄런을 가진 Dense 출력층 추가 소프트맥스 활성화 함수 사용
층을 하나씩 추가하지 않고 Sequential 모델을 만들 때 층의 리스트를 전달할 수 있다.
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dense(300, activation="relu"),
keras.layers.Dense(100, activation="relu"),
keras.layers.Dense(10, activation="softmax")
])
model.summary()
>>>
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
flatten_1 (Flatten) (None, 784) 0
dense_3 (Dense) (None, 300) 235500
dense_4 (Dense) (None, 100) 30100
dense_5 (Dense) (None, 10) 1010
=================================================================
Total params: 266,610
Trainable params: 266,610
Non-trainable params: 0
_________________________________________________________________
summary() 메서드는 모델에 있는 모든 층을 출력한다. 각 층의 이름, 출력 크기, 파라미터 개수가 함께 출력된다. 마지막에 훈련되는 파라미터와 훈련되지 않은 파라미터를 포함하여 전체 파라미터 개수를 출력한다.
Dense 층은 보통 많은 파라미터를 가진다. 첫 번째 은닉층은 784*300 개의 연결 가중치와 300개의 편향을 가진다. 이런 모델은 훈련 데이터를 학습하기 충분한 유연성을 가진다. 또한 과대적합의 위험을 갖는다는 의미이기도 한다.
모델에 있는 층의 리스트를 출력하거 인덱스로 층을 쉽게 선택할 수 있다. 또는 이름으로 층을 선택할 수도 있다.
층의 모든 파라미터는 get_weights() 메서드와 set_weights() 메서드를 사용해 접근할 수 있다. Dense 층의 경우 연결 가중치와 편향이 모두 포함되어 있다.
weights, biases = hidden1.get_weights()
weights
>>>
array([[-0.03772997, -0.05319334, -0.0684712 , ..., 0.05835499,
-0.03910427, -0.04460123],
[-0.05711527, -0.01958616, -0.01586225, ..., 0.06197464,
0.02479436, -0.02260917],
[-0.03777295, -0.03179492, 0.01490857, ..., 0.04383795,
0.02836841, 0.02021407],
...,
[ 0.05032969, -0.03960088, -0.06531814, ..., -0.02436697,
-0.01531855, -0.02781194],
[-0.00873802, 0.00733584, -0.05125324, ..., -0.01713576,
0.02908567, 0.00303198],
[ 0.00017513, 0.03135047, -0.03680541, ..., -0.06795985,
0.02584897, 0.03389594]], dtype=float32)
Dense 층은 연결 가중치를 무작위로 초기화한다. 편향은 0으로 초기화한다.
가중치 행렬의 크기는 입력의 크기에 달려 있다. 이 때문에 Sequential 모델에 첫 번째 층을 추가할 때 input_shape 매개변수를 지정한 것이다. 하지만 입력 크기를 지정하지 않아도 케라스는 모델을 빌드하기 전까지 입력 크기를 기다릴 것이다. 모델 빌드는 실제 데이터를 주입할 때나 build() 메서드를 호출할 때 일어난다. 모델이 실제 빌드되기 전에 층이 가중치를 가지지 않으면 특정 작업을 수행할 수 없기 대문에 모델을 만들 때 입력 크기를 알고 있다면 지정하는 것이 좋다.
모델 컴파일
모델을 만들고 나서 compile() 메서드를호출하여 사용할 손실 함수와 옵티마이저 optimizer 를 지정해야 한다.
model.compile(loss="sparse_categorical_crossentropy",
optimizer="sgd",
metrics=["accuracy"])
레이블이 정수 하나로 이루어져있고, 클래스가 배타적이므로 sparse_categorical_crossentropy 손실을 사용한다. 만약 샘플마다 클래스별 타깃 확률을 가지고 있다면 categorical_crossentropy 손실을 사용해야 한다.
옵티마이저에 sgd를 지정하면 기본 확률적 경사 하강법 stochastic gradient descent 을 사용하여 모델을 훈련한다.
마지막으로 분류기이므로 훈련과 평가 시에 정확도를 측정하기 위해 accuracy로 지정한다.
모델 훈련과 평가
모델을 훈련하려면 간단하게 fit() 메서드를 호출한다.
history = model.fit(X_train, y_train, epochs=30, validation_data=(X_valid, y_valid))
>>>
Epoch 1/30
1719/1719 [==============================] - 5s 2ms/step - loss: 0.7237 - accuracy: 0.7644 - val_loss: 0.5207 - val_accuracy: 0.8234
Epoch 2/30
1719/1719 [==============================] - 3s 2ms/step - loss: 0.4843 - accuracy: 0.8318 - val_loss: 0.4345 - val_accuracy: 0.8538
입력 특성과 타깃 클래스, 훈련할 에포크 횟수(기본값이 1)를 전달한다. 검증 세트도 전달했다(이는 선택 사항).
케라스는 에포크가 끝날 때마다 검증 세트를 사용해서 손실과 추가적인 측정 지표를 계산한다.
훈련 에포크마다 케라스는 처리한 샘플 개수와 샘플마다 걸린 평균 훈련 시간, 훈련 세트와 검증 세트에 대한 손실과 정확도를 출력한다.
validation_data 매개변수에 검증 세트를 전달하는 대신 케라스가 검증에 사용할 훈련 세트의 비율을 지정할 수 있다. validation_split = 0.1 로 쓰면 검증에 데이터의 마지막 10%를 사용한다.
클래스의 편향으로 훈련 세트가 편증되어 있다면 fit 메서드를 호출할 때 class_weight 매개변수를 지정하는 것이 좋다. 적게 등장하는 클래스는 높은 가중치를 부여하고 많이 등장하는 클래스는 낮은 가중치를 부여한다. 케라스가 손실을 계산할 때 이 가중치를 사용한다. 샘플별로 가중치를 부여하고 싶다면 sample_weight 매개변수를 지정한다. ( 두 값이 모두 지정되면 케라스는 두 값을 곱하여 사용한다). validation_data 튜플의 세 번째 원소로 검증 세트에 대한 샘플별 가중치를 지정할 수도 있다.
fit() 메서드가 반환하는 History 객체에는 훈련 파라미터(history.params), 수행된 에포크 리스트(history.epoch)가 포함된다. 이 객체의 가장 중요한 속성은 에포크가 끝날 때마다 훈련 세트와 검증 세트에 대한 손실과 측정한 지표를 담은 딕셔너리(history.history)이다.
훈련하는 동안 훈련 정확도와 검증 정확도가 꾸준히 상승하는 것을 볼 수 있다. 바면 훈련 손실과 검증 손실은 감소한다.
또한 검증 곡선이 훈련 곡선과 가깝다. 크게 과대적합되지 않았다는 증거이다.
일반적으로 충분히 오래 훈련하면 훈련 세트의 성능이 검증 세트의 성능을 앞지른다. 검증 손실이 여전히 감소한다면 모델이 아직 완전히 수렴되지 않았다고 볼 수 있다. 훈련을 계속해서 진행해야 한다.
모델 튜닝 시 확인할 것은 학습률이다. 다른 옵티마이저의 테스트, 층 개수, 층에 있는 뉴런 개수, 은닉층이 사용하는 활성화 함수 같은 하이퍼파라미터를 튜닝한다.
모델의 검증 정확도가 만족스럽다면 모델을 상용 환경으로 배포하기 전에 테스트 세트로 모델을 평가하여 일반화 오차를 추정해야 한다. 이때 evaluate() 메서드를 사용한다.
model.evaluate(X_test, y_test)
>>>
313/313 [==============================] - 1s 3ms/step - loss: 0.3303 - accuracy: 0.8820
[0.33026599884033203, 0.8820000290870667]
검증 세트보다 테스트 세트에서 성능이 조금 낮은 것이 일반적이다.
모델을 사용해 예측 만들기
모델의 predict() 메서드를 사용해 새로운 샘플에 대해 예측을 만들 수 있다.
테스트 세트의 처음 3개 샘플을 사용
X_new = X_test[:3]
y_proba = model.predict(X_new)
y_proba.round(2)
>>>
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.02, 0. , 0.98],
[0. , 0. , 1. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 1. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ]],
dtype=float32)
각 샘플에 대해 클래스 별 확률을 모델이 추정했다.
가장 높은 확률을 가진 클래스에만 관심이 있다면 np.argmax(model.predict(X_new), axis=-1)를 사용
y_pred = np.argmax(model.predict(X_new), axis=-1)
y_pred
>>>
array([9, 2, 1])
10.2.3 시퀀셜 API를 사용하여 회귀용 다층 퍼셉트론 만들기
model = keras.models.Sequential([
keras.layers.Dense(30, activation="relu", input_shape=X_train.shape[1:]),
keras.layers.Dense(1)
])
model.compile(loss="mean_squared_error", optimizer="sgd")
history = model.fit(X_train, y_train, epochs=20, validation_data=(X_valid, y_valid))
mse_test = model.evaluate(X_test, y_test)
X_new = X_test[:3]
y_pred = model.predict(X_new)
이전의 방법과 차이점으로 출력층이 활성화 함수가 없는 하나의 뉴런을 가진다는 것과, 손실 함수로 평균 제곱 오차를 사용한다는 것이다.
10.2.4 함수형 API를 사용해 복잡한 모델 만들기
MLP의 경우 데이터에 있는 간단한 패턴이 연속된 변환으로 인해 왜곡될 수 있다. 이를 해결하기 위해 입력의 일부 또는 전체를 출력층에 바로 연결한다.
이런 신경망을 만들어 캘리포니아 주택 문제를 해결한다.
input_ = keras.layers.Input(shape=X_train.shape[1:])
hidden1 = keras.layers.Dense(30, activation="relu")(input_)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.Concatenate()([input_, hidden2])
output = keras.layers.Dense(1)(concat)
model = keras.Model(inputs=[input_], outputs=[output])
- 먼저 input 객체 생성 이 객체는 shape와 dtype를 포함하여 모델의 입력을 정의한다. 한 모델은 여러 개의 입력을 가질 수 있다.
- 30개의 뉴런과 ReLU 활성화 함수를 가진 Dense층을 만든다. 이 층은 만들어지자마자 입력과 함께 함수처럼 호출된다. 이를 함수형 API라고 부르는 이유이다. 케라스에 층이 연결될 방법을 알려주었을 뿐 아직 어떤 데이터도 처리하지 않았다.
- 파이썬에는 객체를 함수처럼 호출하면 실행되는 특수한 __call__() 메서드가 있다. 이 메서드에서 build() 메서드를 호출하여 층의 가중치를 생성한다.
- 두 번째 은닉층을 만들고 함수처럼 호출한다. 첫 번째 층의 출력을 전달한다.
- Concatenate 층을 만들고 함수처럼 호출하여 두 번째 은닉층의 출력과 입력을 연결한다. karas.layers.concatenate() 함수를 사용할 수도 있다. 이 함수는 Concatenate 층을 만들고 주어진 입력으로 바로 호출한다.
- 하나의 뉴런과 활성화 함수가 없는 출력층을 만들고 concatenate 층이 만든 결과를 사용해 호출한다.
- 사용할 입려과 출력을 지정하여 케라스 모델을 만든다.
만약 일부 특성은 짧은 경로로 전달하고 다른 특성들은 깊은 경로로 전달하고 싶다면 어떻게 해야 할까? 이런 경우 한 가지 방법은 여러 입력을 사용하는 것,
예를 들어 5개 특성을 짧은 경로로 보내고 6개 특성은 깊은 경로로 보낸다고 가정
input_A = keras.layers.Input(shape=[5], name="wide_input")
input_B = keras.layers.Input(shape=[6], name="deep_input")
hidden1 = keras.layers.Dense(30, activation="relu")(input_B)
hidden2 = keras.layers.Dense(30, activation="relu")(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
output = keras.layers.Dense(1, name="output")(concat)
model = keras.Model(inputs=[input_A, input_B], outputs=[output])
위 코드로 이해가 가능하다. 이렇게 모델이 복잡해질 경우 중요한 층에는 이름을 붙이는 것이 좋다.
모델을 만들 때 inputs=[input_A, input_B]와 같이 지정, 모델 컴파일은 동일하지만, fit() 메서드를 호출할 때 하나의 입력 행렬 X_train을 전달하는 것이 아닌 입력마다 하나씩 행렬의 튜플(X_train_A, X_train_B) 을 전달해야 한다.
model.compile(loss="mse", optimizer=keras.optimizers.SGD(lr=1e-3))
X_train_A, X_train_B = X_train[:,:5], X_train[:,2:]
X_valid_A, X_valid_B = X_valid[:,:5], X_valid[:,2:]
X_test_A, X_test_B = X_test[:,:5], X_test[:,2:]
X_new_A, X_new_B = X_test_A[:3], X_test_B[:3]
history = model.fit((X_train_A, X_train_B), y_train, epochs=20, validation_data=((X_valid_A, X_valid_B), y_valid))
mse_test = model.evaluate((X_test_A, X_test_B), y_test)
y_pred = model.predict((X_new_A, X_new_B))
여러 개의 출력이 필요한 경우는 많다.
- 여러 출력이 필요한 작업일 때, 그림에 있는 주요 물체를 분류하고 위치를 알아야 할 때, 회귀와 분류를 해야할 때
- 동일한 데이터에서 독립적인 여러 작업을 수행할 때, 보통 작업마다 하나의 출력을 가진 단일 신경망을 훈련하는 것이 보통 더 나은 결과를 낸다. 신경망이 여러 작업에 걸쳐 유용한 특성을 학습할 수 있기 때문이다. 예를 들어 얼굴 사진으로 다중 작업 분류를 수행할 수 있다.
- 규제 기법으로 사용하는 경우 보조 출력을 추가해 하위 네트워크가 나머지 네트워크에 의존하지 않고 그 자체로 유용한 것을 학습하는지 확인 가능
보조 출력을 추가하는 것은 매우 쉽다. 적절한 층에 연결하고 모델의 출력 리스트에 추가하면 된다. 다음 코드는 아래의 네트워크를 만든다.
output = keras.layers.Dense(1, name="main_output")(concat)
aux_output = keras.layers.Dense(1, name="aux_outpuy")(hidden2)
model = keras.Model(inputs=[input_A, input_B], outputs=[output, aux_output])
각 출력은 자신만의 손실 함수가 필요하다. 따라서 모델을 컴파일할 때 손실의 리스트를 전달해야 한다. 기본적으로 케라스는 나열된 손실을 모두 더하여 최종 손실을 구해 훈련에 사용한다. 보조 출력보다 주 출력에 더 관심이 많다면 주 출력의 손실에 더 많은 가중치를 부여해야 한다. 모델을 컴파일 할 때 손실 가중치를 지정할 수 있다.
model.compile(loss=["mse", "mse"], loss_weights=[0.9, 0.1], optimizer="sgd")
이제 모델을 훈련할 때 각 출력에 대한 레이블을 제공해야 한다.
history = model.fit([X_train_A, X_train_B],[y_train, y_train], epochs=20, validation_data=([X_valid_A, X_valid_B], [y_valid, y_valid]))
total_loss, main_loss, aux_loss = model.evaluate([X_test_A, X_test_B], [y_test, y_test])
y_pred_main, y_pred_aux = model.predict([X_new_A, X_new_B])
모델을 평가하면 케라스는 개별 손실과 함께 총 손실을 반환한다.
비슷하게 predict() 메서드는 각 출력에 대한 예측을 반환한다.
10.2.5 서브클래싱 API로 동적 모델 만들기
시퀀셜 API와 함수형 API 모두 선언적 declarative 이다. 이 방식은 모델을 저장하거나 복사, 공유하기 쉽고, 모델의 구조를 출력하거나 분석하기 좋다. 프레임워크가 크기를 짐작하고 타입을 확인하여 에러를 일찍 발견할 수 있다. 하지만 어떤 모델은 반복문을 포함하고 다양한 크기를 다뤄야 하며 조건문을 가지는 등 여러 가지 동적인 구조를 필요로 한다. 조금 더 명령형 imperative 프로그래밍 스타일이 필요하면 서브 클래싱 subclassing API가 좋다.
간단히 Model 클래스를 상속한 다음 생성자 안에서 필요한 층을 만든다. 그다음 call 메서드 안에 수행하려는 연산을 기술한다. 다음 WideAndDeepModel 클래스의 인스턴스는 앞서 함수형 API로 만든 모델과 동일한 기능을 수행한다.
이 인스턴스를 사용해 모델 컴파일, 훈련, 평가, 예측을 수행할 수 있다.
class WideAndDeepModel(keras.Model):
def __init__(self, units=30, activation="relu", **kwargs):
super().__init__(**kwargs)
self.hidden1 = keras.layers.Dense(units, activation=activation)
self.hidden2 = keras.layers.Dense(units, activation=activation)
self.main_output = keras.layers.Dense(1)
self.aux_output = keras.layers.Dense(1)
def call(self, inputs):
input_A, input_B = inputs
hidden1 = self.hidden1(input_B)
hidden2 = self.hidden2(hidden1)
concat = keras.layers.concatenate([input_A, hidden2])
main_output = self.main_output(concat)
aux_output = self.aux_output(hidden2)
return main_output, aux_output
model = WideAndDeepModel()
이 예제는 함수형 API와 매우 비슷하지만 Input 클래스의 객체를 만들 필요가 없다. 대신 call() 메서드의 input 매개변수를 사용한다. 생성자에 있는 층 구성과 call() 메서드에 있는 정방향 계산을 분리했다. 주된 차이점은 call() 메서드 안에서 원하는 어떤 계산도 사용할 수 있다. for, if, 텐서플로 저수준 연산을 사용할 수 있다.
유연성이 높아지면 그에 따른 비용이 발생, 모델 구조가 call() 메서드 안에 숨겨져 있기 때문에 케라스가 쉽게 이를 분석할 수 없다. 즉 모델을 저장하거나 복사할 수 없다. 또한 케라스가 타입과 크기를 미리 확인할 수 없어 실수가 발생하기 쉽다.
10.2.6 모델 저장과 복원
model.save("my_keras_model.h5")
model = keras.models.load_model("my_keras_model.h5")
케라스는 HDF5 포맷을 사용하여 모델 구조와 층의 모든 모델 파라미터를 저장한다. 또한 옵티마이저도 저장한다.
모델 로드도 위와 같이 간단하다.
매우 큰 훈련의 경우 훈련 도중 일정 간격으로 체크포인트를 저장하기 위해 콜백을 사용한다.
모델 서브클래싱에서는 위와 같은 방식을 사용할 수 없다. 이 경우 save_weights() 와 save_weights() 메서드를 사용하여 모델 파라미터를 저장하고 복원
10.2.7 콜백 사용하기
fit() 메서드의 callbacks 매개변수를 사용하여 케라스의 훈련의 시작이나 끝에 호출할 객체 리스트를 지정할 수 있다. 또는 에포크의 시작이나 끝, 각 배치 처리 전후에 호출할 수도 있다.
예를 들어 ModelCheckpoint 는 훈련하는 동안 일정한 간격으로 모델의 체크포인트를 저장한다. 기본적으로 매 에포크의 끝에서 호출된다.
checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5")
history = model.fit(X_train, y_train, epochs=10, callbacks=[checkpoint_cb])
훈련하는 동안 검증 세트를 사용하면 ModelCheckpoint 를 만들 때 save_best_only=True 로 지정할 수 있다. 이렇게 하면 최상의 검증 세트 점수에서만 모델을 저장한다.
다음 코드는 조기 종료를 구현하는 방법이다.
checkpoint_cb = keras.callbacks.ModelCheckpoint("my_keras_model.h5", save_best_only=True)
history = model.fit(X_train, y_train, epochs=10, validation_data=(X_valid, y_valid), callbacks=[checkpoint_cb])
model = keras.models.load_model("my_keras_model_h5")
조기 종료를 구현하는 또 다른 방법은 EarlyStopping 콜백을 사용하는 것이다. 일정 에포크(patience) 동안 검증 세트에 대한 점수가 향상되지 않으면 훈련을 멈춘다. 선택적으로 최상의 모델을 복원할 수도 있다. 체크포인트 저장 콜백과 진전이 없는 경우 훈련을 일찍 멈추는 콜백을 함께 사용할 수 있다.
early_stopping_cb = keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=100, validation_data=(X_valid, y_valid), callbacks=[checkpoint_cb, early_stopping_ch])
모델이 향상되지 않으면 훈련이 자동으로 중지되기 때문에 에포크의 숫자를 크게 지정해도 된다.
더 많은 제어를위해 사용자 정의 콜백을 만들 수 있다. 다음과 같은 사용자 정의 콜백은 훈련하는 동안 검증 손실과 훈련 손실의 비율을 출력한다.
class PrintValTrainRatioCallback(keras.callbacks.Callback):
def on_epoch_end(self, epoch, logs):
print("\nval/train: {:.2f".format(logs["val_loss"] / logs["loss"]))
10.3 신경망 하이퍼파라미터 튜닝하기
신경망의 유연성은 단점이기도 하다. 조정할 하이퍼파라미터가 많기 때문이다. 어떤 하이퍼파라미터 조합이 주어진 문제에 최적일지 알 수 있을까?
한 가지 방법은 많은 하이퍼파라미터 조합을 시도해보고 어떤 것이 검증 세트에서 가장 좋은 점수를 내는지 확인하는 것이다.
예를 들어 GridSearchCV 나 RandomizedSearchCV를 사용해 하이퍼파라미터 공간을 탐색할 수 있다. 이렇게 하기 위해서는 케라스 모델을 사이킷런 추정기처럼 보이도록 바꾸어야 한다. 먼저 일련의 하이퍼파라미터로 케라스 모델을 만들고 컴파일하는 함수를 만든다.
def build_model(n_hidden=1, n_neurons=30, learning_rate=3e-3, input_shape=[8]):
model = keras.models.Sequential()
model.add(keras.layers.InputLayer(input_shape=input_shape))
for layer in range(n_hidden):
model.add(keras.layers.Dense(n_neurons, activation="relu"))
model.add(keras.layers.Dense(1))
optimizer = keras.optimizers.SGD(learning_rate=learning_rate)
model.compile(loss="mse", optimizer=optimizer)
return model
이 함수는 주어진 입력 크기와 은닉층 개수, 뉴런 개수로 단변량 회귀를 하는 간단한 Sequential 모델을 만든다. 그리고 지정된 학습률을 사용하는 SGD 옵티마이저로 모델을 컴파일한다. 적절한 기본값을 설정하는 것이 좋다.
build_model() 함수를 사용해 KerasRegressor 클래스의 객체를 만든다.
keras_reg = keras.wrappers.scikit_learn.KerasRegressor(build_model)
KerasRegressor 객체는 build_model() 함수로 만들어진 케라스 모델을 감싸는 간단한 래퍼이다. 이 객체를 만들 때 어떤 하이퍼파라미터도 지정하지 않았으므로 build_model() 에 정의된 기본 하이퍼파라미터를 사용할 것이다.
이제 일반적인 사이킷런 회귀 추정기처럼 이 객체를 사용할 수 있다. fit 메서드로 훈련하고, score 메서드로 평가하고, predict 메서드로 예측을 만들 수 있다.
keras_reg.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
mse_test = keras_reg.score(X_test, y_test)
y_pred = keras_reg.predict(X_new)
fit() 메서드에 지정한 모든 매개변수는 케라스 모델로 전달된다.
모델 하나를 훈련하고 평가하려는 것이 아니라 수백 개의 모델을 훈련하고 검증 세트에서 최상의 모델을 선택해야 한다. 하이퍼파라미터가 많으므로 랜덤 탐색을 사용하는 것이 좋다. 은닉층 개수, 뉴런 개수, 학습률을 사용해 하이퍼파라미터 탐색을 수행한다.
from scipy.stats import reciprocal
from sklearn.model_selection import RandomizedSearchCV
param_distribs = {
"n_hidden": [0, 1, 2, 3],
"n_neurons": np.arange(1, 100).tolist(),
"learning_rate": reciprocal(3e-4, 3e-2).rvs(1000).tolist(),
}
rnd_search_cv = RandomizedSearchCV(keras_reg, param_distribs, n_iter=10, cv=3, verbose=2)
rnd_search_cv.fit(X_train, y_train, epochs=100,
validation_data=(X_valid, y_valid),
callbacks=[keras.callbacks.EarlyStopping(patience=10)])
이 코드는 래퍼로 감싸진 케라스 모델에 전달할 추가적인 매개변수가 fit()메서드에 있는 것을 제외하고는 동일하다.
실행이 끝나면 최상의 하이퍼파라미터와 훈련된 케라스 모델을 얻을 수 있다.
rnd_search_cv.best_params_
>>>
{'learning_rate': 0.005803602934201024, 'n_hidden': 3, 'n_neurons': 74}
rnd_search_cv.best_score_
>>>
-0.3189570407072703
rnd_search_cv.best_estimator_
>>>
<keras.wrappers.scikit_learn.KerasRegressor at 0x7fe0620abd90>
rnd_search_cv.score(X_test, y_test)
>>>
162/162 [==============================] - 0s 1ms/step - loss: 0.3022
-0.30217668414115906
10.3.1 은닉층 개수
은닉층 하나로 시작해도 납득할 만한 결과를 얻을 수 있다.
계층 구조는 심층 신경망이 좋은 솔루션으로 빨리 수렴하게끔 도와줄 뿐만 아니라 새로운 데이터에 일반화되는 능력도 향상시켜준다. 새로운 신경망에서 처음 몇 개 층의 가중치와 편향을 난수로 초기화하는 대신 첫 번째 신경망 층에 있는 가중치와 편향값으로 초기화할 수 있다. 이를 전이 학습 transfer learning 이라고 한다.
비슷한 작업에서 뛰어난 성능을 낸 미리 훈련된 네트워크 일부를 재사용하는 것이 일반적이다. 훈련 속도는 훨씬 빠르고 데이터도 훨씬 적게 필요하다.
10.3.2 은닉층의 뉴런 개수
은닉층의 구성 방식은 일반적으로 각 층의 뉴런을 점점 줄여서 깔때기처럼 구성한다. 저수준의 많은 특성이 고수준의 적은 특성으로 합쳐질 수 있기 때문이다.
층의 개수와 마찬가지로 네트워크가 과대적합이 시작되지 전까지 점진적으로 뉴런 수를 늘릴 수 있다 하지만 실전에서는 필요한 것보다 더 많은 층과 뉴런을 가진 모델을 선택하고 , 그런 다음 과대적합되지 않도록 조기 종료나 규제 기법을 사용하는 것이 간단하고 효과적이다
10.3.3 학습률, 배치 크기 그리고 다른 하이퍼파라미터
학습률
가장 중요한 하이퍼파라미터, 학습률에 대한 손실을 그래프로 그리면 손실이 줄어드는 것이 보인다. 다시 손실이 커지기 전의 지점
옵티마이저
고전적인 평범한 미니배치 경사 하강법보다 더 좋은 옵티마이저를 선택한다.
배치 크기
모델 성능과 훈련 시간에 큰 영향을 준다. 큰 배치를 사용하면 하드웨어 가속기를 효율적으로 사용할 수 있다는 점,
실전에서 큰 배치를 사용하면 불안정하게 훈련되고 일반화 성능을 내지 못 할 수 있다. 훈련이 불안정하거나 최종 성능이 만족스럽지 못하면 작은 배치를 사용해 볼 수 있다.
활성화 함수
일반적으로 ReLU 활성화 함수가 모든 은닉층에 좋은 기본값이다.
반복 횟수
대부분의 경우 조기 종료의 사용
'책 > Hands-On Machine Learning' 카테고리의 다른 글
12. 텐서플로를 사용한 사용자 정의 모델과 훈련 (0) | 2022.10.09 |
---|---|
11.심층 신경망 훈련하기 (0) | 2022.10.08 |
9. 비지도 학습 (1) | 2022.10.08 |
8. 차원 축소 (0) | 2022.10.07 |
7. 앙상블 학습과 랜덤 포레스트 (1) | 2022.10.07 |