Pause & Play
[MS AI School 1차 프로젝트] 검증 정확도 52% → 98%: 화재 감지 AI 'E-gle Eye' 모델 신뢰도 구축기 (1/2) 본문
[MS AI School 1차 프로젝트] 검증 정확도 52% → 98%: 화재 감지 AI 'E-gle Eye' 모델 신뢰도 구축기 (1/2)
eun_ll 2026. 4. 18. 18:20CCTV 화재 감지의 진짜 적은 불이 아니라 '오탐지'다. 1편에서는 과적합(검증 정확도 52.6%)에서 출발해 데이터 정제 4연전과 ResNet50 전이학습으로 검증 정확도 98%대까지 끌어올린 모델 구축 과정을 다룬다.
시리즈 1편 — 모델 편 (현재 글): 문제 정의 · 데이터 파이프라인 · 과적합 트러블슈팅 · ResNet50 전이학습
[2편 — 실시간 시스템 편]: 오탐 13.3%p 감소 시계열 엔진 · Grad-CAM · WebSocket 실시간 서빙
프로젝트 개요
| 서비스 | E-gle Eye — CCTV 기반 실시간 화재 감지 시스템 (화재/연기 분류 + 시계열 검증 + 판단 근거 시각화) |
| 기간 | 2026.02.23 ~ 2026.03.10 (약 2주) |
| 팀 구성 | 6명 (데이터 처리 1 / AI 모델 개발 2 / 로직 처리 1 / UI 개발 2) — 본인: 기획 + Python 딥러닝 아키텍처 설계 및 시계열 검증 로직 |
| 스택 | Python(TensorFlow, PyTorch, OpenCV) · Azure Custom Vision · Azure ML Designer · Azure Blob Storage · FastAPI(WebSocket) · Flask · React · GitHub |
이 글(1편)의 핵심
- 오탐지가 만드는 '양치기 소년 효과'를 문제로 정의하고, 정밀도·재현율 중심의 목표 설정
- Custom Vision 베이스라인의 정체 구간(정밀도 70% 벽)을 계측으로 확인하고 커스텀 학습으로 전환
- 과적합(훈련 85.3% vs 검증 52.6%)의 원인 3가지 분해와 데이터 정제 트러블슈팅 4연전
- MobileNetV2 → ResNet50 아키텍처 전환 + 2단계 파인튜닝으로 검증 정확도 52.6% → 98.15%
1. 프로젝트 배경: 불을 찾는 AI가 아니라, 믿을 수 있는 AI
1-1. 문제 정의
화재 진압의 성패는 골든타임 5분 안에 결정된다(국가재난관리정보시스템 통계 기준). 그러나 현장에는 구조적 병목이 있다.
- 관리자 한 명이 수십 대의 CCTV를 실시간 감시하는 것은 물리적으로 불가능하다.
- 더 치명적인 것은 오탐지의 축적이다. 빛 반사, 수증기, 순간 섬광이 화재로 분류되는 일이 반복되면, 관리자는 경보를 무시하거나 시스템을 꺼버린다 — 이른바 양치기 소년 효과다.
즉 이 도메인의 핵심 지표는 불을 얼마나 잘 찾는지를 말하는 재현율만이 아니라, 울렸을 때 진짜인가를 판단하는 정밀도다. 우리는 시스템의 목표를 '탐지'가 아니라 '관리자가 즉각 신고할 수 있는 신뢰도로 정의했다.
| 구분 | 일반 | AI 화재 감지 시스템 E-gle Eye |
| 인프라 | 현장 설치형 고가 서버 필수 | Azure 기반 클라우드 배포·확장 |
| 탐지 로직 | 단일 프레임/픽셀 기반 판독 | 5초 시계열 가중치 분석 (2편) |
| 운영 효율 | 잦은 오보로 인한 관제 피로 | 지능형 필터링으로 오탐지율 최소화 |
| 데이터 보안 | 불분명한 보관·삭제 주기 | 자동 파기 로직으로 리스크 감소 |
1-2. 투트랙 모델링 전략: Python(로컬) × Azure ML(클라우드)
모델 개발을 Python 로컬 트랙과 Azure ML 트랙으로 병행했다. 단순 백업이 아니라 트레이드오프가 명확했기 때문이다.
| 구분 | Python (로컬) | Azure Machine Learning |
| 모델 정확도 | 근소하게 우세 (초기 학습 시) | 로컬과 동등하거나 근소하게 낮음 |
| 배포·확장성 | 별도 API 서버 구축·연동 필요 | 원클릭 API 배포 |
| 업데이트 | 수동 재학습·재배포 | 자동 파이프라인 재학습 |
| 모델 관리 | 실험 이력 수동 기록 | v1~v5 전 과정 히스토리 자동 저장 |
| 안정성 | 로컬 하드웨어 리스크 존재 | Azure 인프라 기반 24/7 보장 |
로컬 트랙은 세밀한 커스텀 로직(시계열 검증, Grad-CAM)의 실험장으로, Azure 트랙은 배포·확장성 검증용으로 역할을 분담했다.
2. 데이터 파이프라인: 공개 데이터 + 생성형 AI 증강
fire / smoke / negative(일상) 3클래스 체계로 데이터를 구축했다.
- 공개 데이터셋: AI Hub의 산불·화재 CCTV 데이터셋 2종 + Kaggle 화재 이미지
- 생성형 AI 증강: 공개 데이터에 부족한 도시 화재, 공장 화재, 특수 연기 시나리오는 이미지 생성 AI로 보충. fire/smoke 각 300장 수준으로 클래스 균형을 맞춤
- 하드 네거티브 수집: 노을, 조명, 전조등 등 화재와 혼동하기 쉬운 이미지를 negative로 집중 수집 (어떤 결과가 나왔는지는 2편에서 계속)

