Узнайте, как построить и обучить сеть глубокого обучения распознаванию чисел (MNIST), как преобразовать ее в формат CoreML, чтобы затем развернуть на вашем iPhoneX и заставить распознавать числа в реальном времени!
Этот пост изначально был размещен в блоге Liip здесь и перепечатан с разрешения автора. Мы также рекомендуем прочитать другие сообщения Томаса Эберманна о анализе настроений с помощью Keras и Стеке Data Science!
- Томас Эберманн (на Medium под именем plotti)
Создание модели CoreML от А до Я менее чем за 10 шагов
Это третья часть нашей серии статей по глубокому обучению мобильных телефонов. В первой части я показал вам два основных приема использования сверток и пулов для обучения сетей глубокого обучения. Во второй части я показал вам как обучить существующие сети глубокого обучения, такие как resnet50, обнаруживать новые объекты. В третьей части я покажу вам, как обучить сеть глубокого обучения, как преобразовать ее в формат CoreML, а затем развернуть на вашем мобильном телефоне!
TL; DR: я покажу вам, как создать собственное приложение для iPhone от A до Z, которое распознает рукописные числа:
Давайте начнем!
1. Как начать
Чтобы получить полностью рабочий пример, я подумал, что мы начнем с набора данных игрушек, такого как набор рукописных букв MNIST, и обучим сеть глубокого обучения распознавать их. Как только он заработает на нашем ПК, мы перенесем его на iPhone X, используя стандарт CoreML.
2. Получение данных
# Importing the dataset with Keras and transforming it from keras.datasets import mnist from keras import backend as K
def mnist_data(): # input image dimensions img_rows, img_cols = 28, 28 (X_train, Y_train), (X_test, Y_test) = mnist.load_data()
if K.image_data_format() == 'channels_first': X_train = X_train.reshape(X_train.shape[0], 1, img_rows, img_cols) X_test = X_test.reshape(X_test.shape[0], 1, img_rows, img_cols) input_shape = (1, img_rows, img_cols) else: X_train = X_train.reshape(X_train.shape[0], img_rows, img_cols, 1) X_test = X_test.reshape(X_test.shape[0], img_rows, img_cols, 1) input_shape = (img_rows, img_cols, 1)
# rescale [0,255] --> [0,1] X_train = X_train.astype('float32')/255 X_test = X_test.astype('float32')/255
# transform to one hot encoding Y_train = np_utils.to_categorical(Y_train, 10) Y_test = np_utils.to_categorical(Y_test, 10)
return (X_train, Y_train), (X_test, Y_test)
(X_train, Y_train), (X_test, Y_test) = mnist_data()
3. Правильное кодирование
При работе с данными изображения мы должны различать, как мы хотим их кодировать. Поскольку Keras - это библиотека высокого уровня, которая может работать с несколькими бэкэндами, такими как Tensorflow, Theano или CNTK, мы должны сначала выяснить, как наш бэкэнд кодирует данные. Он может быть закодирован сначала каналы или каналы последними, что является значением по умолчанию в Tensorflow в бэкэнде Keras по умолчанию. Итак, в нашем случае, когда мы используем Tensorflow, это будет тензор (batch_size, rows, cols, channels). Итак, мы сначала вводим batch_size, затем 28 строк изображения, затем 28 столбцов изображения и затем 1 для количества каналов, поскольку у нас есть данные изображения в оттенках серого.
Мы можем взглянуть на первые 5 загруженных изображений с помощью следующего фрагмента:
# plot first six training images import matplotlib.pyplot as plt %matplotlib inline import matplotlib.cm as cm import numpy as np
(X_train, y_train), (X_test, y_test) = mnist.load_data()
fig = plt.figure(figsize=(20,20)) for i in range(6): ax = fig.add_subplot(1, 6, i+1, xticks=[], yticks=[]) ax.imshow(X_train[i], cmap='gray') ax.set_title(str(y_train[i]))
4. Нормализация данных
Мы видим, что на черном фоне есть белые цифры, каждое из которых жирно написано посередине, и они имеют довольно низкое разрешение - в нашем случае 28 пикселей на 28 пикселей.
Вы заметили, что выше мы масштабируем каждый пиксель изображения, деля его на 255. Это приводит к значениям пикселей от 0 до 1, что весьма полезно для любого вида обучения. Таким образом, каждое из значений пикселей изображения до преобразования выглядит следующим образом:
# visualize one number with pixel values def visualize_input(img, ax): ax.imshow(img, cmap='gray') width, height = img.shape thresh = img.max()/2.5 for x in range(width): for y in range(height): ax.annotate(str(round(img[x][y],2)), xy=(y,x), horizontalalignment='center', verticalalignment='center', color='white' if img[x][y]<thresh else 'black')
fig = plt.figure(figsize = (12,12)) ax = fig.add_subplot(111) visualize_input(X_train[0], ax)
Как вы заметили, каждый из серых пикселей имеет значение от 0 до 255, где 255 - белый, а 0 - черный. Обратите внимание, что здесь mnist.load_data()
загружает исходные данные в X_train [0]. Когда мы пишем нашу пользовательскую функцию mnist_data (), мы преобразуем интенсивность каждого пикселя в значение 0-1, вызывая X_train = X_train.astype('float32')/255
.
5. Одно горячее кодирование
Первоначально данные кодируются таким образом, что Y-вектор содержит числовое значение, которое содержит X-вектор (пиксельные данные). Так, например, если он выглядит как 7, Y-вектор содержит число 7. Нам нужно выполнить это преобразование, потому что мы хотим сопоставить наш вывод с 10 выходными нейронами в нашей сети, которые срабатывают при распознавании соответствующего числа.
6. Моделирование сети.
Теперь пора определить сверточную сеть, чтобы различать эти числа. Используя приемы свертки и объединения из первой части этой серии статей, мы можем смоделировать сеть, которая сможет отличать числа друг от друга.
# defining the model from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten from keras.layers import Conv2D, MaxPooling2D def network(): model = Sequential() input_shape = (28, 28, 1) num_classes = 10
model.add(Conv2D(filters=32, kernel_size=(3, 3), padding='same', activation='relu', input_shape=input_shape)) model.add(MaxPooling2D(pool_size=2)) model.add(Conv2D(filters=32, kernel_size=2, padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Conv2D(filters=32, kernel_size=2, padding='same', activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.3)) model.add(Flatten()) model.add(Dense(500, activation='relu')) model.add(Dropout(0.4)) model.add(Dense(num_classes, activation='softmax'))
# summarize the model # model.summary() return model
Так что мы там делали? Мы начали с свертки с размером ядра 3. Это означает, что размер окна составляет 3x3 пикселя. Форма ввода - это наши 28x28 пикселей. Затем за этим слоем последовал слой максимального объединения. Здесь размер pool_size равен двум, поэтому мы уменьшаем масштаб всего на 2. Итак, теперь наш вход для следующего сверточного слоя равен 14 x 14. Затем мы повторили это еще два раза, в итоге получая вход для последнего сверточного слоя 3x3. Затем мы используем слой исключения, где мы случайным образом устанавливаем 30% входных единиц на 0, чтобы предотвратить переобучение при обучении. Наконец, мы сглаживаем входные слои (в нашем случае 3x3x32 = 288) и соединяем их с плотным слоем с 500 входами. После этого шага мы добавляем еще один слой исключения и, наконец, соединяем его с нашим плотным слоем с 10 узлами, что соответствует нашему количеству классов (как в количестве от 0 до 9).
7. Обучение модели.
#Training the model model.compile(loss='categorical_crossentropy', optimizer=keras.optimizers.Adadelta(), metrics=['accuracy'])
model.fit(X_train, Y_train, batch_size=512, epochs=6, verbose=1,validation_data=(X_test, Y_test))
score = model.evaluate(X_test, Y_test, verbose=0)
print('Test loss:', score[0]) print('Test accuracy:', score[1])
Сначала мы компилируем сеть, определяя функцию потерь и оптимизатор: в нашем случае мы выбираем category_crossentropy, потому что у нас есть несколько категорий (как в числах 0–9). Keras предлагает ряд оптимизаторов, так что вы можете попробовать несколько и выбрать тот, который лучше всего подходит для вашего случая. Я обнаружил, что AdaDelta (расширенная форма AdaGrad) мне подходит.
Итак, после обучения я получил модель с точностью 98%, что является отличным показателем, учитывая довольно простую сетевую инфраструктуру. На скриншоте также видно, что в каждую эпоху точность увеличивалась, так что мне все хорошо. Теперь у нас есть модель, которая может довольно хорошо предсказывать числа 0–9 по их представлению 28x28 пикселей.
8. Сохранение модели.
Поскольку мы хотим использовать модель на нашем iPhone, мы должны преобразовать ее в формат, который понимает наш iPhone. На самом деле Microsoft, Facebook и Amazon (и другие) продолжают инициативу по гармонизации всех различных сетевых форматов глубокого обучения, чтобы иметь взаимозаменяемый формат обмена открытыми нейронными сетями, который вы можете использовать на любом устройстве. Называется он ONNX.
Однако на сегодняшний день устройства Apple работают только с форматом CoreML. Для преобразования нашей модели Keras в CoreML Apple, к счастью, предоставляет очень удобную вспомогательную библиотеку под названием coremltools, которую мы можем использовать для выполнения работы. Он может конвертировать модели scikit-learn, модели Keras и XGBoost в CoreML, таким образом покрывая довольно много повседневных приложений. Установите его с помощью pip install coremltools, и тогда вы легко сможете им пользоваться.
coreml_model = coremltools.converters.keras.convert(model,
input_names="image",
image_input_names='image',
class_labels=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
)
Наиболее важными параметрами являются class_labels, они определяют, сколько классов модель пытается предсказать, а также имена входов или image_input_names. Установив их на изображение, XCode автоматически распознает, что эта модель берет изображение и пытается что-то из него предсказать. В зависимости от вашего приложения имеет смысл изучить документацию, особенно если вы хотите убедиться, что она кодирует каналы RGB в одном и том же порядке (параметр is_bgr) или чтобы убедиться, что она правильно предполагает, что все входные данные являются значениями. от 0 до 1 (параметр image_scale).
Осталось только добавить метаданные к вашей модели. Этим вы очень помогаете всем разработчикам, поскольку им не нужно гадать, как работает ваша модель и что она ожидает в качестве входных данных.
#entering metadata coreml_model.author = 'plotti' coreml_model.license = 'MIT' coreml_model.short_description = 'MNIST handwriting recognition with a 3 layer network' coreml_model.input_description['image'] = '28x28 grayscaled pixel values between 0-1' coreml_model.save('SimpleMnist.mlmodel')
print(coreml_model)
9. Используйте его, чтобы что-то предсказать.
После сохранения модели в модели CoreML мы можем проверить, правильно ли она работает на нашей машине. Для этого мы можем накормить его изображением и попытаться увидеть, правильно ли он предсказывает метку. Вы можете использовать данные обучения MNIST или сделать снимок на телефон и передать его на свой компьютер, чтобы увидеть, насколько хорошо модель обрабатывает реальные данные.
#Use the core-ml model to predict something
from PIL import Image
import numpy as np
model = coremltools.models.MLModel('SimpleMnist.mlmodel')
im = Image.fromarray((np.reshape(mnist_data()[0][0][12]*255, (28, 28))).astype(np.uint8),"L")
plt.imshow(im)
predictions = model.predict({'image': im})
print(predictions)
Работает ура! Пришло время включить его в проект в XCode.
Перенос нашей модели в XCode за 10 шагов
Позвольте мне начать с того, что я ни в коем случае не являюсь разработчиком XCode или Mobile. Я изучил довольно много супер полезных руководств, пошаговых руководств и видео о том, как создать простое приложение для мобильного телефона с CoreML, и использовал их для создания своего приложения. Я могу только сказать большое спасибо и похвалы сообществу, которое так открыто и полезно.
1. Установите XCode
Пришло время по-настоящему запачкать руки. Прежде чем вы сможете что-либо сделать, у вас должен быть XCode. Так что скачайте его через Apple-Store и установите. Если он у вас уже есть, убедитесь, что у вас версия не ниже 9 и выше.
2. Создайте проект.
Запустите XCode и создайте приложение с одним представлением. Назовите свой проект соответствующим образом. Я назвал свои «числа». Выберите место для сохранения. Вы можете оставить флажок «Создать репозиторий git на моем Mac».
3. Добавьте модель CoreML.
Теперь мы можем добавить модель CoreML, созданную с помощью конвертера coremltools. Просто перетащите модель в каталог вашего проекта. Обязательно перетащите его в правильную папку (см. Снимок экрана). Вы можете использовать опцию «добавить как ссылку», например, каждый раз, когда вы обновляете свою модель, вам не нужно снова перетаскивать ее в свой проект, чтобы обновить ее. XCode должен автоматически распознать вашу модель и понять, что это модель, которая будет использоваться для изображений.
4. Удалите представление или раскадровку.
Поскольку мы собираемся использовать только камеру и отображать метку, нам не нужен причудливый графический интерфейс пользователя - или, другими словами, слой просмотра. Поскольку раскадровка в Swing соответствует представлению в шаблоне MVC, мы просто удалим ее. В информации о развертывании настроек проекта не забудьте также удалить основной интерфейс (см. Снимок экрана), установив его пустым.
5. Программно создайте корневой контроллер представления.
Вместо этого мы собираемся создать корневой контроллер представления программно, заменив funct application
в AppDelegate.swift следующим кодом:
// create the view root controller programmatically func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // create the user interface window, make it visible window = UIWindow() window?.makeKeyAndVisible()
// create the view controller and make it the root view controller let vc = ViewController() window?.rootViewController = vc
// return true upon success return true }
6. Создайте контроллер представления.
Наконец, пришло время создать контроллер представления. Мы будем использовать UIKit - библиотеку для создания кнопок и меток, AVFoundation - библиотеку для захвата камеры на iPhone и Vision - библиотеку для обработки нашей модели CoreML. Последнее особенно удобно, если вы не хотите самостоятельно изменять размер входных данных.
В Viewcontroller мы собираемся унаследовать функциональные возможности UI и AV, поэтому нам нужно позже перезаписать некоторые методы, чтобы они работали.
Первое, что мы сделаем, это создадим ярлык, который будет сообщать нам, что видит камера. Переопределив функцию viewDidLoad
, мы активируем захват камеры и добавим метку к виду.
В функции setupCaptureSession
мы создадим сеанс захвата, возьмем первую камеру (которая находится на передней панели) и захватим ее результат в captureOutput
, а также отобразим его на previewLayer
.
В функции captureOutput
мы, наконец, воспользуемся нашей моделью CoreML, которую мы импортировали ранее. Обязательно нажмите Cmd + B - build при импорте, чтобы XCode знал, что это действительно так. Мы будем использовать его, чтобы что-то предсказать по сделанному нами изображению. Затем мы возьмем первый прогноз модели и отобразим его на нашей этикетке.
\\define the ViewController import UIKit import AVFoundation import Vision
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { // create a label to hold the Pokemon name and confidence let label: UILabel = { let label = UILabel() label.textColor = .white label.translatesAutoresizingMaskIntoConstraints = false label.text = "Label" label.font = label.font.withSize(40) return label }()
override func viewDidLoad() { // call the parent function super.viewDidLoad() setupCaptureSession() // establish the capture view.addSubview(label) // add the label setupLabel() }
func setupCaptureSession() { // create a new capture session let captureSession = AVCaptureSession()
// find the available cameras let availableDevices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .back).devices
do { // select the first camera (front) if let captureDevice = availableDevices.first { captureSession.addInput(try AVCaptureDeviceInput(device: captureDevice)) } } catch { // print an error if the camera is not available print(error.localizedDescription) }
// setup the video output to the screen and add output to our capture session let captureOutput = AVCaptureVideoDataOutput() captureSession.addOutput(captureOutput) let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.frame view.layer.addSublayer(previewLayer)
// buffer the video and start the capture session captureOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue")) captureSession.startRunning() }
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // load our CoreML Pokedex model guard let model = try? VNCoreMLModel(for: SimpleMnist().model) else { return }
// run an inference with CoreML let request = VNCoreMLRequest(model: model) { (finishedRequest, error) in
// grab the inference results guard let results = finishedRequest.results as? [VNClassificationObservation] else { return }
// grab the highest confidence result guard let Observation = results.first else { return }
// create the label text components let predclass = "\(Observation.identifier)"
// set the label text DispatchQueue.main.async(execute: { self.label.text = "\(predclass) " }) }
// create a Core Video pixel buffer which is an image buffer that holds pixels in main memory // Applications generating frames, compressing or decompressing video, or using Core Image // can all make use of Core Video pixel buffers guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
// execute the request try? VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]).perform([request]) }
func setupLabel() { // constrain the label in the center label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
// constrain the the label to 50 pixels from the bottom label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -50).isActive = true } }
Убедитесь, что вы изменили часть модели на название вашей модели. В противном случае вы получите ошибки сборки.
7. Добавить сообщение о конфиденциальности
Наконец, поскольку мы собираемся использовать камеру, нам необходимо сообщить пользователю, что мы собираемся это сделать, и, таким образом, добавить сообщение о конфиденциальности «Конфиденциальность - Описание использования камеры» в файле Info.plist в разделе «Список свойств информации».
8. Добавьте команду сборки.
Чтобы установить приложение на свой мобильный iPhone, вам необходимо зарегистрироваться в программе для разработчиков Apple. Для этого не нужно платить деньги, регистрироваться можно и без комиссии. После регистрации вы можете выбрать команду, которую Apple называет так), которую вы зарегистрировали там, в свойствах проекта.
9. Разверните на своем iPhone.
Наконец, пришло время развернуть модель на вашем iPhone. Вам нужно будет подключить его через USB, а затем разблокировать. После того, как он будет разблокирован, вам нужно выбрать место назначения в разделе Продукт - Место назначения - Ваш iPhone. Затем остается только запустить его на своем мобильном телефоне: выберите «Продукт» - «Выполнить» (или просто нажмите CMD + R) в меню, и XCode создаст и развернет проект на вашем iPhone.
10. Попробуйте
После того, как пришлось преодолеть столько препятствий, наконец-то пришло время опробовать наше приложение. Если вы запускаете его впервые, он попросит вас разрешить ему использовать вашу камеру (в конце концов, мы разместили эту информацию там). Затем обязательно держите iPhone боком, так как это имеет значение для того, как мы обучили сеть. Мы не использовали какие-либо техники увеличения, поэтому наша модель не может распознавать числа, которые лежат на боку. Мы могли бы улучшить нашу модель, применив эти методы, как я показал в этой статье блога.
Вторая вещь, которую вы могли заметить, это то, что приложение всегда распознает какое-то число, так как не существует «фонового» класса. Чтобы исправить это, мы могли бы дополнительно обучить модель на некоторых случайных изображениях, которые мы классифицируем как фоновый класс. Таким образом, наша модель будет лучше оснащена, чтобы различать, видит ли она число или какой-то случайный фон.
Заключение или знаменитое «ну и что»
Очевидно, это очень длинный пост в блоге. Тем не менее, я хотел собрать всю необходимую информацию в одном месте, чтобы показать другим мобильным разработчикам, как легко создавать собственные приложения для глубокого обучения компьютерного зрения. В нашем случае с Liip, это наверняка будет сводиться к сотрудничеству между нашей командой служб данных и нашими мобильными разработчиками, чтобы получить лучшее из обоих миров.
Фактически, в настоящее время мы вместе вводим новшества, создав приложение, которое сможет распознавать животных в зоопарке, и работаем над другой небольшой забавной игрой, которая позволяет двум людям рисовать друг против друга: вам будет дана задача, как в Нарисуйте яблоко, и побеждает тот, кто быстрее нарисует яблоко таким образом, чтобы оно было распознано моделью глубокого обучения.
Помимо таких увлекательных инновационных проектов возможности безграничны, но всегда зависят от контекста бизнеса и пользователей. Очевидно, что поговорка если у вас есть молоток, каждая проблема кажется вам гвоздем применима и здесь, не каждое приложение выиграет от наличия компьютерного зрения, и не все приложения, использующие компьютерное зрение, являются полезными, как некоторые из вас может знать из знаменитого эпизода Кремниевой долины.
Тем не менее, есть немало хороших примеров приложений, успешно использующих компьютерное зрение:
- Leafsnap позволяет различать разные типы листьев.
- Айполы помогает слабовидящим людям познавать мир.
- Snooth предоставит вам больше информации о вашем вине, сфотографировав этикетку.
- Pinterest запустил визуальный поиск, который позволяет искать пины, соответствующие продукту, который вы сняли на свой телефон.
- Caloriemama позволяет сфотографировать вашу пищу и сказать, сколько в ней калорий.
Как обычно, код, который вы видели в этом сообщении блога, доступен в Интернете. Не стесняйтесь экспериментировать с этим. Я с нетерпением жду ваших комментариев и надеюсь, что вам понравилось путешествие. P.S. Я хотел бы поблагодарить Стефани Тэпке за вычитку и за ее полезные комментарии, которые сделали этот пост более читабельным.
Эта статья оказалась полезной? Подпишитесь на нас (Comet.ml) на Medium и ознакомьтесь с другими соответствующими статьями ниже! Пожалуйста, поделитесь этой статьей!
- Примечания к выпуску Comet.ml - ежедневно обновляются новыми функциями и исправлениями!
- Мониторинг результатов модели машинного обучения в прямом эфире из Jupyter Notebooks
- Построение надежных моделей машинного обучения с перекрестной проверкой