├── .gitignore ├── LICENSE ├── NeuralNetwork.playgroundbook └── Contents │ ├── Chapters │ ├── Chapter0.playgroundchapter │ │ ├── Manifest.plist │ │ ├── Pages │ │ │ └── PlaygroundPage.playgroundpage │ │ │ │ ├── Contents.swift │ │ │ │ ├── LiveView.swift │ │ │ │ └── Manifest.plist │ │ └── PrivateResources │ │ │ └── Base.lproj │ │ │ └── ManifestPlist.strings │ ├── Chapter1.playgroundchapter │ │ ├── Manifest.plist │ │ ├── Pages │ │ │ └── PlaygroundPage.playgroundpage │ │ │ │ ├── Contents.swift │ │ │ │ ├── LiveView.swift │ │ │ │ └── Manifest.plist │ │ └── PrivateResources │ │ │ ├── Base.lproj │ │ │ └── ManifestPlist.strings │ │ │ └── XOR@2x.png │ ├── Chapter2.playgroundchapter │ │ ├── Manifest.plist │ │ ├── Pages │ │ │ ├── PlaygroundPage.playgroundpage │ │ │ │ ├── Contents.swift │ │ │ │ ├── LiveView.swift │ │ │ │ └── Manifest.plist │ │ │ ├── PlaygroundPageThree.playgroundpage │ │ │ │ ├── Contents.swift │ │ │ │ ├── LiveView.swift │ │ │ │ └── Manifest.plist │ │ │ └── PlaygroundPageTwo.playgroundpage │ │ │ │ ├── Contents.swift │ │ │ │ ├── LiveView.swift │ │ │ │ └── Manifest.plist │ │ └── PrivateResources │ │ │ └── Base.lproj │ │ │ └── ManifestPlist.strings │ └── Chapter3.playgroundchapter │ │ ├── Manifest.plist │ │ ├── Pages │ │ └── PlaygroundPage.playgroundpage │ │ │ ├── Contents.swift │ │ │ └── Manifest.plist │ │ └── PrivateResources │ │ └── Base.lproj │ │ └── ManifestPlist.strings │ ├── Manifest.plist │ ├── PrivateResources │ ├── Base.lproj │ │ └── ManifestPlist.strings │ ├── Glossary.plist │ ├── cover.png │ └── iris.csv │ └── Sources │ ├── DataInput │ ├── ArtificalDataSets.swift │ ├── CSVDataSet.swift │ ├── DataSet.swift │ ├── Iris │ │ └── IrisDataSet.swift │ └── XORDataSet.swift │ ├── Helper │ ├── CGRect+Extension.swift │ ├── ColorHelper.swift │ ├── Constants.swift │ ├── InterpolationHelper.swift │ └── UIBezierPath+Extension.swift │ ├── Model │ └── NetworkModel.swift │ ├── NeuralNetwork │ ├── ActivationFunction.swift │ ├── EpochReport.swift │ ├── ErrorFunction.swift │ ├── Layer.swift │ ├── Mat.swift │ ├── NeuralNetwork.swift │ └── TestReport.swift │ ├── Playground Support │ ├── NetworkVisualisationState.swift │ ├── PlaygroundLiveViewController.swift │ ├── PlaygroundLiveViewSessionManager.swift │ ├── PlaygroundNetworkVisualisationViewController.swift │ ├── PlaygroundPageExtension.swift │ ├── PlaygroundPageSessionManager.swift │ ├── PlaygroundRemoteNeuralNetworkDebugOutputHandler.swift │ ├── PlaygroundStore.swift │ └── StartedTrainingPlaygroundMessage.swift │ └── UI │ ├── AxisLayer.swift │ ├── ConnectionView.swift │ ├── DecisionBoundaryPlotView.swift │ ├── DecisionBoundaryView.swift │ ├── GridLayer.swift │ ├── LayerView.swift │ ├── LinePlotView.swift │ ├── NetworkView.swift │ ├── NetworkVisualisationViewController.swift │ ├── NeuronView.swift │ ├── PlotView.swift │ ├── ProptionalStackViewChildContainerView.swift │ └── SquaredContainerView.swift ├── NeuralNetwork.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── xcbaselines │ └── 98F865532205C85B00167E55.xcbaseline │ │ ├── 1A90B905-91A3-4577-9043-5B805DE93C46.plist │ │ └── Info.plist │ └── xcschemes │ └── NeuralNetwork.xcscheme ├── NeuralNetwork ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── DebugStoryboard.storyboard ├── Info.plist └── ViewController.swift ├── NeuralNetworkTests ├── DataSetTests.swift ├── Info.plist ├── InterpolationHelperTests.swift ├── MatTests.swift ├── NetworkModelTests.swift ├── NeuralNetworkTests.swift └── testModel.json ├── PlaygroundSupport ├── Info.plist ├── PlaygroundPage.swift └── PlaygroundSupport.h ├── README.md └── Screenshots └── PlaygroundDemo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Leo Thomas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterZeroName 7 | Pages 8 | 9 | PlaygroundPage.playgroundpage 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | 3 | # Introduction to Neural Networks 4 | 5 | Neural network is a machine learning strategy whose structure is loosely based on the structure of a brain's neurons. 6 | 7 | The network consists of several layers of neurons. All neurons are connected to all neurons from the previous and preceding layers. If the network is fed with data, the connections that lead to a correct response are strengthened and connections that lead to errors are weakened. 8 | 9 | Here is an example Network. In the following chapters, you will learn about some details of Neural Networks and develop your own networks. 10 | */ 11 | //#-hidden-code 12 | import PlaygroundSupport 13 | PlaygroundPage.current.needsIndefiniteExecution = true 14 | 15 | func evaluate(result _: TestReport) { 16 | PlaygroundPage.current.assessmentStatus = .pass(message: "[Lets get started and with the \"Hello World\" of Neural Networks](Hello%20World/XOR)") 17 | } 18 | 19 | //#-code-completion(identifier, hide, DecisionBoundaryView, Constants, DecisionBoundaryPlotView, NetworkVisualisationViewController, PlaygroundNetworkVisualisationViewController, PlaygroundLiveViewController, PlaygroundLiveViewSessionManager, PlaygroundStore, PlaygroundPageSessionManager, PlaygroundRemoteNeuralNetworkDebugOutputHandler) 20 | //#-code-completion(identifier, hide, evaluate(result:), outputView) 21 | //#-end-hidden-code 22 | let network = NeuralNetwork(learningRate: /*#-editable-code */0.3/*#-end-editable-code */, epochs: /*#-editable-code */1000/*#-end-editable-code */) 23 | //#-hidden-code 24 | let outputView = PlaygroundNetworkVisualisationViewController() 25 | outputView.page = 0 26 | outputView.setDeciscionBoundaryColorOffset(value: 0) 27 | network.debugOutputHandler = outputView 28 | //#-end-hidden-code 29 | //#-editable-code 30 | network.add(layer: Layer(nodesCount: 2)) 31 | network.add(layer: Layer(nodesCount: 4, activationFunction: .tanh)) 32 | network.add(layer: Layer(nodesCount: 2)) 33 | 34 | let data = ArtificalDataSets.circular() 35 | let (train, test) = data.split(ratio: 0.1) 36 | network.trainAndTest(trainData: train, testData: test) { result in 37 | print(result) 38 | evaluate(result: result) 39 | } 40 | 41 | //#-end-editable-code 42 | /*: 43 | During execution, a plot of the loss function is displayed below the network. The loss function contains the difference between the actual and expected output of the Neural Network. Next to it is a representation of the decision boundary. 44 | 45 | * Callout(Task): 46 | Execute the code 47 | */ 48 | //#-hidden-code 49 | PlaygroundPage.current.liveView = outputView 50 | //#-end-hidden-code 51 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/Pages/PlaygroundPage.playgroundpage/LiveView.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import UIKit 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | let vc = PlaygroundLiveViewController() 6 | PlaygroundPage.current.liveView = vc 7 | vc.page = 0 8 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterZeroPageOneName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/Chapters/Chapter0.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterOneName 7 | Pages 8 | 9 | PlaygroundPage.playgroundpage 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Hello World – XOR 3 | 4 | The layout of a Neural Network depends on the data it is working with and the problem it is supposed to solve. 5 | 6 | - Note: 7 | The input is formated as a Matrix where every row represents an instance and the columns are representing the input features 8 | */ 9 | // The input and expected class for the Neural Network 10 | let data = DataSet(values: [[0, 0]: 0, 11 | [0, 1]: 1, 12 | [1, 0]: 1, 13 | [1, 1]: 0]) 14 | /*: 15 | # XOR 16 | 17 | The initialized dataset is the data for the XOR operation and is one of the smallest non trivial tasks for a Neural Network. 18 | 19 | XOR (exclusive Or) is a logic gate that outputs 1 when exactly one of the two inputs is 1. 20 | 21 | ![XOR Gatter](XOR@2x.png) 22 | 23 | In the current code the input nodes are directly compared to the output nodes. For the network to be able to process and "understand" the input information it needs nodes between input and output, the so-called [hidden layer](glossary://Hidden%20layer). The number of neccesary hidden layers, depennds on the complexity of the problem. 24 | */ 25 | //#-hidden-code 26 | import PlaygroundSupport 27 | PlaygroundPage.current.needsIndefiniteExecution = true 28 | //#-code-completion(identifier, hide, DecisionBoundaryView, Constants, DecisionBoundaryPlotView, NetworkVisualisationViewController, PlaygroundNetworkVisualisationViewController, PlaygroundLiveViewController, PlaygroundLiveViewSessionManager, PlaygroundStore, PlaygroundPageSessionManager, PlaygroundRemoteNeuralNetworkDebugOutputHandler) 29 | //#-code-completion(identifier, hide, evaluate(result:), outputView) 30 | //#-end-hidden-code 31 | let network = NeuralNetwork(learningRate: /*#-editable-code */0.3/*#-end-editable-code */, epochs: /*#-editable-code */3000/*#-end-editable-code */) 32 | 33 | //#-editable-code 34 | // The input layer 35 | network.add(layer: Layer(nodesCount: 2)) 36 | 37 | // "Hidden" layer 38 | // uncomment to add the Layer to the Network 39 | // network.add(layer: Layer(nodesCount: 2, activationFunction: .sigmoid)) 40 | 41 | // The output layer 42 | network.add(layer: Layer(nodesCount: 2, activationFunction: .linear)) 43 | //#-end-editable-code 44 | //#-hidden-code 45 | let outputView = PlaygroundNetworkVisualisationViewController() 46 | outputView.page = 1 47 | network.debugOutputHandler = outputView 48 | 49 | func evaluate(result: TestReport) { 50 | if result.classifications.accuracy == 1 { 51 | PlaygroundPage.current.assessmentStatus = .pass(message: "Good job!") 52 | } else { 53 | PlaygroundPage.current.assessmentStatus = .fail(hints: ["The current accuracy is at \(Int(result.classifications.accuracy * 100))%. The Goal is 100%"], solution: "Uncomment the hidden layer or add your own hidden layer") 54 | } 55 | } 56 | 57 | //#-end-hidden-code 58 | 59 | network.trainAndTest(trainData: data, testData: data) { result in 60 | print(result) 61 | evaluate(result: result) 62 | } 63 | 64 | /*: 65 | * Callout(Task): 66 | Create a Network that can perform the XOR operation with 100% accuracy 67 | */ 68 | //#-hidden-code 69 | PlaygroundPage.current.liveView = outputView 70 | 71 | //#-end-hidden-code 72 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/PlaygroundPage.playgroundpage/LiveView.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import UIKit 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | let vc = PlaygroundLiveViewController() 6 | PlaygroundPage.current.liveView = vc 7 | vc.page = 1 8 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterOnePageOneName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/PrivateResources/XOR@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/PrivateResources/XOR@2x.png -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterTwoName 7 | Pages 8 | 9 | PlaygroundPage.playgroundpage 10 | PlaygroundPageTwo.playgroundpage 11 | PlaygroundPageThree.playgroundpage 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Epochs, Learning Rate and Activation Functions 3 | 4 | Besides the number of nodes and layers there are other parameters to influence the success of the network. 5 | 6 | ## Epochs 7 | The number of iterations that are used for pass the training data through the network. More complex DataSets often and number of hidden layers require more epochs. 8 | 9 | ## Learning Rate 10 | The learning rate determines how quickly the network changes the weights based on the loss. 11 | 12 | ## Activation Function 13 | The activation function defines the output of a node for an input value. It can also be used to set the value to a certain range (e.g. between 0 and 1). 14 | */ 15 | //#-hidden-code 16 | import PlaygroundSupport 17 | PlaygroundPage.current.needsIndefiniteExecution = true 18 | //#-code-completion(identifier, hide, DecisionBoundaryView, Constants, DecisionBoundaryPlotView, NetworkVisualisationViewController, PlaygroundNetworkVisualisationViewController, PlaygroundLiveViewController, PlaygroundLiveViewSessionManager, PlaygroundStore, PlaygroundPageSessionManager, PlaygroundRemoteNeuralNetworkDebugOutputHandler) 19 | //#-code-completion(identifier, hide, evaluate(result:), outputView) 20 | //#-end-hidden-code 21 | let network = NeuralNetwork(learningRate: /*#-editable-code */0.1/*#-end-editable-code */, epochs: /*#-editable-code */2000/*#-end-editable-code */) 22 | /*: 23 | * Note: 24 | Debug-quicklooks might help to get a better understanding of an activation function. Tap one the icon next to the activation function variable after you've run the playground once to get a plot representation. 25 | */ 26 | //#-editable-code 27 | network.add(layer: Layer(nodesCount: 2)) 28 | let activationFunction: ActivationFunction = .sigmoid 29 | network.add(layer: Layer(nodesCount: 4, activationFunction: activationFunction)) 30 | network.add(layer: Layer(nodesCount: 2)) 31 | //#-end-editable-code 32 | 33 | let data = ArtificalDataSets.circular() 34 | 35 | //#-hidden-code 36 | let outputView = PlaygroundNetworkVisualisationViewController() 37 | outputView.setDeciscionBoundaryColorOffset(value: 0) 38 | outputView.page = 2 39 | network.debugOutputHandler = outputView 40 | 41 | func evaluate(result: TestReport) { 42 | if result.classifications.accuracy > 0.9, 43 | network.epochs <= 1000 { 44 | PlaygroundPage.current.assessmentStatus = .pass(message: "You did it!") 45 | } else { 46 | PlaygroundPage.current.assessmentStatus = .fail(hints: ["The current accuracy is at \(Int(result.classifications.accuracy * 100))% for \(network.epochs) epochs. The Goal is 90% in less than 1000 epochs"], solution: "Try using tanh as an activation function for the hidden layer and set the number of epochs to 1000") 47 | } 48 | } 49 | 50 | //#-end-hidden-code 51 | /*: 52 | - Note: 53 | We divide the data set into test and training data. It is important not to use the same data points for testing, as it could be that the model has developed a bias towards the training data. 54 | */ 55 | let (train, test) = data.split(ratio: 0.1) 56 | network.trainAndTest(trainData: train, testData: test) { result in 57 | print(result) 58 | evaluate(result: result) 59 | } 60 | 61 | /*: 62 | * Callout(Task): 63 | Experiment with the amount of Layers and activation functions to create a Network that achieves an accuracy of more than 90% with less than 1000 training epochs 64 | */ 65 | //#-hidden-code 66 | PlaygroundPage.current.liveView = outputView 67 | //#-end-hidden-code 68 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPage.playgroundpage/LiveView.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import UIKit 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | let vc = PlaygroundLiveViewController() 6 | PlaygroundPage.current.liveView = vc 7 | vc.page = 2 8 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterTwoPageOneName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageThree.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Playground 3 | 4 | You've learned about some fundamentals about working with Neural Networks. In this page, you can experiment on our own without restrictions 5 | 6 | As a starting point here is a real world example for the classification of iris flowers using the input features petal length, petal width, sepal length and sepal width. 7 | - Note: 8 | The decision boundary visualisation is only available for networks that work with 2 input features 9 | */ 10 | //#-hidden-code 11 | import PlaygroundSupport 12 | import UIKit 13 | PlaygroundPage.current.needsIndefiniteExecution = true 14 | //#-code-completion(identifier, hide, DecisionBoundaryView, Constants, DecisionBoundaryPlotView, NetworkVisualisationViewController, PlaygroundNetworkVisualisationViewController, PlaygroundLiveViewController, PlaygroundLiveViewSessionManager, PlaygroundStore, PlaygroundPageSessionManager, PlaygroundRemoteNeuralNetworkDebugOutputHandler) 15 | //#-code-completion(identifier, hide, outputView) 16 | 17 | //#-end-hidden-code 18 | let network = NeuralNetwork(learningRate: /*#-editable-code */0.03/*#-end-editable-code */, epochs: /*#-editable-code */2000/*#-end-editable-code */) 19 | //#-hidden-code 20 | let outputView = PlaygroundNetworkVisualisationViewController() 21 | outputView.page = 4 22 | network.debugOutputHandler = outputView 23 | PlaygroundPage.current.liveView = outputView 24 | //#-end-hidden-code 25 | 26 | //#-editable-code 27 | network.add(layer: Layer(nodesCount: 4)) 28 | // we are initialising the layers weights randomly between 0 and 0.01 instead of 0 and 1 29 | network.add(layer: Layer(nodesCount: 5, initialWeightsRange: 0 ..< 0.01, activationFunction: .tanh)) 30 | network.add(layer: Layer(nodesCount: 4, initialWeightsRange: 0 ..< 0.01, activationFunction: .tanh)) 31 | network.add(layer: Layer(nodesCount: 3)) 32 | 33 | let filePath = Bundle.main.path(forResource: "iris", ofType: "csv")! 34 | // Load dataset from CSV file 35 | let data = CSVDataSet(path: filePath) { columns in 36 | // preprocess csv columns 37 | let input = columns[0 ..< 4].compactMap(Double.init) 38 | // create output vector based on the last column 39 | let output: [Double] 40 | switch columns[4] { 41 | case "Iris-setosa": 42 | output = [1, 0, 0] 43 | case "Iris-versicolor": 44 | output = [0, 1, 0] 45 | default: 46 | output = [0, 0, 1] 47 | } 48 | return (input, output) 49 | } 50 | 51 | let (train, test) = data.split(ratio: 0.1) 52 | network.trainAndTest(trainData: train, testData: test) { result in 53 | print(result.classifications.accuracy) 54 | print(result) 55 | } 56 | 57 | //#-end-editable-code 58 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageThree.playgroundpage/LiveView.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import UIKit 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | let vc = PlaygroundLiveViewController() 6 | PlaygroundPage.current.liveView = vc 7 | vc.page = 4 8 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageThree.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterTwoPageThreeName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageTwo.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | /*: 2 | # Data Preprocessing 3 | 4 | An important part of machine learning is the data and especially how it is structured. If the data contains an incorrect representation, such as noise, this can have a negative effect on the results. 5 | 6 | In addition to the correctness of the data, scaling and distribution also plays an important role. 7 | In this chapter we have an example data set where the data points are distributed between 0 and 100. 8 | 9 | This means that the given network cannot achieve good results. However, if the data points are normalized between 0 and 1, it achieves good results, since the deviation due to big numbers is no longer so significant. 10 | 11 | You can get a normalized version of a DataSet with: 12 | 13 | `data = data.normalized()` 14 | */ 15 | //#-hidden-code 16 | import PlaygroundSupport 17 | PlaygroundPage.current.needsIndefiniteExecution = true 18 | //#-code-completion(identifier, hide, DecisionBoundaryView, Constants, DecisionBoundaryPlotView, NetworkVisualisationViewController, PlaygroundNetworkVisualisationViewController, PlaygroundLiveViewController, PlaygroundLiveViewSessionManager, PlaygroundStore, PlaygroundPageSessionManager, PlaygroundRemoteNeuralNetworkDebugOutputHandler) 19 | //#-code-completion(identifier, hide, evaluate(result:), outputView) 20 | 21 | //#-end-hidden-code 22 | let network = NeuralNetwork(learningRate: /*#-editable-code */0.03/*#-end-editable-code */, epochs: /*#-editable-code */2000/*#-end-editable-code */) 23 | 24 | //#-editable-code 25 | network.add(layer: Layer(nodesCount: 2)) 26 | network.add(layer: Layer(nodesCount: 4, activationFunction: .tanh)) 27 | network.add(layer: Layer(nodesCount: 4, activationFunction: .linear)) 28 | //#-end-editable-code 29 | var data = ArtificalDataSets.fourCorners() 30 | //#-editable-code 31 | // Perform preprocessing 32 | 33 | //#-end-editable-code 34 | //#-hidden-code 35 | let outputView = PlaygroundNetworkVisualisationViewController() 36 | outputView.page = 3 37 | network.debugOutputHandler = outputView 38 | 39 | func evaluate(result: TestReport) { 40 | if result.classifications.accuracy == 1 { 41 | PlaygroundPage.current.assessmentStatus = .pass(message: "Yeah 🎉") 42 | } else { 43 | PlaygroundPage.current.assessmentStatus = .fail(hints: ["The current accuracy is at \(Int(result.classifications.accuracy * 100))%. The Goal is 90%"], solution: "Normalize the data with the `data = data.normalized()` method") 44 | } 45 | } 46 | 47 | //#-end-hidden-code 48 | 49 | let (train, test) = data.split(ratio: 0.1) 50 | network.trainAndTest(trainData: train, testData: test) { result in 51 | print(result) 52 | evaluate(result: result) 53 | } 54 | 55 | /*: 56 | * Callout(Task): 57 | Optimize the dataset to achieve a training accuracy of more than 90% 58 | */ 59 | //#-hidden-code 60 | PlaygroundPage.current.liveView = outputView 61 | //#-end-hidden-code 62 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageTwo.playgroundpage/LiveView.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import UIKit 3 | 4 | PlaygroundPage.current.needsIndefiniteExecution = true 5 | let vc = PlaygroundLiveViewController() 6 | PlaygroundPage.current.liveView = vc 7 | vc.page = 3 8 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/Pages/PlaygroundPageTwo.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterTwoPageTwoName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | VisibleByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/Chapters/Chapter2.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter3.playgroundchapter/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterThreeName 7 | Pages 8 | 9 | PlaygroundPage.playgroundpage 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter3.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //#-hidden-code 2 | import PlaygroundSupport 3 | import UIKit 4 | //#-code-completion(everything, hide) 5 | //#-end-hidden-code 6 | /*: 7 | # Congratulations 👏🎉🎈 8 | Take a moment and celebrate yourself! 9 | 10 | You've finished the book and learned [how to add layers to a network](Hello%20World/XOR), [how to use activation functions and other parameters](Creating%20Networks/Epochs,%20Learning%20Rate%20and%20Activation%20Functions), [leaned about the importance of preprocessing](Creating%20Networks/Data%20&%20Preprocessing) and [played around with your own your Network](Creating%20Networks/Playground). 11 | 12 | # What's next 13 | There is still a lot to discover on top of your new gained knowledge. 14 | 15 | ## Ethics and Privacy 16 | Whenever you work with data, you should make sure that it is collected in an ethically correct way. If the data originates from people or their interactions, they should be informed about its use and asked for consent. 17 | 18 | ## CreateML & CoreML 19 | Apple's platforms offer a lot of tools to efficiently execute or train machine learning models such as CoreML or CreateML. 20 | 21 | ## Convolution Neural Networks 22 | Especially for data with many input dimensions, such as images, Convolutional Neural Networks achieve significantly better results. Especially for data with many input dimensions, such as images, Convolutional Neural Networks achieve significantly better results. They use convolutions between the layers. Check out my [Playground](https://github.com/lennet/image-filtering) about Image Filtering with Convolutions if you want to learn more. 23 | 24 | # Attribution: 25 | - The XOR image in the [Hello World](Hello%20World/XOR) chapter is based on the work by Heron [https://de.wikipedia.org/wiki/Datei:Xor-gate-en.svg](https://de.wikipedia.org/wiki/Datei:Xor-gate-en.svg). Licensed under creative commons by attribution 3.0 license 26 | - The Iris Dataset in the [Playground](Creating%20Networks/Playground) chapter provided by Dua, D. and Graff, C. (2019). UCI Machine Learning Repository [http://archive.ics.uci.edu/ml](http://archive.ics.uci.edu/ml). Irvine, CA: University of California, School of Information and Computer Science. 27 | 28 | # Thank's for reading! 29 | */ 30 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter3.playgroundchapter/Pages/PlaygroundPage.playgroundpage/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Name 6 | ChapterThreePageOneName 7 | LiveViewEdgeToEdge 8 | 9 | LiveViewMode 10 | HiddenByDefault 11 | 12 | 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Chapters/Chapter3.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/Chapters/Chapter3.playgroundchapter/PrivateResources/Base.lproj/ManifestPlist.strings -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ImageReference 6 | cover.png 7 | Chapters 8 | 9 | Chapter0.playgroundchapter 10 | Chapter1.playgroundchapter 11 | Chapter2.playgroundchapter 12 | Chapter3.playgroundchapter 13 | 14 | ContentIdentifier 15 | com.example.PlaygroundBook 16 | ContentVersion 17 | 1.0 18 | DeploymentTarget 19 | ios10.3 20 | DevelopmentRegion 21 | en 22 | Name 23 | Name 24 | SwiftVersion 25 | 4.2 26 | Version 27 | 4.0 28 | 29 | 30 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/PrivateResources/Base.lproj/ManifestPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/PrivateResources/Base.lproj/ManifestPlist.strings -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/PrivateResources/Glossary.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Terms 6 | 7 | Hidden layer 8 | 9 | Definition 10 | The Layers that are in between the output layer and the input layer 11 | FirstUse 12 | 13 | PageReference 14 | 15 | Title 16 | 17 | 18 | 19 | Epochs 20 | 21 | Definition 22 | The number of iterations that are used for pass the training data through the network 23 | FirstUse 24 | 25 | PageReference 26 | 27 | Title 28 | 29 | 30 | 31 | Learning Rate 32 | 33 | Definition 34 | The learning rate determines how quickly the network changes the weights based on the loss 35 | FirstUse 36 | 37 | PageReference 38 | 39 | Title 40 | 41 | 42 | 43 | Neural Network 44 | 45 | Definition 46 | Neural network is a machine learning strategy whose structure is loosely based on the structure of a brain's neurons. 47 | 48 | The network consists of several layers of neurons. All neurons are connected to all neurons from the previous and preceding layers. If the network is fed with data, the connections that lead to a correct response are strengthened and connections that lead to errors are weakened. 49 | 50 | FirstUse 51 | 52 | PageReference 53 | 54 | Title 55 | 56 | 57 | 58 | Neurons 59 | 60 | Definition 61 | The nodes inside a Neural Network 62 | FirstUse 63 | 64 | PageReference 65 | 66 | Title 67 | 68 | 69 | 70 | Layer 71 | 72 | Definition 73 | A group of nodes that represents a layer of the network 74 | FirstUse 75 | 76 | PageReference 77 | 78 | Title 79 | 80 | 81 | 82 | Activation Function 83 | 84 | Definition 85 | The activation function defines the output of a node for an input value. It can also be used to set the value to a certain range (e.g. between 0 and 1). 86 | FirstUse 87 | 88 | PageReference 89 | 90 | Title 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/PrivateResources/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/NeuralNetwork.playgroundbook/Contents/PrivateResources/cover.png -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/PrivateResources/iris.csv: -------------------------------------------------------------------------------- 1 | 5.1,3.5,1.4,0.2,Iris-setosa 2 | 4.9,3.0,1.4,0.2,Iris-setosa 3 | 4.7,3.2,1.3,0.2,Iris-setosa 4 | 4.6,3.1,1.5,0.2,Iris-setosa 5 | 5.0,3.6,1.4,0.2,Iris-setosa 6 | 5.4,3.9,1.7,0.4,Iris-setosa 7 | 4.6,3.4,1.4,0.3,Iris-setosa 8 | 5.0,3.4,1.5,0.2,Iris-setosa 9 | 4.4,2.9,1.4,0.2,Iris-setosa 10 | 4.9,3.1,1.5,0.1,Iris-setosa 11 | 5.4,3.7,1.5,0.2,Iris-setosa 12 | 4.8,3.4,1.6,0.2,Iris-setosa 13 | 4.8,3.0,1.4,0.1,Iris-setosa 14 | 4.3,3.0,1.1,0.1,Iris-setosa 15 | 5.8,4.0,1.2,0.2,Iris-setosa 16 | 5.7,4.4,1.5,0.4,Iris-setosa 17 | 5.4,3.9,1.3,0.4,Iris-setosa 18 | 5.1,3.5,1.4,0.3,Iris-setosa 19 | 5.7,3.8,1.7,0.3,Iris-setosa 20 | 5.1,3.8,1.5,0.3,Iris-setosa 21 | 5.4,3.4,1.7,0.2,Iris-setosa 22 | 5.1,3.7,1.5,0.4,Iris-setosa 23 | 4.6,3.6,1.0,0.2,Iris-setosa 24 | 5.1,3.3,1.7,0.5,Iris-setosa 25 | 4.8,3.4,1.9,0.2,Iris-setosa 26 | 5.0,3.0,1.6,0.2,Iris-setosa 27 | 5.0,3.4,1.6,0.4,Iris-setosa 28 | 5.2,3.5,1.5,0.2,Iris-setosa 29 | 5.2,3.4,1.4,0.2,Iris-setosa 30 | 4.7,3.2,1.6,0.2,Iris-setosa 31 | 4.8,3.1,1.6,0.2,Iris-setosa 32 | 5.4,3.4,1.5,0.4,Iris-setosa 33 | 5.2,4.1,1.5,0.1,Iris-setosa 34 | 5.5,4.2,1.4,0.2,Iris-setosa 35 | 4.9,3.1,1.5,0.1,Iris-setosa 36 | 5.0,3.2,1.2,0.2,Iris-setosa 37 | 5.5,3.5,1.3,0.2,Iris-setosa 38 | 4.9,3.1,1.5,0.1,Iris-setosa 39 | 4.4,3.0,1.3,0.2,Iris-setosa 40 | 5.1,3.4,1.5,0.2,Iris-setosa 41 | 5.0,3.5,1.3,0.3,Iris-setosa 42 | 4.5,2.3,1.3,0.3,Iris-setosa 43 | 4.4,3.2,1.3,0.2,Iris-setosa 44 | 5.0,3.5,1.6,0.6,Iris-setosa 45 | 5.1,3.8,1.9,0.4,Iris-setosa 46 | 4.8,3.0,1.4,0.3,Iris-setosa 47 | 5.1,3.8,1.6,0.2,Iris-setosa 48 | 4.6,3.2,1.4,0.2,Iris-setosa 49 | 5.3,3.7,1.5,0.2,Iris-setosa 50 | 5.0,3.3,1.4,0.2,Iris-setosa 51 | 7.0,3.2,4.7,1.4,Iris-versicolor 52 | 6.4,3.2,4.5,1.5,Iris-versicolor 53 | 6.9,3.1,4.9,1.5,Iris-versicolor 54 | 5.5,2.3,4.0,1.3,Iris-versicolor 55 | 6.5,2.8,4.6,1.5,Iris-versicolor 56 | 5.7,2.8,4.5,1.3,Iris-versicolor 57 | 6.3,3.3,4.7,1.6,Iris-versicolor 58 | 4.9,2.4,3.3,1.0,Iris-versicolor 59 | 6.6,2.9,4.6,1.3,Iris-versicolor 60 | 5.2,2.7,3.9,1.4,Iris-versicolor 61 | 5.0,2.0,3.5,1.0,Iris-versicolor 62 | 5.9,3.0,4.2,1.5,Iris-versicolor 63 | 6.0,2.2,4.0,1.0,Iris-versicolor 64 | 6.1,2.9,4.7,1.4,Iris-versicolor 65 | 5.6,2.9,3.6,1.3,Iris-versicolor 66 | 6.7,3.1,4.4,1.4,Iris-versicolor 67 | 5.6,3.0,4.5,1.5,Iris-versicolor 68 | 5.8,2.7,4.1,1.0,Iris-versicolor 69 | 6.2,2.2,4.5,1.5,Iris-versicolor 70 | 5.6,2.5,3.9,1.1,Iris-versicolor 71 | 5.9,3.2,4.8,1.8,Iris-versicolor 72 | 6.1,2.8,4.0,1.3,Iris-versicolor 73 | 6.3,2.5,4.9,1.5,Iris-versicolor 74 | 6.1,2.8,4.7,1.2,Iris-versicolor 75 | 6.4,2.9,4.3,1.3,Iris-versicolor 76 | 6.6,3.0,4.4,1.4,Iris-versicolor 77 | 6.8,2.8,4.8,1.4,Iris-versicolor 78 | 6.7,3.0,5.0,1.7,Iris-versicolor 79 | 6.0,2.9,4.5,1.5,Iris-versicolor 80 | 5.7,2.6,3.5,1.0,Iris-versicolor 81 | 5.5,2.4,3.8,1.1,Iris-versicolor 82 | 5.5,2.4,3.7,1.0,Iris-versicolor 83 | 5.8,2.7,3.9,1.2,Iris-versicolor 84 | 6.0,2.7,5.1,1.6,Iris-versicolor 85 | 5.4,3.0,4.5,1.5,Iris-versicolor 86 | 6.0,3.4,4.5,1.6,Iris-versicolor 87 | 6.7,3.1,4.7,1.5,Iris-versicolor 88 | 6.3,2.3,4.4,1.3,Iris-versicolor 89 | 5.6,3.0,4.1,1.3,Iris-versicolor 90 | 5.5,2.5,4.0,1.3,Iris-versicolor 91 | 5.5,2.6,4.4,1.2,Iris-versicolor 92 | 6.1,3.0,4.6,1.4,Iris-versicolor 93 | 5.8,2.6,4.0,1.2,Iris-versicolor 94 | 5.0,2.3,3.3,1.0,Iris-versicolor 95 | 5.6,2.7,4.2,1.3,Iris-versicolor 96 | 5.7,3.0,4.2,1.2,Iris-versicolor 97 | 5.7,2.9,4.2,1.3,Iris-versicolor 98 | 6.2,2.9,4.3,1.3,Iris-versicolor 99 | 5.1,2.5,3.0,1.1,Iris-versicolor 100 | 5.7,2.8,4.1,1.3,Iris-versicolor 101 | 6.3,3.3,6.0,2.5,Iris-virginica 102 | 5.8,2.7,5.1,1.9,Iris-virginica 103 | 7.1,3.0,5.9,2.1,Iris-virginica 104 | 6.3,2.9,5.6,1.8,Iris-virginica 105 | 6.5,3.0,5.8,2.2,Iris-virginica 106 | 7.6,3.0,6.6,2.1,Iris-virginica 107 | 4.9,2.5,4.5,1.7,Iris-virginica 108 | 7.3,2.9,6.3,1.8,Iris-virginica 109 | 6.7,2.5,5.8,1.8,Iris-virginica 110 | 7.2,3.6,6.1,2.5,Iris-virginica 111 | 6.5,3.2,5.1,2.0,Iris-virginica 112 | 6.4,2.7,5.3,1.9,Iris-virginica 113 | 6.8,3.0,5.5,2.1,Iris-virginica 114 | 5.7,2.5,5.0,2.0,Iris-virginica 115 | 5.8,2.8,5.1,2.4,Iris-virginica 116 | 6.4,3.2,5.3,2.3,Iris-virginica 117 | 6.5,3.0,5.5,1.8,Iris-virginica 118 | 7.7,3.8,6.7,2.2,Iris-virginica 119 | 7.7,2.6,6.9,2.3,Iris-virginica 120 | 6.0,2.2,5.0,1.5,Iris-virginica 121 | 6.9,3.2,5.7,2.3,Iris-virginica 122 | 5.6,2.8,4.9,2.0,Iris-virginica 123 | 7.7,2.8,6.7,2.0,Iris-virginica 124 | 6.3,2.7,4.9,1.8,Iris-virginica 125 | 6.7,3.3,5.7,2.1,Iris-virginica 126 | 7.2,3.2,6.0,1.8,Iris-virginica 127 | 6.2,2.8,4.8,1.8,Iris-virginica 128 | 6.1,3.0,4.9,1.8,Iris-virginica 129 | 6.4,2.8,5.6,2.1,Iris-virginica 130 | 7.2,3.0,5.8,1.6,Iris-virginica 131 | 7.4,2.8,6.1,1.9,Iris-virginica 132 | 7.9,3.8,6.4,2.0,Iris-virginica 133 | 6.4,2.8,5.6,2.2,Iris-virginica 134 | 6.3,2.8,5.1,1.5,Iris-virginica 135 | 6.1,2.6,5.6,1.4,Iris-virginica 136 | 7.7,3.0,6.1,2.3,Iris-virginica 137 | 6.3,3.4,5.6,2.4,Iris-virginica 138 | 6.4,3.1,5.5,1.8,Iris-virginica 139 | 6.0,3.0,4.8,1.8,Iris-virginica 140 | 6.9,3.1,5.4,2.1,Iris-virginica 141 | 6.7,3.1,5.6,2.4,Iris-virginica 142 | 6.9,3.1,5.1,2.3,Iris-virginica 143 | 5.8,2.7,5.1,1.9,Iris-virginica 144 | 6.8,3.2,5.9,2.3,Iris-virginica 145 | 6.7,3.3,5.7,2.5,Iris-virginica 146 | 6.7,3.0,5.2,2.3,Iris-virginica 147 | 6.3,2.5,5.0,1.9,Iris-virginica 148 | 6.5,3.0,5.2,2.0,Iris-virginica 149 | 6.2,3.4,5.4,2.3,Iris-virginica 150 | 5.9,3.0,5.1,1.8,Iris-virginica 151 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/DataInput/ArtificalDataSets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension Array { 8 | func randomElements(n: Int) -> [Element] { 9 | return Array(shuffled().prefix(n)) 10 | } 11 | } 12 | 13 | public class ArtificalDataSets { 14 | public class func circular(numberOfPointsPerClass: Int = 250) -> DataSetProtocol { 15 | func generateValues(numberOfPoints: Int, minRadius: Double = 0, maxRadius: Double, classValue: Double) -> [[Double]] { 16 | let rows: [[Double]] = (0 ..< numberOfPoints).map { _ in 17 | let a = Double.random(in: 0 ..< 1) * 2 * Double.pi 18 | let r = maxRadius * sqrt(Double.random(in: 0 ..< 1)) + minRadius 19 | 20 | let x = r * cos(a) 21 | let y = r * sin(a) 22 | 23 | return [x, y, classValue] 24 | } 25 | return rows 26 | } 27 | let radius: Double = 10 28 | let minRadius: Double = 5 29 | 30 | let values = generateValues(numberOfPoints: numberOfPointsPerClass, maxRadius: 5, classValue: 0) + generateValues(numberOfPoints: numberOfPointsPerClass, minRadius: minRadius + 1, maxRadius: radius, classValue: 1).shuffled() 31 | 32 | let input = values.map { row in Array(row[0 ..< 2]) } 33 | let output: [[Double]] = values.map { row in 34 | if row[2] == 1 { 35 | return [1, 0] 36 | } else { 37 | return [0, 1] 38 | } 39 | } 40 | 41 | return DataSet(input: Mat(values: input), output: Mat(values: output), inputLabels: ["X", "Y"]) 42 | } 43 | 44 | private class func randomPointsForShape(path: UIBezierPath, classValue: Double) -> [[Double]] { 45 | let allPoints = path.allPoints 46 | return allPoints.randomElements(n: allPoints.count / 5).map { point in 47 | [Double(point.x), Double(point.y), classValue] 48 | } 49 | } 50 | 51 | public class func fourCorners() -> DataSetProtocol { 52 | let center = CGPoint(x: 50, y: 50) 53 | let distanceFromCenter: CGFloat = 10 54 | let size = CGSize(width: 10, height: 50) 55 | 56 | let bottomRightShape = UIBezierPath(rect: CGRect(x: center.x + distanceFromCenter, y: center.y + distanceFromCenter, width: size.width, height: size.height)) 57 | bottomRightShape.append(UIBezierPath(rect: CGRect(x: center.x + distanceFromCenter, y: center.y + distanceFromCenter, width: size.height, height: size.width))) 58 | let bottomRight = randomPointsForShape(path: bottomRightShape, classValue: 3) 59 | 60 | let topRightShape = UIBezierPath(rect: CGRect(x: center.x + distanceFromCenter, y: center.y - distanceFromCenter, width: size.width, height: -size.height)) 61 | topRightShape.append(UIBezierPath(rect: CGRect(x: center.x + distanceFromCenter, y: center.y - distanceFromCenter, width: size.height, height: size.width))) 62 | let topRight = randomPointsForShape(path: topRightShape, classValue: 2) 63 | 64 | let bottomLeftShape = UIBezierPath(rect: CGRect(x: center.x - distanceFromCenter, y: center.y + distanceFromCenter, width: size.width, height: size.height)) 65 | bottomLeftShape.append(UIBezierPath(rect: CGRect(x: center.x - distanceFromCenter, y: center.y + distanceFromCenter, width: -size.height, height: size.width))) 66 | let bottomLeft = randomPointsForShape(path: bottomLeftShape, classValue: 0) 67 | 68 | let topLeftShape = UIBezierPath(rect: CGRect(x: center.x - distanceFromCenter, y: center.y - distanceFromCenter, width: size.width, height: -size.height)) 69 | topLeftShape.append(UIBezierPath(rect: CGRect(x: center.x - distanceFromCenter, y: center.x - distanceFromCenter, width: -size.height, height: size.width))) 70 | let topLeft = randomPointsForShape(path: topLeftShape, classValue: 1) 71 | 72 | let allValues = (topRight + bottomRight + bottomLeft + topLeft).shuffled() 73 | 74 | let input = allValues.map { row in Array(row[0 ..< 2]) } 75 | let output: [[Double]] = allValues.map { row in 76 | switch Int(row[2]) { 77 | case 0: 78 | return [1, 0, 0, 0] 79 | case 1: 80 | return [0, 1, 0, 0] 81 | case 2: 82 | return [0, 0, 1, 0] 83 | case 3: 84 | return [0, 0, 0, 1] 85 | default: 86 | fatalError() 87 | } 88 | } 89 | return DataSet(input: Mat(values: input), output: Mat(values: output), inputLabels: ["X", "Y"]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/DataInput/CSVDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class CSVDataSet: DataSetProtocol, CustomPlaygroundDisplayConvertible { 8 | public var inputLabels: [String]? 9 | public let input: Mat 10 | public let output: Mat 11 | 12 | public init(path: String, inputLabels: [String]? = nil, preprocessingBlock: ([Substring.SubSequence]) -> ([Double], [Double])) { 13 | let csv = try! String(contentsOfFile: path) 14 | let csvRows = csv.split(separator: "\n").map { $0.split(separator: ",") } 15 | 16 | let (inputValues, outputValues) = csvRows.shuffled().map(preprocessingBlock).reduce(into: ([[Double]](), [[Double]]())) { result, data in 17 | result.0.append(data.0) 18 | result.1.append(data.1) 19 | } 20 | 21 | input = Mat(values: inputValues) 22 | output = Mat(values: outputValues) 23 | self.inputLabels = inputLabels 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/DataInput/DataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import CoreGraphics 6 | 7 | public protocol DataSetProtocol: CustomPlaygroundDisplayConvertible { 8 | var input: Mat { get } 9 | var output: Mat { get } 10 | var inputLabels: [String]? { get } 11 | } 12 | 13 | public extension DataSetProtocol { 14 | func split(ratio: Float) -> (DataSetProtocol, DataSetProtocol) { 15 | let numberOfRows = input.values2D.count 16 | let splitIndex = numberOfRows - Int(Float(numberOfRows) * ratio) 17 | let aRange = 0 ..< splitIndex 18 | let aInput = Mat(values: [[Double]](input.values2D[aRange])) 19 | let aOutput = Mat(values: [[Double]](output.values2D[aRange])) 20 | let bRange = splitIndex ..< numberOfRows 21 | let bInput = Mat(values: [[Double]](input.values2D[bRange])) 22 | let bOutput = Mat(values: [[Double]](output.values2D[bRange])) 23 | return (DataSet(input: aInput, output: aOutput, inputLabels: inputLabels), DataSet(input: bInput, output: bOutput, inputLabels: inputLabels)) 24 | } 25 | 26 | func normalized() -> DataSet { 27 | return DataSet(input: input.normalized(), output: output, inputLabels: inputLabels) 28 | } 29 | } 30 | 31 | public struct DataSet: DataSetProtocol, Codable { 32 | public let input: Mat 33 | public let output: Mat 34 | public let inputLabels: [String]? 35 | 36 | public init(input: Mat, output: Mat, inputLabels: [String]?) { 37 | self.input = input 38 | self.output = output 39 | self.inputLabels = inputLabels 40 | } 41 | } 42 | 43 | public extension DataSet { 44 | init(input: Mat, output: Mat) { 45 | self.input = input 46 | self.output = output 47 | 48 | let aUniCode = UnicodeScalar("A")! 49 | inputLabels = (0 ..< input.shape.cols).map { index in 50 | String(UnicodeScalar(aUniCode.value + UInt32(index))!) 51 | } 52 | } 53 | 54 | init(values: [[Double]: Int]) { 55 | let numberOfClasses = (values.values.max() ?? 0) + 1 56 | let inputValues = Array(values.keys) 57 | input = Mat(values: inputValues) 58 | 59 | let outputValues: [[Double]] = values.values.map { value in 60 | var outputVector = [Double].init(repeating: 0, count: numberOfClasses) 61 | outputVector[value] = 1 62 | return outputVector 63 | } 64 | output = Mat(values: outputValues) 65 | let aUniCode = UnicodeScalar("A")! 66 | inputLabels = (0 ..< input.shape.cols).map { index in 67 | String(UnicodeScalar(aUniCode.value + UInt32(index))!) 68 | } 69 | } 70 | } 71 | 72 | extension DataSetProtocol { 73 | public var playgroundDescription: Any { 74 | // TODO: fix crash for datasets that are not 2D 75 | let view = PlotView(frame: CGRect(origin: .zero, size: CGSize(width: 200, height: 200))) 76 | view.backgroundColor = .white 77 | view.plot(dataSet: self) 78 | return view 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/DataInput/Iris/IrisDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class IrisDataSet: CSVDataSet { 8 | public init() { 9 | let path = Bundle.main.path(forResource: "iris", ofType: "csv")! 10 | super.init(path: path) { row in 11 | let input = row[0 ..< 4].compactMap(Double.init) 12 | let output: [Double] 13 | switch row[4] { 14 | case "Iris-setosa": 15 | output = [1, 0, 0] 16 | case "Iris-versicolor": 17 | output = [0, 1, 0] 18 | default: 19 | output = [0, 0, 1] 20 | } 21 | return (input, output) 22 | } 23 | } 24 | 25 | required init(from _: Decoder) throws { 26 | fatalError("init(from:) has not been implemented") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/DataInput/XORDataSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct XORDataSet: DataSetProtocol { 8 | public let input: Mat 9 | public let output: Mat 10 | public let inputLabels: [String]? 11 | 12 | public init() { 13 | input = Mat(values: [[0, 0], 14 | [1, 0], 15 | [0, 1], 16 | [1, 1]]) 17 | output = Mat(values: [[0, 1], 18 | [1, 0], 19 | [1, 0], 20 | [0, 1]]) 21 | inputLabels = ["A", "B"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Helper/CGRect+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import CoreGraphics 6 | 7 | extension CGRect { 8 | func map(_ transform: (Int, Int) throws -> T) rethrows -> [T] { 9 | var result = [T]() 10 | result.reserveCapacity(Int(width * height)) 11 | for x in Int(minX) ..< Int(maxX) { 12 | for y in Int(minY) ..< Int(maxY) { 13 | let current = try transform(x, y) 14 | result.append(current) 15 | } 16 | } 17 | return result 18 | } 19 | 20 | var allPoints: [CGPoint] { 21 | return map(CGPoint.init) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Helper/ColorHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | private extension DecisionBoundaryView.Pixel { 8 | init(color: UIColor) { 9 | var red: CGFloat = 0 10 | var green: CGFloat = 0 11 | var blue: CGFloat = 0 12 | var alpha: CGFloat = 0 13 | color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 14 | r = UInt8(red * 255) 15 | g = UInt8(green * 255) 16 | b = UInt8(blue * 255) 17 | a = UInt8(alpha * 255) 18 | } 19 | } 20 | 21 | class ColorHelper { 22 | static var allColors: [UIColor] = [ 23 | #colorLiteral(red: 0.03921568627, green: 0.5176470588, blue: 1, alpha: 1), 24 | #colorLiteral(red: 1, green: 0.2705882353, blue: 0.2274509804, alpha: 1), 25 | #colorLiteral(red: 0.1960784314, green: 0.8431372549, blue: 0.2941176471, alpha: 1), 26 | #colorLiteral(red: 1, green: 0.8392156863, blue: 0.03921568627, alpha: 1), 27 | ] 28 | static var colorMap: [Int: UIColor] = { 29 | var result: [Int: UIColor] = [:] 30 | ColorHelper.allColors.enumerated().forEach { index, color in 31 | result[index] = color 32 | } 33 | return result 34 | }() 35 | 36 | static var pixelValues: [(value: Double, color: DecisionBoundaryView.Pixel)] = { 37 | ColorHelper.allColors.map(DecisionBoundaryView.Pixel.init).enumerated().map { index, color in 38 | (Double(index + 1), color) 39 | } 40 | }() 41 | } 42 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Helper/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class Constants { 8 | public static let errorKey = "error" 9 | public static let hintKey = "hint" 10 | public static let solutionKey = "solution" 11 | public static let passKey = "pass" 12 | } 13 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Helper/InterpolationHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Accelerate 6 | 7 | class InterpolationHelper { 8 | class func interpolate(values: [Double], indices: [Double]) -> [Double] { 9 | guard let last = indices.last, 10 | let first = indices.first else { 11 | return [] 12 | } 13 | let n = vDSP_Length(last - first + 1) 14 | var result = [Double](repeating: 0, count: Int(n)) 15 | let length = vDSP_Length(values.count + 1) 16 | vDSP_vgenpD(values, 1, indices, 1, &result, 1, n, length) 17 | return result 18 | } 19 | 20 | class func interpolate2D(values: [[Double]], indices: [[Double]]) -> [[Double]] { 21 | let yValues = zip(values, indices).map { linearValues, linearIndices in 22 | interpolate(values: linearValues, indices: linearIndices) 23 | } 24 | let row = Mat(values: yValues).transposed().values2D.map { value in 25 | interpolate(values: value, indices: indices[0]) 26 | } 27 | return Mat(values: row).transposed().values2D 28 | } 29 | 30 | class func interpolate(values: [Double], scaleFactor: Int) -> [Double] { 31 | let indices = (0 ..< values.count).map { i in Double(i * scaleFactor) } 32 | return interpolate(values: values, indices: indices) 33 | } 34 | 35 | class func interpolate2D(values: [[Double]], scaleFactor: Int) -> [[Double]] { 36 | let indices = [[Double]](repeating: (0 ..< values.count).map { i in Double(i * scaleFactor) }, count: values[0].count) 37 | return interpolate2D(values: values, indices: indices) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Helper/UIBezierPath+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension UIBezierPath { 8 | var allPoints: [CGPoint] { 9 | return bounds.allPoints.filter(contains) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Model/NetworkModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct NetworkModel: Codable, Equatable { 8 | let layer: [LayerModel] 9 | } 10 | 11 | public extension NetworkModel { 12 | func save(to url: URL) throws { 13 | let data = try JSONEncoder().encode(self) 14 | try data.write(to: url) 15 | } 16 | 17 | static func load(from url: URL) throws -> NetworkModel { 18 | let data = try Data(contentsOf: url) 19 | return try JSONDecoder().decode(NetworkModel.self, from: data) 20 | } 21 | } 22 | 23 | public struct LayerModel: Codable, Equatable { 24 | let neurons: [NeuronModel] 25 | let activationFunction: String? 26 | } 27 | 28 | extension LayerModel { 29 | init(neurons: [NeuronModel]) { 30 | self.neurons = neurons 31 | activationFunction = nil 32 | } 33 | } 34 | 35 | public struct NeuronModel: Codable, Equatable { 36 | let weigths: [Double]? 37 | let bias: Double? 38 | } 39 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/ActivationFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | public struct ActivationFunction { 8 | let function: (Mat) -> Mat 9 | let functionDerivative: (Mat) -> Mat 10 | let name: String 11 | } 12 | 13 | public extension ActivationFunction { 14 | static var sigmoid: ActivationFunction { 15 | let sigmoid: (Mat) -> (Mat) = { x in 16 | let exp = x.exp() // .min(Double.greatestFiniteMagnitude) 17 | return exp.divide(exp + 1.0) // .clamp(min: 0.0000000000000001, max: 0.9999999999999999) 18 | } 19 | return ActivationFunction(function: sigmoid, functionDerivative: { x in 20 | let sig = sigmoid(x) 21 | return sig * (1 - sig) 22 | }, name: #function) 23 | } 24 | 25 | static var relu: ActivationFunction { 26 | return ActivationFunction(function: { mat in 27 | mat.max(0) 28 | }, functionDerivative: { mat in 29 | let newValues = mat.values.map { $0 > 0.0 ? 1.0 : 0.0 } 30 | return Mat(shape: mat.shape, values: newValues) 31 | }, name: #function) 32 | } 33 | 34 | static var tanh: ActivationFunction { 35 | return ActivationFunction(function: { mat in 36 | mat.tanh() 37 | }, functionDerivative: { mat in 38 | 1 - mat.tanh().pow(2) 39 | }, name: #function) 40 | } 41 | 42 | static var linear: ActivationFunction { 43 | return ActivationFunction(function: { $0 }, functionDerivative: { mat in Mat(ones: mat.shape) }, name: #function) 44 | } 45 | 46 | static func activationFunction(for name: String) -> ActivationFunction { 47 | switch name { 48 | case "sigmoid": 49 | return sigmoid 50 | case "relu": 51 | return relu 52 | case "tanh": 53 | return tanh 54 | default: 55 | return linear 56 | } 57 | } 58 | } 59 | 60 | extension ActivationFunction: CustomPlaygroundDisplayConvertible { 61 | public var playgroundDescription: Any { 62 | let view = LinePlotView(frame: CGRect(origin: .zero, size: CGSize(width: 150, height: 150))) 63 | view.shapeLayer.masksToBounds = true 64 | 65 | let containerView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 170, height: 200))) 66 | containerView.backgroundColor = .white 67 | containerView.addSubview(view) 68 | view.center = CGPoint(x: containerView.bounds.midX, y: containerView.bounds.midY - 15) 69 | 70 | let maxInputValue: Double = 5 71 | let possibleInputs = Array(stride(from: -maxInputValue, to: maxInputValue, by: 0.1)) 72 | let mat = Mat(shape: Shape(rows: UInt(possibleInputs.count), cols: 1), values: possibleInputs) 73 | let result = function(mat) 74 | // TODO: filter out trailing constant numbers or find better way to define possible inputs 75 | result.values.forEach(view.add) 76 | view.layoutSubviews() 77 | // TODO: file radar: 78 | // Without the ContainerView the presentation is flipped 79 | view.showAxis = true 80 | view.axisLayer.showYAxis = false 81 | 82 | // TODO: fix resolution 83 | view.axisLayer.displayedValues = [0, 1, Int(maxInputValue)] 84 | view.axisLayer.axisOffSet.dy = view.bounds.midY - CGPoint(x: 0, y: 0).applying(view.pathTransform).y 85 | 86 | view.axisLayer.resolution.dx = view.bounds.midX / CGFloat(possibleInputs.count) * 20 87 | view.axisLayer.resolution.dy = CGPoint(x: 0, y: 1).applying(view.pathTransform).y / CGFloat(maxInputValue) 88 | 89 | return containerView 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/EpochReport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct EpochReport: Codable { 8 | public let epochIndex: Int 9 | public let classifications: [ClassificationReport] 10 | public let loss: Double 11 | } 12 | 13 | extension EpochReport: CustomStringConvertible, CustomPlaygroundDisplayConvertible { 14 | public var playgroundDescription: Any { 15 | return description 16 | } 17 | 18 | public var description: String { 19 | var result = "" 20 | result += "Epoch: \(epochIndex)\n" 21 | result += "-------------------------\n" 22 | result += "Accuracy: \(classifications.accuracy)%\n" 23 | result += "loss: \(loss)\n" 24 | result += "-------------------------" 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/ErrorFunction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct ErrorFunction { 8 | let function: (_ output: Mat, _ expectedOuput: Mat) -> Mat 9 | let functionDerivative: (_ output: Mat, _ expectedOuput: Mat) -> Mat 10 | } 11 | 12 | public extension ErrorFunction { 13 | static var squared: ErrorFunction { 14 | return ErrorFunction(function: { (output, expectedOutput) -> Mat in 15 | 0.5 * (output - expectedOutput).pow(2) 16 | }, functionDerivative: { (output, expectedOutput) -> Mat in 17 | output - expectedOutput 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/Layer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class Layer { 8 | private(set) 9 | var nodes: UInt 10 | var weights: Mat? 11 | var biases: Mat? 12 | 13 | private var output: Mat? 14 | private(set) 15 | var activatedOutput: Mat? 16 | 17 | let activationFunction: ActivationFunction 18 | private let initialWeightsRange: Range 19 | 20 | var previous: Layer? { 21 | didSet { 22 | initWeightsAndBiasIfNeeded() 23 | } 24 | } 25 | 26 | var next: Layer? 27 | 28 | public init(nodesCount: UInt, initialWeightsRange: Range = 0 ..< 1, activationFunction: ActivationFunction = .linear) { 29 | nodes = nodesCount 30 | self.activationFunction = activationFunction 31 | self.initialWeightsRange = initialWeightsRange 32 | } 33 | 34 | convenience init(model: LayerModel) { 35 | self.init(nodesCount: UInt(model.neurons.count), activationFunction: .activationFunction(for: model.activationFunction!)) 36 | let weightsValue = model.neurons.compactMap { $0.weigths } 37 | if !weightsValue.isEmpty { 38 | weights = Mat(values: weightsValue) 39 | } 40 | let biasesModel = model.neurons.compactMap { $0.bias } 41 | if !biasesModel.isEmpty { 42 | biases = Mat(shape: (nodes, 1), values: biasesModel) 43 | } 44 | } 45 | 46 | public func modelRepresentation() -> LayerModel { 47 | let weights = self.weights?.values ?? [] 48 | let biases = self.biases?.values ?? [] 49 | let numberOfNodes = Int(nodes) 50 | let numberOfWeightsPerNode = weights.count / numberOfNodes 51 | let nodesRepresentation: [NeuronModel] = (0 ..< numberOfNodes).map { nodeIndex in 52 | let startIndex = nodeIndex * numberOfWeightsPerNode 53 | let bias = biases.isEmpty ? nil : biases[nodeIndex] 54 | let weightRange = startIndex ..< startIndex + numberOfWeightsPerNode 55 | let neuronWeights = weightRange.isEmpty ? nil : Array(weights[weightRange]) 56 | return NeuronModel(weigths: neuronWeights, bias: 57 | bias) 58 | } 59 | return LayerModel(neurons: nodesRepresentation, activationFunction: activationFunction.name) 60 | } 61 | 62 | func process(input: Mat) -> Mat { 63 | if let weights = weights, 64 | let biases = biases { 65 | output = weights.dot(input) + biases 66 | activatedOutput = activationFunction.function(output!) 67 | } else { 68 | activatedOutput = input 69 | } 70 | return activatedOutput! 71 | } 72 | 73 | func updateForOutputLayer(error: Mat, m: Double, learningRate: Double) { 74 | update(error: error, m: m, learningRate: learningRate) 75 | } 76 | 77 | func updateForHiddenLayer(m: Double, learningRate: Double, nextError: Mat) -> Mat { 78 | let error = next!.weights!.transposed().dot(nextError) * activationFunction.functionDerivative(output!) 79 | update(error: error, m: m, learningRate: learningRate) 80 | return error 81 | } 82 | 83 | private func update(error: Mat, m: Double, learningRate: Double) { 84 | let factor = (1.0 / m) 85 | let dw = factor * error.dot(previous!.activatedOutput!.transposed()) 86 | let db = factor * error.sumRows() 87 | 88 | weights! -= learningRate * dw 89 | biases! -= learningRate * db 90 | } 91 | 92 | private func initWeightsAndBiasIfNeeded() { 93 | guard let previous = previous else { return } 94 | if weights == nil { 95 | let randomWeights = (0 ..< nodes * previous.nodes).map { _ in Double.random(in: initialWeightsRange) } 96 | weights = Mat(shape: (nodes, previous.nodes), values: randomWeights) 97 | } 98 | if biases == nil { 99 | biases = Mat(shape: (nodes, 1), values: [Double](repeating: 0, count: Int(nodes))) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/Mat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Accelerate 6 | 7 | public struct Shape: Codable, Equatable { 8 | public let rows: UInt 9 | public let cols: UInt 10 | } 11 | 12 | public struct Mat: Equatable, Codable { 13 | public let shape: Shape 14 | let values: [Double] 15 | 16 | init(shape: (UInt, UInt), values: [Double]) { 17 | self.init(shape: Shape(rows: shape.0, cols: shape.1), values: values) 18 | } 19 | 20 | init(shape: Shape, values: [Double]) { 21 | self.shape = shape 22 | self.values = values 23 | } 24 | 25 | init(ones shape: Shape) { 26 | self.shape = shape 27 | let numberOfElements = Int(shape.cols * shape.rows) 28 | values = [Double](repeating: 1, count: numberOfElements) 29 | } 30 | 31 | init(zeros shape: Shape) { 32 | self.shape = shape 33 | let numberOfElements = Int(shape.cols * shape.rows) 34 | values = [Double](repeating: 0, count: numberOfElements) 35 | } 36 | 37 | public init(values: [[Double]]) { 38 | self.values = values.reduce([Double](), +) 39 | shape = Shape(rows: UInt(values.count), cols: UInt(values[0].count)) 40 | } 41 | 42 | var values2D: [[Double]] { 43 | return (0 ..< shape.rows).map(row) 44 | } 45 | 46 | func column(at index: UInt) -> [Double] { 47 | return transposed().row(at: index) 48 | } 49 | 50 | func columns(in range: ClosedRange) -> Mat { 51 | let t = transposed() 52 | return Mat(values: range.map(t.row)).transposed() 53 | } 54 | 55 | func value(row: UInt, column: UInt) -> Double { 56 | let index = Int(row * shape.rows + column) 57 | return values[index] 58 | } 59 | 60 | func row(at index: UInt) -> [Double] { 61 | return Array(values[Int(index * shape.cols) ..< Int((index + 1) * shape.cols)]) 62 | } 63 | 64 | public static func == (lhs: Mat, rhs: Mat) -> Bool { 65 | return lhs.shape == rhs.shape && lhs.values == rhs.values 66 | } 67 | 68 | public func broadcast(to newShape: Shape) -> Mat? { 69 | // TODO: Test 70 | if newShape.rows == shape.rows, 71 | newShape.cols % shape.cols == 0 { 72 | let amount = Int(newShape.cols / shape.cols) 73 | let newValues: [Double] = Array(values.lazy.map { value in 74 | [Double](repeating: value, count: amount) 75 | }.joined()) 76 | return Mat(shape: newShape, values: newValues) 77 | } else if newShape.cols == shape.cols, 78 | newShape.rows % shape.rows == 0 { 79 | let newValues = Array([[Double]].init(repeating: values, count: Int(newShape.rows / shape.rows)) 80 | .joined()) 81 | return Mat(shape: newShape, values: newValues) 82 | } 83 | return nil 84 | } 85 | 86 | func reshape(to newShape: Shape) -> Mat? { 87 | guard newShape.cols * newShape.rows == shape.rows * shape.cols else { 88 | return nil 89 | } 90 | return Mat(shape: newShape, values: values) 91 | } 92 | 93 | func dot(_ mat: Mat) -> Mat { 94 | var result = [Double](repeating: 0.0, count: Int(shape.rows * mat.shape.cols)) 95 | vDSP_mmulD(values, 1, mat.values, 1, &result, 1, shape.rows, mat.shape.cols, shape.cols) 96 | return Mat(shape: (shape.rows, mat.shape.cols), values: result) 97 | } 98 | 99 | func add(_ mat: Mat) -> Mat { 100 | var result = [Double](repeating: 0.0, count: values.count) 101 | vDSP_vaddD(values, 1, mat.values, 1, &result, 1, vDSP_Length(values.count)) 102 | return Mat(shape: shape, values: result) 103 | } 104 | 105 | func add(_ scalar: Double) -> Mat { 106 | var scalar = scalar 107 | var result = [Double](repeating: 0.0, count: values.count) 108 | vDSP_vsaddD(values, 1, &scalar, &result, 1, shape.rows * shape.cols) 109 | return Mat(shape: shape, values: result) 110 | } 111 | 112 | func subtract(_ mat: Mat) -> Mat { 113 | return self + mat.negate() 114 | } 115 | 116 | func mult(_ mat: Mat) -> Mat { 117 | var result = [Double](repeating: 0.0, count: values.count) 118 | vDSP_vmulD(values, 1, mat.values, 1, &result, 1, vDSP_Length(values.count)) 119 | return Mat(shape: shape, values: result) 120 | } 121 | 122 | func mult(_ scalar: Double) -> Mat { 123 | var sc = scalar 124 | var result = [Double](repeating: 0.0, count: values.count) 125 | vDSP_vsmulD(values, 1, &sc, &result, 1, vDSP_Length(result.count)) 126 | return Mat(shape: shape, values: result) 127 | } 128 | 129 | func divide(_ mat: Mat) -> Mat { 130 | var result = [Double](repeating: 0.0, count: values.count) 131 | vDSP_vdivD(mat.values, 1, values, 1, &result, 1, vDSP_Length(values.count)) 132 | return Mat(shape: shape, values: result) 133 | } 134 | 135 | func negate() -> Mat { 136 | var result = [Double](repeating: 0.0, count: values.count) 137 | let input = values 138 | vDSP_vnegD(input, 1, &result, 1, vDSP_Length(values.count)) 139 | return Mat(shape: shape, values: result) 140 | } 141 | 142 | func sumRows() -> Mat { 143 | let results: [Double] = (0 ..< shape.rows).map { rowIndex in 144 | let row = self.row(at: rowIndex) 145 | var result = 0.0 146 | vDSP_sveD(Array(row), 1, &result, vDSP_Length(row.count)) 147 | return result 148 | } 149 | return Mat(shape: (shape.rows, 1), values: results) 150 | } 151 | 152 | public func transposed() -> Mat { 153 | var result = [Double](repeating: 0.0, count: values.count) 154 | vDSP_mtransD(values, 1, &result, 1, shape.cols, shape.rows) 155 | return Mat(shape: (shape.cols, shape.rows), values: result) 156 | } 157 | 158 | public func sum() -> Double { 159 | var result = 0.0 160 | vDSP_sveD(values, 1, &result, vDSP_Length(values.count)) 161 | return result 162 | } 163 | 164 | public static func + (lhs: Mat, rhs: Mat) -> Mat { 165 | if lhs.shape == rhs.shape { 166 | return lhs.add(rhs) 167 | } else if let broadcasted = lhs.broadcast(to: rhs.shape) { 168 | return broadcasted.add(rhs) 169 | } else if let broadcasted = rhs.broadcast(to: lhs.shape) { 170 | return lhs.add(broadcasted) 171 | } else { 172 | fatalError("You can't add matrices of shape \(lhs.shape) and \(rhs.shape)") 173 | } 174 | } 175 | 176 | public static func + (lhs: Mat, rhs: Double) -> Mat { 177 | return lhs.add(rhs) 178 | } 179 | 180 | public static func + (lhs: Double, rhs: Mat) -> Mat { 181 | return rhs.add(lhs) 182 | } 183 | 184 | public static func - (lhs: Mat, rhs: Double) -> Mat { 185 | return lhs + (-rhs) 186 | } 187 | 188 | public static func - (lhs: Mat, rhs: Mat) -> Mat { 189 | return lhs.subtract(rhs) 190 | } 191 | 192 | public static func -= (lhs: inout Mat, rhs: Mat) { 193 | lhs = lhs - rhs 194 | } 195 | 196 | public static func - (lhs: Double, rhs: Mat) -> Mat { 197 | return lhs + rhs.negate() 198 | } 199 | 200 | public static func * (lhs: Mat, rhs: Double) -> Mat { 201 | return lhs.mult(rhs) 202 | } 203 | 204 | public static func * (lhs: Double, rhs: Mat) -> Mat { 205 | return rhs.mult(lhs) 206 | } 207 | 208 | public static func * (lhs: Mat, rhs: Mat) -> Mat { 209 | if lhs.shape == rhs.shape { 210 | return lhs.mult(rhs) 211 | } else if let broadcasted = lhs.broadcast(to: rhs.shape) { 212 | return broadcasted.mult(rhs) 213 | } else if let broadcasted = rhs.broadcast(to: lhs.shape) { 214 | return lhs.mult(broadcasted) 215 | } else { 216 | fatalError("You can't multiply matrices of shape \(lhs.shape) and \(rhs.shape)") 217 | } 218 | } 219 | 220 | public static func / (lhs: Mat, rhs: Mat) -> Mat { 221 | if lhs.shape == rhs.shape { 222 | return lhs.divide(rhs) 223 | } else if let broadcasted = lhs.broadcast(to: rhs.shape) { 224 | return broadcasted.divide(lhs) 225 | } else if let broadcasted = rhs.broadcast(to: lhs.shape) { 226 | return lhs.divide(broadcasted) 227 | } else { 228 | fatalError("You can't divide matrices of shape \(lhs.shape) and \(rhs.shape)") 229 | } 230 | } 231 | 232 | public func log() -> Mat { 233 | var result = [Double](repeating: 0.0, count: values.count) 234 | var count = Int32(values.count) 235 | vvlog(&result, values, &count) 236 | return Mat(shape: shape, values: result) 237 | } 238 | 239 | public func exp() -> Mat { 240 | var result = [Double](repeating: 0.0, count: values.count) 241 | var count = Int32(values.count) 242 | vvexp(&result, values, &count) 243 | return Mat(shape: shape, values: result) 244 | } 245 | 246 | public func pow(_ value: Double) -> Mat { 247 | var input = values 248 | var y: [Double] = [Double](repeating: value, count: values.count) 249 | var result = [Double](repeating: 0, count: values.count) 250 | var n = Int32(values.count) 251 | vvpow(&result, &y, &input, &n) 252 | return Mat(shape: shape, values: result) 253 | } 254 | 255 | public func tanh() -> Mat { 256 | var input = values 257 | var result: [Double] = [Double](repeating: 0, count: values.count) 258 | var n = Int32(values.count) 259 | vvtanh(&result, &input, &n) 260 | return Mat(shape: shape, values: result) 261 | } 262 | 263 | public func min(_ value: Double) -> Mat { 264 | return clamp(min: -Double.greatestFiniteMagnitude, max: value) 265 | } 266 | 267 | public func max(_ value: Double) -> Mat { 268 | var results = [Double](repeating: 0.0, count: values.count) 269 | var minValue = value 270 | vDSP_vthrD(values, 1, &minValue, &results, 1, vDSP_Length(values.count)) 271 | return Mat(shape: shape, values: results) 272 | } 273 | 274 | public func clamp(min: Double, max: Double) -> Mat { 275 | var results = [Double](repeating: 0.0, count: values.count) 276 | var min = min 277 | var max = max 278 | vDSP_vclipD(values, 1, &min, &max, &results, 1, vDSP_Length(values.count)) 279 | let newValues = values.map { Swift.min(Swift.max($0, min), max) } 280 | return Mat(shape: shape, values: newValues) 281 | } 282 | 283 | public func normalized() -> Mat { 284 | var mean: Double = 0.0 285 | var std: Double = 0.0 286 | var result = [Double](repeating: 0.0, count: values.count) 287 | vDSP_normalizeD(values, 1, &result, 1, &mean, &std, vDSP_Length(values.count)) 288 | return Mat(shape: shape, values: result) 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/NeuralNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol NeuralNetworkDebugOutputHandler: class { 8 | func didFinishedIteration(epochReport: EpochReport, networkRepresentation: NetworkModel) 9 | func startedTraining(with dataSet: DataSetProtocol) 10 | func didFinishTraining() 11 | var debugOutputFrequency: Int { get } 12 | } 13 | 14 | public class NeuralNetwork { 15 | // TODO: batch size support 16 | public let learningRate: Double 17 | public let epochs: Int 18 | public var debugOutputHandler: NeuralNetworkDebugOutputHandler? 19 | public var errorFunction: ErrorFunction = .squared 20 | private(set) var layers: [Layer] = [] 21 | 22 | public init(learningRate: Double, epochs: Int) { 23 | self.learningRate = learningRate 24 | self.epochs = epochs 25 | } 26 | 27 | /// Restores a Network from a model 28 | /// 29 | /// - Parameters: 30 | /// - model: the model contains information about the layer, activation functions, weights and other information that are needed to create the Network 31 | /// - learningRate: The learning rate determines how quickly the network changes the weights based on the loss 32 | /// - epochs: The number of iterations that are used for pass the training data through the network 33 | public convenience init(model: NetworkModel, learningRate: Double = 0, epochs: Int = 0) { 34 | self.init(learningRate: learningRate, epochs: epochs) 35 | model.layer 36 | .map(Layer.init) 37 | .forEach(add) 38 | } 39 | 40 | deinit { 41 | for layer in layers { 42 | layer.next = nil 43 | layer.previous = nil 44 | } 45 | } 46 | 47 | /// Adds a layer to the network 48 | /// 49 | /// - Parameter layer 50 | public func add(layer: Layer) { 51 | layer.previous = layers.last 52 | layers.last?.next = layer 53 | layers.append(layer) 54 | } 55 | 56 | /// Predicts a value for a given input 57 | /// 58 | /// - Parameter input 59 | /// - Returns: The output of the network after forwarding the input through the layers 60 | public func predict(input: Mat) -> Mat { 61 | return layers.reduce(input) { currentInput, layer in 62 | layer.process(input: currentInput) 63 | } 64 | } 65 | 66 | /// Performs asynchronously a full training of the Network for a given input and the expected output. 67 | /// Training means passing the input through the network and back propagate the the error to update the weights and biases for the layers 68 | /// 69 | /// - Parameters: 70 | /// - input 71 | /// - expectedOutput 72 | /// - completion: The completion closure gets called after the training has been finished 73 | public func train(input: Mat, expectedOutput: Mat, completion: (() -> Void)? = nil) { 74 | guard layers[0].nodes == input.shape.rows else { 75 | fatalError("The number of input nodes doesnt match the number of input parameters. The number of nodes must be \(input.shape.rows)") 76 | } 77 | guard layers.last!.nodes == expectedOutput.shape.rows else { 78 | fatalError("The number of output nodes doesnt match the number of possible classes. The number of nodes must be \(expectedOutput.shape.rows)") 79 | } 80 | 81 | DispatchQueue.global().async { 82 | (0 ... self.epochs).forEach { epoch in 83 | let output = self.predict(input: input) 84 | self.backpropagate(expectedOutput: expectedOutput) 85 | if let debugOutputFrequency = self.debugOutputHandler?.debugOutputFrequency, 86 | epoch % debugOutputFrequency == 0 { 87 | DispatchQueue.main.sync { 88 | self.notifyDebugDelegateIfNeeded(epoch: epoch, output: output, expectedOutput: expectedOutput) 89 | } 90 | } 91 | } 92 | DispatchQueue.main.async { 93 | self.debugOutputHandler?.didFinishTraining() 94 | completion?() 95 | } 96 | } 97 | } 98 | 99 | /// Performs synchronously a full training of the Network for a given input and the expected output. 100 | /// Training means passing the input through the network and back propagate the the error to update the weights and biases for the layers 101 | /// 102 | /// - Parameters: 103 | /// - input 104 | /// - expectedOutput 105 | public func train(input: Mat, expectedOutput: Mat) { 106 | (0 ... epochs).forEach { _ in 107 | _ = self.predict(input: input) 108 | backpropagate(expectedOutput: expectedOutput) 109 | } 110 | } 111 | 112 | /// Performs asynchronously a full training of the Network with a dataset. 113 | /// Training means passing the input through the network and back propagate the the error to update the weights and biases for the layers 114 | /// 115 | /// - Parameters: 116 | /// - input 117 | /// - expectedOutput 118 | /// - completion: The completion closure gets called after the training has been finished 119 | public func train(dataSet: DataSetProtocol, completion: (() -> Void)?) { 120 | let input = dataSet.input.transposed() 121 | let output = dataSet.output.transposed() 122 | 123 | train(input: input, expectedOutput: output, completion: completion) 124 | debugOutputHandler?.startedTraining(with: dataSet) 125 | } 126 | 127 | /// Tests the correctness of a Networkwith with a given input and the expected output. 128 | /// 129 | /// - Parameters: 130 | /// - input 131 | /// - expectedOutput 132 | /// - Returns: The TestReport contains the classification results 133 | public func test(input: Mat, expectedOutput: Mat) -> TestReport { 134 | let prediction = predict(input: input) 135 | let classifications = classificationReports(for: prediction, expectedOutput: expectedOutput) 136 | return TestReport(classifications: classifications) 137 | } 138 | 139 | /// Tests the correctness of a Networkwith with a DataSet. 140 | /// 141 | /// - Parameters: 142 | /// - input 143 | /// - expectedOutput 144 | /// - Returns: The TestReport contains the classification results 145 | public func test(dataSet: DataSetProtocol) -> TestReport { 146 | let input = dataSet.input.transposed() 147 | let output = dataSet.output.transposed() 148 | 149 | return test(input: input, expectedOutput: output) 150 | } 151 | 152 | /// Performs asynchronously a training and testing of of the Network with a dataset. 153 | /// 154 | /// - Parameters: 155 | /// - input 156 | /// - expectedOutput 157 | /// - completion: The completion closure gets called after the training and testing has been finished 158 | public func trainAndTest(trainData: DataSetProtocol, testData: DataSetProtocol, completion: ((TestReport) -> Void)?) { 159 | train(dataSet: trainData) { 160 | completion?(self.test(dataSet: testData)) 161 | } 162 | } 163 | 164 | /// 165 | /// 166 | /// - Returns: A model representation of the network. The model can be used to save the model to disk and recreate the Network. It can also be used to inspect internal parameters such as weights and biases of the layer 167 | public func model() -> NetworkModel { 168 | let layerRepresentation = layers.map { $0.modelRepresentation() } 169 | return NetworkModel(layer: layerRepresentation) 170 | } 171 | 172 | private func backpropagate(expectedOutput: Mat) { 173 | let reversedLayers = layers.reversed().dropLast() 174 | guard let outputLayer = reversedLayers.first else { 175 | fatalError() 176 | } 177 | let error = errorFunction.functionDerivative(outputLayer.activatedOutput!, expectedOutput) 178 | let outputClassesCount: Double = Double(expectedOutput.shape.cols) 179 | outputLayer.updateForOutputLayer(error: error, m: outputClassesCount, learningRate: learningRate) 180 | _ = reversedLayers.dropFirst().reduce(error) { nextError, layer in 181 | layer.updateForHiddenLayer(m: outputClassesCount, learningRate: learningRate, nextError: nextError) 182 | } 183 | } 184 | 185 | private func notifyDebugDelegateIfNeeded(epoch: Int, output: Mat, expectedOutput: Mat) { 186 | guard let debugOutputHandler = debugOutputHandler else { 187 | return 188 | } 189 | let loss = errorFunction.function(output, expectedOutput).sum() / Double(output.values.count) 190 | let report = EpochReport(epochIndex: epoch, classifications: classificationReports(for: output, expectedOutput: expectedOutput), loss: loss) 191 | debugOutputHandler.didFinishedIteration(epochReport: report, networkRepresentation: model()) 192 | } 193 | 194 | private func classificationReports(for output: Mat, expectedOutput: Mat, discrete: Bool = true) -> [ClassificationReport] { 195 | return zip(output.transposed().values2D, expectedOutput.transposed().values2D) 196 | .map { arg in 197 | let (predictedValue, realValue) = arg 198 | let prediction: [Double] 199 | if discrete { 200 | let maxValue = predictedValue.max() ?? 0 201 | prediction = predictedValue.map { value in 202 | value == maxValue ? 1.0 : 0.0 203 | } 204 | } else { 205 | prediction = predictedValue 206 | } 207 | return ClassificationReport(prediction: prediction, real: realValue) 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/NeuralNetwork/TestReport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct ClassificationReport: Codable { 8 | public let prediction: [Double] 9 | public let real: [Double] 10 | } 11 | 12 | public extension Array where Element == ClassificationReport { 13 | /// The percentage of predictions that are correct 14 | var accuracy: Float { 15 | let numberOfHits = Float(filter { $0.prediction == $0.real }.count) 16 | let numberOfElements = Float(count) 17 | return numberOfHits / numberOfElements 18 | } 19 | } 20 | 21 | public struct TestReport { 22 | public let classifications: [ClassificationReport] 23 | } 24 | 25 | extension TestReport: CustomStringConvertible, CustomPlaygroundDisplayConvertible { 26 | public var playgroundDescription: Any { 27 | return description 28 | } 29 | 30 | public var description: String { 31 | var result = "Testing\n-------------------------\n" 32 | result += classifications.map { report in 33 | let label: String 34 | if report.prediction == report.real { 35 | label = "✅" 36 | } else { 37 | label = "❌" 38 | } 39 | return "\(label) Prediction: \(report.prediction), Real: \(report.real)" 40 | }.joined(separator: "\n") 41 | result += "\nAccuracy: \(classifications.accuracy)%\n" 42 | result += "" 43 | return result 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/NetworkVisualisationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import PlaygroundSupport 7 | 8 | struct NetworkVisualisationState: Codable { 9 | let epochReport: EpochReport 10 | let networkRepresentation: NetworkModel 11 | } 12 | 13 | extension NetworkVisualisationState { 14 | init?(_ playgroundValue: PlaygroundValue) { 15 | switch playgroundValue { 16 | case let .data(data): 17 | guard let model = try? JSONDecoder().decode(NetworkVisualisationState.self, from: data) else { 18 | return nil 19 | } 20 | self = model 21 | default: 22 | return nil 23 | } 24 | } 25 | 26 | var playgroundValue: PlaygroundValue { 27 | let data = try! JSONEncoder().encode(self) 28 | return .data(data) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundLiveViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import PlaygroundSupport 6 | import UIKit 7 | 8 | public class PlaygroundLiveViewController: UIViewController, PlaygroundLiveViewMessageHandler, PlaygroundLiveViewSafeAreaContainer { 9 | public var page: Int? { 10 | didSet { 11 | if let page = page { 12 | displayPreview(for: page) 13 | } 14 | } 15 | } 16 | 17 | public lazy var visualisationViewController: NetworkVisualisationViewController = { 18 | let vc = NetworkVisualisationViewController() 19 | addChild(vc) 20 | view.addSubview(vc.view) 21 | vc.didMove(toParent: self) 22 | vc.view.translatesAutoresizingMaskIntoConstraints = false 23 | let padding: CGFloat = 16 24 | NSLayoutConstraint.activate([ 25 | liveViewSafeAreaGuide.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor, constant: padding), 26 | liveViewSafeAreaGuide.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: padding), 27 | liveViewSafeAreaGuide.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: -padding), 28 | liveViewSafeAreaGuide.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor, constant: -padding), 29 | ]) 30 | 31 | return vc 32 | }() 33 | 34 | private func displayPreview(for page: Int) { 35 | if let model = PlaygroundStore.getModel(for: page) { 36 | visualisationViewController.networkView.update(viewModel: model) 37 | visualisationViewController.networkView.stopTrainingAnimation() 38 | visualisationViewController.dashboardStackView.isHidden = true 39 | } 40 | } 41 | 42 | public override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | view.backgroundColor = .white 46 | } 47 | 48 | public func hideDashboard() { 49 | visualisationViewController.dashboardStackView.superview?.isHidden = true 50 | } 51 | 52 | public override func viewDidAppear(_ animated: Bool) { 53 | super.viewDidAppear(animated) 54 | if let page = page { 55 | displayPreview(for: page) 56 | } 57 | } 58 | 59 | public func receive(_ message: PlaygroundValue) { 60 | // TODO: run playground remotely and send result back to contents view or check if liveView can only be used for empty state presentation 61 | if let model = NetworkVisualisationState(message) { 62 | visualisationViewController.didFinishedIteration(epochReport: model.epochReport, networkRepresentation: model.networkRepresentation) 63 | } else if let startMessage = StartedTrainingPlaygroundMessage(message) { 64 | visualisationViewController.startedTraining(with: startMessage.dataSet) 65 | } 66 | } 67 | 68 | public func liveViewMessageConnectionClosed() {} 69 | } 70 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundLiveViewSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import PlaygroundSupport 7 | 8 | public class PlaygroundLiveViewSessionManager { 9 | public static let shared = PlaygroundLiveViewSessionManager() 10 | 11 | private init() {} 12 | 13 | public func sendError(hint: String, solution: String) { 14 | let messageValue = PlaygroundValue.dictionary([ 15 | Constants.errorKey: PlaygroundValue.boolean(true), 16 | Constants.hintKey: PlaygroundValue.string(hint), 17 | Constants.solutionKey: PlaygroundValue.string(solution), 18 | ]) 19 | send(message: messageValue) 20 | } 21 | 22 | public func sendPassMessage(message: String) { 23 | let messageValue = PlaygroundValue.dictionary([ 24 | Constants.errorKey: PlaygroundValue.boolean(false), 25 | Constants.passKey: PlaygroundValue.string(message), 26 | ]) 27 | send(message: messageValue) 28 | } 29 | 30 | private func send(message: PlaygroundValue) { 31 | PlaygroundPage.current.messageHandler?.send(message) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundNetworkVisualisationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import PlaygroundSupport 6 | import UIKit 7 | 8 | public class PlaygroundNetworkVisualisationViewController: NetworkVisualisationViewController, PlaygroundLiveViewSafeAreaContainer { 9 | // private var firstEpoch = true 10 | public var page: Int? 11 | public override func viewDidLoad() { 12 | super.viewDidLoad() 13 | 14 | let padding: CGFloat = 16 15 | horizontalStackView.translatesAutoresizingMaskIntoConstraints = false 16 | NSLayoutConstraint.activate([ 17 | liveViewSafeAreaGuide.trailingAnchor.constraint(equalTo: horizontalStackView.trailingAnchor, constant: padding), 18 | liveViewSafeAreaGuide.bottomAnchor.constraint(equalTo: horizontalStackView.bottomAnchor, constant: padding), 19 | liveViewSafeAreaGuide.topAnchor.constraint(equalTo: horizontalStackView.topAnchor, constant: -padding), 20 | liveViewSafeAreaGuide.leadingAnchor.constraint(equalTo: horizontalStackView.leadingAnchor, constant: -padding), 21 | 22 | ]) 23 | } 24 | 25 | public override func didFinishedIteration(epochReport: EpochReport, networkRepresentation: NetworkModel) { 26 | super.didFinishedIteration(epochReport: epochReport, networkRepresentation: networkRepresentation) 27 | // if firstEpoch { 28 | safeModel() 29 | // firstEpoch = false 30 | // } 31 | } 32 | 33 | public override func didFinishTraining() { 34 | super.didFinishTraining() 35 | safeModel() 36 | } 37 | 38 | private func safeModel() { 39 | if let page = page, 40 | let model = lastModel { 41 | PlaygroundStore.save(model: model, for: page) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundPageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import PlaygroundSupport 6 | 7 | extension PlaygroundPage { 8 | public var proxy: PlaygroundRemoteLiveViewProxy? { 9 | return liveView as? PlaygroundRemoteLiveViewProxy 10 | } 11 | 12 | public var messageHandler: PlaygroundLiveViewMessageHandler? { 13 | return liveView as? PlaygroundLiveViewMessageHandler 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundPageSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import PlaygroundSupport 7 | 8 | public class PlaygroundPageSessionManager: PlaygroundRemoteLiveViewProxyDelegate { 9 | public static let shared = PlaygroundPageSessionManager() 10 | 11 | private init() {} 12 | 13 | public func setup() { 14 | PlaygroundPage.current.proxy?.delegate = self 15 | } 16 | 17 | public func remoteLiveViewProxy(_: PlaygroundRemoteLiveViewProxy, received message: PlaygroundValue) { 18 | guard case let .dictionary(messageValue) = message else { 19 | return 20 | } 21 | 22 | guard case let .boolean(isErrorMessage)? = messageValue[Constants.errorKey] else { 23 | return 24 | } 25 | 26 | if isErrorMessage { 27 | showErrorMessage(messageValue: messageValue) 28 | } else { 29 | showSuccessMessage(messageValue: messageValue) 30 | } 31 | } 32 | 33 | func showErrorMessage(messageValue: [String: PlaygroundValue]) { 34 | guard case let .string(solution)? = messageValue[Constants.solutionKey] else { 35 | return 36 | } 37 | 38 | guard case let .string(hint)? = messageValue[Constants.hintKey] else { 39 | return 40 | } 41 | 42 | showErrorMessage(hint: hint, solution: solution) 43 | } 44 | 45 | public func showErrorMessage(hint: String, solution: String?) { 46 | PlaygroundPage.current.assessmentStatus = .fail(hints: [hint], solution: solution) 47 | } 48 | 49 | public func resetAssesmentStatus() { 50 | PlaygroundPage.current.assessmentStatus = nil 51 | } 52 | 53 | func showSuccessMessage(messageValue: [String: PlaygroundValue]) { 54 | guard case let .string(passMessage)? = messageValue[Constants.passKey] else { 55 | return 56 | } 57 | 58 | PlaygroundPage.current.assessmentStatus = .pass(message: passMessage) 59 | PlaygroundPage.current.finishExecution() 60 | } 61 | 62 | public func remoteLiveViewProxyConnectionClosed(_: PlaygroundRemoteLiveViewProxy) {} 63 | } 64 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundRemoteNeuralNetworkDebugOutputHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import PlaygroundSupport 7 | 8 | public class PlaygroundRemoteNeuralNetworkDebugOutputHandler: NeuralNetworkDebugOutputHandler { 9 | public init() {} 10 | 11 | public func didFinishedIteration(epochReport: EpochReport, networkRepresentation: NetworkModel) { 12 | let playgroundValue = NetworkVisualisationState(epochReport: epochReport, networkRepresentation: networkRepresentation).playgroundValue 13 | PlaygroundPage.current.proxy?.send(playgroundValue) 14 | } 15 | 16 | public func startedTraining(with dataSet: DataSetProtocol) { 17 | let dataSetInstace = DataSet(input: dataSet.input, output: dataSet.output, inputLabels: dataSet.inputLabels) 18 | 19 | PlaygroundPage.current.proxy?.send(StartedTrainingPlaygroundMessage(dataSet: dataSetInstace).playgroundValue) 20 | } 21 | 22 | public func didFinishTraining() {} 23 | 24 | public var debugOutputFrequency: Int { 25 | return 10 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/PlaygroundStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import PlaygroundSupport 6 | import UIKit 7 | 8 | public class PlaygroundStore { 9 | public class func save(model: NetworkModel, for page: Int) { 10 | let data = try! JSONEncoder().encode(model) 11 | PlaygroundKeyValueStore.current["\(page)"] = .data(data) 12 | } 13 | 14 | public class func getModel(for page: Int) -> NetworkModel? { 15 | if case let .data(modelData)? = PlaygroundKeyValueStore.current["\(page)"], 16 | let model = try? JSONDecoder().decode(NetworkModel.self, from: modelData) { 17 | return model 18 | } 19 | 20 | // fallback default models 21 | func modelHelper(layout: [Int]) -> NetworkModel { 22 | let layers = layout.map { x in 23 | LayerModel(neurons: (0 ..< x).map { _ in NeuronModel(weigths: nil, bias: nil) }) 24 | } 25 | return NetworkModel(layer: layers) 26 | } 27 | 28 | switch page { 29 | case 0: 30 | return modelHelper(layout: [2, 4, 2]) 31 | case 1: 32 | return modelHelper(layout: [2, 2]) 33 | case 2: 34 | return modelHelper(layout: [2, 4, 2]) 35 | case 3: 36 | return modelHelper(layout: [2, 4, 4]) 37 | case 4: 38 | return modelHelper(layout: [4, 5, 4, 3]) 39 | default: 40 | return nil 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/Playground Support/StartedTrainingPlaygroundMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import PlaygroundSupport 7 | 8 | struct StartedTrainingPlaygroundMessage: Codable { 9 | let dataSet: DataSet 10 | } 11 | 12 | extension StartedTrainingPlaygroundMessage { 13 | init?(_ playgroundValue: PlaygroundValue) { 14 | switch playgroundValue { 15 | case let .data(data): 16 | guard let model = try? JSONDecoder().decode(StartedTrainingPlaygroundMessage.self, from: data) else { 17 | return nil 18 | } 19 | self = model 20 | default: 21 | return nil 22 | } 23 | } 24 | 25 | var playgroundValue: PlaygroundValue { 26 | let data = try! JSONEncoder().encode(self) 27 | return .data(data) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/AxisLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class AxisLayer: CALayer { 8 | let xLayer = CALayer() 9 | let yLayer = CALayer() 10 | 11 | private let color: CGColor = UIColor.blue.cgColor 12 | private var valueSubLayers: [CALayer] = [] 13 | private let lineSize = CGSize(width: 1, height: 15) 14 | private let fontSize: CGFloat = 22 15 | 16 | var showXAxis = true { 17 | didSet { 18 | xLayer.isHidden = !showXAxis 19 | } 20 | } 21 | 22 | var displayedValues: [Int] = [] 23 | var resolution = CGVector(dx: 50, dy: 50) 24 | var axisOffSet = CGVector(dx: 0, dy: 0) 25 | 26 | var showYAxis = true { 27 | didSet { 28 | yLayer.isHidden = !showYAxis 29 | } 30 | } 31 | 32 | override init(layer: Any) { 33 | super.init(layer: layer) 34 | initAxis() 35 | } 36 | 37 | required init?(coder aCoder: NSCoder) { 38 | super.init(coder: aCoder) 39 | initAxis() 40 | } 41 | 42 | override init() { 43 | super.init() 44 | initAxis() 45 | } 46 | 47 | private func initAxis() { 48 | xLayer.bounds.size.height = lineSize.width 49 | xLayer.backgroundColor = color 50 | yLayer.backgroundColor = color 51 | yLayer.bounds.size.width = lineSize.width 52 | addSublayer(xLayer) 53 | addSublayer(yLayer) 54 | } 55 | 56 | override func prepareForInterfaceBuilder() { 57 | initAxis() 58 | } 59 | 60 | private func updateDisplayedValues() { 61 | // TODO: cleanup 62 | valueSubLayers.forEach { $0.removeFromSuperlayer() } 63 | valueSubLayers.removeAll() 64 | valueSubLayers.reserveCapacity(displayedValues.count * 4) 65 | 66 | for value in displayedValues { 67 | // X Axis 68 | if showXAxis { 69 | let layer = CALayer() 70 | layer.bounds.size = CGSize(width: lineSize.width, height: lineSize.height) 71 | layer.position = CGPoint(x: bounds.midX + resolution.dx * CGFloat(value), y: bounds.midY + axisOffSet.dy) 72 | layer.backgroundColor = color 73 | addSublayer(layer) 74 | 75 | let textLayer = CATextLayer() 76 | textLayer.foregroundColor = color 77 | textLayer.bounds.size = CGSize(width: fontSize, height: fontSize) 78 | textLayer.position = CGPoint(x: bounds.midX + resolution.dx * CGFloat(value), y: bounds.midY + textLayer.bounds.midY + layer.bounds.height + axisOffSet.dy) 79 | textLayer.fontSize = fontSize 80 | textLayer.alignmentMode = .center 81 | textLayer.string = "\(value)" 82 | addSublayer(textLayer) 83 | valueSubLayers.append(textLayer) 84 | valueSubLayers.append(layer) 85 | } 86 | 87 | if showYAxis { 88 | // Y Axis 89 | let layerY = CALayer() 90 | layerY.bounds.size = CGSize(width: lineSize.height, height: lineSize.width) 91 | layerY.position = CGPoint(x: bounds.midX + axisOffSet.dx, y: bounds.midY - resolution.dy * CGFloat(value)) 92 | layerY.backgroundColor = color 93 | addSublayer(layerY) 94 | 95 | let textLayerY = CATextLayer() 96 | textLayerY.foregroundColor = color 97 | textLayerY.bounds.size = CGSize(width: fontSize, height: fontSize) 98 | textLayerY.position = CGPoint(x: bounds.midX - 99 | textLayerY.bounds.midX - layerY.bounds.width + axisOffSet.dx, y: bounds.midY - resolution.dy * CGFloat(value)) 100 | textLayerY.fontSize = fontSize 101 | textLayerY.alignmentMode = .center 102 | textLayerY.preferredFrameSize() 103 | textLayerY.string = "\(value)" 104 | addSublayer(textLayerY) 105 | valueSubLayers.append(textLayerY) 106 | valueSubLayers.append(layerY) 107 | } 108 | } 109 | } 110 | 111 | override func layoutSublayers() { 112 | super.layoutSublayers() 113 | 114 | xLayer.bounds.size.width = bounds.width 115 | yLayer.bounds.size.height = bounds.height 116 | xLayer.position = CGPoint(x: bounds.midX, y: bounds.midY + axisOffSet.dy) 117 | yLayer.position = CGPoint(x: bounds.midX + axisOffSet.dx, y: bounds.midY) 118 | 119 | updateDisplayedValues() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/ConnectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension CGPoint { 8 | fileprivate func convert(to view: UIView) -> CGPoint { 9 | return applying(.init(scaleX: view.bounds.width, y: view.bounds.height)) 10 | } 11 | } 12 | 13 | @IBDesignable 14 | class ConnectionView: UIView { 15 | @IBInspectable 16 | var source: CGPoint = CGPoint(x: 0, y: 0.2) { 17 | didSet { 18 | controlPoint2.y = source.y == 0.5 ? 0.55 : source.y 19 | } 20 | } 21 | 22 | @IBInspectable 23 | var sink: CGPoint = CGPoint(x: 1, y: 0.8) { 24 | didSet { 25 | controlPoint.y = sink.y == 0.5 ? 0.45 : sink.y 26 | } 27 | } 28 | 29 | @IBInspectable 30 | var controlPoint: CGPoint = CGPoint(x: 0.6, y: 0.2) 31 | 32 | @IBInspectable 33 | var controlPoint2: CGPoint = CGPoint(x: 0.4, y: 0.8) 34 | 35 | @IBInspectable 36 | var lineWidth: CGFloat = 0.5 { 37 | didSet { 38 | shapeLayer.lineWidth = lineWidth 39 | } 40 | } 41 | 42 | var strokeColor: UIColor = UIColor.blue 43 | 44 | private let shapeLayer = CAShapeLayer() 45 | 46 | override init(frame: CGRect) { 47 | super.init(frame: frame) 48 | 49 | addLineLayer() 50 | } 51 | 52 | required init?(coder aDecoder: NSCoder) { 53 | super.init(coder: aDecoder) 54 | 55 | addLineLayer() 56 | } 57 | 58 | override func prepareForInterfaceBuilder() { 59 | super.prepareForInterfaceBuilder() 60 | 61 | addLineLayer() 62 | updatePath() 63 | } 64 | 65 | override func didMoveToSuperview() { 66 | super.didMoveToSuperview() 67 | updatePath() 68 | } 69 | 70 | public func startAnimation() { 71 | let animation = CABasicAnimation(keyPath: "lineDashPhase") 72 | animation.fromValue = 0 73 | animation.toValue = shapeLayer.lineDashPattern!.reduce(0) { $0 - $1.intValue } 74 | animation.duration = 0.8 75 | animation.repeatCount = .infinity 76 | shapeLayer.add(animation, forKey: "line") 77 | } 78 | 79 | public func stopAnimation() { 80 | shapeLayer.removeAnimation(forKey: "line") 81 | } 82 | 83 | private func addLineLayer() { 84 | shapeLayer.frame = bounds 85 | shapeLayer.lineWidth = lineWidth 86 | shapeLayer.strokeColor = strokeColor.withAlphaComponent(0.7).cgColor 87 | shapeLayer.fillColor = nil 88 | shapeLayer.lineDashPattern = [24, 6] 89 | layer.addSublayer(shapeLayer) 90 | } 91 | 92 | private func updatePath() { 93 | let path = UIBezierPath() 94 | path.move(to: source.applying(.init(scaleX: bounds.width, y: bounds.height))) 95 | 96 | if controlPoint.y < controlPoint2.y { 97 | path.addCurve(to: sink.convert(to: self), controlPoint1: controlPoint.convert(to: self), controlPoint2: controlPoint2.convert(to: self)) 98 | } else { 99 | // the sink is higher than the source and we are swapping the control points to get the same curve 100 | path.addCurve(to: sink.convert(to: self), controlPoint1: controlPoint2.convert(to: self), controlPoint2: controlPoint.convert(to: self)) 101 | } 102 | 103 | shapeLayer.path = path.cgPath 104 | } 105 | 106 | override func layoutSubviews() { 107 | super.layoutSubviews() 108 | updatePath() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/DecisionBoundaryPlotView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | public class DecisionBoundaryPlotView: UIView { 8 | lazy var decisionBoundaryView: DecisionBoundaryView = { 9 | let view = DecisionBoundaryView(frame: bounds) 10 | view.backgroundColor = .white 11 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 12 | insertSubview(view, at: 0) 13 | return view 14 | }() 15 | 16 | private lazy var plotView: PlotView = { 17 | let view = PlotView(frame: bounds) 18 | view.useGrid = true 19 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 20 | addSubview(view) 21 | return view 22 | }() 23 | 24 | public func plotPoints(dataSet: DataSetProtocol) { 25 | plotView.plot(dataSet: dataSet) 26 | } 27 | 28 | public func plotDecisionBoundary(values: Mat) { 29 | decisionBoundaryView.plot(values: values) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/DecisionBoundaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class DecisionBoundaryView: UIView { 8 | private var imageBuffer: CGImage? 9 | private let context = CIContext() 10 | 11 | public var colorIndexOffset = -1 12 | 13 | struct Pixel { 14 | let r: UInt8, g: UInt8, b: UInt8, a: UInt8 15 | } 16 | 17 | public func plot(values: Mat) { 18 | DispatchQueue.global(qos: .background).async { 19 | guard let image = self.image(for: values.values2D) else { 20 | return 21 | } 22 | DispatchQueue.main.async { 23 | self.imageBuffer = image 24 | self.setNeedsDisplay() 25 | } 26 | } 27 | } 28 | 29 | private func image(for values: [[Double]]) -> CGImage? { 30 | let bitsPerComponent = 8 31 | let bitsPerPixel = 32 32 | let rgbColorSpace = CGColorSpaceCreateDeviceRGB() 33 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) 34 | 35 | let pixels = values.map(pixel) 36 | let sizef = sqrt(Float(pixels.count)) 37 | let size = Int(sizef) 38 | let imageData = Data(bytes: pixels, count: pixels.count * MemoryLayout.stride) as CFData 39 | guard let providerRef = CGDataProvider(data: imageData) else { 40 | return nil 41 | } 42 | return CGImage(width: size, height: size, 43 | bitsPerComponent: bitsPerComponent, 44 | bitsPerPixel: bitsPerPixel, 45 | bytesPerRow: size * MemoryLayout.stride, 46 | space: rgbColorSpace, 47 | bitmapInfo: bitmapInfo, 48 | provider: providerRef, 49 | decode: nil, 50 | shouldInterpolate: true, 51 | intent: .defaultIntent) 52 | } 53 | 54 | private func pixel(for values: [Double]) -> Pixel { 55 | let maxValue = values.max()! 56 | let maxIndex = Double((values.index(of: maxValue) ?? 0) + colorIndexOffset) 57 | let colorValues = ColorHelper.pixelValues.prefix(values.count) 58 | if maxIndex < 0 { 59 | return colorValues.last!.color 60 | } 61 | return colorValues[Int(maxIndex)].color 62 | } 63 | 64 | override func draw(_ rect: CGRect) { 65 | let context = UIGraphicsGetCurrentContext() 66 | guard let imageBuffer = imageBuffer else { 67 | return 68 | } 69 | context?.draw(imageBuffer, in: rect) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/GridLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import QuartzCore 6 | import UIKit 7 | 8 | class GridLayer: CALayer { 9 | private let squareLayer = CALayer() 10 | private let innerReplicatorLayer = CAReplicatorLayer() 11 | private let replicatorLayer = CAReplicatorLayer() 12 | 13 | public var instanceCount = 10 { 14 | didSet { 15 | setNeedsLayout() 16 | } 17 | } 18 | 19 | override init() { 20 | super.init() 21 | commonInit() 22 | } 23 | 24 | override init(layer: Any) { 25 | super.init(layer: layer) 26 | commonInit() 27 | } 28 | 29 | required init?(coder aCoder: NSCoder) { 30 | super.init(coder: aCoder) 31 | commonInit() 32 | } 33 | 34 | func commonInit() { 35 | let borderWidth: CGFloat = 1 36 | let borderColor: CGColor = UIColor.black.cgColor 37 | self.borderWidth = borderWidth + 1 38 | self.borderColor = borderColor 39 | 40 | squareLayer.borderColor = borderColor 41 | squareLayer.borderWidth = borderWidth 42 | innerReplicatorLayer.addSublayer(squareLayer) 43 | 44 | replicatorLayer.addSublayer(innerReplicatorLayer) 45 | addSublayer(replicatorLayer) 46 | } 47 | 48 | override func layoutSublayers() { 49 | super.layoutSublayers() 50 | innerReplicatorLayer.instanceCount = instanceCount 51 | replicatorLayer.instanceCount = instanceCount 52 | let squareSize = CGSize(width: bounds.width / instanceCount, height: bounds.height / instanceCount) 53 | innerReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(squareSize.width, 0, 0) 54 | replicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, squareSize.height, 0) 55 | replicatorLayer.frame = bounds 56 | squareLayer.frame = CGRect(x: 0, y: 0, width: squareSize.width, height: squareSize.height) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/LayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | extension CGFloat { 8 | public static func * (left: Int, right: CGFloat) -> CGFloat { 9 | return CGFloat(left) * right 10 | } 11 | 12 | public static func / (left: CGFloat, right: Int) -> CGFloat { 13 | return left / CGFloat(right) 14 | } 15 | } 16 | 17 | extension CGSize { 18 | var midX: CGFloat { 19 | return width / 2 20 | } 21 | } 22 | 23 | class LayerView: UIView { 24 | var viewModel: LayerModel? 25 | private var shouldColorizeNodes: Bool = false 26 | 27 | init(frame: CGRect, viewModel: LayerModel, shouldColorizeNodes: Bool) { 28 | self.viewModel = viewModel 29 | self.shouldColorizeNodes = shouldColorizeNodes 30 | super.init(frame: frame) 31 | displayNodes() 32 | } 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | } 37 | 38 | required init?(coder _: NSCoder) { 39 | fatalError() 40 | } 41 | 42 | lazy var neuronSize = { 43 | CGSize(width: bounds.width, height: bounds.width) 44 | }() 45 | 46 | public func nodeFrame(for index: Int) -> CGRect { 47 | guard let viewModel = viewModel else { 48 | return .zero 49 | } 50 | let numberOfNeurons: CGFloat = max(CGFloat(viewModel.neurons.count), 1) 51 | let verticalPadding = ((bounds.height - (numberOfNeurons * neuronSize.height)) / numberOfNeurons) / 2 52 | 53 | let point = CGPoint(x: 0, y: verticalPadding + index * (neuronSize.height + verticalPadding * 2)) 54 | return CGRect(origin: point, size: neuronSize) 55 | } 56 | 57 | func displayNodes() { 58 | guard let viewModel = viewModel else { 59 | return 60 | } 61 | viewModel.neurons.enumerated().map { 62 | var color: UIColor 63 | if shouldColorizeNodes, viewModel.neurons.count <= ColorHelper.allColors.count { 64 | color = ColorHelper.allColors[$0.offset] 65 | } else { 66 | color = .black 67 | } 68 | return NeuronView(frame: nodeFrame(for: $0.offset), viewModel: $0.element, color: color) 69 | }.forEach(addSubview) 70 | } 71 | 72 | override func layoutSubviews() { 73 | super.layoutSubviews() 74 | 75 | for (index, nodeView) in subviews.enumerated() { 76 | nodeView.frame = nodeFrame(for: index) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/LinePlotView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class LinePlotView: UIView { 8 | private var path = UIBezierPath() 9 | 10 | public var showAxis: Bool = false { 11 | didSet { 12 | axisLayer.isHidden = !showAxis 13 | } 14 | } 15 | 16 | var maxX: CGFloat = -1 17 | var maxY: CGFloat = 0 18 | var minY: CGFloat = 0 19 | 20 | public let axisLayer = AxisLayer() 21 | 22 | var shapeLayer = CAShapeLayer() 23 | 24 | var pathTransform: CGAffineTransform { 25 | let pathBounds = path.bounds 26 | let yScaleFactor = bounds.height / max(maxY - minY, 1) 27 | let xScaleFactor = bounds.width / max(pathBounds.maxX, 1) 28 | return CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor) 29 | .translatedBy(x: 0, y: abs(minY)) 30 | } 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | layer.addSublayer(axisLayer) 35 | axisLayer.isHidden = !showAxis 36 | 37 | shapeLayer.lineWidth = 2 38 | shapeLayer.strokeColor = UIColor.black.cgColor 39 | shapeLayer.fillColor = nil 40 | shapeLayer.isGeometryFlipped = true 41 | layer.addSublayer(shapeLayer) 42 | 43 | layoutSublayers(of: layer) 44 | } 45 | 46 | required init?(coder _: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | public func add(value: Double) { 51 | let y = CGFloat(value * 100) 52 | let x = maxX + 1 53 | let newPoint = CGPoint(x: x, y: y) 54 | updatePath(with: newPoint) 55 | updateUI() 56 | maxX = newPoint.x 57 | } 58 | 59 | private func updatePath(with point: CGPoint) { 60 | if point.y > maxY { 61 | maxY = point.y 62 | } else if point.y < minY { 63 | minY = point.y 64 | } 65 | if maxX < 0 { 66 | path.move(to: point) 67 | } 68 | path.addLine(to: point) 69 | } 70 | 71 | private func updateUI() { 72 | let currentPath = path.copy() as! UIBezierPath 73 | currentPath.apply(pathTransform) 74 | shapeLayer.path = currentPath.cgPath 75 | } 76 | 77 | override func layoutSubviews() { 78 | super.layoutSubviews() 79 | updateUI() 80 | } 81 | 82 | override func layoutSublayers(of layer: CALayer) { 83 | super.layoutSublayers(of: layer) 84 | 85 | axisLayer.bounds = bounds 86 | axisLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) 87 | shapeLayer.bounds = bounds 88 | shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/NetworkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class NetworkView: UIView { 8 | private var layerViews: [LayerView] = [] 9 | private var connectionViews: [ConnectionView] = [] 10 | private var inputLabels: [UILabel] = [] 11 | private var viewModel: NetworkModel? { 12 | didSet { 13 | if oldValue == nil { 14 | displayNetwork() 15 | } 16 | } 17 | } 18 | 19 | public var inputLabelValues: [String] = [] { 20 | didSet { 21 | updateInputLabels() 22 | } 23 | } 24 | 25 | init() { 26 | super.init(frame: .zero) 27 | } 28 | 29 | init(frame: CGRect, viewModel: NetworkModel) { 30 | self.viewModel = viewModel 31 | super.init(frame: frame) 32 | displayNetwork() 33 | } 34 | 35 | required init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | } 38 | 39 | public func update(viewModel: NetworkModel) { 40 | self.viewModel = viewModel 41 | var index = 0 42 | for layer in viewModel.layer { 43 | for neuron in layer.neurons { 44 | guard let neuronWeights = neuron.weigths else { 45 | continue 46 | } 47 | for weight in neuronWeights { 48 | let connectionView = connectionViews[index] 49 | connectionView.lineWidth = min(CGFloat(abs(weight) + 0.1), 10) 50 | index += 1 51 | } 52 | } 53 | } 54 | } 55 | 56 | public func startTrainingAnimation() { 57 | connectionViews.forEach { $0.startAnimation() } 58 | } 59 | 60 | public func stopTrainingAnimation() { 61 | connectionViews.forEach { $0.stopAnimation() } 62 | } 63 | 64 | private var horizontalPadding: CGFloat { 65 | return 50 66 | } 67 | 68 | private var distanceBetweenLayer: CGFloat { 69 | guard let viewModel = viewModel else { 70 | return 0 71 | } 72 | return (bounds.width - (horizontalPadding * 2) - (viewModel.layer.count * layerSize.width)) / max(CGFloat(viewModel.layer.count - 1), 1) 73 | } 74 | 75 | private var layerSize: CGSize { 76 | return CGSize(width: 44, height: bounds.height) 77 | } 78 | 79 | private func displayNetwork() { 80 | guard let viewModel = viewModel else { 81 | return 82 | } 83 | 84 | for i in 0 ..< viewModel.layer.count { 85 | let frame = layerFrame(at: i) 86 | let layerView = LayerView(frame: frame, viewModel: viewModel.layer[i], shouldColorizeNodes: i == viewModel.layer.count - 1) 87 | layerViews.append(layerView) 88 | addSubview(layerView) 89 | } 90 | 91 | (0 ..< viewModel.layer.count - 1).forEach(addConnectionViews) 92 | updateLayout() 93 | startTrainingAnimation() 94 | } 95 | 96 | private func layerFrame(at index: Int) -> CGRect { 97 | let point = CGPoint(x: index * (layerSize.width + distanceBetweenLayer) + horizontalPadding, y: 0) 98 | return CGRect(origin: point, size: layerSize) 99 | } 100 | 101 | private func addConnectionViews(layerIndex: Int) { 102 | guard let viewModel = viewModel else { 103 | return 104 | } 105 | 106 | let numberOfNeurons = viewModel.layer[layerIndex].neurons.count 107 | let nextNumberOfNeurons = viewModel.layer[layerIndex + 1].neurons.count 108 | for _ in 0 ..< numberOfNeurons { 109 | for _ in 0 ..< nextNumberOfNeurons { 110 | let connectionView = ConnectionView(frame: .zero) 111 | insertSubview(connectionView, at: 0) 112 | connectionViews.append(connectionView) 113 | } 114 | } 115 | } 116 | 117 | private func updateInputLabels() { 118 | inputLabels.forEach { $0.removeFromSuperview() } 119 | inputLabels = inputLabelValues.map { value in 120 | let label = UILabel() 121 | label.font = UIFont.boldSystemFont(ofSize: 24) 122 | label.text = value 123 | return label 124 | } 125 | inputLabels.forEach(addSubview) 126 | } 127 | 128 | override func layoutSubviews() { 129 | super.layoutSubviews() 130 | updateLayout() 131 | } 132 | 133 | private func updateLayout() { 134 | updateLayerViewLayout() 135 | updateConnectionViewLayout() 136 | updateInputLabelsLayout() 137 | } 138 | 139 | private func updateInputLabelsLayout() { 140 | guard !layerViews.isEmpty else { 141 | return 142 | } 143 | for (index, label) in inputLabels.enumerated() { 144 | let nodeFrame = layerViews[0].nodeFrame(for: index) 145 | label.sizeToFit() 146 | label.center = CGPoint(x: horizontalPadding / 2, y: nodeFrame.midY) 147 | } 148 | } 149 | 150 | private func updateLayerViewLayout() { 151 | for (index, layerView) in layerViews.enumerated() { 152 | layerView.frame = layerFrame(at: index) 153 | } 154 | } 155 | 156 | private func updateConnectionViewLayout() { 157 | guard let viewModel = viewModel else { 158 | return 159 | } 160 | 161 | let connectionViewSize = CGSize(width: distanceBetweenLayer, height: bounds.height) 162 | var connectionViewIndex = 0 163 | 164 | for index in 0 ..< layerViews.count - 1 { 165 | let numberOfNeurons = viewModel.layer[index].neurons.count 166 | let nextNumberOfNeurons = viewModel.layer[index + 1].neurons.count 167 | 168 | let point = CGPoint(x: layerFrame(at: index).maxX, y: 0) 169 | for i in 0 ..< numberOfNeurons { 170 | let currentNodeFrame = layerViews[index].nodeFrame(for: i) 171 | for k in 0 ..< nextNumberOfNeurons { 172 | let connectionView = connectionViews[connectionViewIndex] 173 | let nextNodeFrame = layerViews[index + 1].nodeFrame(for: k) 174 | 175 | connectionView.sink = CGPoint(x: 1, y: nextNodeFrame.midY / connectionViewSize.height) 176 | connectionView.source = CGPoint(x: 0, y: currentNodeFrame.midY / connectionViewSize.height) 177 | 178 | connectionView.frame = CGRect(origin: point, size: connectionViewSize) 179 | 180 | connectionViewIndex += 1 181 | } 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/NetworkVisualisationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | public class NetworkVisualisationViewController: UIViewController, NeuralNetworkDebugOutputHandler { 8 | let horizontalStackView = UIStackView() 9 | let dashboardStackView = UIStackView() 10 | let networkView = NetworkView() 11 | private var minInput: Double = 0 12 | private var maxInput: Double = 0 13 | 14 | public let decisionPlotView = DecisionBoundaryPlotView() 15 | let lossPlotView = LinePlotView() 16 | var lastModel: NetworkModel? 17 | private let epochLabel: UILabel = { 18 | let label = UILabel() 19 | label.font = UIFont.monospacedDigitSystemFont(ofSize: 22, weight: .bold) 20 | label.textAlignment = .center 21 | return label 22 | }() 23 | 24 | private let lossLabel: UILabel = { 25 | let label = UILabel() 26 | label.font = UIFont.monospacedDigitSystemFont(ofSize: 22, weight: .bold) 27 | label.textAlignment = .center 28 | return label 29 | }() 30 | 31 | public override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | view.backgroundColor = .white 35 | 36 | horizontalStackView.frame = view.bounds 37 | horizontalStackView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 38 | horizontalStackView.distribution = .fillProportionally 39 | 40 | dashboardStackView.distribution = .fillEqually 41 | 42 | dashboardStackView.addArrangedSubview(dashboardItemStackView(label: epochLabel, content: SquaredContainerView(frame: .zero, subview: decisionPlotView))) 43 | dashboardStackView.addArrangedSubview(dashboardItemStackView(label: lossLabel, content: SquaredContainerView(frame: .zero, subview: lossPlotView))) 44 | 45 | horizontalStackView.addArrangedSubview(ProptionalStackViewChildContainerView(networkView, priority: 5)) 46 | horizontalStackView.addArrangedSubview(ProptionalStackViewChildContainerView(dashboardStackView, priority: 2)) 47 | view.addSubview(horizontalStackView) 48 | } 49 | 50 | public override func viewDidLayoutSubviews() { 51 | super.viewDidLayoutSubviews() 52 | updateStackViewLayout() 53 | } 54 | 55 | private func dashboardItemStackView(label: UIView, content: UIView) -> UIStackView { 56 | let stackView = UIStackView() 57 | stackView.distribution = .fillProportionally 58 | stackView.axis = .vertical 59 | stackView.addArrangedSubview(ProptionalStackViewChildContainerView(label, priority: 1)) 60 | stackView.addArrangedSubview(ProptionalStackViewChildContainerView(content, priority: 5)) 61 | return stackView 62 | } 63 | 64 | private func updateStackViewLayout() { 65 | if view.bounds.width > view.bounds.height { 66 | horizontalStackView.axis = .horizontal 67 | dashboardStackView.axis = .vertical 68 | } else { 69 | horizontalStackView.axis = .vertical 70 | dashboardStackView.axis = .horizontal 71 | } 72 | } 73 | 74 | public func didFinishedIteration(epochReport: EpochReport, networkRepresentation: NetworkModel) { 75 | lossPlotView.add(value: epochReport.loss) 76 | networkView.update(viewModel: networkRepresentation) 77 | updateDashboards(for: networkRepresentation) 78 | 79 | epochLabel.text = "Epoch: \(String(format: "%04d", epochReport.epochIndex))" 80 | lossLabel.text = "Loss: \(String(format: "%.3f", epochReport.loss))" 81 | lastModel = networkRepresentation 82 | } 83 | 84 | public func startedTraining(with dataSet: DataSetProtocol) { 85 | minInput = dataSet.input.values.min() ?? 0 86 | maxInput = dataSet.input.values.max() ?? 0 87 | 88 | decisionPlotView.plotPoints(dataSet: dataSet) 89 | networkView.inputLabelValues = dataSet.inputLabels ?? [] 90 | } 91 | 92 | public func didFinishTraining() { 93 | networkView.stopTrainingAnimation() 94 | } 95 | 96 | public func setDeciscionBoundaryColorOffset(value: Int) { 97 | decisionPlotView.decisionBoundaryView.colorIndexOffset = value 98 | } 99 | 100 | lazy var possibleInput: Mat = { 101 | let sideStide = Array(stride(from: minInput, to: maxInput, by: (maxInput * 2.0) / 100)) 102 | let inputValues = sideStide.map { x in 103 | sideStide.map { [Double(x), Double($0)] } 104 | }.joined() 105 | return Mat(values: Array(inputValues)).transposed() 106 | }() 107 | 108 | private func updateDashboards(for model: NetworkModel) { 109 | guard model.layer[0].neurons.count == 2 else { 110 | // We currently can only plot two dimensional data 111 | decisionPlotView.isHidden = true 112 | return 113 | } 114 | let result = NeuralNetwork(model: model) 115 | .predict(input: possibleInput) 116 | .transposed() 117 | decisionPlotView.plotDecisionBoundary(values: result) 118 | } 119 | 120 | public var debugOutputFrequency: Int { 121 | return 10 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/NeuronView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class NeuronView: UIView { 8 | let viewModel: NeuronModel? 9 | 10 | init(frame: CGRect, viewModel: NeuronModel, color: UIColor) { 11 | self.viewModel = viewModel 12 | super.init(frame: frame) 13 | layer.borderWidth = 5 14 | if color == UIColor.black { 15 | backgroundColor = color 16 | } else { 17 | backgroundColor = color 18 | layer.borderColor = UIColor.black.cgColor 19 | } 20 | layer.cornerRadius = bounds.width / 2 21 | layer.masksToBounds = true 22 | } 23 | 24 | required init?(coder _: NSCoder) { 25 | fatalError() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/PlotView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @IBDesignable 8 | class PlotView: UIView { 9 | struct Point { 10 | let point: CGPoint 11 | let color: UIColor 12 | } 13 | 14 | private let grid = GridLayer() 15 | 16 | @IBInspectable var useGrid: Bool = true { 17 | didSet { 18 | grid.isHidden = !useGrid 19 | } 20 | } 21 | 22 | @IBInspectable var pointRadius: CGFloat = 10 23 | public var maxValue: CGFloat = 10 { 24 | didSet { 25 | grid.instanceCount = Int(maxValue) / 3 26 | } 27 | } 28 | 29 | private var resolution: CGFloat = 2 30 | 31 | private var currentlyPlottedPoints: [Point] = [] 32 | private var currentlyPlottedPointLayer: [CALayer] = [] 33 | 34 | override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | setupGridIfNeeded() 37 | layer.masksToBounds = true 38 | } 39 | 40 | required init?(coder _: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | override func prepareForInterfaceBuilder() { 45 | super.prepareForInterfaceBuilder() 46 | layer.sublayers?.forEach { $0.removeFromSuperlayer() } 47 | setupGridIfNeeded() 48 | // let testData = XORDataSet() 49 | let testData = ArtificalDataSets.fourCorners() 50 | plot(dataSet: testData) 51 | } 52 | 53 | public func plot(mat: Mat) { 54 | let points: [Point] = (0 ..< mat.shape.rows).map { rowIndex in 55 | let row = mat.row(at: rowIndex) 56 | let point = CGPoint(x: row[0], y: row[1]) 57 | let color: UIColor = row[2] == 1 ? .red : .blue 58 | return Point(point: point, color: color) 59 | } 60 | plot(points: points) 61 | } 62 | 63 | public func plot(dataSet: DataSetProtocol) { 64 | let points: [Point] = zip(dataSet.input.values2D, dataSet.output.values2D).map { input, output in 65 | let point = CGPoint(x: input[0], y: input[1]) 66 | let color: UIColor 67 | if let index = output.index(of: 1) { 68 | color = ColorHelper.colorMap[index] ?? .black 69 | } else { 70 | color = .black 71 | } 72 | return Point(point: point, color: color) 73 | } 74 | plot(points: points) 75 | } 76 | 77 | private func setupGridIfNeeded() { 78 | guard grid.superlayer == nil else { 79 | return 80 | } 81 | 82 | layer.addSublayer(grid) 83 | } 84 | 85 | public func plot(points: [CGPoint]) { 86 | let plotPoints = points.map { Point(point: $0, color: .blue) } 87 | plot(points: plotPoints) 88 | } 89 | 90 | public func plot(points: [Point]) { 91 | currentlyPlottedPoints = points 92 | 93 | // remove old layer if needed 94 | currentlyPlottedPointLayer.forEach { $0.removeFromSuperlayer() } 95 | currentlyPlottedPointLayer.removeAll() 96 | 97 | // Add new layer 98 | currentlyPlottedPointLayer.reserveCapacity(currentlyPlottedPoints.count) 99 | for point in currentlyPlottedPoints { 100 | let pointLayer = CALayer() 101 | pointLayer.backgroundColor = point.color.cgColor 102 | pointLayer.borderColor = UIColor.white.cgColor 103 | pointLayer.borderWidth = 1 104 | layer.addSublayer(pointLayer) 105 | currentlyPlottedPointLayer.append(pointLayer) 106 | } 107 | 108 | // define resolution based on plotted points 109 | let maxX = points.lazy.map { abs($0.point.x) }.max() ?? 0 110 | let maxY = points.lazy.map { abs($0.point.y) }.max() ?? 0 111 | 112 | maxValue = max(maxX, maxY) 113 | 114 | resolution = floor(maxValue) == maxValue ? 1 : 2 115 | // layout 116 | updatedPresentedPoints() 117 | } 118 | 119 | override func layoutSubviews() { 120 | super.layoutSubviews() 121 | 122 | grid.frame = layer.bounds 123 | grid.setNeedsLayout() 124 | updatedPresentedPoints() 125 | } 126 | 127 | private func updatedPresentedPoints() { 128 | let scaleRatio = (bounds.width / (maxValue * resolution)) 129 | 130 | pointRadius = max(min(bounds.height / (CGFloat(currentlyPlottedPoints.count) / 8), bounds.height / 10), 5) 131 | 132 | let scaleTransform = CGAffineTransform(scaleX: scaleRatio, y: scaleRatio) 133 | let pointLayerSize = CGSize(width: pointRadius, height: pointRadius) 134 | for (pointLayer, point) in zip(currentlyPlottedPointLayer, currentlyPlottedPoints) { 135 | let transformedPoint = point.point.applying(scaleTransform) 136 | pointLayer.cornerRadius = pointRadius / 2 137 | pointLayer.frame = CGRect(origin: CGPoint(x: transformedPoint.x - pointLayerSize.midX + layer.bounds.midX * (resolution - 1), y: transformedPoint.y - pointLayerSize.midX + layer.bounds.midY * (resolution - 1)), size: pointLayerSize) 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/ProptionalStackViewChildContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class ProptionalStackViewChildContainerView: UIView { 8 | private var priority: CGFloat 9 | 10 | init(_ view: UIView, priority: CGFloat) { 11 | self.priority = priority 12 | super.init(frame: .zero) 13 | 14 | view.frame = bounds 15 | view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 16 | addSubview(view) 17 | } 18 | 19 | required init?(coder _: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override var intrinsicContentSize: CGSize { 24 | return CGSize(width: priority, height: priority) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NeuralNetwork.playgroundbook/Contents/Sources/UI/SquaredContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class SquaredContainerView: UIView { 8 | init(frame: CGRect, subview: UIView) { 9 | super.init(frame: frame) 10 | addSubview(subview) 11 | } 12 | 13 | required init?(coder _: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | override func layoutSubviews() { 18 | super.layoutSubviews() 19 | 20 | let side = min(bounds.width, bounds.height) 21 | for subview in subviews { 22 | subview.frame.size = CGSize(width: side, height: side) 23 | subview.center = center 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NeuralNetwork.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NeuralNetwork.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NeuralNetwork.xcodeproj/xcshareddata/xcbaselines/98F865532205C85B00167E55.xcbaseline/1A90B905-91A3-4577-9043-5B805DE93C46.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | classNames 6 | 7 | NeuralNetworkTests 8 | 9 | testPerformanceCircularDataSet100Epochs() 10 | 11 | com.apple.XCTPerformanceMetric_WallClockTime 12 | 13 | baselineAverage 14 | 0.16266 15 | baselineIntegrationDisplayName 16 | Local Baseline 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NeuralNetwork.xcodeproj/xcshareddata/xcbaselines/98F865532205C85B00167E55.xcbaseline/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | runDestinationsByUUID 6 | 7 | 1A90B905-91A3-4577-9043-5B805DE93C46 8 | 9 | localComputer 10 | 11 | busSpeedInMHz 12 | 400 13 | cpuCount 14 | 1 15 | cpuKind 16 | Intel Core i9 17 | cpuSpeedInMHz 18 | 2900 19 | logicalCPUCoresPerPackage 20 | 12 21 | modelCode 22 | MacBookPro15,1 23 | physicalCPUCoresPerPackage 24 | 6 25 | platformIdentifier 26 | com.apple.platform.macosx 27 | 28 | targetArchitecture 29 | x86_64 30 | targetDevice 31 | 32 | modelCode 33 | iPad8,5 34 | platformIdentifier 35 | com.apple.platform.iphonesimulator 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /NeuralNetwork.xcodeproj/xcshareddata/xcschemes/NeuralNetwork.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /NeuralNetwork/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @UIApplicationMain 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | var window: UIWindow? 10 | 11 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 12 | return true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /NeuralNetwork/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /NeuralNetwork/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NeuralNetwork/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NeuralNetwork/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /NeuralNetwork/DebugStoryboard.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /NeuralNetwork/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /NeuralNetwork/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | // This ViewController is a helper to test a Network outside of Swift Playgrounds 8 | class ViewController: UIViewController { 9 | lazy var networkVisualisation: NetworkVisualisationViewController = { 10 | let controller = NetworkVisualisationViewController() 11 | controller.view.frame = view.bounds 12 | controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 13 | addChild(controller) 14 | view.addSubview(controller.view) 15 | return controller 16 | }() 17 | 18 | override func viewDidAppear(_ animated: Bool) { 19 | super.viewDidAppear(animated) 20 | 21 | // performCustom() 22 | // performIris() 23 | // performFourCorners() 24 | // performXOR() 25 | } 26 | 27 | private func performXOR() { 28 | let network = NeuralNetwork(learningRate: 0.3, epochs: 5000) 29 | network.add(layer: Layer(nodesCount: 2)) 30 | network.add(layer: Layer(nodesCount: 2, activationFunction: .sigmoid)) 31 | network.add(layer: Layer(nodesCount: 2, activationFunction: .linear)) 32 | 33 | network.debugOutputHandler = networkVisualisation 34 | 35 | let data = DataSet(values: [[0, 0]: 0, 36 | [0, 1]: 1, 37 | [1, 0]: 1, 38 | [1, 1]: 0]) 39 | 40 | network.trainAndTest(trainData: data, testData: data) { report in 41 | print(report) 42 | } 43 | } 44 | 45 | private func performFourCorners() { 46 | let network = NeuralNetwork(learningRate: 0.03, epochs: 5000) 47 | network.add(layer: Layer(nodesCount: 2)) 48 | network.add(layer: Layer(nodesCount: 5, activationFunction: .tanh)) 49 | network.add(layer: Layer(nodesCount: 4)) 50 | 51 | network.debugOutputHandler = networkVisualisation 52 | 53 | let data = ArtificalDataSets.fourCorners().normalized() 54 | networkVisualisation.decisionPlotView.plotPoints(dataSet: data) 55 | let (train, test) = data.split(ratio: 0.1) 56 | network.trainAndTest(trainData: train, testData: test) { report in 57 | print(report) 58 | } 59 | } 60 | 61 | private func performCustom() { 62 | let network = NeuralNetwork(learningRate: 0.03, epochs: 5000) 63 | network.add(layer: Layer(nodesCount: 2)) 64 | network.add(layer: Layer(nodesCount: 4, activationFunction: .tanh)) 65 | network.add(layer: Layer(nodesCount: 2, activationFunction: .linear)) 66 | 67 | network.debugOutputHandler = networkVisualisation 68 | 69 | let data = ArtificalDataSets.circular() 70 | let (train, test) = data.split(ratio: 0.1) 71 | network.trainAndTest(trainData: train, testData: test) { report in 72 | print(report) 73 | } 74 | } 75 | 76 | private func performIris() { 77 | let irisDataSet = IrisDataSet() 78 | let network = NeuralNetwork(learningRate: 0.03, epochs: 2000) 79 | network.add(layer: Layer(nodesCount: 4)) 80 | network.add(layer: Layer(nodesCount: 5, initialWeightsRange: 0 ..< 0.01, activationFunction: .tanh)) 81 | network.add(layer: Layer(nodesCount: 4, initialWeightsRange: 0 ..< 0.01, activationFunction: .tanh)) 82 | network.add(layer: Layer(nodesCount: 3, activationFunction: .linear)) 83 | 84 | network.debugOutputHandler = networkVisualisation 85 | 86 | let (train, test) = irisDataSet.split(ratio: 0.1) 87 | network.trainAndTest(trainData: train, testData: test) { report in 88 | print(report) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /NeuralNetworkTests/DataSetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import NeuralNetwork 6 | import XCTest 7 | 8 | class DataSetTests: XCTestCase { 9 | func testSplit() { 10 | let dataSet = IrisDataSet() 11 | let (a, b) = dataSet.split(ratio: 0.1) 12 | 13 | XCTAssertEqual(a.input.shape.rows, 135) 14 | XCTAssertEqual(a.output.shape.rows, 135) 15 | 16 | XCTAssertEqual(b.input.shape.rows, 15) 17 | XCTAssertEqual(b.output.shape.rows, 15) 18 | } 19 | 20 | func testIrisDataSetTotalCount() { 21 | let dataSet = IrisDataSet() 22 | XCTAssertEqual(dataSet.input.shape.rows, 150) 23 | XCTAssertEqual(dataSet.output.shape.rows, 150) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NeuralNetworkTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NeuralNetworkTests/InterpolationHelperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | @testable import NeuralNetwork 6 | import XCTest 7 | 8 | class InterpolationHelperTests: XCTestCase { 9 | func testInterpolateWithIndices() { 10 | let input: [[Double]] = [[0, 2, 4], [0, 2, 4]] 11 | let expectedOutput: [Double] = [0, 1, 2, 3, 4] 12 | let realOutput = InterpolationHelper.interpolate(values: input[0], indices: input[1]) 13 | XCTAssertEqual(expectedOutput, realOutput) 14 | } 15 | 16 | func testInterpolateWithIndicesNotStartingFromZero() { 17 | let input: [[Double]] = [[5, 7, 9], [0, 2, 4]] 18 | let expectedOutput: [Double] = [5, 6, 7, 8, 9] 19 | let realOutput = InterpolationHelper.interpolate(values: input[0], indices: input[1]) 20 | XCTAssertEqual(expectedOutput, realOutput) 21 | } 22 | 23 | func testInterpolateWithIndicesWithScaleFactor() { 24 | let input: [Double] = [0, 2, 4, 6] 25 | let expectedOutput: [Double] = [0, 1, 2, 3, 4, 5, 6] 26 | let realOutput = InterpolationHelper.interpolate(values: input, scaleFactor: 2) 27 | XCTAssertEqual(expectedOutput, realOutput) 28 | } 29 | 30 | func testInterpolate2DWithScaleFactor() { 31 | let input: [[Double]] = Mat(shape: (3, 3), values: [0, 2, 4, 10, 12, 14, 20, 22, 24]).values2D 32 | let expectedOutput: [[Double]] = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]] 33 | let realOutput = InterpolationHelper.interpolate2D(values: input, scaleFactor: 2) 34 | XCTAssertEqual(expectedOutput, realOutput) 35 | } 36 | 37 | func testInterpolate2DWithIndices() { 38 | let input: [[Double]] = [[0, 2, 4], [10, 12, 14], [20, 22, 24]] 39 | let expectedOutput: [[Double]] = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]] 40 | let realOutput = InterpolationHelper.interpolate2D(values: input, indices: [[0, 2, 4], [0, 2, 4], [0, 2, 4]]) 41 | XCTAssertEqual(expectedOutput, realOutput) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NeuralNetworkTests/MatTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | @testable import NeuralNetwork 6 | import XCTest 7 | 8 | class MatTests: XCTestCase { 9 | func testNegate() { 10 | let input = Mat(shape: (2, 2), values: [1, 1, 1, 1]) 11 | let expectedOutput = Mat(shape: (2, 2), values: [-1, -1, -1, -1]) 12 | 13 | XCTAssertEqual(input.negate(), expectedOutput) 14 | } 15 | 16 | func testSumrows() { 17 | let input = Mat(shape: (2, 2), values: [1, 1, 1, 1]) 18 | let expectedOutput = Mat(shape: (2, 1), values: [2, 2]) 19 | 20 | XCTAssertEqual(input.sumRows(), expectedOutput) 21 | } 22 | 23 | func testTransposed() { 24 | let input = Mat(shape: (2, 3), values: [1, 2, 3, 4, 5, 6]) 25 | let expectedOutput = Mat(shape: (3, 2), values: [1, 4, 2, 5, 3, 6]) 26 | 27 | XCTAssertEqual(input.transposed(), expectedOutput) 28 | } 29 | 30 | func testSum() { 31 | let input = Mat(shape: (2, 3), values: [1, 2, 3, 4, 5, 6]) 32 | let expectedOutput: Double = 21 33 | 34 | XCTAssertEqual(input.sum(), expectedOutput) 35 | } 36 | 37 | func testValueAtIndex() { 38 | let mat = Mat(shape: (3, 3), values: [1, 2, 3, 4, 5, 6, 7, 8, 9]) 39 | XCTAssertEqual(mat.value(row: 0, column: 0), 1) 40 | XCTAssertEqual(mat.value(row: 0, column: 1), 2) 41 | XCTAssertEqual(mat.value(row: 0, column: 2), 3) 42 | XCTAssertEqual(mat.value(row: 1, column: 0), 4) 43 | XCTAssertEqual(mat.value(row: 1, column: 1), 5) 44 | XCTAssertEqual(mat.value(row: 1, column: 2), 6) 45 | XCTAssertEqual(mat.value(row: 2, column: 0), 7) 46 | XCTAssertEqual(mat.value(row: 2, column: 1), 8) 47 | XCTAssertEqual(mat.value(row: 2, column: 2), 9) 48 | } 49 | 50 | func testValues2D() { 51 | let input = Mat(shape: (3, 3), values: [0, 2, 4, 5, 7, 9, 10, 12, 14]) 52 | let expectedOutput: [[Double]] = [[0, 2, 4], [5, 7, 9], [10, 12, 14]] 53 | let realOutput = input.values2D 54 | XCTAssertEqual(expectedOutput, realOutput) 55 | } 56 | 57 | func testReshape() { 58 | let input = Mat(shape: (1, 9), values: [0, 2, 4, 5, 7, 9, 10, 12, 14]) 59 | let expectedOutput: [[Double]] = [[0, 2, 4], [5, 7, 9], [10, 12, 14]] 60 | let realOutput = input.reshape(to: Shape(rows: 3, cols: 3))?.values2D 61 | XCTAssertEqual(expectedOutput, realOutput) 62 | } 63 | 64 | func testMax() { 65 | let input = Mat(shape: (1, 9), values: [0, 2, 4, 5, 7, 9, 10, 12, 14]) 66 | let expectedOutput = Mat(shape: (1, 9), values: [4, 4, 4, 5, 7, 9, 10, 12, 14]) 67 | let realOutput = input.max(4) 68 | XCTAssertEqual(expectedOutput, realOutput) 69 | } 70 | 71 | func testMin() { 72 | let input = Mat(shape: (1, 9), values: [0, 2, 4, 5, 7, 9, 10, 12, 14]) 73 | let expectedOutput = Mat(shape: (1, 9), values: [0, 2, 4, 5, 7, 7, 7, 7, 7]) 74 | let realOutput = input.min(7) 75 | XCTAssertEqual(expectedOutput, realOutput) 76 | } 77 | 78 | func testClamp() { 79 | let input = Mat(shape: (1, 9), values: [-100, 2, 4, 5, 7, 9, 10, 12, 14]) 80 | let expectedOutput = Mat(shape: (1, 9), values: [4, 4, 4, 5, 7, 7, 7, 7, 7]) 81 | let realOutput = input.clamp(min: 4, max: 7) 82 | XCTAssertEqual(expectedOutput, realOutput) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /NeuralNetworkTests/NetworkModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | @testable import NeuralNetwork 6 | import XCTest 7 | 8 | class NetworkModelTests: XCTestCase { 9 | func testModelForLayerWithWeights() { 10 | let layer = Layer(nodesCount: 3, activationFunction: .sigmoid) 11 | layer.weights = Mat(values: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]) 12 | layer.biases = Mat(values: [[1], [2], [3]]) 13 | let output = layer.modelRepresentation() 14 | let expectedOuput = LayerModel(neurons: [NeuronModel(weigths: [1, 2, 3], bias: 1), NeuronModel(weigths: [4, 5, 6], bias: 2), NeuronModel(weigths: [7, 8, 9], bias: 3)], activationFunction: "sigmoid") 15 | XCTAssertEqual(output, expectedOuput) 16 | } 17 | 18 | func testInitLayerWithModel() { 19 | let model = LayerModel(neurons: [NeuronModel(weigths: [1, 2, 3], bias: 1), NeuronModel(weigths: [4, 5, 6], bias: 2), NeuronModel(weigths: [7, 8, 9], bias: 3)], activationFunction: "tanh") 20 | let layer = Layer(model: model) 21 | let ouput = layer.modelRepresentation() 22 | XCTAssertEqual(model, ouput) 23 | } 24 | 25 | func testInitNetworkFromFile() { 26 | let url = Bundle(for: NetworkModelTests.self).url(forResource: "testModel", withExtension: "json")! 27 | let model = try! NetworkModel.load(from: url) 28 | let network = NeuralNetwork(model: model) 29 | let output = network.model() 30 | 31 | XCTAssertEqual(model, output) 32 | } 33 | 34 | func testInitNetworkAndTestFromFile() { 35 | let url = Bundle(for: NetworkModelTests.self).url(forResource: "testModel", withExtension: "json")! 36 | let model = try! NetworkModel.load(from: url) 37 | let network = NeuralNetwork(model: model) 38 | let dataSet = ArtificalDataSets.circular(numberOfPointsPerClass: 250).split(ratio: 0.1).1 39 | let report = network.test(dataSet: dataSet) 40 | 41 | XCTAssertGreaterThan(report.classifications.accuracy, 0.90) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /NeuralNetworkTests/NeuralNetworkTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import NeuralNetwork 6 | import XCTest 7 | 8 | class NeuralNetworkTests: XCTestCase { 9 | func testTrainCircularDataSet() { 10 | let network = NeuralNetwork(learningRate: 0.03, epochs: 1700) 11 | network.add(layer: Layer(nodesCount: 2)) 12 | network.add(layer: Layer(nodesCount: 4, activationFunction: .relu)) 13 | network.add(layer: Layer(nodesCount: 1, activationFunction: .sigmoid)) 14 | 15 | let data = ArtificalDataSets.circular(numberOfPointsPerClass: 200) 16 | 17 | let input = data.input.transposed() 18 | let output = data.output.transposed() 19 | 20 | network.train(input: input, expectedOutput: output) 21 | 22 | let report = network.test(dataSet: data) 23 | XCTAssertGreaterThanOrEqual(report.classifications.accuracy, 0.95) 24 | } 25 | 26 | func testTrainIrisDataSet() { 27 | let exp = expectation(description: "Wait for training") 28 | let irisDataSet = IrisDataSet() 29 | let network = NeuralNetwork(learningRate: 0.03, epochs: 5000) 30 | network.add(layer: Layer(nodesCount: 4)) 31 | network.add(layer: Layer(nodesCount: 5, initialWeightsRange: 0 ..< 0.01, activationFunction: .tanh)) 32 | network.add(layer: Layer(nodesCount: 3, activationFunction: .linear)) 33 | 34 | let (train, test) = irisDataSet.split(ratio: 0.1) 35 | network.trainAndTest(trainData: train, testData: test) { report in 36 | XCTAssertGreaterThanOrEqual(report.classifications.accuracy, 0.90) 37 | exp.fulfill() 38 | } 39 | wait(for: [exp], timeout: 5) 40 | } 41 | 42 | func testTrainXORDataSet() { 43 | let exp = expectation(description: "Wait for training") 44 | let network = NeuralNetwork(learningRate: 0.3, epochs: 5000) 45 | network.add(layer: Layer(nodesCount: 2)) 46 | network.add(layer: Layer(nodesCount: 2, activationFunction: .sigmoid)) 47 | network.add(layer: Layer(nodesCount: 2, activationFunction: .linear)) 48 | 49 | let data = XORDataSet() 50 | 51 | network.trainAndTest(trainData: data, testData: data) { report in 52 | XCTAssertGreaterThanOrEqual(report.classifications.accuracy, 0.90) 53 | exp.fulfill() 54 | } 55 | wait(for: [exp], timeout: 5) 56 | } 57 | 58 | func testPerformanceCircularDataSet100Epochs() { 59 | let network = NeuralNetwork(learningRate: 0.03, epochs: 100) 60 | network.add(layer: Layer(nodesCount: 2)) 61 | network.add(layer: Layer(nodesCount: 4, activationFunction: .relu)) 62 | network.add(layer: Layer(nodesCount: 2, activationFunction: .sigmoid)) 63 | 64 | let data = ArtificalDataSets.circular(numberOfPointsPerClass: 200) 65 | 66 | let input = data.input.transposed() 67 | let output = data.output.transposed() 68 | 69 | measure { 70 | network.train(input: input, expectedOutput: output) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /NeuralNetworkTests/testModel.json: -------------------------------------------------------------------------------- 1 | {"layer":[{"neurons":[{},{}],"activationFunction":"linear"},{"neurons":[{"weigths":[0.11520045253737941,-0.15052973952802096],"bias":-1.2686300187201529},{"weigths":[0.21345043960222107,0.037769272155157373],"bias":1.1986158804760885},{"weigths":[0.063912369170051747,-0.089532319877929673],"bias":0.10132228335087481},{"weigths":[0.1004205741972849,0.22614234517910117],"bias":-1.3776776687694094}],"activationFunction":"tanh"},{"neurons":[{"weigths":[0.78736431898243986,-0.64841131815875297,-0.12749113493188088,0.64115101895486781],"bias":1.6884175753522546},{"weigths":[-0.78816099679029283,0.64812925002077204,0.12864120537912332,-0.64084423374082577],"bias":-0.68867458399113124}],"activationFunction":"linear"}]} -------------------------------------------------------------------------------- /PlaygroundSupport/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /PlaygroundSupport/PlaygroundPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | public protocol LiveViewable {} 9 | 10 | extension UIView: LiveViewable {} 11 | extension UIViewController: LiveViewable {} 12 | 13 | public enum AssesmentStatus { 14 | case fail(hints: [String], solution: String?) 15 | case pass(message: String) 16 | } 17 | 18 | public class PlaygroundPage { 19 | public static var current = PlaygroundPage() 20 | public var assessmentStatus: AssesmentStatus? 21 | public var liveView: LiveViewable! 22 | public var needsIndefiniteExecution = true 23 | public func finishExecution() {} 24 | } 25 | 26 | public protocol PlaygroundRemoteLiveViewProxyDelegate {} 27 | 28 | public class PlaygroundRemoteLiveViewProxy { 29 | public var delegate: Any? 30 | public func send(_: PlaygroundValue) {} 31 | } 32 | 33 | public enum PlaygroundValue { 34 | case array([PlaygroundValue]) 35 | case integer(Int) 36 | case string(String) 37 | case data(Data) 38 | case dictionary([String: PlaygroundValue]) 39 | case boolean(Bool) 40 | case floatingPoint(Float) 41 | } 42 | 43 | public class PlaygroundKeyValueStore { 44 | public static var current = PlaygroundKeyValueStore() 45 | public subscript(_: String) -> PlaygroundValue? { 46 | get { 47 | return nil 48 | } 49 | set {} 50 | } 51 | } 52 | 53 | public protocol PlaygroundLiveViewMessageHandler { 54 | func send(_ message: PlaygroundValue) 55 | func receive(_ message: PlaygroundValue) 56 | func liveViewMessageConnectionClosed() 57 | } 58 | 59 | extension PlaygroundLiveViewMessageHandler { 60 | public func send(_: PlaygroundValue) {} 61 | } 62 | 63 | public protocol PlaygroundLiveViewSafeAreaContainer { 64 | var liveViewSafeAreaGuide: UILayoutGuide { get } 65 | } 66 | 67 | public extension PlaygroundLiveViewSafeAreaContainer { 68 | var liveViewSafeAreaGuide: UILayoutGuide { 69 | return UILayoutGuide() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /PlaygroundSupport/PlaygroundSupport.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Leonard Thomas. All rights reserved. 3 | // 4 | 5 | #import 6 | 7 | //! Project version number for PlaygroundSupport. 8 | FOUNDATION_EXPORT double PlaygroundSupportVersionNumber; 9 | 10 | //! Project version string for PlaygroundSupport. 11 | FOUNDATION_EXPORT const unsigned char PlaygroundSupportVersionString[]; 12 | 13 | // In this header, you should import all the public headers of your framework using statements like #import 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeuralNetwork 2 | A Swift PlaygroundBook about Neural Networks 3 | 4 | ![Training of a Neural Network](Screenshots/PlaygroundDemo.gif) 5 | 6 | # Attribution: 7 | - The XOR image in the Hello World chapter is based on the work by Heron [https://de.wikipedia.org/wiki/Datei:Xor-gate-en.svg](https://de.wikipedia.org/wiki/Datei:Xor-gate-en.svg). Licensed under creative commons by attribution 3.0 license 8 | - The Iris Dataset in the Playground chapter provided by Dua, D. and Graff, C. (2019). UCI Machine Learning Repository [http://archive.ics.uci.edu/ml](http://archive.ics.uci.edu/ml). Irvine, CA: University of California, School of Information and Computer Science. 9 | -------------------------------------------------------------------------------- /Screenshots/PlaygroundDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lennet/NeuralNetwork/2af496af288c0522065dc0efd79639db95ab6ae2/Screenshots/PlaygroundDemo.gif --------------------------------------------------------------------------------