본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 9. · 139 Views
이미지 세그멘테이션 완벽 가이드
이미지의 각 픽셀을 분류하는 세그멘테이션 기술을 처음부터 차근차근 배워봅니다. Semantic, Instance, Panoptic 세그멘테이션의 차이부터 FCN, UNet 같은 핵심 아키텍처까지 실무에서 바로 활용할 수 있는 내용을 담았습니다.
목차
- Semantic vs Instance vs Panoptic
- FCN (Fully Convolutional Network)
- UNet 아키텍처
- Encoder-Decoder 구조
- Skip Connection의 역할
- 의료 영상 세그멘테이션 예시
1. Semantic vs Instance vs Panoptic
이미지 인식 프로젝트를 맡게 된 김개발 씨는 팀장님께 보고를 하러 갔습니다. "이미지에서 사람을 찾는 건 할 수 있는데, 각 사람을 구분하는 건 어떻게 해야 하나요?" 팀장님은 미소를 지으며 답했습니다.
"세그멘테이션 방식을 제대로 선택해야 합니다."
이미지 세그멘테이션은 이미지의 각 픽셀에 의미를 부여하는 작업입니다. 마치 색칠 공부를 하듯이 이미지의 모든 픽셀을 분류합니다.
Semantic은 같은 클래스를 하나로 묶고, Instance는 개별 객체를 구분하며, Panoptic은 둘을 결합한 방식입니다. 어떤 방식을 선택하느냐에 따라 프로젝트의 성공이 달라집니다.
다음 코드를 살펴봅시다.
import numpy as np
import cv2
# Semantic Segmentation: 모든 사람을 같은 색으로
def semantic_seg(image, model):
# 각 픽셀을 클래스로 분류 (사람=1, 배경=0)
mask = model.predict(image) # shape: (H, W)
return mask # 모든 사람이 같은 값 1을 가짐
# Instance Segmentation: 각 사람을 다른 색으로
def instance_seg(image, model):
# 각 객체마다 고유 ID 부여
instances = model.predict(image) # shape: (H, W)
# 첫 번째 사람=1, 두 번째 사람=2, 세 번째 사람=3...
return instances
김개발 씨는 자율주행 스타트업에 입사한 지 한 달이 된 주니어 개발자입니다. 오늘 첫 미팅에서 팀장님이 과제를 주셨습니다.
"도로 위의 사람들을 정확히 인식해야 합니다." 김개발 씨는 자신 있게 대답했습니다. "네, 이미지 분류는 해봤습니다!" 하지만 막상 시작하려니 막막했습니다.
이미지에 사람이 여러 명 있는데, 각각을 어떻게 구분해야 할까요? 같은 팀의 박시니어 씨가 다가왔습니다.
"세그멘테이션에도 여러 종류가 있어요. 우리 프로젝트에 맞는 걸 골라야죠." Semantic Segmentation이란 무엇일까요?
쉽게 비유하자면, Semantic Segmentation은 마치 초등학교 미술 시간에 하던 색칠 공부와 같습니다. "하늘은 파란색으로, 땅은 갈색으로, 사람은 빨간색으로 칠하세요." 같은 종류는 같은 색으로 칠하는 것입니다.
이미지의 모든 픽셀을 클래스 단위로 분류합니다. 예를 들어 도로 사진을 Semantic Segmentation하면 모든 사람 픽셀은 같은 값으로 표시됩니다.
첫 번째 사람이든 두 번째 사람이든 구분하지 않습니다. 단지 "이 픽셀은 사람이다"라고만 알려줍니다.
Instance Segmentation은 어떻게 다를까요? Instance Segmentation은 한 단계 더 나아갑니다.
마치 출석부에 학생 이름을 하나하나 적는 것처럼, 각 객체마다 고유한 번호를 붙입니다. 같은 사람이라도 첫 번째 사람은 1번, 두 번째 사람은 2번, 세 번째 사람은 3번으로 구분합니다.
자율주행에서는 이 구분이 매우 중요합니다. 도로를 건너는 사람이 한 명인지 세 명인지에 따라 브레이크 타이밍이 달라지기 때문입니다.
Semantic만으로는 "사람이 있다"는 사실만 알 수 있지만, Instance를 사용하면 "세 명의 사람이 있다"는 정확한 정보를 얻습니다. Panoptic Segmentation은 또 무엇일까요?
Panoptic은 그리스어로 "모든 것을 본다"는 뜻입니다. Semantic과 Instance를 합친 방식입니다.
사람이나 자동차처럼 셀 수 있는 객체는 Instance로 구분하고, 하늘이나 도로처럼 셀 수 없는 배경은 Semantic으로 처리합니다. 실제 자율주행 시스템에서는 대부분 Panoptic Segmentation을 사용합니다.
"도로가 어디 있고, 그 위에 자동차 3대와 사람 2명이 있다"는 완전한 정보를 제공하기 때문입니다. 박시니어 씨가 설명을 마치자 김개발 씨는 고개를 끄덕였습니다.
"아, 그래서 요구사항을 먼저 파악해야 하는군요!" 단순히 사람을 찾기만 하면 되는지, 각 사람을 구분해야 하는지에 따라 선택이 달라집니다. 실무에서는 어떻게 선택할까요?
의료 영상 분석에서 종양을 찾는다면 Instance Segmentation이 필요합니다. 종양이 몇 개인지가 중요하기 때문입니다.
반면 농작물 분류 같은 경우는 Semantic으로 충분할 수 있습니다. "이 영역은 밀이고, 저 영역은 옥수수다"만 알면 되니까요.
성능과 속도의 트레이드오프도 고려해야 합니다. Semantic이 가장 빠르고, Instance가 중간이며, Panoptic이 가장 느립니다.
실시간 처리가 필요한 로봇 비전이라면 속도를 우선해야 할 수도 있습니다. 김개발 씨는 이제 자신의 프로젝트에 Instance Segmentation이 필요하다는 것을 깨달았습니다.
각 사람을 구분해서 추적해야 하기 때문입니다. 올바른 도구를 선택하는 것, 그것이 프로젝트 성공의 첫걸음입니다.
실전 팁
💡 - 개수 세기가 필요하면 Instance, 영역 분류만 필요하면 Semantic을 선택하세요
- 실시간 처리가 중요하다면 더 간단한 방식부터 시도해보세요
- 최신 자율주행과 로봇 비전은 대부분 Panoptic을 사용합니다
2. FCN (Fully Convolutional Network)
김개발 씨가 세그멘테이션 논문을 읽다가 FCN이라는 단어를 발견했습니다. "Fully Convolutional Network?
완전히 컨볼루션으로만 이루어진 네트워크라는 건가?" 박시니어 씨가 옆에서 말했습니다. "바로 그겁니다.
세그멘테이션의 혁명을 일으킨 구조죠."
FCN은 이미지 세그멘테이션을 위한 최초의 딥러닝 아키텍처입니다. 기존 분류 네트워크의 Fully Connected Layer를 Convolutional Layer로 바꾼 것이 핵심입니다.
이를 통해 입력 크기에 상관없이 픽셀 단위 예측이 가능해집니다. 2015년에 발표되어 현대 세그멘테이션의 기초를 다졌습니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
class FCN(nn.Module):
def __init__(self, num_classes=21):
super(FCN, self).__init__()
# 기존 분류 네트워크 사용 (VGG16 등)
self.features = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2) # 크기 절반으로
)
# Fully Connected 대신 1x1 Conv 사용
self.classifier = nn.Conv2d(64, num_classes, 1)
# Upsampling으로 원본 크기 복원
self.upsample = nn.ConvTranspose2d(num_classes, num_classes,
kernel_size=4, stride=2, padding=1)
김개발 씨는 이미지 분류 모델은 만들어 본 경험이 있습니다. VGG나 ResNet 같은 유명한 네트워크를 사용해서 "고양이인지 강아지인지"를 판별하는 프로젝트였습니다.
그런데 세그멘테이션은 완전히 다른 문제처럼 느껴졌습니다. "분류는 이미지 하나당 답이 하나였는데, 세그멘테이션은 픽셀마다 답이 있잖아요.
어떻게 해야 하죠?" 김개발 씨의 질문에 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다. 기존 분류 네트워크의 문제점은 무엇이었을까요?
일반적인 이미지 분류 네트워크는 마지막에 Fully Connected Layer를 사용합니다. 이 레이어는 입력을 1차원으로 펼쳐서 처리합니다.
예를 들어 224×224 크기의 이미지라면, 마지막 단계에서 모든 공간 정보가 사라지고 하나의 벡터가 됩니다. 이것은 분류에는 완벽합니다.
"이 이미지는 고양이다"라는 하나의 답만 내면 되니까요. 하지만 세그멘테이션은 다릅니다.
모든 픽셀의 위치를 유지하면서 각 픽셀의 클래스를 예측해야 합니다. FCN의 혁신적인 아이디어는 간단했습니다.
"Fully Connected Layer를 버리고, 전부 Convolution으로 바꾸자!" 이것이 FCN의 핵심입니다. Convolutional Layer는 공간 정보를 유지합니다.
입력이 2차원 이미지라면 출력도 2차원 맵입니다. 예를 들어 기존 VGG16의 마지막 부분은 이랬습니다.
"Flatten → FC(4096) → FC(4096) → FC(1000)". FCN은 이것을 모두 1×1 Convolution으로 바꿨습니다.
"Conv(1×1, 4096) → Conv(1×1, 4096) → Conv(1×1, num_classes)". 1×1 Convolution이 뭔지 궁금해하는 김개발 씨에게 박시니어 씨가 설명했습니다.
"1×1 Convolution은 각 픽셀 위치에서 독립적으로 작동하는 작은 분류기예요. 공간 정보는 그대로 유지하면서 채널 차원만 변환합니다." 마치 엑셀에서 각 셀마다 수식을 적용하는 것과 비슷합니다.
그런데 문제가 하나 있습니다. 네트워크를 거치면서 이미지 크기가 줄어든다는 것입니다.
MaxPooling 때문에 원본의 1/32 크기가 되어버립니다. 이걸 어떻게 해결할까요?
Upsampling이 답입니다. FCN은 줄어든 특성 맵을 다시 원본 크기로 키웁니다.
이를 위해 Transposed Convolution(또는 Deconvolution)을 사용합니다. 마치 MaxPooling의 반대 작업을 하는 것입니다.
실제로 FCN에는 세 가지 버전이 있습니다. FCN-32s는 한 번에 32배 업샘플링합니다.
빠르지만 결과가 거칠습니다. FCN-16s와 FCN-8s는 중간 레이어의 정보를 합쳐서 더 정밀한 결과를 만듭니다.
김개발 씨가 코드를 보며 놀라워했습니다. "생각보다 간단한데요?" 박시니어 씨가 웃으며 답했습니다.
"좋은 아이디어는 언제나 간단해 보이죠. 하지만 처음 생각해낸 사람들은 정말 대단한 겁니다." 실무에서 FCN의 활용은 어떨까요?
2015년 당시 FCN은 PASCAL VOC 데이터셋에서 최고 성능을 달성했습니다. 이전 방법들과 비교해 압도적인 차이였습니다.
이후 UNet, DeepLab 같은 더 발전된 구조들이 나왔지만, 모두 FCN의 기본 아이디어를 계승합니다. 오늘날 FCN을 그대로 사용하는 경우는 드뭅니다.
하지만 세그멘테이션을 배우는 사람이라면 반드시 이해해야 할 기초입니다. 마치 프로그래밍을 배울 때 "Hello World"를 거치는 것처럼 말입니다.
김개발 씨는 이제 세그멘테이션 네트워크의 기본 원리를 이해했습니다. Convolution으로 특징을 뽑고, Upsampling으로 크기를 복원하는 것.
이 간단한 원리가 현대 컴퓨터 비전의 핵심입니다.
실전 팁
💡 - FCN은 입력 크기에 제한이 없어 다양한 해상도의 이미지를 처리할 수 있습니다
- 1×1 Convolution은 Fully Connected와 같은 역할을 하지만 공간 정보를 유지합니다
- FCN-8s가 FCN-32s보다 정밀하지만 계산량이 더 많습니다
3. UNet 아키텍처
"의료 영상 세그멘테이션 프로젝트를 맡았는데, 데이터가 30장밖에 없어요." 김개발 씨의 하소연에 박시니어 씨가 답했습니다. "그럼 UNet을 써보세요.
적은 데이터로도 놀라운 성능을 내는 구조입니다."
UNet은 2015년 의료 영상 세그멘테이션을 위해 개발된 아키텍처입니다. U자 모양의 대칭 구조가 특징이며, Encoder에서 특징을 추출하고 Decoder에서 복원합니다.
Skip Connection으로 세밀한 디테일을 보존하여 적은 데이터로도 높은 정확도를 달성합니다. 현재까지도 가장 널리 사용되는 세그멘테이션 구조 중 하나입니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
class UNet(nn.Module):
def __init__(self, in_channels=1, num_classes=2):
super(UNet, self).__init__()
# Encoder (Contracting Path)
self.enc1 = self.conv_block(in_channels, 64)
self.enc2 = self.conv_block(64, 128)
# Decoder (Expanding Path)
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
self.dec1 = self.conv_block(128, 64) # 128 = 64(up) + 64(skip)
self.out = nn.Conv2d(64, num_classes, 1)
def conv_block(self, in_ch, out_ch):
return nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, padding=1),
nn.ReLU(),
nn.Conv2d(out_ch, out_ch, 3, padding=1),
nn.ReLU()
)
김개발 씨는 대학 병원과 함께하는 프로젝트를 맡게 되었습니다. 세포 현미경 이미지에서 세포막을 정확히 분리해내는 작업입니다.
의사 선생님들이 손으로 하던 작업을 자동화하는 것이 목표였습니다. 하지만 큰 문제가 있었습니다.
데이터가 고작 30장이었습니다. 딥러닝은 보통 수천, 수만 장의 데이터가 필요한데 말입니다.
낙담한 김개발 씨에게 박시니어 씨가 희망을 주었습니다. UNet의 탄생 배경을 이해하면 왜 적은 데이터에 강한지 알 수 있습니다.
2015년 독일 프라이부르크 대학의 연구팀도 같은 문제에 직면했습니다. 의료 영상은 구하기 어렵습니다.
개인정보 보호 때문에 공개할 수 없고, 전문가가 라벨링하는 데 시간이 오래 걸립니다. 그들은 "적은 데이터로 최고의 성능을 내는 구조"를 고민했습니다.
그 결과가 바로 UNet입니다. 이름에서 알 수 있듯이 네트워크 구조가 알파벳 U자 모양입니다.
왼쪽이 내려가는 경로(Contracting Path), 오른쪽이 올라가는 경로(Expanding Path)입니다. 왼쪽 경로는 일반적인 CNN과 비슷합니다.
Convolution과 MaxPooling을 반복하면서 이미지를 점점 작게 만듭니다. 동시에 채널 수는 늘어납니다.
예를 들어 512×512×1 이미지가 256×256×64, 128×128×128, 64×64×256 이런 식으로 변합니다. 마치 망원경으로 보듯이 전체적인 맥락(context)을 파악합니다.
오른쪽 경로는 거꾸로 갑니다. Upsampling과 Convolution을 반복하면서 이미지를 다시 키웁니다.
64×64에서 128×128, 256×256, 512×512로 돌아옵니다. 마치 현미경으로 확대하듯이 세밀한 위치 정보를 복원합니다.
여기까지는 FCN과 비슷합니다. 그런데 UNet만의 특별한 비밀이 있습니다.
바로 Skip Connection입니다. 왼쪽 경로의 각 단계에서 나온 특성 맵을 복사해서, 오른쪽 경로의 같은 레벨에 직접 전달합니다.
마치 다리를 놓듯이 U자의 양쪽을 연결하는 것입니다. 김개발 씨가 물었습니다.
"왜 굳이 이런 연결이 필요한가요?" 박시니어 씨가 그림을 그리며 설명했습니다. Skip Connection의 핵심 역할을 이해해봅시다.
왼쪽 경로를 거치면서 세밀한 정보가 많이 손실됩니다. MaxPooling 때문입니다.
예를 들어 세포막의 정확한 경계선 같은 디테일이 사라집니다. 오른쪽 경로에서 아무리 Upsampling해도 이미 사라진 정보는 되돌릴 수 없습니다.
그런데 Skip Connection을 사용하면 어떻게 될까요? 왼쪽 경로의 초기 단계에는 세밀한 정보가 그대로 남아 있습니다.
이것을 오른쪽 경로에 직접 전달하면, 사라졌던 디테일을 복구할 수 있습니다. 쉽게 비유하자면, 사진을 축소했다가 다시 확대하면 흐려집니다.
하지만 원본 사진을 따로 보관해두었다가 나중에 합치면 선명함을 유지할 수 있습니다. Skip Connection이 바로 "원본 보관" 역할을 합니다.
실제 구현에서의 디테일을 살펴봅시다. 왼쪽에서 오른쪽으로 특성 맵을 전달할 때, 단순히 더하는 것이 아니라 Concatenate(연결)합니다.
예를 들어 Decoder의 64채널 특성 맵과 Encoder의 64채널 특성 맵을 합치면 128채널이 됩니다. 이후 Convolution으로 다시 64채널로 줄입니다.
이 방식이 중요한 이유는 "맥락 정보"와 "위치 정보"를 모두 활용하기 때문입니다. Decoder는 "이것이 세포다"라는 맥락을 알고 있고, Encoder에서 온 정보는 "정확히 어디에 있다"는 위치를 알려줍니다.
데이터 증강도 UNet의 비밀입니다. 논문 저자들은 Elastic Deformation이라는 기법을 사용했습니다.
이미지를 마치 고무처럼 늘이고 구부리는 것입니다. 세포 이미지는 자연스럽게 변형되는 경우가 많아서, 이런 증강이 매우 효과적이었습니다.
30장의 데이터로도 수천 장의 효과를 낼 수 있었습니다. 김개발 씨가 UNet을 구현해서 학습시켰습니다.
놀랍게도 30장의 데이터만으로 95% 정확도를 달성했습니다. 의사 선생님들도 결과에 만족했습니다.
"이제 라벨링 시간을 10분의 1로 줄일 수 있겠어요!" UNet의 영향력은 의료 영상을 넘어 확산되었습니다. 위성 이미지 분석, 자율주행의 도로 세그멘테이션, 산업 현장의 불량 검출 등 다양한 분야에서 UNet을 사용합니다.
간단하면서도 강력한 구조 덕분입니다. 2015년 논문은 현재까지 수만 회 인용되었습니다.
최근에는 UNet의 변형들이 계속 나오고 있습니다. Attention UNet, UNet++, 3D UNet 등입니다.
하지만 모두 원본 UNet의 핵심 아이디어인 "U자 구조"와 "Skip Connection"을 계승합니다. 김개발 씨는 이제 UNet을 자신 있게 사용할 수 있게 되었습니다.
데이터가 적어도 포기할 필요가 없다는 것을 배웠습니다. 올바른 아키텍처와 데이터 증강을 결합하면 놀라운 결과를 낼 수 있습니다.
실전 팁
💡 - 의료 영상이나 작은 데이터셋에서는 UNet을 먼저 시도해보세요
- Skip Connection의 채널 수가 맞는지 확인하세요 (Concatenate 후 2배가 됨)
- Elastic Deformation 같은 도메인 특화 증강 기법을 활용하세요
4. Encoder-Decoder 구조
"세그멘테이션 모델들이 다 비슷해 보이는데, 공통점이 있나요?" 김개발 씨의 질문에 박시니어 씨가 답했습니다. "맞아요, 대부분 Encoder-Decoder 구조를 따릅니다.
이게 세그멘테이션의 기본 패턴이에요."
Encoder-Decoder 구조는 세그멘테이션의 표준 아키텍처 패턴입니다. Encoder는 입력 이미지를 압축하여 고수준 특징을 추출하고, Decoder는 이를 다시 원본 크기로 복원하며 픽셀 단위 예측을 수행합니다.
마치 정보를 압축했다가 푸는 것과 같습니다. 이 패턴은 AutoEncoder와 유사하지만 목적이 다릅니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
class EncoderDecoderNet(nn.Module):
def __init__(self, num_classes=21):
super(EncoderDecoderNet, self).__init__()
# Encoder: 이미지 압축 및 특징 추출
self.encoder = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2), # 크기 1/2
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2) # 크기 1/4
)
# Decoder: 특징을 원본 크기로 복원
self.decoder = nn.Sequential(
nn.ConvTranspose2d(128, 64, 2, stride=2), # 크기 2배
nn.ReLU(),
nn.ConvTranspose2d(64, num_classes, 2, stride=2) # 크기 2배
)
김개발 씨는 여러 세그멘테이션 논문을 읽으면서 패턴을 발견했습니다. FCN도, UNet도, DeepLab도 모두 비슷한 구조를 가지고 있었습니다.
먼저 이미지를 작게 만들었다가, 다시 크게 키우는 과정을 거칩니다. "이게 다 같은 원리인가요?" 김개발 씨가 물었습니다.
박시니어 씨가 고개를 끄덕였습니다. "네, Encoder-Decoder 패턴이라고 부르는 보편적인 설계 방식입니다." Encoder-Decoder가 왜 필요한지 이해하려면 세그멘테이션의 두 가지 모순된 요구사항을 봐야 합니다.
첫째, 전체적인 맥락을 이해해야 합니다. 예를 들어 도로 사진에서 어떤 픽셀이 사람인지 판단하려면 주변 상황을 봐야 합니다.
건물 앞에 서 있나요? 횡단보도 위인가요?
이런 넓은 시야가 필요합니다. 둘째, 정확한 위치를 알아야 합니다.
사람의 윤곽선을 픽셀 단위로 정확히 그려내야 합니다. 조금만 어긋나도 손가락이 잘려 보이거나 배경이 사람으로 인식될 수 있습니다.
문제는 이 두 가지가 서로 상충한다는 것입니다. 넓은 시야를 가지려면 이미지를 축소해야 하는데, 그러면 정밀도가 떨어집니다.
정밀도를 유지하려면 원본 크기를 써야 하는데, 그러면 맥락을 놓칩니다. Encoder-Decoder는 이 문제를 단계적으로 해결합니다.
쉽게 비유하자면, Encoder-Decoder는 마치 지도를 보는 방식과 같습니다. 먼저 전국 지도로 대략적인 위치를 파악합니다.
"서울에 있구나." 그다음 서울 지도로 좁혀갑니다. "강남구구나." 마지막으로 상세 지도로 정확한 주소를 찾습니다.
Encoder의 역할을 자세히 살펴봅시다. Encoder는 점진적으로 이미지를 압축합니다.
Convolution으로 특징을 뽑고, MaxPooling이나 Stride로 크기를 줄입니다. 512×512가 256×256이 되고, 128×128, 64×64로 계속 작아집니다.
크기가 작아질수록 한 픽셀이 담당하는 영역은 커집니다. 이것을 Receptive Field(수용 영역)라고 부릅니다.
마지막 단계의 한 픽셀은 원본 이미지의 매우 넓은 영역을 "본" 것입니다. 이것이 맥락 정보입니다.
동시에 채널 수는 늘어납니다. 3채널(RGB)에서 시작해서 64, 128, 256, 512채널로 증가합니다.
각 채널은 서로 다른 특징을 감지합니다. "수직선을 감지하는 채널", "동그란 모양을 감지하는 채널" 이런 식입니다.
Decoder의 역할은 Encoder의 반대입니다. Decoder는 압축된 특징 맵을 다시 원본 크기로 복원합니다.
64×64에서 시작해서 128×128, 256×256, 512×512로 키웁니다. 이 과정에서 Upsampling 기법을 사용합니다.
가장 간단한 방법은 Bilinear Interpolation(이중선형보간)입니다. 주변 픽셀 값들을 평균 내서 새 픽셀을 채웁니다.
하지만 학습 가능한 방법인 Transposed Convolution이 더 많이 쓰입니다. 채널 수는 반대로 줄어듭니다.
512에서 256, 128, 64로 감소하다가, 마지막에는 클래스 개수만큼의 채널이 됩니다. 21개 클래스면 21채널입니다.
각 채널이 "이 픽셀이 해당 클래스일 확률"을 나타냅니다. 김개발 씨가 코드를 보다가 질문했습니다.
"Encoder는 VGG나 ResNet 같은 기존 모델을 쓰면 되나요?" 박시니어 씨가 웃으며 답했습니다. "정확합니다!
그걸 Transfer Learning이라고 하죠." 실무에서의 활용을 봅시다. 대부분의 세그멘테이션 모델은 ImageNet에서 사전 학습된 분류 모델을 Encoder로 사용합니다.
ResNet-50, ResNet-101, EfficientNet 등입니다. 이미 좋은 특징 추출 능력을 가지고 있으니, 처음부터 학습할 필요가 없습니다.
Decoder는 프로젝트마다 다르게 설계합니다. 간단한 Transposed Convolution만 쓸 수도 있고, UNet처럼 Skip Connection을 추가할 수도 있습니다.
속도가 중요하면 간단하게, 정확도가 중요하면 복잡하게 만듭니다. Encoder-Decoder의 한계도 있습니다.
Encoder를 거치면서 정보 손실이 발생합니다. 아무리 Decoder가 열심히 복원해도 이미 사라진 정보는 되돌릴 수 없습니다.
그래서 UNet의 Skip Connection이나 DeepLab의 Atrous Convolution 같은 기법들이 개발되었습니다. 또한 계산량이 많습니다.
Encoder와 Decoder를 모두 거쳐야 하므로 추론 시간이 길어집니다. 실시간 세그멘테이션이 필요한 자율주행 같은 분야에서는 경량화가 필수입니다.
김개발 씨는 이제 Encoder-Decoder 구조의 핵심을 이해했습니다. 압축과 복원, 맥락과 위치.
이 균형을 어떻게 잡느냐에 따라 모델의 성능이 달라집니다. 세그멘테이션 논문을 읽을 때 이 관점으로 보면 훨씬 명확하게 이해됩니다.
실전 팁
💡 - Encoder로는 ImageNet 사전학습 모델을 사용하면 성능이 크게 향상됩니다
- Decoder는 프로젝트 요구사항에 맞게 단순하거나 복잡하게 설계하세요
- 정보 손실을 줄이려면 Skip Connection 같은 추가 기법을 고려하세요
5. Skip Connection의 역할
"UNet의 Skip Connection이 왜 그렇게 중요한가요?" 김개발 씨가 물었습니다. 박시니어 씨가 두 장의 세그멘테이션 결과를 보여주며 답했습니다.
"이게 Skip Connection이 있을 때고, 이게 없을 때입니다. 차이가 보이나요?"
Skip Connection은 Encoder의 특징 맵을 Decoder에 직접 전달하는 연결 구조입니다. 정보 손실을 방지하고 그래디언트 흐름을 개선하여 학습을 안정화시킵니다.
세밀한 경계선 검출에 핵심적인 역할을 하며, UNet을 비롯한 대부분의 현대 세그멘테이션 모델에서 필수 요소로 자리잡았습니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
class UNetWithSkip(nn.Module):
def __init__(self):
super(UNetWithSkip, self).__init__()
self.enc1 = nn.Conv2d(3, 64, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.enc2 = nn.Conv2d(64, 128, 3, padding=1)
self.up1 = nn.ConvTranspose2d(128, 64, 2, stride=2)
# Skip Connection: enc1의 64채널 + up1의 64채널 = 128채널
self.dec1 = nn.Conv2d(128, 64, 3, padding=1)
def forward(self, x):
# Encoder
e1 = self.enc1(x) # 저장: Skip Connection용
p1 = self.pool(e1)
e2 = self.enc2(p1)
# Decoder
u1 = self.up1(e2)
# Skip Connection: Concatenate
u1 = torch.cat([u1, e1], dim=1) # 채널 차원으로 연결
d1 = self.dec1(u1)
return d1
김개발 씨는 Skip Connection을 빼고 UNet을 학습시켜 봤습니다. 결과가 놀라웠습니다.
정확도가 90%에서 75%로 떨어졌고, 세포막 경계선이 흐릿하게 나왔습니다. "이 작은 연결이 이렇게 큰 차이를 만드나요?" 박시니어 씨가 설명을 시작했습니다.
"Skip Connection의 마법을 이해하려면 깊은 네트워크의 문제점부터 알아야 합니다." 깊은 네트워크의 근본적인 문제는 무엇일까요? 레이어를 거칠 때마다 정보가 변형됩니다.
Convolution, ReLU, Pooling을 거치면서 원본 정보의 일부가 사라집니다. 특히 MaxPooling은 75%의 픽셀을 버립니다.
2×2 영역에서 최댓값 하나만 남기고 나머지 셋을 삭제하는 것입니다. 10개 레이어를 거치면 어떻게 될까요?
원본 정보의 대부분이 사라집니다. 마치 복사기로 복사를 반복하면 글씨가 흐려지는 것과 같습니다.
이것을 정보 손실(Information Loss)이라고 부릅니다. 세그멘테이션에서는 이것이 치명적입니다.
세포막의 미세한 굴곡, 도로 표시의 정확한 위치, 종양의 불규칙한 경계선 같은 디테일이 모두 사라져버립니다. Skip Connection의 첫 번째 역할은 정보 보존입니다.
쉽게 비유하자면, Skip Connection은 마치 타임캡슐과 같습니다. Encoder 초기 단계의 특징 맵을 "그대로" 보관했다가, Decoder에서 필요할 때 꺼내 씁니다.
Pooling을 거치지 않았으니 원본의 디테일이 생생하게 남아 있습니다. 예를 들어 512×512 이미지를 처리한다고 가정합시다.
Encoder의 첫 번째 레이어 출력은 512×512×64입니다. 이것을 복사해둡니다.
Encoder를 끝까지 거치면 32×32×512가 됩니다. 공간 정보가 1/256로 줄었습니다.
Decoder에서 다시 512×512로 키울 때, 복사해뒀던 512×512×64를 가져와서 합칩니다. 이제 Decoder는 두 가지 정보를 모두 가집니다.
"무엇이 있는가"(Encoder 출력)와 "어디에 있는가"(Skip Connection). Skip Connection의 두 번째 역할은 그래디언트 흐름 개선입니다.
김개발 씨가 처음 듣는 개념이었습니다. "그래디언트 흐름이 뭔가요?" 박시니어 씨가 설명했습니다.
"역전파(Backpropagation) 할 때 그래디언트가 얼마나 잘 전달되느냐를 말합니다." 깊은 네트워크는 Vanishing Gradient(기울기 소실) 문제가 있습니다. 역전파를 하면서 그래디언트가 점점 작아져서, 초기 레이어는 거의 학습이 안 됩니다.
마치 전화기 게임에서 메시지가 왜곡되는 것과 비슷합니다. Skip Connection은 그래디언트가 지름길로 흐를 수 있게 합니다.
Decoder에서 Encoder로 직접 연결되어 있으니, 그래디언트가 수십 개 레이어를 거칠 필요가 없습니다. ResNet의 Residual Connection과 같은 원리입니다.
구현 방법에는 두 가지가 있습니다. 첫째는 Concatenation(연결)입니다.
UNet이 사용하는 방식입니다. Encoder의 특징 맵과 Decoder의 특징 맵을 채널 차원으로 붙입니다.
64채널 + 64채널 = 128채널이 됩니다. 이후 Convolution으로 채널 수를 조정합니다.
둘째는 Addition(덧셈)입니다. ResNet이나 일부 세그멘테이션 모델이 사용합니다.
같은 크기의 특징 맵끼리 픽셀별로 더합니다. 채널 수는 그대로입니다.
Concatenation보다 메모리 효율적이지만, 정보 손실이 약간 있을 수 있습니다. 김개발 씨가 코드를 보며 깨달았습니다.
"아, 그래서 Decoder의 입력 채널이 2배인 거군요!" 맞습니다. Skip Connection으로 받은 채널과 Upsampling에서 온 채널을 합치니까요.
실무에서 주의할 점이 있습니다. 첫째, 크기를 정확히 맞춰야 합니다.
Encoder의 256×256 특징 맵과 Decoder의 256×256 특징 맵을 연결해야 합니다. 크기가 다르면 에러가 납니다.
Padding을 잘 설정해서 크기를 맞추는 것이 중요합니다. 둘째, 메모리 사용량이 증가합니다.
Encoder의 모든 중간 특징 맵을 메모리에 보관해야 하기 때문입니다. GPU 메모리가 부족하면 배치 크기를 줄이거나 이미지 해상도를 낮춰야 합니다.
셋째, 모든 레벨에서 Skip Connection을 쓸 필요는 없습니다. 실험을 통해 어느 레벨이 중요한지 파악하세요.
때로는 초기 레벨 몇 개만 연결해도 충분한 성능이 나옵니다. Skip Connection의 변형들도 많습니다.
Attention UNet은 Skip Connection에 Attention 메커니즘을 추가합니다. "어느 부분이 중요한가"를 학습해서, 중요한 부분만 강조해서 전달합니다.
Dense UNet은 모든 레이어를 서로 연결합니다. 더 풍부한 정보 흐름을 만들지만, 계산량이 많습니다.
김개발 씨는 이제 Skip Connection의 중요성을 완전히 이해했습니다. 단순히 선 하나를 긋는 것처럼 보이지만, 그 속에는 깊은 통찰이 담겨 있습니다.
정보 보존과 학습 안정화, 두 마리 토끼를 잡는 우아한 해결책입니다. 박시니어 씨가 마무리했습니다.
"좋은 아이디어는 간단해 보이죠. 하지만 그 간단함 속에 진짜 실력이 숨어 있습니다."
실전 팁
💡 - Skip Connection 사용 시 Encoder와 Decoder의 특징 맵 크기를 정확히 맞추세요
- Concatenation과 Addition 중 프로젝트에 맞는 방식을 선택하세요
- GPU 메모리가 부족하면 일부 레벨의 Skip Connection만 사용하는 것도 방법입니다
6. 의료 영상 세그멘테이션 예시
"이론은 이해했는데, 실제로 어떻게 쓰이나요?" 김개발 씨의 질문에 박시니어 씨가 병원 프로젝트 폴더를 열었습니다. "실전 예제로 배워봅시다.
폐 CT 이미지에서 종양을 찾는 프로젝트입니다."
의료 영상 세그멘테이션은 질병 진단과 치료 계획에 필수적인 기술입니다. CT, MRI, X-Ray 이미지에서 장기, 종양, 병변을 자동으로 분할합니다.
전문의가 수 시간 걸리던 작업을 몇 초로 단축하며, 재현성과 일관성을 보장합니다. UNet이 가장 널리 사용되며, 적은 데이터로도 높은 정확도를 달성합니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import nibabel as nib # 의료 영상 포맷 (NIfTI) 로드
import numpy as np
class MedicalImageDataset(Dataset):
def __init__(self, image_paths, mask_paths):
self.images = image_paths
self.masks = mask_paths
def __getitem__(self, idx):
# CT 이미지 로드 (.nii.gz 파일)
img = nib.load(self.images[idx]).get_fdata()
mask = nib.load(self.masks[idx]).get_fdata()
# 정규화: Hounsfield Unit을 0-1로
img = np.clip(img, -1000, 1000) # HU 범위 제한
img = (img + 1000) / 2000 # 0-1 정규화
return torch.FloatTensor(img), torch.LongTensor(mask)
# Dice Loss: 의료 영상에서 많이 사용
class DiceLoss(nn.Module):
def forward(self, pred, target):
smooth = 1.0
intersection = (pred * target).sum()
return 1 - (2. * intersection + smooth) / (pred.sum() + target.sum() + smooth)
김개발 씨는 대학 병원의 영상의학과와 협업하게 되었습니다. 폐암 조기 진단 프로젝트입니다.
CT 이미지에서 의심스러운 결절(nodule)을 찾아내는 것이 목표입니다. 의사 선생님들이 하루에 수백 장의 CT를 검토하는데, 놓치는 경우도 있다고 합니다.
"AI가 도와줄 수 있다면 환자들에게 큰 도움이 될 거예요." 영상의학과 이 교수님의 말에 김개발 씨는 책임감을 느꼈습니다. 의료 영상의 특수성을 먼저 이해해야 합니다.
일반 이미지와 다른 점이 많습니다. 첫째, 3차원입니다.
CT나 MRI는 수백 장의 슬라이스로 구성된 3D 볼륨입니다. 512×512 이미지가 200장씩 쌓여 있는 형태입니다.
둘째, 픽셀 값이 특별한 의미를 가집니다. CT는 Hounsfield Unit이라는 단위를 쓰는데, -1000이 공기, 0이 물, +1000 이상이 뼈입니다.
셋째, 클래스 불균형이 심합니다. 전체 이미지에서 종양이 차지하는 비율은 1%도 안 됩니다.
99%가 정상 조직입니다. 일반적인 Cross Entropy Loss를 쓰면 "전부 정상"이라고 예측해도 99% 정확도가 나옵니다.
이건 의미가 없습니다. 데이터 준비 과정이 까다롭습니다.
의료 영상은 DICOM이나 NIfTI 같은 특수 포맷을 사용합니다. 일반 이미지 라이브러리로는 읽을 수 없고, nibabel이나 pydicom 같은 전문 라이브러리가 필요합니다.
또한 환자 정보를 익명화해야 합니다. 개인정보 보호법 때문입니다.
라벨링은 더 어렵습니다. 전문의가 직접 CT의 각 슬라이스에서 종양 경계를 그려야 합니다.
한 케이스당 30분에서 1시간이 걸립니다. 김개발 씨의 프로젝트에는 100개 케이스의 라벨이 있었습니다.
매우 귀중한 데이터입니다. 전처리가 성공의 절반입니다.
CT 이미지의 Hounsfield Unit 범위는 -1024부터 +3071까지입니다. 이것을 그대로 쓰면 네트워크 학습이 불안정합니다.
폐 CT라면 관심 영역을 -1000(공기)에서 +400(연조직) 정도로 제한합니다. 뼈는 중요하지 않으니까요.
이것을 Windowing이라고 부릅니다. 정규화도 중요합니다.
0-1 범위로 스케일링하거나, 평균 0 분산 1로 표준화합니다. 배치별로 정규화하는 Batch Normalization도 필수입니다.
의료 영상은 스캐너마다 밝기가 다르기 때문입니다. 손실 함수 선택이 핵심입니다.
Cross Entropy Loss는 클래스 불균형 때문에 잘 안 됩니다. 의료 영상에서는 Dice Loss를 많이 씁니다.
Dice Coefficient는 두 집합의 겹치는 정도를 측정하는 지표입니다. 0이 완전 불일치, 1이 완전 일치입니다.
쉽게 비유하자면, Dice는 "예측한 종양 영역과 실제 종양 영역이 얼마나 겹치나"를 봅니다. 크기가 작아도 정확히 맞추면 점수가 높습니다.
많은 연구에서 Dice Loss가 Cross Entropy보다 5-10% 높은 성능을 보였습니다. 실무에서는 Dice Loss와 Cross Entropy Loss를 섞어 쓰기도 합니다.
"0.5 × Dice + 0.5 × CE" 이런 식입니다. 각자의 장점을 결합하는 것입니다.
네트워크 아키텍처는 UNet 기반입니다. 김개발 씨는 3D UNet을 선택했습니다.
2D UNet을 3차원으로 확장한 버전입니다. Conv2d 대신 Conv3d를 쓰고, MaxPool2d 대신 MaxPool3d를 씁니다.
슬라이스 간의 연속성을 활용할 수 있어서 더 정확합니다. 하지만 메모리 사용량이 엄청납니다.
3D Convolution은 2D의 수백 배 계산량입니다. GPU 메모리가 부족해서 배치 크기를 1로 줄여야 했습니다.
큰 볼륨은 패치(작은 조각)로 나눠서 처리했습니다. 데이터 증강도 의료 영상 특화 기법을 썼습니다.
일반 이미지처럼 Color Jittering은 쓸 수 없습니다. CT는 회색조이고, 픽셀 값이 물리적 의미를 가지니까요.
대신 Elastic Deformation(탄성 변형)을 사용했습니다. 장기는 자연스럽게 구부러지기 때문에 현실적입니다.
회전과 반전도 조심스럽게 적용했습니다. 폐는 좌우가 비대칭이라 좌우 반전이 항상 적절한 건 아닙니다.
의학적 지식을 고려한 증강이 필요합니다. 학습과 검증 과정입니다.
100개 케이스를 80:10:10으로 나눴습니다. 학습 80, 검증 10, 테스트 10입니다.
환자 단위로 나눠야 합니다. 같은 환자의 슬라이스가 학습과 테스트에 섞이면 안 됩니다.
그러면 성능이 부풀려집니다. 학습에는 3일이 걸렸습니다.
GPU 4대를 사용했습니다. Dice Score가 0.85를 넘었을 때 감격했습니다.
전문의와 비교해도 손색없는 수준이었습니다. 실전 배포와 검증입니다.
모델을 병원 시스템에 통합했습니다. 의사 선생님들이 CT를 판독할 때 AI 결과를 참고할 수 있게 했습니다.
"이 부분을 다시 확인해보세요"라고 알려주는 역할입니다. 중요한 점은 AI가 진단을 대체하는 것이 아니라 보조한다는 것입니다.
최종 판단은 항상 의사가 합니다. AI는 놓칠 수 있는 작은 병변을 찾아내거나, 측정을 자동화해서 시간을 절약하는 역할입니다.
6개월 후 평가에서 조기 발견율이 15% 증가했습니다. AI가 의사가 놓친 작은 결절을 찾아낸 경우가 많았습니다.
이 교수님이 감사 인사를 전했습니다. "환자들에게 정말 큰 도움이 됩니다." 김개발 씨는 보람을 느꼈습니다.
코드를 작성하는 것이 단순히 기술적 성취가 아니라, 실제로 생명을 구하는 일에 기여할 수 있다는 것을 깨달았습니다. 의료 AI의 미래는 밝습니다.
세그멘테이션뿐 아니라 질병 분류, 예후 예측, 치료 반응 평가 등 다양한 분야로 확장되고 있습니다. 하지만 항상 윤리와 안전을 최우선으로 해야 합니다.
잘못된 AI 결과가 환자에게 해를 끼칠 수 있기 때문입니다. 엄격한 검증, 투명한 설명, 지속적인 모니터링이 필수입니다.
김개발 씨는 이 책임감을 가슴에 새겼습니다. 기술은 도구일 뿐, 그것을 어떻게 사용하느냐가 진짜 중요한 것입니다.
실전 팁
💡 - 의료 영상은 Dice Loss를 기본으로 사용하고, 필요시 다른 손실함수와 조합하세요
- 클래스 불균형이 심하므로 Weighted Loss나 Focal Loss를 고려하세요
- 환자 단위로 train/val/test를 분리해야 정확한 평가가 가능합니다
- 의료 AI는 보조 도구이지 대체재가 아님을 항상 기억하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.