Время прочтения: 12 мин.

В 21 веке лавинообразно распространяется телефонное мошенничество, а доля разоблачения и поимки таких преступников мала. Можно ли определять мошенников в первые минуты разговора, если их телефонные номера постоянно меняются? Рассмотрим подробнее.

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

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

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

Базовый подход к работе со звуковыми данными в ML заключается в предобработке записей:

  • в сэмплировании звуков с заданной частотой дискретизации;
  • в выделении частот быстрым преобразованием Фурье и расчёте спектральных и мел-частотных кепстральных коэффициентов;
  • расчёте длительности сигнала,
  • средней частоты,
  • стандартного отклонения частоты,
  • медианы частоты
  • квантилей (в кГц, например),
  • коэффициентов асимметрии, эксцесса,
  • спектральной энтропии и плотности,
  • центроида частоты,
  • пиковой частоты, фундаментальной частоты,
  • частоты перехода через ноль,
  • частоты цветности (в случае со спектрограммой)
  • и т. п., и дальнейшей передаче полученных датасэтов с признаками на вход моделям (нейронным сетям или другим классификаторам).

Конечно, это всё круто и интересно, но затратно по времени и не всегда полезно заниматься подробным «feature engineering»-ом.

Есть ли способ «побыстрее»? В плане чтобы «загрузил модель, настроил, и она работает»? Думаем, есть.

Обратим взор на два проекта (это наш выбор, но список этим не ограничивается):

набор инструментов NVIDIA NeMo и фреймворк wav2vec2 от платформы Meta (признана экстремистской организацией и запрещена на территории РФ).

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

Модель wav2vec2 обучена на немаркированных файлах с более чем 50 000 часов устной речи.

Подобно моделированию языка с маскировкой БЕРТА, модель изучает контекстуализированные речевые представления путем случайной маскировки векторов признаков перед передачей их в сеть transformer.

Рисунок 1
Рисунок 1

На рисунке приведена иллюстрация w2v-кодировщика и его предварительного обучения. Основная часть модели состоит из кодировщика на основе CNN (свёрточной нейронной сети), контекстной сети на основе трансформера и модуля квантования. CNN-кодировщик преобразует необработанный звук X в скрытые речевые представления Z.

Контекстная сеть объединяет 12 блоков трансформеров с 8 слоями внимания. Затем к замаскированным представлениям добавляется относительное позиционное вложение. Преобразователь контекстуализирует замаскированные представления и генерирует контекстные представления C.

Модуль квантования используется для дискретизации скрытых речевых представлений Z в Q. В модуле квантования имеется G = 2 кодовых книги. Каждая из них содержит V = 320 записей размером 128. Модуль квантования сначала преобразует Z в логиты. Затем Функция gumbel softmax используется для выбора одной записи из каждой кодовой книги полностью дифференцируемым способом. Все выбранные элементы объединяются в результирующие векторы [e1; e2; …; EG], которые линейно размечаются на q.

Но в таком исходном виде модель нам не поможет, так как мы пытаемся идентифицировать говорящего без привязки к тексту. Необходимо произвести finetuning с некоторой доработкой.

Так согласно статье необходимо в модель добавить средний объединяющий слой и полносвязный слой поверх w2v-encoder. После этого модель необходимо настроить, произвести finetunig. Это можно сделать, но нам бы хотелось более «коробочного» решения.

Поэтому мы обратились к разработкам Nvidia.

NVIDIA NeMo — это набор инструментов для создания новых современных моделей разговорного искусственного интеллекта. В NeMo есть отдельные коллекции для моделей автоматического распознавания речи (ASR), обработки естественного языка (NLP) и преобразования текста в речь (TTS). Каждая коллекция состоит из готовых модулей, которые включают в себя все необходимое для обучения на ваших данных. Каждый модуль может быть легко настроен, расширен и составлен для создания новых архитектур моделей разговорного искусственного интеллекта.

