|
|
1. Faster R-CNN 구조 및 동작 원리
Faster R-CNN은 RPN(Region Proposal Network)과 Fast R-CNN을 결합한 2단계 객체검출 모델이다. 기존 방식은 후보 영역을 Selective Search 같은 별도 알고리즘으로 찾았지만, Faster R-CNN은 이 과정까지 신경망으로 처리하여 속도와 정확도를 모두 개선했다.
전체 동작 순서
Faster R-CNN은 크게 두 단계로 동작한다. 1단계에서는 Backbone과 FPN을 통해 이미지의 특징을 추출하고, RPN이 "어디쯤에 객체가 있을 것 같다"는 후보 영역을 제안한다. 2단계에서는 RoI Align으로 후보 영역을 동일한 크기로 변환한 뒤, Detection Head에서 실제 클래스와 정확한 바운딩박스를 최종 출력한다.
Backbone + FPN
Backbone으로 ResNet을 사용하여 입력 이미지(3채널)에서 고수준 특징맵(2048채널)을 추출한다. 그러나 깊은 레이어의 특징맵만 사용하면 해상도가 낮아 작은 객체를 검출하기 어렵다. 이를 해결하기 위해 FPN(Feature Pyramid Network)을 사용한다. FPN은 깊은 레이어의 의미적 정보를 얕은 레이어의 고해상도 특징맵과 역방향으로 합쳐, 다양한 크기의 객체를 동시에 검출할 수 있게 한다.
RPN과 앵커 박스
RPN은 특징맵 위에 앵커 박스를 배치하여 후보 영역을 제안한다. 앵커 박스는 3가지 크기(128×128, 256×256, 512×512)와 3가지 비율(1:1, 1:2, 2:1)을 조합한 총 9가지로 구성된다. 각 앵커마다 "객체가 있는지 없는지(분류)"와 "박스 위치 보정값(회귀)"을 출력한다. Ground Truth와의 IoU가 0.7 이상이면 Positive, 0.3 이하면 Negative로 분류하여 학습에 사용한다. 최종적으로 Objectness Score가 높은 상위
N개의 후보 영역만 다음 단계로 전달한다.
RoI Align
RPN이 제안한 후보 영역들은 크기가 제각각이므로, Detection Head에 넣기 전에 동일한 크기로 변환해야 한다. RoI Align은 이 변환을 수행하는 과정으로, 기존 RoI Pooling과 달리 소수점 위치를 버리지 않고 보간(interpolation)하여 정보 손실을 줄이고 더 정확한 위치 추출이 가능하다.
Detection Head
RoI Align을 통과한 고정 크기의 특징맵은 FC Layer를 거쳐 두 가지 결과를 출력한다. 첫 번째는 클래스 분류로, 해당 영역이 어떤 객체인지(예: big robot, small robot, background) 판별한다. 두 번째는 바운딩박스 회귀로, RPN이 제안한 위치를 더 정밀하게 보정하여 최종 검출 결과를 출력한다.
2. 훈련결과 실시간 시각화
시각화를 하려면 에폭마다 발생하는 수치를 수집해야 한다. my_train.py에서는 두 함수의 반환값을
활용했다. train_one_epoch()의 반환값에서 해당 에폭 전체 배치의 loss 평균값을 가져오고, evaluate()의
반환값에서 AP@0.50:0.95와 AP@0.50을 가져온다.
metric_logger = train_one_epoch(model, optimizer, data_loader, device, epoch, args.print_freq,
scaler)
train_loss = metric_logger.meters['loss'].global_avg
coco_evaluator = evaluate(model, data_loader_test, device=device)
ap = coco_evaluator.coco_eval['bbox'].stats[0] # AP@0.50:0.95
ap50 = coco_evaluator.coco_eval['bbox'].stats[1] # AP@0.50
coco_eval['bbox'].stats는 COCO 평가 기준의 전체 지표 배열이고 인덱스 0이 AP@0.50:0.95, 인덱스 1이
AP@0.50이다. 이 값들을 에폭마다 리스트에 누적해서 시각화에 사용한다.
train_losses = []
val_ap50_list = []
val_ap_list = []
matplotlib을 이용한 시각화
save_graphs() 함수를 별도로 정의하여 에폭 루프 끝에서 매번 호출하도록 설계했다. 왼쪽 그래프는 에폭별
훈련 loss, 오른쪽 그래프는 검증 AP@0.50(초록)과 AP@0.50:0.95(빨간)를 출력한다.
def save_graphs(train_losses, val_ap50, val_ap, output_dir):
epochs = list(range(len(train_losses)))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(epochs, train_losses, 'b-o', markersize=4, label='Train Loss')
axes[0].set_title('Training Loss')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].legend()
axes[0].grid(True)
axes[1].plot(epochs, val_ap50, 'g-o', markersize=4, label='AP@0.50')
axes[1].plot(epochs, val_ap, 'r-o', markersize=4, label='AP@0.50:0.95')
axes[1].set_title('Validation AP')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('AP')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.savefig(os.path.join(output_dir, 'training_graph.png'))
plt.close()
에폭 루프 안에서 수치를 누적한 뒤 매번 같은 파일명으로 덮어쓰도록 호출한다.
for epoch in range(args.start_epoch, args.epochs):
metric_logger = train_one_epoch(...)
train_loss = metric_logger.meters['loss'].global_avg
train_losses.append(train_loss)
coco_evaluator = evaluate(...)
val_ap_list.append(float(coco_evaluator.coco_eval['bbox'].stats[0]))
val_ap50_list.append(float(coco_evaluator.coco_eval['bbox'].stats[1]))
save_graphs(train_losses, val_ap50_list, val_ap_list, args.output_dir)
훈련 중 output_dir/training_graph.png 파일을 파일 탐색기에서 열어두면 에폭이 끝날 때마다 파일이
갱신되므로 이미지 뷰어를 새로고침해서 loss가 내려가는지, val AP가 수렴하는지 실시간으로 확인할 수
있다. GUI가 없는 환경을 위해 파일 저장 방식인 matplotlib.use('Agg')를 사용했다.
TensorBoard를 이용한 시각화
TensorBoard는 훈련 중 브라우저에서 실시간으로 그래프를 확인할 수 있는 도구다. 훈련 시작 전에
SummaryWriter를 초기화하여 로그 저장 경로를 지정한다.
from torch.utils.tensorboard import SummaryWriter
tb_log_dir = os.path.join(args.output_dir, 'tensorboard_logs')
writer = SummaryWriter(log_dir=tb_log_dir)
에폭 루프 안에서 writer.add_scalar()로 태그명, 값, 현재 에폭을 기록한다. 태그에 슬래시(/)를 사용하면
TensorBoard에서 그룹으로 묶어 표시된다.
writer.add_scalar('Loss/train', train_loss, epoch)
writer.add_scalar('AP/val_AP50_95', float(ap), epoch)
writer.add_scalar('AP/val_AP50', float(ap50), epoch)
writer.add_scalar('LR', optimizer.param_groups[0]['lr'], epoch)
학습률(LR)도 함께 기록해두면 --lr-steps 16 22 옵션에 의해 epoch 16, 22에서 학습률이 실제로 0.1배로
낮아지는 시점을 그래프로 직접 확인할 수 있다. 훈련이 끝난 뒤에는 반드시 writer.close()를 호출해야
파일이 정상적으로 닫힌다.
훈련 실행 후 별도 터미널에서 아래 명령어로 TensorBoard를 실행하고 브라우저에서
http://localhost:6006을 열면 에폭이 진행될수록 그래프가 자동으로 업데이트된다.
tensorboard --logdir D:\Users\tjdwls\pytorch\dataset2\output_resnet101\tensorboard_logs
matplotlib은 파일을 직접 열어 확인하는 방식인 반면, TensorBoard는 브라우저에서 여러 지표를 탭으로
분리해 동시에 비교할 수 있다는 차이가 있다.
실행 명령어
python my_train.py `
--data-path D:\Users\tjdwls\pytorch\dataset2 `
--dataset coco `
--model fasterrcnn_resnet101_fpn `
--device cuda `
--epochs 26 `
--lr-steps 16 22 `
--aspect-ratio-group-factor 3 `
--output-dir D:\Users\tjdwls\pytorch\dataset2\output_resnet101
3. my_test.py 파이토치 테스트 코드
os.listdir()로 지정한 폴더에서 jpg, jpeg, png 확장자를 가진 파일 목록을 한 번에 가져온 뒤 정렬하여 순서대로 처리한다.
img_files = [f for f in os.listdir(TEST_IMG_DIR) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
img_files.sort()
추론 시간은 time.time()을 이미지 로드 직전과 추론 직후에 각각 찍어서 차이를 구했다. 이미지 로드,
텐서 변환, 모델 추론까지 전 과정을 포함한 시간이다.
start_time = time.time()
img_pil = Image.open(img_path).convert("RGB")
img_tensor = transform(img_pil)
with torch.no_grad():
pred = model([img_tensor])
inference_time = time.time() - start_time
모델은 훈련 완료 후 저장된 model.pth를 torch.load()로 불러와 eval() 모드로 전환한 뒤 추론에
사용했다. eval 모드로 전환하지 않으면 Dropout이나 BatchNorm이 훈련 모드로 동작하여 추론 결과가
달라질 수 있다.
추론 결과에서 score가 0.5 이상인 것만 실제 검출로 판정하고, 해당 위치에 바운딩박스와 클래스명,
score를 표시한 결과 이미지를 폴더에 저장한다.
두 모델의 장당 추론 시간은 CPU 기준으로 ResNet50이 약 0.65초 , ResNet101이 약 0.84초로 측정됐다.
ResNet101이 약 29% 더 느린데 이는 레이어 깊이 차이에서 오는 연산량 증가 때문이다.
4. my_test.cpp OpenCV C++ 테스트 코드
C++에서는 PyTorch pth 파일을 직접 로드할 수 없어 onnx 파일로 변환 후 ONNX Runtime C++ API를
사용했다. 테스트 영상은 big robot과 small robot을 각각 10장씩 새로 촬영하여 사용했다.
C++17 std::filesystem::directory_iterator로 지정한 폴더의 이미지 파일 전체를 자동 탐색한다.
for (const auto& entry : fs::directory_iterator(TEST_IMG_DIR)) {
std::string ext = entry.path().extension().string();
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png")
imgFiles.push_back(entry.path().string());
}
std::sort(imgFiles.begin(), imgFiles.end());
Python에서는 모델 내부 GeneralizedRCNNTransform이 자동으로 처리하던 전처리를 C++에서는 직접 구현해야
한다. BGR→RGB 변환, 0~1 정규화, ImageNet mean/std 정규화, CHW 형식 변환을 순서대로 수행한다.
resized.convertTo(imgFloat, CV_32F, 1.0 / 255.0);
cv::cvtColor(imgFloat, imgFloat, cv::COLOR_BGR2RGB);
// ImageNet 정규화: (pixel - mean) / std
const float mean_val[3] = {0.485f, 0.456f, 0.406f};
const float std_val[3] = {0.229f, 0.224f, 0.225f};
추론 시간은 std::chrono::high_resolution_clock으로 전처리 시작부터 추론 완료까지 측정한다.
auto start = std::chrono::high_resolution_clock::now();
// 전처리 + session.Run()
auto end = std::chrono::high_resolution_clock::now();
double inferTime = std::chrono::duration<double>(end - start).count();
모델 입력을 800×800으로 리사이즈했으므로 출력된 바운딩박스 좌표를 원본 이미지 크기로 역변환해야
한다.
float scaleX = (float)img.cols / INPUT_SIZE;
float scaleY = (float)img.rows / INPUT_SIZE;
int x1 = (int)(boxes[i * 4 + 0] * scaleX);
int y1 = (int)(boxes[i * 4 + 1] * scaleY);
ONNX Runtime CPU 기준 장당 추론 시간은 약 0.35초로, Python PyTorch(0.65초)보다 약 46% 빠르다. 동일한
ONNX Runtime 엔진을 사용하므로 C++ 빌드도 유사한 수치를 보인다.
5. 훈련 성능 지표 설명
IoU (Intersection over Union)
IoU는 예측 바운딩박스와 정답 바운딩박스가 겹치는 비율로, 교집합 넓이를
합집합 넓이로 나눈 값이다. 0~1 사이의 값을 가지며 1에 가까울수록 예측 박스가 정답에 정확히
일치한다는 의미다. AP와 AR 모두 IoU 임계값을 기준으로 정답/오답을 판별하기 때문에 모든 성능 지표의
기반이 되는 개념이다.
Loss
Loss는 훈련 중 모델이 정답과 얼마나 차이나는지를 수치로 나타낸 것으로, 낮을수록 모델이 정답에 가깝게
예측하고 있음을 의미한다. Faster R-CNN은 RPN과 Detection Head 두 단계로 구성되어 있어 Loss도 각
단계별로 나뉜다.
loss_objectness는 RPN이 각 앵커(후보 영역) 위치에 객체가 있는지 없는지를 얼마나 틀리는지를 나타낸다.
이 값이 높으면 후보 영역 자체를 제대로 잡지 못하고 있다는 뜻이다.
loss_rpn_box_reg는 RPN이 제안하는 후보 영역의 위치 정확도를 나타낸다. 객체 유무는 맞게 판단하더라도 박스 위치가
부정확하면 이 값이 높게 나온다.
loss_classifier는 Detection Head가 최종적으로 클래스(big robot, small robot, background)를 얼마나
틀리는지를 나타낸다.
loss_box_reg는 Detection Head가 출력하는 최종 바운딩박스 위치의 오차다. Loss를 4가지로 분리해서 보는 이유는
문제가 RPN 단계에서 발생하는지, Detection Head 단계에서 발생하는지 구분하여 진단할 수 있기 때문이다.
AP (Average Precision)
AP는 Precision-Recall 곡선의 아래 면적으로, 특정 임계값 하나에 의존하지 않고 전체 범위에 걸친
정밀도를 종합적으로 평가한다. Precision은 모델이 검출한 것 중 실제 정답인 비율로, 오탐(없는 객체를
있다고 하는 것)을 측정한다.
AP@0.50은 IoU가 0.5 이상이면 정답으로 인정하는 관대한 기준이다. 박스가 대략적으로 맞기만 해도 정답
처리되므로, 모델이 객체를 검출하는 능력 자체를 확인할 때 유용하다. AP@0.50:0.95는 IoU 임계값을
0.5부터 0.95까지 0.05 간격으로 11번 측정하여 평균을 낸 값이다. 박스가 정밀하게 일치해야 높은 점수를
받을 수 있어 실질적인 성능을 나타내는 주요 지표로 사용된다.
AR (Average Recall)
AR은 실제 존재하는 객체 중 모델이 얼마나 빠짐없이 찾아냈는지를 나타낸다. Recall은 실제 정답 중
검출에 성공한 비율로, 미탐(있는 객체를 놓치는 것)을 측정한다. AP만으로는 모델이 찾은 것 중 얼마나
맞았는지는 알 수 있지만, 실제로 존재하는 객체를 얼마나 많이 찾았는지는 알 수 없기 때문에 AR을 함께
사용한다.
maxDets
maxDets=1은 이미지당 1개만 검출을 허용했을 때의 Recall로, 가장 확신 있는 검출 결과 하나가 얼마나
정확한지 확인한다. maxDets=10은 10개까지, maxDets=100은 100개까지 허용하며, 실질적으로 모델이 낼 수
있는 최대 Recall에 해당한다. 세 값을 비교하면 검출 개수를 늘릴수록 Recall이 얼마나 향상되는지 파악할
수 있다.
AP와 AR을 함께 보는 이유
AP만 높고 AR이 낮으면 맞히긴 잘 맞히지만 많은 객체를 놓치는 소극적 검출 모델이다. 반대로 AR만 높고
AP가 낮으면 많이 찾긴 하지만 오탐이 많은 과도한 검출 모델이다. 실용적으로 사용 가능한 모델이 되려면
두 지표가 모두 높아야 한다.
6. 결과 표 의미 설명
결과 표는 훈련이 완료된 모델을 검증 데이터셋에 적용하여 측정한 최종 성능 지표이다. 바운딩박스 기준(IoU metric: bbox)으로 평가되었으며 AP와 AR 두 가지 항목으로 구성된다.
AP 수치 해석
AP@0.50:0.95는 0.764로, IoU 임계값을 0.5부터 0.95까지 단계적으로 높여가며 측정한 평균값이다. 박스
위치가 정밀하게 일치해야 높은 점수를 받는 엄격한 기준에서 76.4%의 정밀도를 달성했다는 의미로, 높은
수치에 해당한다. AP@0.50과 AP@0.75는 모두 0.983으로, IoU 기준을 0.75까지 올려도 성능이 유지된다는
것은 모델이 객체 위치를 매우 정밀하게 예측하고 있음을 나타낸다.
area=small과 area=medium은 -1.000으로 표시되어 있는데, 이는 성능이 나쁜 것이 아니라 검증 데이터셋에
해당 크기의 객체가 존재하지 않아 측정 자체가 불가능하다는 의미다. area=large의 AP@0.50:0.95가
0.764로 전체 AP와 동일한 것은 우리 데이터셋의 객체가 모두 large로 분류되기 때문이다.
AR 수치 해석
maxDets=1일 때 AR은 0.683으로, 이미지당 가장 확신 있는 검출 결과 1개만 허용했을 때 실제 객체의
68.3%를 찾아낸다는 의미다. maxDets=10과 maxDets=100은 모두 0.800으로 동일한데, 허용 검출 수를
10개에서 100개로 늘려도 수치가 변하지 않는 것은 검증 이미지당 실제 객체 수가 10개를 넘지 않는다는
의미다. 최대 Recall이 0.800이므로 모델은 실제 객체의 약 20%를 검출하지 못하고 있다.
종합 해석
AP가 0.764로 높고 AR이 0.800인 결과를 종합하면, 모델이 검출한 객체는 대부분 정확하지만 실제 객체 중
일부를 놓치는 패턴을 보인다. area=small과 area=medium의 -1.000은 모델의 성능 문제가 아닌 데이터셋
구성의 특성이므로 성능 해석에서 제외하고 판단해야 한다.
7. 전이학습 가중치 설명
전이학습은 이미 다른 데이터셋으로 훈련된 모델의 가중치를 가져와 새로운 과제에 재사용하는 방법으로,
처음부터 훈련하는 것보다 적은 데이터와 시간으로 높은 성능을 낼 수 있다.
우리 모델에서는 Backbone인 ResNet101에 ImageNet으로 사전훈련된 가중치를 적용하고 freeze 처리했다.
freeze란 훈련 중 해당 부분의 가중치를 업데이트하지 않도록 고정하는 것이다.
ImageNet으로 학습된 엣지, 텍스처, 형태 등의 특징 추출 능력은 로봇 이미지에도 그대로 유효하기 때문에
굳이 다시 학습시킬 필요가 없다.
반면 Detection Head는 big robot, small robot이라는 우리 데이터 고유의 클래스를 처음부터 학습해야 하므로
랜덤 초기화 후 훈련했다.
전이학습의 효과는 훈련 초반 수치에서 확인할 수 있다. epoch 0에서 AP@0.50이 이미 0.38로 시작한 것은
Backbone이 처음부터 유의미한 특징을 추출했다는 의미로, 전이학습 없이 처음부터 훈련했다면 초반 성능이
훨씬 낮았을 것이다.
8. ResNet101 백본 변경 및 성능 비교
torchvision은 fasterrcnn_resnet50_fpn은 기본 제공하지만 fasterrcnn_resnet101_fpn은 제공하지 않는다.
그래서 resnet_fpn_backbone()으로 ResNet101 위에 FPN을 직접 붙인 backbone을 만들고, 이를
FasterRCNN()에 전달하여 RPN과 Detection Head가 자동으로 구성되도록 했다. ResNet50 기본 모델과 내부
구조는 동일하고 backbone 부분만 교체된 것이다.
훈련 명령어는 --model 인자만 다르고 나머지는 동일하다.
# ResNet50
python my_train.py --model fasterrcnn_resnet50_fpn ...
# ResNet101
python my_train.py --model fasterrcnn_resnet101_fpn ...
두 모델을 동일한 validation 데이터셋으로 평가한 최종 성능은 다음과 같다.
┌──────────────────────┬──────────┬───────────┐
│ 지표 │ ResNet50 │ ResNet101 │
├──────────────────────┼──────────┼───────────┤
│ AP@0.50:0.95 │ 0.622 │ 0.671 │
├──────────────────────┼──────────┼───────────┤
│ AP@0.50 │ 0.984 │ 0.980 │
├──────────────────────┼──────────┼───────────┤
│ AP@0.75 │ 0.768 │ 0.715 │
├──────────────────────┼──────────┼───────────┤
│ AR@100 │ 0.687 │ 0.747 │
├──────────────────────┼──────────┼───────────┤
│ 추론시간 (CPU, 장당) │ 2.89초 │ 3.79초 │
└──────────────────────┴──────────┴───────────┘
AP@0.50:0.95 기준으로 ResNet101이 0.671로 ResNet50의 0.622보다 높다.
이 지표는 IoU 임계값을 0.5부터 0.95까지 높여가며 평균을 낸 엄격한 기준으로, ResNet101이 바운딩박스 위치를
전반적으로 더 정밀하게 예측한다는 의미이다.
반면 AP@0.50과 AP@0.75는 ResNet50이 더 높은데, 이는 IoU 기준이 낮거나 중간 수준일 때는 ResNet50도
충분한 검출 능력을 보인다는 뜻이다. AR@100은 ResNet101이 0.747로 ResNet50의 0.687보다 높아 실제
객체를 더 많이 찾아낸다.
추론시간은 ResNet101이 장당 3.79초로 ResNet50의 2.89초보다 약 31% 느리다. ResNet101은 ResNet50보다
레이어가 더 깊어 파라미터 수가 많기 때문이다. 정밀도가 중요한 상황에서는 ResNet101이, 속도가 우선인
상황에서는 ResNet50이 유리하다.
9. 과적합 판단 방법
과적합이란 모델이 훈련 데이터에만 지나치게 맞춰져서 새로운 데이터에는 성능이 떨어지는 현상이다.
훈련 loss는 계속 낮아지는데 검증 성능은 오히려 떨어지거나 정체되는 시점이 과적합의 시작점이다.
training_graph.png를 보면 epoch별 훈련 loss와 검증 AP의 변화를 확인할 수 있다.
val AP가 최고점을 찍은 뒤 이후 epoch에서 하락하거나 정체되면 그 시점부터 과적합으로 판단한다.
우리 모델에서는 epoch 21에서 AP@0.50:0.95가 0.6890으로 최고점을 기록했고 이후 epoch 25에서 0.6710으로 하락했다.
같은 기간 훈련 loss는 계속 낮아지고 있어 훈련 데이터에는 점점 잘 맞춰지지만 새로운 데이터에 대한 성능은
떨어지는 전형적인 과적합 패턴이다.
단, 검증 AP의 하락이 반드시 과적합을 의미하지는 않는데, 검증 데이터셋의 표본 수가 적을 경우
epoch마다 AP 수치가 출렁거리는 현상이 나타날 수 있다.
표본이 적으면 특정 epoch에서 우연히 어려운 이미지가 평가에 포함되거나 쉬운 이미지만 포함되는 것만으로도
수치가 크게 달라지기 때문이다. 따라서 단일 epoch의 수치 변화만으로 과적합을 판단하기보다는 여러 epoch에 걸친
전반적인 추세를 보고 판단하는 것이 정확하다.
따라서 우리 모델의 경우 epoch 21 이후 val AP가 소폭 하락했으나 하락폭이 크지 않고 검증 데이터셋 표본 수가
적다는 점을 고려하면 심각한 과적합보다는 수렴에 가까운 상태로도 볼 수 있다.
과적합을 방지하려면 val AP가 더 이상 오르지 않는 시점에 훈련을 조기 종료하는 Early Stopping을 적용하거나,
데이터 증식으로 훈련 데이터를 다양하게 변형하여 모델이 특정 패턴에 과도하게 맞춰지는 것을 방지할 수 있다.
10. 데이터 증식 적용
파이토치 예제에서 사용한 증식 방법
파이토치 예제는 presets.py의 DetectionPresetTrain 클래스에서 증식 정책을 관리한다. 기본값은
--data-augmentation hflip으로, 아래 코드에서 확인할 수 있다.
# presets.py, DetectionPresetTrain 클래스 내부 (42~43번 라인)
if data_augmentation == "hflip":
transforms += [T.RandomHorizontalFlip(p=hflip_prob)] # p=0.5
RandomHorizontalFlip(p=0.5)는 이미지를 50% 확률로 좌우 반전시키는 단순한 증식이다. 객체 검출에서는
이미지를 반전할 때 바운딩박스 좌표도 함께 변환되므로 별도 처리가 필요 없다. 이 외에도 "multiscale",
"ssd", "lsj" 정책이 정의되어 있으나 기본 훈련에서는 hflip만 사용된다.
증식 전후 비교
영상 분류와 달리 객체 검출에서 증식은 이미지와 바운딩박스 좌표를 반드시 동시에 변환해야 한다. 예를
들어 회전 증식을 적용하면 이미지가 회전된 만큼 바운딩박스의 네 꼭짓점 좌표도 동일한 각도로 회전
변환되어야 한다. 좌표가 변환되지 않으면 박스가 객체와 어긋나 잘못된 학습이 이루어진다.
위 이미지는 동일한 사진에 회전 증식을 적용한 결과로, 왼쪽은 원본, 오른쪽은 회전 후 이미지이다.
바운딩박스 좌표값이 이미지 회전에 맞게 함께 이동한 것을 확인할 수 있다.
증식 추가 방법 및 소스코드 위치
원본 presets.py를 수정하지 않고 my_train.py에 직접 구현했다. 추가한 코드는 두 곳이다.
첫 번째는 RandomRotationWithBoxes 클래스로, my_train.py 상단에 정의했다. PIL 이미지를 회전시키면서
바운딩박스의 네 꼭짓점을 동일한 각도로 회전 변환하고, 회전 후 이미지 밖으로 완전히 벗어난 박스는 제거한다.
# my_train.py, RandomRotationWithBoxes 클래스
class RandomRotationWithBoxes:
def __call__(self, image, target):
angle = random.uniform(-self.degrees, self.degrees)
rotated = TF.rotate(image, angle, expand=False)
# 각 박스의 4개 꼭짓점을 회전 변환 후 axis-aligned 박스로 재계산
...
return rotated, target
두 번째는 get_transform() 함수 내부로, --data-augmentation rotation 인자를 받으면 위에서 정의한
클래스를 반환하도록 분기를 추가했다.
# my_train.py, get_transform() 함수
def get_transform(is_train, args):
if is_train:
if args.data_augmentation == "rotation":
return RotationPreset(degrees=30) # ← 추가한 부분
return presets.DetectionPresetTrain(...)
훈련 실행 시 --data-augmentation rotation만 추가하면 적용된다.
python my_train.py --data-augmentation rotation `
--model fasterrcnn_resnet50_fpn `
--output-dir D:\Users\tjdwls\pytorch\dataset2\output_rotation ...
훈련 결과 비교
┌───────────────┬──────────────┬─────────┐
│ 증식 │ AP@0.50:0.95 │ AP@0.50 │
├───────────────┼──────────────┼─────────┤
│ 기본 (hflip) │ 0.622 │ 0.984 │
├───────────────┼──────────────┼─────────┤
│ rotation 추가 │ 0.503 │ 0.961 │
└───────────────┴──────────────┴─────────┘
rotation 증식을 추가했을 때 AP@0.50:0.95가 0.622에서 0.503으로 오히려 낮아졌다. 이는 훈련 데이터가
160장으로 적은 상황에서 ±30도 회전이라는 강한 증식이 오히려 모델 학습을 방해했기 때문으로 보인다.
로봇 영상은 대부분 정면이나 측면의 일정한 방향으로 촬영되어 있어 강한 회전 변환이 실제 테스트 환경과
동떨어진 데이터를 만들어냈을 가능성이 있다. 증식이 항상 성능을 높이는 것이 아니라 데이터 특성에
맞는 방법을 선택해야 함을 보여주는 결과다.
0517
|
|
