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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  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 
  } 
 

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

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

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

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

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

  • CoreMl tools for TensorFlow
  • CoreMl tools for PyTorch

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

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

  • Разработка