본문 바로가기

AI

Residual Network 구현 및 학습

Residual Network (ResNet) 개요와 핵심 내용 정리

Residual Network(ResNet)는 딥러닝 모델에서 신경망을 깊게 쌓을 때 발생하는 문제를 해결하기 위해 2015년 He et al.이 제안한 네트워크입니다. ResNet은 특히 딥러닝 모델의 깊이 증가에 따른 성능 저하 문제그래디언트 소실 문제를 해결하는 데 중요한 기여를 했습니다. 이 글에서는 ResNet의 기본 개념과 논문에서 강조된 중요한 내용들을 정리해 보겠습니다.

1. ResNet의 주요 문제 해결 접근

딥러닝 모델의 깊이 문제: 딥러닝에서는 일반적으로 모델의 깊이를 늘리면 더 높은 수준의 표현 학습이 가능해지지만, 지나치게 깊은 모델은 학습이 어려워지고 성능이 오히려 저하될 수 있습니다. 이러한 문제의 주요 원인 중 하나는 그래디언트 소실/폭주 문제기울기 손실 문제입니다. ResNet은 이러한 문제를 해결하기 위해 새로운 네트워크 구조인 잔차 블록(Residual Block) 을 도입했습니다.

2. 잔차 블록(Residual Block)

잔차 블록은 입력값을 그대로 출력값에 더하는 스킵 연결(skip connection)을 특징으로 합니다. 이러한 연결을 통해 모델은 학습 과정에서 각 레이어가 입력과 출력 사이의 변화를 학습하는 대신, 단순히 입력을 그대로 전달하는 지름길을 만들 수 있습니다. 이는 다음과 같은 주요 효과가 있습니다:

  • 그래디언트 흐름 개선: 스킵 연결 덕분에 그래디언트가 역전파될 때 소실되지 않고 깊은 네트워크를 안정적으로 학습할 수 있습니다.
  • 식별 함수 학습: 일반적인 레이어가 아닌 잔차를 학습하기 때문에 레이어는 항상 입력을 그대로 전달하거나 그와 약간 다른 출력을 만들 수 있습니다. 즉, 모델이 학습하지 않아야 할 경우 네트워크가 스스로 식별 함수(identity function)를 학습하도록 돕습니다.

수식적으로: 입력을 x라 할 때, Residual Block의 출력은 F(x) + x 형태로 표현됩니다. 여기서 F(x)는 여러 레이어를 통과한 뒤의 변환 결과입니다. 스킵 연결을 통해 x가 그대로 더해지므로, 네트워크는 F(x)와 x의 합을 학습하는 방식으로 안정적인 학습을 할 수 있게 됩니다.

3. ResNet의 구조적 특징

  • 네트워크 깊이의 증가: ResNet은 잔차 블록을 반복적으로 쌓아서 네트워크의 깊이를 극대화할 수 있습니다. ResNet-50, ResNet-101, ResNet-152와 같은 모델은 각각 50, 101, 152개의 레이어를 사용하여 구성됩니다. 깊은 네트워크임에도 불구하고 스킵 연결 덕분에 성능 저하 없이 안정적으로 학습이 가능합니다.
  • 잔차 연결의 다양한 활용: 논문에서는 간단한 두 개의 convolution layer로 구성된 기본 잔차 블록 외에도, 입력과 출력 차원이 다를 경우 이를 맞춰주기 위한 1x1 컨볼루션을 사용하는 방법도 설명합니다. 이를 통해 네트워크 구조 내에서 유연하게 잔차 연결을 활용할 수 있습니다.

4. 논문의 주요 기여

  • 깊은 네트워크의 성공적인 학습: ResNet은 최대 152개 레이어를 사용해 ImageNet 데이터셋에서 최고의 성능을 달성했습니다. 이는 딥러닝 모델이 단순히 레이어 수를 늘리는 것만으로는 성능이 개선되지 않는다는 점을 보여줬으며, 모델의 깊이를 효과적으로 사용하기 위해서는 잔차 연결과 같은 기술이 필요하다는 것을 입증했습니다.
  • Residual Learning의 개념 도입: 논문에서는 딥러닝 모델이 본질적으로 학습해야 하는 문제를 잔차(Residual)로 재정의함으로써, 깊이 증가에 따른 비선형 표현력의 제한 문제를 해결했습니다. 이를 통해 학습의 수렴 속도를 개선하고 훈련 효율성을 극대화할 수 있음을 보였습니다.

K. He, X. Zhang, S. Ren and J. Sun, "Deep Residual Learning for Image Recognition," 2016 IEEE Conference on Computer Vision and Pattern Recognition (CVPR), Las Vegas, NV, USA, 2016, pp. 770-778, doi: 10.1109/CVPR.2016.90.

 

import tensorflow as tf
import numpy as np

 

Residual Unit 구현

class ResidualUnit(tf.keras.Model):
    def __init__(self, filter_in, filter_out, kernel_size):
        super(ResidualUnit, self).__init__()
        self.bn1 = tf.keras.layers.BatchNormalization()
        self.conv1 = tf.keras.layers.Conv2D(filter_out, kernel_size, padding='same')
        
        self.bn2 = tf.keras.layers.BatchNormalization()
        self.conv2 = tf.keras.layers.Conv2D(filter_out, kernel_size, padding='same')
        
        if filter_in == filter_out:
            self.identity = lambda x: x
        else:
            self.identity = tf.keras.layers.Conv2D(filter_out, (1,1), padding='same')

    def call(self, x, training=False, mask=None):
        h = self.bn1(x, training=training)
        h = tf.nn.relu(h)
        h = self.conv1(h)
        
        h = self.bn2(h, training=training)
        h = tf.nn.relu(h)
        h = self.conv2(h)
        return self.identity(x) + h

 

