De bouw van Vigil, een radar-valdetectiedemo voor de ouderenzorg: op een vaste synthetische set van 360 events evenaarde de naïeve baseline 1.0 recall bij 0.167 specificiteit.
Machine learningZorgtechnologieAI-veiligheid

De naïeve baseline in mijn radar-valdetector haalde dezelfde 1.0 recall. Hij gaf ook zeven valse alarmen in één nacht.

Ashutosh SinghalAshutosh Singhal18 juli 202613 min

Tijdens de synthetische nachtdienst die ik voor Vigil heb gebouwd, activeert de standaard baseline negen meldingen tussen 02:00 en 06:00 uur en zeven daarvan zijn onterecht. Een plafondventilator, gedetecteerd met een pieksnelheid van 5.0 m/s. Een therapiehond, radardoorsnede 0.27. Een bewoner die hard op een stoel gaat zitten met 2.92 m/s. Twee van die negen meldingen zijn echte valpartijen, en een daarvan is in een badkamer: een zwaartepuntspoor dat loopt van 1.53 m staand omlaag via 1.07, 0.84, 0.625 en 0.344 alvorens te stabiliseren op 0.119 m, vloerniveau, ademhaling aanwezig, geen herstel.

Elke gebeurtenis tijdens die dienst is synthetisch, gelabeld en fysisch onderbouwd, gegenereerd vanuit een vaste seed, en ik heb beide detectoren geschreven. Daarom kan ik het ongemakkelijke deel ronduit uitspreken: de baseline ving die val in de badkamer ook op.

Vigil is de intelligentielaag die ik heb gebouwd tussen een radar-featurestream en een verpleegoproepsysteem in de ouderenzorg. Het retourneert ALERT, SUPPRESS of ROUTE TO HUMAN, voorzien van een reden bij elke beslissing die een zorginstelling kan archiveren. De demo-route is https://veriprajna.com/demos/smart-facility-fall-detection. Ik begon de bouw met de aanname dat het moeilijke deel het zien van de val was. De benchmark was het bij de eerste run al niet met me eens.

Recall was het cijfer waarmee ik wilde beginnen

Ik draaide de benchmark in de verwachting dat valsensitiviteit de kop zou worden, en het is een mooi cijfer: 1.0 recall voor de cascade over een vaste set van 360 gelabelde, ruisachtige synthetische gebeurtenissen. De kolom ernaast veranderde het artikel dat ik dacht te schrijven. De naïeve baseline bestaat uit twee rekenkundige clausules: elke snelle of lage beweging is een val (peak_v > 2.0 OR min_cz < 0.45), en op dezelfde set scoort deze eveneens 1.0 recall. Sensitiviteit is waar de marketing rond valdetectie leeft, en beide detectoren zitten vastgepind aan de top.

Het onderscheid zit volledig in de rij die niemand op een slide zet. Confounder-specificiteit is 1.0 voor de cascade en 0.167 voor de baseline, een percentage valse alarmen van 0.833 per onschuldige gebeurtenis. Projecteer dat op de 30 onschuldige bewegingstriggers per kamer per dag die de benchmark veronderstelt, en de baseline komt uit op 25.0 valse alarmen per kamer per dag. Het gepubliceerde bereik voor bestaande standaardsensoren ligt tussen 5 en 15 valse alarmen per kamer per dag, en alarmmoeheid in plaats van sensorsensitiviteit is gedocumenteerd als de voornaamste reden waarom deze implementaties mislukken.

Ik moet dit zeggen voordat een technische lezer het voor mij doet. De fusiegewichten in data/fall_model.json werden gefit door tools/fit_fall_classifier.py op de eigen scenariogeneratoren van de demo, dezelfde generatoren die de set van 360 gebeurtenissen produceren. Dat is het sterkste bezwaar dat iemand kan inbrengen tegen mijn twee 1.0-scores, en het is ook waarom ik meer geef om de 0.167 dan om een van beide tienen. Het falen van de baseline is geen artefact van mijn trainingsopzet. Het is wat een drempelwaarde doet wanneer de wereld plafondventilatoren bevat.

