С момента представления CoreML прошло уже почти два месяца, волна хайпа поутихла, и настало время напомнить, какие есть другие способы использования машинного обучения непосредственно на девайсах Apple.
В данном туториале речь пойдет об инструменте, представленном на прошлогоднем WWDC — BNNS (Basic Neural Network Subroutines).
Постановка задачи
В качестве примера реализуем простенькую нейронную сеть для вычисления операции «исключающее ИЛИ» (XOR).
Нейронная сеть будет иметь следующую структуру:
Сеть состоит из трех слоёв:
- входной слой (input layer);
- скрытый слой (hidden layer);
- выходной слой (output layer).
Входной слой содержит два нейрона in1 и in2 — это те бинарные значения, которые мы будет подавать на вход сети. Эти два нейрона имеют связь с двумя нейронами на скрытом слое h1 и h2. Именно в скрытом слое будут происходить все вычисления, результат которых будет передаваться в нейрон выходного слоя out.
Как можно заметить, каждый нейрон входного слоя связан с каждым нейроном скрытого слоя, каждый из которых, в свою очередь, связан с нейроном выходного слоя. Слои такого типа называются полносвязными (fully-connected).
Добавим немного «магических» цифр к нашей сети:
Цифры на связях между нейронами называются весами (weights). Цифры, «входящие» в нейроны — баясы (bias).
Нейроны скрытого слоя выполняют следующие вычисления:
h1 = sigmoid(in1 * w1 + in2 * w2 + b1)
h2 = sigmoid(in1 * w3 + in2 * w4 + b2),
где w1, w2, w3, w4 — веса, а b1 и b2 — значения баясов.
sigmoid() — математическая функция, являющаяся одной из функция активации нейрона, и называющаяся сигмоидной.
Сигмоидная функция выражается следующей формулой
func sigmoid(x) {
return 1 / (1 + exp(-x))
}
и имеет вот такой график:
Из графика видно, что значение сигмоидной функции всегда находится в промежутке от 0 до 1, что очень подходит для нашей задачи. Если значение x меньше 0, то результат функции будет приближен к 0, если же больше 0, то к 1.
Помимо сигмоидной функции, также существуют многие другие функции активации, например, гиперболический тангенс или выпрямленная линейная функция активации (ReLU). У каждой функции есть свои преимущества и недостатки, но их описание выходит за рамки данной статьи.
Решение задачи
Закончим с сухой теорией и перейдет к практики. Реализовывать нейронную сеть будем в плэйграунде, который назовём BNNS_XOR.
Первым делом импортируем фрэймворк Accelerate, внутри которого и находится BNNS:
import Accelerate
Создадим структуру Network, у которой будет два приватных свойства hiddenLayer и outputLayer, а также два метода createNetwork() и predict(a: Float, b: Float):
struct Network {
private var hiddenLayer: BNNSFilter
private var outputLayer: BNNSFilter
mutating func createNetwork() {
}
mutating func predict(a: Float, b: Float) {
}
}
BNNSFilter и есть представление слоёв сети. Благодаря такой реализации, нейроны слоя являются простыми переменными, и вся вычислительная логика с их значения происходит внутри фильтра.
Внутри метода createNetwork() определим массивы весов связей и баясов нейронов, значения которых были получения в результате обучения сети, реализованной на python:
let inputToHiddenWeights: [Float] = [ -65, 66, -68, 67]
let inputToHiddenBias: [Float] = [ 32, -35]
let hiddenToOutputWeights: [Float] = [ -112, 117]
let hiddenToOutputBias: [Float] = [ 53 ]
Далее необходимо задать нашу функцию активации. Как я уже раньше говорил, использовать мы будет сигмоидную функцию:
let activation = BNNSActivation(function: BNNSActivationFunctionSigmoid, alpha: 0, beta: 0, iscale: 0, ioffset: 0, ishift: 0, iscale_per_channel: nil, ioffset_per_channel: nil, ishift_per_channel: nil)
Для описания структуры слоёв используются несколько внутренних структур. Первая из них- BNNSLayerData служит для описания параметров слоя (весов или баясов):
let inputToHiddenWeightsData = BNNSLayerData(data: inputToHiddenWeights, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
let inputToHiddenBiasData = BNNSLayerData(data: inputToHiddenBias, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
Вторая- BNNSFullyConnectedLayerParameters непосредственно «связывает» воедино веса, баясы и функцию активации:
var inputToHiddenParams = BNNSFullyConnectedLayerParameters(in_size: 2, out_size: 2, weights: inputToHiddenWeightsData, bias: inputToHiddenBiasData, activation: activation)
За описания результата вычислений внутри слоя отвечает структура BNNSVectorDescriptor:
var inputDesc = BNNSVectorDescriptor(size: 2, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0)
Сам слой(в терминах BNNS- фильтр) создаётся с помощью функции BNNSFilterCreateFullyConnectedLayer, которая и принимает на вход все ранее описанные структуры:
hiddenLayer = BNNSFilterCreateFullyConnectedLayer(&inputDesc, &hiddenDesc, &inputToHiddenParams, nil)
После описания параметров выходного слоя метод createNetwork() примет вид:
mutating func createNetwork() -> Bool {
let inputToHiddenWeights: [Float] = [ -65, 66, -68, 67]
let inputToHiddenBias: [Float] = [ 32, -35]
let hiddenToOutputWeights: [Float] = [ -112, 117]
let hiddenToOutputBias: [Float] = [ 53 ]
let activation = BNNSActivation(function: BNNSActivationFunctionSigmoid, alpha: 0, beta: 0, iscale: 0, ioffset: 0, ishift: 0, iscale_per_channel: nil, ioffset_per_channel: nil, ishift_per_channel: nil)
let inputToHiddenWeightsData = BNNSLayerData(data: inputToHiddenWeights, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
let inputToHiddenBiasData = BNNSLayerData(data: inputToHiddenBias, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
var inputToHiddenParams = BNNSFullyConnectedLayerParameters(in_size: 2, out_size: 2, weights: inputToHiddenWeightsData, bias: inputToHiddenBiasData, activation: activation)
var inputDesc = BNNSVectorDescriptor(size: 2, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0)
let hiddenToOutputWeightsData = BNNSLayerData(data: hiddenToOutputWeights, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
let hiddenToOutputBiasData = BNNSLayerData(data: hiddenToOutputBias, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0, data_table: nil)
var hiddenToOutputParams = BNNSFullyConnectedLayerParameters(in_size: 2, out_size: 1, weights: hiddenToOutputWeightsData, bias: hiddenToOutputBiasData, activation: activation)
var hiddenDesc = BNNSVectorDescriptor(size: 2, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0)
hiddenLayer = BNNSFilterCreateFullyConnectedLayer(&inputDesc, &hiddenDesc, &inputToHiddenParams, nil)
if hiddenLayer == nil {
print("BNNSFilterCreateFullyConnectedLayer - ошибка при создании скрытого слоя")
return false
}
var outputDesc = BNNSVectorDescriptor(size: 1, data_type: BNNSDataTypeFloat32, data_scale: 0, data_bias: 0)
outputLayer = BNNSFilterCreateFullyConnectedLayer(&hiddenDesc, &outputDesc, &hiddenToOutputParams, nil)
if outputLayer == nil {
print("BNNSFilterCreateFullyConnectedLayer - ошибка при создании выходного слоя")
return false
}
return true
}
Перейдем к написанию метода predict(a: Float, b: Float).
Внутри него в случае успешного создания сети мы создадим массивы нейронов (напомню, что благодаря реализации BNNS, они являются всего лишь переменным). После чего последовательно применим функцию BNNSFilterApply к скрытому слою и к выходному:
var status = BNNSFilterApply(hiddenLayer, input, &hidden)
if status != 0 {
print("BNNSFilterApply - ошибка в скрытом слое")
}
status = BNNSFilterApply(outputLayer, hidden, &output)
if status != 0 {
print("BNNSFilterApply - ошибка в выходном слое")
}
В результате функция примет вид:
mutating func predict(a: Float, b: Float) {
if createNetwork() {
let input = [a, b]
var hidden: [Float] = [0, 0]
var output: [Float] = [0]
var status = BNNSFilterApply(hiddenLayer, input, &hidden)
if status != 0 {
print("BNNSFilterApply - ошибка в скрытом слое")
}
status = BNNSFilterApply(outputLayer, hidden, &output)
if status != 0 {
print("BNNSFilterApply - ошибка в выходном слое")
}
print("\(a) ^ \(b) = \(output[0])")
}
}
Что бы протестировать нашу сеть, создадим экземпляр структуры и вызовем метод predict(a: Float, b: Float) для разных значений:
var net = Network()
net.predict(a: 0, b: 0)
net.predict(a: 0, b: 1)
net.predict(a: 1, b: 0)
net.predict(a: 1, b: 1)
В консоли мы увидем наши заветные значения:
Вот мы и реализовали простую нейронную сеть на Swift.
Проект можно найти здесь.
Следите за обновлениями и подписывайтесь на группу в ВК: cocoa-beans.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Pingback: Digest MBLTdev — свежак для iOS-разработчиков / Блог компании e-Legion Ltd. / Хабрахабр ⋆ Русский Эпик()
Pingback: Digest MBLTdev — свежак для iOS-разработчиков | Все новости - Самые последние новости!()