1. 소스코드 수정
PyTorch Vision 라이브러리의 segmentation 예제 코드를 우리 데이터셋에 맞게 5곳 수정하였다.
수정 파일: train.py, coco_utils.py, v2_extras.py
수정 1 — train.py: num_classes 변경
# 원본
"coco": (args.data_path, get_coco, 21),
# 수정
"coco": (args.data_path, get_coco, 9),
원본은 COCO 공식 데이터셋의 21개 클래스 기준이다. 우리 데이터셋은 배경 포함 9개 클래스이므로 9로 변경하였다. 이 값이
모델 출력층의 채널 수가 된다.
수정 2 — train.py: 백본 freeze 추가
for param in model.backbone.parameters():
param.requires_grad = False
ResNet50 백본은 ImageNet으로 사전학습된 특징 추출기이다. 백본 가중치는 고정하고 분류기(classifier)만 우리 데이터에
맞게 학습하는 전이학습(Transfer Learning) 전략을 적용하였다. 적은 데이터로도 빠르게 수렴하며 성능이 향상된다.
수정 3 — train.py: 전체 모델 저장 추가
torch.save(model_without_ddp, os.path.join(args.output_dir, "model.pth"))
기존 checkpoint.pth는 가중치만 저장한다. model.pth는 모델 구조와 가중치를 함께 저장하여 추론 시 torch.load 한 줄로
바로 사용 가능하다.
수정 4 — coco_utils.py: 경로 및 CAT_LIST 변경
# 원본
PATHS = {
"train": ("train2017", os.path.join("annotations", "instances_train2017.json")),
"val": ("val2017", os.path.join("annotations", "instances_val2017.json")),
}
CAT_LIST = [0, 5, 2, 16, 9, 44, 6, 3, 17, 62, 21, ...]
# 수정
PATHS = {
"train": ("train", os.path.join("train", "annotations.json")),
"val": ("val", os.path.join("val", "annotations.json")),
}
CAT_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8]
COCO 공식 경로 구조에서 우리 labelme 변환 데이터셋 경로로 변경하였다.
수정 5 — v2_extras.py: COCO_TO_VOC_LABEL_MAP 변경
# 수정
COCO_TO_VOC_LABEL_MAP = dict(zip([0, 1, 2, 3, 4, 5, 6, 7, 8], range(9)))
원본은 COCO 91개 ID를 VOC 21개 순서로 재매핑하는 복잡한 딕셔너리이다. 우리 데이터는 0~8로 순서대로 정의되어 있으므로
1:1 매핑으로 단순화하였다.
2. DeepLabV3 모델 구조
DeepLabV3는 Semantic Segmentation을 위한 딥러닝 모델로, 다음 세 부분으로 구성된다.
3. 훈련 실행 (my_train.py)
훈련 시각화를 위해 matplotlib과 TensorBoard를 추가한 my_train.py를 작성하였다.
python my_train.py -j 6 --data-path D:\Users\tjdwls\pytorch\dataset3 --lr 0.02 --dataset coco -b 4 --model
deeplabv3_resnet50 --aux-loss --device cuda --epochs 40 --weights-backbone ResNet50_Weights.IMAGENET1K_V1 --output-dir
D:\Users\tjdwls\pytorch\dataset3\output
매 에폭마다 training_graph.png를 갱신하여 학습 곡선을 실시간으로 확인할 수 있도록 하였다.
ResNet50 기준 40 에폭 훈련 결과는 아래와 같다.
4. 클래스 불균형 문제 및 Weighted Loss 적용
훈련 결과에서 chair, glassdoor, table의 IoU가 낮게 나타났다. annotation 수를 분석한 결과 해당 클래스의 샘플이
상대적으로 적었다.
가중치 계산식: weight = √(최대 수 / 해당 클래스 수) = √(219 / count)
단순 역비율 대신 제곱근을 적용하여 과도한 가중치로 인한 훈련 불안정을 방지하였다.
CLASS_WEIGHTS = torch.tensor([1.00, 1.33, 1.87, 1.61, 1.18, 3.09, 2.05, 1.00, 1.40])
losses[name] = nn.functional.cross_entropy(
x, target, ignore_index=255, weight=CLASS_WEIGHTS.to(x.device)
)
Weighted Loss 적용 후 훈련 결과: mean IoU 56.6%, Global Acc 84.5%
glassdoor는 val set 자체에 샘플이 부족하여 가중치 적용에도 개선이 어려웠다.
5. ResNet50 vs ResNet101 비교
ResNet101 백본으로 변경하여 동일한 조건으로 재훈련하였다.
python my_train.py ... --model deeplabv3_resnet101 --weights-backbone ResNet101_Weights.IMAGENET1K_V1 --output-dir
D:\Users\tjdwls\pytorch\dataset3\output_resnet101
6. 추론 테스트
6-1. PyTorch 추론 (my_test.py)
model.pth를 그대로 로드하여 val 이미지 30장을 추론하였다. GPU가 훈련에 사용 중이어서 CPU로 실행하였다.
6-2. ONNX 변환 및 Python OpenCV 추론 (my_test_opencv.py)
model.pth를 ONNX 형식으로 변환한 후 PyTorch 없이 OpenCV DNN 모듈로 추론하였다.
# pth2onnx.py
torch.onnx.export(model, dummy_input, "resnet50.onnx", opset_version=11,
input_names=["input"], output_names=["out", "aux"])
OpenCV의 blobFromImage는 채널별 std 나누기를 지원하지 않아 numpy로 직접 정규화하였다.
img = img.astype(np.float32) / 255.0
img = (img - MEAN) / STD
blob = img.transpose(2, 0, 1)[np.newaxis]
6-3. C++ OpenCV 추론 (my_test_opencv.cpp)
동일한 ONNX 파일을 C++에서 로드하여 추론하였다. MSYS2 MinGW + OpenCV 4.13.0 + CMake/Ninja로 빌드하였다.
7. 데이터 증식 (Augmentation) 분석
훈련 중 적용되는 데이터 증식 파이프라인을 시각화하였다.
- RandomResize: 260~1040px 사이로 무작위 크기 조정
- RandomHorizontalFlip: 50% 확률로 좌우 반전
- RandomCrop: 480×480으로 무작위 위치 잘라내기
이미지와 정답 마스크에 동일한 변형이 적용되어 레이블 정합성이 유지된다.
8. 동영상 테스트 (my_test_video.py)
교내 건물 실내를 촬영한 동영상(1080×1920, 30fps, 285프레임)으로 테스트하였다.
ResNet50 + GPU 조합에서 30fps 실시간 처리가 가능하였다. ResNet101은 GPU를 사용해도 전처리(리사이즈, 정규화) 병목으로
인해 30fps에 미치지 못하였다.
실시간 처리 성능을 높이려면 전처리를 GPU 연산으로 이전하거나, 입력 해상도를 낮추거나, TensorRT로 모델을 최적화하는
방법이 있다.