🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

EfficientNet과 모델 스케일링 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 9. · 12 Views

EfficientNet과 모델 스케일링 완벽 가이드

CNN 모델의 효율적인 스케일링 기법인 EfficientNet을 실무 관점에서 완벽하게 이해할 수 있습니다. Compound Scaling부터 MBConv 블록까지, 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링 방식으로 풀어냈습니다.


목차

  1. 모델_스케일링_전략
  2. Compound_Scaling
  3. EfficientNet_아키텍처
  4. MBConv_블록_이해
  5. EfficientNetV2_개선점
  6. 모델_선택_가이드
  7. 고해상도 입력(384 이상)인가? → Yes: V2 시리즈 우선

1. 모델 스케일링 전략

어느 날 김개발 씨는 이미지 분류 모델의 정확도를 높이라는 과제를 받았습니다. 선배 박시니어 씨가 다가와 물었습니다.

"모델 성능을 올리려면 어떻게 할 건가요?" 김개발 씨는 막연히 "레이어를 더 깊게 만들면 되지 않을까요?"라고 답했습니다.

모델 스케일링이란 한마디로 신경망 모델의 크기를 키워서 성능을 높이는 기법입니다. 마치 건물을 지을 때 층수를 늘리거나, 각 층의 면적을 넓히거나, 방의 개수를 늘리는 것과 같습니다.

전통적으로는 깊이, 너비, 해상도 중 하나만 조정했지만, 이것이 최선의 방법일까요?

다음 코드를 살펴봅시다.

# 전통적인 스케일링 방법들
# 1. Depth Scaling (깊이 증가)
model_deep = Sequential([
    Conv2D(32, 3, activation='relu'),
    Conv2D(32, 3, activation='relu'),
    Conv2D(32, 3, activation='relu'),  # 레이어 추가
    Conv2D(32, 3, activation='relu'),  # 더 깊게
    Dense(10, activation='softmax')
])

# 2. Width Scaling (채널 증가)
model_wide = Sequential([
    Conv2D(64, 3, activation='relu'),  # 32 -> 64 채널
    Conv2D(64, 3, activation='relu'),  # 더 넓게
    Dense(10, activation='softmax')
])

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 회사의 이미지 분류 프로젝트에 투입되었는데, 현재 모델의 정확도가 목표치에 미치지 못한다는 피드백을 받았습니다.

선배 박시니어 씨가 다가와 화면을 들여다보더니 물었습니다. "성능을 올리려면 어떻게 할 건가요?" 김개발 씨는 머릿속으로 그동안 배운 내용을 떠올렸습니다.

논문에서 본 유명한 모델들은 대부분 레이어가 많았던 것 같았습니다. "레이어를 더 깊게 만들면 되지 않을까요?" 박시니어 씨가 고개를 끄덕이며 말했습니다.

"맞아요. 그런데 깊이만 늘리면 될까요?" 그렇다면 모델 스케일링이란 정확히 무엇일까요?

쉽게 비유하자면, 모델 스케일링은 마치 건물을 확장하는 것과 같습니다. 건물을 크게 만드는 방법은 여러 가지입니다.

층수를 늘릴 수도 있고, 각 층의 면적을 넓힐 수도 있고, 각 방의 크기를 키울 수도 있습니다. 신경망 모델도 마찬가지입니다.

레이어를 더 쌓거나, 각 레이어의 채널 수를 늘리거나, 입력 이미지의 해상도를 높일 수 있습니다. 모델 스케일링이 없던 시절에는 어땠을까요?

연구자들은 주먹구구식으로 모델을 키웠습니다. "일단 레이어를 두 배로 늘려볼까?" 또는 "채널을 128에서 256으로 올려볼까?" 같은 식이었습니다.

문제는 이런 방식이 매우 비효율적이라는 것입니다. 레이어만 깊게 쌓으면 학습이 어려워지고, 채널만 늘리면 계산량이 폭발적으로 증가했습니다.

더 큰 문제는 최적의 균형을 찾기가 거의 불가능했다는 점입니다. 깊이, 너비, 해상도 중 어떤 것을 얼마나 조정해야 하는지 알 수 없었습니다.

수백 번의 실험을 해도 "이게 최선인가?"라는 의문만 남았습니다. 바로 이런 문제를 체계적으로 해결하기 위해 모델 스케일링 전략이 등장했습니다.

**Depth Scaling(깊이 스케일링)**은 레이어의 개수를 늘리는 방법입니다. ResNet이 대표적인 예인데, 152개의 레이어를 쌓아서 높은 성능을 달성했습니다.

깊은 모델은 더 복잡한 패턴을 학습할 수 있지만, 너무 깊어지면 Vanishing Gradient 문제가 발생합니다. **Width Scaling(너비 스케일링)**은 각 레이어의 채널 수를 늘리는 방법입니다.

채널이 많을수록 더 다양한 특징을 포착할 수 있습니다. 하지만 채널만 늘리면 얕은 네트워크의 한계를 극복하기 어렵습니다.

**Resolution Scaling(해상도 스케일링)**은 입력 이미지의 크기를 키우는 방법입니다. 224x224 대신 299x299나 331x331을 사용하는 식입니다.

해상도가 높으면 미세한 패턴까지 잡아낼 수 있지만, 계산량이 제곱으로 증가합니다. 위의 코드를 살펴보겠습니다.

첫 번째 예시는 Depth Scaling입니다. 같은 32채널 Conv2D 레이어를 4개로 늘렸습니다.

레이어가 많아지면 더 깊은 특징을 학습할 수 있지만, 학습 난이도가 올라갑니다. 두 번째 예시는 Width Scaling입니다.