3. 베이스라인 검증: Custom Vision의 '70% 벽'
본격 개발 전, 노코드 도구인 Azure Custom Vision으로 베이스라인을 측정했다. 세 번의 반복(iteration)을 통해 이 도구로 갈 수 있는 상한선을 확인했다.
| 단계 | Precision | Recall | mAP | 비고 |
| 초기 학습 (임계치 50%) | 44.0% | 74.3% | 64.4% | 클래스 비율 균등에도 정밀도 절반 미만 |
| 임계치 80% 상향 | 63.1% | 83.7% | 83.4% | 임계치를 올려야만 60%대 진입 |
| 생성형 AI 증강 후 재학습 | 70.5% | 87.7% | 85.6% | fire/smoke 각 300장 균형 — 여기서 정체 |
임계치를 80%까지 올리고 데이터를 증강해도 정밀도는 70% 부근에서 더 이상 오르지 않았다. 우리의 목표는 임계치 50%에서 정밀도 70~80% 이상 — 오탐지를 잡겠다는 서비스에서 이 수준의 오보율은 수용 불가능했다. 태깅 기반 자동 학습의 상한을 수치로 확인하고, 커스텀 전이학습으로 전환을 결정했다. 베이스라인을 버리는 결정도, 계측이 있어야 근거가 된다.
4. [Trouble Shooting 1] 과적합 — 훈련 85% vs 검증 52%의 간극
첫 번째 장애물: 학습이 도중에 죽는 이유
첫 커스텀 학습부터 순탄치 않았다. 에폭 중간에 학습이 강제 중단되는 현상이 반복됐는데, 원인은 steps_per_epoch를 수동 계산한 값이 실제 배치 수와 미세하게 어긋난 것이었다.
# Before — 수동 계산: 실제 데이터 수와 어긋나 학습 중단 유발
history = model.fit(
train_generator,
steps_per_epoch=train_generator.samples // train_generator.batch_size,
validation_data=val_generator,
validation_steps=val_generator.samples // val_generator.batch_size,
epochs=20
)
# After — 스텝 계산을 generator에 위임
history = model.fit(
train_generator,
steps_per_epoch=None,
validation_data=val_generator,
validation_steps=None,
epochs=20
)

