
레이더 낙상 감지기의 단순 기준선도 동일한 1.0 재현율을 기록했다. 하룻밤 새 7번의 오경보도 함께 울렸다.
Vigil을 위해 구축한 합성 야간 근무조에서 기성 기준선(off-the-shelf baseline)은 02:00에서 06:00 사이에 9번의 경보를 발생시켰고, 그중 7번은 틀렸다. 최대 속도 5.0 m/s로 포착된 천장 선풍기, 레이더 단면적(RCS) 0.27의 치료견, 2.92 m/s로 의자에 세게 주저앉는 입소자가 그것이다. 9번의 경보 중 실제 낙상은 2건이었으며, 그중 하나는 욕실에서 발생했다. 즉, 1.53 m 서 있는 자세에서 1.07, 0.84, 0.625, 0.344 m를 거쳐 바닥 높이인 0.119 m에서 안정되고, 호흡은 존재하나 회복 움직임은 없는 중심점 궤적이다.
그 근무조의 모든 이벤트는 고정 시드에서 생성되어 물리적으로 근거가 있고 라벨이 지정된 합성 데이터이며, 두 감지기 모두 내가 직접 작성했다. 그렇기 때문에 나는 껄끄러운 부분을 솔직하게 말할 수 있다. 기준선 역시 그 욕실 낙상을 포착했다는 점이다.
Vigil은 노인 주거 시설에서 레이더 피처 스트림과 간호사 호출 시스템 사이에 위치하도록 내가 구축한 지능형 계층이다. ALERT, SUPPRESS 또는 ROUTE TO HUMAN을 반환하며, 각 결정에는 시설에서 공식 기록으로 보관할 수 있는 사유가 첨부된다. 데모 경로는 https://veriprajna.com/demos/smart-facility-fall-detection이다. 나는 낙상을 포착하는 것이 어려운 부분이라고 가정하고 구축을 시작했다. 첫 번째 실행에서 벤치마크는 내 생각과 달랐다.
재현율은 내가 맨 앞에 내세우고 싶었던 숫자였다
나는 낙상 민감도가 헤드라인이 될 것으로 기대하며 벤치마크를 실행했고, 이는 훌륭한 숫자다: 라벨링된 노이즈 합성 이벤트 360개로 구성된 고정 데이터셋에서 캐스케이드의 1.0 재현율. 그 옆의 열이 내가 쓰려고 했던 글의 방향을 바꾸어 놓았다. 단순 기준선은 두 개의 산술 조건문으로, 빠르거나 낮은 움직임은 모두 낙상이라는 식이다(peak_v > 2.0 OR min_cz < 0.45), 동일한 데이터셋에서 이 기준선 역시 1.0의 재현율을 기록한다. 민감도는 낙상 감지 마케팅이 집중되는 영역이며, 두 감지기 모두 그 최상단에 고정되어 있다.
두 감지기의 차이는 아무도 슬라이드에 올리지 않는 행에 온전히 드러난다. 교란 요인 특이도는 캐스케이드가 1.0, 기준선이 0.167이다, 이는 정상적인 이벤트당 0.833건의 오경보 비율이다. 벤치마크가 가정한 방당 하루 30건의 정상 움직임 트리거에 이를 대입하면 기준선은 방당 하루 25.0건의 오경보에 도달한다. 기성 센서에 대해 공개된 기존 제품군의 범위는 방당 하루 5~15건의 오경보이며, 센서 민감도가 아니라 경보 피로가 이러한 배치가 실패하는 주요 원인으로 문서화되어 있다.
기술적 독자가 대신 지적하기 전에 내가 먼저 밝혀두어야겠다. data/fall_model.json에 담긴 융합 가중치는 tools/fit_fall_classifier.py가 데모 자체의 시나리오 생성기에서 적합(fit)한 것으로, 이 생성기는 360개 이벤트 데이터셋을 생성한 것과 동일하다. 이것은 나의 두 1.0이라는 수치에 대해 누군가 제기할 수 있는 가장 강력한 반론이며, 내가 두 1.0보다 0.167에 훨씬 더 신경을 쓰는 이유이기도 하다. 기준선의 실패는 내 훈련 환경이 만들어낸 인위적 결과가 아니다. 천장 선풍기가 존재하는 현실 세계에서 단순 임계값이 맞닥뜨리는 필연적 결과다.
진짜 낙상이 발생했을 때 여전히 경보에 귀를 기울일지 여부는 바로 그 일곱 건의 오경보가 결정한다.
단일 피처를 무력화하도록 설계된 교란 요인들
내 첫 본능은 분류기를 개선하는 것이었고, 그것은 잘못된 본능이었다. 구축 초기에는 이를 구별 문제로 취급했다. 즉, 낙상과 낙상이 아닌 것을 구분하는 피처를 찾아 가중치를 크게 두고 넘어가는 방식이었다. 그러나 내가 이미 작성해 둔 시나리오 생성기는 의도적으로 그런 접근을 불가능하게 만들었다.
각 교란 요인은 특정 개별 피처에서 실제 낙상과 겹치도록 생성된다. Cam 5에서 털썩 주저앉는 동작은 낙상 수준의 크기인 2.92 m/s의 급격한 속도 상승을 보이며 0.46 m에서 안정된다. Cam 10의 욕실 유사 사례는 3.31 m/s로 정점을 찍고 0.44 m에서 안정된다. 치료견과 수건을 줍기 위해 몸을 숙이는 동작은 둘 다 중심점을 낮추며, 이는 기준선 규칙의 나머지 절반에 해당한다. 내가 작성할 수 있는 어떤 단일 테스트도 구조상 무력화되도록 설계되었으며, 이것이 바로 기준선이 패배하도록 내가 꾸며낸 허수아비에 속은 것이 아니라 0.167이라는 수치로 진짜 속아 넘어간 이유다.
속도는 내가 가장 확신했던 피처였지만, 정작 분류기에는 살아남지 못한 피처였다. 살아남은 것은 바닥 근접도, 충격 에너지, 하강 낙차, 레이더 단면적 대리 변수라는 네 가지 피처에 대한 로지스틱 모델이며, 이들이 보정된 P(fall)로 융합된다. P(fall)에는 속도 항이 전혀 들어가지 않는다. 모듈 독스트링에는 속도가 들어갈 것이라 생각했던 시절의 오래된 문장이 여전히 남아 있다. 이는 순수한 numpy 코드로 작성되어 있어 누구나 classifier.py를 열어 전체 구조를 머릿속에 담을 수 있을 만큼 간결하다. 인명 안전 경로에서는 AUC 점수 1점을 더 올리는 것보다 이것이 내게 훨씬 더 가치 있다.
나는 Cam 10의 억제 사례를 사람들에게 가장 먼저 보여주는데, 그 패널이 단 한 줄로 전체적인 의견 차이를 명확히 드러내기 때문이다.