레이어 개수는 그대로지만 각 레이어의 채널을 32에서 64로 두 배 늘렸습니다. 더 많은 필터가 서로 다른 특징을 잡아냅니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 의료 영상 분석 서비스를 개발한다고 가정해봅시다.

작은 병변을 정확히 찾아내려면 고해상도 입력이 필요합니다. 하지만 해상도만 올리면 GPU 메모리가 부족해집니다.

이때 깊이와 너비를 함께 조정하면 더 나은 결과를 얻을 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 한 가지 차원만 무작정 늘리는 것입니다. "일단 레이어를 100개로 만들어볼까?" 같은 생각은 위험합니다.

균형 없이 한쪽만 키우면 성능 향상은 미미하고 계산 비용만 폭발적으로 증가합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨가 웃으며 말했습니다. "깊이만 늘리는 것보다, 깊이, 너비, 해상도를 함께 고려해야 해요.

그게 바로 EfficientNet이 제안한 방법이죠." 전통적인 스케일링 전략들을 이해하면, 왜 새로운 접근법이 필요한지 알 수 있습니다. 다음 카드에서는 이 세 가지를 동시에 조정하는 Compound Scaling을 배워보겠습니다.

실전 팁

💡 - 깊이만 늘리면 Vanishing Gradient 문제가 생길 수 있으니 Residual Connection을 고려하세요

  • 너비를 늘릴 때는 메모리 사용량이 제곱으로 증가한다는 점을 기억하세요
  • 해상도를 올리기 전에 데이터 증강을 먼저 시도해보는 것도 좋은 방법입니다

2. Compound Scaling

박시니어 씨가 화이트보드에 그림을 그리며 설명을 시작했습니다. "깊이, 너비, 해상도를 따로따로 조정하면 비효율적이에요.

마치 자동차 엔진만 좋게 만들고 타이어는 그대로 두는 것과 같죠." 김개발 씨는 고개를 끄덕이며 궁금해졌습니다. 그럼 어떻게 해야 할까요?

Compound Scaling은 한마디로 깊이, 너비, 해상도를 동시에 균형있게 조정하는 기법입니다. 마치 레시피에서 재료의 비율을 일정하게 유지하며 전체 양을 늘리는 것과 같습니다.

EfficientNet은 이 세 가지 차원을 하나의 **복합 계수(compound coefficient)**로 제어하여 최적의 균형을 자동으로 찾아냅니다.

다음 코드를 살펴봅시다.

# Compound Scaling 수식
# depth = alpha ^ phi
# width = beta ^ phi
# resolution = gamma ^ phi
# 제약조건: alpha * beta^2 * gamma^2 ≈ 2

def compound_scaling(base_depth, base_width, base_resolution, phi):
    """
    phi: 복합 계수 (사용자가 조정하는 단일 값)
    alpha, beta, gamma: 그리드 서치로 찾은 최적 비율
    """
    alpha = 1.2  # depth 계수
    beta = 1.1   # width 계수
    gamma = 1.15 # resolution 계수

    # 세 차원을 동시에 스케일링
    new_depth = base_depth * (alpha ** phi)
    new_width = base_width * (beta ** phi)
    new_resolution = base_resolution * (gamma ** phi)

    return new_depth, new_width, new_resolution

# EfficientNet-B0 -> B1 예시
d, w, r = compound_scaling(base_depth=1.0, base_width=1.0,
                           base_resolution=224, phi=1)
print(f"Depth: {d:.2f}, Width: {w:.2f}, Resolution: {r:.0f}")

김개발 씨는 박시니어 씨의 설명을 듣고 고개를 갸우뚱했습니다. "그럼 깊이, 너비, 해상도를 각각 얼마씩 늘려야 하나요?

일일이 실험해야 하나요?" 박시니어 씨가 웃으며 대답했습니다. "그게 바로 핵심이에요.

EfficientNet 연구팀이 이미 최적의 비율을 찾아뒀어요." 그렇다면 Compound Scaling이란 정확히 무엇일까요? 쉽게 비유하자면, Compound Scaling은 마치 케이크 레시피와 같습니다.

케이크를 두 배로 만들 때 밀가루만 두 배로 늘리면 안 됩니다. 계란, 설탕, 버터도 모두 같은 비율로 늘려야 맛있는 케이크가 완성됩니다.

신경망도 마찬가지입니다. 깊이, 너비, 해상도가 모두 조화롭게 증가해야 최적의 성능을 냅니다.

전통적인 방식의 문제점은 무엇이었을까요? ResNet을 예로 들어봅시다.

ResNet-50에서 ResNet-101로 넘어갈 때 레이어만 두 배로 늘렸습니다. 성능은 올라갔지만, 효율성은 떨어졌습니다.

왜냐하면 깊이만 늘리고 너비와 해상도는 그대로 두었기 때문입니다. 마치 키만 크고 몸무게는 그대로인 불균형한 상태였습니다.

연구자들은 이 문제를 해결하기 위해 수백 번의 실험을 했습니다. "깊이를 1.5배 늘리고, 너비를 1.2배 늘리고, 해상도를 1.3배 늘려볼까?" 하지만 경우의 수가 너무 많아서 최적점을 찾기가 거의 불가능했습니다.

바로 이런 문제를 해결하기 위해 Compound Scaling이 등장했습니다. EfficientNet 연구팀은 **그리드 서치(Grid Search)**를 통해 최적의 스케일링 비율을 발견했습니다.

그 결과가 바로 alpha, beta, gamma 값입니다. 이 세 값은 다음과 같은 제약 조건을 만족합니다.

alpha × beta² × gamma² ≈ 2 왜 제곱일까요? 너비(채널 수)를 두 배로 늘리면 연산량은 네 배가 됩니다.

