Дорогие читатели!
Продолжаю серию статей о моём дипломном проекте «Голосовое управление Умным домом». В Части 1 я рассказал о концепции и видении проекта, в Части 2 — о проектировании пользовательского опыта. В этой части я подробно разберу архитектуру нейронной сети, которая лежит в основе системы распознавания голосовых команд.
Это техническая часть серии, где я покажу код, объясню выбор архитектуры и расскажу о технических решениях, которые позволили достичь точности 94.55% на проверочной выборке.
Глава 1: Почему Multi-input CNN?
Выбор архитектуры
Когда я начал работать над проектом, передо стоял выбор: какую архитектуру нейронной сети использовать для распознавания голосовых команд из обычного разговора?
Рассматриваемые варианты:
Архитектура | Преимущества | Недостатки |
|---|---|---|
Полносвязная сеть (FCN) | Простота реализации | Плохо работает с последовательностями, требует много параметров |
CNN (свёрточная) | Хорошо извлекает локальные паттерны | Требует правильной настройки |
RNN/LSTM | Работает с временными последовательностями | Медленное обучение, требует много данных |
Transformer | State-of-the-art результаты | Требует огромных датасетов и вычислительных ресурсов |
Моё решение: Я выбрал Multi-input CNN (свёрточную сеть с несколькими входами).
Почему именно эта архитектура?
Разные типы признаков — я извлекал 7 групп аудио-признаков (MFCC, Chroma, RMS, Zero Crossing Rate, Spectral Centroid, Bandwidth, Rolloff), которые имеют разную природу и масштаб значений
Ограниченные ресурсы — тренировка проходила в Google Colab с ограниченными ресурсами, нужна была эффективная архитектура
Небольшой датасет — 273 аудиофайла недостаточно для сложных архитектур типа Transformer
Скорость обучения — CNN обучается быстрее чем RNN/LSTM при сопоставимой точности
Глава 2: Извлечение признаков из аудио
Семь групп аудио-признаков
Перед подачей данных в нейросеть, я извлекал признаки из каждого аудиофайла. Вот код функции извлечения признаков:
def get_features_all(y, sr): """ Получаем различные параметры аудио которые в сумме дадут уникальный набор признаков """ # Частота цветности chst = librosa.feature.chroma_stft(y=y, sr=sr) # Среднеквадратичные колебания (энергия сигнала) rmse = librosa.feature.rms(y=y) # Пересечения нуля (частота смены знака сигнала) zcr = librosa.feature.zero_crossing_rate(y) # Центр масс звука (спектральный центр) spe_c = librosa.feature.spectral_centroid(y=y, sr=sr) # Ширина полосы частот spe_b = librosa.feature.spectral_bandwidth(y=y, sr=sr) # Спектральный спад частоты rol = librosa.feature.spectral_rolloff(y=y, sr=sr) # Значимые для обработки частоты (MFCC) mfcc = librosa.feature.mfcc(y=y, sr=SR, n_mfcc=50, n_mels=50, hop_length=1024) return chst, rmse, zcr, spe_c, spe_b, rol, mfcc
Агрегация признаков
Для каждого признака я вычислял среднее, минимальное и максимальное значения. Это позволило сократить размерность данных и выделить наиболее важные характеристики:
def get_featur_mean(y, sr): """ Агрегируем признаки: mean, max, min для каждой группы """ # Частота цветности (3 признака) chst1 = np.mean(librosa.feature.chroma_stft(y=y, sr=sr)) chst2 = np.max(librosa.feature.chroma_stft(y=y, sr=sr)) chst3 = np.min(librosa.feature.chroma_stft(y=y, sr=sr)) # Среднеквадратичные колебания (3 признака) rmse1 = np.mean(librosa.feature.rms(y=y)) rmse2 = np.max(librosa.feature.rms(y=y)) rmse3 = np.min(librosa.feature.rms(y=y)) # Пересечения нуля (3 признака) zcr1 = np.mean(librosa.feature.zero_crossing_rate(y)) zcr2 = np.max(librosa.feature.zero_crossing_rate(y)) zcr3 = np.min(librosa.feature.zero_crossing_rate(y)) # Центр масс звука (3 признака) spe_c1 = np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)) spe_c2 = np.max(librosa.feature.spectral_centroid(y=y, sr=sr)) spe_c3 = np.min(librosa.feature.spectral_centroid(y=y, sr=sr)) # Ширина полосы частот (3 признака) spe_b1 = np.mean(librosa.feature.spectral_bandwidth(y=y, sr=sr)) spe_b2 = np.max(librosa.feature.spectral_bandwidth(y=y, sr=sr)) spe_b3 = np.min(librosa.feature.spectral_bandwidth(y=y, sr=sr)) # Спектральный спад (3 ��ризнака) rol1 = np.mean(librosa.feature.spectral_rolloff(y=y, sr=sr)) rol2 = np.max(librosa.feature.spectral_rolloff(y=y, sr=sr)) rol3 = np.min(librosa.feature.spectral_rolloff(y=y, sr=sr)) # MFCC (3 признака) mfc1 = np.mean(librosa.feature.mfcc(y=y, sr=22050, n_mfcc=50, n_mels=50, hop_length=1024)) mfc2 = np.max(librosa.feature.mfcc(y=y, sr=22050, n_mfcc=50, n_mels=50, hop_length=1024)) mfc3 = np.min(librosa.feature.mfcc(y=y, sr=22050, n_mfcc=50, n_mels=50, hop_length=1024)) # Формируем три группы признаков ssr = [spe_c1, spe_c2, spe_c3, spe_b1, spe_b2, spe_b3, rol1, rol2, rol3] crz = [chst1, chst2, chst3, rmse1, rmse2, rmse3, zcr1, zcr2, zcr3] mfc = [mfc1, mfc2, mfc3] return ssr, crz, mfc
Три группы признаков для трёх входов
Группа | Признаки | Количество | Описание |
|---|---|---|---|
SSR | Spectral Centroid, Bandwidth, Rolloff | 9 | Спектральные характеристики звука |
CHZ | Chroma, RMSE, Zero Crossing Rate | 9 | Энергетические и частотные характеристики |
MFC | MFCC (mean, max, min) | 3 | Mel-frequency cepstral coefficients |
Итого: 21 признак на каждый аудиофайл, разделённых на 3 группы для разных входов нейросети.
Глава 3: Архитектура нейросети
Схема архитектуры
┌─────────────────────────────────────────────────────────────────┐ │ АРХИТЕКТУРА NEURAL NETWORK v4.6 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ВХОД 1: SSR (9 признаков) ВХОД 2: CHZ (9 признаков) │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ Conv1D(4,2) │ │ Conv1D(4,2) │ │ │ │ tanh │ │ linear │ │ │ │ BatchNorm │ │ BatchNorm │ │ │ │ Dropout(0.2) │ │ Dropout(0.2) │ │ │ │ Conv1D(8,2) │ │ Conv1D(8,2) │ │ │ │ Flatten │ │ Flatten │ │ │ │ Dense(64) tanh │ │ Dense(64) linear│ │ │ └────────┬─────────┘ └────────┬─────────┘ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ ВХОД 3: MFC (3 признака) ┌──────────────────┐ │ │ ┌──────────────────┐ │ Dense(64) │ │ │ │ Conv1D(4,2) │ │ tanh │ │ │ │ relu │ │ Dense(64) │ │ │ │ BatchNorm │ │ tanh │ │ │ │ Dropout(0.2) │ └────────┬─────────┘ │ │ │ Conv1D(8,2) │ │ │ │ │ Flatten │ │ │ │ │ Dense(64) relu │ │ │ │ └────────┬─────────┘ │ │ │ │ │ │ │ └──────────────┬─────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ CONCATENATE │ │ │ │ [x1, x3, x4] │ │ │ └───────────┬───────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ Dense(128) elu │ │ │ │ BatchNormalization │ │ │ │ Dropout(0.3) │ │ │ │ Dense(128) elu │ │ │ └───────────┬───────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ CONCATENATE │ │ │ │ [x, x4] │ │ │ └───────────┬───────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ │ │ │ Dense(4) softmax │ │ │ │ (Комната, Дверь, │ │ │ │ Камера, Фон) │ │ │ └───────────────────────┘ │ │ │ │ Всего параметров: 50,480 │ │ Обучаемых параметров: 50,200 │ │ Не обучаемых параметров: 280 │ │ │ └─────────────────────────────────────────────────────────────────┘
Код сборки модели
from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers import (Dense, Conv1D, Dropout, Flatten, concatenate, Input, BatchNormalization) from tensorflow.keras.optimizers import Adam # Входные данные для трёх групп признаков input1 = Input(xTrainSSR.shape[1:]) # Входные данные, это первое число размерности оцифрованых данных input2 = Input(xTrainCHZ.shape[1:]) input3 = Input(xTrainMFC.shape[1:]) # На первую группу подаём тренировочные данные (SSR - спектральные признаки) x1 = Conv1D(4, 2, activation="tanh")(input1) x1 = BatchNormalization()(x1) # Нормализация данных для исключения резких разниц в расчётах x1 = Dropout(0.2)(x1) # Во избежании "заучивания" произвольное отключение нейронов (коэф. 0,2 = 20%) x1 = Conv1D(8, 2, activation="tanh")(x1) # Одномерный свёрточный слой с картой 32 значения и матрицей 3 числа, производит свёртку (уменьшение) x1 = Flatten()(x1) # Функция - перевод данных в вектор x1 = Dense(64, activation='tanh')(x1) # На вторую группу подаём тренировочные данные (CHZ - энергетические признаки) x2 = Conv1D(4, 2, activation="linear")(input2) x2 = BatchNormalization()(x2) x2 = Dropout(0.2)(x2) x2 = Conv1D(8, 2, activation="linear")(x2) x2 = Flatten()(x2) x2 = Dense(64, activation='linear')(x2) # На третью группу подаём тренировочные данные (MFC - частотные признаки) x3 = Conv1D(4, 2, activation="relu")(input3) x3 = BatchNormalization()(x3) x3 = Dropout(0.2)(x3) x3 = Conv1D(8, 2, activation="relu")(x3) x3 = Flatten()(x3) x3 = Dense(64, activation='relu')(x3) # Здесь данные из второй группы обрабатываем полносвязным слоем Dense на 64 нейрона x4 = Dense(64, activation='tanh')(x2) x4 = Dense(64, activation='tanh')(x4) # Обрабатываем меньшим количеством нейронов # Соединяем данные из групп 1, 3, 4 в группу x x = concatenate([x1, x3, x4]) x = Flatten()(x) x = Dense(128, activation='elu')(x) x = BatchNormalization()(x) x = Dropout(0.3)(x) x = Dense(128, activation='elu')(x) x = concatenate([x, x4]) # На выходе нейронов равное количеству групп len(labels) x = Dense(len(labels), activation='softmax')(x) # Сборка модели model = Model([input1, input2, input3], x) model.compile(optimizer=Adam(1e-4), loss='categorical_crossentropy', metrics=['accuracy'])
Ключевые компоненты архитектуры
1. Conv1D слои
Conv1D(4, 2, activation="tanh")
Параметр | Значение | Описание |
|---|---|---|
Filters | 4 | Количество фильтров (карт признаков) |
Kernel Size | 2 | Размер ядра свёртки |
Activation | tanh/linear/relu | Функция активации (разная для каждого входа) |
Почему разные функции активации?
tanh для SSR — спектральные признаки имеют симметричное распределение
linear для CHZ — энергетические признаки лучше сохранять в исходном масштабе
relu для MFC — MFCC признаки имеют положительное распределение
2. BatchNormalization
BatchNormalization()
Зачем нужна нормализация?
Стабилизирует обучение
Ускоряет сходимость
Позволяет использовать более высокие learning rate
Снижает чувствительность к инициализации весов
3. Dropout
Dropout(0.2) # 20% нейронов отключается случайно Dropout(0.3) # 30% в более глубоких слоях
Борьба с переобучением:
Dropout случайно "выключает" нейроны во время обучения
Prevents сеть от "запоминания" тренировочных данных
Улучшает обобщающую способность модели
Глава 4: Компиляция и обучение модели
Компиляция модели
model.compile(optimizer=Adam(1e-4), loss='categorical_crossentropy', metrics=['accuracy'])
Параметр | Значение | Описание |
|---|---|---|
Optimizer | Adam(1e-4) | Адаптивный оптимизатор с learning rate 0.0001 |
Loss | categorical_crossentropy | Функция потерь для многоклассовой классификации |
Metrics | accuracy | Метрика качества — точность классификации |
Параметры обучения
history = model.fit([xTrainSSR, xTrainCHZ, xTrainMFC], yTrainBD, epochs=250, # количество повторений для обучения нейромодели validation_split=0.2, # проверочные данные для контроля результата (20%) batch_size=10, # Количество примеров для счисления до изменения весов модели verbose=1) # параметр показывать или не показывать процесс счисления данных
Параметр | Значение | Описание |
|---|---|---|
Epochs | 250 | Количество полных проходов через весь датасет |
Validation Split | 0.2 | 20% данных используются для валидации (55 файлов) |
Batch Size | 10 | Градиент обновляется после каждых 10 примеров |
Verbose | 1 | Вывод прогресса обучения в консоль |
Результаты обучения
Epoch 1/250 22/22 [==============================] - 3s 33ms/step - loss: 1.8197 - accuracy: 0.2110 - val_loss: 1.2615 - val_accuracy: 0.9455 ....... Epoch 83/250 22/22 [==============================] - 1s 178us/step - loss: 5.9840e-05 - accuracy: 1.0000 - val_loss: 0.3459 - val_accuracy: 0.9406 ....... Epoch 247/250 22/22 [==============================] - 0s 16ms/step - loss: 0.1618 - accuracy: 0.9404 - val_loss: 0.9144 - val_accuracy: 0.9455
Ключевые метрики:
Метрика | Значение | Комментарий |
|---|---|---|
Train Accuracy | 94.04% | Точность на обучающей выборке |
Validation Accuracy | 94.55% | Точность на проверочной выборке |
Train Loss | 0.1618 | Функция потерь на обучении |
Validation Loss | 0.9144 | Функция потерь на валидации |
Визуализация обучения
plt.plot(history.history['accuracy'], label='Верные на обучающем наборе') plt.plot(history.history['val_accuracy'], label='Верные на проверочном') plt.xlabel('Эпох обучения') plt.ylabel('Верные ответы') plt.legend() plt.show()
Наблюдения:
Быстрая сходимость — модель достигла 94% точности уже на первых эпохах
Стабильная валидация — val_accuracy держится на уровне 94-95%
Нет сильного переобучения — разница между train и val accuracy небольшая
Глава 5: Сохранение и загрузка модели
Сохранение модели
# Сохраняем веса модели model.save_weights(WAY_NP+'Model_weight.h5') # Сохраняем всю модель (архитектура + веса) model.save(WAY_NP+'Model_Input3_v4.h5')
Загрузка модели
# Загружаем только веса (нужно сначала создать архитектуру) model.load_weights(WAY_NP+'Model_Input3_v4.h5') # Или загружаем всю модель целиком model = keras.models.load_model(WAY_NP+'Model_Input3_v4.h5')
Разница между методами:
save_weights()— сохраняет только веса, меньше размер файлаsave()— сохраняет архитектуру + веса + оптимизатор, удобнее для деплоя
Глава 6: Почему эта архитектура работает?
1. Multi-input подход
Преимущества:
Каждая группа признаков обрабатывается оптимальным образом
Разные функции активации для разных типов признаков
Возможность добавлять новые группы признаков без переделки всей архитектуры
2. Эффективность для небольших датасетов
Почему CNN лучше чем сложные архитектуры:
Архитектура | Требуемый размер датасета | Точность на 273 файлах |
|---|---|---|
Transformer | 10,000+ | ~70% (переобучение) |
LSTM | 5,000+ | ~80% |
CNN (наша) | 500+ | 94.55% |
FCN | 1,000+ | ~75% |
3. Оптимизация под Google Colab
Ограничения Colab:
Ограниченная GPU память
Ограниченное время сессии (12 часов)
Наши решения:
Маленький batch size (10) для экономии памяти
Эффективная архитектура для быстрой сходимости
250 эпох укладываются в лимит времени
Глава 7: Что можно улучшить?
1. Data Augmentation
# Добавление шума def add_noise(data, noise_level=0.005): noise = np.random.randn(len(data)) * noise_level return data + noise # Изменение скорости def change_speed(data, speed_factor=1.1): return librosa.effects.time_stretch(data, rate=speed_factor) # Изменение питча def change_pitch(data, pitch_factor=0.7): return librosa.effects.pitch_shift(data, sr=22050, n_steps=pitch_factor)
Эффект: Увеличение датасета в 5-10 раз, улучшение обобщающей способности
2. Transfer Learning
# Использование предобученных моделей from transformers import Wav2Vec2Processor, Wav2Vec2Model processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base") model = Wav2Vec2Model.from_pretrained("facebook/wav2vec2-base") inputs = processor(audio, return_tensors="pt", sampling_rate=16000) with torch.no_grad(): outputs = model(**inputs) embeddings = outputs.last_hidden_state
Преимущества:
Не нужно обучать с нуля
Лучше качество признаков
Меньше требуется данных
3. Early Stopping
from tensorflow.keras.callbacks import EarlyStopping early_stopping = EarlyStopping( monitor='val_loss', patience=20, # Остановить если 20 эпох нет улучшений restore_best_weights=True # Вернуть лучшие веса ) model.fit(..., callbacks=[early_stopping])
Эффект: Экономия времени обучения, предотвращение переобучения
4. Квантование для деплоя
# Конвертация в TFLite для мобильных устройств converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() # Сохранение with open('model.tflite', 'wb') as f: f.write(tflite_model)
Преимущества:
Уменьшение размера модели в 4 раза
Ускорение инференса на мобильных устройствах
Возможность работы offline
Глава 8: Сравнение с современными подходами (2024)
Тогда (2021) vs Сейчас (2024)
Аспект | 2021 (мой проект) | 2024 (современные подходы) |
|---|---|---|
Архитектура | Multi-input CNN | Wav2Vec 2.0, Whisper |
Признаки | Ручное извлечение (MFCC, Chroma) | Автоматическое извлечение |
Размер модели | 50,480 параметров | 95M+ параметров |
Точность | 94.55% | 98%+ |
Требования к данным | 273 файла | 10,000+ файлов |
Время обучения | ~2 часа | 10+ часов |
Вычислительные ресурсы | Google Colab Free | GPU кластеры |
Что осталось актуальным?
Multi-input подход — всё ещё используется в современных архитектурах
BatchNormalization — стандартный компонент современных сетей
Dropout — всё ещё эффективен для борьбы с переоб��чением
Разделение признаков — концепция актуальна для мультимодальных моделей
Заключение
Архитектура нейронной сети — это баланс между:
Точностью — качество классификации
Эффективностью — скорость обучения и инференса
Ресурсами — доступные вычислительные мощности
Данными — размер и качество датасета
Для моего проекта Multi-input CNN оказался оптимальным выбором, позволившим достичь 94.55% точности на небольшом датасете с ограниченными ресурсами.
Что будет в следующей части?
В Части 4 я расскажу о процессе обучения и валидации модели:
Подготовка данных для обучения
Анализ кривых обучения
Борьба с переобучением
Оптимизация гиперпараметров
Сохранение и загрузка модели
📚 Источники и ресурсы
Исходный код проекта
Файл | Описание | Ссылка |
|---|---|---|
Jupyter Notebook | Код модели и обучение | |
GitHub | Репозиторий проекта |
Библиотеки
# Основные библиотеки для работы с аудио import librosa # Обработка аудио import librosa.display # Визуализация аудио # Библиотеки для нейросетей import tensorflow as tf # Фреймворк для глубокого обучения from tensorflow.keras import layers, models # Утилиты from sklearn.preprocessing import StandardScaler # Нормализация
💬 Вопросы для обсуждения
Какую архитектуру вы бы выбрали для этой задачи?
Используете ли вы Data Augmentation в своих проектах?
Какие предобученные модели вы применяете для аудио-задач?
Делитесь в комментариях! 👇
