
이번 포스트에서는 HED (Holistically-Nested Edge Detection) 모델 구조에 대해서 알아보겠습니다.
HED는 VGG-16을 백본으로 사용하는 fully convolutional network입니다. 핵심 아이디어는 deep supervision과 multi-scale feature fusion입니다.
Input Image (H×W×3)
↓
[VGG-16 Encoder with Side Outputs]
↓
Conv1 → Side Output 1 ─┐
Conv2 → Side Output 2 ─┤
Conv3 → Side Output 3 ─┼→ Fusion Layer → Final Edge Map
Conv4 → Side Output 4 ─┤
Conv5 → Side Output 5 ─┘

Input: 224×224×3 (또는 임의 크기)
[Stage 1] - Conv1
- conv1_1: 3→64, 3×3
- conv1_2: 64→64, 3×3
- pool1: 2×2 maxpool
→ Output: 112×112×64
[Stage 2] - Conv2
- conv2_1: 64→128, 3×3
- conv2_2: 128→128, 3×3
- pool2: 2×2 maxpool
→ Output: 56×56×128
[Stage 3] - Conv3
- conv3_1: 128→256, 3×3
- conv3_2: 256→256, 3×3
- conv3_3: 256→256, 3×3
- pool3: 2×2 maxpool
→ Output: 28×28×256
[Stage 4] - Conv4
- conv4_1: 256→512, 3×3
- conv4_2: 512→512, 3×3
- conv4_3: 512→512, 3×3
- pool4: 2×2 maxpool
→ Output: 14×14×512
[Stage 5] - Conv5
- conv5_1: 512→512, 3×3
- conv5_2: 512→512, 3×3
- conv5_3: 512→512, 3×3
(pool5 없음 - HED에서는 제거)
→ Output: 14×14×512
각 stage의 출력마다 side output을 생성합니다:
#각 side output 구조
Side Output = 1×1 Conv → Deconv/Upsample → Sigmoid
#구체적으로:
side1: conv1_2 (112×112×64) → 1×1 conv (64→1) → 1×1×1 → sigmoid
side2: conv2_2 (56×56×128) → 1×1 conv (128→1) → upsample×2 → sigmoid
side3: conv3_3 (28×28×256) → 1×1 conv (256→1) → upsample×4 → sigmoid
side4: conv4_3 (14×14×512) → 1×1 conv (512→1) → upsample×8 → sigmoid
side5: conv5_3 (14×14×512) → 1×1 conv (512→1) → upsample×8 → sigmoid
모든 side output은 원본 이미지 크기로 upsampling됩니다.
5개의 side output을 결합:
#Concatenate along channel dimension
concat = [side1, side2, side3, side4, side5] # Shape: H×W×5
#1×1 convolution for fusion
fuse = Conv1×1(concat, filters=1) # 5→1
fuse = Sigmoid(fuse) # Final edge map
##시각적 표현
224×224×3 Input
↓
┌────────────────────────────────────────┐
│ VGG-16 Encoder │
├────────────────────────────────────────┤
│ Conv1 (64 ch) → [Side1: 1×1 conv] ───┼→ 224×224×1
│ ↓ pool │
│ Conv2 (128 ch) → [Side2: 1×1→up×2] ───┼→ 224×224×1
│ ↓ pool │
│ Conv3 (256 ch) → [Side3: 1×1→up×4] ───┼→ 224×224×1
│ ↓ pool │
│ Conv4 (512 ch) → [Side4: 1×1→up×8] ───┼→ 224×224×1
│ ↓ pool │
│ Conv5 (512 ch) → [Side5: 1×1→up×8] ───┼→ 224×224×1
└────────────────────────────────────────┘
↓ ↓ ↓ ↓ ↓
[Concatenate 5 channels]
↓
[1×1 Conv + Sigmoid]
↓
Final Edge Map (224×224×1)
class HED(nn.Module):
def __init__(self):
super(HED, self).__init__()
# VGG-16 backbone (pretrained 가능)
self.conv1 = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.Conv2d(64, 64, 3, padding=1),
nn.ReLU()
)
self.conv2 = nn.Sequential(
nn.MaxPool2d(2, stride=2),
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.Conv2d(128, 128, 3, padding=1),
nn.ReLU()
)
# ... conv3, conv4, conv5 similar
# Side outputs
self.side1 = nn.Conv2d(64, 1, 1)
self.side2 = nn.Conv2d(128, 1, 1)
self.side3 = nn.Conv2d(256, 1, 1)
self.side4 = nn.Conv2d(512, 1, 1)
self.side5 = nn.Conv2d(512, 1, 1)
# Fusion
self.fuse = nn.Conv2d(5, 1, 1)
def forward(self, x):
# Encoder
h1 = self.conv1(x) # 224×224×64
h2 = self.conv2(h1) # 112×112×128
h3 = self.conv3(h2) # 56×56×256
h4 = self.conv4(h3) # 28×28×512
h5 = self.conv5(h4) # 14×14×512
# Side outputs with upsampling
s1 = torch.sigmoid(self.side1(h1))
s2 = torch.sigmoid(F.interpolate(self.side2(h2),
size=x.shape[2:],
mode='bilinear'))
s3 = torch.sigmoid(F.interpolate(self.side3(h3),
size=x.shape[2:],
mode='bilinear'))
s4 = torch.sigmoid(F.interpolate(self.side4(h4),
size=x.shape[2:],
mode='bilinear'))
s5 = torch.sigmoid(F.interpolate(self.side5(h5),
size=x.shape[2:],
mode='bilinear'))
# Fusion
fuse = torch.sigmoid(self.fuse(torch.cat([s1,s2,s3,s4,s5], dim=1)))
return [s1, s2, s3, s4, s5, fuse]
HED는 class-balanced cross-entropy loss를 사용합니다. 이는 edge detection에서 발생하는 심각한 클래스 불균형 문제를 해결하기 위한 것입니다.
각 side output에 대한 손실함수는 다음과 같습니다:
L_side = -β ∑(y=1) log P(y=1|X) - (1-β) ∑(y=0) log P(y=0|X)
여기서:
β = |Y-| / |Y|
1-β = |Y+| / |Y|
-> 일반적인 이미지에서 edge 픽셀은 전체의 5% 미만입니다. 가중치 없이 학습하면 모델이 모든 픽셀을 non-edge로 예측해도 95% 정확도를 얻게 되어 제대로 학습되지 않습니다.
HED는 여러 side output을 가지므로:
L_total = ∑(m=1 to M) α_m × L_side^(m) + L_fuse
- M: side output 개수 (보통 5개)
- α_m: 각 side output의 가중치 (논문에서는 모두 1로 설정)
- L_fuse: 최종 fused output의 손실
각 side output과 최종 fusion output 모두 동일한 ground truth와 비교하여 학습됩니다.
def hed_loss(predictions, targets):
"""
predictions: [batch, 1, H, W] - sigmoid 출력
targets: [batch, 1, H, W] - binary ground truth
"""
# Class balancing weight 계산
pos_pixels = torch.sum(targets == 1).float()
neg_pixels = torch.sum(targets == 0).float()
total_pixels = pos_pixels + neg_pixels
beta = neg_pixels / total_pixels # non-edge 비율
# Class-balanced cross entropy
loss = -beta * targets * torch.log(predictions + 1e-8) \
- (1 - beta) * (1 - targets) * torch.log(1 - predictions + 1e-8)
return torch.mean(loss)
# 전체 손실 (5개 side output + 1개 fuse)
total_loss = sum([hed_loss(side_out, gt) for side_out in side_outputs]) \
+ hed_loss(fuse_output, gt)