Как научить iOS приложение распознавать тон речи? — обложка

Если вы хотите понять, что такое нейронная сеть и как она работает, лучше сначала заглянуть сюда. Эта статья — о практическом применении нейросетей на iOS. В частности — о распознавании речи и определении её характеристик. С примерами кода покажем, как это реализовали в одном из наших проектов. Начнём с фреймворка, который будем использовать. Затем объясним, как создать модель и обучить её на данных из приложения, а также как анализировать результаты.

Какой фреймворк использовать?

CML (Core Machine Learning) — это фреймворк от Apple, позволяющий добавлять в приложения для iOS возможности машинного обучения. Он появился в 2016 году как расширение наработок по работе с матрицами и векторной алгеброй (объединённых в фреймворк Accelerate) и вычислениям на графической технологии Metal — основных инструментах для нейросетей.

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

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

Упрощенная схема процесса классифкации текста
Упрощённая схема процесса классификации текста

Благодаря интеграции готовой натренированной модели в CoreML получается мощный и гибкий инструмент для работы с нейронными сетями. В него можно импортировать практически все популярные нейросети:

  • BERT, GPT — для задач с естественным языком (на котором мы говорим каждый день),
  • нейронные сети для задач классификации изображений и других подобных задач

Ограничение одно: число компонент тензора должно быть <= 5. То есть, не больше пяти измерений.

Стоит уточнить, что такое модель нейронной сети. Это результат обучения нейронной сети — взвешенный граф с наилучшей комбинацией весов. У него есть вход, а на выходе — какой-то результат.

Как на iOS определить токсичность фразы в реальном времени?

Алгоритм, описанный ниже, можно использовать для анализа общей характеристики речи. Рассмотрим на конкретном примере — «токсичности».

Итак, чтобы определить токсичность фразы, нужно разбить задачу на несколько этапов:

  1. Подготовить тренировочные данные с токсичными и нетоксичными фразами;
  2. Получить модель нейронной сети, обученную на наборе данных;
  3. Записать фразу;
  4. Отправить фразу в библиотеку SFSpeechRecognition для анализа голоса и получить её в текстовом виде;
  5. Отправить текст обученной модели на классификацию и получить результат.

Если описать это с помощью диаграммы, то задача выглядит так:

Как устроен процесс распознания речи
Как устроен процесс распознания речи

Фаза 1: подготовка данных для обучения модели классификации текста

Чтобы получить обученную модель, можно выбрать один из двух путей:

  1. Разработать нейронную сеть самостоятельно и обучить с её помощью модель;
  2. Взять готовую модель и нейронную сеть, дообучить её на своих данных и использовать, например, Python.

Чтобы упростить процесс, выберем второй путь. У Apple есть отличный набор инструментов, поэтому начиная с Xcode 13 отладка моделей стала максимально простой.

Для начала запустим утилиту CreateML — она уже входит в Xcode — и создадим новый проект. Выбираем TextClassification (Apple использует для этого нейросеть BERT) и создаём проект. Откроется окно, в которое можно загрузить подготовленные данные.

Утилита принимает на вход два набора данных:

  • набор, на котором модель будет дообучаться;
  • набор, с которым будут сравниваться полученные результаты.

Формат данных должен быть JSON или CSV. Структура набора данных должна соответствовать шаблону:

Для json:

 
 [ 
   { 
       "text": "The movie was fantastic!", 
       "label": "positive" 
   }, { 
       "text": "Very boring. Fell asleep.", 
       "label": "negative" 
   }, { 
       "text": "It was just OK.", 
       "label": "neutral" 
   } ... 
] 
 

Для csv:

 
 text,label 
"The movie was fantastic!",positive 
"Very boring. Fell asleep.",negative 
"It was just OK.",neutral 
 

Данные подготовлены — можно загружать и начинать обучение модели.

Как начать новое обучение
Как начать новое обучение

Как понять, что всё готово и работает?

Оценить полученные результаты довольно просто. Для каждого проекта обучения есть такие сводки.

Пример отчета
Пример отчета

Precision — показывает, насколько хорошо модель находит нужную цель (в данном случае — фразу, которую нужно охарактеризовать), не выдавая при этом лишние результаты.

