Иногда кажется, что звук — лишь колебания воздуха, но что если за ним скрывается нечто большее — биения сердца, ритмы эмоций, немые сигналы тела? В этой статье я расскажу о том, как современные архитектуры нейросетей могут «слышать» сердце — буквально и метафорически. Я подниму вопросы предобработки, особенностей модели, шума физиологических сигналов, покажу примеры кода и реальные кейсы. Для тех, кто уже имеет дело с нейросетями и аудиосигналами — будет что обсудить.

Скрытый сигнал под шумом

Когда я впервые задумался над задачей извлечения пульса из микрофона смартфона, мне казалось: ну вот, возьмём аудиодорожку, отфильтруем ошибочные частоты, применим модель — и всё. Наивно, скажете вы? Да, я и сам через это прошёл. В нашем распоряжении — сигнал с массой шумов: дыхание, речь, фоновые звуки, шёпот ветра, металлическое эхо. Как отделить лишь сердце?

Сначала надо задуматься: на каком уровне этот пульс вообще проявляется? Это не ультразвук, не что-то очевидное. Это слабая модуляция амплитуды, лёгкие гармоники, периодические компоненты, маскирующиеся под другие воздействия. Один из подходов — использовать сверхточную оконную Фурье-анализу (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 удар/мин, и просто убедитесь, что сеть может вообще увидеть пульс в шуме. Потом постепенно усложняйте задачу, усложняя архитектуру и режимы. Это путь моего дизайна: сначала простое, потом более сложное.

Надеюсь, вам было интересно — я не претендую на завершённость, но хотел поделиться тем, что придумал и испробовал. Буду рад, если кто-то продолжит и улучшит.

Комментарии (1)


  1. vigfam
    10.10.2025 13:17

    у вас есть идеи как лучше смешивать аудио и другие сенсоры для повышения надёжности?

    Есть. Можно даже и без аудио. :)

    Радиолокация гомодинным методом. Цена железа - смешная.