Читать книгу «Python в аудио-спецэффектах. Как работают нейросети изнутри.» онлайн полностью📖 — Александр Иванов — MyBook.

cumsum = np.cumsum(positive_spectrum ** 2)

if cumsum[-1] > 0:

rolloff_idx = np.where(cumsum >= 0.85 * cumsum[-1])[0][0]

rolloff = positive_freqs[rolloff_idx]

else:

rolloff = 0

features.append([rms, zcr, centroid / 1000, rolloff / 1000])

return np.array(features, dtype=np.float32)

class SoundClassifier(nn.Module):

"""

Полносвязная сеть для классификации звуков.

4 входных признака -> 16 скрытых нейронов -> 8 скрытых нейронов -> 3 класса

"""

def __init__(self):

super().__init__()

self.fc1 = nn.Linear(4, 16) # входной слой -> скрытый слой 1

self.fc2 = nn.Linear(16, 8) # скрытый слой 1 -> скрытый слой 2

self.fc3 = nn.Linear(8, 3) # скрытый слой 2 -> выходной слой (3 класса)

self.relu = nn.ReLU() # функция активации ReLU

self.softmax = nn.Softmax(dim=1) # для получения вероятностей

def forward(self, x):

x = self.relu(self.fc1(x))

x = self.relu(self.fc2(x))

x = self.softmax(self.fc3(x))

return x

# Создаём модель, функцию потерь и оптимизатор

model = SoundClassifier()

criterion = nn.CrossEntropyLoss()

optimizer = optim.Adam(model.parameters(), lr=0.001)

print("Модель создана:")

print(model)

print(f"\nКоличество параметров: {sum(p.numel() for p in model.parameters())}")

Наша сеть принимает четыре признака для каждого аудиофрейма: среднюю громкость, частоту пересечений нуля, спектральный центроид и спектральный спад. Частота пересечений нуля показывает, насколько сигнал колеблется — у шума она высокая, у музыки средняя, у речи низкая. Спектральный центроид показывает, где сосредоточена энергия спектра — у шипящих звуков он высокий, у басовых — низкий. Спектральный спад показывает, насколько спектр сконцентрирован — у тональных звуков он низкий, у шумовых — высокий.

Сеть состоит из трёх слоёв. Первый слой принимает четыре признака и выдаёт шестнадцать скрытых представлений. Второй сжимает их до восьми. Третий выдаёт три числа — вероятности для каждого класса: речь, музыка, шум. Функция активации ReLU оставляет положительные значения без изменений и обнуляет отрицательные — это простая нелинейность, которая хорошо работает на практике.

Обучение классификатора

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

python

def generate_synthetic_data(n_samples=500):

"""

Генерирует синтетические данные для демонстрации обучения.

В реальном проекте здесь была бы загрузка реальных аудиофайлов.

"""

np.random.seed(42)

data = []

labels = []

for _ in range(n_samples):

# Класс 0: Речь

data.append([

np.random.normal(0.3, 0.1), # rms: умеренная громкость

np.random.normal(0.15, 0.05), # zcr: низкая частота пересечений нуля

np.random.normal(0.8, 0.2), # centroid: низкий центроид

np.random.normal(0.6, 0.3), # rolloff: умеренный спад

])

labels.append(0)

# Класс 1: Музыка

data.append([

np.random.normal(0.4, 0.15), # rms: средняя громкость

np.random.normal(0.25, 0.08), # zcr: средняя

np.random.normal(1.5, 0.5), # centroid: средний

np.random.normal(2.0, 1.0), # rolloff: выше

])

labels.append(1)

# Класс 2: Шум

data.append([

np.random.normal(0.2, 0.1), # rms: низкая громкость

np.random.normal(0.45, 0.1), # zcr: высокая

np.random.normal(2.5, 1.0), # centroid: высокий

np.random.normal(3.0, 1.5), # rolloff: высокий

])

labels.append(2)

# Перемешиваем

data = np.array(data, dtype=np.float32)