De zeven valse alarmen bepalen of er nog iemand luistert wanneer het echte alarm afgaat.

De confounders waren ontworpen om één enkel kenmerk te verslaan

Mijn eerste instinct was om de classifier beter te maken, en dat was het verkeerde instinct. In het vroege deel van de bouw behandelde ik dit als een discriminatieprobleem: vind het kenmerk dat een val scheidt van een niet-val, geef het veel gewicht, en ga door. De scenariogeneratoren die ik al had geschreven maakten dat met opzet onmogelijk.

Elke confounder is gegenereerd om op een afzonderlijk kenmerk te overlappen met een echte val. Het hard neerploffen bij Cam 5 vertoont een snelheidspiek van 2.92 m/s, de ordegrootte van een val, en stabiliseert op 0.46 m. Zijn badkamervariant bij Cam 10 piekt op 3.31 m/s en stabiliseert op 0.44 m. De therapiehond en het bukken om een handdoek op te rapen brengen beide het zwaartepunt omlaag, wat de andere helft van de regel van de baseline is. Elke afzonderlijke test die ik kon schrijven werd door de constructie verslagen, en daarom wordt de baseline oprecht misleid bij 0.167 in plaats van misleid door een stroman die ik heb opgezet om te verliezen.

Snelheid was het kenmerk waarvan ik het zekerst was, en het is het kenmerk dat de classifier niet heeft overleefd. Wat overbleef is een logistisch model over vier kenmerken: nabijheid van de vloer, impactenergie, daalafstand en een proxy voor de radardoorsnede, samengevoegd tot een gekalibreerde P(val). Er bereikt helemaal geen snelheidsterm P(val). Er staat nog steeds een verouderde regel in de docstring van de module uit de tijd dat ik dacht van wel. Het is eenvoudige numpy, klein genoeg dat iemand classifier.py kan openen en het geheel in zijn hoofd kan bevatten, wat op een levensveiligheidspad voor mij meer waard is dan nog een punt AUC.

Ik toon mensen de onderdrukking bij Cam 10 altijd eerst, omdat het paneel het hele meningsverschil in één enkele regel samenvat.

Detailpaneel van Vigils Cam 10 Badkamer met een SUPPRESS-beslissing en de redenregel: snelheidspiek maar zwaartepunt gestabiliseerd op 0.44 m, zithoogte, niet de vloer, geen harde impact.
Cam 10 · Badkamer is het harde gaan zitten in de badkamer, met een piek van 3.31 m/s. Vigil logt SUPPRESS met het doorslaggevende kenmerk in de redenregel: het zwaartepunt stabiliseerde op 0.44 m, zithoogte, niet de vloer, zonder harde impact. Die snelheid alleen al voldoet aan de `peak_v > 2.0`-clausule van de naïeve baseline.

Vier voorwaarden, één venster van 8 seconden

Ik schreef de temporele narratieve verifiëerder als het stuk dat ik als buitenstaander zou willen lezen. temporal.py vereist vier voorwaarden binnen hetzelfde venster van 8 seconden, waarbij staan wordt vastgesteld in het eerste vijfde deel: een mediaan zwaartepunt boven 1.2 m, een daling groter dan 0.6 m gecombineerd met een pieksnelheid boven 1.8 m/s ergens in het venster, een aanhoudende breedbandimpact waarvan het voortschrijdend gemiddelde over 3 frames hoger is dan 0.50, en het zwaartepunt dat daadwerkelijk tot onder 0.30 m reikt. De test voor aanhoudende impact bestaat omdat een piek in één enkel frame goedkoop is en een lichaam dat op een vloer slaat dat niet is.

De redenreeks die de app afgeeft luidt "standing → descent → impact → floor", wat de manier is waarop een verpleegkundige een incident leest, maar de implementatie verbindt die voorwaarden met een logische EN (AND) over het venster in plaats van een volgorde af te dwingen. Het is geen toestandautomaat, en ik schrijf dat liever zelf op dan dat een engineer het in de broncode ontdekt en zich afvraagt wat de tekst nog meer heeft afgerond.

