3단계 다섯 번째 논문. 이 글은 Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun의 Deep Residual Learning for Image Recognition CVPR 2016 최종 원문을 직접 읽고 썼다. 최초 공개본은 arXiv 1512.03385다. 여기서 residual connection(잔차 연결)은 새 층이 입력 전체를 새로 만들지 않고, 입력에 더할 변화량만 배우게 한 뒤 두 값을 원소별로 더하는 길이다.
이 글에서 얻을 답과 범위
ResNet(Residual Network, 잔차 네트워크)은 “깊을수록 무조건 좋다”라는 말을 증명한 논문이 아니다. 오히려 이미 충분히 깊은 network(신경망)에 층을 더했더니 test error(시험 오차)만이 아니라 training error(학습 오차)도 높아진 이상한 현상을 출발점으로 삼는다. 정답을 외우는 데도 더 실패했다면, 데이터가 부족해서 생기는 overfitting(과적합)과는 다른 문제다.
이 글을 끝까지 읽으면 다음을 설명할 수 있어야 한다.
- ResNet이 겨냥한 degradation problem(성능 저하 문제)과 vanishing gradient(기울기 소실)는 왜 같은 말이 아닌가.
- 원하는 함수 (H(x))를 바로 배우는 대신 residual function(잔차 함수) (F(x)=H(x)-x)를 배우면 무엇이 달라지는가.
- identity shortcut(항등 지름길)과 projection shortcut(투영 지름길)은 언제 쓰며, 왜 두 branch(가지)의 shape(모양)가 같아야 덧셈할 수 있는가.
- 2015년 원 ResNet block의 순서가
conv → BN → ReLU → … → add → ReLU인 이유와, 뒤에 나온 pre-activation(사전 활성화) ResNet과 무엇이 다른가. - ResNet-50이 1×1, 3×3, 1×1 bottleneck block(병목 block)을 쓰는 이유와, 공식 Caffe prototxt가 실제로 어디에서 합치는가.
- ImageNet과 CIFAR-10 수치는 어떤 평가 조건의 숫자인가. 특히 3.57%가 ResNet-152 단일 모델 성적이 아닌 이유는 무엇인가.
- backend(백엔드)·DBA(Database Administrator, 데이터베이스 관리자) 출신이 이 논문에서 무엇을 가져가고, Transformer의 residual connection과 무엇을 혼동하면 안 되는가.
범위는 He et al.의 2016년 원 논문에 나온 image classification(이미지 분류) ResNet, ImageNet·CIFAR-10 실험, 저자 공식 Caffe 배포 모델의 forward path(순전파 경로)다. 이 글은 ImageNet 전체 재학습이나 최신 PyTorch 구현의 재현이 아니다. 2026년의 ConvNeXt, ViT(Vision Transformer, 비전 트랜스포머), diffusion model(확산 모델)을 대체 설명하지 않는다. Python 예제는 원 논문 전체의 수십억 회 연산을 재현한 것이 아니라, 식 (1)과 식 (2)의 작은 벡터 계산과 gradient(기울기) 식을 검증한 것이다.
먼저 잡을 한 문장
ResNet은 층이 입력을 전부 새로 변환하도록 시키는 대신, 입력을 그대로 보내는 길과 작은 수정값을 만드는 길을 합쳐서, 깊은 network가 “아무 변화도 하지 않음”에 가까운 해를 찾기 쉽게 만든 구조다.
왜 필요한가 — 깊게 쌓았는데 학습부터 망가진 문제
깊이는 표현력을 늘리지만 optimizer가 해답을 찾는 일은 따로다
convolutional neural network, CNN(합성곱 신경망)은 이미지의 작은 지역을 보는 filter(필터)를 여러 층 쌓는다. 아래 층은 선·모서리 같은 작은 패턴을, 뒤쪽 층은 조합된 모양을 표현할 수 있다. 그래서 2010년대 ImageNet 분류 경쟁에서는 network depth(깊이)를 늘리는 일이 중요한 전략이었다.
그런데 “더 많은 층 = 더 나쁜 모델은 흉내 낼 수 있는 모델”이라는 직관에는 중요한 빈칸이 있다. 표현 가능한 해가 존재한다는 것과 SGD(Stochastic Gradient Descent, 확률적 경사 하강법)가 유한한 시간 안에 그 해를 찾는 것은 다르다.
원 논문은 18층과 34층 plain network(지름길 없이 순서대로만 쌓은 network)를 같은 ImageNet 설계 철학 아래 비교했다. 34층은 18층의 층을 복사하고 추가 층을 identity mapping(항등 함수)으로 만들 수 있다면, 최소 training error가 18층보다 나쁠 이유가 없어 보인다. 하지만 실제 학습 곡선에서 34층 plain net은 18층보다 training error가 높았다. 이것이 degradation이다.원 논문 1절·그림 1·표 2
| 현상 | 무엇이 높은가 | 이 논문에서 뜻하는 문제 |
|---|---|---|
| underfitting(과소적합) | train과 test 모두 높다 | 모델·특징·학습 시간이 문제를 충분히 표현하지 못한다 |
| overfitting | train은 낮고 test가 상대적으로 높다 | train data에만 너무 맞춘다 |
| degradation | 더 깊은 plain net의 train error도 높다 | 더 깊은 해 공간이 있어도 optimizer가 좋은 해를 못 찾는다 |
| vanishing or exploding gradient | 역전파 값이 너무 작거나 너무 커진다 | 학습이 어려워지는 한 원인일 수 있지만 degradation의 동의어는 아니다 |
논문은 normalized initialization(정규화된 초기화)과 BN(Batch Normalization, 배치 정규화)이 수십 층 network가 학습을 시작하게 하는 데 도움이 되었다고 전제한다. 그리고 18층·34층 plain network에서 forward signal variance(순전파 신호 분산)와 backward gradient norm(역전파 기울기 크기)이 건강한 상태임을 확인했다고 썼다. 따라서 이 실험에서 degradation을 “gradient가 0이 돼서 발생했다” 한 문장으로 끝내면 원문보다 강한 주장이 된다. 더 정확한 설명은 정규화와 초기화로 단순한 폭발·소실만으로 설명하기 어려운 optimization difficulty(최적화 난점)가 드러났다는 것이다.원 논문 4.1절
데이터베이스 비유로 보면 전체 테이블 재작성과 patch다
입력 (x)를 어떤 중간 feature map(특징 지도)이라고 하자. plain layer stack은 “이 feature map을 완전히 새 결과 (H(x))로 바꿔라”라고 배운다. ResNet은 “기존 (x)는 여기 있으니, 필요한 수정분만 (F(x))로 만들어 더하라”라고 바꾼다.
plain network
x ── 여러 층 ──> H(x)
residual block
┌─ 여러 층 ──> F(x) ─┐
x ────────────────┴─ shortcut x ────────┴─ 원소별 덧셈 ─> H(x)
H(x) = F(x) + x
DBA 관점의 비유로는, 매번 전체 row를 새로 materialize(구체화)하는 대신 기존 row + 변경분 patch를 저장·적용하는 모양에 가깝다. 물론 ResNet은 database update protocol(갱신 프로토콜)이 아니다. transaction(트랜잭션), durability(지속성), rollback(되돌리기)은 없다. 비유가 말하는 것은 오직 기준값을 보존하고 delta(변화량)를 학습한다는 계산 관점이다.
무엇인가 — H 대신 F를 배우는 residual learning
식 (1)의 모든 기호
원 논문의 residual block은 다음이다.
[ y=F(x,{W_i})+x \tag{1} ]
그리고 원 논문은 이 덧셈 뒤에 두 번째 ReLU(Rectified Linear Unit, 정류 선형 유닛)를 둔다. 즉 실제 block output은 (\sigma(y))이며, (\sigma(z)=\max(0,z))다.원 논문 3.1절·그림 2
| 기호 | 뜻 | 12살 설명 |
|---|---|---|
| (x) | block의 입력 vector 또는 feature map | 앞 block이 만든 여러 숫자 그림 |
| (H(x)) | 이 block이 최종적으로 만들고 싶은 변환 | 입력을 받아 만들 새 숫자 그림 |
| (F(x,{W_i})) | residual function | “입력에서 무엇을 얼마나 고칠지”라는 수정분 |
| (W_i) | convolution 등 학습되는 weight(가중치) | 고칠 방법을 배우는 다이얼 값 |
| (+) | element-wise addition(원소별 덧셈) | 같은 위치·같은 channel의 숫자끼리 더하기 |
| (y) | ReLU 직전 합 | 수정분과 원본을 합친 숫자 그림 |
| (\sigma) | ReLU | 음수는 0, 양수는 그대로 두는 문지기 |
원래 원하는 함수가 (H(x))라면, ResNet은
[ F(x)=H(x)-x \quad\Longleftrightarrow\quad H(x)=F(x)+x ]
로 문제를 다시 쓴다. 이 재작성은 함수 표현력 자체를 마법처럼 늘리는 theorem(정리)이 아니다. 원 논문도 “easier to optimize”라는 가설로 제시한다. 특히 최선의 결과가 identity에 가까우면, 여러 nonlinear layer(비선형 층)가 identity를 정교하게 흉내 내는 것보다 residual branch의 weight를 0 근처로 보내는 일이 더 다루기 쉬울 수 있다는 동기다.
residual은 예측 오차가 아니다
residual이라는 단어는 통계에서 관측값과 예측값의 차이를 뜻하기도 한다. 여기서는 다르다. ResNet의 residual은 block 입력 (x)에서 목표 변환 (H(x))까지 필요한 함수의 차이인 (F(x)=H(x)-x)다. label(정답 class)과 model prediction(모델 예측)의 차이가 아니다.
또한 shortcut은 “층을 건너뛰니 정보가 손실되지 않는다”는 절대 보증도 아니다. 원 2015 ResNet에는 addition 뒤 ReLU가 있다. 합이 음수면 ReLU output과 그 구간의 미분값은 0이 된다. shortcut은 중요한 직접 경로를 추가하지만, 원 논문 block에서 항상 gradient 1을 보장한다고 말하면 틀린다. 이 차이는 뒤의 직접 검증에서 숫자로 확인한다.
선행 개념 — 이미지가 숫자 지도로 바뀌는 과정
image tensor와 feature map의 shape
컴퓨터는 RGB image를 보통 (height, width, channel) 숫자 배열로 본다. 빨강·초록·파랑 channel이 있으므로 color image는 3개 channel을 가진다. framework에 따라 batch 차원까지 (N, C, H, W) 또는 (N, H, W, C)로 적는다.
| 말 | 뜻 | 작은 예 |
|---|---|---|
| tensor(텐서) | 여러 축을 가진 숫자 상자 | image batch (32, 3, 224, 224) |
| channel(채널) | 같은 위치에 겹쳐 저장한 feature의 종류 | 입력 RGB 3개, 중간 feature 64개 |
| feature map(특징 지도) | 하나의 channel이 가진 2차원 숫자 지도 | 56 × 56 위치마다 detector 반응 하나 |
| convolution(합성곱) | 작은 kernel을 슬라이드해 주변 숫자를 섞는 연산 | 3×3 창으로 모서리 같은 패턴 탐지 |
| kernel or filter | convolution의 학습되는 작은 weight 배열 | 3×3×64 weight 묶음 |
| stride(보폭) | kernel을 몇 칸씩 옮길지 | stride 2면 높이·너비를 대략 절반으로 줄임 |
| padding(패딩) | 가장자리에 0 등을 덧대는 일 | 3×3 conv가 크기를 유지하도록 pad 1 |
2차원 convolution의 출력 높이는 다음이다. 너비도 같은 식이다.
[ H_{out}=\left\lfloor\frac{H_{in}+2P-K}{S}\right\rfloor+1 ]
| 기호 | 뜻 |
|---|---|
| (H_{in}) | 입력 높이 |
| (P) | 양쪽 padding 크기 |
| (K) | kernel 높이 |
| (S) | stride |
| (\lfloor\cdot\rfloor) | 소수점을 버리고 아래 정수로 내림 |
예를 들어 56 × 56에 K=3, P=1, S=1이면 출력도 56 × 56이다. S=2면 28 × 28로 줄어든다. 이 shape 규칙은 ResNet shortcut에서 아주 중요하다. 56 × 56 × 64와 28 × 28 × 128은 같은 위치끼리 더할 수 없다.
BN과 ReLU는 무엇을 하나
ReLU는 negative activation(음수 활성값)을 0으로 잘라내고 positive activation(양수 활성값)은 통과시킨다. 이것만으로는 깊은 network 학습이 안정적이라는 뜻이 아니다.
BN은 training 중 mini-batch(미니배치)에서 channel별 activation을 평균과 분산으로 정규화한 뒤, 학습되는 scale(크기)와 shift(이동)를 적용한다. 원 ResNet 논문은 각 convolution 뒤, activation 앞에 BN을 사용했다. 이 논문의 공식 Caffe deploy model은 inference(추론)용이라 BatchNorm 뒤에 학습 가능한 Scale layer를 따로 둔다. Caffe의 당시 BatchNorm layer에는 gamma(스케일)·beta(이동) 학습이 없었기 때문이다.공식 저장소 README
BN은 batch statistics(배치 통계), training·evaluation mode(학습·평가 모드), framework마다의 running statistics(이동 통계) 처리와 연결된다. 따라서 model.eval()을 빠뜨리거나 pretrained model의 BN 통계를 잘못 복원하면, ResNet block 구조가 맞아도 결과가 달라질 수 있다.
밑바닥 원리 — 더하기가 forward와 backward를 어떻게 바꾸나
identity shortcut은 F가 0이면 입력을 남긴다
양수 input에서 (F(x)=0)인 가장 단순한 경우를 보자.
[ x=[2,1],\qquad F(x)=[0,0] ]
[ y=F(x)+x=[2,1],\qquad \sigma(y)=[2,1] ]
이때 block은 identity다. “모든 입력에서 항상 identity”는 아니다. 예를 들어 (x=[-2,1])이면 (F(x)=0)이어도 덧셈 뒤 ReLU가 [-2, 1]을 [0, 1]로 바꾼다. 원 논문의 post-activation block을 설명할 때 이 조건을 빼면 안 된다.
projection shortcut은 shape를 맞추는 학습 가능한 변환이다
채널 수 또는 가로세로 크기를 바꾸는 stage 전환에서는 다음을 쓴다.
[ y=F(x,{W_i})+W_sx \tag{2} ]
(W_s)는 shortcut branch의 linear projection(선형 투영)이다. CNN에서는 보통 1×1 convolution이 이 역할을 한다. 1×1은 한 pixel 위치의 channel들을 섞어 새 channel 수로 바꾸되, 3×3처럼 이웃 위치를 보는 비싼 공간 연산은 하지 않는다.
다음은 2개 channel 입력을 3개 channel 출력으로 맞추는 작은 숫자 예다.
[ x=[2,1] ]
[ F(x)=[0.4,-0.2,0.9],\qquad W_sx=[2,1,1.5] ]
[ y=[2.4,0.8,2.4] ]
두 branch가 모두 길이 3 vector를 내기 때문에 더할 수 있다. 하나가 길이 2, 다른 하나가 길이 3이면 “어느 숫자끼리 더할지” 정의되지 않는다.
gradient에는 residual branch와 shortcut branch가 함께 들어온다
ReLU 직전 값이 모두 양수인 구간만 따로 보자. residual branch를 단순 선형 함수 (F(x)=Wx)로 놓으면
[ y=(W+I)x ]
이다. 여기서 (I)는 identity matrix(항등 행렬)다. loss(손실)를 (L)이라고 하면 chain rule(연쇄 법칙)에 따라
[ \frac{\partial L}{\partial x} =W^T\frac{\partial L}{\partial y} +\frac{\partial L}{\partial y} \tag{3} ]
가 된다. 첫 항은 residual branch를 거친 gradient, 둘째 항은 identity shortcut에서 온 direct term(직접 항)이다. 이 식은 “어떤 gradient도 절대 작아지지 않는다”는 뜻이 아니다. (W), ReLU mask(활성화 마스크), 많은 block의 곱, BN 등 전체 network 조건이 모두 영향을 준다. 다만 plain chain에 없던 더하기 항 하나가 역전파 식에 생긴다는 사실은 명확하다.
original ResNet과 pre-activation ResNet을 섞지 말자
원 논문 block은 F(x) + x 뒤에 ReLU를 둔다. 이후 저자들은 Identity Mappings in Deep Residual Networks에서 activation과 normalization 위치를 다시 분석하고, identity path를 더 직접적으로 유지하는 pre-activation 설계를 제안했다.
| 구분 | 2015 ResNet 원 논문 | 2016 후속 pre-activation 논문 |
|---|---|---|
| shortcut 합 뒤 | ReLU가 있다 | 다음 block 앞의 BN·ReLU 배치가 다르다 |
| 이 글의 식·공식 Caffe deploy | 이 구조를 설명한다 | 이 글의 원 구조로 바꾸어 말하지 않는다 |
| 흔한 잘못된 말 | “shortcut이 늘 gradient 1을 보장한다” | 후속 구조의 분석을 원 논문에 덮어쓴 말 |
두 논문 모두 residual learning 계열이지만, block 내부 순서와 gradient를 설명하는 조건이 다르다.
내부 구조와 실제 실행 흐름 — ResNet-50을 소스에서 따라가기
basic block과 bottleneck block
원 논문은 ImageNet의 18층·34층 network에는 3×3 convolution 두 개로 구성된 basic block을 썼다. 50층·101층·152층은 1×1, 3×3, 1×1의 세 layer bottleneck block을 썼다.
basic block
x ── shortcut ───────────────────────────────────────┐
3×3 conv → BN → ReLU → 3×3 conv → BN ────────────┼─ add → ReLU
└────────────
bottleneck block
x ── shortcut 또는 1×1 projection ──────────────────┐
1×1 reduce → 3×3 process → 1×1 restore ──────────┼─ add → ReLU
└────────────
bottleneck은 데이터베이스의 병목 query와 같은 뜻이 아니다. 채널 폭을 잠시 줄여 가장 비싼 3×3 convolution을 더 작은 channel 수에서 하게 하는 설계다. 원 논문은 ResNet-50을 3.8 billion FLOPs(Floating Point Operations, 부동소수점 연산 횟수), ResNet-101을 7.6B, ResNet-152를 11.3B로 제시했다. 비교 대상으로 든 VGG-16·19는 각각 15.3B·19.6B다. 이 숫자는 특정 ImageNet 입력·논문 설계의 계산량이며 latency(지연 시간)를 직접 뜻하지는 않는다.원 논문 표 1
| architecture | conv2_x | conv3_x | conv4_x | conv5_x | 논문 FLOPs |
|---|---|---|---|---|---|
| ResNet-50 | 3 blocks | 4 blocks | 6 blocks | 3 blocks | 3.8B |
| ResNet-101 | 3 blocks | 4 blocks | 23 blocks | 3 blocks | 7.6B |
| ResNet-152 | 3 blocks | 8 blocks | 36 blocks | 3 blocks | 11.3B |
공식 Caffe ResNet-50은 두 branch를 Eltwise에서 합친다
저자 공식 저장소의 prototxt/ResNet-50-deploy.prototxt는 deploy용 graph를 plain text로 적는다. 이 저장소 commit은 a7026cb6d478e131b765b898c312e25f9f6dc031이다.
첫 입력은 (1, 3, 224, 224)이고, conv1은 7×7, 64 output channel, stride 2다.공식 source 1–58행 첫 bottleneck block res2a는 다음 순서다.
layer {
bottom: "pool1"
top: "res2a_branch1"
name: "res2a_branch1"
type: "Convolution"
convolution_param { num_output: 256 kernel_size: 1 stride: 1 }
}
layer {
bottom: "pool1"
top: "res2a_branch2a"
name: "res2a_branch2a"
type: "Convolution"
convolution_param { num_output: 64 kernel_size: 1 stride: 1 }
}
layer {
bottom: "res2a_branch1"
bottom: "res2a_branch2c"
top: "res2a"
name: "res2a"
type: "Eltwise"
}
layer {
bottom: "res2a"
top: "res2a"
name: "res2a_relu"
type: "ReLU"
}
위 코드는 공식 source 60–223행에서 확인했다. branch1은 1×1 projection으로 256 channel을 만들고, branch2는 1×1 64 → 3×3 64 → 1×1 256을 만든다. 마지막 Eltwise가 두 (H, W, 256) feature map을 원소별로 더하고, res2a_relu가 합 뒤의 ReLU다.
stage 경계에서는 downsampling과 channel 증가가 동시에 있어 projection이 필요하다. 예를 들어 res3a는 shortcut branch와 residual branch 첫 convolution에 stride 2를 두고 output channel을 512로 맞춘다. 이런 projection은 공식 source 487–650행에서 확인할 수 있다. 마지막에는 7×7 average pooling, 1000-way fully connected layer, softmax가 이어진다.공식 source 2292–2319행
공식 deploy model을 원 학습 binary로 오해하면 안 된다
공식 README는 중요한 경계를 명시한다. 공개 Caffe model은 저자 구현에서 Caffe 2016/2/3으로 변환된 model이며, 이 Caffe version으로 훈련된 것이 아니다. testing 또는 fine-tuning 용도라고 적는다. 또한 원 학습은 8 GPU에서 image 256개, GPU당 32개를 썼고, mini-batch size·augmentation·BN 통계·SGD momentum 구현 차이가 정확도에 영향을 줄 수 있다고 경고한다.공식 README의 disclaimer
따라서 deploy.prototxt는 block의 실제 inference graph 근거로 쓰되, “이 파일을 그대로 실행해 논문 학습을 완전 재현했다”는 근거로 쓰면 안 된다.
직접 검증과 재현 — 작은 residual block을 손으로 실행하기
실행 환경과 범위
다음 코드는 macOS의 Python 3.9.6, 표준 라이브러리만 사용해 직접 실행했다. PyTorch, Caffe, GPU, ImageNet은 쓰지 않았다. 검증 대상은 다음 세 가지다.
- (F(x)=0)이고 입력이 양수일 때 identity shortcut이 input을 보존하는가.
- shape가 바뀔 때 projection shortcut이 두 branch를 같은 shape로 만드는가.
- 활성 ReLU 구간에서 식 (3)의 analytic gradient(해석적 기울기)가 finite difference(유한 차분)와 같은가.
이 코드는 ResNet-50 training 또는 ImageNet 정확도를 재현하지 않는다. 작은 행렬로 원리를 검증한다.
#!/usr/bin/env python3
"""Small, dependency-free checks for He et al. (2016) residual blocks."""
from __future__ import annotations
import math
def mat_vec(matrix, vector):
assert all(len(row) == len(vector) for row in matrix)
return [sum(weight * value for weight, value in zip(row, vector)) for row in matrix]
def transpose(matrix):
return [list(column) for column in zip(*matrix)]
def add(a, b):
assert len(a) == len(b), "element-wise residual addition requires equal shapes"
return [left + right for left, right in zip(a, b)]
def relu(vector):
return [max(0.0, value) for value in vector]
def residual_block(x, residual_matrix, shortcut_matrix=None):
residual = mat_vec(residual_matrix, x)
shortcut = x if shortcut_matrix is None else mat_vec(shortcut_matrix, x)
before_relu = add(residual, shortcut)
return relu(before_relu), residual, shortcut, before_relu
def squared_error(output, target):
return 0.5 * sum((value - expected) ** 2 for value, expected in zip(output, target))
def input_gradient_when_relu_active(x, residual_matrix, target):
y, _, _, before_relu = residual_block(x, residual_matrix)
assert all(value > 0 for value in before_relu)
upstream = [value - expected for value, expected in zip(y, target)]
return add(mat_vec(transpose(residual_matrix), upstream), upstream)
def finite_difference_gradient(x, residual_matrix, target, epsilon=1e-6):
gradient = []
for i in range(len(x)):
plus, minus = list(x), list(x)
plus[i] += epsilon
minus[i] -= epsilon
plus_loss = squared_error(residual_block(plus, residual_matrix)[0], target)
minus_loss = squared_error(residual_block(minus, residual_matrix)[0], target)
gradient.append((plus_loss - minus_loss) / (2.0 * epsilon))
return gradient
def show(label, values):
print(label + ': [' + ', '.join(f'{value:.6f}' for value in values) + ']')
def main():
print('== Eq. (1): identity shortcut ==')
x = [2.0, 1.0]
zero_residual = [[0.0, 0.0], [0.0, 0.0]]
y, residual, shortcut, before_relu = residual_block(x, zero_residual)
show('input x', x)
show('F(x)', residual)
show('shortcut x', shortcut)
show('F(x) + x before ReLU', before_relu)
show('output', y)
assert y == x
print('\n== Eq. (2): projection shortcut for a shape change ==')
residual_matrix = [[0.10, 0.20], [-0.30, 0.40], [0.50, -0.10]]
projection = [[1.00, 0.00], [0.00, 1.00], [0.50, 0.50]]
projected_y, projected_residual, projected_shortcut, _ = residual_block(
x, residual_matrix, projection
)
show('F(x) with 3 channels', projected_residual)
show('W_s x with 3 channels', projected_shortcut)
show('projected block output', projected_y)
assert len(projected_residual) == len(projected_shortcut) == 3
print('\n== gradient: residual branch plus direct identity path ==')
gradient_matrix = [[0.20, 0.10], [-0.10, 0.30]]
target = [0.0, 0.0]
analytical = input_gradient_when_relu_active(x, gradient_matrix, target)
numerical = finite_difference_gradient(x, gradient_matrix, target)
show('analytical dL/dx = W^T*dL/dy + dL/dy', analytical)
show('finite-difference dL/dx', numerical)
assert all(math.isclose(a, b, rel_tol=1e-6, abs_tol=1e-6)
for a, b in zip(analytical, numerical))
print('\n== depth illustration: this is not a proof for nonlinear ResNets ==')
depth = 20
print(f'plain linear chain derivative 0.5^{depth}: {0.5 ** depth:.9f}')
print(f'residual linear chain derivative (1 + 0.05)^{depth}: {(1.05) ** depth:.9f}')
print('checks passed')
if __name__ == '__main__':
main()
실제 출력은 다음과 같았다.
== Eq. (1): identity shortcut ==
input x: [2.000000, 1.000000]
F(x): [0.000000, 0.000000]
shortcut x: [2.000000, 1.000000]
F(x) + x before ReLU: [2.000000, 1.000000]
output: [2.000000, 1.000000]
== Eq. (2): projection shortcut for a shape change ==
F(x) with 3 channels: [0.400000, -0.200000, 0.900000]
W_s x with 3 channels: [2.000000, 1.000000, 1.500000]
projected block output: [2.400000, 0.800000, 2.400000]
== gradient: residual branch plus direct identity path ==
analytical dL/dx = W^T*dL/dy + dL/dy: [2.890000, 1.680000]
finite-difference dL/dx: [2.890000, 1.680000]
== depth illustration: this is not a proof for nonlinear ResNets ==
plain linear chain derivative 0.5^20: 0.000000954
residual linear chain derivative (1 + 0.05)^20: 2.653297705
checks passed
첫 block은 (F(x)=0)이고 input이 양수라 output이 정확히 input과 같다. 둘째 block은 두 branch가 길이 3 vector를 만들었기에 덧셈이 된다. 셋째 block은 식 (3)으로 계산한 [2.89, 1.68]과 입력을 아주 조금씩 움직여 loss 차이를 나눈 finite difference 결과가 일치한다.
마지막 두 숫자는 nonlinear ResNet에 대한 증명도, ResNet-20과 ResNet-152의 실제 gradient 측정도 아니다. 단지 0.5를 20번 곱하는 plain linear chain과 (1 + 0.05)를 20번 곱하는 residual linear toy model(장난감 모델)이 매우 다를 수 있음을 보인다. 실제 network에는 weight, ReLU mask, BN, data distribution(데이터 분포), optimizer가 들어간다.
값을 하나 바꿔 보는 실패 실험
위 코드에서 x = [2.0, 1.0]을 x = [-2.0, 1.0]으로 바꾸고 zero_residual을 유지하면 F(x)+x는 [-2, 1]이다. 그러나 output은 ReLU 뒤 [0, 1]이 된다. assert y == x는 실패해야 맞다. 이 실패가 알려 주는 것은 “원 2015 post-activation block에서는 identity shortcut이라는 말에도 activation 조건이 붙는다”는 점이다.
shape를 일부러 다르게 만들면 add의 assert가 element-wise residual addition requires equal shapes로 실패한다. 실제 framework에서는 더 이른 단계에서 tensor shape mismatch error가 난다. 디버깅 순서는 다음이 효율적이다.
- add 직전 두 tensor의
(N, C, H, W)를 log로 확인한다. - stage 경계에서 stride가 branch 둘 다에 같은 방식으로 적용됐는지 본다.
- channel이 바뀌는 곳에 1×1 projection 또는 논문 옵션 A의 padding 규칙이 있는지 본다.
- checkpoint를 쓸 때 train·eval mode와 BN statistics를 확인한다.
성능과 트레이드오프 — 정확도만 보면 놓치는 비용
ImageNet 수치는 평가 protocol을 떼면 의미가 바뀐다
논문 ImageNet 분류는 1,000 classes, 약 1.28M training image, 50,000 validation image, 100,000 test image를 쓴다. training에서는 image의 shorter side(짧은 변)를 무작위 256에서 480 사이로 잡고 random 224×224 crop 또는 horizontal flip(수평 뒤집기)을 사용했다. mini-batch는 256, initial learning rate는 0.1, weight decay는 0.0001, momentum은 0.9, dropout은 사용하지 않았다. test에는 10-crop evaluation을 사용했고, 더 좋은 결과에는 fully convolutional multi-scale evaluation도 사용했다.원 논문 4.1절
top-1 error는 model이 가장 높은 확률 하나로 고른 class가 정답이 아닌 비율이다. top-5 error는 확률 상위 다섯 class 안에도 정답이 없는 비율이다. 사람이 “정확도 1위”라고 말할 때 이 둘 중 무엇인지, crop 수가 몇 개인지, single model인지 ensemble(앙상블)인지 빼면 비교가 깨진다.
| 비교 또는 model | top-1 error | top-5 error | 조건 |
|---|---|---|---|
| plain-18 | 27.94% | 표 2에 없음 | ImageNet validation, 10-crop |
| plain-34 | 28.54% | 표 2에 없음 | ImageNet validation, 10-crop |
| ResNet-18 | 27.88% | 표 2에 없음 | ImageNet validation, 10-crop |
| ResNet-34 option A | 25.03% | 7.76% | ImageNet validation, 10-crop |
| ResNet-50 | 22.85% | 6.71% | ImageNet validation, 10-crop |
| ResNet-101 | 21.75% | 6.05% | ImageNet validation, 10-crop |
| ResNet-152 | 21.43% | 5.71% | ImageNet validation, 10-crop |
위 18층·34층 비교가 degradation의 핵심 evidence다. ResNet-34 option A가 25.03%까지 낮아진 것은 같은 깊이에서 shortcut이 trainability를 바꾼 결과다.원 논문 표 2·3
논문 표 4의 best single model multi-scale result에서 ResNet-152는 top-1 19.38%, top-5 4.49%다. 논문 초록의 3.57% top-5 test error는 깊이가 다른 여섯 model의 ensemble이다. ResNet-152가 3.57%라고 적으면 사실이 아니다.원 논문 표 4·5
identity와 projection은 공짜와 성능의 교환이다
논문은 dimension increase(차원 증가)에서 세 방법을 비교했다.
| 방법 | shortcut에서 하는 일 | 추가 parameter | ResNet-34 top-1 / top-5 |
|---|---|---|---|
| option A | identity, channel 증가분 zero padding, size 감소에는 stride 2 | 없음 | 25.03% / 7.76% |
| option B | dimension 증가 때만 1×1 projection | 증가 지점만 | 24.52% / 7.46% |
| option C | 모든 shortcut에 1×1 projection | 많음 | 24.19% / 7.40% |
논문은 B가 A보다 조금 낫고, C가 B보다 조금 낫다는 결과를 냈다. 그러나 C의 이득은 extra projection parameters(추가 투영 파라미터)에서 온 부분도 있고, memory·time·model size를 줄이기 위해 이후 깊은 bottleneck architecture에는 C를 사용하지 않았다고 적었다. “projection이 항상 더 좋다”가 아니라 어디까지 비용을 지불할지의 선택이다.원 논문 4.1절·표 3
FLOPs가 작아도 serving 비용이 자동으로 작지는 않는다
identity shortcut은 parameter와 convolution FLOPs를 거의 추가하지 않는다. 그러나 training에서는 residual branch 계산이 끝날 때까지 입력 activation을 보관해야 add와 backpropagation을 할 수 있다. 깊은 network는 activation memory(활성값 메모리), gradient memory, optimizer state(optimizer 상태) 비용이 커진다.
inference에서도 branch graph는 병렬 실행 가능성이 있어 보이지만 실제 latency는 kernel launch, device memory bandwidth(장치 메모리 대역폭), batch size, framework fusion(연산 결합), input resolution에 따라 다르다. 논문 FLOPs만 보고 API p95 latency를 추정하면 안 된다. production(운영)에서는 다음을 따로 재야 한다.
| 지표 | 왜 재나 |
|---|---|
| p50·p95·p99 latency | 평균이 숨기는 tail latency를 본다 |
| images per second | 같은 hardware와 batch에서 처리량을 비교한다 |
| GPU memory peak | 배치 크기와 input resolution의 상한을 정한다 |
| model artifact size | 배포·cold start·cache 비용을 본다 |
| accuracy per class | 전체 top-1이 가리는 class별 실패를 찾는다 |
| train·validation loss | degradation, overfitting, data leak을 구분하는 출발점이다 |
실패와 운영 기준 — ResNet을 붙였는데도 망가지는 경우
1202층은 optimization 성공인데 test는 더 나빴다
원 논문 CIFAR-10 실험은 32×32 image, 10 classes, 50,000 training과 10,000 test image를 쓴다. 세 stage의 channel 수는 16·32·64이고, 6n+2 weighted layers라는 단순한 설계다. ResNet-110은 1.7M parameter, test error 6.43%였고, ResNet-1202는 19.4M parameter, training error는 0.1% 미만까지 갔지만 test error는 7.93%였다.원 논문 4.2절·표 6
이는 중요한 구분이다. residual connection 덕분에 1202층이 최적화가 안 되는 문제는 피했지만, 작은 CIFAR-10 data에 큰 19.4M model을 쓰면 overfitting이 남는다. ResNet은 data quantity(데이터 양), label quality(라벨 품질), regularization(정규화), evaluation design(평가 설계)을 없애 주지 않는다.
checkpoint 이식과 BN은 숨은 호환성 문제다
공식 README는 당시 Caffe SGD momentum을 다음처럼 설명한다.
[ v\leftarrow \text{momentum}\cdot v+\text{lr}\cdot g ]
일부 library는 다음처럼 (1 - momentum) 계수를 넣는다.
[ v\leftarrow \text{momentum}\cdot v+(1-\text{momentum})\cdot\text{lr}\cdot g ]
두 식은 lr 숫자가 같아도 effective learning rate(실효 학습률)가 다를 수 있다. README는 다른 library로 port(이식)할 때 이 차이를 주의하라고 명시한다. 또 공개 BN mean·variance는 training 후 큰 batch에서 평균한 값이며, 일반적인 moving average와 다를 수 있다고 적는다.공식 README의 known issues
이식 실패를 “ResNet architecture가 안 맞는다”로 단정하기 전에는 아래를 확인한다.
- model이 기대하는 preprocessing, RGB/BGR 순서, image scale과 crop을 맞췄는가.
- checkpoint의 architecture revision과 block 수를 맞췄는가.
- BN running statistics와 evaluation mode를 맞췄는가.
- training을 재개한다면 optimizer equation, momentum, weight decay 적용 위치를 맞췄는가.
- top-1, top-5, 1-crop, 10-crop, multi-scale 중 같은 protocol끼리 비교했는가.
보안과 안전 경계
ResNet 자체는 image tensor를 class probability로 바꾸는 model이다. authentication(인증), authorization(인가), input upload 검사, 개인정보 보존 기간, adversarial input(적대적 입력) 대응을 제공하지 않는다. 사람 얼굴·의료 영상·게임 등 민감 도메인에서는 특히 data consent(데이터 동의), access control(접근 제어), audit log(감사 로그), 사람 검토 경계가 model 정확도와 별도로 필요하다.
대안과 선택 기준 — 언제 ResNet을 쓰고 언제 다른 길을 택하나
| 선택지 | 핵심 생각 | ResNet과의 관계 | 고를 때 |
|---|---|---|---|
| plain CNN | layer를 순서대로만 쌓는다 | baseline이며 degradation 비교 대상 | 작고 얕은 실험, 교육용 baseline |
| Highway Network | gate가 shortcut 양을 조절한다 | 원 논문이 비교한 동시대 shortcut 계열 | gate의 data-dependent control이 필요한 설계 연구 |
| pre-activation ResNet | normalization·activation 위치를 바꿔 더 직접적인 identity path를 분석 | 원 논문 후속 개선 | 매우 깊은 residual CNN의 현대적 변형을 공부할 때 |
| DenseNet | 이전 feature를 더하기보다 concatenate한다 | feature 재사용 방식이 다르다 | memory·channel growth 대가를 감수하고 feature reuse를 연구할 때 |
| Vision Transformer | patch token 사이 attention으로 image를 처리한다 | CNN convolution과 다른 inductive bias(귀납 편향) | 대규모 pretraining·Transformer 생태계가 목표일 때 |
LLM(Large Language Model, 대규모 언어 모델)의 Transformer에도 residual addition이 있다. 하지만 Transformer block은 attention과 MLP(Multi-Layer Perceptron, 다층 퍼셉트론)를 residual path에 더하고 LayerNorm(층 정규화)을 사용한다. ResNet은 CNN feature map과 convolution을 다룬다. 공통점은 “입력에 변경분을 더하는 계산 모양”이며, tokenization(토큰화), self-attention(자기 어텐션), causal mask(인과 마스크), next-token prediction(다음 토큰 예측)은 ResNet 논문의 내용이 아니다.
백엔드에서 RAG(Retrieval-Augmented Generation, 검색 증강 생성)를 만들 때도 이 논문을 바로 retrieval algorithm으로 쓰지는 않는다. 다만 다음 판단력은 이어진다.
- 복잡한 system에 새로운 stage를 추가할 때, 기존 동작을 보존하는 fallback path와 수정분을 분리하면 migration 위험을 줄일 수 있다.
- “기능이 더 많으니 성능도 좋아야 한다”가 아니라 train·validation·latency·memory를 분리해 관찰해야 한다.
- 논문 구조와 공개 deploy artifact의 차이를 확인하지 않으면 재현 주장에 구멍이 난다.
흔한 오해와 최초 질문에 대한 답
ResNet은 vanishing gradient를 완전히 해결했나
아니다. ResNet은 깊은 plain network의 degradation 문제를 residual learning으로 다뤘다. 원 논문은 초기화·BN 뒤에도 degradation이 관찰됐다고 보고한다. shortcut이 gradient에 direct term을 더하지만 original post-activation block에서 모든 입력·모든 layer에 gradient가 항상 1이라는 정리는 아니다.
residual branch가 0이면 무조건 완벽한 identity인가
아니다. 원 논문 block은 addition 뒤 ReLU를 쓴다. 합이 음수인 component는 0이 된다. “양수 활성 영역에서 identity가 된다”가 정확한 조건이다. 후속 pre-activation 논문과 섞어 설명하면 안 된다.
더 깊은 ResNet이면 항상 더 좋은가
아니다. CIFAR-10의 1202층은 training error를 낮췄지만 110층보다 test error가 높았다. model size, data, regularization, compute budget, latency budget이 함께 결정한다.
1×1 convolution은 아무 일도 안 하나
아니다. 공간 이웃을 보지 않을 뿐, 같은 위치의 channel을 학습된 weight로 섞고 channel 수를 줄이거나 늘린다. bottleneck의 계산량을 줄이고 projection shortcut의 shape를 맞추는 핵심 도구다.
ResNet을 이해하면 Transformer residual connection도 이해한 것인가
반만 맞다. 덧셈과 residual function이라는 공통된 생각은 이해한 것이다. 하지만 Transformer의 attention, normalization 순서, token sequence, autoregressive generation(자기회귀 생성)은 별도로 배워야 한다. 다음 LLM 논문에서 ResNet의 “입력 + 변경분” 관점을 가져가되, CNN의 shape 규칙을 그대로 적용하지 않는다.
한 문장 답
ResNet은 깊은 모델이 더 깊은 모델을 흉내 낼 수 있어도 optimizer가 그 항등 해를 쉽게 찾지 못하던 문제에서, 각 block이 입력을 보존하고 필요한 수정분만 더하게 해 학습 경로를 바꾼 CNN 구조다. 그렇다고 과적합, shape mismatch, BN 이식, serving 비용까지 자동으로 해결하지는 않는다.
출처 및 검증 경로
1차 자료
- Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun. Deep Residual Learning for Image Recognition, CVPR 2016. 이 글은 PDF 본문 1절부터 4절, 그림 1·2·5·6·7, 표 1부터 6까지를 직접 확인했다. 최초 공개본과 판본 비교는 arXiv 1512.03385로 확인했다.
- KaimingHe. deep-residual-networks 공식 저장소, commit
a7026cb6d478e131b765b898c312e25f9f6dc031. README의 Caffe 변환·BN·momentum 주의사항과ResNet-50-deploy.prototxt의res2abranch와Eltwise흐름을 직접 확인했다. - Kaiming He et al. Identity Mappings in Deep Residual Networks, arXiv 1603.05027. 원 2015 post-activation ResNet과 후속 pre-activation 설계를 구분하는 보조 1차 논문이다.
- Sergey Ioffe, Christian Szegedy. Batch Normalization, arXiv 1502.03167. BN의 일반 개념을 확인했다. 이 글의 공식 Caffe artifact 설명은 위 ResNet README를 우선 근거로 삼았다.
직접 실행
- Python
3.9.6, 표준 라이브러리만 사용했다. 식 (1)의 positive input identity 사례, 식 (2)의 2 channel→3 channel projection, 활성 ReLU 구간에서의 analytic gradient와 finite difference gradient를 위 코드로 실행했고, 본문 출력은 그 실제 결과다. - 이 검증은 ResNet-50·101·152의 ImageNet 재학습, GPU 성능 benchmark, 논문 표의 정확도 재현이 아니다. 그런 수치는 원 논문의 명시된 data·augmentation·crop·model·ensemble 조건에서 나온 외부 실험 결과로 인용했다.
이 글의 해석
기존 값 + patchdatabase 비유와 RAG migration에 연결한 문단은 residual learning의 기준값·수정분 구조를 설명하기 위한 해석이다. ResNet 논문이 database system 설계를 주장한 것은 아니다.- original post-activation block에서 shortcut이 “항상 gradient 1”을 보장하지 않는다는 설명은 원 논문의
add → ReLU구조와 본문의 작은 ReLU 계산에서 나온 해석이다. 후속 pre-activation 논문의 식을 원 논문에 소급 적용하지 않았다.
댓글