Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Pause & Play

[MS AI School 1차 프로젝트] 검증 정확도 52% → 98%: 화재 감지 AI 'E-gle Eye' 모델 신뢰도 구축기 (1/2) 본문

MS ai School

[MS AI School 1차 프로젝트] 검증 정확도 52% → 98%: 화재 감지 AI 'E-gle Eye' 모델 신뢰도 구축기 (1/2)

eun_ll 2026. 4. 18. 18:20

CCTV 화재 감지의 진짜 적은 불이 아니라 '오탐지'다. 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는 오히려 발산한다. 이 모델은 학습 데이터를 암기했을 뿐, 새로운 화면을 판별하지 못한다.

원인 분해

  1. 데이터 부족 — 클래스당 수백 장 수준으로 심층 CNN을 밑바닥부터 학습시키기엔 절대량 부족
  2. 클래스 불균형 — 특정 클래스로 판정이 쏠리는 통계적 편향 유발
  3. 배경 간섭 — 모델이 불꽃이 아닌 배경 특징을 학습

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 하락.

원인 분석:

  1. 데이터 노이즈 간섭 — 양이 늘며 판별을 방해하는 저품질 데이터의 비중도 함께 증가
  2. 모델 수용량 한계 — 기존 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편 — 실시간 시스템 편 바로가기]