이전 글에서는 불안정한 그레이디언트 문제를 해결하기 위한 방법 중 하나인 활성화 함수를 알아보았습니다.
이번 글에서는 완전히 다른 방법인 배치 정규화에 대해 정리하겠습니다.
*배치 정규화
ReLU와 함께 He 초기화를 사용하면 훈련 초기 단계에서 그레이디언트 소실이나 폭주 문제를 크게 줄일 수 있지만,
훈련하는 동안 다시 발생하지 않으리란 보장은 없습니다.
2015년 논문에서 그레이디언트 소실과 폭주 문제를 해결하기 위한 배치 정규화(batch normalization) - BN 기법을 제안했습니다.
이 기법은 각 층에서 활성화 함수를 통과하기 전이나 후에 모델에 연산을 하나 추가합니다.
이 연산은 단순하게 입력을 원점에 맞추고 정규화한 다음, 각 층에서 두 개의 새로운 파라미터로 결괏값의 스케일을 조정하고 이동시킵니다.
하나는 스케일 조정에, 다른 하나는 이동에 사용됩니다.
많은 경우 신경망의 첫 번째 층으로 배치 정규화를 추가하면 훈련 세트를 표준화할 필요가 없습니다.
즉, 배치 정규화 층이 이런 역할을 대신하기 때문에 StandardScaler나 Normalization 클래스가 필요하지 않습니다.
또한 한 번에 하나의 배치만 처리하기 때문에 근사적입니다.
더불어 입력 특성마다 스케일을 조정하고 이동시킬 수 있습니다.
입력 데이터를 원점에 맞추고 정규화하려면 알고리즘은 평균과 표준 편차를 추정해야 합니다.
이를 위해 현재 미니배치에서 입력의 평균과 표춘 편차를 평가합니다. 그래서 이름이 배치 정규화인 것입니다.