Pas dan voegt de gate een ademhalingsbevestiging boven 0.20 en een valbetrouwbaarheid van minstens 0.70 toe. Die drie waarden, vloerniveau 0.30 m, ademhaling 0.20 en de ondergrens van 0.70 voor betrouwbaarheid, staan in gewone code buiten elk model om. De richting is wat telt: de deterministische voorwaarden moeten gelden voordat de score van het model überhaupt wordt geraadpleegd, zodat een betrouwbaarheidsscore nooit uit zichzelf een melding kan fabriceren. Een lagere P(val) kan een ALERT nog steeds veranderen in een SUPPRESS, wat de juiste asymmetrie is voor een laag die mag zwijgen en niet mag verzinnen. Nog één bepalende drempelwaarde bevindt zich buiten dat gedocumenteerde blok: een hardgecodeerde p_fall >= 0.40 in gate.py die een gebeurtenis met meerdere personen kan doorsturen naar een menselijke controle louter op basis van de score van het model. Ik noem deze omdat "drie gedocumenteerde drempelwaarden" anders meer eer opstrijkt dan het verdient.

De onderdrukkingen zijn het dossier waar een inspectie daadwerkelijk om vraagt

Ik bouwde het Decision Ledger voordat ik iets bouwde dat op een product leek, want de vraag die ik niet kon beantwoorden was nooit "hebben jullie het opgemerkt". Het was "waarom ging er om 2:13 uur geen melding af in Kamer 203", en het antwoord moet al in een schriftelijk dossier staan op het moment dat iemand ernaar vraagt. Tien van de twaalf gebeurtenissen tijdens de dienst zijn onderdrukkingen, en elk daarvan draagt de geregistreerde kenmerkwaarde met zich mee: het niet-menselijke doelwit bij Cam 6 met een radardoorsnede van 0.27 tegenover een menselijk minimum van 0.55, het bukken bij Cam 7 en Cam 12 waarbij het zwaartepunt stopt op 0.60 m en 0.59 m met een impactenergie van 0.07 tegenover een drempelwaarde van 0.50.

Vigils plattegrondweergave en Decision Ledger, met vermelding van de SUPPRESS-rijen van Cam 12 omlaag tot Cam 6, elk met de bijbehorende redentekst.
Het Decision Ledger na de dienst, met het actieve incident op Cam 3 · Badkamer bij 99% betrouwbaarheid. Elke onderdrukte rij draagt zijn doorslaggevende reden: het stabiliseren op 0.44 m zithoogte bij Cam 10, de radardoorsnede van 0.27 onder het menselijk minimum bij Cam 6, de neerwaartse beweging bij Cam 7 en Cam 12 die terugkeerde naar een staande positie.
Die tien onderdrukte rijen zijn waar een inspecteur naar vraagt, omdat het de gebeurtenissen zijn waarbij niets gebeurde en iemand toch moet kunnen uitleggen waarom.

Slechts een deel van de kalibratie per kamer is daadwerkelijk gekoppeld aan een beslissing. De plafondventilator van Cam 1 wordt onderdrukt omdat de clutter-map van Kamer 214 een Doppler-invoer op een vaste locatie bevat op (1.5, 1.5, 2.45 m) en check_clutter deze op die voxel maskeert. Dat pad is reëel. De zit- en bedhoogten per kamer in rooms.json, 0.42 m in Kamer 118 Badkamer en 0.45 m in Kamer 203, en de wandbeugelvermeldingen daarnaast, zijn kalibratiegegevens die door geen enkel V1-codepad worden gelezen; de zitband die ik gebruik om een harde zit te labelen is één globale test van 0.38 tot 0.60 m. Er staat zelfs een long_lie_sec: 180.0-sleutel in dat bestand die door niets wordt gebruikt. Kalibratie per kamer is het integratiewerk waarvoor een echte implementatie betaalt, en in deze build zijn alleen de Doppler-maskers verbonden met een beslissing.