네 가지 조건, 하나의 8초 윈도우
나는 외부인의 입장에서 읽고 싶어 할 만한 구성 요소로서 시간적 서사 검증기를 작성했다. temporal.py는 동일한 8초 윈도우 내에서 네 가지 조건을 요구하며, 윈도우의 첫 5분의 1 지점에서 기립 상태가 확인되어야 한다. 즉, 중심점 중앙값이 1.2 m 초과, 윈도우 내 어딘가에서 1.8 m/s를 초과하는 최대 속도와 함께 0.6 m 이상의 하강, 3개 프레임 이동 평균이 0.50을 초과하는 지속적인 광대역 충격, 그리고 중심점이 실제로 0.30 m 미만에 도달하는 조건이다. 지속 충격 테스트가 존재하는 이유는 단일 프레임 스파이크는 쉽게 발생하지만, 인체가 바닥에 부딪히는 것은 그렇지 않기 때문이다.
앱이 출력하는 사유 문자열에는 "standing → descent → impact → floor"라고 표시되며, 이는 간호사가 사건을 파악하는 방식과 일치한다. 하지만 실제 구현은 순서를 강제하기보다 해당 윈도우 전체에 걸쳐 이 조건들을 AND 연산으로 결합한다. 이는 상태 기계가 아니며, 엔지니어가 소스 코드에서 이를 발견하고 설명 문구가 다른 어떤 부분을 축약했는지 의아해하게 만들기보다는 차라리 내가 직접 명시하는 편을 택하겠다.
그제서야 게이트는 0.20 이상의 호흡 확인과 최소 0.70의 낙상 신뢰도를 추가한다. 바닥 높이 0.30 m, 호흡 0.20, 신뢰도 하한선 0.70이라는 이 세 가지 값은 모든 모델 외부의 순수 코드에 존재한다. 중요한 것은 방향성이다. 모델의 점수를 참조하기 전에 결정론적 조건이 반드시 충족되어야 한다, 따라서 신뢰도 수치만으로 경보를 단독 생성할 수는 없다. 더 낮은 P(fall)는 여전히 ALERT를 SUPPRESS로 바꿀 수 있으며, 이는 침묵을 지킬 수는 있어도 없는 사실을 지어내서는 안 되는 계층에 적합한 비대칭성이다. 문서화된 해당 블록 바깥에는 또 하나의 결정적 임계값이 존재하는데, 바로 p_fall >= 0.40라는 gate.py의 하드코딩된 값이다. 이는 모델의 점수만으로 다중 재실자 이벤트를 사람의 확인 단계로 보낼 수 있다. 이를 굳이 언급하는 이유는, 그렇지 않으면 "문서화된 세 가지 임계값"이라는 표현이 실제보다 과장된 역할을 하게 되기 때문이다.
주정부 감사관이 실제로 요구하는 기록은 바로 억제 이력이다
나는 제품다운 형태를 갖추기 전에 결정 원장(Decision Ledger)부터 구축했다. 내가 답할 수 없었던 질문은 결코 "낙상을 잡았는가"가 아니었기 때문이다. 그것은 바로 "2시 13분에 203호에서 왜 경보가 울리지 않았는가"였으며, 누군가 이를 물어볼 때에는 이미 서면 기록으로 답이 준비되어 있어야 한다. 해당 근무조에서 발생한 12건의 이벤트 중 10건이 억제이며, 각 이벤트에는 기록된 피처 값이 함께 남는다. Cam 6의 비인간 표적은 인간 최소 기준인 0.55에 미치지 못하는 0.27의 레이더 단면적을 기록했고, Cam 7과 Cam 12의 몸 숙이기 동작에서는 중심점이 0.60 m와 0.59 m에서 멈추었으며 충격 에너지는 0.50 임계값에 한참 못 미치는 0.07에 불과했다.