해상도를 두 배로 늘려도 픽셀 수가 네 배가 되므로 연산량이 네 배입니다. 따라서 제곱으로 표현한 것입니다.

사용자는 단 하나의 값 **phi(파이)**만 조정하면 됩니다. phi를 1로 설정하면 세 차원이 모두 적당히 증가하고, phi를 2로 설정하면 더 크게 증가합니다.

이렇게 하나의 노브만 돌리면 모델 크기를 자유자재로 조절할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 alpha, beta, gamma는 EfficientNet 논문에서 그리드 서치로 찾은 최적값입니다. 이 값들은 ImageNet 데이터셋에서 실험적으로 검증되었습니다.

다음으로 compound_scaling 함수는 phi 값 하나를 입력받아서 세 차원의 새로운 크기를 계산합니다. phi=1이면 약간 커지고, phi=2면 더 많이 커집니다.

마지막 출력 결과를 보면, phi=1일 때 깊이는 1.2배, 너비는 1.1배, 해상도는 1.15배가 됩니다. 이 비율은 수많은 실험을 통해 최적화된 값입니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 모바일 앱용 경량 모델이 필요하다면 EfficientNet-B0을 사용합니다.

정확도를 조금 더 높이고 싶다면 phi를 1로 설정해서 B1을 만듭니다. 서버 환경에서 최고 성능이 필요하다면 phi를 7로 설정해서 B7을 만듭니다.

이렇게 하나의 베이스 모델에서 다양한 크기의 모델을 쉽게 파생시킬 수 있습니다. 구글의 여러 팀에서 이 방식을 적극 활용하고 있습니다.

같은 아키텍처로 모바일부터 서버까지 커버할 수 있기 때문에 개발 및 유지보수 비용이 크게 줄어듭니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 alpha, beta, gamma 값을 임의로 바꾸는 것입니다. "내 데이터셋에는 해상도가 더 중요할 것 같은데, gamma를 2로 올려볼까?" 이렇게 하면 균형이 깨져서 오히려 성능이 떨어질 수 있습니다.

논문에서 제시한 값을 그대로 사용하는 것이 안전합니다. 또 다른 실수는 phi를 너무 크게 설정하는 것입니다.

phi=10으로 설정하면 모델이 엄청나게 커지고 학습이 불가능해질 수 있습니다. 대부분의 경우 phi는 0에서 7 사이 값을 사용합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 코드를 가리키며 말했습니다.

"보세요, phi 값 하나만 바꾸면 모델 크기를 자유롭게 조절할 수 있어요. 이게 Compound Scaling의 핵심이에요." 김개발 씨는 감탄했습니다.

"와, 이렇게 간단한 방법으로 최적의 균형을 찾을 수 있다니 신기해요!" Compound Scaling을 제대로 이해하면 모델을 효율적으로 스케일링할 수 있습니다. 여러분도 자신의 프로젝트에서 EfficientNet의 phi 값을 조정해 보세요.

실전 팁

💡 - alpha, beta, gamma 값은 논문의 기본값을 사용하는 것이 안전합니다

  • phi는 07 사이에서 선택하세요 (B0B7에 해당)
  • 자신의 데이터셋에 맞는 최적 phi 값을 찾으려면 작은 값부터 시작해서 점진적으로 늘려보세요

3. EfficientNet 아키텍처

김개발 씨가 물었습니다. "Compound Scaling은 알겠는데, EfficientNet의 베이스 모델은 어떻게 생겼나요?" 박시니어 씨가 노트북을 열어 논문 그림을 보여주며 답했습니다.

"EfficientNet-B0이라는 작고 효율적인 베이스 모델이 있어요. 이걸 Compound Scaling으로 키우는 거죠."

EfficientNet 아키텍처는 한마디로 MBConv 블록을 쌓아 만든 효율적인 CNN입니다. 마치 레고 블록을 쌓듯이 동일한 구조의 MBConv 블록을 여러 개 연결합니다.

베이스 모델인 B0는 **Neural Architecture Search(NAS)**로 자동 설계되어, 사람이 수동으로 만든 모델보다 훨씬 효율적입니다.

다음 코드를 살펴봅시다.

# EfficientNet-B0 전체 구조 (간소화 버전)
from tensorflow.keras import layers, models

