
Иногда кажется, что звук — лишь колебания воздуха, но что если за ним скрывается нечто большее — биения сердца, ритмы эмоций, немые сигналы тела? В этой статье я расскажу о том, как современные архитектуры нейросетей могут «слышать» сердце — буквально и метафорически. Я подниму вопросы предобработки, особенностей модели, шума физиологических сигналов, покажу примеры кода и реальные кейсы. Для тех, кто уже имеет дело с нейросетями и аудиосигналами — будет что обсудить.
Скрытый сигнал под шумом
Когда я впервые задумался над задачей извлечения пульса из микрофона смартфона, мне казалось: ну вот, возьмём аудиодорожку, отфильтруем ошибочные частоты, применим модель — и всё. Наивно, скажете вы? Да, я и сам через это прошёл. В нашем распоряжении — сигнал с массой шумов: дыхание, речь, фоновые звуки, шёпот ветра, металлическое эхо. Как отделить лишь сердце?
Сначала надо задуматься: на каком уровне этот пульс вообще проявляется? Это не ультразвук, не что-то очевидное. Это слабая модуляция амплитуды, лёгкие гармоники, периодические компоненты, маскирующиеся под другие воздействия. Один из подходов — использовать сверхточную оконную Фурье-анализу (STFT) с высокой временной и частотной разрешающей способностью, затем искать пики, регулярно повторяющиеся в диапазоне, скажем, 0,8–3 Гц (от 48 до 180 ударов в минуту). Но одной спектральной обработки недостаточно — слишком много ложных пиков.
На практике я применяю гибридный подход: сначала узкополосная фильтрация по полосам, потом свёрточный фильтр для удаления резких помех, затем бегущий автокорреляционный анализ. И только затем — глубокая модель, которая уже берет на себя последние шаги. Если попробовать упрощённый псевдокод (на Python + PyTorch), он может выглядеть так:
import torch
import torchaudio
def preprocess(audio: torch.Tensor, sr: int):
# аудио — одномерный тензор
spec = torch.stft(audio, n_fft=2048, hop_length=512, return_complex=True)
mag = spec.abs()
# полоса 0.8–3 Гц в частотной сетке:
freqs = torch.fft.rfftfreq(2048, 1/sr)
mask = (freqs >= 0.8) & (freqs <= 3.0)
narrow = mag[:, mask]
return narrow.log1p()
class PulseNet(torch.nn.Module):
def __init__(self, in_channels, hidden):
super().__init__()
self.conv = torch.nn.Conv1d(in_channels, hidden, kernel_size=5, padding=2)
self.relu = torch.nn.ReLU()
self.lin = torch.nn.Linear(hidden, 1)
def forward(self, x):
# x: B×C×T (C — полосы, T — время фреймы)
h = self.conv(x)
h = self.relu(h)
# глобальный пульс через агрегацию по времени
agg = h.mean(dim=2)
out = self.lin(agg)
return out
# пример использования
wav, sr = torchaudio.load("recording.wav")
narrow = preprocess(wav.mean(dim=0), sr) # моно
narrow = narrow.unsqueeze(0) # batch
model = PulseNet(narrow.size(1), hidden=32)
pulse = model(narrow) # предсказание пульса в уд/мин (с некоторой шкалировкой)
Конечно, это набросок, но иллюстрирует общий pipeline: предобработка + конволюция + агрегатор. При таком подходе модель может «слышать» слабый пульсовый компонент, если он есть.
Интересно: лично я экспериментировал с записью через стену—такая запись почти бессмысленна, но в ряде случаев, при хорошей чувствительности микрофона, модель всё равно угадывала пульс с ошибкой ±5 ударов в минуту. Это своего рода магия, и она зависит от тонкой балансировки архитектур и предобработки.
Архитектуры, которые чувствуют биение
После первых экспериментов я убедился, что простые CNN с усреднением не годятся, если аудио длительное и шумное. Нужно сохранить временные зависимости и позволить модели «следить» за периодичностью, корреляциями и фазовыми сдвигами. Тогда я стал пробовать архитектуры, ориентированные на аудиосигналы: 1D-трансформеры, свёрточные блоки с резидуалами, а также гибриды CNN + LSTM.
Один из подходов, который дал наилучшие результаты — использовать 1D-Transformer (трансформер вдоль временного измерения, действующий на полосы спектра). Он получает вход размерности (batch, полосы, время) и применяет attention внутри каждой полосы и между полосами, чтобы увидеть, как фазы взаимодействуют. На выходе — регрессия пульса либо распределение вероятностей (softmax над диапазоном пульса).
Псевдокод такого блока (PyTorch-ish):
class TimeTransformerBlock(torch.nn.Module):
def __init__(self, dim):
super().__init__()
self.attn = torch.nn.MultiheadAttention(dim, num_heads=4)
self.ff = torch.nn.Sequential(
torch.nn.Linear(dim, dim * 2),
torch.nn.ReLU(),
torch.nn.Linear(dim * 2, dim)
)
self.norm1 = torch.nn.LayerNorm(dim)
self.norm2 = torch.nn.LayerNorm(dim)
def forward(self, x):
# x: T × B × C (transpose из batch × C × T)
y, _ = self.attn(x, x, x)
x = self.norm1(x + y)
z = self.ff(x)
x = self.norm2(x + z)
return x
class PulseTransformer(torch.nn.Module):
def __init__(self, in_channels, dim):
super().__init__()
self.proj = torch.nn.Linear(in_channels, dim)
self.blocks = torch.nn.ModuleList([TimeTransformerBlock(dim) for _ in range(3)])
self.head = torch.nn.Linear(dim, 1)
def forward(self, x):
# x: B × C × T
x = x.permute(2, 0, 1) # T, B, C
x = self.proj(x)
for b in self.blocks:
x = b(x)
# среднее по времени
agg = x.mean(dim=0)
return self.head(agg)
Этот подход позволял модели рассуждать: «если в полосе Х фаза смещается относительно полосы Y с периодом T, возможно, это пульс». То есть внимание помогает связать межполосные корреляции, что чистый CNN не может. Я пробовал разные глубины, разные размеры attention, регулировал маски внимания (например, запрещал ей смотреть слишком далеко назад, чтобы не зацепить глюки).
И знаете что? Иногда модель угадывала не пульс (то, что я ожидал), а частоту дыхания. Это достаточно близко, и смешение этих сигналов — ключевая проблема. Поэтому мотивация разбивать задачи: дышим ли, пульс ли, шум ли — классификатор, который помогает предрешать модель регрессии.
В таких задачах важна не просто архитектура, но регуляризация: я вводил маски внимания, dropout, шумовые вариации на тренировке (добавлял помехи, перемешанные записи, форсированные шумы). И обязательно — контроль ошибок: если модель предсказывает 10 ударов в минуту, это явный сбой — нужно ловить такое экстремальное значение и отбрасывать.
Примеры практического применения и ограничения
Допустим, вы хотите внедрить подобную систему в приложение для мониторинга здоровья. Вы записываете звук через фронтальный микрофон, и модель в фоне выдаёт пульс. Это звучит здорово, но что мешает? Во-первых — разрешение микрофона. На дешёвых смартфонах чувствительность низкая, шумовой порог высок. Во-вторых — акустическое окружение: кафель, эхо, другие люди, шум улицы. В-третьих — отставание: модель должна работать в режиме реального времени, с минимальной задержкой — иначе сигнал устареет.
Я опробовал вариант с окнами длиной 10 секунд, и скользящее обновление каждую секунду. Но при этом часть окна уже старая, и пульс меняется. Решение: использовать overlapping окна и экспоненциальное сглаживание выходов. При этом я сталкивался с «подтасовкой» пульса — когда модель, под возмущениями, медленно дрейфует к среднему значению. Мне пришлось вводить контрмеры: внешние корректоры, которые ограничивают рывки больше 10 уд/мин за секунду.
Я также тестировал кейсы: запись под тканью, через одежду, со спящего человека (тихо), на шумном концерте. На концерте результат был почти бесполезен, но в остальных случаях — средняя ошибка 3–7 ударов, что уже можно признать почти полезным для не медицинского мониторинга. Иногда модель определяла «ритм качания» (качка дороги в автомобиле), а не сердце — и это немного забавно.
Но вот важный момент: такие системы не являются медицинскими устройствами, и требования к точности, стабильности и сертификации абсолютно иные. Моё решение — считать это экспериментальным фичей, приложением пограничного класса, а не заменой пульсометра.
Как дальше развивать — идеи и вызовы
Я хочу поделиться мыслями, куда можно эволюционировать этот подход. Можно пытаться не просто выдавать средний пульс, а строить пульсовую кривую: то есть сигнал пульса во времени с точностью до десятых секунды. Это существенно сложнее, потому что нужно решать задачу синхронизации фаз в каждом моменте.
Другой вектор — мультимодальность: дополнить аудио сейсмическими датчиками, акселерометром, микрофоном тела, ИК-зондом. Тогда модель может сверять показания и выбирать «прослушиваемый» канал. Бывает, я пробовал совмещать аудио + акселерометр (например, телефон в кармане) — в таких сетях attention мог игнорировать смещения тела и делать более стабильные предсказания.
Интересно ещё направление генеративных моделей: можно пытаться предсказывать «якобы пульс» и выдавать реконструкцию аудио, в котором пульс усиливается. Это как обратная задача: проверяешь себя, поправляя. GAN-like подход: генератор пытается вшить пульсовую компоненту, дискриминатор — отличить это от настоящего снятого сигнала. Смешно звучит, но даёт альтернативную регуляризацию.
И последнее, о чем часто думаю: как визуализировать доверие модели? Вот ты получил пульс 70 ударов — но можно ли показать «спектр доверия», «маску внимания», «где модель это увидела»? Для адекватного применения нужны инструменты объяснимости — чтобы пользователь видел: «я в этом месте услышал биение». Я встроил визуализацию attention-масок, чтобы показать, в каких полосах и на каких временных отрезках модель «слышит» пульс. Это не просто красиво — это помощь в диагностике и отладке.
Личный опыт, мысли и вопросы к вам
Когда я впервые работал над прототипом, мне казалось, что это будет просто игрушка для энтузиастов. Но через год я показал результаты знакомым врачам, и они признали: «неплохо для простой камеры». Это дало мне мотивацию — если копнуть глубже, можно сделать что-то полезное.
Я хочу спросить вас: если бы вы работали над этим проектом, какие данные вы бы собирали в первую очередь? На моём опыте — разнообразные записи от разных микрофонов, разной акустики, разного положения тела — именно это помогает обучить устойчивость модели. И ещё: у вас есть идеи как лучше смешивать аудио и другие сенсоры для повышения надёжности?
И да, один лайфхак: не пытайтесь сразу получить точность до десятых — начните с coarse оценки, c шагом 1 удар/мин, и просто убедитесь, что сеть может вообще увидеть пульс в шуме. Потом постепенно усложняйте задачу, усложняя архитектуру и режимы. Это путь моего дизайна: сначала простое, потом более сложное.
Надеюсь, вам было интересно — я не претендую на завершённость, но хотел поделиться тем, что придумал и испробовал. Буду рад, если кто-то продолжит и улучшит.
vigfam
Есть. Можно даже и без аудио. :)
Радиолокация гомодинным методом. Цена железа - смешная.