Александр Иванов – Python в аудио-спецэффектах. Как работают нейросети изнутри. (страница 5)
error = neuron.train_step(x, y_true, learning_rate=0.5)
total_error += abs(error)
if epoch % 50 == 0 or epoch < 10:
x_example, y_example = train_data[0]
y_pred_example = neuron.forward(x_example)
print(f"{epoch:5d} | {neuron.weight:+.3f} | {neuron.bias:+.3f} | "
f"{x_example:.2f} | {y_pred_example:.4f} | {y_example:.1f} | "
f"{abs(y_pred_example - y_example):.4f}")
# Проверяем обученный нейрон
print("\nРезультаты после обучения:")
test_data = [0.12, 0.30, 0.65, 0.90, 0.20, 0.85]
for x in test_data:
pred = neuron.forward(x)
label = "ГРОМКИЙ" if pred > 0.5 else "ТИХИЙ"
print(f" Громкость: {x:.2f} -> {pred:.4f} ({label})")
Запустите этот код. Вы увидите, как в начале обучения нейрон выдаёт случайные предсказания — примерно 0.5 для любого входа. Постепенно вес и смещение подстраиваются. К концу обучения нейрон уверенно отличает тихие звуки от громких: для значений около 0.2 предсказание близко к нулю, для значений около 0.8 — близко к единице.
Разберём ключевые моменты. Сигмоидная функция активации превращает любое число в значение между нулём и единицей. Большое положительное число на входе даёт значение, близкое к единице. Большое отрицательное — близкое к нулю. Это удобно для бинарной классификации: мы интерпретируем выход как вероятность того, что вход принадлежит классу «1».
Обучение происходит через корректировку веса и смещения. Мы вычисляем ошибку — разницу между предсказанием и правильным ответом. Затем вычисляем градиенты — насколько нужно изменить параметры, чтобы ошибка уменьшилась. И делаем маленький шаг в направлении, противоположном градиенту. Это называется градиентным спуском.
От нейрона к сети
Один нейрон может решать только очень простые задачи — разделять данные прямой линией. В реальном мире границы между классами гораздо сложнее. Граница между речью и музыкой, между эмоцией радости и эмоцией грусти, между чистым голосом и зашумлённым — всё это нелинейные, запутанные границы, которые один нейрон не может описать.
Решение — объединить нейроны в сеть. В полносвязной сети нейроны организованы в слои. Каждый нейрон первого слоя получает входные данные. Каждый нейрон второго слоя получает выходы всех нейронов первого слоя. Каждый нейрон третьего — выходы второго. И так далее. Между слоями применяются нелинейные функции активации, и именно эти нелинейности позволяют сети описывать сложные границы.
Обучение сети происходит так же, как обучение одного нейрона, но с одним усложнением: ошибку нужно распространить от выходного слоя к входному через все промежуточные слои. Это называется обратным распространением ошибки. Мы вычисляем, как ошибка на выходе зависит от каждого веса в сети, и обновляем все веса одновременно в направлении уменьшения ошибки.
Классификация звуков: речь, музыка, шум
Давайте применим полносвязную сеть к реальной задаче классификации звуков. Мы научим сеть отличать речь от музыки и шума по набору признаков, извлечённых из аудиофрагментов. Для этого мы воспользуемся PyTorch — фреймворком, который берёт на себя вычисление градиентов и обновление параметров.
python
import torch
import torch.nn as nn
import torch.optim as optim
import librosa
import numpy as np
def extract_features(y, sr, frame_size=2048, hop_length=512):
"""
Извлекает признаки из аудиосигнала для подачи в нейросеть.
Возвращает список векторов признаков для каждого фрейма.
"""
features = []
# Разбиваем на фреймы
num_frames = (len(y) - frame_size) // hop_length + 1
for i in range(num_frames):
start = i * hop_length
frame = y[start:start + frame_size]
# Признак 1: RMS энергия (средняя громкость)
rms = np.sqrt(np.mean(frame ** 2))
# Признак 2: Zero-crossing rate (частота пересечений нуля)
zcr = np.sum(np.abs(np.diff(np.sign(frame)))) / (2 * len(frame))
# Признак 3: Спектральный центроид (центр тяжести спектра)
spectrum = np.abs(np.fft.fft(frame * np.hamming(len(frame))))
freqs = np.fft.fftfreq(len(frame), 1 / sr)
positive_freqs = freqs[:len(freqs)//2]
positive_spectrum = spectrum[:len(spectrum)//2]
if np.sum(positive_spectrum) > 0:
centroid = np.sum(positive_freqs * positive_spectrum) / np.sum(positive_spectrum)
else:
centroid = 0
# Признак 4: Спектральный спад (частота, ниже которой 85% энергии)
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):
"""