Recall — показывает, насколько хорошо модель находит нужную цель.

F1 score — метрика, которая объединяет точность и полноту алгоритма. Её рассчитывают по формуле:

Как считать F1
Как считать F1

Чем выше Precision и Recall, тем лучше. Однако на практике достичь максимума по обоим показателям одновременно невозможно.

Остается только экспортировать полученную модель в формате *.mlmodel.

Фаза 2: получение аудиосигнала и отправка на распознавание речи

Speech framework переводит речь в текст на iOS. В нём уже есть обученная модель. Так как наша задача — распознавать речь в реальном времени, первым делом нужно получать аудиоданные в виде образцов AVAudioPCMBuffer и передавать их распознавателю.

 
 class AudioRecordService { 
   private var audioEngine: AVAudioEngine? 
   func start() { 
            do { 
                audioEngine = try configureAudioEngine() 
            } catch { 
                audioRecordingEvents.onNext(.error(.startingAudioEngineError)) 
            } 
    } 
    private func configureAudioEngine() throws -> AVAudioEngine { 
        let audioEngine = AVAudioEngine() 
        let inputNode = audioEngine.inputNode 
        let recordingFormat = inputNode.outputFormat(forBus: 0) 
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in 
            self?.audioRecordingEvents.onNext(.audioBuffer(buffer)) 
        } 
        audioEngine.prepare() 
        try audioEngine.start() 
        return audioEngine 
    } 
}
 

Устанавливаем ответвление на нулевую шину — образцы начнут поступать, как только количество аудиокадров достигнет 1024. Кстати, объект AVAudioNode может иметь несколько входных и выходных шин.

Полученный буфер нужно отправить на распознавание речи:

  • Создадим перечисление для обработки ошибок
 
 enum SpeechReconitionError { 

    case nativeError(String) 

    case creatingTaskError 

} 
 
  • Создадим перечисление для событий распознавания
 
 enum SpeechReconitionEvents { 

    case phrase(result: String, isFinal: Bool) 

    case error(SpeechReconitionError) 

} 
 
  • Создадим объект SFSpeechRecognizer
 
 private var request: SFSpeechAudioBufferRecognitionRequest? 

private var reconitionTask: SFSpeechRecognitionTask? 

private let recognizer: SFSpeechRecognizer? 

 init() { 

        recognizer = SFSpeechRecognizer(locale: Locale.preferredLanguages[0]) 

    } 
 
  • Конфигурируем recognizer и запускаем задачу распознавания
 
  func configureRecognition() { 
        request = SFSpeechAudioBufferRecognitionRequest() 
        if #available(iOS 16.0, *) { 
            request?.addsPunctuation = true 
        } 
       if let supports = recognizer?.supportsOnDeviceRecognition, supports { 
            request?.requiresOnDeviceRecognition = true 
        } 
        request?.shouldReportPartialResults = true 
        guard let request else { 
            stopRecognition() 
            events.onNext(.error(.creatingTaskError)) 
            return 
        } 
  reconitionTask = recognizer?.recognitionTask(with: request, resultHandler: recognitionTaskHandler(result:error:)) 
    } 
 
  • Функция для добавления аудиобуферов в очередь распознавания
 
 func transcribeFromBuffer(buffer: AVAudioPCMBuffer) { 
        request?.append(buffer) 
    }
 
  • Конфигурируем обработчик результатов
 
  private func recognitionTaskHandler(result: SFSpeechRecognitionResult?, error: Error?) { 
        if let result = result { 
            events.onNext(.phrase(result: result.bestTranscription.formattedString, isFinal: result.isFinal)) 
            if result.isFinal { 
                eraseRecognition() 
            } 
        } 
        
        if let error { 
            events.onNext(.error(.nativeError(error.localizedDescription))) 
            return 
        } 
    } 

    private func eraseRecognition() { 
        reconitionTask?.cancel() 
        request = nil 
        reconitionTask = nil 
    }
 

Процесс распознавания запустится сразу же после configureRecognition(). Дальше передаем полученные аудиобуферы в метод transcribeFromBuffer(buffer: AVAudioPCMBuffer).