Residual Layer 구현

class ResnetLayer(tf.keras.Model):
    def __init__(self, filter_in, filters, kernel_size):
        super(ResnetLayer, self).__init__()
        self.sequence = list()
        for f_in, f_out in zip([filter_in] + list(filters), filters):
            self.sequence.append(ResidualUnit(f_in, f_out, kernel_size))

    def call(self, x, training=False, mask=None):
        for unit in self.sequence:
            x = unit(x, training=training)
        return x

 

모델 정의

class ResNet(tf.keras.Model):
    def __init__(self):
        super(ResNet, self).__init__()
        self.conv1 = tf.keras.layers.Conv2D(8, (3, 3), padding='same', activation='relu') # 28x28x8
        
        self.res1 = ResnetLayer(8, (16, 16), (3, 3)) # 28x28x16
        self.pool1 = tf.keras.layers.MaxPool2D((2, 2)) # 14x14x16
        
        self.res2 = ResnetLayer(16, (32, 32), (3, 3)) # 14x14x32
        self.pool2 = tf.keras.layers.MaxPool2D((2, 2)) # 7x7x32
        
        self.res3 = ResnetLayer(32, (64, 64), (3, 3)) # 7x7x64
        
        self.flatten = tf.keras.layers.Flatten()
        self.dense1 = tf.keras.layers.Dense(128, activation='relu')
        self.dense2 = tf.keras.layers.Dense(10, activation='softmax')
        
    def call(self, x, training=False, mask=None):
        x = self.conv1(x)
        
        x = self.res1(x, training=training)
        x = self.pool1(x)
        x = self.res2(x, training=training)
        x = self.pool2(x)
        x = self.res3(x, training=training)
        
        x = self.flatten(x)
        x = self.dense1(x)
        return self.dense2(x)

 

학습, 테스트 루프 정의

# Implement training loop
@tf.function
def train_step(model, images, labels, loss_object, optimizer, train_loss, train_accuracy):
    with tf.GradientTape() as tape:
        predictions = model(images, training=True)
        loss = loss_object(labels, predictions)
    gradients = tape.gradient(loss, model.trainable_variables)

    optimizer.apply_gradients(zip(gradients, model.trainable_variables))
    train_loss(loss)
    train_accuracy(labels, predictions)

# Implement algorithm test
@tf.function
def test_step(model, images, labels, loss_object, test_loss, test_accuracy):
    predictions = model(images, training=False)

    t_loss = loss_object(labels, predictions)
    test_loss(t_loss)
    test_accuracy(labels, predictions)

 

데이터셋 준비

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

x_train = x_train[..., tf.newaxis].astype(np.float32)
x_test = x_test[..., tf.newaxis].astype(np.float32)

train_ds = tf.data.Dataset.from_tensor_slices((x_train, y_train)).shuffle(10000).batch(32)
test_ds = tf.data.Dataset.from_tensor_slices((x_test, y_test)).batch(32)

 

학습 환경 정의

모델 생성, 손실함수, 최적화 알고리즘, 평가지표 정의

# Create model
model = ResNet()

# Define loss and optimizer
loss_object = tf.keras.losses.SparseCategoricalCrossentropy()
optimizer = tf.keras.optimizers.Adam()

# Define performance metrics
train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')

 

학습 루프 동작

EPOCHS = 10

for epoch in range(EPOCHS):
    for images, labels in train_ds:
        train_step(model, images, labels, loss_object, optimizer, train_loss, train_accuracy)

    for test_images, test_labels in test_ds:
        test_step(model, test_images, test_labels, loss_object, test_loss, test_accuracy)

    template = 'Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'
    print(template.format(epoch + 1,
                          train_loss.result(),
                          train_accuracy.result() * 100,
                          test_loss.result(),
                          test_accuracy.result() * 100))
    train_loss.reset_states()
    train_accuracy.reset_states()
    test_loss.reset_states()
    test_accuracy.reset_states()
Epoch 1, Loss: 0.15172302722930908, Accuracy: 95.86000061035156, Test Loss: 0.08555025607347488, Test Accuracy: 97.44999694824219
Epoch 2, Loss: 0.0681067630648613, Accuracy: 98.1050033569336, Test Loss: 0.07039548456668854, Test Accuracy: 98.25999450683594
Epoch 3, Loss: 0.05503721907734871, Accuracy: 98.45333099365234, Test Loss: 0.05514393374323845, Test Accuracy: 98.36000061035156
Epoch 4, Loss: 0.040750861167907715, Accuracy: 98.83833312988281, Test Loss: 0.037818700075149536, Test Accuracy: 98.94999694824219
Epoch 5, Loss: 0.036504119634628296, Accuracy: 99.00166320800781, Test Loss: 0.034332212060689926, Test Accuracy: 99.02999877929688
Epoch 6, Loss: 0.029598338529467583, Accuracy: 99.1066665649414, Test Loss: 0.04355289414525032, Test Accuracy: 98.83999633789062
Epoch 7, Loss: 0.025894133374094963, Accuracy: 99.25, Test Loss: 0.05185529962182045, Test Accuracy: 98.7699966430664
Epoch 8, Loss: 0.02600380778312683, Accuracy: 99.288330078125, Test Loss: 0.04667523875832558, Test Accuracy: 98.83999633789062
Epoch 9, Loss: 0.02270493097603321, Accuracy: 99.35832977294922, Test Loss: 0.054312728345394135, Test Accuracy: 99.05999755859375
Epoch 10, Loss: 0.020289067178964615, Accuracy: 99.42500305175781, Test Loss: 0.04631120711565018, Test Accuracy: 98.97999572753906