두 번째 장애물: 숫자가 말해주는 과적합
학습 루프를 안정화하고 20에폭을 완주하자, 이번엔 결과가 문제였다. 훈련 정확도 85.3% / 검증 정확도 52.6% (val_loss 1.09) — 전형적인 과적합이다. 학습 곡선을 보면 훈련 정확도는 꾸준히 오르는데 검증 정확도는 0.5 부근에서 요동치고, 검증 Loss는 오히려 발산한다. 이 모델은 학습 데이터를 암기했을 뿐, 새로운 화면을 판별하지 못한다.
원인 분해
- 데이터 부족 — 클래스당 수백 장 수준으로 심층 CNN을 밑바닥부터 학습시키기엔 절대량 부족
- 클래스 불균형 — 특정 클래스로 판정이 쏠리는 통계적 편향 유발
- 배경 간섭 — 모델이 불꽃이 아닌 배경 특징을 학습
Solution
- fire / smoke / negative를 유사 비율로 재밸런싱
- image_dataset_from_directory 기반 TensorFlow 데이터셋 파이프라인으로 로딩 표준화
- 밑바닥 학습 대신 ImageNet 사전학습 가중치 기반 전이학습(MobileNetV2) 구조로 전환
— 적은 데이터로도 일반화 성능을 확보하는 정석 경로
5. [Trouble Shooting 2] 데이터 정제 4연전 — 학습을 멈추는 것은 모델이 아니라 데이터였다
전이학습 전환 후에도 학습 중단이 반복됐다. 원인은 전부 데이터 품질이었고, 네 차례에 걸쳐 파이프라인을 방어했다. 실제로 이후 ResNet 학습 로그에서도 Premature end of JPEG data 에러가 학습 도중 출력되는 것을 확인할 수 있다
— 손상 파일은 언제든 파이프라인을 습격한다.
| 회차 | 증상 | 원인 | 해결 |
| 1차 | InvalidArgumentError (Input is empty) | 0KB·비정상 파일 혼입 | os.path.getsize로 100바이트 미만 파일 자동 삭제 |
| 2차 | Operation timed out (Errno 60) | iCloud 동기화 폴더의 파일 접근 지연 | cache() + prefetch()로 메모리 선적재 |
| 3차 | Invalid JPEG data | 헤더는 정상이나 끝이 잘린 손상 JPEG | Pillow img.verify() 전수 조사 + tf.data.experimental.ignore_errors() |
| 4차 | cv2.imread가 None 반환 | 한글 파일명 경로 인식 실패 | 바이너리로 읽어 메모리에서 디코딩하는 커스텀 로더 |
4차의 한글 경로 문제는 국내 데이터셋을 다룰 때 반드시 만나는 함정이라 해결 코드를 남긴다.
읽기만 우회하는 게 아니라 정규 JPEG로 재인코딩해 덮어써서 문제를 근본 제거하는 방식이다.
import os, cv2
import numpy as np
for root, dirs, files in os.walk(data_dir):
for file in files:
if file.lower().endswith(('.jpg', '.jpeg', '.png')):
file_path = os.path.join(root, file)
# 1. 한글 경로 대응: 바이너리로 읽어 메모리에서 디코딩
with open(file_path, "rb") as f:
chunk = np.frombuffer(f.read(), dtype=np.uint8)
img = cv2.imdecode(chunk, cv2.IMREAD_COLOR)
if img is not None:
# 2. 깨끗한 정규 JPEG로 재인코딩 후 덮어쓰기
ok, buffer = cv2.imencode(".jpg", img)
if ok:
with open(file_path, "wb") as f:
f.write(buffer)
여기에 AUTOTUNE 프리페치를 더해 CPU 전처리와 GPU 연산이 겹치도록 최적화 — 일부 파일이 깨져 있어도 멈추지 않고(안정성), 유휴 없이 도는(효율성) 학습 파이프라인을 완성했다.
이 시점의 MobileNetV2 재학습 결과: 훈련 93.9% / 검증 94.6% (val_loss 0.1667). 검증 곡선이 훈련 곡선과 나란히 수렴하며 과적합 구간을 완전히 탈출했다.
6. 아키텍처 선정: MobileNetV2 vs ResNet50
동일 데이터로 두 백본을 비교 실험했다. 모델 설계에서 눈여겨볼 부분은 전처리를 모델 그래프 내부에 Lambda 층으로 내장했다는 점이다 — 학습과 서빙이 같은 전처리 코드를 타게 하려는 의도였다(이 설계가 2편의 전처리 중첩 버그와 어떻게 얽히는지는 뒤에서 밝혀진다).
# ResNet50 모델 설계
base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
base_model.trainable = False
model = models.Sequential([
layers.Input(shape=(224, 224, 3)),
layers.Lambda(preprocess_input, name='resnet_preprocess'), # 전처리를 모델 내부에 내장
base_model,
layers.GlobalAveragePooling2D(),
layers.Dense(128, activation='relu'), # ResNet 깊이에 맞춰 노드 확장
layers.Dropout(0.5),
layers.Dense(3, activation='softmax') # fire / smoke / negative
])
| 지표 | MobileNetV2 | ResNet50 |
| 훈련 정확도 | 93.92% | 94.38% |
| 검증 정확도 | 94.64% | 95.24% |
| 검증 Loss | 0.1667 | 0.1178 |
| 특성 | 경량·저지연 (엣지 디바이스 지향) | 깊은 레이어로 정밀 특징 추출 (서버 지향) |
화재 감지는 프레임당 수 ms를 아끼는 것보다 오보 한 건을 줄이는 것이 가치가 큰 도메인이고, 추론 환경도 서버급이었다. 정확도·Loss 모두 우위인 ResNet50을 채택했다. 오탐지 단골손님인 연기/안개 구분 테스트에서도 실제 화재 현장의 연기는 Smoke 98.24%, 산안개는 Negative 90.25%로 안정적인 판별을 확인했다.
7. 2단계 전이학습 파인튜닝 — 얼리고, 다시 녹인다
ResNet50 기반으로 Freeze → Fine-tune 2단계 학습 전략을 적용했다.
# 1단계: 백본 동결, 상부 레이어만 학습
model.fit(train_ds, validation_data=val_ds,
epochs=10, class_weight=class_weights)
# 2단계: 백본 상위 블록만 해동 후 초저속 파인튜닝
base_model.trainable = True
for layer in base_model.layers[:-30]:
layer.trainable = False # 하위 레이어는 유지
model.compile(optimizer=tf.keras.optimizers.Adam(1e-5), # LR 대폭 축소
loss='categorical_crossentropy',
metrics=['accuracy', 'precision', 'recall'])
| 단계 | Epoch | 정확도 | Loss | 비고 |
| 1단계 (Freeze) | 8/10 | 96.9% | 0.0773 | 기본 분류 능력 확보 |
| 2단계 (Fine-tune) | 19/20 | 97.5% | 0.1013 | 화재 특화 정밀도 향상 |
사전학습 특징을 보존한 채(1단계) 화재 도메인에 최상위 블록만 미세 적응(2단계)시키는 구조로, ImageNet의 일반 시각 지식과 화재 특화 판별력을 모두 확보했다. 여기서 입력 전처리 방식(Squash → Padding)까지 교정하면 검증 정확도는 98.15%, Loss는 0.0586까지 개선되는데, 이 전처리 이야기는 실전 오탐 문제와 얽혀 있어 2편에서 상세히 다룬다.
8. Azure ML 트랙: "데이터를 늘렸더니 성능이 떨어졌다"
병행 중이던 Azure ML Designer 트랙도 순탄치 않았다. 첫 파이프라인은 Train PyTorch Model 단계에서 실패했는데, 원인은 로컬 트랙과 동일한 이미지 해상도·전처리 부재였다. 디자이너에 Init Image Transformation → Apply Image Transformation 모듈을 삽입해 파이프라인 안에서 전처리를 수행하도록 수정하자 학습이 완주됐고, v1 Baseline 정확도 0.8939를 확보했다.
성능 역전 (v1 → v2)
이후 데이터를 대량 추가 투입했다. 결과는 교과서적 통념을 깨는 정확도 0.8939 → 0.8738, 오히려 2%p 하락.
원인 분석:
- 데이터 노이즈 간섭 — 양이 늘며 판별을 방해하는 저품질 데이터의 비중도 함께 증가
- 모델 수용량 한계 — 기존 ResNet18 구조로는 늘어난 데이터의 복잡도를 담지 못함
대응 (v3)
- ResNet18 → ResNet50 업그레이드 (수용량 확대)
- Learning Rate 0.0005로 미세 조정 (안정적 수렴 유도)
결과: 정확도 0.9078로 반등 (Macro Recall 0.9102).
데이터는 많을수록 좋다가 아니라, 데이터 양·품질·모델 수용량은 함께 설계해야 하는 삼각 변수임을 확인한 사례였다.
Azure 트랙의 v4~v5 최종 고도화(이진 분류 전환, Hard Negative 보강 → 정확도 0.993)는 2편에서 이어진다.
다음 편 예고
여기까지 오프라인 지표는 훌륭했다 — 검증 정확도 95%+, Loss 0.12. 그런데 웹캠과 실제 영상에 물리자 오답이 속출했다. 심지어 불이 활활 타는 장면을 Negative로 판정하는 치명적 미탐까지. 지표와 실전의 간극은 어디서 왔을까?
2편(실시간 시스템 편)에서 다루는 내용:
- 전처리 중첩·불일치: 성능 지표는 좋은데 실전에서 틀리는 이유 (Train-Serving Skew) — 모델 안에 내장한 그 Lambda 전처리 층이 복선이었다
- Squash → Padding: CNN이 인식하는 형태 특징을 지키는 리사이징
- 시계열 신뢰도 엔진: deque + EWMA + 투표 로직으로 오탐 판정 13.3%p 감소
- Grad-CAM: "AI가 어디를 보고 판단했는가"를 관리자에게 보여주기
- WebSocket 실시간 서빙: 유튜브 라이브를 프레임 단위로 분석하는 파이프라인
[2편 — 실시간 시스템 편 바로가기]
'MS ai School' 카테고리의 다른 글
| [MS AI School] 6개월 수료 — 전공자의 솔직한 후기 (0) | 2026.07.09 |
|---|---|
| [MS AI School 최종 프로젝트] AI 응답 14초 → 1.5초, 성능 최적화 전면전 — SimSpeak 개발기 (2/2) (0) | 2026.07.07 |
| [MS AI School 최종 프로젝트] 하루 만에 DB 비용 10만 원이 청구됐다 — AI 회화 서비스 SimSpeak 개발기 (1/2) (0) | 2026.07.07 |
| [MS AI School 1차 프로젝트] 오탐 판정 13.3%p 감소: '양치기 소년'을 잡는 시계열 엔진 — E-gle Eye 실시간 시스템 구축기 (2/2) (0) | 2026.04.18 |
| [MS ai school] 최종합격 후기 (0) | 2026.04.07 |