Процесс распознавания требует времени — примерно 0,5-1 сек. Поэтому результат приходит асинхронно в функцию ecognitionTaskHandler(result: SFSpeechRecognitionResult?, error: Error?). SFSpeechRecognitionResult и содержит результаты распознавания последнего аудиобуфера, а также результаты всех предыдущих распознаваний! То есть на экране пользователь видит последнее распознанное предложение и все, что было распознано раньше.

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

 
  if let supports = recognizer?.supportsOnDeviceRecognition, supports { 
            request?.requiresOnDeviceRecognition = true 
 } 
 

Apple утверждает, что результаты на устройстве хуже, чем в онлайн-режиме. Но для онлайн-использования есть ограничения.

 Сравнение резульаттов распознания речи на сервере и на устройстве. Источник: Apple Tech Talks (https://developer.apple.com/videos/tech-talks/)
Сравнение результатов распознавания речи на сервере и на устройстве. Источник: Apple Tech Talks (https://developer.apple.com/videos/tech-talks/)

Фаза 3: классификация речи

Важно: главное правило использования нейронных сетей для классификации речи — чем больше контекста, тем выше точность.

Сначала импортируем ML-модель в проект как обычный файл. Затем создаём экземпляр класса модели. Имя файла будет совпадать с названием класса.

 
 init?() { 
        do { 
            let config = MLModelConfiguration() 
            config.computeUnits = .all 
            if #available(iOS 16, *) { 
                config.computeUnits = .cpuAndNeuralEngine 
            } 
            mlModel = try ToxicTextClassificatorConditionalAlgoritm(configuration: MLModelConfiguration()).model 
            if let mlModel { 
                predicator = try NLModel(mlModel: mlModel) 
            } 
        } catch { 
            print("Can not initilaize ToxicTextClassificatorConditionalAlgoritm") 
            return nil 
        } 
    }
 

NLModel — это объект, с которым придётся работать дальше.

После создания модель готова принимать текст для классификации.

Сделаем перечисление возможных исходов классификации.

 
 enum PredictResult: String { 
    case toxic 
    case positive 
} 
 

Теперь попробуем получить результат!

 
 func predictResult(phrase: String) -> PredictResult? { 
        guard let predict = predicator?.predictedLabel(for: phrase), 
              let result = PredictResult(rawValue: predict) else { return nil } 
        return result 
   } 
 

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

Как повысить точность полученных результатов?

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

Reminder: фраза будет приходить каждый раз больше на N слов, потому что SFSpeechRecognitionResult возвращает результаты распознавания последнего аудиобуфера и результаты всех предыдущих распознаваний.

 
 func analyze(phrase: String, isFinalResult: Bool) { 
        guard let predict = predictResult(phrase: phrase) else { 
            if isFinalResult, let result = predictResult { 
                event.onNext(.finalResult(result)) 
            } 
            return 
        } 
        predictResult = predict 
    } 
 

б) Если пунктуации нет*, но нужно снизить накладные расходы на классификацию, берём только последние N слов из предложения. Однако это сильно снизит точность результатов.
*Чтобы добавить функцию расстановки пунктуации (пока доступна только на английском):

 
  if #available(iOS 16.0, *) { 
            request?.addsPunctuation = true 
  } 
 

Повысить точность и снизить накладные расходы на вычисления можно с помощью алгоритма разделения текста на предложения в заданной пропорции. Например, если в тексте три предложения, можно делить их в соотношении 2:1 или 1:2 — то есть сначала анализировать первые два, а потом одно, или наоборот.

Финальный скриншот с результатами
Финальный скриншот с результатами

Важно: Обязательно запросите разрешения на использование микрофона и анализ речи.

Альтернативные способы получить MLModel

Набор утилит для Python CoreML tools, который позволяет конвертировать модель, обученную с помощью других нейронных сетей, в формат mlmodel:

  • CoreMl tools for TensorFlow
  • CoreMl tools for PyTorch

TensorFlow Lite для iOS. Он позволяет работать с моделями, натренированными с помощью TensorFlow.

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

  • Разработка