labels = np.array(labels)

shuffle_idx = np.random.permutation(len(data))

return data[shuffle_idx], labels[shuffle_idx]

# Генерируем данные

X, y = generate_synthetic_data(500)

# Разделяем на обучающую и тестовую выборки

split = int(0.8 * len(X))

X_train, X_test = X[:split], X[split:]

y_train, y_test = y[:split], y[split:]

# Преобразуем в тензоры PyTorch

X_train_tensor = torch.tensor(X_train)

y_train_tensor = torch.tensor(y_train, dtype=torch.long)

X_test_tensor = torch.tensor(X_test)

y_test_tensor = torch.tensor(y_test, dtype=torch.long)

print(f"Обучающих примеров: {len(X_train)}")

print(f"Тестовых примеров: {len(X_test)}")

# Обучаем модель

model = SoundClassifier()

criterion = nn.CrossEntropyLoss()

optimizer = optim.Adam(model.parameters(), lr=0.01)

num_epochs = 200

for epoch in range(num_epochs):

# Прямой проход

outputs = model(X_train_tensor)

loss = criterion(outputs, y_train_tensor)

# Обратный проход и оптимизация

optimizer.zero_grad()

loss.backward()

optimizer.step()

if epoch % 40 == 0 or epoch == num_epochs - 1:

# Оцениваем точность на тестовой выборке

with torch.no_grad():

test_outputs = model(X_test_tensor)

_, predicted = torch.max(test_outputs, 1)

accuracy = (predicted == y_test_tensor).float().mean()

print(f"Эпоха {epoch:3d}: Потери = {loss.item():.4f}, Точность = {accuracy:.2%}")

print("\nОбучение завершено!")

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

Разберём, что происходит при обучении. criterion = nn.CrossEntropyLoss() — функция потерь, которая измеряет, насколько предсказания модели отличаются от правильных ответов. optimizer = optim.Adam(model.parameters(), lr=0.01) — оптимизатор Adam, который сам подбирает величину шага для каждого параметра. loss.backward() — магия PyTorch: автоматически вычисляет градиенты для всех параметров модели. optimizer.step() — обновляет параметры в направлении уменьшения ошибки.

Применяем модель к реальному звуку

Теперь давайте проверим нашу модель на реальном аудиофайле. Мы загрузим запись, извлечём признаки и посмотрим, какие классы модель предсказывает для разных фреймов.

python

def classify_audio_file(filepath, model, label_names):

"""Классифицирует аудиофайл по фреймам."""

y, sr = librosa.load(filepath, sr=22050, mono=True)

features = extract_features(y, sr)

# Преобразуем в тензор

features_tensor = torch.tensor(features)

# Предсказываем

with torch.no_grad():

outputs = model(features_tensor)

_, predicted = torch.max(outputs, 1)

# Считаем статистику

unique, counts = np.unique(predicted.numpy(), return_counts=True)

total = len(predicted)

print(f"\nАнализ файла: {filepath}")

print(f" Всего фреймов: {total}")

for cls, count in zip(unique, counts):

print(f" {label_names[cls]}: {count} фреймов ({count / total:.1%})")

return predicted.numpy()

# Классифицируем тестовый файл (если есть) или создадим синтетический

label_names = ['Речь', 'Музыка', 'Шум']

# Создадим тестовый сигнал: первые 2 секунды — тон (музыка), затем шум, затем речь из файла

sr = 22050

t = np.linspace(0, 4.0, int(sr * 4.0), endpoint=False)

test_signal = np.zeros_like(t)

# Музыка: синусоида с обертонами

test_signal[:int(1.5 * sr)] = (np.sin(2 * np.pi * 440 * t[:int(1.5 * sr)]) +

0.5 * np.sin(2 * np.pi * 880 * t[:int(1.5 * sr)]))

# Шум

test_signal[int(1.5 * sr):int(3.0 * sr)] = np.random.randn(int(1.5 * sr)) * 0.3