De export is een dienst-audit-JSON die elke melding, routering en onderdrukking dekt met de doorslaggevende kenmerkwaarden en beleidsreden, wat precies is wat een CMS F689- of QAPI-dossier vereist. De klinische incidentnotitie wordt apart samengesteld uit dat gestructureerde bewijs en getoond in het incidentpaneel, niet binnen de JSON.

De melding die ik liet geven, en de val die ik niet liet bevestigen

Ik heb de climax van de dienst bewust in een badkamer geplaatst. Het is de ruimte met het hoogste risico en de enige plek waar een camera geen bruikbare optie is: negentien Amerikaanse staten hebben wetgeving aangenomen over camera's in verpleeghuiskamers, die ze over het algemeen met toestemming toestaan in de kamer van een bewoner, terwijl badkamers in de praktijk uitgesloten blijven vanwege privacyredenen. Radar-features bevatten geen beeld, en dat is precies waarom ze kunnen komen waar een camera dat niet kan.

Cam 3 is die gebeurtenis, en Vigil retourneert ALERT, categorie long_lie, betrouwbaarheid 0.99, vloertijd 4.8 s, waarbij de escalatieladder direct op CNA staat, op 90 s voor de Hoofdverpleegkundige en op 180 s voor het Hoofd Zorg (DON). De tekst op de verpleegoproepbadge luidt "Room 118B Bathroom: Fall Detected, 99% confidence. Resident on floor 5s. Breathing confirmed." Dispatch verstuurt zowel een traditioneel Rauland potentiaalvrij contactsignaal als een Ascom/Austco MQTT/REST-payload, en beide lopen via een gelogde adapterstub. Er is aan niets hiervan fysieke verpleegoproephardware gekoppeld. De reden dat het toch de moeite waard is om te bouwen, is het langdurig liggen: de helft van de ouderen die langer dan een uur op de vloer ligt, overlijdt binnen zes maanden.

Vigils incidentdetail van Cam 3 Badkamer: de Room 118B verpleegoproepbadge, de escalatieladder, de payload bij een betrouwbaarheid van 0.99 en de klinische incidentnotitie.
De uitgeklapte melding van Cam 3 · Badkamer (Kamer 118B in de badge, payload en notitie). De dispatch-payload registreert betrouwbaarheid 0.99, floor_time_sec 4.8, breathing true en long_lie_risk true, geflankeerd door de ladder voor CNA, Hoofdverpleegkundige en Hoofd Zorg (DON), en de incidentnotitie samengesteld uit dat gestructureerde bewijs.

Eén getal op dat paneel weiger ik te verkopen. De 7.0 seconden van impact tot melding wordt berekend in gate.py als de hold-timer plus drie seconden, een constante. De app toont het, en ik citeer het scherm, maar het is rekenkunde in plaats van een gemeten systeemsnelheid, en het benoemen als een gebenchmarkte latentie zou het soort kleine onwaarheid zijn dat je de grote waarheden ernaast kost.

De gebeurtenis waar ik trotser op ben, is degene die Vigil weigert. Cam 2 is een echte val in de grondwaarheid en P(val) bereikt 0.99, en toch bevestigt Vigil hem niet: de kamer bevat twee doelen, het volgen van een enkele persoon valt buiten de V1-dekking, en de gate retourneert ROUTE TO HUMAN bij lage betrouwbaarheid. De naïeve baseline activeert automatisch en strijkt de eer op voor een detectie die hij niet heeft verdiend. Er vonden twee echte valpartijen plaats tijdens die dienst. Vigil sloeg alarm bij de ene en stuurde de andere door naar een personeelscontrole, en ik zal dat niet omschrijven als het vangen van elke val, want dat is het niet. Over de gehele benchmark worden 40 van de 40 valpartijen bij meervoudige aanwezigheid doorgestuurd naar een mens, zonder over-alarmering en zonder dat er één gemist is.

Wat het scorebord mag claimen

