TensorFlow <-> Kears 관계와 적용 방식
작성자 : 박정현
케라스와 텐서플로 소개
이번 글은 조금 중요성을 인식하고 읽기 바란다. 딥러닝을 시작하는 데 필요한 거의 대부분의 내용을 담고 있다.
케라스와 텐서플로 소개 후 딥러닝 작업 환경 설정, 신경망의 핵심 구성 요소를 리뷰 후 텐서플로 API로 변환되는지 작성되어 있다.
텐서플로
TensorFlow 는 구글에서 만든 파이썬 기반의 무료 오픈소스 머신 러닝 플랫폼이다. Numpy와 매우 유사하게 텐서플로의 핵심은 Tensor에 대한 수학적 표현을 적용할 수 있도록 하는 것이다. 하지만 텐서플로는 넘파이의 기능을 넘어선다.
미분 가능한 어떤 표현에 대해 자동으로 그레이디언트를 계산할 수 있으므로 머신 러닝에 매우 적합하다. 또한 GPU, TPU(텐서 처리 장치라고 불림)에서 병렬 가속을 수행할 수 있다.(사이킷 런, 넘파이는 x) 뿐만아니라 텐서플로에서 정의한 계산은 여러 머신에 쉽게 분산시킬 수 있으며 C++이나 JS 등과 같은 런타임에 맞게 변환할 수 있다.
이러한 이유로 텐서플로 애플리케이션을 실전 환경에 쉽게 배포할 수 있다.
텐서플로는 하나의 라이브러리 그 이상이라는 것을 유념하는 것이 중요하다. 단지 하나의 플랫폼이고 구글과 서드파티에서 개발하는 많은 구성 요소로 이루어진 거대한 것이다.
서드파티 : 제3자를 의미함.
IT업계에서 서드파티는 어떠한 분야에서 처음 개발하거나 원천기술을 가지고 있는 게 아닌, 원천기술과 호환되는 상품을 출시하거나 해당 기술을 이용한 파생상품을 생산하는 회사
케라스
케라스는 텐서플로 위에 구축된 파이썬용 딥러닝 API로 어떤 종류의 딥러닝 모델도 쉽게 만들고 훈련할 수 있는 방법을 제공 즉, 텐서플로위에 케라스가 있다.
케라스와 텐서플로 : 텐서플로는 저수준 텐서 컴퓨팅 플랫폼이고 케라스는 고수준 딥러닝 API다.
텐서플로는 텐서 연산을 지원하는 연산 층, 케라스는 딥러닝을 설계하는 층 이라고 생각
케라스는 개발자 경험을 중요하게 생각한다. 아래의 이유 때문에 초보자도 배우기 쉬우면서 전문가가 사용하기에 생산성이 높다.
- 일관되고 간단한 워크플로를 제공
- 작업의 횟수를 최소화
- 에러에 대해 명확하고 실행 가능한 피드백을 제공
- 다양한 사용자 층으로 표준 방식을 따르도록 강요하지 않음
이러한 케라스의 철학은 파이썬의 철학과 유사하다. 어떤 언어는 프로그램을 작성하는 데 한 가지 방법만 제공한다. JAVA가 객체지향 프로그래밍인 것
하지만, 파이썬은 멀티패러다임 언어다. 즉, 여러 사용 방식을 제공하고 섞어서 사용해도 잘 작동한다.(객체지향, 함수형 프로그래밍 모두 가능)
케라스와 텐서플로의 간략한 역사
케라스는 원래 Theano를 위한 라이브러리였다. 텐서플로가 릴리스된 후 **케라스는 멀티백엔드 구조로 리팩터링되었다.
백엔드 : 애플리케이션의 데이터 저장 및 리턴하는 컴포넌트
멀티백엔드 : 여러 백엔드로 구성되어 유연성, 호환성을 제공
ex. 여러 종류의 DB, 메시징 시스템, 검색 엔진과 연동
초창기 케라스의 멀티백엔드에는 여러 딥러닝 엔진이 있었다.(TensorFlow, Theano, CNTK) 때문에 멀티백엔드의 장점인 호환성, 유연성을 제공 했던 것이다. 즉, 동일한 딥러닝 코드를 여러 엔진에 적용할 수 있었다.
하지만 Theano, CNTK가 개발 중단되었고 다시 케라스는 텐서플로를 사용하는 단일 백엔드 API가 된것임.
딥러닝 작업 환경 설정
반드시 필요한 것은 아니지만 CPU보단 GPU, TPU 활용을 추천한다. 이유는 병렬처리로 인한 가속화에 있는데, 현존 가장 많은 CPU의 코어는 Ryzen의 EPYC 시리즈다.(96 코어) 하지만 GPU의 코어는 단위부터 틀리다.
최근에 출시된 NVIDIA RTX 4090은 CUDA코어가 16,384개 이다. 여러 코어를 동시에 사용해 병렬적으로 연산을 처리함에 있어서 GPU가 매우 좋은 성능을 가지는 것이 분명하다.
만약, 윈도우를 사용한다면 자신의 컴퓨터에서 딥러닝 작업을 하기 위해 우분투, WSL을 사용하는 것을 추천한다.
경험상 일부 라이브러리는 리눅스에서 동작하는 것이 있었으며, 윈도우 차별함
여담으로 나는 데이터 분석를 위한 IDE로 DataSpell을 사용하고 있다. 이외, Vscode, Jupyter notebook, Colab이 있다.
텐서플로 시작하기
여기부터 하이라이트 시작이다.
지난 스터디에서 신경망 훈련은 다음과 같은 개념을 중심으로 진행되는 것을 학습했다.
- 텐서플로 API를 통해서 텐서 연산 수행
- 케라스 API를 통해서 Layer 구성 등 모델 설계
상수 텐서와 변수
텐서플로에서 어떤 작업을 하려면 텐서가 필요하다. 텐서를 만들기 위해서는 초기값이 필요하다(즉, 비어있는 것은 x) 예시로 모두 1이거나 0인 텐서를 만들거나 랜덤 분포에서 뽑은 값으로 만들 수 있다.
# 모두 1인 텐서
import tensorflow as tf
x = tf.ones(shape=(2, 1))
print(x)
tf.Tensor( [[1.] [1.]], shape=(2, 1), dtype=float32)
# 모두 2인 텐서
x = tf.zeros(shape=(2, 1))
print(x)
tf.Tensor( [[0.] [0.]], shape=(2, 1), dtype=float32)
# 랜덤 텐서
x = tf.random.normal(shape=(3, 1), mean=0., stddev=1.)
print(x)
tf.Tensor( [[ 0.6189793 ] [ 0.77359086] [-0.66645133]], shape=(3, 1), dtype=float32)
이러한 연산은 Numpy에서도 지원한다. 그럼 넘파이랑 다른게 뭐길래 텐서를 그렇게 강조할까?
넘파이 배열과 텐서플로 텐서 사이의 큰 차이점은 텐서플로 텐서에는 값을 할당할 수 없다는 것이다. 즉, 텐서는 상수다!
# 넘파이 값 할당
import numpy as np
x = np.ones(shape=(2, 2))
x[0, 0] = 0.
이런 작업을 텐서는 불가능하다. 대입연산 자체가 불가능
모델을 훈련하려면 모델의 상태(가중치, Bias) 즉, 텐서를 업데이트 해야한다. 모델의 상태는 Layer 마다 존재하고 학습하며 최정화 과정에서(옵티마이저) 갱신된다.
방금 텐서는 상수라면서 어떻게 값을 갱신할까?
tf.Variable 은 텐서플로에서 텐서를 변수로 관리하기 위한 클래스다. 변수를 만들기 위해 초깃값을 제공해야 한다.
# 텐서를 변수로!
v = tf.Variable(initial_value=tf.random.normal(shape=(3, 1)))
print(v)
<tf.Variable 'Variable:0' shape=(3, 1) dtype=float32, numpy= array([[-0.27607322], [-2.4914272 ], [ 1.2171313 ]], dtype=float32)>
변수의 상태는 다음과 같이 assign 메서드로 수정 가능하다.
v.assign(tf.ones((3, 1))) # 모든 요소가 1인 3 by 1 텐서
<tf.Variable 'UnreadVariable' shape=(3, 1) dtype=float32, numpy= array([[1.], [1.], [1.]], dtype=float32)>
변수의 일부 원소에만 적용 할 수 있으며, +=, -= 도 가능하다.
v[0, 0].assign(3.) # 일부에 적용
v.assign_add(tf.ones((3, 1))) # +=
v.assign_sub(tf.ones((3, 1))) # -=
정리하면 텐서는 상수다. tf.Variable를 통해 변수로 관리 하도록 설정 후 assign을 통해 값을 할당한다.
텐서 연산 : 텐서플로에서 수학 계산하기
넘파이와 마찬가지로 텐서플로더 수학 공식을 표현하기 위해 많은 연산을 제공함
a = tf.ones((2, 2)) # 텐서 생성
b = tf.square(a) # 제곱
c = tf.sqrt(a) # 제곱근
d = b + c # 원소별 덧셈 연산
e = tf.matmul(a, b) # 행렬곱
e *= d # 원소별 곱셈 연산
중요한 점은 앞의 연산이 모두 바로 실행된다는 것이다. 당연히 코드를 실행하면 바로 실행되는 것 아니야? 라고 생각할 수 있다.
이런 것을 즉시 실행 모드 라고 부른다. 이와 반대로 그래프 실행 모드가 존재한다.
아래에서 차이점을 확인해보라
# 즉시 실행 모드
import tensorflow as tf
x = tf.Variable(3.0)
assign_op = x.assign(4.0)
# 그래프 실행 모드
import tensorflow as tf
x = tf.Variable(3.0)
assign_op = x.assign(4.0)
with tf.session() as sess:
sess.run(tf.global_variables_initializer()) # 변수 초기화
sess.run(assign_op) # 변수에 새로운 값 할당
print(sess.run(x)) # 변수 값 출력 (4.0)
즉, 즉시 실행은 우리가 아는 일반적인 방식으로 바로 실행 가능하며, 그래프 실행 모드는 별도의 Session을 만들어서 실행해야한다.
GradientTape API
지금까지 봐서는 텐서플로가 넘파이랑 매우 비슷하게 보일 것인데, 지금 소개하는 것이 바로 매우 치명적인 차이점이다.
미분 가능한 표현이라면 어떤 입력에도 그레이디언트를 계산할 수 있다. 이것은 GradientTape 블록을 시작하고 하나 또는 여러 입력 텐서에 대해서 계산을 수행한 후 입력에 대해 결과의 그레이디언트를 구한다.
input_var = tf.Variable(initial_value=3.) # 텐서 -> 변수
with tf.GradientTape() as tape: # GradientTape() 블록에서 제곱연산 수행
result = tf.square(input_var)
gradient = tape.gradient(result, input_var)
gradient = tape.gradient(loss, weights)
와 같이 가중치에 대한 모델 손실의 그레이디언트를 계산하는 데 가장 널리 사용되는 방법이다
지금까지 tape.gradient()
의 입력 덴서가 텐서플로 변수인 경우만 보았다. 실제로 입력은 어떤 텐서라도 가능하다 하지만 텐서플로는 기본적으로 훈련 가능한 변수만 추적한다. 상수 텐서의 경우 tape.watch()
를 호출해서 추적을 수동으로 알려야한다.
# 상수 입력 텐서
input_const = tf.constant(3.) # 상수 텐서
with tf.GradientTape() as tape:
tape.watch(input_const)
result = tf.square(input_const)
gradient = tape.gradient(result, input_const)
이러한 것이 필요한 이유는 모든 텐서에 대한 모든 그레이디언트를 계산한다면 너무 많은 비용이 발생된다. 즉, 불필요한 연산을 줄이는 것에 목적이 있다.
모델의 상태는 가중치, 편향(bias)가 있으며 이것은 Layer 마다 존재
n차 미분을 지원하기 때문에 gradient tape는 강력한 도구라고 한다.
# 이차 미분 예시
time = tf.Variable(0.)
with tf.GradientTape() as outer_tape:
with tf.GradientTape() as inner_tape:
position = 4.9 * time ** 2
speed = inner_tape.gradient(position, time)
acceleration = outer_tape.gradient(speed, time)
엔드 투 엔드 예제 : 선형 분류기
선형적으로 잘 구분되는 합성 데이터를 만들어 본 뒤 분류를 해보자!
합성 데이터(Synthetic data) : 인공적으로 생성된 데이터임(센서 데이터, 공공데이터는 이것에 해당 안됨)
2D 평면의 포인트로 2개의 클래스를 가진다(x, y) 특정한 평균, 공분산 행렬을 가진 랜덤한 분포에서 좌표 값을 뽑아 각 클래스의 포인트를 생성하자.(x = 평균, y = 공분산 행렬 값)
직관적으로 생각하면 공분산 행렬은 포인트 클래우드의 형태를 결정하고, 평균은 평면에서의 위치를 나타낸다. 두 포인트 클라우드는 동일한 공분산 행렬을 사용하지만 평균 값은 다르게 사용한다. (같은 모양이지만 다른 위치에 있도록 하기 위함)
num_samples_per_class = 1000
negative_samples = np.random.multivariate_normal( # 1000개의 런덤한 2D 포인트 클라우드 생성
mean=[0, 3],
cov=[[1, 0.5],[0.5, 1]],
size=num_samples_per_class)
positive_samples = np.random.multivariate_normal( # 1000개의 런덤한 2D 포인트 클라우드 생성
mean=[3, 0],
cov=[[1, 0.5],[0.5, 1]],
size=num_samples_per_class)
각 샘플은 모두 (1000, 2) 크기의 배열이다(텐서로 치면 rank-2) 이것을 수직으로 연결하면 (2000, 2) 크기의 단일 배열을 획득할 수 있다.
inputs = np.vstack((negative_samples, positive_samples)).astype(np.float32) # vstack : 수직 연결
(2000, 1) 크기의 0 배열과 1 배열을 합쳐 타깃 레이블을 생성해 보겠다. input[i]
가 클래스 0에 속하면 target[i, 0]
은 0이다.(1에 속하면 target[i, 1]
)
targets = np.vstack((np.zeros((num_samples_per_class, 1), dtype="float32"),
np.ones((num_samples_per_class, 1), dtype="float32")))
이를 시각화 하면 아래와 같다.
이제 두 포인트 클라우드를 구분할 수 있는 선형 분류기를 만들어 보자! 이 선형 분류기는 하나의 아핀 변환 \(prediction = W \bullet input + b\)이며, 예측과 타깃 사이의 차이를 제곱한 값을 최소화 하도록 훈련된다. 즉, MSE 최소화를 말함.
각각 랜덤한 값과 0으로 초기화한 변수 W와 b를 만들어 보겠다.
input_dim = 2 # 입력 차원
output_dim = 1 # 출력 차원
W = tf.Variable(initial_value=tf.random.uniform(shape=(input_dim, output_dim)))
b = tf.Variable(initial_value=tf.zeros(shape=(output_dim,)))
forward pass 함수 정의
def model(inputs):
return tf.matmul(inputs, W) + b
이것이 의미하는 것을 그림으로 나타내면 아래와 같다.
이 선형 분류기는 2D 입력을 다루기 때문에 W는 2개의 스칼라 w1, w2를 갖는다.($$W = [[w_1], [w_2]]) 반면 b는 하나의 스칼라 값이다. 어떤 포인트가 주어지면 예측 값은 아래와 같다.
\[predoction = [[w_1], [w_2]] \bullet [x, y] + b = w_1 * x + w_2 * y + b\]다음은 손실함수다.
def square_loss(targets, predictions):
per_sample_losses = tf.square(targets - predictions) # 오차의 제곱
return tf.reduce_mean(per_sample_losses) # 오차의 제곱 평균 즉, MSE
다음은 손실을 최소화 하도록 가중치 W와 B를 업데이트 한다.
learning_rate = 0.1
def training_step(inputs, targets):
with tf.GradientTape() as tape: # 그레이디언트 테이프 블록 안의 정방향 패스
predictions = model(inputs) # 정방향 패스
loss = square_loss(targets, predictions) # 오차 제곱
grad_loss_wrt_W, grad_loss_wrt_b = tape.gradient(loss, [W, b]) # 가중치에 대한 손실의 그레이디언트 계산
W.assign_sub(grad_loss_wrt_W * learning_rate) # 업데이트
b.assign_sub(grad_loss_wrt_b * learning_rate) # 업데이트
return loss
간한단 예를 들기 위해 지금은 미니 배치 대신 배치 훈련을 했다. (샘플 수 작음)
배치 훈련 루프
for step in range(40): # 에포크 의미함
loss = training_step(inputs, targets)
print(f"{step}번째 스텝의 손실: {loss:.4f}")
이렇게 학습 후 결과를 보면 아래와 같다.
포인트에 대한 예측 값은 \(predoction = [[w_1], [w_2]] \bullet [x, y] + b = w_1 * x + w_2 * y + b\)임을 기억하자 따라서 클래스 0은 \(w_1 * x + w_2 * y + b < 0.5\)를 의미하고 클래스 1은 \(w_1 * x + w_2 * y + b > 0.5\)로 정의 가능하다. 우리가 찾고자 하는 것은 Decision Boundary이니까 \(w_1 * x + w_2 * y + b = 0.5\)가 된다.
동일한 형태로 앞의 방정식을 변경하면 \(y = - w_1/w_2 * x + (0.5 - b) / w_2\) 가 된다.
Decision Boundary를 그려보면?
신경망의 구조 : 핵심 Keras API 이해하기
여기서 말하는 내용은 앞으로 배울 내용의 튼튼한 기초가 된다. 딥러닝 모델을 구축하는 데 필요한 더 생산적이고 강력한 방법인 케라스 API를 소개한다.
층: 딥러닝의 구성요소
신경망의 기본 데이터 구조는 Layer이며, 하나 이상의 텐서를 입력으로 받고 하나 이상의 텐서를 출력하는 데이터 처리 모듈이다. 어떤 종류의 층은 상태가 없지만 대부분의 경우 가중치라는 상태를 갖는다 이는 SGD(확률적 경사하강법)으로 학습되는 하나 이상의 텐서이며 여기에 신경망이 학습한 지식이 담겨 있다.
단, 층마다 적절한 텐서 포맷과 데이터 처리 방식은 다르다. (Samples, features) 크기의 rank-2 텐서에 저장된 간단한 벡터 데이터는 Desns 층으로 처리하는 경우가 대부분이다. rank-3의 경우 LSTM같은 순환 신경망, 합성곱 층(CNN)을 이용한다. rank-4는 2D 합성곱 층으로 처리한다.
케라스에서 딥러닝 모델을 만드는 것은 호환되는 층을 서로 연결하여 유용한 데이터 변환 파이프라인을 구성하는 것이다.
케라스의 Layer 클래스에 대해 알아보자. 간단한 API는 모든 것이 중심에 모인 하나의 추상적인 모습을 가져야한다.(객체가 아닌, 클래스 형태) Layer는 상태와 연산을 캡슐화한 객체다.
가중치는 일반적으로 bulid()
메서드에서 정의하고 연산은 call()
메서드에서 정의한다.
from tensorflow import keras
class SimpleDense(keras.layers.Layer): # 모든 케라스 층은 Layer 상속
def __init__(self, units, activation=None):
super().__init__()
self.units = units # 출력 유닛 수
self.activation = activation # 활성 함수
def build(self, input_shape): # 가중치 생성
input_dim = input_shape[-1]
self.W = self.add_weight(shape=(input_dim, self.units),
initializer="random_normal")
self.b = self.add_weight(shape=(self.units,),
initializer="zeros")
def call(self, inputs): # 정방향 패스 연산 정의(즉, 입력 층을 연산 후 출력 층으로 만든다)
y = tf.matmul(inputs, self.W) + self.b
if self.activation is not None:
y = self.activation(y)
return y
이렇게 추상화를 후 인스턴스(객체)를 생성하면 텐서를 입력으로 받는 함수처럼 사용 가능함
my_dense = SimpleDense(units=32, activation=tf.nn.relu)
input_tensor = tf.ones(shape=(2, 784))
output_tensor = my_dense(input_tensor) # 일급 객체(고차함수)라서 가능한 짓거리 즉, 입력 텐서를 지정한거랑 다름 없음.
print(output_tensor.shape)
자동 크기 추론 : 동적으로 층 만들기
레고 블록처럼 구멍이 잘 맞는 것(호환되는 층만) 서로 연결 가능하다. 층 호환(Layer compatibility)개념 은 모든 층이 특정 크기의 입력 텐서만 받고, 특정 크기의 출력 텐서만 반환한다는 사실을 의미함.
from tensorflow.keras import layers
layer = layers.Dense(32, activation="relu")
이 층은 첫 번째 차원이 32인 텐서를 반환한다. 입력으로 32차원을 받는 층에 연결할 수 있다. 이러면 항상 사용자는 후속 층에 대해 고민해야함.
하지만 케라스는 이런 걱정이 없다 -> 동적으로 만들어지기 때문이지!
from tensorflow.keras import models
from tensorflow.keras import layers
model = models.Sequential([
layers.Dense(32, activation="relu"), # 출력 층 32, relu 사용
layers.Dense(32)
])
지난 스터디에서는 명시적으로 전달했음(대로 바보임)
model = NaiveSequential([
NaiveDense(input_size=28 * 28, output_size=512, activation=tf.nn.relu),
NaiveDense(input_size=512, output_size=10, activation=tf.nn.softmax)
])
만약에 출력을 만드는 층의 규칙이 복잡하면 문제는 더 심각해진다.
if input_size % 2 ==0:
input_size * 2
else:
input_szie * 3
동적으로 층을 만드는 기능이 없고 이런 규칙의 출력을 반환한다면? 어떻게 될 것같음?
내 생각엔 후속 층을 연결하는 것에 있어 매우 연산 복잡성이 증가할 것 같음
SimpleDense
클래스는 NaiveDense
처럼 생성자에서(__init__
에서) 가중치를 만들지 않는다. 대신 상태 생성을 위한 build()
에서 만든다. build()
는 층이 처음 호출될 때 __call__()
를 통해 자동으로 호출된다. 때문에 __call()__
이 아니라 별도로 call()
을 만든 것이다.
call() 사용 이유 정리
keras Layer를 Callbable 객체로 만들면 보다 정확하게는 call()이 내부적으로 호출함
call()은 부모 Layer에 감춰져 있기에 작성해줄 필요가 없으며, call()이 해주는 것은 build()를 호출한 뒤에 call()을 호출하는 역할임
__call__()
은 자동 크기 추론만 처리하는 것이 아니라 즉시 실행과 그래프 실행(7장) 사이를 전환, 입력 마스킹(11장)을 처리하는 등 많은 작업을 관리한다.
여기서 절대 잊지 말아야 하는 것은 사용자 정의 층을 구현할 때 call()
메서드에 정방향 패스 계산을 넣는 다는 것만 기억하자
층에서 모델로
딥러닝 모델은 층으로 구성된 그래프다. 케라스의 Model
클래스에 해당한다. 앞으로 다양한 종류의 네트워크를 볼 것임(논문….) 다음은 자주 등장하는 구조라고 함.
- 2개의 branch를 가진 네트워크
- 멀티헤드 네트워크
- 잔차 연결
네트워크 구조(topology)는 꽤 복잡할 수 있다. 예를 들어 아래 그림은 텍스트 데이터를 처리하기 위해 설계되어 널리 사용되는 트랜스포머 층의 구조다.
이거 설명아직 못 함(여기에 들어갈 이론이 너무 많아 인코더 디코더, 어텐션 …)
여튼,… 케라스에서 이러한 모델을 만드는 방법은 일반적으로 두 가지다. Model 클래스의 서브클래스를 만들거나, 더 적은 코드로 많은 일을 수행할 수 있는 함수형 API를 사용할 수 있음. 7장에서 알려준다고함.
모델의 구조는 가설 공간을 정의함. 네트워크 구조를 선택하면 가능성 있는 공간이 입력 데이터를 출력 데이터로 매핑하는 일련의 특정한 텐서 연산으로 제한됨
이후 우리가 찾을 것은 가중치 텐서의 좋은 값임.
가설 공간의 구조가 중요한 이유는 데이터에서 학습을 하기 위해 데이터에 대한 가정을 하기 때문임
컴파일 단계 : 학습 과정 설정
모델 구조를 정의하고 아래 3가지를 선택해야함
- 손실 함수(= 목적 함수)
- 옵티마이저
- 측정 지표
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer="rmsprop",
loss="mean_squared_error",
metrics=["accuracy"])
여기서 문자열로 값을 전달하는데 이게 실제로는 객체로 전달됨(딕셔너리 구조 생각하셈)
“rmsprop” : keras.optimizers.RMSprop() 요런 느낌
이렇게 직접도 가능
model.compile(optimizer=keras.optimizers.RMSprop(),
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])
위 같은 방법은 사용자 정의 손실이나 측정 지표를 전달하고 싶을 때 유용함. 또는 사용할 객체를 상세히 설정하고 싶은 경우!
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate = 1e-4), # 커스텀
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])
케라스는 다양한 옵션을 기본적으로 제공하는데 뒤에서 나중에 말하겠지..
손실 함수 선택
네트워크가 손실을 최소화하기 위해 편법을 사용할 수 있기 때문에 올바른 손실 함수 선택은 중요함. 우리가 만든 신경망은 단지 손실 함수를 최소화하기만 한다는 것을 기억하셈
fit() 이해
컴파일 후 fit()
을 호출하는데 fit()
은 훈련 루프를 구현한다. 매개변수는 아래와 같음
- 훈련할 데이터
- 에포크
- 배치 크기
history = model.fit(
inputs, # 입력 샘플
targets, # 훈련 타깃
epochs=5,
batch_size=128
)
지난 스터디 대로가 말했지만 history를 통해 loss, 지표 확인 가능했음
검증 데이터에서 손실, 측정 지표 모니터링
과대적합인 모델, 과소적합 모델은 필요없다 우리가 원하는건 일반화된 모델이다.(범용적으로 잘 동작하는 것) 새로운 데이터에 모델이 어떻게 동작하는지 테스트를 위해 검증 데이터를 사용함.
사람으로 치면 수능을 위해 문제집을 푼다(훈련 데이터) 모의고사를 본다(검증 데이터) 수능을 친다(테스트 데이터)인셈.
model = keras.Sequential([keras.layers.Dense(1)])
model.compile(optimizer=keras.optimizers.RMSprop(learning_rate=0.1),
loss=keras.losses.MeanSquaredError(),
metrics=[keras.metrics.BinaryAccuracy()])
indices_permutation = np.random.permutation(len(inputs)) # 훈련 데이터에서 임의로 검증 데이터를 뽑기 위해 섞는 과정
shuffled_inputs = inputs[indices_permutation]
shuffled_targets = targets[indices_permutation]
num_validation_samples = int(0.3 * len(inputs)) # 30%만 추출
val_inputs = shuffled_inputs[:num_validation_samples]
val_targets = shuffled_targets[:num_validation_samples]
training_inputs = shuffled_inputs[num_validation_samples:]
training_targets = shuffled_targets[num_validation_samples:]
model.fit(
training_inputs,
training_targets,
epochs=5,
batch_size=16,
validation_data=(val_inputs, val_targets) # 검증 데이터는 검증 손실, 측정 지표를 모니터링하는 데만 사용함
)
Epoch 1/5 88/88 [==============================] - 1s 4ms/step - loss: 0.2450 - binary_accuracy: 0.9093 - val_loss: 0.0913 - val_binary_accuracy: 0.9850 Epoch 2/5 88/88 [==============================] - 0s 3ms/step - loss: 0.0680 - binary_accuracy: 0.9643 - val_loss: 0.0270 - val_binary_accuracy: 1.0000 Epoch 3/5 88/88 [==============================] - 0s 5ms/step - loss: 0.0779 - binary_accuracy: 0.9507 - val_loss: 0.0232 - val_binary_accuracy: 0.9967 Epoch 4/5 88/88 [==============================] - 0s 4ms/step - loss: 0.0765 - binary_accuracy: 0.9407 - val_loss: 0.0598 - val_binary_accuracy: 0.9883 Epoch 5/5 88/88 [==============================] - 0s 5ms/step - loss: 0.0638 - binary_accuracy: 0.9721 - val_loss: 0.0726 - val_binary_accuracy: 0.9533 <keras.callbacks.History at 0x7f0787cb8bd0>
검증 데이터 손실 값을 훈련 손실과 구분하기 위해 검증 손실이라 부른다. 훈련 데이터, 검증 데이터는 엄격하게 분리되어야 함.
만약에 모의고사 시험지가 유출된다면??
훈련 후 검증 손실과 지표를 계산하고 싶다면 evaluate()
메서드를 사용하면 됨.
loss_and_metrics = model.evaluate(val_inputs, val_targets, batch_size = 128)
만약에 컴파일 과정에서 측정 지표를 지정하지 않았다면 검증 손실만 반환해줌 ^^
추론
모델을 잘 훈련하면 이것을 사용해서 예측을 만들어야함. 이걸 추론이라고 부른다. 간단한 방법은 __call()__를 호출하는 것이라고 함.
__call()__을 호출하는 방법이 뭐게?
그러나 대용량의 데이터인 경우 불가능 할 수 있음. 추론을 하는 더 나은 방법은 predict()
를 사용하는 것임. 요놈은 작은 배치로 순회하면서 넘파이 배열로 예측을 반환함.
predictions = model.predict(new_inputs, batch_size=128)
print(predictions[:10])
요약
- 텐서플로는 좋다
- 케라스는 텐서플로 위에서 동작한다
- 케라스의 핵심은 Layer다. 층은 가중치와 연산을 캡슐화 한다(build, call) 이를 조합해 모델을 만든다.
- 모델 훈련 전 컴파일 하면서 손실, 측정 지표 설정해야함
- 미니 배치 경사 하강법을 실행하는 fit()으로 훈련 가능하다. 또한 검증 데이터에 대한 솔실과 측정 지표도 모니터링 가능
- predict 메서드로 새로운 입력에 대해 예측을 만든다.
help me..