CNN: 제로에서 프로덕션까지 컨볼루셔널 네트워크
컴퓨터는 어떻게 사진 속의 고양이를 "볼"까요? 교통 표지판과 사람 얼굴을 어떻게 구별하나요? 대답은 CNN(컨벌루션 신경망), 딥 러닝 아키텍처 컴퓨터 비전에 혁명을 일으켰습니다. 자율주행차부터 의료영상진단까지, CNN은 우리가 매일 사용하는 수백만 개의 애플리케이션 뒤에 있는 보이지 않는 엔진입니다.
이 시리즈의 첫 번째 기사에서는 딥러닝을 이용한 컴퓨터 비전, 우리는 구축할 것이다 CNN에 대한 이해: CNN이 무엇인지, 합성곱 필터가 어떻게 작동하는지, 역사를 만든 아키텍처와 PyTorch에서 완전한 CNN을 구현하고 훈련하는 방법. 이 과정을 마치면 실제 이미지를 분류하고 모델을 제작에 적용할 수 있는 기술을 갖추게 됩니다.
시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | 현재 위치 - CNN: 컨볼루셔널 네트워크 | 아키텍처, 교육, 배포 |
| 2 | 전이 학습 및 미세 조정 | 사전 훈련된 모델, 도메인 적응 |
| 3 | YOLO를 이용한 객체 감지 | 실시간 객체 감지 |
| 4 | 의미론적 분할 | 픽셀 수준 분류 |
| 5 | GAN 및 확산을 사용한 이미지 생성 | 합성 이미지 생성 |
| 6 | 엣지 배포 및 최적화 | 임베디드 장치의 모델 |
무엇을 배울 것인가
- 컴퓨터가 이미지를 표현하는 방법(픽셀, RGB 채널, 텐서)
- 컨볼루션 작업: 커널, 기능 맵, 슬라이딩 윈도우
- CNN의 기본 구성 요소: 컨볼루션, 풀링, 활성화
- 아키텍처의 진화: LeNet-5에서 ConvNeXt까지
- ResNet 및 연결 건너뛰기: 사라지는 그래디언트를 해결하는 방법
- PyTorch의 완전한 구현: CIFAR-10 분류를 위한 CNN
- 전이 학습: ImageNet에서 사전 훈련된 모델 재사용
- 평가 지표: 정확성, 정밀도, 재현율, 혼동 행렬
- 배포: 훈련된 모델에서 프로덕션 추론까지(ONNX, TorchScript)
1. 픽셀에서 기능까지: 컴퓨터가 이미지를 보는 방식
우리 인간에게 사진을 보는 것은 자연스러운 행위입니다. 우리의 뇌는 즉시 인식합니다. 모양, 색상, 윤곽 및 개체. 그러나 컴퓨터에게 이미지는 단지 숫자의 격자일 뿐입니다. 각 픽셀은 빛의 강도를 나타내는 숫자 값으로 표시됩니다.
1.1 숫자 행렬로서의 이미지
회색조 이미지는 각 셀에 0(검은색) 사이의 값이 포함된 2D 배열입니다.
255(흰색). 컬러 이미지는 다음과 같이 구성됩니다. 세 개의 채널 (빨간색, 녹색, 파란색),
각각은 별도의 매트릭스입니다. 따라서 224x224 컬러 이미지는 차원 텐서입니다.
3 x 224 x 224또는 150,528개의 숫자 값.
Immagine 4x4 (scala di grigi, valori 0-255):
+-----+-----+-----+-----+
| 10 | 20 | 30 | 40 |
+-----+-----+-----+-----+
| 50 | 120 | 180 | 60 |
+-----+-----+-----+-----+
| 70 | 200 | 220 | 80 |
+-----+-----+-----+-----+
| 90 | 100 | 110 | 130 |
+-----+-----+-----+-----+
Immagine a colori (3 canali RGB):
Canale R: [[10, 20, ...], ...] --> tonalita di rosso
Canale G: [[30, 40, ...], ...] --> tonalita di verde
Canale B: [[50, 60, ...], ...] --> tonalita di blu
Tensore finale: shape = (3, H, W) --> (canali, altezza, larghezza)
1.2 조밀한 신경망이 충분하지 않은 이유
가장 먼저 떠오르는 접근 방식은 이미지를 1D 벡터로 평면화하고 이를 통과시키는 것입니다. 완전히 연결된(밀도가 높은) 신경망에 연결됩니다. 224x224x3 이미지의 경우 이는 다음을 의미합니다. 입력 레이어 150,528개의 뉴런. 1,000개의 뉴런으로 구성된 은닉층으로, 첫 번째 레이어에만 이미 1억 5천만 개의 매개변수가 있을 것입니다.
이미지의 밀집된 네트워크 문제
- 매개변수 폭발: 첫 번째 레이어에 이미 수백만 개의 가중치가 있어 계산이 불가능합니다.
- 공간 불변성 없음: 고양이가 오른쪽으로 10픽셀 이동하면 네트워크는 더 이상 고양이를 인식하지 못합니다.
- 2D 구조 손실: 이미지를 병합하면 인접한 픽셀 간의 공간 관계가 파괴됩니다.
- 과적합: 데이터가 너무 적고 매개변수가 너무 많으면 일반화가 아닌 암기로 이어집니다.
CNN은 세 가지 주요 통찰력을 활용하여 이러한 모든 문제를 해결합니다. 소재지 (시각적 패턴은 지역적임) 부담 공유 (이미지의 모든 곳에서 동일한 필터가 작동함) e 번역 불변성 (가장자리는 어디에 있든 가장자리입니다).
2. 컨볼루션 연산
La 회선 이는 CNN의 핵심인 수학적 연산입니다. 약간의 필터 (말했다 핵심) 입력 이미지 위로 스크롤하여 각 위치의 합계를 계산합니다. 가려진 픽셀의 가중치. 결과는 다음과 같은 새로운 배열입니다. 기능 맵, 특정 패턴(수직 가장자리, 수평 가장자리, 모서리, 텍스처)을 강조 표시합니다.
2.1 커널과 슬라이딩 윈도우
커널은 스크롤되는 작은 가중치 행렬(일반적으로 3x3 또는 5x5)입니다. (슬라이드) 전체 입력 이미지에 걸쳐. 각 위치에서 커널 값은 다음과 같습니다. 요소별로 기본 픽셀을 곱하고 함께 더하여 단일을 생성합니다. 출력 특징 맵의 값.
Input (5x5): Kernel (3x3):
+---+---+---+---+---+ +----+----+----+
| 1 | 2 | 3 | 0 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 0 | 1 | 2 | 3 | 1 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 1 | 0 | 1 | 2 | 0 | | -1 | 0 | 1 |
+---+---+---+---+---+ +----+----+----+
| 2 | 1 | 0 | 1 | 3 | (Filtro per bordi verticali)
+---+---+---+---+---+
| 0 | 1 | 2 | 1 | 0 |
+---+---+---+---+---+
Posizione (0,0): applica kernel ai pixel evidenziati [*]
[*1][*2][*3] 0 1
[*0][*1][*2] 3 1 Calcolo:
[*1][*0][*1] 2 0 (-1x1)+(0x2)+(1x3)+(-1x0)+(0x1)+(1x2)+(-1x1)+(0x0)+(1x1)
2 1 0 1 3 = -1 + 0 + 3 + 0 + 0 + 2 - 1 + 0 + 1 = 4
0 1 2 1 0
Output feature map (3x3):
+---+---+---+
| 4 | . | . | <-- Il 4 appena calcolato
+---+---+---+
| . | . | . | Il kernel scorre e calcola ogni cella
+---+---+---+
| . | . | . |
+---+---+---+
클래식 커널 유형
| 핵심 | 범위 | 값(3x3) |
|---|---|---|
| 세로 테두리 | 수직 전환 감지 | [-1, 0, 1] 반복됨 |
| 가로 테두리 | 수평 전환 감지 | [-1,-1,-1], [0,0,0], [1,1,1] |
| 샤프닝 | 선명도 증가 | [0,-1.0], [-1.5,-1], [0,-1.0] |
| 가우시안 블러 | 가우시안 블러 | [1,2,1], [2,4,2], [1,2,1] / 16 |
CNN과 기존 이미지 처리의 주요 차이점은 다음과 같습니다. CNN에 커널 값은 직접 설정되지 않습니다. 네트워크 자동으로 학습 훈련 중 최적의 필터를 통해 역전파. 첫 번째 레이어는 간단한 가장자리와 텍스처를 감지하는 방법을 학습하고, 레이어는 더 깊은 패턴은 이러한 특징을 점점 더 복잡한 패턴(눈, 바퀴, 얼굴)으로 결합합니다.
3. CNN의 구성요소
CNN은 각각 특정 역할을 가진 다양한 유형의 레이어로 구성됩니다. 효과적인 아키텍처를 설계하려면 각 구성 요소의 기능을 이해하는 것이 중요합니다.
3.1 컨벌루션 레이어
컨벌루션 레이어는 입력에 여러 필터를 적용하여 다음에 대한 기능 맵을 생성합니다. 각 필터. RGB 이미지에 32개의 3x3 필터를 적용하면 32개의 특징 맵이 생성됩니다. 각각은 다른 패턴을 강조합니다. 주요 매개변수는 다음과 같습니다.
컨벌루션 레이어 매개변수
| 매개변수 | 설명 | 일반적인 값 |
|---|---|---|
| 커널 크기 | 필터 크기(너비 x 높이) | 3x3, 5x5, 7x7 |
| 보폭 | 필터 스크롤 단계 | 1, 2 |
| Padding | 입력 가장자리에 추가된 픽셀 | 0(유효), 1(3x3과 동일) |
| 필터 수 | 출력에 포함된 기능 맵 수 | 32, 64, 128, 256, 512 |
Formula dimensione output:
output_size = (input_size - kernel_size + 2 * padding) / stride + 1
Esempio: input 32x32, kernel 3x3
Stride=1, Padding=0: (32 - 3 + 0) / 1 + 1 = 30x30 (si riduce)
Stride=1, Padding=1: (32 - 3 + 2) / 1 + 1 = 32x32 (same padding)
Stride=2, Padding=1: (32 - 3 + 2) / 2 + 1 = 16x16 (dimezza)
Stride=1, Padding=1 ("same"):
Input: [A B C D E] Output: [A B C D E] --> stessa dimensione
(con zero-padding ai bordi)
Stride=2 (dimezza la risoluzione):
Input: [A B C D E F] Output: [A C E] --> meta dimensione
(salta un pixel ad ogni passo)
3.2 활성화 함수(ReLU)
각 컨볼루션 후에 비선형 활성화 함수가 적용됩니다. 가장 일반적인
거기 있어요 ReLU(정류 선형 장치): f(x) = max(0, x).
ReLU는 모든 음수 값을 재설정하고 양수 값은 변경하지 않고 그대로 둡니다. 비선형성 없이,
일련의 컨볼루션은 단일 선형 변환과 동일합니다.
네트워크가 복잡한 패턴을 학습할 수 없도록 만듭니다.
ReLU: f(x) = max(0, x) Semplice, veloce, standard
LeakyReLU: f(x) = x se x>0, 0.01x altrimenti Evita "dying ReLU"
GELU: f(x) = x * Phi(x) Usata nei Transformers, smooth
Swish: f(x) = x * sigmoid(x) Usata in EfficientNet
Input feature map: Dopo ReLU:
+----+-----+----+ +----+----+----+
| -3 | 5 | -1 | | 0 | 5 | 0 |
+----+-----+----+ +----+----+----+
| 2 | -7 | 4 | | 2 | 0 | 4 |
+----+-----+----+ +----+----+----+
| -2 | 1 | -5 | | 0 | 1 | 0 |
+----+-----+----+ +----+----+----+
3.3 풀링 레이어
풀링은 특징 맵의 공간 차원을 줄여 매개변수 수를 줄입니다. 특징 위치의 작은 변화에도 네트워크를 더욱 강력하게 만듭니다. 두 가지 유형 주요한 것들은 최대 풀링 (각 창에서 최대값을 취함) 전자 평균 풀링 (평균을 계산하십시오).
Input (4x4): Output (2x2):
+----+----+----+----+ +----+----+
| 12 | 20| 30| 0 | | 20 | 30 | max(12,20,0,8)=20
+----+----+----+----+ --> +----+----+ max(30,0,2,14)=30
| 0 | 8 | 2 | 14 | | 15 | 16 |
+----+----+----+----+ +----+----+
| 15 | 2 | 3 | 16 |
+----+----+----+----+ Riduce 4x4 --> 2x2
| 1 | 6 | 7 | 8 | (dimezza altezza e larghezza)
+----+----+----+----+
Max Pooling: prende il massimo --> preserva le feature più forti
Avg Pooling: calcola la media --> effetto di smoothing
Global Average Pooling: media su tutta la feature map --> un singolo valore per canale
3.4 배치 정규화
La 배치 정규화(BatchNorm) 각 레이어의 출력을 정규화합니다. 평균과 단위 분산이 0이 되도록 합니다. 이를 통해 훈련이 안정화되고 학습률이 높고 가벼운 정규화 장치 역할을 합니다. 실무에서는 딱 맞는데 활성화 전 각 컨볼루션 후 BatchNorm 레이어.
4. 전형적인 CNN 아키텍처
표준 CNN은 반복 패턴을 따릅니다: 특징 추출 블록(컨볼루션) + 활성화 + 풀링) 최종 분류를 위해 완전히 연결된 레이어가 이어집니다. 깊이가 있으면 특징 맵의 공간 크기는 줄어들지만 개수는 늘어납니다. 점점 더 추상적인 패턴을 포착하는 채널.
Input Immagine (3 x 32 x 32)
|
v
[Conv2d 3->32, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 32 x 16 x 16
v
[Conv2d 32->64, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 64 x 8 x 8
v
[Conv2d 64->128, 3x3, pad=1] --> [BatchNorm] --> [ReLU] --> [MaxPool 2x2]
| Feature maps: 128 x 4 x 4
v
[Flatten] --> Vettore di 128 * 4 * 4 = 2048 valori
|
v
[Linear 2048 -> 256] --> [ReLU] --> [Dropout 0.5]
|
v
[Linear 256 -> 10] --> Output: 10 classi (es. CIFAR-10)
|
v
[Softmax] --> Probabilità per ogni classe: [0.02, 0.01, 0.85, ...]
Flusso delle dimensioni:
(3, 32, 32) -> (32, 16, 16) -> (64, 8, 8) -> (128, 4, 4) -> (2048) -> (256) -> (10)
[immagine] [bordi, texture] [parti] [oggetti] [decisione] [classe]
학습된 기능의 계층 구조
- 레이어 1-2(낮은 수준): 테두리, 색상 그라데이션, 단순한 텍스처
- 레이어 3-5(중간 수준): 모서리, 윤곽선, 물체의 일부(눈, 바퀴)
- 레이어 6+(높은 수준): 완전한 사물, 장면, 추상적 개념
이 계층 구조는 훈련 중에 자동으로 나타납니다. 필요없어 네트워크에 무엇을 찾아야 하는지 알려줍니다. 필터는 데이터에 적응합니다.
5. CNN 아키텍처의 진화
CNN의 역사는 혁명적인 아키텍처로 특징지어지며, 각각의 아키텍처는 분야를 바꾼 아이디어. 이러한 진화를 아는 것이 이해의 기본입니다. 현대 건축 선택.
CNN 아키텍처의 타임라인
| 년도 | 건축학 | 주요 혁신 | 상위 1위 ImageNet |
|---|---|---|---|
| 1998년 | 르넷-5 | 최초의 실용적인 CNN(숫자 인식) | 해당 없음 |
| 2012년 | 알렉스넷 | GPU 훈련, ReLU, 드롭아웃 | 63.3% |
| 2014년 | VGGNet | 균일한 3x3 필터를 갖춘 심층 네트워크 | 74.5% |
| 2014년 | GoogLeNet/인셉션 | 개시 모듈, 다중 규모 병렬 | 74.8% |
| 2015년 | 레스넷 | 연결 건너뛰기, 152개 이상의 레이어 네트워크 | 78.6% |
| 2019 | EfficientNet | 복합 스케일링(깊이+너비+해상도) | 84.4% |
| 2022년 | ConvNeXt | Vision Transformers에서 영감을 받은 현대화된 CNN | 87.8% |
5.1 LeNet-5(1998) - 개척자
필기 숫자 인식(MNIST)을 위해 Yann LeCun이 설계한 LeNet-5 실질적으로 성공한 최초의 CNN입니다. 단 5개의 레이어와 60,000개의 매개변수만으로 입증되었습니다. 컨볼루션은 이미지로부터 차별적인 특징을 학습할 수 있습니다. 그것은 다음과 같은 용도로 사용되었습니다. 자동으로 은행 수표를 읽습니다.
5.2 AlexNet(2012) - 혁명
AlexNet은 ImageNet 2012 대회에서 큰 차이로 승리하여 오류를 줄였습니다. 26%에서 16%로. 주요 혁신: GPU 트레이닝(NVIDIA GTX 580 2개), 기능 시그모이드 대신 ReLU 활성화, 정규화 및 데이터 증대를 위한 드롭아웃. 이 결과는 학계와 업계에서 딥러닝이 효과가 있다는 확신을 갖게 했습니다.
5.3 VGGNet(2014) - 깊이 문제
VGG는 더 깊은 네트워크가 더 나은 결과를 생성한다는 것을 보여주었습니다. 그의 핵심 아이디어 단순함의 급진적: 3x3 스택 필터만 사용하세요. 2개의 연속된 3x3 레이어 단일 5x5 레이어와 동일한 수용 필드를 가지지만 매개변수는 점점 더 많아집니다. 비선형성. VGG-16에는 16개 레이어와 1억 3,800만 개의 매개변수가 있습니다.
5.4 EfficientNet(2019) - 지능형 확장
EfficientNet은 복합 스케일링: 그냥 늘리는 대신 깊이(예: VGG) 또는 너비는 세 가지 차원을 모두 균일하게 확장합니다. (깊이, 너비, 입력 해상도) 균형 잡힌 계수를 사용합니다. EfficientNet-B0 단 530만 개의 매개변수로 ImageNet에서 77.1%의 정확도를 달성한 보고서 전례 없는 정확도 매개변수.
5.5 ConvNeXt(2022) - 현대화된 CNN
ConvNeXt는 Vision Transformers에서 영감을 받은 기술로 현대화된 CNN이 트랜스포머 아키텍처와 경쟁하거나 능가할 수 있습니다. 혁신에는 다음이 포함됩니다. 7x7 깊이 분리 가능한 커널, BatchNorm 대신 LayerNorm, GELU 활성화, 그리고 단계적으로 크기가 증가하는 "동형" 디자인입니다. ConvNeXt V2 버전 E-ConvNeXt-Tiny, 단 2.0 GFLOP로 80.6% Top-1 달성, 배포에 탁월함 효율적이다.
6. ResNet 및 연결 건너뛰기
ResNet(잔여 네트워크), He et al.에 의해 제안됨. 2015년에 해결됐어요 딥러닝의 근본적인 문제 중 하나는 성능 저하 문제. ResNet 이전에는 심층 네트워크에 레이어를 추가하면 결과가 좋아지기는커녕 오히려 나빠졌습니다. 훈련 세트에서도요. 해결책은 단순한 만큼 우아합니다.
6.1 경사도 소실 문제
역전파 동안 기울기는 다음과 같이 반복적으로 곱해집니다. 네트워크의 계층. 이러한 곱셈이 1보다 작은 값을 생성하는 경우 기울기는 초기 레이어로 전파되면서 기하급수적으로 "사라집니다". 50개 이상의 레이어를 사용하면 그라데이션이 너무 작아져서 처음 몇 개의 레이어가 중단됩니다. 배우기 위해. 최근 연구에 따르면 연결 건너뛰기가 없으면 L2는 초기 레이어에서는 그라디언트가 급격히 떨어지는 반면 건너뛰기 연결을 사용하면 네트워크 전체에서 균일하게 유지됩니다.
Rete profonda SENZA skip connections (50 layer):
Layer 50 Layer 49 Layer 48 ... Layer 2 Layer 1
grad=1.0 * 0.8 * 0.8 ... * 0.8 * 0.8
Gradiente al layer 1: 0.8^49 = 0.00001 --> quasi zero!
I primi layer NON apprendono.
Rete profonda CON skip connections (ResNet):
Il gradiente ha un "percorso diretto" attraverso le skip connections.
Non viene moltiplicato ripetutamente per valori piccoli.
Gradiente al layer 1: ~0.5 --> i layer apprendono normalmente!
6.2 해결책: 잔여 학습
ResNet의 뛰어난 아이디어는 간단합니다. 블록을 만드는 대신 변환을 학습하는 것입니다.
완료 H(x), 당신은 그에게 단지 차이점 (잔여)
입력과 관련하여: F(x) = H(x) - x. 블록의 출력은 다음과 같습니다.
y = F(x) + x, 어디 x 블록을 "점프"하는 것은 입력입니다.
하나 연결 건너뛰기 (또는 바로가기 연결).
Blocco Standard: Residual Block:
x x ------+
| | |
v v | (skip connection)
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
[ReLU] [ReLU] |
| | |
[Conv 3x3] [Conv 3x3] |
| | |
[BatchNorm] [BatchNorm] |
| | |
v v |
H(x) = output F(x) + x <--+
|
[ReLU]
|
v
output
Se F(x) = 0, l'output è semplicemente x (identità).
La rete può "saltare" un blocco se non serve.
Questo rende il training di reti profonde stabile.
작동하기 때문에
잔여물 학습 F(x) = 0 (즉, "아무 것도 하지 않음")이 훨씬 쉽습니다.
완전한 정체성 변화를 배우는 것보다. 레이어가 유용하지 않으면 네트워크
그냥 배우세요 F(x) = 0 입력을 변경하지 않고 전달합니다. 이를 통해
성능 저하 없이 수백 개의 레이어로 구성된 네트워크를 구축합니다.
7. CNN 훈련
CNN을 훈련한다는 것은 모든 커널(필터)에 대한 최적의 값을 찾는 것을 의미합니다. 그리고 완전히 연결된 레이어의 가중치. 이는 반복적인 프로세스를 통해 발생합니다. 순방향 통과, 손실 계산 및 그래디언트 역전파.
7.1 손실함수
이미지 분류를 위한 표준 손실 함수는 다음과 같습니다. 교차 엔트로피 손실. 네트워크에서 예측한 확률이 실제 레이블에서 얼마나 벗어나는지를 측정합니다. 완벽한 예측은 손실 = 0을 생성합니다. 완전히 잘못된 예측이 나온다 무한대로 가는 손실.
Etichetta reale (one-hot): [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] (classe "gatto" = indice 2)
Predizione buona: [0.02, 0.03, 0.85, 0.02, 0.01, 0.02, 0.01, 0.02, 0.01, 0.01]
Loss = -log(0.85) = 0.16 --> Loss bassa, predizione corretta
Predizione cattiva: [0.30, 0.25, 0.05, 0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]
Loss = -log(0.05) = 3.00 --> Loss alta, predizione sbagliata
7.2 옵티마이저
최적화 프로그램은 손실을 줄이는 방향으로 네트워크 가중치를 업데이트합니다. 가장 많이 사용되는 것은 다음과 같습니다:
최적화 비교
| 옵티마이저 | 형질 | 언제 사용하는가 |
|---|---|---|
| SGD + 모멘텀 | 간단하고 견고하며 안정적인 수렴 | 긴 훈련, 최대 최종 정확도 |
| 아담 | 적응형 학습률, 빠른 수렴 | 프로토타이핑, 중소 네트워크 |
| 아담 여 | 올바른 체중 감소를 보이는 Adam | CNN에 권장되는 최신 표준 |
7.3 데이터 확대
La 데이터 증대 Overfitting을 방지하기 위한 기본 기술입니다. 네트워크 일반화를 개선합니다. 무작위 변환을 적용하는 것으로 구성됩니다. 변형을 만들기 위한 이미지 교육(회전, 뒤집기, 자르기, 밝기 변경) 새로운 데이터를 수집하지 않고 합성합니다.
Immagine Originale: Trasformazioni:
+-------+ [Flip orizzontale] --> Immagine specchiata
| Gatto | [Rotazione +-15 gradi]--> Leggera rotazione
| --o-- | [Random Crop] --> Ritaglio casuale
| /|\ | [Color Jitter] --> Variazione colori
+-------+ [Gaussian Noise] --> Aggiunta rumore
[Cutout/Erasing] --> Maschera rettangolare casuale
[MixUp] --> Media pesata di 2 immagini
[CutMix] --> Porzione di un'immagine sovrapposta
Effetto: da 50.000 immagini di training, ogni epoca vede variazioni
diverse, come se avessi milioni di immagini uniche.
8. PyTorch에서 구현 완료
이론에서 실습으로 넘어가겠습니다. 분류를 위해 전체 CNN을 구현하겠습니다. 데이터 세트의 이미지 CIFAR-10 (10개 클래스의 32x32 이미지 60,000개: 비행기, 자동차, 새, 고양이, 사슴, 개, 개구리, 말, 배, 트럭).
8.1 설정 및 데이터 세트
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# Trasformazioni con data augmentation per il training
train_transform = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomCrop(32, padding=4),
transforms.ColorJitter(brightness=0.2, contrast=0.2),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465], # Media CIFAR-10
std=[0.2470, 0.2435, 0.2616] # Std CIFAR-10
),
])
# Trasformazioni per il test (nessuna augmentation)
test_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
# Caricamento dataset
train_dataset = torchvision.datasets.CIFAR10(
root='./data', train=True, download=True, transform=train_transform
)
test_dataset = torchvision.datasets.CIFAR10(
root='./data', train=False, download=True, transform=test_transform
)
# DataLoader con batching e shuffling
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=4)
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
8.2 모델의 정의
class ResidualBlock(nn.Module):
"""Blocco residuale con skip connection."""
def __init__(self, in_channels: int, out_channels: int, stride: int = 1):
super().__init__()
self.conv1 = nn.Conv2d(
in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False
)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(
out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False
)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# Skip connection: se le dimensioni cambiano, usa conv 1x1
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = self.shortcut(x)
out = self.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out = out + identity # Skip connection
out = self.relu(out)
return out
class CIFAR10CNN(nn.Module):
"""CNN con blocchi residuali per classificazione CIFAR-10."""
def __init__(self, num_classes: int = 10):
super().__init__()
# Primo layer convoluzionale
self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(32)
self.relu = nn.ReLU(inplace=True)
# Blocchi residuali con profondità crescente
self.layer1 = self._make_layer(32, 64, num_blocks=2, stride=1)
self.layer2 = self._make_layer(64, 128, num_blocks=2, stride=2)
self.layer3 = self._make_layer(128, 256, num_blocks=2, stride=2)
# Classificatore finale
self.global_avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.3)
self.fc = nn.Linear(256, num_classes)
def _make_layer(
self, in_ch: int, out_ch: int, num_blocks: int, stride: int
) -> nn.Sequential:
layers = [ResidualBlock(in_ch, out_ch, stride)]
for _ in range(1, num_blocks):
layers.append(ResidualBlock(out_ch, out_ch, stride=1))
return nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.relu(self.bn1(self.conv1(x))) # (B, 32, 32, 32)
x = self.layer1(x) # (B, 64, 32, 32)
x = self.layer2(x) # (B, 128, 16, 16)
x = self.layer3(x) # (B, 256, 8, 8)
x = self.global_avg_pool(x) # (B, 256, 1, 1)
x = x.view(x.size(0), -1) # (B, 256)
x = self.dropout(x)
x = self.fc(x) # (B, 10)
return x
8.3 훈련 루프
def train_model(
model: nn.Module,
train_loader: DataLoader,
test_loader: DataLoader,
epochs: int = 50,
lr: float = 0.01
) -> dict:
"""Addestra il modello e restituisce la storia del training."""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(
model.parameters(), lr=lr, momentum=0.9, weight_decay=1e-4
)
scheduler = optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=lr, epochs=epochs,
steps_per_epoch=len(train_loader)
)
history = {'train_loss': [], 'test_loss': [], 'test_acc': []}
for epoch in range(epochs):
# --- Training ---
model.train()
running_loss = 0.0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
scheduler.step()
running_loss += loss.item()
avg_train_loss = running_loss / len(train_loader)
# --- Evaluation ---
model.eval()
test_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
test_loss += loss.item()
_, predicted = outputs.max(1)
total += labels.size(0)
correct += predicted.eq(labels).sum().item()
avg_test_loss = test_loss / len(test_loader)
accuracy = 100.0 * correct / total
history['train_loss'].append(avg_train_loss)
history['test_loss'].append(avg_test_loss)
history['test_acc'].append(accuracy)
print(
f"Epoch [{epoch+1}/{epochs}] "
f"Train Loss: {avg_train_loss:.4f} | "
f"Test Loss: {avg_test_loss:.4f} | "
f"Accuracy: {accuracy:.2f}%"
)
return history
# Esecuzione
model = CIFAR10CNN(num_classes=10)
history = train_model(model, train_loader, test_loader, epochs=50, lr=0.1)
# Output atteso dopo 50 epoche con OneCycleLR:
# Test Accuracy: ~92-93%
CIFAR-10에 권장되는 훈련 매개변수
| 매개변수 | Valore | 동기 부여 |
|---|---|---|
| 배치 크기 | 128 | 속도와 경사 안정성 사이의 균형 |
| 학습률 | 0.1(OneCycleLR 포함) | 스케줄링 기능을 갖춘 SGD는 탁월한 정확성을 달성합니다. |
| 체중 감소 | 1e-4 | 과적합을 방지하기 위한 L2 정규화 |
| 시대 | 50 | OneCycleLR을 사용하면 50개의 에포크가 수렴하는 데 충분합니다. |
| 탈락자 | 0.3 | 최종 레이어 이전의 추가 정규화 |
9. 전이 학습
실제로 CNN을 처음부터 훈련시키는 경우는 거의 없습니다. 그만큼 전이 학습 대규모 데이터 세트(예: 120만 개가 포함된 ImageNet)에서 사전 훈련된 모델을 재사용할 수 있습니다. 이미지와 1,000개의 클래스) 이를 특정 문제에 맞게 조정하세요. 이렇게 하면 훈련 시간과 필요한 데이터 양을 대폭 줄이고 성능을 향상시킵니다.
9.1 특징 추출과 미세 조정
두 가지 전이 학습 전략
| 전략 | 작동 방식 | 언제 사용하는가 |
|---|---|---|
| 특징 추출 | 사전 훈련된 모든 레이어를 동결하고 최종 분류자만 훈련시킵니다. | ImageNet과 유사한 도메인의 데이터가 적음(이미지 1,000개 미만) |
| 미세 조정 | 마지막 N개 레이어를 동결 해제하고 낮은 학습률로 재훈련 | 더 많은 데이터 사용 가능, ImageNet 이외의 도메인 |
import torchvision.models as models
def create_transfer_model(
num_classes: int,
freeze_backbone: bool = True
) -> nn.Module:
"""Crea un modello con transfer learning da ResNet-18."""
# Carica ResNet-18 pre-addestrato su ImageNet
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# Strategia 1: Feature Extraction (congela il backbone)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
# Sostituisci il classificatore finale
num_features = model.fc.in_features # 512 per ResNet-18
model.fc = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(num_features, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, num_classes)
)
return model
# Feature extraction: solo il classificatore viene addestrato
feature_model = create_transfer_model(num_classes=10, freeze_backbone=True)
# Fine-tuning: tutto il modello viene riaddestrato
finetune_model = create_transfer_model(num_classes=10, freeze_backbone=False)
# Per il fine-tuning, usa un learning rate più basso
optimizer = optim.AdamW([
{'params': finetune_model.layer4.parameters(), 'lr': 1e-4},
{'params': finetune_model.fc.parameters(), 'lr': 1e-3},
], weight_decay=1e-2)
Guida al Transfer Learning:
Dati simili a ImageNet Dati diversi da ImageNet
(oggetti, animali, scene) (medico, satellite, micro)
+---------------------------+---------------------------+
Pochi dati | Feature Extraction | Feature Extraction |
(<1.000 img) | Congela tutto, addestra FC | + Aumenta data augment. |
+---------------------------+---------------------------+
Molti dati | Fine-tuning ultimi layer | Fine-tuning completo |
(>5.000 img) | LR basso per il backbone | o training da zero |
+---------------------------+---------------------------+
10. 평가 지표
정확도만으로는 CNN이 제대로 작동하는지 이해하는 데 충분하지 않습니다. 불균형 데이터 세트 사용 (예: 클래스 A 95%, 클래스 B 5%), 항상 "클래스 A"를 예측하는 모델의 정확도는 95%입니다. 하지만 그것은 전혀 쓸모가 없습니다. 보다 세부적인 측정항목이 필요합니다.
이미지 분류 측정항목
| 미터법 | 측정 대상 | 공식 |
|---|---|---|
| 정확성 | 전체 예측 중 올바른 예측의 비율 | TP + TN / 합계 |
| 정도 | 긍정적인 예측 중 몇 개가 맞나요? | TP / (TP + FP) |
| 상기하다 | 실제 양성 중에서 몇 개가 발견되었습니까? | TP / (TP + FN) |
| F1 점수 | 정밀도와 재현율의 조화 평균 | 2 * (P * R) / (P + R) |
Confusion Matrix (predizioni vs realta):
Predetto: aereo auto uccel gatto cervo cane rana caval nave camion
Reale:
aereo [92] 1 2 0 0 0 1 0 3 1
auto 0 [95] 0 0 0 0 0 0 1 4
uccello 3 0 [85] 3 2 2 3 1 1 0
gatto 0 1 2 [78] 1 12 3 2 0 1
cervo 1 0 3 2 [88] 1 2 3 0 0
Interpretazione:
- Diagonale = predizioni corrette (più alto = meglio)
- Fuori diagonale = errori (confusioni tra classi)
- gatto vs cane: 12 gatti classificati come cani --> classi "confuse"
from sklearn.metrics import (
classification_report,
confusion_matrix
)
import numpy as np
def evaluate_model(
model: nn.Module,
test_loader: DataLoader,
device: torch.device,
class_names: list[str]
) -> dict:
"""Valuta il modello e restituisce metriche dettagliate."""
model.eval()
all_preds = []
all_labels = []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
_, predicted = outputs.max(1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.numpy())
preds_array = np.array(all_preds)
labels_array = np.array(all_labels)
# Report dettagliato per classe
report = classification_report(
labels_array, preds_array,
target_names=class_names, output_dict=True
)
# Confusion matrix
cm = confusion_matrix(labels_array, preds_array)
# Accuracy globale
accuracy = np.mean(preds_array == labels_array) * 100
print(f"Accuracy globale: {accuracy:.2f}%")
print(classification_report(
labels_array, preds_array, target_names=class_names
))
return {'accuracy': accuracy, 'report': report, 'confusion_matrix': cm}
11. 배포: 훈련된 모델에서 프로덕션까지
모델을 훈련하는 것은 작업의 절반에 불과합니다. 제품화하려면 수출해야 합니다. 최적화된 형식으로 추론 API를 생성하고 모든 것을 컨테이너화합니다. 확장 가능한 배포를 위해.
11.1 모델 내보내기
내보내기 형식
| 체재 | 사용 | 장점 |
|---|---|---|
| 토치스크립트 | Python 없이 PyTorch 추론 | Python 종속성 없음, 전체 직렬화 |
| ONNX | 범용 형식, 다중 프레임워크 | TensorRT, OpenVINO, CoreML과 호환 가능 |
| 텐서RT | NVIDIA GPU에 최적화된 추론 | 기본 PyTorch보다 최대 5배 더 빠릅니다. |
import torch.onnx
def export_model(model: nn.Module, export_path: str) -> None:
"""Esporta il modello in TorchScript e ONNX."""
model.eval()
dummy_input = torch.randn(1, 3, 32, 32)
# --- TorchScript ---
scripted_model = torch.jit.trace(model, dummy_input)
scripted_model.save(f"{export_path}/model_scripted.pt")
print("TorchScript salvato.")
# --- ONNX ---
torch.onnx.export(
model,
dummy_input,
f"{export_path}/model.onnx",
input_names=['image'],
output_names=['prediction'],
dynamic_axes={
'image': {0: 'batch_size'},
'prediction': {0: 'batch_size'}
},
opset_version=17
)
print("ONNX salvato.")
export_model(model, './exports')
11.2 FastAPI를 사용한 추론 API
# inference_api.py
from fastapi import FastAPI, UploadFile
from PIL import Image
import torch
import torchvision.transforms as transforms
import io
app = FastAPI(title="CNN Image Classifier")
# Carica il modello TorchScript
model = torch.jit.load("./exports/model_scripted.pt")
model.eval()
CLASSES = ['aereo', 'auto', 'uccello', 'gatto', 'cervo',
'cane', 'rana', 'cavallo', 'nave', 'camion']
preprocess = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2470, 0.2435, 0.2616]
),
])
@app.post("/predict")
async def predict(file: UploadFile):
"""Classifica un'immagine caricata."""
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
tensor = preprocess(image).unsqueeze(0)
with torch.no_grad():
outputs = model(tensor)
probabilities = torch.softmax(outputs, dim=1)
confidence, predicted = probabilities.max(1)
return {
"class": CLASSES[predicted.item()],
"confidence": round(confidence.item() * 100, 2),
"all_probabilities": {
name: round(prob.item() * 100, 2)
for name, prob in zip(CLASSES, probabilities[0])
}
}
# Esecuzione: uvicorn inference_api:app --host 0.0.0.0 --port 8000
11.3 Docker를 사용한 컨테이너화
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY exports/ ./exports/
COPY inference_api.py .
EXPOSE 8000
CMD ["uvicorn", "inference_api:app", "--host", "0.0.0.0", "--port", "8000"]
# Build: docker build -t cnn-classifier .
# Run: docker run -p 8000:8000 cnn-classifier
# Test: curl -X POST -F "file=@cat.jpg" http://localhost:8000/predict
Pipeline: Training --> Export --> Container --> Deploy
[Training] [Export] [Container] [Deploy]
PyTorch + GPU --> TorchScript/ONNX --> Docker --> Kubernetes
50 epoche ~10 MB file FastAPI Auto-scaling
~92% accuracy Ottimizzato Health checks Load balancer
No dipendenza Python GPU opzionale Monitoring
Alternativa serverless:
Export ONNX --> AWS Lambda + ONNX Runtime --> API Gateway
Pro: Pay-per-use, zero infrastruttura da gestire
Contro: Cold start (~2s), limite 250MB package
결론 및 다음 단계
이 기사에서 우리는 컨볼루셔널 신경망(Convolutional Neural Networks)에 대한 포괄적인 이해를 구축했습니다. 기본(컴퓨터가 이미지를 보는 방법, 컨볼루션 작업)부터 시작하여 실제 구현(PyTorch의 잔여 블록이 있는 CNN) 및 프로덕션 배포 (TorchScript, ONNX, FastAPI, 도커).
우리는 1998년 LeNet-5에서 2022년 ConvNeXt까지 아키텍처가 어떻게 진화했는지 살펴보았습니다. Skip Connection(ResNet)과 같은 아이디어 덕분에 성능이 점진적으로 향상되었습니다. 복합 스케일링(EfficientNet) 및 변압기에서 영감을 받은 설계(ConvNeXt).
기억해야 할 핵심 사항
- CNN의 악용 소재지, 부담 공유 e 공간적 불변성 이미지를 효율적으로 처리하기 위해
- 표준 아키텍처는 Conv + BatchNorm + ReLU + Pooling 패턴을 따르며, 깊이가 증가하면서 반복됩니다.
- Le 연결 건너뛰기 (ResNet)은 경사도 소멸 없이 심층 네트워크를 훈련하는 데 필수적입니다.
- Il 전이 학습 특히 데이터가 거의 없는 경우 처음부터 훈련하는 것보다 거의 항상 선호됩니다.
- La 데이터 증대 일반화에 필수적이며 데이터 수집 측면에서 비용이 0입니다.
- 제조를 위해 수출 ONNX o 토치스크립트 Docker로 컨테이너화
시리즈의 다음 기사에서는 전이 학습 및 미세 조정: 올바른 사전 학습 모델을 선택하는 방법, 점진적인 미세 조정 전략, 영역 적응 및 지식 증류와 같은 고급 기술. 세 번째 기사에서는 우리는 직면하게 될 것이다 YOLO를 이용한 객체 감지, 물체 감지 시스템 업계에서 가장 많이 사용되는 실시간.
추가 리소스
- 원본 ResNet 논문: “이미지 인식을 위한 심층 잔여 학습”(He et al., 2015)
- 종이 ConvNeXt: “2020년대를 위한 ConvNet”(Liu 외, 2022)
- 종이 EfficientNet: “EfficientNet: CNN을 위한 모델 스케일링 재고”(Tan & Le, 2019)
- PyTorch 문서: CNN 및 Torchvision에 대한 튜토리얼
- CS231n 스탠포드: 시각적 인식을 위한 컨볼루셔널 신경망(온라인 과정)
- ONNX 런타임: 최적화된 크로스 플랫폼 추론 문서







