실습 로그 / 산학 실시간 심전도 처리 서버

산학 7단계 · 부하테스트용 기준 AI 모델

공개 데이터(PTB-XL)로 기준 CNN을 직접 학습해 시험셋 민감도 0.929·특이도 0.883·AUC 0.969를 얻고, 모델 6종이 정확도는 비슷하지만 판정 속도는 16배 차이 나는 것을 확인했다.

진행 과정은 실습 내용을 AI와 함께 정리했고, 배운 점은 그걸 보고 제가 이해한 대로 직접 썼습니다. 배운 점 바로가기 ↓

AI모델트러블슈팅

목표

10.13에 1D-CNN을 LTSTDB 기록 몇 개로 학습해 워커에 연결까지 했지만, 임시 라벨로 만든 연결 테스트용이라 성능 수치는 의미가 없었다. 부하테스트는 진짜 모델 무게로 재야 의미가 있어서, 공개 데이터(PTB-XL)로 기준 모델을 직접 학습하고 여러 모델의 정확도와 판정 속도를 비교한다. (10.28)

  • 서버 담당 관점 : 정확도가 오른 만큼 판정 속도를 내줘도 되는지를 숫자로 본다 (100명 실시간 기준)
  • 흐름 : A 기존 AI 구조 이해 → B 학습 데이터 만들기 → C 시연·평가 데이터 분리 → D AI 흐름 공부 → E 기준 CNN 학습·채점과 모델 비교
  • 결과 : 시험셋 민감도 0.929, 특이도 0.883, AUC 0.969. 모델 6종은 정확도가 비슷하고 속도는 16배 차이 → 경량 CNN 기준선, CNN+LSTM 최종 후보

A. 기존 AI 구조 이해

저장소에 있는 AI 코드가 어떻게 돌아가는지, 뭐가 있고 뭐가 비어 있는지 파악한다.

AI가 파이프라인 어디에 있나

생성기 → 수신부 → Redis 큐 → 워커
                              │
                    ① 0.1초 묶음 50개를 모음 = 5초 창 [3, 1250]
                    ② R파 찾기 (Pan-Tompkins, 3리드 중 2개 이상 동의)
                    ③ 판정 predict()   ← 여기가 AI 모델
                              ↓
                    (정상 / 이상 의심 / 판정 불가, 확률)
  • 워커는 모델이 뭔지 모름. --model 옵션으로 ③에 끼울 걸 고름
--model 정체 상태
demo 랜덤포레스트, R파로 숫자 10개를 뽑아 판정 (10.8 내가 만든 임시 모델) 돌아감 (시연은 이걸로 함)
demo-nbeat 위와 같고 R파 4개 구간만 판정 돌아감
cnn 1D-CNN 정식 모델 자리 10.13 LTSTDB로 연결 테스트 학습만 함 (model.pt는 저장소에 없음)

1D-CNN 한 번 판정

5초 창 [3리드, 1250샘플]
  ① 리드 순서를 학습 때 순서로 맞춤 (이름이 다르면 에러)
  ② 정규화 : 리드마다 중앙값을 빼고 크기로 나눈 뒤 ±10으로 자름
  ③ 합성곱 블록 4개 : 특징 3→16→32→64→96개, 길이 1250→625→312→156→78
  ④ 평균 → 숫자 96개 → 점수 1개 → 0~1 확률
  ⑤ 확률 ≥ 임계값 → abnormal, 아니면 normal
  ⑥ 전극이 빠지면(lead off) 모델을 안 돌리고 unavailable
  • 모델이 작음 (파라미터 약 5만 개) → GPU로 충분, CPU로도 돌아감

학습 설정 (train.py)

항목 설정 의미
입력 npz : X [N,3,1250], y [N], patient_ids [N] 10.13 LTSTDB용 make_npz.py는 내가 만듦. 의사 판독 라벨이 있는 PTB-XL용은 없었음 → B
학습/검증 환자 단위 80:20 같은 환자가 양쪽에 섞이면 성능이 부풀려짐
불균형 pos_weight = 정상 수 / 이상 수 이상이 적어도 무시당하지 않게
최적화 Adam, lr 0.001, 20 epoch, 배치 64 기본값
저장 검증 손실 가장 낮은 시점 과적합 방지
임계값 특이도 0.80 이상에서 민감도 최대 미탐이 더 치명적이라 민감도 우선
장치 기본 cpu GPU는 --device cuda

