인공 신경망
퍼셉트론(perceptron), 다층 퍼셉트론 (MLP)
이전 내용
시퀀셜 API로 회귀용 다층 퍼셉트론 만들기
케라스를 이용하여 캘리포니아 주택 가격 데이터셋을 각각 50개의 뉴런으로 구성된 3개의 은닉 층을 가진 MLP를 구축해 본다.
시퀀셜 API를 사용하여 회귀 MLP를 구축하고, 훈련하고, 평가 및 사용하는 것은 분류용 다층 퍼셉트론과 유사하나, 몇 가지 차이가 있다.
from sklearn.datasets import fetch_california_housing
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
import tensorflow as tf
housing = fetch_california_housing()
X_train_full, X_test, y_train_full, y_test = train_test_split(
housing.data, housing.target, random_state=42)
X_train, X_valid, y_train, y_valid = train_test_split(
X_train_full, y_train_full, random_state=42)
tf.random.set_seed(42)
norm_layer = tf.keras.layers.Normalization(input_shape=X_train.shape[1:])
model = tf.keras.Sequential([
norm_layer,
tf.keras.layers.Dense(50, activation='relu'),
tf.keras.layers.Dense(50, activation='relu'),
tf.keras.layers.Dense(50, activation='relu'),
tf.keras.layers.Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss='mse', optimizer=optimizer, metrics=['RootMeanSquaredError'])
norm_layer.adapt(X_train)
history = model.fit(X_train, y_train, epochs=20,
validation_data=(X_valid, y_valid))
mse_test, rmse_test = model.evaluate(X_test, y_test)
X_new = X_test[:3]
y_pred = model.predict(X_new)
▶
- 출력 층에 하나의 값만 예측하고 싶기 때문에 하나의 뉴런이 있고 활성화 함수를 사용하지 않음.
- 손실 함수는 MSE, 측정 지표는 RMSE
- Adam 옵티마이저 사용
- Flatten 층이 필요하지 않고 Normalization (정규화) 층을 첫 번째 층으로 사용: Normalization 층은 사이킷런의 StandardScaler와 동일한 작업을 수행하지만 모델의 fit() 메서드를 호출하기 전에 adapt() 메서드를 사용하여 훈련 데이터에 적응시켜야 함.
rmse_test
y_pred
함수형 API로 복잡한 모델 만들기
시퀀셜 API는 매우 깔끔하고 간단하다. Sequential 모델이 널리 사용되지만 입력과 출력이 여러 개이거나 더 복잡한 네트워크 토폴로지를 갖는 신경망을 만들어야 할 때에는 케라스에서 제공하는 함수형 API를 이용하면 된다.
순차적이지 않은 신경망의 한 예는 와이드 & 딥 신경망으로, 이 신경망은 입력의 일부 또는 전체가 출력 층에 바로 연결된다. 이 구조를 사용하면 신경망이 깊게 쌓은 층을 사용한 복잡한 패턴과 짧은 경로를 사용한 간단한 규칙을 모두 학습할 수 있다.
이와는 대조적으로 일반적인 MLP는 네트워크에 있는 층 전체에 모든 데이터를 통과시켜 데이터에 있는 간단한 패턴이 연속된 변환으로 인해 왜곡될 수 있다.
와이드 & 딥 신경망을 만들어 캘리포니아 주택 문제 해결
normalization_layer = tf.keras.layers.Normalization()
hidden_layer1 = tf.keras.layers.Dense(30, activation='relu')
hidden_layer2 = tf.keras.layers.Dense(30, activation='relu')
concat_layer = tf.keras.layers.Concatenate()
output_layer = tf.keras.layers.Dense(1)
input_ = tf.keras.layers.Input(shape=X_train.shape[1:])
normalized = normalization_layer(input_)
hidden1 = hidden_layer1(normalized)
hidden2 = hidden_layer2(hidden1)
concat = concat_layer([normalized, hidden2])
output = output_layer(concat)
model = tf.keras.Model(inputs=[input_], outputs=[output])
▶ [코드 설명]
1. 처음 다섯 줄: 모델을 만드는 데 필요한 모든 층
2. 다음 여섯 줄: 이 층을 함수처럼 사용하여 입력에서 출력으로 이동
3. 마지막 줄: 입력과 출력을 지정하여 케라스 모델 객체 생성
- 처음 다섯 줄: 입력을 표준화하기 위한 Normalization 층, ReLU 활성화 함수를 사용하여 각각 30개의 뉴런이 있는 두 개의 Dense 층, Concatenate 층, 출력 층을 위해 활성화 함수 없이 하나의 뉴런이 있는 Dense 층 5개의 층 생성
- input_ : Input 객체 생성. 모델이 받을 입력의 종류에 대한 사양으로, input과 선택적으로 dytpe 매개변수를 가진다. dtype의 기본값은 32비트 부동소수점이다. * 파이썬의 내장 함수 input()과 이름이 충돌되지 않도록 input_으로 이름 지정
- normalized: Normalization 층을 함수처럼 사용하여 Input 객체 전달. Input 객체는 데이터 사양일 뿐이므로 실제로 데이터는 아직 처리되지 않으며, 케라스에 층을 어떻게 연결해야 하는지 알려주는 심볼릭 입력. 이 호출의 출력도 마찬가지로 기호이며, normalized는 실제 데이터를 가지고 있지 않고 모델을 구성하는 데만 사용 됨.
- hidden1 & hidden2: normalized를 hidden_layer1에 전달하면 hidden1이 출력되고, hidden1을 hidden_layer2에 전달하면 hidden2 출력
- concat: concat_layer를 사용하여 입력과 두 번째 은닉층의 출력을 연결. 실제 데이터는 아직 연결되지 않았으며 모델을 구축하기 위한 기호임.
- output: concat을 output_layer로 전달하여 최종 output 얻음.
model.summary()
케라스 모델을 만들고 나면 이전과 동일하게 모델을 컴파일하고 Normalization 층의 adapt() 메서드를 호출한 다음, 훈련, 평가, 예측을 수행한다.
tf.random.set_seed(42)
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss='mse', optimizer=optimizer, metrics=['RootMeanSquaredError'])
normalization_layer.adapt(X_train)
history = model.fit(X_train, y_train, epochs=20,
validation_data=(X_valid, y_valid))
mse_test = model.evaluate(X_test, y_test)
y_pred = model.predict(X_new)
일부 특성을 짧은 경로로 전달하고 다른 특성들을 깊은 경로로 전달하고 싶은 경우 여러 입력을 사용한다.
예를 들어 5개 특성(특성 인덱스 0~4)을 짧은 경로로 보내고 6개 특성(특성 인덱스 2~7)을 깊은 경로로 보낸다고 가정할 경우, 아래와 같이 할 수 있다.
tf.random.set_seed(42)
input_wide = tf.keras.layers.Input(shape=[5]) # 특성 인덱스 0~4
input_deep = tf.keras.layers.Input(shape=[6]) # 특성 인덱스 2~7
norm_layer_wide = tf.keras.layers.Normalization()
norm_layer_deep = tf.keras.layers.Normalization()
norm_wide = norm_layer_wide(input_wide)
norm_deep = norm_layer_deep(input_deep)
hidden1 = tf.keras.layers.Dense(30, activation='relu')(norm_deep)
hidden2 = tf.keras.layers.Dense(30, activation='relu')(hidden1)
concat = tf.keras.layers.concatenate([norm_wide, hidden2])
output = tf.keras.layers.Dense(1)(concat)
model = tf.keras.Model(inputs=[input_wide, input_deep], outputs=[output])
▶ 이전 코드와 비교
- 모든 Dense 층은 한 줄에서 생성되어 호출되는데, 이런 방식은 명확성을 잃지 않으면서 코드를 더 간결하게 만들기 때문에 널리 사용된다. 그러나 Normalization 층의 경우에는 모델을 훈련하기 전에 Normalization 층의 adapt() 메서드를 호출할 수 있도록 이 층에 대한 참조가 필요하다.
- tf.keras.layers.concatenate()을 사용하여 Concatenate 층을 만들고 주어진 입력으로 이 층을 호출
- 두 개의 입력이 있으므로 모델을 만들 때 inputs=[input_wide, input_deep] 지정
이전과 동일하게 모델을 컴파일 할 수 있지만 fit() 메서드를 호출할 때 하나의 입력 행렬 X_train을 전달하는 것이 아니라 입력마다 하나씩 행렬의 튜플 (X_train_wide, X_train_deep)을 전달해야 하며, X_valid에도 동일하게 적용된다.
evaluate()나 predictd()를 호출할 때 X_test와 X_new에도 동일하다.
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss='mse', optimizer=optimizer, metrics=['RootMeanSquaredError'])
X_train_wide, X_train_deep = X_train[:, :5], X_train[:, 2:]
X_valid_wide, X_valid_deep = X_valid[:, :5], X_valid[:, 2:]
X_test_wide, X_test_deep = X_test[:, :5], X_test[:, 2:]
X_new_wide, X_new_deep = X_test_wide[:3], X_test_deep[:3]
norm_layer_wide.adapt(X_train_wide)
norm_layer_deep.adapt(X_train_deep)
history = model.fit((X_train_wide, X_train_deep), y_train, epochs=20,
validation_data=((X_valid_wide, X_valid_deep), y_valid))
mse_test = model.evaluate((X_test_wide, X_test_deep), y_test)
y_pred = model.predict((X_new_wide, X_new_deep))
[여러 개의 출력이 필요한 경우]
- 여러 출력이 필요한 작업: 그림에 있는 주요 물체를 분류하고 위치 파악 (회귀 작업 + 분류 작업)
- 동일한 데이터에서 독립적인 여러 작업 수행: 예를 들어 얼굴 사진으로 다중 작업 분류를 수행 시 한 출력은 사람의 얼굴 표정을, 다른 출력은 안경 착용 여부 구별
- 규제 기법으로 사용: 과대적합을 줄이고 모델의 일반화 성능을 높이도록 훈련에 제약을 가하는 경우. 신경망 구조 안에 보조 출력을 추가하고, 보조 출력을 사용해 하위 네트워크가 나머지 네트워크에 의존하지 않고 그 자체로 유용한 것을 학습하는지 확인
보조 출력을 추가하기 위해서는 적절한 층에 연결하고 모델의 출력 리스트에 추가하면 된다.
input_wide = tf.keras.layers.Input(shape=[5]) # 특성 인덱스 0~4
input_deep = tf.keras.layers.Input(shape=[6]) # 특성 인덱스 2~7
norm_layer_wide = tf.keras.layers.Normalization()
norm_layer_deep = tf.keras.layers.Normalization()
norm_wide = norm_layer_wide(input_wide)
norm_deep = norm_layer_deep(input_deep)
hidden1 = tf.keras.layers.Dense(30, activation='relu')(norm_deep)
hidden2 = tf.keras.layers.Dense(30, activation='relu')(hidden1)
concat = tf.keras.layers.concatenate([norm_wide, hidden2])
output = tf.keras.layers.Dense(1)(concat) # 출력 층까지는 동일
# 보조 출력 추가
aux_output = tf.keras.layers.Dense(1)(hidden2)
model = tf.keras.Model(inputs=[input_wide, input_deep],
outputs=[output, aux_output])
각 출력은 자신만의 손실 함수가 필요하므로, 모델을 컴파일할 때 손실의 리스트를 전달해야 하는데, 하나의 손실을 전달하면 케라스는 모든 출력의 손실 함수가 동일하다고 가정한다.
기본적으로 케라스는 나열된 손실을 모두 더하여 최종 손실을 구해 훈련에 사용한다. 보조 출력은 규제로만 사용되므로, 보조 출력보다 주 출력에 더 관심이 많다면 주 출력의 손실에 더 많은 가중치를 부여해야 하며, 모델을 컴파일할 때 손실 가중치를 지정할 수 있다.
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss=("mse", "mse"), loss_weights=(0.9, 0.1), optimizer=optimizer,
metrics=["RootMeanSquaredError", "RootMeanSquaredError"])
모델을 훈련할 때 각 출력에 대한 레이블을 제공해야 한다. 여기에서는 주 출력과 보조 출력이 같은 것을 예측해야 하므로 동일한 것을 사용하므로, y_train 대신에 (y_train, y_train)을 전달하거나 출력 이름이 'output' 및 'aux_output'인 경우 딕셔너리 {'output' : y_train, 'aux_output' : y_train}을 전달해야 한다.
y_valid와 y_test도 동일하다.
norm_layer_wide.adapt(X_train_wide)
norm_layer_deep.adapt(X_train_deep)
history = model.fit(
(X_train_wide, X_train_deep), (y_train, y_train), epochs=20,
validation_data=((X_valid_wide, X_valid_deep), (y_valid, y_valid))
)
모델을 평가하면 케라스는 개별 손실과 측정 지표, 손실의 가중치 합을 반환한다
eval_results = model.evaluate((X_test_wide, X_test_deep), (y_test, y_test), return_dict=True)
weighted_sum_of_losses, main_rmse, aux_rmse = eval_results
predict() 메서드는 각 출력에 대한 예측을 반환한다
y_pred_main, y_pred_aux = model.predict((X_new_wide, X_new_deep))
predict() 메서드는 튜플을 반환하며, 딕셔너리를 반환하기 위한 return_dict 매개변수가 없으나, model.output_names를 사용하여 딕셔너리를 만들 수 있다.
y_pred_tuple = model.predict((X_new_wide, X_new_deep))
y_pred = dict(zip(model.output_names, y_pred_tuple))
서브클래싱 API로 동적 모델 만들기
시퀀셜 API와 함수형 API는 사용할 층과 연결 방식을 먼저 정의하고 모델에 데이터를 주입하여 훈련이나 추론을 시작할 수 있는선언적 방식이다.
[선언적 방식의 장점]
- 모델 저장, 복사, 공유 용이
- 모델 구조 출력 및 분석 용이
- 프레임워크가 크기를 짐작하고 타입을 확인하여 모델에 데이터가 주입되기 전에 에러 발견 용이
- 전체 모델이 층으로 구성된 정적 그래프라 디버깅 용이
그러나 어떤 모델은 반복문을 포함하고 다양한 크기를 다루어야 하며 조건문을 가지는 등 여러 가지 동적인 구조를 필요로 하기 때문에 명령형 프로그래밍 스타일이 필요한 경우 서브클래싱 API를 이용하면 된다.
간단히 Model 클래스를 상속한 다음 생성자 안에서 필요한 층을 만들고 call() 매서드 안에 수행하려는 연산을 기술한다.
아래의 WideAndDeepModel 클래스의 인스턴스는 함수형 API로 만든 모델과 동일한 기능을 수행한다.
class WideAndDeepModel(tf.keras.Model):
def __init__(self, units=30, activation='relu', **kwargs):
super().__init__(**kwargs) # 모델에 이름 부여 위해 필요
self.norm_layer_wide = tf.keras.layers.Normalization()
self.norm_layer_deep = tf.keras.layers.Normalization()
self.hidden1 = tf.keras.layers.Dense(units, activation=activation)
self.hidden2 = tf.keras.layers.Dense(units, activation=activation)
self.main_output = tf.keras.layers.Dense(1)
self.aux_output = tf.keras.layers.Dense(1)
def call(self, inputs):
input_wide, input_deep = inputs
norm_wide = self.norm_layer_wide(input_wide)
norm_deep = self.norm_layer_deep(input_deep)
hidden1 = self.hidden1(norm_deep)
hidden2 = self.hidden2(hidden1)
concat = tf.keras.layers.concatenate([norm_wide, hidden2])
output = self.main_output(concat)
aux_output = self.aux_output(hidden2)
return output, aux_output
model = WideAndDeepModel(30, activation='relu', name='my_cool_model')
▶ 이 예제는 생성자에서 층을 생성하는 것과 call() 메서드에서 이 층을 사용하는 것을 분리한다는 점을 제외하면 함수형 API 예제와 비슷하다. 그리고 Input 객체를 생성할 필요 없이 call() 메서드에 inputs 매개변수를 사용할 수 있다.
생성한 모델 객체를 사용하여 모델 객체를 컴파일하고, 정규화 층을 적용하고, 훈련하고, 평가하고, 예측해 본다.
optimizer = tf.keras.optimizers.Adam(learning_rate=1e-3)
model.compile(loss="mse", loss_weights=[0.9, 0.1], optimizer=optimizer,
metrics=["RootMeanSquaredError", "RootMeanSquaredError"])
model.norm_layer_wide.adapt(X_train_wide)
model.norm_layer_deep.adapt(X_train_deep)
history = model.fit(
(X_train_wide, X_train_deep), (y_train, y_train), epochs=10,
validation_data=((X_valid_wide, X_valid_deep), (y_valid, y_valid)))
eval_results = model.evaluate((X_test_wide, X_test_deep), (y_test, y_test))
weighted_sum_of_losses, main_rmse, main_rmse, aux_rmse = eval_results
y_pred_main, y_pred_aux = model.predict((X_new_wide, X_new_deep))
이 API의 가장 큰 차이점은 call() 메서드에 for 반복문, if 문, 저수준 텐서플로 연산 등 원하는 거의 모든 것을 포함시킬 수 있다는 것이다. 그러나 이러한 추가적인 유연성에는 대가가 따른다.
- 모델의 구조가 call() 메서드에 숨겨져 있어 케라스가 쉽게 검사할 수 없다.
- tf.keras.models.clone_model() 을 사용하여 모델 복제 불가
- 케라스가 타입과 크기를 미리 확인할 수 없음.
model.summary()
또한, summary() 메서드를 호출하면 층이 서로 어떻게 연결되어 있는지 ('Connected to' 항목)에 대한 정보 없이 층의 목록만 출력된다.
따라서, 추가적인 유연성이 정말 필요한 것이 아니라면 시퀀셜 API나 함수형 API를 사용하는 것이 좋다.
다음 내용
[출처]
핸즈 온 머신러닝
'[파이썬 Projects] > <파이썬 딥러닝, 신경망>' 카테고리의 다른 글
[딥러닝] 신경망 하이퍼파라미터 튜닝하기 (1) | 2024.11.21 |
---|---|
[딥러닝] 케라스로 다층 퍼셉트론 구현하기 - 3 (1) | 2024.11.20 |
[딥러닝] 케라스로 다층 퍼셉트론 구현하기 - 1 (0) | 2024.11.19 |
[딥러닝] 인공 신경망: 퍼셉트론, 다층 퍼셉트론 (1) | 2024.11.19 |
[딥러닝] 인공 신경망(ANN) (2) | 2024.11.19 |