모델 효율성 평가의 핵심 지표: FLOPs와 Parameters
딥러닝 모델을 실제 하드웨어나 엣지 디바이스에 배포하기 전, 반드시 거쳐야 하는 과정이 모델의 연산 복잡도를 분석하는 것입니다. 이때 가장 핵심이 되는 지표는 FLOPs(Floating Point Operations)와 파라미터 수(Parameters)입니다. FLOPs는 모델의 추론 속도와 전력 소모에 직접적인 영향을 미치며, 파라미터 수는 메모리 점유율 및 대역폭 요구사항과 직결됩니다.
단순히 라이브러리를 호출해 결과값만 확인하는 것을 넘어, 내부 계산 로직을 이해하고 직접 분석 도구를 구현해 보면 모델 설계 단계에서부터 성능 병목을 예측하고 최적화할 수 있는 통찰력을 얻을 수 있습니다.
합성곱 계층(Convolutional Layer)의 연산량 분석
가장 먼저 표준적인 컨볼루션 연산의 비용을 계산해 보겠습니다. 연산량은 출력 피처 맵의 각 위치에서 필터(커널)가 수행하는 곱셈과 덧셈의 총합으로 결정됩니다.
입력 텐서의 크기를 (in_ch, in_h, in_w), 출력 채널을 out_ch, 커널 크기를 k_size, 스트라이드(Stride)를 s, 패딩(Padding)을 p라고 할 때, 출력 피처 맵의 가로/세로 크기는 다음과 같습니다.
out_h = floor((in_h - k_size + 2 * p) / s) + 1
out_w = floor((in_w - k_size + 2 * p) / s) + 1
출력 피처 맵의 한 점을 만들기 위해 in_ch * k_size * k_size 만큼의 곱셈 연산과 그에 상응하는 덧셈 연산이 필요합니다. 따라서 전체 FLOPs는 다음과 같이 계산됩니다 (곱셈과 덧셈을 각각 1회 연산으로 간주하여 2를 곱함):
Total_FLOPs = out_ch * out_h * out_w * (in_ch * k_size * k_size * 2)
파라미터 수는 커널 가중치와 편향(Bias)의 합입니다.
Total_Params = out_ch * (in_ch * k_size * k_size) + out_ch (편향 포함 시)
연산 복잡도 비교 예시
아래 표는 다양한 컨볼루션 구성에 따른 이론적 비용 차이를 보여줍니다.
| 레이어 유형 | 입력 (C, H, W) | 커널 (Out, K, S, P) | FLOPs (M) | 파라미터 (K) |
|---|---|---|---|---|
| 초기 특징 추출 | (3, 224, 224) | (64, 7, 2, 3) | 약 118 | 9.4 |
| 표준 3x3 Conv | (256, 56, 56) | (512, 3, 1, 1) | 약 37,700 | 1,179 |
| Depthwise Separable | (512, 28, 28) | (512, 3, 1, 1) | 약 2,300 | 5.1 |
Python을 활용한 분석 도구 구현
이제 수식을 코드로 옮겨 CNN의 레이어별 복잡도를 자동으로 계산하는 간단한 분석기 클래스를 작성해 보겠습니다.
import math
class ModelAnalyzer:
def __init__(self):
self.total_flops = 0
self.total_params = 0
def analyze_conv2d(self, in_ch, out_ch, kernel_size, input_res, stride=1, padding=0, use_bias=True):
# 출력 해상도 계산
res_h = math.floor((input_res[0] - kernel_size + 2 * padding) / stride) + 1
res_w = math.floor((input_res[1] - kernel_size + 2 * padding) / stride) + 1
# 파라미터 수 계산
weight_params = out_ch * in_ch * kernel_size * kernel_size
bias_params = out_ch if use_bias else 0
current_params = weight_params + bias_params
# FLOPs 계산 (곱셈+덧셈 2회 가정)
ops_per_pixel = in_ch * kernel_size * kernel_size * 2
current_flops = res_h * res_w * out_ch * ops_per_pixel
self.total_params += current_params
self.total_flops += current_flops
return {"flops": current_flops, "params": current_params, "out_res": (res_h, res_w)}
def analyze_linear(self, in_features, out_features, use_bias=True):
# 완전 연결 계층 연산 분석
current_params = (in_features + 1 if use_bias else in_features) * out_features
current_flops = (in_features * 2) * out_features
self.total_params += current_params
self.total_flops += current_flops
return {"flops": current_flops, "params": current_params}
# 도구 사용 예시
analyzer = ModelAnalyzer()
conv_info = analyzer.analyze_conv2d(in_ch=3, out_ch=64, kernel_size=3, input_res=(224, 224), padding=1)
fc_info = analyzer.analyze_linear(in_features=64*224*224, out_features=1000)
print(f"전체 모델 FLOPs: {analyzer.total_flops / 1e9:.2f} GFLOPs")
print(f"전체 파라미터 수: {analyzer.total_params / 1e6:.2f} M")
추가 고려 사항: 완전 연결 및 풀링 계층
완전 연결 계층(Linear/Fully Connected Layer)은 커널 크기가 입력 데이터 전체 크기와 같은 특수한 형태의 컨볼루션으로 볼 수 있습니다. 따라서 연산량은 단순히 입력 차원 * 출력 차원 * 2로 계산됩니다. 풀링(Pooling)이나 활성화 함수(Activation)는 파라미터가 없으며, 연산량 또한 컨볼루션에 비해 매우 작아 무시되는 경우가 많지만, 정밀한 프로파일링을 위해서는 비교 연산 등을 별도로 카운트할 수 있습니다.
이러한 원리를 바탕으로 MobileNet의 Depthwise Separable Convolution을 분석해 보면, 일반 컨볼루션 대비 얼마나 파격적으로 연산량을 줄였는지 수치적으로 입증할 수 있습니다. 이는 효율적인 아키텍처를 설계하는 데 있어 필수적인 기술적 근거가 됩니다.