# Речь (если есть файл) или ещё один тон другой частоты

try:

speech, _ = librosa.load('voice_sample.wav', sr=sr, mono=True)

test_signal[int(3.0 * sr):] = speech[:len(test_signal) - int(3.0 * sr)]

except:

test_signal[int(3.0 * sr):] = np.sin(2 * np.pi * 220 * t[int(3.0 * sr):])

import soundfile as sf

sf.write('test_mixed.wav', test_signal, sr)

print("Создан test_mixed.wav — смесь музыки, шума и речи")

# Классифицируем

predictions = classify_audio_file('test_mixed.wav', model, label_names)

Запустите код и посмотрите на результат. Модель должна правильно определить: первые фреймы — музыка, средние — шум, последние — речь. Это базовая классификация, но принцип, который мы здесь использовали, лежит в основе всех нейросетевых систем обработки звука.

За кулисами: история перцептрона

Первый искусственный нейрон — перцептрон — был предложен Фрэнком Розенблаттом в 1958 году. Это было аналоговое устройство размером с комнату, которое использовало резисторы и потенциометры для настройки весов. Розенблатт верил, что перцептроны станут основой искусственного интеллекта.

В 1969 году Марвин Минский и Сеймур Пейперт опубликовали книгу, в которой математически доказали, что однослойный перцептрон не может решить даже простейшую логическую задачу XOR — исключающее ИЛИ. Это доказательство охладило интерес к нейросетям на десятилетие — период, известный как «зима искусственного интеллекта».

Возрождение началось в 1980-х, когда Джеффри Хинтон и другие исследователи показали, что многослойные сети с нелинейными функциями активации способны решать XOR и множество других задач. Ключевым стало изобретение алгоритма обратного распространения ошибки, который позволял эффективно обучать глубокие сети. Сегодня тот самый XOR, который убил перцептрон, решается сетью из трёх нейронов за доли секунды.

Лаборатория ошибок

Модель не обучается, потери не уменьшаются. Проверьте скорость обучения. Слишком большая — модель «перепрыгивает» минимум и расходится. Слишком маленькая — обучение идёт слишком медленно. Попробуйте значения 0.1, 0.01, 0.001. Для Adam хороший стартовый диапазон — 0.001–0.0001.

Модель отлично работает на обучающих данных, но плохо на тестовых. Это переобучение. Модель запомнила обучающие примеры вместо того, чтобы выучить общие закономерности. Решения: уменьшить количество нейронов, добавить регуляризацию, увеличить количество обучающих данных.

Точность на одном классе высокая, на других — низкая. Данные несбалансированы. Если речевых примеров в десять раз больше, чем шумовых, модель будет предсказывать речь чаще. Решение: сбалансировать выборку или использовать взвешенную функцию потерь.

При попытке обучить модель на CPU всё работает очень медленно. Для маленьких моделей и небольших данных процессора достаточно. Если данных много, используйте GPU: в PyTorch это делается переносом модели и данных на устройство командой .to('cuda').

Творческое задание

Запишите три типа звуков: речь, музыку и шум — по несколько минут каждого. Извлеките из них реальные признаки с помощью extract_features. Обучите модель на реальных данных и сравните точность с синтетическими данными. Насколько реальные данные сложнее?

Добавьте четвёртый класс — например, тишину или звук природы. Измените модель так, чтобы она классифицировала четыре класса. Понадобится ли изменить архитектуру или достаточно просто переобучить выходной слой?

Визуализируйте границы решений. Возьмите два признака (например, RMS и спектральный центроид), обучите модель только на них и постройте график: точки разных цветов для разных классов и цветной фон, показывающий предсказания модели. Вы увидите, где модель ошибается и почему.


В следующей главе мы перейдём от абстрактных признаков к главному инструменту нейросетевой обработки звука — спектрограмме. Мы узнаем, почему звук для нейросети — это картинка, и как свёрточные сети, придуманные для анализа изображений, стали основой современных аудиоалгоритмов.