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

В данном туториале речь пойдет об инструменте, представленном на прошлогоднем WWDC — BNNS (Basic Neural Network Subroutines).

Постановка задачи

В качестве примера реализуем простенькую нейронную сеть для вычисления операции «исключающее ИЛИ» (XOR).

Group 1

Нейронная сеть будет иметь следующую структуру:

Netowrk

Сеть состоит из трех слоёв:

  • входной слой (input layer);
  • скрытый слой (hidden layer);
  • выходной слой (output layer).

Входной слой содержит два нейрона in1 и in2 — это те бинарные значения, которые мы будет подавать на вход сети. Эти два нейрона имеют связь с двумя нейронами на скрытом слое h1 и h2. Именно в скрытом слое будут происходить все вычисления, результат которых будет передаваться в нейрон выходного слоя out.

Как можно заметить, каждый нейрон входного слоя связан с каждым нейроном скрытого слоя, каждый из которых, в свою очередь, связан с нейроном выходного слоя. Слои такого типа называются полносвязными (fully-connected).

Добавим немного «магических» цифр к нашей сети:

Netowrk_with_params

 

Цифры на связях между нейронами называются весами (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))
} 

и имеет вот такой график:

sigmod

Из графика видно, что значение сигмоидной функции всегда находится в промежутке от 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 и есть представление слоёв сети. Благодаря такой реализации, нейроны слоя являются простыми переменными, и вся вычислительная логика с их значения происходит внутри фильтра.

Netowrk_filters

 

 

Внутри метода 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)

В консоли мы увидем наши заветные значения:

consol

Вот мы и реализовали простую нейронную сеть на Swift.

Проект можно найти здесь.

Следите за обновлениями и подписывайтесь на группу в ВК: cocoa-beans.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.