- μB는 미니배치 B에 대해 평가한 입력의 평균 벡터입니다.
(입력마다 하나의 평균을 가짐) - mB는 미니배치에 있는 샘플 수입니다.
- σB도 미니배치에 대해 평가한 입력의 표준 편차 벡터입니다.
- ε은 분모가 0이 되는 것을 막고 그레이디언트가 너무 커지지 않게 만들기 위한 작은 숫자(기본값:10^-5) 입니다.
이를 안전을 위한 항이라고 합니다. - x^i는 평균이 0이고 정규화된 샘플 i의 입력입니다.
- γ는 층의 출력 스케일 파라미터 벡터입니다.
(입력마다 하나의 스케일 파라미터가 있음) - ⓧ 는 원소별 곱셈입니다.
(각 입력은 해당되는 출력 스케일 파라미터와 곱해짐) - β는 층의 출력 이동 파라미터 벡터입니다. 각 입력은 해당 파라미터만큼 이동합니다.
(입력마다 하나의 스케일 파라미터가 있음) - z^i는 배치 정규화 연산의 출력입니다. 즉, 입력의 스케일을 조정하고 이동시킨 것입니다.
훈련하는 동안 배치 정규화는 입력을 정규화한 다음 스케일을 조정하고 이동시킵니다.
테스트 시에는 어떻게 할까요? 간단하게 정의할 수 있는 문제는 아닙니다.
아마 샘플의 배치가 아니라 샘플 하나에 대한 예측을 만들어야 할 것입니다.
이 경우 입력의 평균과 표준 편차를 계산할 방법이 없습니다.
샘플의 배치를 사용한다 하더라도 매우 작거나 독립 동일 분포(independent and identically distributed - IID) 조건을 만족하지 못할 수 있습니다.
이런 배치 샘플에서 계산한 통계는 신뢰도가 떨어집니다.
한 가지 방법은 훈련이 끝난 후 전체 훈련 세트를 신경망에 통과시켜 배치 정규화 층의 각 입력에 대한 평균과 표준 편차를 계산하는 것입니다.
예측할 때 배치 입력 평균과 표준 편차로 이 최종 입력 평균과 표준 편차를 대신 사용할 수 있습니다.
그러나 대부분 배치 정규화 구현은 층의 입력 평균과 표준 편차의 이동 평균을 사용해 훈련하는 동안 최종 통계를 추정합니다.
케라스의 BatchNormalization 층은 이를 자동으로 수행합니다.
정리하면 배치 정규화 층마다 네 개의 파라미터 벡터가 학습됩니다.
γ (출력 스케일 벡터)와 β (출력 이동 벡터)는 일반적인 역전파를 통해 학습됩니다.
μ (최종 입력 평균 벡터)와 σ(최종 입력 표준 편차 벡터)는 지수 이동 평균을 사용하여 추정됩니다.
μ와 σ는 훈련하는 동안 추정되지만 훈련이 끝난 후에 사용됩니다.
GELU의 활성화 함수 식에 있는 배치 입력 평균과 표준 편차를 대체하기 위함입니다.
이오페는 실험한 모든 심층 신경망에서 배치 정규화가 성능을 크게 향상한다는 것을 보였습니다.
특히 이미지넷(iagenet) 분류 작업에서 큰 성과를 냈습니다.
그레이디언트 소실 문제가 크게 감소하여 tanh나 시그모이드 활성화 함수 같은 수렴성을 가진 활성화 함수를 사용할 수 있습니다.
또 가중치 초기화에 네트워크가 훨씬 덜 민감해집니다.
* γ와 β의 자원은 모두 z^i 같습니다.
즉, 층의 뉴런마다 γ와 β를 하나씩 가집니다.
* 올바른 평가를 위해 테스트할 때도 새로운 데이터에 대해 예측을 하는 것처럼 샘플이 하나씩 주입된다고 가정하기 때문입니다.
* 이미지넷은 여러 클래스로 분류된 대규모 이미지 데이터베이스로, 컴퓨터 비전 시스템을 평가하는 용도로 널리 사용됩니다.
저자들은 훨씬 큰 학습률을 사용하여 학습 과정의 속도를 크게 높일 수 있었습니다.
가장 뛰어난 이미지 분류 모델에 적용하면 배치 정규화가 14배나 적은 훈련 단계에서 같은 정확도를 달성하고 상당한 차이로 원래 모델을 앞지릅니다.
배치 정규화된 모델의 앙상블을 사용해 이미지넷 분류의 가장 뛰어난 수준에 도달했습니다.
마지막으로 마치 또 하나의 선물처럼, 배치 정규화는 규제와 같은 역할을 하여 다른 규제 기법의 필요성을 줄여 줍니다.
그러나 앞서 언급한 것처럼 입력 데이터를 정규화할 필요는 없어지지만, 배치 정규화는 모델의 복잡도를 키웁니다.
더군다나 실행 시간에서도 손해입니다.
층마다 추가되는 계산이 신경망의 예측을 느리게 합니다.
다행히 훈련이 끝난 후에 이전 층과 배치 정규화 층을 합쳐 실행 속도 저하를 피할 수 있습니다.
이전 층의 가중치를 바꾸어 바로 스케일이 조정되고 이동된 출력을 만듭니다.
예를 들면 이전 층이 XW + b를 게산하면,
배치 정규화 층은 γⓧ(XW + b - μ) / σ + β 를 계산합니다. (분모의 안전을 위해 추가하는 항인 ε를 무시)
만약 W' = γⓧW/σ 와 b' = γⓧ(b - μ) / σ + β로 정의하면 이 식은 XW' + b로 단순화됩니다.
따라서 이전 층의 가중치와 편향 (W와 b)을 업데이트된 가중치와 편향(W'와 b')으로 바꾸면 배치 정규화 층을 제거할 수 있습니다.
(나중에 볼 TFLite 변환기는 이를 자동으로 처리함!)
* 배치 정규화를 사용할 때 에포크마다 더 많은 시간이 걸리므로 훈련이 오히려 느려질 수 있습니다.
하지만 배치 정규화를 사용하면 수렴이 훨씬 빨라지므로 보통 상쇄됩니다.
따라서 더 적은 에포크로 동일한 성능에 도달할 수 있습니다.
대부분 실제 걸리는 시간은 보통 더 짧습니다.
* 배치 정규화는 전체 데이터셋이 아니라 미니배치입니다.
평균과 표준 편차를 계산하므로 훈련 데이터에 일종의 잡음을 넣는다고 볼 수 있습니다.
이런 잡음은 훈련 세트에 과대적합되는 것을 방지하는 규제의 효과를 가지며 미니배치의 크기가 클수록 효과는 줄어듭니다.
하지만 배치 정규화로 인한 규제는 부수 효과여서 비교적 크지 않으므로 규제를 위해서는 드롭아웃을 함께 사용하는 것이 좋습니다.
*케라스로 배치 정규화 구현하기
다른 케라스 코드와 마찬가지로 배치 정규화 층도 간단하고 쉽게 구현할 수 있습니다.
은닉 층의 활성화 함수 전이나 후에 BatchNormalization 층을 추가하면 됩니다.
모델의 첫 번째 층으로 배치 정규화 층을 추가할 수도 있습니다.
하지만 Normalization 층도 일반적으로 이 위치에서 잘 작동합니다.
유일한 단점은 먼저 adapt() 메서드를 호출해야 한다는 점...
예를 들면 다음 모델은 각 은닉층 모델의 첫 번째 층으로 배치 정규화 층을 적용합니다.
import tensorflow as tf
from tensorflow import keras
#MNIST 데이터셋 가져오기
(X_train, y_train), (X_test, y_test) = keras.datasets.mnist.load_data()
#전처리
X_train = X_train.astype("float32") / 255.0
X_test = X_test.astype("float32") / 255.0
#모델 정의
model = keras.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.BatchNormalization(),
keras.layers.Dense(300, activation="relu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(100, activation="relu", kernel_initializer="he_normal"),
keras.layers.BatchNormalization(),
keras.layers.Dense(10, activation="softmax")
])
#컴파일
model.compile(loss="sparse_categorical_crossentropy",
optimizer=keras.optimizers.Adam(),
metrics=["accuracy"])
#모델 요약 출력
model.summary()
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 ━━━━━━━━━━━━━━━━━━━━ 1s 0us/step
/usr/local/lib/python3.11/dist-packages/keras/src/layers/reshaping/flatten.py:37: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead.
super().__init__(**kwargs)
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ flatten (Flatten) │ (None, 784) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization │ (None, 784) │ 3,136 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense) │ (None, 300) │ 235,500 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_1 │ (None, 300) │ 1,200 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense) │ (None, 100) │ 30,100 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_2 │ (None, 100) │ 400 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense) │ (None, 10) │ 1,010 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 271,346 (1.04 MB)
Trainable params: 268,978 (1.03 MB)
Non-trainable params: 2,368 (9.25 KB)
이게 전부입니다.
은닉 층 두 개를 가진 작은 예제라서 배치 정규화가 큰 도움이 되진 않을 수 있습니다.
하지만 깊은 네트워크에서는 엄청난 차이를 만들 수 있습니다.
모델의 summary() 메서드 호출 결과를 보면 알 수 있듯이
배치 정규화 층은 입력마다 네 개의 파라미터 γ , β , μ , σ 를 추가합니다.
(ex 첫 번째 배치 정규화 층은 4 * 784 = 3136개의 파라미터가 존재)
마지막 두 개의 파라미터 μ , σ는 이동 평균입니다.
이 파라미터는 역전파로 학습되지 않기 때문에 케라스는 Non-trainable 파라미터로 분류합니다.
(배치 정규화 파라미터의 전체 개수는 3136 + 1200 + 400 임. 이를 2로 나누면 이 모델에서 훈련되지 않는 전체 파라미터 개수 2368을 얻을 수 있음)
* 훈련하는 동안 훈련 데이터를 기반으로 추정되므로 학습된다고도 볼 수 있습니다.
케라스에서 Non-trainable은 역전파로 업데이트되지 않는다는 의미입니다.
첫 번째 배치 정규화 층의 파라미터를 살펴봅시다.
두 개는 역전파로 훈련되고 두 개는 훈련되지 않습니다.
[(var.name , var.trainable) for var in model.layers[1].variables]
[('gamma', True),
('beta', True),
('moving_mean', False),
('moving_variance', False)]
* variables 속성 외에 훈련되는 파라미터만 가지고 있는 trainable_variables와 훈련되지 않는 파라미터를 가지고 있는 non_trainable_variables 속성이 있습니다.
이 세 가지 속성은 각각 weights, trainable_weights, non_trainable_weights 속성과 동일합니다.
배치 정규화 논문의 저자들은 방금 전 예제처럼 활성화 함수 이후보다 활성화 함수 이전에 배치 정규화 층을 추가하는 것이 좋다고 조언합니다.
하지만 작업에 따라 선호되는 방식이 달라서 이 조언에 대해서는 논란이 조금 있습니다.
두 가지 방법 모두 실험해 보고 어떤 것이 주어진 데이터셋에 가장 잘 맞는지 확인하는 것이 좋습니다.
활성화 함수 전에 배치 정규화 층을 추가하려면 은닉 층에서 활성화 함수를 지정하지 말고 배치 정규화 층 뒤에 별도의 층으로 추가해야 합니다.
또한 배치 정규화 층은 입력마다 이동 파라미터를 포함하기 때문에 이전 층에서 편향을 뺄 수 있습니다.
층을 만들 때 use_bias = False로 설정하면 됩니다.
마지막으로 첫 번째 은닉 층이 배치 정규화 층 두 개 사이에 끼이지 않도록 첫 번째 배치 정규화 층을 삭제할 수 있습니다.
업데이트된 코드는 다음과 같습니다.
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
#MNIST 데이터셋 가져오기
(X_train_full, y_train_full), (X_test, y_test) = keras.datasets.mnist.load_data()
#전처리
X_train_full = X_train_full.astype("float32") / 255.0
X_test = X_test.astype("float32") / 255.0
#데이터 분리
X_train, X_valid, y_train, y_valid = train_test_split(
X_train_full, y_train_full, test_size=0.1, random_state=42
)
#모델 정의
model = keras.Sequential([
keras.layers.Flatten(input_shape=[28, 28]),
keras.layers.Dense(300, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("relu"),
keras.layers.Dense(100, kernel_initializer="he_normal", use_bias=False),
keras.layers.BatchNormalization(),
keras.layers.Activation("relu"),
keras.layers.Dense(10, activation="softmax")
])
#컴파일
model.compile(loss="sparse_categorical_crossentropy",
optimizer="sgd",
metrics=["accuracy"])
#학습
history = model.fit(X_train, y_train, epochs=2,
validation_data=(X_valid, y_valid))
#모델 구조 확인
model.summary()
Epoch 1/2
1688/1688 ━━━━━━━━━━━━━━━━━━━━ 10s 5ms/step - accuracy: 0.7824 - loss: 0.7630 - val_accuracy: 0.9383 - val_loss: 0.2232
Epoch 2/2
1688/1688 ━━━━━━━━━━━━━━━━━━━━ 7s 4ms/step - accuracy: 0.9338 - loss: 0.2450 - val_accuracy: 0.9552 - val_loss: 0.1611
Model: "sequential_2"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ flatten_2 (Flatten) │ (None, 784) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_6 (Dense) │ (None, 300) │ 235,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_5 │ (None, 300) │ 1,200 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_2 (Activation) │ (None, 300) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_7 (Dense) │ (None, 100) │ 30,000 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_6 │ (None, 100) │ 400 │
│ (BatchNormalization) │ │ │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_3 (Activation) │ (None, 100) │ 0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_8 (Dense) │ (None, 10) │ 1,010 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 267,812 (1.02 MB)
Trainable params: 267,010 (1.02 MB)
Non-trainable params: 800 (3.12 KB)
Optimizer params: 2 (12.00 B)
BatchNormalization 클래스는 조정할 하이퍼파라미터가 적습니다.
보통 기본 값이 잘 작동하지만 이따금 momentum 매개변수를 변경해야 할 수 있습니다.
BatchNormalization 층이 지수 이동 평균을 업데이트할 때 이 하이퍼파라미터를 사용합니다.
새로운 값 V 즉, 현재 배치에서 계산한 새로운 입력 평균 벡터나 표준 편차 벡터가 주어지면 다음 식을 사용해 이동 평균 v'을 업데이트합니다.
v' = v' x momentum + v x (1 - momentum) 적절한 모멘텀 값을 일반적으로 1에 가깝습니다.
예를 들면 0.9, 0.99, 0.999입니다.
데이터셋이 크고 미니배치가 작으면 소수점 뒤에 9를 더 넣어 1에 더 가깝게 합니다.
중요한 다른 하이퍼파라미터는 axis입니다.
이 매개변수는 정규화할 축을 정합니다. 기본값은 -1입니다.
다른 축을 따라 계산한 평균과 표준 편차를 사용하여 마지막 축을 정규화합니다.
입력 배치가 2D (배치 크기가 [샘플 개수, 특성 개수])이면 각 입력 특성이 배치에 있는 모든 샘플에 대해 계산한 평균과 표준 편차를 기반으로 정규화됩니다.
예를 들어 이전코드 예제에서 첫 번째 배치 정규화 층은 784개의 입력 특성마다 독립적으로 정규화 그리고 스케일 조정과 이동이 될 것입니다.
만약 배치 정규화 층을 Flatten 층 이전으로 옮기면 입력 배치는 [샘플 개수, 높이, 너비] 크기의 3D가 됩니다.
따라서 배치 정규화 층이 28개의 평균과 28개의 표준 편차를 계산합니다.
(열에 있는 모든 행과 샘플에 대해 계산하므로 픽셀의 열마다 1개씩 만들어짐)
그다음 동일한 평균과 표준 편차를 사용하여 해당 열의 모든 픽셀을 정규화합니다.
또한 28개의 스케일 파라미터와 28개의 이동 파라미터가 있습니다.
784개의 픽셀을 독립적으로 다루고 싶다면 axis = [1,2]로 지정해야 합니다.
BatchNormalization은 심층 신경망, 특히 심층 합성곱 신경망에서 매우 널리 사용하는 층이 되었고,
보통 모든 층 뒤에 배치 정규화가 있다고 가정하므로 종종 신경망 그림에서 빠져 있습니다.
배치 정규화와 관련된 내용은 이상입니다.
다음 글에서는 훈련 중 그레이디언트를 안정화시키는 마지막 기법인 그레이디언트 클리핑을 정리하도록 하겠습니다!
'딥러닝 | 머신러닝' 카테고리의 다른 글
| 케라스를 활용한 심층 신경망: 훈련 중 데이터 부족과 훈련 속도 문제 해결하기! (12) | 2025.08.22 |
|---|---|
| 케라스를 활용한 심층 신경망: 그레이디언트 클리핑으로 학습 안정화 시키기! (7) | 2025.08.21 |
| 케라스를 활용한 심층 신경망: 활성화 함수와 심층 신경망 훈련! (12) | 2025.08.19 |
| 케라스를 활용한 인공 신경망: 하이퍼파라미터 튜닝 가이드! (7) | 2025.08.18 |
| 케라스로 시작하는 인공 신경망: 캘리포니아 주택 가격으로 배우는 3가지 모델 설계법! (7) | 2025.08.16 |