피드백 반영 점검

피드백 반영
250Hz, 3리드 전부 사용 O
미탐 우선 (민감도) O
시간 기준 5초 고정 창 1순위 O
전극 빠짐이면 판정 안 함 O
박동 기준(N-Beat) 비교 X (CNN에는 없음)
부정맥 2종(심방세동, 조기심실수축) X (정상/이상 이진 분류)
노이즈 대응 X (노이즈 학습·평가 없음)
판정 속도 측정 △ (10.13 맥북에서 창 하나 약 0.6ms, 테스트 서버에선 아직)

정리

  • 틀(모델, 학습, 예측, 워커 연결)은 있고, 10.13에 LTSTDB로 한 번 학습해서 워커 연결까지 확인함 (내 작업: make_npz.py, cnn_model.py, eval_cnn.py)
  • 비어 있는 건 의사 판독 라벨이 있는 데이터로 학습한 모델. LTSTDB 학습은 기록 몇 개에 임시 라벨이라 성능 수치에 의미가 없었음
  • 테스트 서버에서 단위 테스트 8개 통과 확인 (틀은 멀쩡함)

B. 학습 데이터 만들기 (PTB-XL → NPZ)

공개 심전도 데이터 PTB-XL을 학습 코드가 먹는 모양(X [N, 3, 1250])으로 바꾼다. 파일 : ai/AI모델/make_ptbxl_npz.py

변환 흐름

PTB-XL 기록 1개 (12리드 · 500Hz · 10초 = [5000, 12])
  ① 리드 3개만 : V4, V3, II
  ② 500Hz → 250Hz            [2500, 3]
  ③ [시간, 리드] → [리드, 시간]  [3, 2500]
  ④ 10초 → 5초 창 2개          [3, 1250] × 2
  ▼
ptbxl_train.npz (fold 1~9) + ptbxl_test.npz (fold 10, 시험 전용)

정한 것과 이유

정한 것 이유
리드 V4·V3·II 생성기 기본 기록이 V4·V3·II. 모델은 학습 때와 리드 이름이 다르면 거부함
250Hz 우리 파이프라인·기기 속도
라벨 : 정상(NORM)=0, 심방세동(AFIB)·조기심실수축(PVC)=1, 그 외 제외 목표 부정맥 2종. 다른 질환을 0으로 넣으면 "정상이 아닌데 정상"이 생김
fold 10 시험 전용 PTB-XL 권장 방식. 학습·임계값에 안 써야 진짜 성능

핵심 코드

def label_of(scp_codes):
    codes = set(ast.literal_eval(scp_codes).keys())
    if codes & ABNORMAL:
        return 1
    if "NORM" in codes:
        return 0
    return None
  • 1줄 : scp_codes는 "{'NORM': 100.0, 'SR': 0.0}" 같은 문자열이라 ast.literal_eval로 진짜 dict로 바꾸고 키(진단 코드)만 꺼냄
  • 2줄 : 집합 교집합(&). AFIB나 PVC가 하나라도 있으면 이상
  • 4줄 : 이상 코드 없이 NORM만 있으면 정상
  • 6줄 : 둘 다 아니면 None → 학습에서 뺌