Распознавание говорящих (speakers recognition ­ SR) — это обширная область исследований, которая решает две основные задачи: идентификация говорящего (кто говорит?) и проверка говорящего (является ли говорящий тем, за кого он себя выдает?).

Нам интересно распознавание говорящего без привязки к тексту, то есть мы не отслеживаем особенности построения фраз и диалекты.

Обычно такие системы SR работают с неограниченными речевыми высказываниями, которые преобразуются в векторы фиксированной длины, называемые эмбединги дикторов/говорящих. Эти эмбединги также используется в автоматическом распознавании речи (ASR) и синтезе речи.

Так как нам не важно, что говорит диктор, то не важно и на каком языке. Поэтому для предварительного обучения мы можем использовать датасет HI-MIA, например. Высказывания в нём были записаны по сценарию «далеко от микрофона и с посторонними шумами» и «близко к микрофону с шумами», чтобы модель научилась определять именно голос и манеру произношения.

Также нужно учесть, что все аудио звуки нужно привести к 16 кГц. (можно с помощью модуля librosa)

Для нашего эксперимента установим такие зависимости:

!pip install wget
!apt-get install sox libsndfile1 ffmpeg
!pip install unidecode

Устанавливаем NeMo:

BRANCH = 'r1.7.0'
!python -m pip install git+https://github.com/NVIDIA/NeMo.git@$BRANCH#egg=nemo_toolkit[asr]

Устанавливаем TorchAudio:

!pip install torchaudio>=0.10.0 -f https://download.pytorch.org/whl/torch_stable.html

Получаем данные:

import os
NEMO_ROOT = os.getcwd()
print(NEMO_ROOT)
import glob
import subprocess
import tarfile
import wget
From NeMo.scripts.dataset_processing import get_hi-mia_data

Загрузим датасет:

Os.system(f‘python –m NeMo/scripts/dataset_processing/get_hi-mia_data.py --data_root={NEMO_ROOT}‘)

В процессе скачивания через указанный модуль файлы уже были преобразованы и созданы json-манифесты для обучения.

Но если вы будете использовать, например, an4 датасет или какой-нибудь другой, то придётся выполнить следующие строки (на примере an4):

data_dir = os.path.join(NEMO_ROOT,'data')
os.makedirs(data_dir, exist_ok=True)

print("******")
if not os.path.exists(data_dir + '/an4_sphere.tar.gz'):
    an4_url = 'https://dldata-public.s3.us-east-2.amazonaws.com/an4_sphere.tar.gz'
    an4_path = wget.download(an4_url, data_dir)
    print(f"Dataset downloaded at: {an4_path}")
else:
    print("Tarfile already exists.")
    an4_path = data_dir + '/an4_sphere.tar.gz'

tar = tarfile.open(an4_path)
tar.extractall(path=data_dir)

print("Converting .sph to .wav...")
sph_list = glob.glob(data_dir + '/an4/**/*.sph', recursive=True)
for sph_path in sph_list:
    wav_path = sph_path[:-4] + '.wav'
    cmd = ["sox", sph_path, wav_path]
    subprocess.run(cmd)
print("Finished conversion.\n******")

Сначала получим файл (ы) scp, содержащий все файлы wav с абсолютными путями для каждого из наборов train, dev и test. Это можно легко сделать с помощью команды find bash

os.system(f’find {data_dir}/an4/wav/an4_clstk -iname "*.wav" > data/an4/wav/an4_clstk/train_all.scp’)

Если посмотреть на первые 3 строчки scp файла для обучения:

Os.system(f’head -n 3 {data_dir}/an4/wav/an4_clstk/train_all.scp
’)

То получим, например, такой вывод:

/content/data/an4/wav/an4_clstk/mtje/cen4-mtje-b.wav
/content/data/an4/wav/an4_clstk/mtje/an33-mtje-b.wav
/content/data/an4/wav/an4_clstk/mtje/cen5-mtje-b.wav