이 10개의 억제된 행이야말로 감사관이 묻는 대상이다. 아무 일도 일어나지 않았지만 누군가는 그 이유를 여전히 설명해야 하는 사건들이기 때문이다.
방별 캘리브레이션 중 일부만이 실제 결정 로직에 연결되어 있다. Cam 1의 천장 선풍기가 억제되는 이유는 214호의 클러터 맵에 (1.5, 1.5, 2.45 m) 위치의 고정 도플러 항목이 포함되어 있고, check_clutter가 해당 복셀에서 이를 마스킹하기 때문이다. 이 경로는 실제로 동작한다. rooms.json에 들어 있는 118호 욕실의 0.42 m, 203호의 0.45 m와 같은 방별 좌석 및 침대 높이, 그리고 그 옆의 안전 손잡이 항목들은 어떤 V1 코드 경로에서도 읽지 않는 캘리브레이션 데이터다. 강한 착석을 라벨링하는 데 사용하는 좌석 높이 대역은 전체에 일괄 적용되는 0.38~0.60 m 테스트 하나뿐이다. 해당 파일에는 어떤 로직도 참조하지 않는 long_lie_sec: 180.0 키까지 들어 있다. 방별 캘리브레이션은 실제 상용 배치 시 비용을 지불해야 하는 통합 작업이며, 이번 구축에서는 도플러 마스크만이 결정 로직에 연결되어 있다.
내보내기 파일은 모든 경보, 전달 및 억제를 결정 피처 값 및 정책 사유와 함께 담아낸 근무조 감사 JSON이며, 이는 바로 CMS F689나 QAPI 바인더가 요구하는 자료다. 임상 사고 일지는 이러한 구조화된 증거로부터 별도로 작성되어 JSON 내부가 아닌 사건 패널에 표시된다.
경보를 울리도록 허용한 사건, 그리고 낙상이라 단정하지 못하게 막은 사건
나는 의도적으로 근무조의 클라이맥스를 욕실에 배치했다. 욕실은 가장 위험도가 높은 방이자 카메라를 사용할 수 없는 유일한 장소이기 때문이다. 미국의 19개 주에서는 요양원 내 카메라 설치를 규제하는 법률을 제정하여 거주자의 동의 하에 거주실 설치를 일반적으로 허용하지만, 욕실은 사생활 보호를 이유로 실제로는 계속 제외된다. 레이더 피처에는 이미지가 포함되지 않으며, 이것이 바로 카메라가 들어갈 수 없는 곳에 레이더가 진입할 수 있는 정확한 이유다.
Cam 3가 바로 그 사건이며, Vigil은 ALERT, 범주 long_lie, 신뢰도 0.99, 바닥 체류 시간 4.8초를 반환하며, 에스컬레이션 사다리는 즉시 간호조무사(CNA), 90초 후 책임간호사, 180초 후 간호부서장(DON)으로 설정된다. 간호사 호출 배지 텍스트에는 "118B호 욕실: 낙상 감지, 신뢰도 99%. 바닥 체류 5초. 호흡 확인됨."이라고 표시된다. 발송 로직은 레거시 Rauland 무전압 접점 신호와 Ascom/Austco MQTT/REST 페이로드를 모두 발행하며, 둘 다 로그가 기록되는 어댑터 스텁을 통과한다. 이 중 어떤 것에도 실제 간호사 호출 하드웨어는 연결되어 있지 않다. 그럼에도 이를 구축할 가치가 있는 이유는 방치 때문이다. 바닥에 한 시간 이상 쓰러져 방치된 노인의 절반이 6개월 이내에 사망한다.

