├── .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 | 
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 | 
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
--------------------------------------------------------------------------------