«Нейросеть — не чёрный ящик. Это конструктор. Ты можешь собрать его сам»
О чём эта глава
Всё, что мы будем делать в этой книге, строится на одном фундаменте: искусственных нейронных сетях. Шумоподавление, разделение источников, синтез речи, детектор эмоций — все эти впечатляющие технологии начинаются с одной простой идеи. С идеи о том, что компьютер может учиться на примерах. Не выполнять заранее написанные инструкции, а самостоятельно находить закономерности в данных.
В этой главе мы начнём с самого простого — с одного искусственного нейрона. Мы поймём, как он работает, как принимает решения и, главное, как он учится. Мы напишем нейрон на чистом Python, без единой строчки из библиотек глубокого обучения. Мы научим его различать громкий и тихий звук — задача простая, но она даст нам понимание механики обучения.
Затем мы соберём из нейронов полносвязную сеть. Мы узнаем, что такое скрытые слои, функции активации и обратное распространение ошибки. Мы познакомимся с PyTorch — фреймворком, который станет нашим основным инструментом на всю книгу. Мы напишем первую настоящую нейросеть, которая будет классифицировать аудиофрагменты — отличать речь от музыки, а музыку от шума.
К концу этой главы у вас будет работающая модель классификации звуков и, что важнее, понимание того, как она устроена внутри. Не магия. Математика и код.
Зачем нейросети звук
Прежде чем погружаться в нейроны и слои, давайте ответим на простой вопрос: зачем вообще применять нейросети к звуку? У нас уже есть классические алгоритмы. Фильтры. Свёртка. Спектральное вычитание. Они работают. Они понятны. Они не требуют обучающих данных и видеокарт. Зачем усложнять?
Представьте, что вы хотите написать программу, которая отличает лай собаки от плача ребёнка. С помощью классических алгоритмов вы бы пошли примерно таким путём. Вы бы записали десятки примеров лая и плача. Вы бы посмотрели на их спектрограммы. Вы бы заметили, что у лая есть характерные паттерны — короткие всплески в среднем диапазоне частот, у плача — более продолжительные звуки с модуляцией высоты. Вы бы придумали правила: если средняя частота выше X и длительность меньше Y, то это лай. Если есть периодическая модуляция с частотой Z, то это плач.
Этот подход работает. Но у него есть проблемы. Каждый новый звук требует нового набора правил. Плач одного ребёнка может сильно отличаться от плача другого. Лай таксы не похож на лай овчарки. Шум дождя можно спутать с шипением радио. Вам придётся придумывать и настраивать правила для каждого случая, и в какой-то момент сложность станет неуправляемой.
Нейросеть решает эту проблему иначе. Вы не придумываете правила. Вы собираете dataset — набор примеров: тысячу записей лая, тысячу записей плача, тысячу записей шума. Вы показываете их нейросети, и она сама находит закономерности, которые отличают один класс от другого. Ей не нужны формулы для средней частоты и длительности. Она сама выяснит, какие признаки важны, а какие нет.
Более того, нейросеть может найти закономерности, которые человек вообще не замечает. Мелкие детали в фазовом спектре. Тонкие корреляции между далёкими частотами. Паттерны, которые не видны на спектрограмме невооружённым глазом. Именно поэтому нейросетевое шумоподавление работает лучше классического, а нейросетевой синтез речи звучит естественнее, чем механическая сборка из фонем.
Один нейрон: модель, которая учится
Давайте начнём с самого простого строительного блока нейросети — искусственного нейрона. Биологический нейрон получает сигналы от других нейронов через дендриты, обрабатывает их в теле клетки и, если суммарный сигнал достаточно силён, посылает импульс по аксону. Искусственный нейрон работает похоже, но проще.
Искусственный нейрон получает на вход несколько чисел. Он умножает каждое число на свой вес — параметр, который определяет важность этого входа. Затем он суммирует все взвешенные входы, добавляет смещение — ещё один параметр — и применяет к результату функцию активации. Функция активации решает, насколько сильным будет выход нейрона. Если взвешенная сумма большая и положительная — выход близок к единице. Если маленькая или отрицательная — к нулю.
Формула нейрона предельно проста:
y = f(w1·x1 + w2·x2 + ... + wn·xn + b)
где x1, x2 — входы, w1, w2 — веса, b — смещение, f — функция активации, y — выход.
Веса и смещение — это то, что нейрон настраивает в процессе обучения. В начале они случайные, и нейрон выдаёт бессмысленный результат. Но у нас есть обучающие примеры — входы и правильные ответы для них. Мы подаём вход, смотрим на выход нейрона, сравниваем с правильным ответом и вычисляем ошибку. Затем мы чуть-чуть меняем веса так, чтобы ошибка уменьшилась. Повторяем тысячи раз. Постепенно нейрон учится давать правильные ответы.
Пишем нейрон на Python
Давайте реализуем эту идею в коде. Мы создадим класс для одного нейрона и научим его решать простейшую задачу: определять, громкий звук или тихий.
python
import numpy as np
class SingleNeuron:
"""
Один искусственный нейрон.
Принимает один входной признак и выдаёт бинарное предсказание.
"""
def __init__(self):
# Инициализируем вес и смещение случайными малыми значениями
self.weight = np.random.randn() * 0.1
self.bias = np.random.randn() * 0.1
def sigmoid(self, x):
"""Сигмоидная функция активации: превращает любое число в значение от 0 до 1."""
return 1.0 / (1.0 + np.exp(-x))
def forward(self, x):
"""
Прямой проход: вычисляет выход нейрона по входу x.
x — одно число (например, средняя громкость аудиофрагмента).
"""
z = self.weight * x + self.bias
return self.sigmoid(z)
def train_step(self, x, y_true, learning_rate=0.1):
"""
Один шаг обучения.
x — входное значение
y_true — правильный ответ (0 или 1)
learning_rate — скорость обучения
"""
# Прямой проход
y_pred = self.forward(x)
# Вычисляем ошибку (разница между предсказанием и истиной)
error = y_pred - y_true
# Вычисляем градиенты (насколько нужно изменить параметры)
# Для сигмоиды: производная = y_pred * (1 - y_pred)
d_weight = error * y_pred * (1 - y_pred) * x
d_bias = error * y_pred * (1 - y_pred)
# Обновляем параметры
self.weight -= learning_rate * d_weight
self.bias -= learning_rate * d_bias
return error
# Создаём нейрон
neuron = SingleNeuron()
# Готовим обучающие данные
# Представим, что мы измерили среднюю громкость нескольких аудиофрагментов
# Значения около 0.8 — громкий звук (метка 1)
# Значения около 0.2 — тихий звук (метка 0)
train_data = [
(0.15, 0.0), (0.22, 0.0), (0.18, 0.0), (0.25, 0.0), (0.10, 0.0), # тихие
(0.75, 1.0), (0.82, 1.0), (0.70, 1.0), (0.88, 1.0), (0.79, 1.0), # громкие
]
print("Обучение одного нейрона:")
print("Эпоха | Вес | Смещение | Пример | Предсказание | Истина | Ошибка")
print("-" * 70)
# Обучаем нейрон
for epoch in range(200):
total_error = 0
for x, y_true in train_data:
y_pred = neuron.forward(x)
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)
О проекте
О подписке
Другие проекты