Поскольку мы создали scp-файл для обучения, мы используем модуль scp_to_manifest.py, чтобы преобразовать этот файл scp в файл манифеста, а затем при необходимости разделить файлы на «train & dev» для оценки моделей во время обучения с помощью флага --split. Нам не понадобилась бы опция --split для тестовой папки. Соответственно, укажем id номер поля, разделенного символом /, которое будет рассматриваться как метка диктора/говорящего.

После загрузки и преобразования ваша папка с данными должна будет содержать каталоги с файлами манифеста в таком виде:

data/<path>/train.jason

data/<path>/dev.json

data/<path>/train_all.json

Каждая строка в файле манифеста описывает обучающий образец — audio_file path, содержит путь к файлу wav, duration — это длительность в секундах, а label — это метка класса говорящего:

{«{«audio_file path»: «<absolute path to dataset>data/an4/wav/an4 test_clstk/menk/cen4-menk-b.wav», «duration»: 3.9, «label»: «menk»}

Итак, создадим манифесты:

if not os.path.exists('scripts'):
  print("Downloading necessary scripts")
  os.system(‘mkdir -p scripts/speaker_tasks’)
  os.system(f’wget - scripts/speaker_tasks/ https://raw.githubusercontent’
  f’.com/NVIDIA/NeMo/$BRANCH/scripts/speaker_tasks/scp_to_manifest.py’)
os.system(f’python {NEMO_ROOT}/scripts/speaker_tasks/scp_to_manifest.py’
f’ --scp {data_dir}/an4/wav/an4_clstk/train_all.scp --id -2 --out {data_dir}’
f’/an4/wav/an4_clstk/all_manifest.json –split’)

Сгенерируем scp для тестовой папки, а затем преобразуйте его в манифест.

Os.system(f’find {data_dir}/an4/wav/an4test_clstk  -iname "*.wav"’
f’ > {data_dir}/an4/wav/an4test_clstk/test_all.scp’)
os.system(f‘python {NEMO_ROOT}/scripts/speaker_tasks/scp_to_manifest.py –scp’
f’ {data_dir}/an4/wav/an4test_clstk/test_all.scp --id -2 –out’
f’ {data_dir}/an4/wav/an4test_clstk/test.json’)

Задаём путь к файлам манифеста:

train_manifest = os.path.join(data_dir,'an4/wav/an4_clstk/train.json')
validation_manifest = os.path.join(data_dir,'an4/wav/an4_clstk/dev.json')
test_manifest = os.path.join(data_dir,'an4/wav/an4_clstk/dev.json')

Поскольку цель большинства систем, связанных с определением дикторов, состоит в том, чтобы получить хорошие эмбединги по дикторам, которые могли бы помочь отличить их друг от друга, мы сначала обучим модель end-to-end методом, оптимизируя модель TitaNet. Мы модифицируем декодер, чтобы сделать эти эмбединги фиксированного размера независимыми от длины входного аудио.

Кратко о модели TitaNet

Модель основана на архитектуре ContextNet ASR (ASR – автоматическое определение речи), состоящей из структуры кодера и декодера. Используется кодировщик модели ContextNet в качестве средства извлечения признаков верхнего уровня и передаёт выходные данные на объединяющий слой внимания. Этот уровень вычисляет характеристики внимания по измерениям канала, чтобы фиксировать представления дикторов на уровне произнесения, не зависящие от времени.

Titanet — это сверточная модель с разделением каналов по глубине в масштабе 1D с архитектурой, подобной Contextnet, в сочетании со слоем группировки (pool) внимания каналов.

Рисунок 2
Рисунок 2

На рис. 2 описывается кодировщик модели ContextNet-B×R×C и декодер объединения внимания, где B — количество блоков, R – количество повторяющихся подблоков на блок, а C – количество фильтров в слоях свертки каждого блока. Кодировщик начинается с блока пролога B0, за которым следуют мегаблоки B1 … BN−1 и заканчивается блоком эпилога BN. Блоки Prologue и epilogue отличаются от мегаблоков тем, что они оба имеют один и тот же модуль свертки (Conv), уровни batchnorm и relu слои и имеют фиксированные размеры ядра 3 в prologue и 1 в epilogue для всех предлагаемых сетевых архитектур. Они не содержат остаточные соединения и dropout слои. Каждый мегаблок начинается с разделяемого по временному каналу сверточного слоя с шагом 1 и расширением 1, за которым следуют batchnorm, relu и dropout.

Вернёмся к процессу настройки модели.

Импортируем необходимые модули.

import nemo
import nemo.collections.asr as nemo_asr
from omegaconf import OmegaConf

Модель TitaNet определена в конфигурационном файле, в котором объявлено несколько важных разделов.

Разделы:

  1. Модель: все аргументы, которые будут относиться к Модели — препроцессоры, кодировщик, декодер, оптимизаторы и планировщики, наборы данных и любая другая связанная информация.
  2. учитель: Любой аргумент, который должен быть передан в PyTorch Lightning.

Следующими строками мы выведем конфигурацию:

os.system(‘mkdir conf ‘)
os.system(f‘wget -P conf https://raw.githubusercontent.com/NVIDIA/NeMo’
f’/$BRANCH/examples/speaker_tasks/recognition/conf/titanet-large.yaml’)
MODEL_CONFIG = os.path.join(NEMO_ROOT,'conf/titanet-large.yaml')
config = OmegaConf.load(MODEL_CONFIG)
print(OmegaConf.to_yaml(config))

Установим пути к манифестам:

config.model.train_ds.manifest_filepath = train_manifest
config.model.validation_ds.manifest_filepath = validation_manifest

Чтобы включить его набор данных test_ds, добавьте его в конфигурацию и замените файл манифеста как config.model.test_ds.manifest_file path = test_manifest.

Также установим количество классов в датасете:

config.model.decoder.num_classes = 340

(для an4 будет 74).

Модели Nemo являются модулями PyTorch Lightning и поэтому полностью совместимы с экосистемой PyTorch Lightning.

Импортируем модули

import torch
import pytorch_lightning as pl

Проверяем доступность GPU, устанавливаем количество эпох, убираем признак стратегии и аугментации:

accelerator = 'gpu' if torch.cuda.is_available() else 'cpu'
config.trainer.devices = 1
config.trainer.accelerator = accelerator

config.trainer.max_epochs = 10

config.trainer.strategy = None

config.model.train_ds.augmentor=None
trainer = pl.Trainer(**config.trainer)

У Nemo есть менеджер экспериментов, который обрабатывает протоколирование и контрольные точки для нас:

from nemo.utils.exp_manager import exp_manager
log_dir = exp_manager(trainer, config.get("exp_manager", None))
# The log_dir provides a path to the current logging directory for easy access
print(log_dir)

TitaNet — это модель экстрактора эмбедингов диктора, которая может использоваться для задач идентификации дикторов — она генерирует одну метку для всего предоставленного аудиопотока. Поэтому мы инкапсулируем его внутри EncDecSpeakerLabelModel следующим образом:

Любая модель NeMo по своей сути является моделью PyTorch Lightning, ее можно легко обучить в одну строку:

trainer.fit(speaker_model)

Если у вас есть файл тестового манифеста, мы можем легко вычислить точность, выполнив:

trainer.test(speaker_model, ckpt_path=None)

Мы можем значительно сократить время, затрачиваемое на обучение этой модели, используя обучение с несколькими графическими процессорами наряду со смешанной точностью.

Смешанная точность:

trainer = Trainer(amp_level='O1', precision=16)

Тренажер с распределенным бэкендом:

trainer = Trainer(devices=2, num_nodes=2, accelerator='gpu', strategy='dp')

Можно и комбинировать эти флаги.

NeMo также позволяет сохранять и восстанавливать модели.

При использовании NEMO для обучения рекомендуется использовать фреймворк exp_manager. Ему поручено обрабатывать контрольные точки и ведение журнала.

При использовании фреймворка exp_manager, у нас есть доступ к каталогу, в котором существуют контрольные точки.

exp_manager с настройками по умолчанию сохранит для нас несколько контрольных точек —

  • Несколько контрольных точек с определенных этапов обучения. У них будут: —val_loss=tags
  • Контрольная точка в последнюю эпоху обучения обозначается символом —last.
  • Если модель завершит обучение, у нее также будет контрольная точка —last.

Выведем последнюю точку:

checkpoint_dir = os.path.join(log_dir, 'checkpoints')
checkpoint_paths = list(glob.glob(os.path.join(checkpoint_dir, "*.ckpt")))
checkpoint_paths
final_checkpoint = list(filter(lambda x: "-last.ckpt" in x, checkpoint_paths))[0]
print(final_checkpoint)

Чтобы восстановить модель используем метод: LightningModule.load_from_checkpoint():

restored_model = nemo_asr.models.EncDecSpeakerLabelModel.load_from_checkpoint(final_checkpoint)

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

config.model.train_ds.manifest_filepath = test_manifest
config.model.validation_ds.manifest_filepath = test_manifest
config.model.decoder.num_classes = 10

Настраиваем конфигурацию самой модели:

restored_model.setup_finetune_model(config.model)

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

Для примера также изменим некоторые параметры trainer:

accelerator = 'gpu' if torch.cuda.is_available() else 'cpu'

trainer_config = OmegaConf.create(dict(
    devices=1,
    accelerator=accelerator,
    max_epochs=5,
    max_steps=None,
    num_nodes=1,
    accumulate_grad_batches=1,
    enable_checkpointing=False,  # обеспечиваются exp_manager
    logger=False,  # обеспечиваются exp_manager
    log_every_n_steps=1,
    val_check_interval=1.0,
))
print(OmegaConf.to_yaml(trainer_config))
trainer_finetune = pl.Trainer(**trainer_config)

Установим параметр учителя в восстановленную модель:

restored_model.set_trainer(trainer_finetune)
log_dir_finetune = exp_manager(trainer_finetune, config.get("exp_manager", None))
print(log_dir_finetune)

Настроим оптимизатор и планировщик:

import copy
optim_sched_cfg = copy.deepcopy(restored_model._cfg.optim)
# Struct mode не позволяет нам удалять элементы из конфигурации, поэтому отключим его
OmegaConf.set_struct(optim_sched_cfg, False)
# изменим максимальную скорость обучения на предыдущую минимальную скорость # обучения
optim_sched_cfg.lr = 0.001

# Установим "min_lr" на низкий уровень
optim_sched_cfg.sched.min_lr = 1e-4

print(OmegaConf.to_yaml(optim_sched_cfg))
# Обновим настройки оптимизатора
restored_model.setup_optimization(optim_sched_cfg)
# Можем заменить конфигурацию
restored_model._cfg.optim = optim_sched_cfg

Дообучение:

trainer_finetune.fit(restored_model)

Теперь мы можем сохранить всю конфигурацию и параметры модели в одном файле .nemo и в любое время восстановить из него

restored_model.save_to(os.path.join(log_dir_finetune, '..',"titanet-large.nemo"))
!ls {log_dir_finetune}/..
restored_model_2 = nemo_asr.models.EncDecSpeakerLabelModel.restore_from(os.path.join(log_dir_finetune, '..', "titanet-large.nemo"))

В целом это всё. Данное руководство к действию не очень сложное для реализации, и им можно пользоваться.

Для работы модели рекомендуем использовать GPU.

Использование подобных моделей освобождает пользователей от долгих изысканий новых признаков для обучения модели.

Добавим, что в github Nvidia (https://github.com/NVIDIA) есть ещё много полезных инструментов для ML.