De voorbehoudsregel onder het venster 'Shift Results' is het onderdeel van dat paneel dat ik als eerste ontwierp. Het venster meldt 0 valse alarmen voor de engine tegenover 7 voor de gevestigde partij tijdens deze dienst, 1 van de 2 echte valincidenten gedetecteerd, 1 doorgestuurd naar een mens, 100% confounder-specificiteit over 360 gelabelde gebeurtenissen, en 0.0 tegenover 25 geprojecteerde valse alarmen per kamer per dag.

Vigils venster Shift Results voor de nachtdienst van 02:00 tot 06:00 uur: 0.0 versus 25 valse alarmen per kamer per dag, 1 van de 2 echte valincidenten opgemerkt, 1 doorgestuurd naar een mens, 100% confounder-specificiteit over 360 gelabelde gebeurtenissen.
Het scorebord van Shift Results. De 0.0 valse alarmen per kamer per dag van de engine staat tegenover de 25 van de bestaande oplossing, waarbij 1 van de 2 echte valpartijen gealarmeerd werd en 1 doorgestuurd naar een menselijke controle. De voettekst vermeldt de reikwijdte: 360 gelabelde ruisachtige gebeurtenissen, een synthetische radar-featurestream, geen live-sensoren, geen beschermde gezondheidsinformatie (PHI), geen camera.

Die cijfers beschrijven een vaste synthetische gouden set en niets anders. Geen productienauwkeurigheid, geen klinisch resultaat, geen gevalideerde medische claim, en nooit een garantie voor een instelling. Een echte pilot mikt op minder dan 2 valse alarmen per kamer per dag na kalibratie in de schaduwmodus, en dat is het getal dat ik aan een Hoofd Zorg zou voorleggen, omdat ik daarop kan worden afgerekend. De 0.0 is het bewijs dat het mechanisme vallen scheidt van confounders op een set die ik u kan overhandigen; het is geen belofte over een gebouw waar ik nooit binnen ben geweest.

De norm waaraan ik een levensveiligheidsalarm nu toets

Ik kwam uit deze bouw naar voren met een veel scherpere definitie van waar valdetectie goed in moet zijn. Detectie is een drempelwaarde, en een drempelwaarde scoort op mijn eigen testset al 1.0 recall. Het werk dat de aandacht van een verpleegkundige verdient, is de weigering: de clutter-map die weet welke voxel de ventilator bezet, de impacttest die geen genoegen neemt met één enkel frame, de voorwaarde voor het bereiken van de vloer die een harde zit scheidt van een val, en een gate die zo is geschreven dat een overheidsinspecteur in plaats van een model kan lezen waarom het systeem deed wat het deed.

De volledige walkthrough is te vinden op https://veriprajna.com/demos/smart-facility-fall-detection, en de tien onderdrukte rijen in het ledger zijn waar de dienst daadwerkelijk wordt beslist.

En als u de dienst liever bekijkt dan leest hoe ik hem beschrijf: hier is de hele nacht van begin tot eind te zien.

Een systeem dat alarm slaat op de plafondventilator wordt binnen een week gedempt, en een gedempt systeem detecteert helemaal niets meer. Het meest geavanceerde gedrag dat ik deze laag kon meegeven was het vermogen om te weigeren, vastgelegd in het dossier, met het doorslaggevende getal erbij. Bij Cam 2 was dat getal 0.99, en de juiste beslissing was nog steeds om de gebeurtenis over te dragen aan een mens.

Gerelateerd onderzoek

Ook gepubliceerd op

Bouw uw AI met vertrouwen.

Werk samen met een team met diepgaande ervaring in het bouwen van de volgende generatie enterprise-AI. Laat ons u helpen bij het ontwerpen, bouwen en implementeren van een AI-strategie waarop u kunt vertrouwen.

Veriprajna Deep Tech-adviesbureau is gespecialiseerd in het bouwen van veiligheidskritische AI-systemen voor de gezondheidszorg, de financiële sector en gereguleerde domeinen. Onze architecturen worden gevalideerd aan de hand van gevestigde protocollen met uitgebreide compliancedocumentatie.