def efficientnet_b0(input_shape=(224, 224, 3), num_classes=1000):
    inputs = layers.Input(shape=input_shape)

    # Stem: 첫 번째 Conv 레이어
    x = layers.Conv2D(32, 3, strides=2, padding='same')(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('swish')(x)  # ReLU 대신 Swish

    # MBConv 블록들 (총 7개 스테이지)
    # Stage 1: MBConv1, k3x3
    x = mbconv_block(x, filters=16, expand_ratio=1, stride=1)

    # Stage 2: MBConv6, k3x3
    x = mbconv_block(x, filters=24, expand_ratio=6, stride=2)
    x = mbconv_block(x, filters=24, expand_ratio=6, stride=1)

    # ... (중간 스테이지 생략)

    # Head: 최종 분류 레이어
    x = layers.GlobalAveragePooling2D()(x)
    x = layers.Dense(num_classes, activation='softmax')(x)

    return models.Model(inputs, x)

김개발 씨는 화면에 표시된 EfficientNet 구조도를 보며 놀랐습니다. "생각보다 단순한데요?" 박시니어 씨가 고개를 끄덕였습니다.

"맞아요. 복잡해 보이지만 사실 같은 블록을 반복하는 구조예요.

그게 바로 EfficientNet의 강점이죠." 그렇다면 EfficientNet 아키텍처란 정확히 무엇일까요? 쉽게 비유하자면, EfficientNet은 마치 조립식 가구와 같습니다.

기본 부품(MBConv 블록) 하나를 잘 설계한 다음, 그것을 여러 번 조립해서 완성품을 만듭니다. 각 부품의 크기와 개수만 조정하면 다양한 크기의 가구를 만들 수 있습니다.

전통적인 CNN 설계 방식은 어땠을까요? VGG, ResNet, Inception 같은 유명한 모델들은 모두 연구자가 손으로 직접 설계했습니다.

"여기에 3x3 Conv를 넣고, 저기에는 1x1 Conv를 넣어볼까?" 같은 식으로 수많은 시행착오를 거쳤습니다. 문제는 이렇게 만든 모델이 정말 최적인지 보장할 수 없다는 점입니다.

더 큰 문제는 확장성입니다. ResNet-50을 ResNet-101로 키울 때 단순히 레이어만 복사했습니다.

각 레이어의 채널 수나 구조는 크게 고민하지 않았습니다. 그 결과 비효율적인 구조가 만들어졌습니다.

바로 이런 문제를 해결하기 위해 EfficientNet은 **Neural Architecture Search(NAS)**를 사용했습니다. NAS는 한마디로 AI가 AI를 설계하는 기술입니다.

수천 개의 후보 구조를 자동으로 만들고, 각각을 학습시켜서 성능을 평가합니다. 그중에서 정확도는 높고 연산량은 적은 최적의 구조를 선택합니다.

EfficientNet-B0는 이런 과정을 거쳐 탄생한 베이스 모델입니다. B0의 구조는 다음과 같습니다.

먼저 Stem이라 불리는 첫 번째 Conv 레이어가 입력 이미지를 처리합니다. 224×224×3 이미지가 112×112×32 특징맵으로 변환됩니다.

다음으로 7개의 MBConv 스테이지가 이어집니다. 각 스테이지는 동일한 해상도의 MBConv 블록을 여러 개 쌓은 구조입니다.

Stage 1은 해상도가 112×112이고, Stage 2는 56×56으로 줄어듭니다. 이렇게 점진적으로 해상도를 낮추며 채널 수를 늘립니다.

마지막으로 Head에서 Global Average Pooling과 Fully Connected Layer로 최종 분류를 수행합니다. 위의 코드를 살펴보겠습니다.

첫 번째 Stem 부분에서는 Swish 활성화 함수를 사용합니다. ReLU보다 성능이 좋다고 알려진 함수입니다.

수식은 x × sigmoid(x)입니다. 다음으로 MBConv 블록들이 쌓입니다.

Stage 1에서는 expand_ratio=1을 사용하고, Stage 2부터는 expand_ratio=6을 사용합니다. 이 숫자가 무엇을 의미하는지는 다음 카드에서 자세히 배웁니다.

각 스테이지에서 stride=2인 블록은 해상도를 절반으로 줄이고, stride=1인 블록은 해상도를 유지합니다. 이렇게 점진적으로 다운샘플링하며 고수준 특징을 추출합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 제조업 품질 검사 시스템을 개발한다고 가정해봅시다.

결함을 빠르게 탐지해야 하므로 경량 모델이 필요합니다. 이때 EfficientNet-B0을 사용하면 MobileNetV2보다 정확도는 높으면서도 속도는 비슷합니다.

반대로 의료 영상 진단처럼 최고 정확도가 필요한 경우에는 EfficientNet-B7을 사용합니다. 같은 아키텍처이므로 코드 수정 없이 모델만 교체하면 됩니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 EfficientNet의 구조를 임의로 수정하는 것입니다.

"Stage를 하나 더 추가하면 성능이 오르지 않을까?" 같은 생각은 위험합니다. NAS로 최적화된 구조이므로 함부로 바꾸면 균형이 깨집니다.

또 다른 실수는 Swish 활성화 함수를 ReLU로 바꾸는 것입니다. 일부 구형 하드웨어에서는 Swish를 지원하지 않아서 ReLU로 교체하는 경우가 있는데, 이렇게 하면 성능이 2~3% 하락합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 코드를 가리키며 말했습니다.

"보세요, MBConv 블록만 이해하면 전체 구조를 파악할 수 있어요." 김개발 씨는 고개를 끄덕이며 다음 질문을 던졌습니다. "그럼 MBConv 블록은 어떻게 생겼나요?" EfficientNet의 전체 구조를 이해하면 왜 이 모델이 효율적인지 알 수 있습니다.

다음 카드에서는 핵심 빌딩 블록인 MBConv를 자세히 살펴보겠습니다.

실전 팁

💡 - EfficientNet의 구조는 NAS로 최적화되었으므로 함부로 수정하지 마세요

  • Swish 활성화 함수는 성능 향상에 중요하므로 가급적 유지하세요
  • Transfer Learning을 할 때는 Head 부분만 교체하고 나머지는 그대로 두는 것이 좋습니다

4. MBConv 블록 이해

김개발 씨가 코드를 들여다보며 물었습니다. "MBConv가 뭔가요?

처음 보는 이름인데요." 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. "Mobile Inverted Bottleneck Convolution의 약자예요.

MobileNetV2에서 처음 제안된 구조인데, EfficientNet의 핵심 블록이죠."

MBConv 블록은 한마디로 적은 연산으로 높은 성능을 내는 효율적인 Conv 블록입니다. 마치 압축 파일을 풀었다가 다시 압축하는 것처럼, 채널을 확장했다가 다시 줄입니다.

이 과정에서 Depthwise ConvolutionSqueeze-and-Excitation을 사용하여 연산량을 획기적으로 줄입니다.

다음 코드를 살펴봅시다.

# MBConv 블록 구현
def mbconv_block(inputs, filters, expand_ratio, stride):
    """
    inputs: 입력 텐서
    filters: 출력 채널 수
    expand_ratio: 확장 비율 (보통 6)
    stride: 스트라이드 (1 또는 2)
    """
    # 1. Expansion: 채널 확장 (1x1 Conv)
    expanded_channels = inputs.shape[-1] * expand_ratio
    x = layers.Conv2D(expanded_channels, 1, padding='same')(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('swish')(x)

    # 2. Depthwise Conv: 공간 정보 추출 (3x3)
    x = layers.DepthwiseConv2D(3, strides=stride, padding='same')(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('swish')(x)

    # 3. Squeeze-and-Excitation: 채널 어텐션
    se = layers.GlobalAveragePooling2D()(x)
    se = layers.Dense(expanded_channels // 4, activation='swish')(se)
    se = layers.Dense(expanded_channels, activation='sigmoid')(se)
    x = layers.Multiply()([x, se])

    # 4. Projection: 채널 축소 (1x1 Conv)
    x = layers.Conv2D(filters, 1, padding='same')(x)
    x = layers.BatchNormalization()(x)

    # 5. Skip Connection (stride=1일 때만)
    if stride == 1 and inputs.shape[-1] == filters:
        x = layers.Add()([inputs, x])

    return x

김개발 씨는 화이트보드의 그림을 보며 고개를 갸우뚱했습니다. "채널을 늘렸다가 다시 줄인다고요?

왜 그런 복잡한 과정을 거치나요?" 박시니어 씨가 웃으며 답했습니다. "언뜻 비효율적으로 보이지만, 실제로는 엄청난 연산량 절감 효과가 있어요." 그렇다면 MBConv 블록이란 정확히 무엇일까요?

쉽게 비유하자면, MBConv는 마치 여행 가방을 싸는 과정과 같습니다. 옷을 가방에 넣기 전에 먼저 펼쳐서 공기를 빼고(확장), 돌돌 말아서 부피를 줄인 다음(Depthwise Conv), 중요한 옷만 골라내고(SE), 마지막으로 압축백에 넣습니다(Projection).

이렇게 하면 같은 크기의 가방에 더 많은 옷을 넣을 수 있습니다. 전통적인 Convolution의 문제점은 무엇이었을까요?

일반적인 3×3 Conv는 모든 입력 채널과 출력 채널을 연결합니다. 입력이 64채널, 출력이 128채널이라면 64×128×3×3 = 73,728개의 파라미터가 필요합니다.

채널이 많아질수록 파라미터가 폭발적으로 증가합니다. MobileNet 이전에는 이 문제를 해결할 방법이 마땅치 않았습니다.

모델을 경량화하려면 채널 수를 줄여야 했고, 그러면 성능이 크게 떨어졌습니다. 바로 이런 문제를 해결하기 위해 MBConv 블록이 등장했습니다.

MBConv의 핵심 아이디어는 Depthwise Separable Convolution입니다. 일반 Conv를 두 단계로 분리합니다.

첫 번째는 Depthwise Conv로 각 채널을 독립적으로 처리하고, 두 번째는 1×1 Conv로 채널 간 정보를 섞습니다. 위의 코드를 단계별로 살펴보겠습니다.

첫 번째 단계: Expansion 1×1 Conv로 채널을 expand_ratio배 만큼 늘립니다. 보통 6배를 사용합니다.

예를 들어 입력이 24채널이면 144채널로 확장합니다. 왜 늘릴까요?

Depthwise Conv는 채널 간 정보를 섞지 못하므로, 미리 채널을 늘려서 표현력을 높입니다. 두 번째 단계: Depthwise Conv 3×3 Depthwise Conv로 공간 정보를 추출합니다.

일반 Conv와 달리 각 채널을 독립적으로 처리합니다. 144채널이면 144개의 3×3 필터만 사용합니다.

파라미터가 144×9 = 1,296개로 엄청나게 줄어듭니다. 세 번째 단계: Squeeze-and-Excitation (SE) 채널별 중요도를 계산하여 가중치를 부여합니다.

Global Average Pooling으로 각 채널의 평균값을 구하고, 작은 FC Layer를 거쳐 0~1 사이의 가중치를 만듭니다. 중요한 채널은 강조하고 불필요한 채널은 억제합니다.

네 번째 단계: Projection 1×1 Conv로 채널을 원하는 개수로 줄입니다. 144채널을 40채널로 축소하는 식입니다.

이 과정에서 활성화 함수를 사용하지 않는데, 이를 Linear Bottleneck이라고 부릅니다. 다섯 번째 단계: Skip Connection stride=1이고 입출력 채널이 같으면 Skip Connection을 추가합니다.

ResNet처럼 gradient flow를 개선하는 효과가 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 스마트폰에서 실시간 객체 인식 앱을 개발한다고 가정해봅시다. 일반 Conv를 사용하면 배터리 소모가 심하고 발열이 심합니다.

MBConv를 사용하면 연산량이 1/10로 줄어들어 실시간 처리가 가능해집니다. Edge AI 디바이스에서도 MBConv는 필수입니다.

제한된 메모리와 연산 능력으로 높은 정확도를 내야 하기 때문입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 expand_ratio를 너무 크게 설정하는 것입니다. "6배 대신 12배로 하면 성능이 더 오르지 않을까?" 하지만 실험 결과 6배가 최적입니다.

그 이상은 메모리만 낭비합니다. 또 다른 실수는 SE 블록을 생략하는 것입니다.

SE 블록은 파라미터가 적지만 성능 향상 효과가 큽니다. 약 1~2%의 정확도 개선이 있으므로 반드시 포함해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 코드를 가리키며 설명했습니다.

"보세요, Depthwise Conv 덕분에 파라미터가 수십 배 줄었죠? 이게 MBConv의 핵심이에요." 김개발 씨는 감탄했습니다.

"와, 이렇게 똑똑한 방법이 있었다니!" MBConv 블록을 제대로 이해하면 EfficientNet의 효율성을 이해할 수 있습니다. 여러분도 자신의 모델에 MBConv를 적용해 보세요.

실전 팁

💡 - expand_ratio는 6을 사용하는 것이 일반적입니다 (실험적으로 검증됨)

  • SE 블록은 성능 향상에 중요하므로 생략하지 마세요
  • Depthwise Conv는 GPU보다 모바일 하드웨어에서 더 효율적입니다

5. EfficientNetV2 개선점

김개발 씨가 논문을 읽다가 궁금해졌습니다. "EfficientNet도 충분히 좋은데, 왜 V2가 나왔나요?" 박시니어 씨가 답했습니다.

"V1은 학습 속도가 느리고, 큰 이미지를 처리할 때 메모리 문제가 있었어요. V2는 이걸 해결했죠."

EfficientNetV2는 한마디로 학습 속도를 획기적으로 개선한 버전입니다. 마치 자동차 엔진을 튜닝해서 연비와 속도를 동시에 높인 것과 같습니다.

Fused-MBConv라는 새로운 블록과 Progressive Learning이라는 학습 기법으로 V1보다 5~11배 빠른 학습이 가능합니다.

다음 코드를 살펴봅시다.

# Fused-MBConv 블록 (EfficientNetV2의 새로운 블록)
def fused_mbconv_block(inputs, filters, expand_ratio, stride):
    """
    Depthwise Conv를 일반 Conv로 대체한 버전
    초기 레이어에서 사용 (해상도가 큰 구간)
    """
    expanded_channels = inputs.shape[-1] * expand_ratio

    # 1. Fused Expansion + Spatial Conv (3x3)
    # Expansion과 Depthwise를 하나의 3x3 Conv로 통합
    x = layers.Conv2D(expanded_channels, 3, strides=stride, padding='same')(inputs)
    x = layers.BatchNormalization()(x)
    x = layers.Activation('swish')(x)

    # 2. SE 블록 (선택적)
    # Fused-MBConv는 SE를 사용하지 않는 경우가 많음

    # 3. Projection (1x1 Conv)
    x = layers.Conv2D(filters, 1, padding='same')(x)
    x = layers.BatchNormalization()(x)

    # 4. Skip Connection
    if stride == 1 and inputs.shape[-1] == filters:
        x = layers.Add()([inputs, x])

    return x

# Progressive Learning 예시
def progressive_training(model, epochs=300):
    """
    작은 이미지부터 시작해서 점진적으로 크기와 regularization 증가
    """
    stages = [
        {'epochs': 100, 'image_size': 128, 'dropout': 0.1, 'mixup': 0.0},
        {'epochs': 100, 'image_size': 192, 'dropout': 0.2, 'mixup': 0.2},
        {'epochs': 100, 'image_size': 256, 'dropout': 0.3, 'mixup': 0.4}
    ]
    # 단계별로 이미지 크기와 정규화 강도 증가

김개발 씨는 회사에서 EfficientNet-B7을 학습시키고 있었습니다. 하지만 며칠이 지나도 학습이 끝나지 않았습니다.

"왜 이렇게 느린 거죠?" 박시니어 씨가 다가와서 설명했습니다. "B7은 입력 해상도가 600×600이라서 메모리를 많이 먹어요.

배치 사이즈를 줄이다 보니 학습이 느려지는 거죠." 그렇다면 EfficientNetV2는 무엇을 개선했을까요? 쉽게 비유하자면, V2는 마치 고속도로를 확장한 것과 같습니다.

V1은 좁은 도로에서 큰 트럭(고해상도 이미지)을 운반하려다 보니 정체가 심했습니다. V2는 도로를 넓히고(Fused-MBConv), 교통 신호를 최적화해서(Progressive Learning) 흐름을 개선했습니다.

EfficientNetV1의 문제점은 무엇이었을까요? 첫 번째 문제는 학습 속도입니다.

Depthwise Conv는 파라미터는 적지만 하드웨어 활용도가 낮습니다. GPU나 TPU는 병렬 처리에 최적화되어 있는데, Depthwise Conv는 병렬화가 어렵습니다.

그 결과 이론적 연산량은 적어도 실제 학습 시간은 깁니다. 두 번째 문제는 메모리 사용량입니다.

고해상도 입력을 처리할 때 중간 activation의 메모리 사용량이 폭발적으로 증가합니다. 600×600 이미지를 배치 사이즈 32로 학습하려면 GPU 메모리가 부족해집니다.

세 번째 문제는 정규화입니다. 큰 모델일수록 강한 정규화가 필요한데, 작은 이미지에 강한 정규화를 적용하면 학습이 불안정해집니다.

바로 이런 문제를 해결하기 위해 EfficientNetV2가 등장했습니다. 첫 번째 개선: Fused-MBConv 초기 스테이지(해상도가 큰 구간)에서는 Depthwise Conv 대신 일반 3×3 Conv를 사용합니다.

파라미터는 약간 늘어나지만, 하드웨어 활용도가 크게 향상되어 실제 학습 속도는 더 빠릅니다. 후기 스테이지(해상도가 작은 구간)에서는 여전히 MBConv를 사용합니다.

해상도가 작아지면 메모리 문제가 덜하므로 파라미터 효율이 중요해지기 때문입니다. 두 번째 개선: 더 작은 Expansion Ratio V1은 expand_ratio=6을 사용했지만, V2는 일부 레이어에서 4를 사용합니다.

Expansion Ratio가 크면 중간 activation이 커져서 메모리를 많이 차지합니다. 4로 줄이면 메모리 효율이 개선되고, 성능 하락은 미미합니다.

세 번째 개선: Progressive Learning 작은 이미지(예: 128×128)로 학습을 시작해서 점진적으로 크기를 키웁니다(192→256). 동시에 Dropout과 Mixup 같은 정규화 강도도 함께 증가시킵니다.

초기에는 작은 이미지로 빠르게 학습하고, 후반에 큰 이미지로 fine-tuning합니다. 이렇게 하면 전체 학습 시간이 크게 줄어듭니다.

위의 코드를 살펴보겠습니다. Fused-MBConv는 Expansion과 Depthwise Conv를 하나의 3×3 Conv로 통합했습니다.

이렇게 하면 연산 횟수는 비슷하지만 메모리 접근 패턴이 개선되어 실제 속도가 빨라집니다. Progressive Training 코드를 보면, 세 단계로 나누어 점진적으로 난이도를 높입니다.

초기에는 128×128 이미지로 빠르게 수렴하고, 후반에는 256×256으로 세밀한 특징을 학습합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 대규모 데이터셋으로 모델을 학습해야 하는 경우, V2를 사용하면 학습 시간을 크게 단축할 수 있습니다. ImageNet을 V1으로 학습하면 일주일 걸리던 것이 V2로는 하루 만에 끝납니다.

또한 Transfer Learning을 할 때도 V2가 유리합니다. Pre-trained 모델을 fine-tuning할 때 Progressive Learning을 적용하면 더 빠르고 안정적으로 수렴합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 레이어를 Fused-MBConv로 바꾸는 것입니다.

Fused-MBConv는 초기 스테이지에서만 효과적입니다. 후기 스테이지에서는 파라미터 효율이 중요하므로 일반 MBConv를 사용해야 합니다.

또 다른 실수는 Progressive Learning 없이 처음부터 큰 이미지로 학습하는 것입니다. 이렇게 하면 V2의 속도 이점을 제대로 활용하지 못합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨가 말했습니다.

"V2로 바꾸고 Progressive Learning을 적용하면, 학습 시간이 절반으로 줄어들 거예요." 김개발 씨는 즉시 코드를 수정했고, 정말로 학습이 훨씬 빨라졌습니다. EfficientNetV2의 개선점을 이해하면 더 빠르고 효율적인 학습이 가능합니다.

여러분도 대규모 학습을 할 때 V2를 적극 활용해 보세요.

실전 팁

💡 - 초기 스테이지에는 Fused-MBConv, 후기 스테이지에는 MBConv를 사용하세요

  • Progressive Learning을 적용하면 학습 시간이 크게 단축됩니다
  • V2는 특히 고해상도 이미지 학습에서 메모리 효율이 뛰어납니다

6. 모델 선택 가이드

김개발 씨가 마지막으로 물었습니다. "EfficientNet-B0부터 B7까지 있는데, 어떤 걸 선택해야 하나요?" 박시니어 씨가 웃으며 답했습니다.

"프로젝트의 요구사항에 따라 다르죠. 속도가 중요하면 B0, 정확도가 중요하면 B7을 선택하면 됩니다."

모델 선택은 한마디로 속도와 정확도의 트레이드오프를 고려하는 과정입니다. 마치 자동차를 고를 때 연비와 성능을 저울질하는 것과 같습니다.

모바일 앱에는 EfficientNet-B0나 V2-S를, 서버 환경에는 B5 이상이나 V2-L을 선택하는 것이 일반적입니다.

다음 코드를 살펴봅시다.

# EfficientNet 모델 선택 가이드
import tensorflow as tf
from tensorflow.keras.applications import EfficientNetB0, EfficientNetB3, EfficientNetB7

def choose_model(deployment_target, accuracy_priority):
    """
    deployment_target: 'mobile', 'edge', 'server', 'cloud'
    accuracy_priority: 'speed', 'balanced', 'accuracy'
    """

    # 모바일/엣지 디바이스
    if deployment_target in ['mobile', 'edge']:
        if accuracy_priority == 'speed':
            return EfficientNetB0(weights='imagenet')  # 5.3M params
        elif accuracy_priority == 'balanced':
            return EfficientNetB1(weights='imagenet')  # 7.8M params
        else:
            return EfficientNetB3(weights='imagenet')  # 12M params

    # 서버/클라우드
    elif deployment_target in ['server', 'cloud']:
        if accuracy_priority == 'speed':
            return EfficientNetB3(weights='imagenet')
        elif accuracy_priority == 'balanced':
            return EfficientNetB5(weights='imagenet')  # 30M params
        else:
            return EfficientNetB7(weights='imagenet')  # 66M params

    return EfficientNetB0(weights='imagenet')

# 실전 예시: Transfer Learning
base_model = choose_model('mobile', 'balanced')  # B1 선택
base_model.trainable = False  # Feature Extractor로 사용

# 커스텀 Head 추가
inputs = tf.keras.Input(shape=(224, 224, 3))
x = base_model(inputs, training=False)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
outputs = tf.keras.layers.Dense(10, activation='softmax')(x)
model = tf.keras.Model(inputs, outputs)

김개발 씨는 회사의 신규 프로젝트에 투입되었습니다. 스마트폰 앱에서 실시간으로 음식 사진을 분류하는 기능을 개발해야 합니다.

"어떤 모델을 쓸까요?" 박시니어 씨가 질문을 던졌습니다. "앱이 서버에 이미지를 보내서 처리할 건가요, 아니면 스마트폰에서 직접 처리할 건가요?" 김개발 씨는 잠시 생각하다가 답했습니다.

"스마트폰에서 직접 처리해야 해요. 인터넷 없이도 작동해야 하거든요." 그렇다면 어떤 모델을 선택해야 할까요?

쉽게 비유하자면, 모델 선택은 마치 여행 가방을 고르는 것과 같습니다. 짧은 출장에는 작은 캐리어면 충분하지만, 장기 여행에는 큰 트렁크가 필요합니다.

프로젝트도 마찬가지입니다. 실시간 처리가 필요하면 경량 모델을, 최고 정확도가 필요하면 대형 모델을 선택해야 합니다.

잘못된 모델을 선택하면 어떤 일이 벌어질까요? 모바일 앱에 EfficientNet-B7을 넣으면 어떻게 될까요?

파라미터가 6600만 개나 되어서 앱 크기가 250MB를 넘어갑니다. 실행하면 스마트폰이 뜨거워지고 배터리가 금방 닳습니다.

추론 속도도 느려서 사용자가 불편을 겪습니다. 반대로 서버 환경에서 높은 정확도가 필요한데 B0를 사용하면 어떨까요?

속도는 빠르지만 정확도가 목표에 미치지 못합니다. 경쟁사 제품보다 성능이 낮아서 사용자가 떠나갈 수 있습니다.

바로 이런 실수를 피하기 위해 프로젝트 요구사항에 맞는 모델을 선택해야 합니다. 모바일/엣지 디바이스용 선택 가이드 EfficientNet-B0: 530만 개 파라미터, 앱 크기 20MB, 추론 속도 30ms(스마트폰 기준).

실시간 처리가 필수이고 정확도 요구사항이 낮은 경우에 적합합니다. 예: 간단한 제스처 인식, 바코드 스캐너.

EfficientNet-B1: 780만 개 파라미터, 앱 크기 30MB, 추론 속도 50ms. 속도와 정확도의 균형이 좋습니다.

예: 음식 분류, 상품 검색. EfficientNet-B3: 1200만 개 파라미터, 앱 크기 45MB, 추론 속도 100ms.

정확도가 중요하고 약간의 지연은 허용되는 경우. 예: 의료 영상 보조 진단.

서버/클라우드용 선택 가이드 EfficientNet-B3: 서버에서 빠른 응답이 필요한 경우의 최소 모델. 예: 대량 이미지 자동 태깅.

EfficientNet-B5: 3000만 개 파라미터, 추론 속도 200ms(GPU 기준). 정확도와 속도의 균형이 우수.

예: 검색 엔진 이미지 분류. EfficientNet-B7: 6600만 개 파라미터, 추론 속도 400ms.

최고 정확도가 필요한 경우. 예: 자율주행 객체 인식, 위성 영상 분석.

EfficientNetV2 시리즈 V1보다 학습 속도가 빠르고 정확도도 약간 높습니다. 특히 고해상도 이미지 처리에 유리합니다.

V2-S (Small): B0B1 대체용, 모바일 최적화. V2-M (Medium): B3B4 대체용, 범용.

V2-L (Large): B5~B7 대체용, 고성능 서버. 위의 코드를 살펴보겠습니다.

choose_model 함수는 배포 환경과 우선순위를 입력받아 적절한 모델을 반환합니다. 예를 들어 모바일에서 속도가 중요하면 B0을, 균형이 중요하면 B1을 선택합니다.

Transfer Learning 예시에서는 선택한 모델의 가중치를 동결하고(trainable=False), 커스텀 분류 Head만 학습시킵니다. 이렇게 하면 적은 데이터로도 빠르게 학습할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 네이버 쇼핑의 이미지 검색 서비스를 예로 들어봅시다.

사용자가 상품 사진을 업로드하면 서버에서 유사한 상품을 찾아줍니다. 정확도가 매우 중요하므로 EfficientNet-B7을 사용합니다.

GPU 서버가 여러 대 있어서 속도는 문제없습니다. 반면 구글 렌즈처럼 스마트폰에서 작동하는 앱은 EfficientNet-B0이나 V2-S를 사용합니다.

약간의 정확도를 희생하더라도 실시간 처리가 중요하기 때문입니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 무조건 가장 큰 모델을 선택하는 것입니다. "B7이 제일 좋다고 했으니 이걸 쓰자!" 하지만 프로젝트 요구사항을 고려하지 않으면 과도한 리소스 낭비가 발생합니다.

또 다른 실수는 벤치마크 점수만 보고 판단하는 것입니다. ImageNet Top-1 Accuracy가 1% 높다고 해서 실제 프로젝트에서도 그만큼 좋은 것은 아닙니다.

자신의 데이터셋으로 직접 테스트해봐야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨가 조언했습니다. "스마트폰 앱이니까 B1을 먼저 시도해보세요.

성능이 부족하면 B2로 올리고, 너무 느리면 B0로 내리면 됩니다." 김개발 씨는 B1으로 프로토타입을 만들었고, 속도와 정확도 모두 만족스러운 결과를 얻었습니다. 실전 선택 플로우차트


4. 고해상도 입력(384 이상)인가? → Yes: V2 시리즈 우선

실전 팁

💡 - 처음에는 중간 크기(B3 또는 V2-M)로 시작해서 요구사항에 따라 조정하세요

  • 실제 배포 환경에서 추론 속도를 직접 측정해보는 것이 중요합니다
  • Transfer Learning을 할 때는 베이스 모델의 가중치를 동결하고 Head만 학습시키세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#EfficientNet#CompoundScaling#MBConv#CNN#ModelOptimization#EfficientNet,CNN

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.