Бурное развитие технологий генеративных нейронных сетей за последние полтора года вызвало волну желания стекхолдеров отметить наличие искусственного интеллекта в каждом приложении, которое они оплачивают. Как правило, без указания области применения. Это поставило перед разработчиками нетривиальную задачу – найти точку приложения силы в проектах, которые такого поворота не предполагали.
С одной стороны – Apple предлагает великолепные инструменты для создания нейро-моделей (краткий обзор в статьях на Хабре (1) , (2) ), с другой стороны, очень ограниченное количество задач покрывается предложенными возможностями, что порождает закономерный вопрос: «зачем это все надо?». Любому разработчику требуется опыт, который непонятно как получить, когда нет релевантных задач. Без опыта – нет оснований для внедрения в приложение новых решений.
Подавляющее большинство приложений строится вокруг клиент-серверной архитектуры. Те приложения которые коммуникации с сервером еще не имеют – тяготеют к тому, чтоб ими стать. А это, в свою очередь порождает новую ловушки приобретения опыта – «бэкенд большой – пусть он и думает» – перенося зону ответственности за интеллектуальную нагрузку куда-то в облако, что может казаться верной стратегией, если руководствоваться опытом пятнадцатилетней давности. В настоящее время теория глубокого обучения настоятельно рекомендует использовать опыт федерализации. А значит, часть интеллектуальной нагрузки должна находится на стороне клиентского приложения.
Поскольку задача бизнеса – это получение прибыли, то и приложения, которые заказываются бизнесом обладают схожими характеристиками – получение прибыли и удержание клиента (потребителя). А значит, как правило, требуется наличие профиля пользователя и знание о его платежных атрибутах. iOS, конечно, последнюю часть в состоянии опосредовать благодаря внедрению wallet, но, вот кастомеры с таким подходом, обычно, не согласны, если речь не идет о банальной продаже товаров, и требуют более достоверную информацию от пользователя приложения.
Использование AI (ИИ), при заполнении профиля, позволяет снизить вычислительную нагрузку на сервере, и повысить комфорт пользовательскому опыту от использования приложения.
Рассмотрим две типовые задачи:
Проблема публикации аватара.
Проблема ввода буквенно-цифровых идентификаторов.
Задача аватара: некоторые, особенно финтеховские, компании хотят видеть аватар пользователя для визуальной идентификации клиента при посещении офиса. Возможность отсеять часть отправляемых изображений – существенно уменьшит трафик и нагрузку на тех сотрудников, которые занимаются одобрением таких фотографий – кошечки и автомобильчики будут помечены специальным атрибутом, и попросту не будут отправлены бэкенду. Конечно, это не гарантирует то, что пользователи добросовестно отправят свои фотографии, но, уменьшит количество дополнительной работы по их обработке.
Для первого примера сформулируем задачу следующим образом: Определить что на фотографии присутствует изображение человека, которое можно соотнести с личностью пользователя в достаточной степени, для его идентификации. Высокохудожественные изображения следует игнорировать (это требование связано с тем, что такого рода фотографии редко делаются на телефон, и точно не предназначены для идентификации пользователя, и, скорее всего, содержат в себе ретушь). Кроме того, следует распознать прочие объекты на фотографии, если они являются существенным для данного изображения и вернуть название объекта и вероятность того, что объект таковым является.
Для решения задачи из этого и последующих примеров, которые работают с изображениями, нам потребуется базовое приложение, которое загружает из галереи выбранные фотографии. Его структура достаточно очевидна, и основана на стандартном примере использования PhotosPicker, поэтому не будем останавливаться на описании реализации. Исходный код можно получить в GitHub репозитарии (быть может, оно сгодится и для других Ваших проектов).
Для решения ранее сформулированной задачи, нам потребуется нейронная сеть, которая в состоянии классифицировать объекты на изображении. Тема создания такой нейронной сети выходит за рамки данной статьи – она не настолько сложная, но предполагает разъяснение некоторых операций, кроме того, требует очень широкой базы стимульного материала для ее обучения и валидации. Благо, Apple предоставляет готовое решение: на странице Machine-Learnig Вы можете загрузить полностью обученную нейронну сеть. Следует учитывать следующее, что часть предлагаемых сетей предоставляется в нескольких вариантах – чем больше файл сети – тем больше классов объектов эта сеть в состоянии распознать, и тем выше точность распознания, но при этом размер сети отличается в разы.
В частности, для наших целей нам нужна будет сеть YOLOv3, которая поставляется в трех вариантах, каждый из которых может быть запущен на мобильном устройстве, но самая маленькая из них, YOLOv3TinyInt8LUT, специально адаптирована для работы с мобильными устройствами. В чем суть – суффикс TinyInt8 указывает на то, что нейронная сеть прошла процедуру компактификации, при которой вещественные значения весов (как правило Double) заменяются близкими значениями Unsigned Int числами в диапазоне от 0 до 255. Конечно, при этом нейросеть теряет точность, но существенно выигрывает в производительности. И если потеря точности составляет несколько процентов, в порой – сотых долей процентов, то производительность возрастает на порядки в десятичном исчислении. Поэтому, при прочих равных, следует использовать сети, в названии которых присутствует данный суффикс. v3, в свою очередь, намекает на то, что существуют и другие версии данной сети. В настоящее время самой современной считается v7, но мы будем использовать несколько более консервативное решение. Однако, может быть Вам захочется ознакомится с самой современной реализацией – исходный код и тестовые характеристики находятся в открытом доступе.
Основные характеристики сети следующие:
Размер файла: 8,9 Мбайт.
Количество категорий (классов, в терминах ML): 80 – эта характеристика указывает на то, какое количество типов объектов может быть распознано нейронной сетью.
Image size: 416 x 416 RGB – идеальное разрешение изображения, для передачи нейросети. Нет необходимости придерживаться строгого соответствия, но следует понимать, что если размеры будут отличаться в разы, то и точность прогноза будет ниже. Причем, это касается как уменьшения размера изображения, так и увеличения. После определенного размера прогноз вообще может быть невозможен. Поэтому следует помнить – что высокая резолюция изображения отнюдь не делает прогноз лучше. Необходимые преобразования изображения нейросеть делает сама, но, не стоит этим злоупотреблять. Так как использование нейросети – это всегда вероятностный подход, то нет точных границ, до которых нейросеть воспринимает изображение, а после которых перестает.
Качество распознавания сильно зависит от того, будете ли Вы делать предсказание на реальном устройстве или на симуляторе. Если Вы создали нейронную сеть сами, а не использовали готовую, то, вероятнее всего, количество стимульного материала, применяемого во время обучения было невелико, и идентификация объектов на симуляторе будет в сотни раз хуже чем на реальном устройстве. Для высокооптимизированной нейронной сети, такой как YOLOv3TinyInt8LUT это свойство существенно нивелируется.
Следующий шаг, после загрузки нейронной сети и развертывания стартового проекта, это создание класса-предсказателя. Предсказатель (Predictor) – довольно простой класс: который выполняет всего 3 задачи:
Создает экземпляр нейронной сети.
Создает предсказание
Возвращает результат в вызывающий код при помощи коллбека.
Прим: да, можно, и, даже пора перейти на async await, но для простоты мы будем использовать callbacks.
Внутри класса-предсказателя инстанцирование объекта нейросети в коде позволяет обращаться к свойствам нейросети прямо в редакторе исходного кода – эту магию делает для нас XCode – это наиболее неочевидная часть кода. Главное запомнить, что для нейросети YOLOv3TinyInt8LUT будет существовать конструктор класса YOLOv3TinyInt8LUT, не смотря на то, что он нигде не описан. Главное, не забудьте добавить файл YOLOv3TinyInt8LUT.mlmodel к своему текущему таргету. Еще один нюанс который стоит запомнить – согласно текущей реализации подхода в работе с ML, в конструктор нужно передать конфигурацию. Конфигурация – это инстанс класса MLModelConfiguration без какого либо реального конфигурирования. На этом завершается первый этап.
static func createImageClassifier() -> VNCoreMLModel {
let defaultConfig = MLModelConfiguration()
let imageClassifierWrapper = try? YOLOv3TinyInt8LUT(configuration: defaultConfig)
guard let imageClassifier = imageClassifierWrapper else {
fatalError("App failed to create an image classifier model instance.")
}
let imageClassifierModel = imageClassifier.model
guard let imageClassifierVisionModel = try? VNCoreMLModel(for: imageClassifierModel) else {
fatalError("App failed to create a `VNCoreMLModel` instance.")
}
return imageClassifierVisionModel
}
Второй этап – создать экземпляр класса предсказателя в классе ViewModel, и передать в него полученное при помощи ImagePicker или PhotosPicker изображение.
func makePredictions(for photo: UIImage, completionHandler: @escaping ImagePredictionHandler) {
let orientation = CGImagePropertyOrientation(photo.imageOrientation)
guard let photoImage = photo.cgImage else {
fatalError("Photo doesn't have underlying CGImage.")
}
let request = self.createImageClassificationRequest()
self.predictionHandler = completionHandler
let handler = VNImageRequestHandler(cgImage : photoImage, orientation : orientation)
self.requests = [request]
do {
try handler.perform(self.requests)
} catch {
print("Vision was unable to make a prediction...\n\n\(error.localizedDescription)")
}
}
Третий этап – из колбека предсказателя извлечь массив предсказаний относительно содержимого изображения.
// MARK: - Public methods
func picked(_ image: UIImage?) {
guard let image = image else { return }
self.countObjects = 0
self.result = ""
DispatchQueue.global(qos: .userInitiated).async {
self.imagePredictor.makePredictions(for: image, completionHandler: self.predictHandler)
}
}
// MARK: - Private methods
// MARK: Image prediction methods
private func predictHandler(_ prediction: ImagePredictor.Prediction) {
var result = ""
for item in prediction.items {
result += "\(item.name.capitalized) detected\nwith \(String(format: "%.2f",item.confidence)) confidence.\n"
}
let personCount = prediction.items.filter { $0.name == self.mlTag }.count
switch personCount {
case 0: result += "\nMISTAKE"
case 1: result += "\nSUCCESS"
default:
result += "\nMORE THAN ONE PERSON"
}
self.result = result
}
Предсказания представлены в виде экземпляра структуры Prediction, внутри класса-предсказателя, которую Вы легко можете модифицировать по своему вкусу.
struct Prediction {
struct Item {
let name: String
let confidence: Double
}
let items:[Item]
}
Собственно, экземпляр содержит набор предсказаний. В каждом предсказании представлено тип распознанного объекта (в тривиальном, а не ООП смысле слова), и вероятность верности прогноза.
Hidden text
Лирическое отступление: Если Вы экспериментируете с нейросетью на симуляторе – просто перетащите набор картинок внутрь галереи симулятора – таким образом у Вас появится набор релевантного стимульного материала прямо в приложении.
Что именно делать с полученными результатами – зависит от бизнес логики приложения. Из соображений ранее поставленной задачи, условием успешного выполнения должно быть следующее: в отсортированном по полю confidence массиве первым типом должен быть класс (в ML смысле слова) person (т.е, поле name содержало бы строку «person»), и такой класс был бы единственным в массиве. Что интересно, нейросеть вполне себе интеллектуально в состоянии определить центральный объект изображения, и, даже если Вы скормите ей групповой снимок с человеком на переднем плане – она даст правильный прогноз, проигнорировав другие лица. И в то же время, если на изображении два человека находятся рядом, то нейросеть выдаст два класса person, что можно рассматривать как условно-успешный результат, т.е. такой, где объекты были распознаны правильно, но результат был негативный для использования в текущей задаче.
Если же Вы разрабатываете приложение для использования в компании, которая занимается страхованием автотранспорта, то тот же самый код позволит Вам отделить мотоциклы и автомобили от всего остального.
Исходный код проекта вместе с настроенной нейросетью можно загрузить с GitHub.
Использование классов объектов для идентификации нейронной сетью плавно подводит нас к идее способа решения второй задачи – ввода буквенно-цифровой информации даже на незнакомом нам языке. Использование классов активно применяется при разработке OCR (Optical character recognition) систем. На бытовом уровне задачу можно было бы сформулировать следующим образом: получить строку символов отображаемых на изображении, а затем передать их в приложение в привычной буквенно-цифровой последовательности. Или, другими словами, передать набор египетских иероглифов по буквам, как в известном меме.
Иероглифическое письмо – это, конечно, гипербола, хотя и достаточно любопытная, с точки зрения реализациии, поскольку позволяет потренироваться в разработке нейронных сетей, которые осуществляют дешифрацию археологических данных или тайнописи. Но что значительное ближе к нашей действительности – такой способ позволяет прочитать сообщение записанное буквицей или готическим шрифтом.
Если нам известен способ начертания шрифта – мы можем превратить изображение в осмысленный текст, вне зависимости от того, под каким углом сделана надпись.
Рассмотрим пример на основе нейронной сети CoreOCR которая была натренирована на шрифте INCONSOLATA
Характеристики модели следующие:
Размер файла: 2,4 МБайт
Количество классов: 36, включающих в себя последовательность символов 0-9, A-Z (Upper case).
Для реализации взаимодействия воспользуемся тем же самым стартовым проектом, в который добавим класс-предсказатель. Структура файла аналогичная предыдущей задаче. Но возвращать мы будем уже не объект, включающий в себя вероятностный массив типов, а объект, включающий в себя массив строк (для удобства восприятия). Нейросеть комплектуется несколькими изображениями для тестирование, но, Вы можете установить шрифт Inconsolata и сделать надписи самостоятельно.
// MARK: - Private methods
// MARK: Image prediction methods
private func classifyImage(_ image: UIImage) {
self.imagePredictor.makePredictions(for: image, completionHandler: predictHandler)
}
private func predictHandler(_ prediction: ImagePredictor.Prediction) {
let words = prediction.names.joined(separator: "\n")
self.result = prediction.names.count >= 0 ? words : ""
}
Полный пример реализации можно загрузить с GitHub.
Для большей части случаев применения распознавания текста в быту такой пример кажется притянутым за уши. Автомобильные номера или номера кредитных карт не могут быть распознаны, в том случае если нейросеть не была натренирована на конкретных примерах начертания шрифтов – т.е. область применения такой нейросети ограничена областью где такой шрифт применяется – если это египетская иероглифика, то поблизости от Гизы.
Для практического использования нам потребуется более универсальное решение. И Apple об этом позаботилась – нам не придется искать и устанавливать нейросеть в приложение - чтоб достичь успеха Apple предлагает использовать класс VNRecognizeTextRequest, который имеет все необходимые для этого свойства.
func doOCR(_ cgImage:CGImage?) {
guard let cgImage = cgImage else { return }
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNRecognizeTextRequest {[weak self] request, error in
guard let observations = request.results?.compactMap({ $0 as? VNRecognizedTextObservation }),
error == nil else {return}
let text = observations.compactMap({
$0.topCandidates(1).first?.string
}).joined(separator: ", ")
print(text)
self?.handleResult(text)
}
request.recognitionLevel = .fast
DispatchQueue.global(qos: .userInteractive).async {
do {
try handler.perform([request])
} catch {
print ("Error")
}
}
}
Как и предыдущие случаи код опирается на стартовый проект, к котором нам нужно использовать класс-предсказатель. Возвращаемый объект аналогичен предыдущему проекту. Исходный код можно загрузить с GitHub.
Область применения данного приложения многократно выше – при помощи VNRecognizeTextRequest можно уверенно распознать кириллицу / латиницу, надписи автомобильных номеров, номера кредитных карт, идентификационные статистические или VIN коды.
Заключение, сгенерированное ChatGPT 4o :))
Hidden text
"Использование нейронных сетей в iOS-приложениях открывает множество возможностей. Рассмотренные примеры показывают, как AI может улучшить пользовательский опыт и оптимизировать работу серверов. Будущее за интеграцией ИИ, и понимание этих технологий поможет разработчикам создавать более эффективные и востребованные приложения."
Ресурсы в статье:
Apple Machine Learning (ML). «Create ML»: https://habr.com/ru/articles/711400
Табличная классификация и регрессия Apple ML: https://habr.com/ru/articles/712094
Стартовый проект: https://github.com/DemonSoft/AI-Starting
Распознавание людей: https://github.com/DemonSoft/AI-Person-Detection
Распознавание шрифта: https://github.com/DemonSoft/AI-SymbolsDetect
Распознавание текстов: https://github.com/DemonSoft/AI-TextRecognition
Нейросеть. YOLO: https://github.com/pjreddie/darknet
Нейросеть CoreOCR: https://github.com/DrNeuroSurg/CoreOCR
Apple models: https://developer.apple.com/machine-learning/models/
Пообщаться можно в телеграм-канале: https://t.me/swift_pub_ru