sig = rec.p_signal[:, idx].astype(np.float32)
sig = decimate(sig, SRC_FS // DST_FS, axis=0, zero_phase=True)
return np.ascontiguousarray(sig.T, dtype=np.float32)
  • 1줄 : 12리드 중 V4·V3·II 열만 꺼냄 → [5000, 3]
  • 2줄 : 500→250Hz. 한 칸씩 건너뛰면 높은 주파수가 낮은 주파수로 둔갑함(에일리어싱). decimate는 저역통과 필터를 먼저 걸고 줄임. zero_phase로 파형이 시간축에서 밀리지 않게
  • 3줄 : .T로 [시간, 리드] → [리드, 시간]. 모델 입력 모양이 [3, 시간]

한계

  • 라벨이 10초 단위라 5초 창 2개에 똑같이 붙음. 심방세동은 계속 이어져서 괜찮지만, 조기심실수축은 박동 하나라 한쪽 창에는 없을 수 있음 → 결과 볼 때 감안
  • 생성기 기록 중에는 리드가 다른 것(V6·V5·aVF 등)도 있어서 그 환자는 모델이 거부함. 실제 기기가 보내는 3리드 확인 필요

확인

  • 가짜 PTB-XL(12개 기록)로 돌려서 X (14, 3, 1250), 리드 순서 V4·V3·II 확인
  • 실제 데이터 : 전체 21,799기록 중 정상 9,290 · 이상 2,452 사용, 10,057 제외. 학습용 창 약 2만 개(5초 간격) / 63,390개(1초 간격), 시험용 2,354개. 건너뛴 기록 0

옵션 추가 (10.28)

옵션 하는 일 이유
--leads 학습할 3리드 바꾸기 (기본 V4 V3 II) 실제 패치 리드가 정해지면 값만 바꿔 다시 학습
--stride 1 학습용 창을 1초씩 겹쳐 자르기 (10초 → 창 6개) 데이터 늘리기. 시험셋은 같은 구간이 여러 번 채점되지 않게 늘 5초 간격
파일 이름 ptbxl_V4-V3-II_s1_train.npz, ptbxl_V4-V3-II_test.npz 리드 조합마다 코드를 복사하지 않고 결과 파일로 구분
  • 리드 조합 하나 = 모델 하나 (models/model_V4-V3-II_s1.pt)
  • 학습된 모델 파일(models/, *.pt)은 저장소에 올리지 않음. GPU 장비에서 다시 만들 수 있음

C. 시연·평가 데이터 분리 (LTSTDB → INCART)

생성기가 보내는 파형을 용도별로 나눈다. 부하테스트는 지금처럼 LTSTDB, AI 시연과 평가는 INCART로 바꾼다.

문제

  • LTSTDB 86개 기록 중 3리드는 18개뿐 (나머지는 2리드)
  • 그 18개도 녹음할 때 전극을 다르게 붙여서 리드가 제각각
s30661 : V4 · V3 · II      ← 모델과 맞음
s30691 : V6 · V5 · aVF     ← 모델이 거부
s30742 : E-S · A-S · A-I   ← 모델이 거부
  • 처음부터 3개만 녹음해서, 없는 리드를 만들어 낼 수 없음
  • 모델은 학습한 리드 조합만 받음 → LTSTDB로 시연하면 대부분 "판정 불가"

결정

쓰임 데이터 이유
서버 부하테스트 LTSTDB 250Hz 연속 기록, 이전 측정과 비교 가능. 부하에는 리드 이름이 상관없음
AI 시연·평가 INCART 75개 기록 전부 표준 12리드 → 모델 리드를 골라 꺼낼 수 있음. 박동 주석이 있어 학습에 안 쓴 데이터로 평가 가능
  • INCART는 원래 "정확도 평가용"으로 계획돼 있었는데(10.8) 아무도 쓰지 않았음. 원래 역할(평가) + 시연 스트림 역할을 더함

생성기 코드 (server/stream/stream_tcp.py)

idx = [names.index(l.upper()) for l in leads]
sig = np.asarray(rec.p_signal[:, idx], dtype=float)
sig = resample_poly(sig, FS, INCART_FS, axis=0)
  • 1줄 : INCART 12리드 이름 목록에서 고른 3리드(기본 V4·V3·II)가 몇 번째 열인지 찾음
  • 2줄 : 그 3개 열만 꺼냄
  • 3줄 : 257Hz → 250Hz. 한 칸씩 건너뛰는 게 아니라 250/257 비율로 늘렸다 줄이면서 필터가 같이 걸려 파형이 안 깨짐
# 부하테스트 (기본값, 그대로)
python server/stream/stream_tcp.py 100 --records auto
# AI 시연·평가
python server/stream/stream_tcp.py 100 --source incart --records auto --incart-dir ~/data/incart
  • 옵션 : --source ltstdb|incart, --leads(모델 학습 리드와 같게), --incart-dir(받아둔 폴더)
  • 캐시 파일 이름에 리드 조합을 넣음 → 조합을 바꾸면 따로 저장
  • 확인 : 가짜 INCART 기록 2개로 V4·V3·II 열이 맞게 골라지고 1분 = 15,000샘플(250Hz), 4명 × 3초 120건 전송. 없는 리드를 주면 그 기록의 리드 목록을 보여주고 멈춤

D. AI 흐름 공부 정리 (10.28)

학습과 판정

  • 학습 : 정답이 붙은 데이터를 보여주며 모델을 만드는 것. 미리 한 번, 결과물은 파일 하나(model.pt)
  • 판정(추론) : 학습한 모델에 처음 보는 환자 파형을 넣어 정상인지 물어보는 것. 실시간, 환자마다 1초에 한 번
[학습]  저장된 파일 → npz → train.py → model.pt      (오프라인, 한 번)
[판정]  생성기 → 서버 → 워커가 model.pt로 판정          (실시간)
  • 워커 안에서 R파 검출과 CNN은 따로 돎. R파는 심박수·박동 위치, CNN은 5초 파형 통째로 보고 이상 확률
  • CNN에 넣는 건 R파가 아니라 파형 그 자체 [3리드 × 1250샘플]

학습이 하는 일

① 파형 하나를 모델에 넣음
② 모델이 답함 (이상일 확률 0.30)
③ 정답 확인 (실제로는 이상 = 1)
④ 틀린 정도 계산 (손실)
⑤ 가중치를 다음엔 덜 틀리게 조금 고침
⑥ 다음 파형으로 반복
  • 학습 데이터 전체를 한 바퀴 = 1 epoch. 우리 설정은 20바퀴
  • 정상과 이상을 둘 다 보여줘야 둘을 가르는 기준을 배움
  • 오래 걸리는 건 계산보다 데이터 준비와 여러 번 다시 학습하는 것. 우리 모델은 작아서 학습 자체는 몇 분~수십 분

npz

  • 넘파이 배열 여러 개를 파일 하나에 묶어 압축 저장하는 형식
  • 공개 데이터는 우리 형식과 다름 → 변환 코드가 세 가지를 한 번에 함
    • 형식 변환 : 12리드 → 3리드, 500Hz → 250Hz, 10초 → 5초 창
    • 정답 붙이기 : 진단 코드를 보고 0 / 1, 애매한 건 빼기
    • 나누기 : 학습용과 시험용을 따로 저장
  • 한 번 변환해두면 학습을 여러 번 돌려도 파일 하나만 읽으면 됨

데이터 세 가지의 역할

데이터 녹음 역할
PTB-XL 병원에서 누워서 10초, 12리드, 의사 판독 라벨, 약 1만 8천 명 학습 + 시험
LTSTDB 24시간 홀터, 250Hz, 3리드 18개 서버 부하테스트 스트림
INCART 30분, 12리드, 박동 주석, 75개 AI 시연 스트림 + 다른 데이터로 평가
NSTDB 노이즈만 녹음 시험셋과 생성기에 섞어 강건성 확인
  • 학습은 저장된 녹음으로 미리 하고, 실시간은 생성기가 저장된 녹음을 0.1초씩 꺼내 흉내냄
  • LTSTDB 학습은 안 함. 정답은 정확하지만 18명뿐이라 모델이 그 사람들만 외움

리드와 모델

  • 모델 하나는 3리드를 한꺼번에 보고 판단함 → 리드 조합 하나 = 모델 하나
  • 12리드 데이터(PTB-XL, INCART)는 어떤 조합이든 골라 꺼낼 수 있음. 3리드로만 녹음한 데이터(LTSTDB)는 안 됨
  • 코드는 하나로 두고 설정값(--leads)만 바꿔 돌리며, 결과 파일 이름으로 구분. 코드를 복사하면 버그를 고칠 때 복사본마다 다 고쳐야 함

최종 모델까지 부족한 것

부족한 것 지금 데이터 실제 현장
측정 환경 병원 침대에 누워서 10초 패치를 붙이고 돌아다니는 환자
리드 표준 12리드 중 3개 패치 전극 위치는 다를 수 있음
노이즈 거의 깨끗함 움직임, 뒤척임, 샤워
  • 진짜 최종 모델은 기기 데이터가 있어야 하는데, 이번에는 공개 데이터로 만들 수 있는 최선 + 기기 데이터가 오면 바로 다시 학습할 수 있는 파이프라인이 목표
  • 정답이 없는 것(리드 구성, 평가 기준)은 기다리지 않고 근거를 갖고 우리가 정해 진행한 뒤, "이렇게 했는데 괜찮은지" 확인받음. 단 바꾸기 쉽게 해두고, 기준은 보고할 때 밝힐 것

E. 기준 CNN 학습·채점과 모델 비교

PTB-XL로 기준 CNN을 학습하고 학습에 안 쓴 시험셋으로 채점한다. 그다음 모델 구조·리드·학습 방식을 바꿔 가며 정확도와 판정 속도를 비교해, 서버에 올릴 모델을 고른다.

1. 기준 CNN 성적

GPU 장비에서 PTB-XL 전체로 학습하고, 학습에 한 번도 안 쓴 시험셋(fold 10)으로 채점 (eval_ptbxl.py)

구분 민감도 특이도 AUC 미검출 오검출
검증셋 (참고, 임계값 고른 데이터) 0.963 0.885 0.979 - -
시험셋 5초 창 2,354개 0.929 0.883 0.969 34 / 482 219 / 1,872
시험셋 10초 기록 1,177개 0.967 0.842 0.984 8 / 241 148 / 936
  • 판정 속도 : 창 1개 CPU 0.40ms / GPU 장비 0.41ms → 100명을 1초마다 판정해도 코어 1개의 4%. GPU가 필요 없음
  • 검증 → 시험 하락이 작아 과적합은 크지 않음
  • 약점 : 오검출이 정상 창의 12%. 누워서 잰 데이터라 실제 패치 환경에선 낮아질 것

2. 모델 6종 비교 (시드 3번 평균, 리드 V4·V3·II)

모델 창 AUC 민감도 특이도 CPU 판정 파라미터
CNN (기준선) 0.969 ± 0.002 0.939 0.853 0.40ms 4.5만
MobileNet1D 0.971 ± 0.002 0.946 0.877 0.95ms 7.2만
BiLSTM = CNN+LSTM (최종 후보) 0.972 ± 0.001 0.947 0.892 1.59ms 17.7만
Inception1D 0.973 ± 0.003 0.950 0.854 3.78ms 47.8만
FCN 0.962 ± 0.001 0.934 0.822 5.99ms 26.7만
ResNet1D 0.969 ± 0.001 0.945 0.874 6.55ms 327만
  • 정확도는 구조와 상관없이 비슷하고, 판정 속도는 최대 16배 차이
  • 채택 기준(실험 전에 정함) : AUC가 최고와 0.005 이내인 모델 중 CPU 판정이 가장 빠른 것 → CNN을 기준선으로
  • CNN+LSTM은 오검출이 가장 적음(특이도 0.892). 1.59ms라 100명도 감당 가능 → 최종 후보. PVC 구분·노이즈 평가·부하테스트에서 비교해 확정

3. 리드 조합·학습 방식 비교 (CNN)

조건 창 AUC
V4·V3·II (생성기 기본) 0.969
I·II·III (팔다리 전극) 0.972
II·V1·V5 (홀터) 0.974
I·II·V2 (노이즈 데이터셋과 같은 조합) 0.975
V4·V3·II, 학습 창 1초씩 겹쳐 자르기 (데이터 3배) 0.969
  • 리드 구성 차이는 0.006 안쪽. 실제 장비 리드가 정해지면 같은 방식으로 다시 학습하면 됨
  • 1초 겹쳐 자르기는 비슷한 창만 늘어 AUC 그대로 → 기준은 5초 간격 유지. 조교 피드백대로 진폭 조절·가우시안 노이즈 같은 다른 증강을 다음에 비교
  • fold : PTB-XL이 같은 환자가 섞이지 않게 미리 나눠 둔 10개 묶음. 1~9 학습, 10 시험 (K-Fold 교차검증은 다음에 5-Fold로)

4. 정리

  • 데이터 3배, 모델 73배, 구조 6종, 시드 3번을 바꿔도 AUC는 0.97 근처 → 이 데이터로는 거의 다 뽑음. 처음 세운 "병목은 모델 크기" 가설은 틀렸고, 라벨(10초 기록 라벨을 5초 창에 그대로 붙임) 쪽 한계로 판단
  • 다음은 학습보다 평가 : 노이즈 섞은 데이터, INCART 외부 평가, 100명 부하테스트

트러블슈팅

1. 학습 데이터 다운로드가 하루 넘게 걸림

  • 증상: wget으로 PTB-XL을 받는데 속도가 27KB/s, 남은 시간 18시간 이상
  • 원인: physionet.org 원본 서버가 느림
  • 해결: PhysioNet 공식 AWS 미러(aws s3 sync --no-sign-request s3://physionet-open/...)로 바꿔 PTB-XL 2.6GB를 약 10분, INCART를 약 1분에 받음
  • 배운 것: 공개 데이터는 원본 말고 공식 미러가 있는지 먼저 확인

2. "병목은 모델 크기"라는 가설이 틀림

  • 증상: 학습 데이터를 3배로 늘려도(1초 겹쳐 자르기) AUC가 0.969 그대로
  • 원인 추정: 모델이 작아서라고 보고 73배 큰 ResNet1D로 바꿨지만 AUC가 같았음 → 모델 크기 문제가 아니었음
  • 결론: 병목은 라벨(10초 기록 라벨을 5초 창에 그대로 붙임) 쪽. 다음 단계를 학습에서 평가(노이즈·외부 데이터)로 바꿈
  • 배운 것: 가설은 숫자로 확인하기 전까지 결론이 아님

남은 작업

  • 기존 AI 구조 파악, 단위 테스트 통과 확인
  • PTB-XL → NPZ 변환 코드 (리드 옵션, 1초 겹쳐 자르기)
  • 생성기 INCART 소스 (시연·평가용)
  • 기준 CNN 학습 → 시험셋 채점, 판정 속도 측정
  • 1초 겹쳐 자르기 비교
  • 모델 6종 × 시드 3번, 리드 4조합 비교
  • 노이즈 섞은 시험셋으로 강건성 확인
  • INCART 외부 평가
  • CNN·CNN+LSTM을 워커에 넣고 100명 부하테스트
  • 증강(진폭 조절, 가우시안 노이즈, NSTDB 노이즈) 재학습 비교
  • 학습 묶음에서 환자 단위 5-Fold 교차검증
  • 오검출 줄이기 (연속 N번 이상일 때 알람)

배운 점

직접 작성

목표

  • 목적 : 임시 라벨로 만든 연결 테스트용 모델 대신, 의사 판독 라벨이 있는 공개 데이터(PTB-XL)로 기준 모델을 학습하고 정확도와 판정 속도를 서버 관점에서 비교
    • 단계 흐름 : 6단계까지 = 서버 쪽 (전송·부하테스트) / 7단계 = 테스트 서버에 올릴 진짜 모델 만들기
  • 흐름 : A 기존 AI 구조 이해 → B 학습 데이터 만들기 → C 시연·평가 데이터 분리 → E 학습·채점과 모델 비교

A. 기존 AI 구조 이해

  • 목적 : 팀에 있던 AI 코드가 어디까지 됐고 뭐가 비어 있는지 파악
  • 설명 : 모델·학습·판정·워커 연결 틀은 있었지만, 의사 판독 라벨이 있는 학습 데이터를 만드는 코드가 없어서 정식 학습이 된 적이 없었음
    • 학습 : 정답이 붙은 데이터를 보여주며 모델을 만드는 것. 미리 한 번, 결과물은 파일 하나(model.pt)
    • 판정(추론) : 학습한 모델에 처음 보는 파형을 넣어 정상인지 물어보는 것. 실시간, 환자마다 1초에 한 번
    • R파 검출과 AI는 따로 돎 : R파는 심박수·박동 위치, CNN은 5초 파형 [3, 1250]을 통째로 보고 이상 확률을 냄

B. 학습 데이터 만들기 (make_ptbxl_npz.py)

  • 목적 : 공개 데이터 PTB-XL을 학습 코드가 읽는 모양으로 바꾸기
  • 설명 : 12리드 중 3개만 고르고, 500Hz → 250Hz, 10초 → 5초 창 2개로 자른 뒤, 진단 코드로 정답(정상 0 / 이상 1)을 붙임
    • PTB-XL : 병원에서 누워서 잰 10초 12리드 심전도 약 2만 건. 의사 판독 라벨이 있음
    • npz : 넘파이 배열 여러 개를 파일 하나에 묶어 압축 저장하는 형식
    • 다운샘플링(decimate) : 샘플 수를 줄이는 것. 그냥 건너뛰면 높은 주파수가 낮은 주파수로 둔갑해서(에일리어싱) 필터를 먼저 걸고 줄임
    • fold 10 : PTB-XL이 같은 환자가 섞이지 않게 미리 나눠둔 10묶음 중 마지막. 시험용으로만 쓰고 학습에는 절대 안 씀
    • 환자 단위 분리 : 같은 환자 기록이 학습과 시험에 같이 들어가면 외운 걸로 점수가 부풀려짐

C. 시연·평가 데이터 분리 (LTSTDB → INCART)

  • 목적 : 부하테스트와 AI 시연에 쓸 데이터를 용도별로 나누기
  • 설명 : LTSTDB는 기록마다 리드가 달라서 모델이 대부분 거부함 → 부하테스트는 LTSTDB 그대로, AI 시연·평가는 12리드인 INCART로
    • 리드 : 전극 두 개 사이의 전압 차이. 같은 심장을 다른 방향에서 본 것
    • 리드 조합 하나 = 모델 하나 : 모델은 학습한 3리드 순서로만 판정할 수 있음
    • 리샘플링(resample_poly) : 257Hz → 250Hz처럼 비율로 늘렸다 줄이며 필터를 같이 걸어 파형이 안 깨지게 바꿈

E. 기준 CNN 학습·채점과 모델 비교 (train.py → eval_ptbxl.py)

  • 목적 : GPU 장비에서 학습하고 학습에 안 쓴 시험셋으로 진짜 성적을 잰 뒤, 정확도가 오른 만큼 판정 속도를 내줘도 되는지 서버 관점에서 비교
  • 설명 : 시험셋 5초 창 기준 민감도 0.929, 특이도 0.883, AUC 0.969, 창 1개 판정 CPU 0.40ms. 모델 6종 × 리드 2조합 × 시드 3번 비교에서 AUC는 0.9620.976으로 비슷한데 CPU 판정은 0.40ms6.55ms로 16배 차이 → 경량 CNN은 기준선, CNN+LSTM(BiLSTM)은 최종 후보
    • epoch : 학습 데이터 전체를 한 바퀴 다 본 것
    • val_loss : 학습에 안 쓴 검증 데이터에서 틀린 정도. 내려가다 다시 오르면 외우기 시작한 것(과적합)
    • 민감도 : 실제 이상 중에 이상이라고 잡은 비율 (미검출을 안 하는 능력)
    • 특이도 : 실제 정상 중에 정상이라고 맞힌 비율 (오검출을 안 하는 능력)
    • AUC : 임계값과 상관없이 정상과 이상을 얼마나 잘 가르는지. 1에 가까울수록 좋음
    • 임계값 : 이상 확률이 몇 이상이면 이상으로 판정할지 정하는 기준. 미검출이 더 위험해서 민감도 우선으로 낮게 잡음 (0.1)
    • 검증셋 vs 시험셋 : 검증셋은 임계값 고를 때 써서 점수가 후함. 시험셋은 한 번도 안 쓴 데이터라 진짜 성적
    • 시드 : 학습 시작점을 정하는 난수. 바꿔서 여러 번 돌려야 차이가 우연인지 알 수 있음
    • 채택 기준을 미리 정함 : 결과를 보고 기준을 바꾸면 원하는 결론에 끼워 맞추게 됨
    • 파라미터 : 모델이 학습으로 조정하는 숫자 개수. 많을수록 무겁고 느림
    • CNN+LSTM : 합성곱이 파형 모양(PVC)을, LSTM이 시간 순서·리듬(심방세동)을 봄
    • 데이터 증강 : 데이터를 변형해 양을 늘리는 것. 1초 겹쳐 자르기는 비슷한 창만 늘어 효과가 없었고, 진폭 조절·가우시안 노이즈 같은 다른 방법이 필요 (조교 피드백)

트러블 슈팅

  • 학습 데이터 다운로드가 하루 넘게 걸림
    • 원인 : physionet.org 직접 받기가 27KB/s
    • 해결 : PhysioNet 공식 AWS 미러(aws s3 sync --no-sign-request)로 바꿔 2.6GB를 약 10분에
  • "병목은 모델 크기"라는 가설이 틀림
    • 원인 : 데이터를 3배로 늘려도, 모델을 73배 키워도 AUC가 그대로였음
    • 해결 : 병목은 라벨(10초 라벨을 5초 창에 그대로 붙임) 쪽으로 판단. 학습보다 노이즈·외부 평가로 다음 단계를 바꿈