- BrainTools - https://www.braintools.ru -
Дорогие читатели!
Продолжаю серию статей о моём дипломном проекте «Голосовое управление Умным домом». В Части 1 [1] я рассказал о концепции и видении проекта, в Части 2 [1] — о проектировании пользовательского опыта [2]. В этой части я подробно разберу архитектуру нейронной сети, которая лежит в основе системы распознавания голосовых команд.
Это техническая часть серии, где я покажу код, объясню выбор архитектуры и расскажу о технических решениях, которые позволили достичь точности 94.55% на проверочной выборке.
Когда я начал работать над проектом, передо стоял выбор: какую архитектуру нейронной сети использовать для распознавания голосовых команд из обычного разговора?
Рассматриваемые варианты:
|
Архитектура |
Преимущества |
Недостатки |
|---|---|---|
|
Полносвязная сеть (FCN) |
Простота реализации |
Плохо работает с последовательностями, требует много параметров |
|
CNN (свёрточная) |
Хорошо извлекает локальные паттерны |
Требует правильной настройки |
|
RNN/LSTM |
Работает с временными последовательностями |
Медленное обучение [3], требует много данных |
|
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 при сопоставимой точности
Перед подачей данных в нейросеть, я извлекал признаки из каждого аудиофайла. Вот код функции извлечения признаков:
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 группы для разных входов нейросети.
┌─────────────────────────────────────────────────────────────────┐
│ АРХИТЕКТУРА 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'])
Conv1D(4, 2, activation="tanh")
|
Параметр |
Значение |
Описание |
|---|---|---|
|
Filters |
4 |
Количество фильтров (карт признаков) |
|
Kernel Size |
2 |
Размер ядра свёртки |
|
Activation |
tanh/linear/relu |
Функция активации (разная для каждого входа) |
Почему разные функции активации?
tanh для SSR — спектральные признаки имеют симметричное распределение
linear для CHZ — энергетические признаки лучше сохранять в исходном масштабе
relu для MFC — MFCC признаки имеют положительное распределение
BatchNormalization()
Зачем нужна нормализация?
Стабилизирует обучение
Ускоряет сходимость
Позволяет использовать более высокие learning rate
Снижает чувствительность к инициализации весов
Dropout(0.2) # 20% нейронов отключается случайно
Dropout(0.3) # 30% в более глубоких слоях
Борьба с переобучением:
Dropout случайно “выключает” нейроны [5] во время обучения
Prevents сеть от “запоминания” тренировочных данных
Улучшает обобщающую способность модели
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 небольшая
# Сохраняем веса модели
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() — сохраняет архитектуру + веса + оптимизатор, удобнее для деплоя
Преимущества:
Каждая группа признаков обрабатывается оптимальным образом
Разные функции активации для разных типов признаков
Возможность добавлять новые группы признаков без переделки всей архитектуры
Почему CNN лучше чем сложные архитектуры:
|
Архитектура |
Требуемый размер датасета |
Точность на 273 файлах |
|---|---|---|
|
Transformer |
10,000+ |
~70% (переобучение) |
|
LSTM |
5,000+ |
~80% |
|
CNN (наша) |
500+ |
94.55% |
|
FCN |
1,000+ |
~75% |
Ограничения Colab:
Ограниченная GPU память [6]
Ограниченное время сессии (12 часов)
Наши решения:
Маленький batch size (10) для экономии памяти
Эффективная архитектура для быстрой сходимости
250 эпох укладываются в лимит времени
# Добавление шума
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 раз, улучшение обобщающей способности
# Использование предобученных моделей
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
Преимущества:
Не нужно обучать с нуля
Лучше качество признаков
Меньше требуется данных
from tensorflow.keras.callbacks import EarlyStopping
early_stopping = EarlyStopping(
monitor='val_loss',
patience=20, # Остановить если 20 эпох нет улучшений
restore_best_weights=True # Вернуть лучшие веса
)
model.fit(..., callbacks=[early_stopping])
Эффект: Экономия времени обучения, предотвращение переобучения
# Конвертация в 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
|
Аспект |
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 в своих проектах?
Какие предобученные модели вы применяете для аудио-задач?
Делитесь в комментариях! 👇
Автор: AlekseiVB
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/26479
URLs in this post:
[1] Части 1: https://%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B0
[2] опыта: http://www.braintools.ru/article/6952
[3] обучение: http://www.braintools.ru/article/5125
[4] Image: https://sourcecraft.dev/
[5] нейроны: http://www.braintools.ru/article/9161
[6] память: http://www.braintools.ru/article/4140
[7] SmartHome v4.6.ipynb: https://drive.google.com/file/d/1TTu9fZLEwH694adJ-SrkrZoMcG0g0SKe/view?usp=drive_link
[8] Источник: https://habr.com/ru/articles/1005320/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1005320
Нажмите здесь для печати.