해당 패널에서 내가 내세우기를 거부하는 숫자가 하나 있다. 충격부터 경보까지의 7.0초는 gate.py에서 홀드 타이머에 3초를 더한 상수값으로 계산된다. 앱은 이를 표시하고 나 역시 화면에 나타난 것을 인용하겠지만, 이는 측정된 시스템 속도가 아니라 단순 산술값일 뿐이다. 이를 벤치마크된 지연 시간이라 부르는 것은 그 옆에 있는 거대한 진실들을 훼손하는 사소한 거짓말이 될 것이다.
내가 더 자랑스럽게 생각하는 이벤트는 Vigil이 경보를 거부한 건이다. Cam 2는 실측 자료상 실제 낙상이며 P(fall)이 0.99에 달하지만, Vigil은 여전히 이를 낙상이라 단정하지 않는다. 해당 방에는 두 개의 표적이 존재하고 1인 추적은 V1 적용 범위를 벗어나기 때문에 게이트는 낮은 신뢰도로 ROUTE TO HUMAN을 반환한다. 단순 기준선은 자동으로 경보를 발생시키며 자신이 자격을 갖추지 못한 감지에 대한 공로를 챙긴다. 그 근무조에서는 두 건의 실제 낙상이 발생했다. Vigil은 한 건에 대해 경보를 울렸고 다른 한 건은 직원 확인 단계로 보냈다. 나는 이를 모든 낙상을 포착했다고 설명하지 않을 것이다. 실제로 그렇지 않기 때문이다. 벤치마크 전체에서 다중 재실자 낙상 40건 중 40건 모두가 과잉 경보나 누락 없이 사람의 확인 단계로 라우팅된다.
스코어보드가 주장할 수 있는 범위
근무조 결과 모달 아래의 단서 조항은 해당 패널에서 내가 가장 먼저 작성한 부분이다. 이 모달은 이번 근무조에서 기존 제품군의 7건 대비 엔진의 오경보 0건, 실제 낙상 2건 중 1건 감지 및 1건 사람에게 라우팅, 라벨링된 360개 이벤트에 대해 100% 교란 요인 특이도, 그리고 방당 하루 예상 오경보 25건 대비 0.0건을 보고한다.

이 수치들은 고정된 합성 골든 데이터셋을 설명할 뿐 그 이상도 그 이하도 아니다. 상용 프로덕션 정확도도 아니고, 임상 결과도 아니며, 검증된 의학적 주장도 아니고, 시설에 대한 보증은 더더욱 아니다. 실제 파일럿은 섀도 모드 캘리브레이션 후 방당 하루 2건 미만의 오경보를 목표로 한다, 그리고 그것이야말로 내가 간호부서장에게 제시할 수 있는 숫자다. 내가 책임을 질 수 있는 숫자이기 때문이다. 0.0이라는 숫자는 내가 여러분에게 건넬 수 있는 데이터셋에서 메커니즘이 낙상과 교란 요인을 분리해낸다는 증거일 뿐, 내가 한 번도 가보지 않은 건물에 대한 약속이 아니다.
내가 이제 인명 안전 경보에 적용하는 기준
이번 구축을 마치며 나는 낙상 감지가 무엇을 잘해야 하는가에 대해 훨씬 더 좁은 정의를 갖게 되었다. 감지는 임계값의 문제이며, 단순 임계값조차 내 자체 테스트 데이터셋에서 이미 1.0의 재현율을 기록한다. 간호사의 신뢰와 주의를 얻는 핵심 작업은 바로 거부다. 선풍기가 어느 복셀을 차지하는지 알고 있는 클러터 맵, 단일 프레임 충격을 용납하지 않는 충격 테스트, 강한 착석과 낙상을 구분하는 바닥 도달 조건, 그리고 모델이 아니라 주정부 감사관이 시스템의 판단 이유를 읽을 수 있도록 작성된 게이트가 그것이다.
전체 워크스루는 https://veriprajna.com/demos/smart-facility-fall-detection에서 확인할 수 있으며, 원장의 10개 억제 행이야말로 근무조의 승패가 실제로 결정된 곳이다.
내가 설명하는 글을 읽기보다 근무조 영상을 직접 보고 싶다면, 밤새 진행된 전체 실행 과정을 처음부터 끝까지 여기서 확인할 수 있다.
천장 선풍기에 경보를 울리는 시스템은 일주일도 안 되어 음소거되며, 음소거된 시스템은 아무것도 감지하지 못한다. 내가 이 계층에 부여할 수 있었던 가장 정교한 동작은 결정적 수치를 첨부하여 공식 기록을 남기면서 경보를 거부할 수 있는 능력이었다. Cam 2에서 그 수치는 0.99였지만, 여전히 올바른 결정은 그 사건을 사람에게 넘기는 것이었다.


