Neural Network Exemplar Parallelization with Go
This paper presents a case for exemplar parallelism of neural networks using
Go as parallelization framework. Further it is shown that also limited
multi-core hardware systems are feasible for these parallelization tasks, as
notebooks and single board computer systems. The main question was how much
speedup can be generated when using concurrent Go goroutines specifically. A
simple concurrent feedforward network for MNIST digit recognition with the
programming language Go was created to find the answer. The first findings when
using a notebook (Lenovo Yoga 2) showed a speedup of 252% when utilizing 4
goroutines. Testing a single board computer (Banana Pi M3) delivered more
convincing results: 320% with 4 goroutines, and 432% with 8 goroutines.
Georg Wiesinger, Erich Schikuta
# Neural Network Exemplar Parallelization with Go

###### Abstract

This paper presents a case for exemplar parallelism of neural networks using Go as parallelization framework. Further it is shown that also limited multi-core hardware systems are feasible for these parallelization tasks, as notebooks and single board computer systems. The main question was how much speedup can be generated when using concurrent Go goroutines specifically. A simple concurrent feedforward network for MNIST [1] digit recognition with the programming language Go [2, 3, 4, 5] was created to find the answer. The first findings when using a notebook (Lenovo Yoga 2) showed a speedup of 252% when utilizing 4 goroutines. Testing a single board computer (Banana Pi M3) delivered more convincing results: 320% with 4 goroutines, and 432% with 8 goroutines.

 Backpropagation, Exemplar Parallelization, Go Programming Language, MNIST

## I Introduction

Neural networks and artificial intelligence are becoming more and more important not only in research, but also in daily used technology. Due to higher amounts of data these artificial agents have to analyze there is the need for a larger throughput and highly efficient neural networks. The programming language Go looks promising for developing such a highly efficient agent, as the language itself has been made not only for highly efficient parallelization but also with fast development in mind. The main question is if Go is suitable for a highly efficient parallelization of neural networks. The main objective is the creation of an efficient parallelized neural network. There is a possibility that Go could lead to a higher parallelization efficiency/speedup than other programming languages. As Go is a young programming language, the literature about this specific topic is very sparse to almost nonexistent. There are tertiary sources like websites comparing the general throughput of Go in comparison to web languages like NodeJS, PHP and Java1. Other literature is related to parallelization speedup. There are also some neural networks realized in Go. No sources for a better comparison of parallelization techniques has been found. The scope of this work is to find out the speedup when using multiple goroutines with a neural network while maintaining a high and sustainable classification accuracy. A working MNIST digit recognition system has been created for testing the speedup with up to sixteen cores. The network and parameters have been optimized, but due to only negligible improvements with more than 100 hidden layer nodes this amount has not been exceeded. The execution time for one epoch has been sped up from 856.57-1271.73 (median 1005.80) seconds with 1 sporoutine to only 171.50-221.38 (median 201.82) seconds with 16 goroutines with a Banana Pi M3. The Lenovo Yoga 2 showed a less significant speedup with 137.29-146.01 (median 142.33) for 1 goroutine to 55.10-64.62 (median 56.49) with 4 goroutines. Additional goroutines exceeding the maximum thread limit brought further speedup due to pipelining with the Banana Pi, but a negligible speed loss for the Lenovo Yoga.

Footnote 1: [https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go](https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go)

## II Related Work and Baseline Research

Artificial neural networks and their parallel simulation gained high attention in the scientific community. Parallelization is a classic approach for speeding up execution times and exploiting the full potential of modern processors. Still, not every algorithm can profit from parallelization, as the concurrent execution might add a non-negligible overhead. This can also be the case for data parallel neural networks, where accuracy problems usually occur, as the results have to be merged.

In the literature a huge number of papers on parallelizing neural networks can be found. An excellent source of references is the survey by Tal Ben-Nun and Torsten Hoefler [7]. However, only few research was done on using Golang in this endeavour.

In the following only specific references are listed, which influenced the presented approach directly. The authors of [8] presented a parallel backpropagation algorithm dealing with the accuracy problem only by using a MapReduce and Cascading model. In the course of our work on parallel and distributed systems [9, 10, 11] we developed several approaches for the parallelization of neural networks. In [12], two novel parallel training approaches were presented for face recognizing backpropagation neural networks. The authors use the OpenMP environment for classic CPU multithreading and CUDA for parallelization on GPU architectures. Aside from that, they differentiated between topological data parallelism and structural data parallelism [13], where the latter is focus of the presented approach here. [14] gave a comparison of different parallelization approaches on a cluster computer. The
results differed depending on the network size, data set sizes and number of processors. Using Go as parallelization tool was already analyzed for the Single-Program-Multiple-Data approach [15, 16] and showed promising results. However, in this paper we focus on exemplar parallelization. Besides parallelizing the backpropagation algorithm for training speed-up, alternative training algorithms like the Resilient Backpropagation described in [17] might lead to faster convergence. One major difference to standard backpropagation is that every weight and bias has a different and variable learning rate. A detailed comparison of both network training algorithms was given in [18] in the case of spam classification.

## III Data and Methods

The following data and methods have been used to gain insight on the speedup possibilities.

### _Choosing the data and parallelization method_

Different approaches of which data could be used have been evaluated. Weather data, crime rates, etc. all seemed to be a good fit, but with the possibility of very inconclusive outputs. Finally the "Hello World!" of neural networks has been chosen: The MNIST dataset [1]. With this ready to use dataset the development process sped up as the convolutional part was already done.

Exemplar parallelism [19] has been chosen as parallelization technique. Within the workers the learning method is stochastic gradient descent [20], but due to combining the data in the main connectome, it behaves like a mini-batch update [21].

### _Basic structure_

First a functional code for a basic neural network has been prepared. With this code it is also possible to define simple multi-layer feedforward networks. From that stable basis more functionality has been added (i.E. different activation functions) to ease up the future development. Then the parallelization of the neural network has been implemented. There were additional challenges with avoiding possible race conditions. Go was very helpful with its built in race detector which can be utilized with the "-race" flag. It was easy to spot any race conditions and therefore the development sped up in the area deemed to take the most time. Afterwards the possibility to input and compute large datasets has been implemented. A batch file functionality for ease of testing as well as data output functionality have been added too. Afterwards the code and neural network have been optimized for a balance of speed, memory usage and training quality. Shuffling of the training data has been implemented to prevent any unwanted behavior that comes from repeated data. The elastic net regularization [22] has been chosen to get better results and more stability for the neural network.

### _The math_

Different activation functions have been tested to get a high accuracy, although this is not the purpose of this work. At the end it has been concluded that the best way is to start without any data normalization for the data to be put into the input layer. But before any activation function runs over the layer, the data is normalized by dividing each value by the size of the layer (including the bias) to minimize the risk of exploding gradients [23].

The following activation functions have been used:

* Input layer: Identity
* Hidden layer: ELU [24]
* Output layer: SoftMax

Variables are aas followed:

* \(\eta\) = learning rate
* t = target
* x = neuron value before activation
* \(\varphi\) = activation function
* \(\delta\) = error
* w = weight

#### Iii-B1 Activation functions

**Identity**

The simplest activation function is the identity function. It simply states that the value stays the same, as in equation (1).

\[\varphi(x)=x \tag{1}\]

So the derivation simply is 1 as shown in equation (2).

\[\varphi^{\prime}(x)=1 \tag{2}\]

**Exponential Linear Unit**

In comparison to ReLU and leaky ReLU, ELU "[...] speeds up learning in deep neural networks and leads to higher classification accuracies." [24] and therefore has been chosen over the other options. Equation (3) shows the math, where alpha is a positive value that can be freely chosen.

\[\varphi(x)=\begin{cases}x&\text{if x }\geq 0\\ \alpha*(e^{\text{x}}-1)&\text{if x }<0\end{cases} \tag{3}\]

The derivation for training is shown in equation (4).

\[\varphi^{\prime}(x)=\begin{cases}1&\text{if x }\geq 0\\ \varphi(x)+\alpha&\text{if x }<0\end{cases} \tag{4}\]

**SoftMax**

The SoftMax function gives us a classification of the likelihood that the current input represents a certain number. The math is straightforward and shown in equation (5).

\[\varphi_{\text{i}}(\overrightarrow{x})=\frac{e^{\text{x}_{\text{i}}}}{ \sum\limits_{j=1}^{J}e^{\text{x}_{\text{j}}}} \tag{5}\]
But with this equation, exploding or vanishing gradients [25] can become a problem due to the high likelihood of numbers exceeding.For the SoftMax activation there is a little "trick". It is possible to add a scalar, as shown in (6), without changing the value of the softmax function [26].

\[\varphi_{\text{i}}(\overrightarrow{x^{\prime}})=\frac{e^{\text{x}\text{ + S}}}{\sum\limits_{j=1}^{J}e^{\text{x}_{\text{j}}\text{ + S}}} \tag{6}\]

So, instead of using softmax(x), softmax(z) - with a scalar value of the negative x maximum - has been used, as in equation (7).

\[z_{\text{i}}=(x_{\text{i}}-max_{\text{i}}(x_{\text{i}})) \tag{7}\]

If we use the maximum, we push the calculation into the negative number spectrum. So, instead of having values ranging over ]-\(\infty\), \(\infty\)[, they've been shifted to ]0, 1], as in (8).

\[\varphi_{\text{i}}(\overrightarrow{x})=\frac{e^{\text{z}_{\text{i}}}}{\sum \limits_{j=1}^{J}e^{\text{z}_{\text{i}}}} \tag{8}\]

Equation (9) derivation for training is a little bit more complicated.

\[\varphi^{\prime}_{\text{i}}(\overrightarrow{x})=\frac{\partial\varphi_{\text {i}}(\overrightarrow{x})}{\partial x_{\text{j}}}=\begin{cases}\varphi_{\text {i}}(\overrightarrow{x})*(1-\varphi_{\text{j}}(\overrightarrow{x}))&\text{i = j}\\ \varphi_{\text{i}}(\overrightarrow{x})*(0-\varphi_{\text{j}}(\overrightarrow{x }))&\text{i \neq j}\end{cases} \tag{9}\]

Mathematicans use (10) to shorten the equation to (11).

\[\delta_{\text{ij}}=\begin{cases}1&\text{i = j}\\ 0&\text{i \neq j}\end{cases} \tag{10}\]

\[\varphi^{\prime}_{\text{i}}(\overrightarrow{x})=\frac{\partial\varphi_{\text {i}}(\overrightarrow{x})}{\partial x_{\text{j}}}=\varphi_{\text{i}}( \overrightarrow{x})*(\delta_{\text{ij}}-\varphi_{\text{j}}(\overrightarrow{x })) \tag{11}\]

But in the end it comes down to the same function as the logistic derivation, shown in equation (12). As the result of the derivation is a diagonal matrix [27] there is no need to calculate the whole matrix.

\[\varphi^{\prime}_{\text{i}}(\overrightarrow{x})=(1-\varphi_{\text{i}}( \overrightarrow{x}))*\varphi_{\text{i}}(\overrightarrow{x})=(1-x)*x \tag{12}\]

#### Iii-B2 Elastic Net Regularization

The elastic net regularization [22] has been used for weight updates.

It's a combination of the lasso regression (13) and the ridge regression (14).

\[L^{1}=\lambda|w| \tag{13}\]

\[L^{2}=\lambda w^{2} \tag{14}\]

The elastic net (15) is simple.

\[ElasticNet=\lambda|w|+\lambda w^{2} \tag{15}\]

Computational optimization (16)

\[ElasticNet=\lambda(|w|+w^{2}) \tag{16}\]

For the derivation (17) the signum function (18) is needed.

\[|w|=w*sgn(w) \tag{17}\]

\[sgn(w)=\begin{cases}1&\text{w > 0}\\ 0&\text{w = 0}\\ -1&\text{w < 0}\end{cases} \tag{18}\]

Which leads to (19).

\[ElasticNet^{\prime}=\lambda(sgn(w)+2w) \tag{19}\]

#### Iii-B3 Loss function

Quadratic loss has been chosen as loss function. Although the classification of handwritten digits - as the name says - is a classification problem and therefore cross entropy loss should show better results. Different loss functions will be implemented in the future of this work.

\[L=-\frac{1}{2}\sum\limits_{i}^{nclass}\Big{(}t_{\text{i}}-\varphi(x_{\text{i }})\Big{)}^{2}+\lambda\frac{1}{2}\sum\limits_{i}^{k}\Big{(}|w_{\text{i}}|+{w_{ \text{i}}}^{2}\Big{)} \tag{20}\]

The derivative is the logistic equation, therefore the loss function is equation (21).

\[L^{\prime}=-\sum\limits_{i}^{nclass}\Big{(}t_{\text{i}}-\varphi(x_{\text{i}}) \Big{)}+\lambda\sum\limits_{i}^{k}\Big{(}\frac{1}{2}sgn(w_{\text{i}})+w_{ \text{i}})\Big{)} \tag{21}\]
#### Iii-C4 Forward and backward propagation

All the previous information is needed to understand the forward and backward propagation methods.

**Forward**

After setting the inputs and targets, the first layer of the neural network gets activated. Then for each layer, the next neuron gets excited with the product of the activated value and the weight of the connection between the neurons (22).

\[x_{\mathrm{j}}=\varphi(x_{\mathrm{i}})*w_{\mathrm{ij}} \tag{22}\]

**Backward**

When learning, equation (23) is used to calculate the error.

\[\delta=t-\varphi(x) \tag{23}\]

The formula for the weight update, with learning rate and the regularization in (24).

\[\Delta w=\eta*(\delta*\varphi(x)+\lambda(sgn(w)+w)) \tag{24}\]

The final equation in (25).

\[w_{\mathrm{ij}}^{+}=w_{\mathrm{ij}}-\Delta w, \tag{25}\]

### _Choosing the parameters_

ELU alpha = 0.5 (currently hardcoded)

The alpha value for ELU has been hardcoded as there was no incentive to do otherwise in the current software iteration.

**workerBatch:** 100

The worker batch has been chosen to merge the single neural networks as often as possible, but without losing too much performance due to context switching.

**Minimum/Maximum weight (starting weights):** [-0.1; 0.1]

As the weights are usually getting smaller, when learning occurs, the starting values have to be chosen to be 0.1 instead of 1. This led to the best outcome.

**LearningRate:** 0.8

The learning rate has been set to 0.8, as this led to the best outcome.

**Lambda:** 0.0000001

The multiplicator for the elastic net has been set to this value, as it provided the highest accuracy for the training and test set. As it is hard to tell if either L1 or L2 regularization is the best, there is only one lambda for setting both methods, to achieve a balance between the two methods.

## IV Results/Evaluation

For testing the neural network, two available systems have been chosen: The Lenovo Yoga 2 laptop, as it is a dual core consumer product which utilizes threading and a turbo mode for higher workloads, with 64 Bit Linux. The Banana Pi M3, as it is a well known octa core home server, with no threading, and without data distortion due to turbo mode kicking in, and 32 Bit Linux. Both systems have a standard CPU frequency of 1.80 GHz, although the minimum and maximum values differ.

There are stark differences in computation speed as well as speedup between the Intel and the ARM architecture. As RISC and CISC lost their meaning to describe newer architectures, it is not possible to draw the conclusion here, although the main effect could come from the smaller - and therefore faster access rates - of the Intel L1 and L2 caches, or the lack of an L3 cache in the ARM architecture. Further research would be needed.

### _Benchmark_

When using pprof for checking the total cpu usage of the code parts with BenchBatch (it utilizes 4 cores, uses a worker batch of 100 lines, and processes 1 training with 60.000 MNIST lines as well as 1 test with 10.000 MNIST lines), it can be seen in Figure 1 that thinking and training takes up about 96% of the total time. Thinking takes about 40% of the time, training takes about 56%. Thinking is the forward propagation, training is the backward propagation. Due to that high amount of cpu usage heavy optimizations were made in these code parts, as these had the greatest effects.

The utility functions only play a marginal role. Even though they wouldn't need any optimization, they've been optimized for general code quality reasons. In example the garbage collector (mallocgc) is hardly used in the utility functions, and almost never in the main code part. As strings are only converted when needed, these parts of the code - even though they're not impacting the measurements - have been highly optimized. Maybe there's still room for further optimization, but for the general purpose this goal has been exceeded.

Fig. 1: pprof worker CPU profile
### _Test systems_

The final tests were made with a "Lenovo Yoga 2 Pro Multimode Ultrabool" as well as a "Banana Pi M3".

Specifications of the Lenovo: Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz, Dual Core (4 threads)

Min CPU: 800 MHz, Max CPU: 3.0 GHz

32 KiB L1 cache, 256 KiB L2 cache, 4 MiB L3 cache

2x4096 DIMM @ Clockspeed 1600 MHz

64 Bit Linux Ubuntu 18.04.1 LTS

Specifications of the Banana Pi M3:

A83T ARM Cortex-A7 octa-core CPU @ 1.80 GHz, Octa Core (8 threads), 4800 BogoMIPS

ARMv7 Processor rev 5 (v7l)

Min CPU: 480 MHz, Max CPU: 1.8 GHz

512 KiB L1 cache, 1 MiB L2 cache

2GB LPDDR3

32 bit (armv7l) Linux Ubuntu 16.04.5 LTS, MATE Desktop Environment 1.12.1

_1) Lenovo Yoga 2:_ The 252% speedup generated with 4 goroutines on the Lenovo Yoga 2 when utilizing more than 1 processor is clearly visible in Figure 2. It is also visible that using more goroutines than processors slows the execution time down only by an almost negligible amount.

Parallelization speedup comes at a price. Although very small, there is a slight decrease in recognition rates when utilizing more goroutines as shown in Figure 3.

#### Iv-C2 Banana Pi M3

When looking at the results of the Banana Pi M3 in Figure 4, it is apparent that utilizing multiple cores leads to an even greater benefit than with the Lenovo. It was possible to generate a 320% speedup with 4 goroutines, and - due to pipelining - it was even possible to generate over 498% speedup when using more goroutines than there were threads available.

The training and test set accuracies look promising too. A 99.26% training set accuracy and 97.14% test set accuracy with only one core has been accomplished. The accuracy does not get lower when utilizing more cores, even though quality differences in the recognition rate can occur. In Figure 5 it is clearly visible that recognition rate drops can occur at any time.

### _Accuracy growth depending on goroutines_

When only one goroutine is usedFigure 6 with the Banana Pi, the neural network starts with a very high recognition accuracy after the first epoch and has a very good learning rate.

With 16 goroutinesFigure 7 the recognition accuracy starts lower and the network takes longer to learn.

#### Iv-C

Fig. 3: Lenovo Yoga 2 accuracy

Fig. 2: Lenovo Yoga 2 speedup
1 Goroutine, accuracy > 90%/95%/99%:

93.24% accuracy after 1 epoch, 1040 seconds

95.22% accuracy after 2 epochs, 2006 seconds

99.03% accuracy after 15 epochs, 15608 seconds

16 goroutines, accuracy > 90%/95%/99%:

90.18% accuracy after 2 epochs, 392 seconds

95.12% accuracy after 8 epochs, 1583 seconds

99.02% accuracy after 49 epochs, 9628 seconds

To reach a higher accuracy with more goroutines more epochs and training samples are needed. But the speedup allows to train it in shorter time - or, to look at it from another perspective - to compute more inputs in a much shorter timespan.

## V Lessons Learned

The final part of this work is to look at what has been learned about the "do's and don'ts of implementing neural networks", Go as a language, the drawn conclusion, and possible future work.

### _"To do, or not to do?" of implementing neural networks_

There are certain roads to victory and many paths to development hell. The latter leads to a steeper learning curve and should therefore be preferred when trying to understand the implications of certain design decisions - but under normal circumstances the beaten path is the quicker route. These recommendations for other coders shall make implementing neural networks a little bit easier and shine a light on which thought processes are good and which are impractical to do.

#### V-1 Arrays instead of structs

Do not use one struct instance per neuron, connection, etc. as it has a large overhead. The compiler is able to optimize the usage of arrays. The first iteration of the neural network took hours for just one epoch on the Lenovo, while the array version takes less than a minute.

#### V-2 Only save when necessary

Only save and load data when needed. In the context of the neural network: Save either batchwise or after every epoch. Try to hold the data in the memory as long as possible.

#### V-3 Machine readable is better than human readable

The conversion of data to XML, JSON, or any other human readable format takes a higher amount of computation time, memory, and disk space, than machine readable formats. If a human readable format is needed, it should only be created, if a human wants to read it and there is a need for them to do so. Sifting through millions of weights and updates is not something a human should do. But, depending on the use case, the human readable format can be created, when

* The process is finished and the results shall be shown.
* An error occurs and the data is necessary to fix it.

Fig. 4: Banana Pi M3 speedup

Fig. 5: Banana Pi M3 accuracy
If human entities want to access data while the process is running (in real time, or stepwise for debugging) there are different approaches:

* Create only one file every few epochs which can be accessed by multiple human entities. Do NOT create it for every entity that accesses the file.
* Duplicate the machine readable results and parse them on a different system. For snapshots a simple ID can be given to every file.

#### Iii-B4 Parallelization and context switches

It takes time to store states of threads. Data has to be shoved around between CPU caches. If applicable give a worker as much data as possible, with one drawback in mind: More data merges mean higher fluctuations and slower computation - less data merges can lead to a more stable convergence and faster computation [28], as well as a higher level of generalization [23]. All while being able to perform online learning due to the singular workers performing stochastic gradient descent.

#### Iii-B5 Struct packing

In Go it is possible to pack structs. That means organizing the data types in a way so that they waste the least amount of memory. The principle for this work was "Memory is cheap, but even though students lack the money to buy some, there is no need to overdo it". But one late evening (at 7 o'clock in the morning) these principles had been thrown over board. So structs have been packed.

#### Iii-B6 Slices

Do not loop through lines of data to append it to a batch line by line. Use the slice functionality of Go - which passes them by reference - if applicable. The following data has been taken from the old model with using the updated weights for the error calculation Figure 8.

In example the code in Figure 9 takes 129.68 seconds for 1 training and 1 testing with the MNIST dataset, 4 cores, and a worker batch setting of 100, as shown in Figure 10.

In comparison when utilizing slices instead of making a slice and appending the data lines within core_run() to send the worker batches to the workers as shown in Figure 11, saved about 17 seconds on the Lenovo, as is visible in Figure 12.

Fig. 8: Code changes from wrong to correct code

Fig. 6: Accuracy growth with 1 Goroutine

Fig. 7: Accuracy growth with 16 goroutines
for each cluster's \(\alpha\)-constraint, activationless parameter \(\alpha\) and activationless parameter \(\beta\).

For each cluster's \(\alpha\)-constraint, activationless parameter \(\beta\) is defined by \(\alpha\).

For each cluster's \(\alpha\)-constraint, activationless parameter \(\beta\) is defined by \(\alpha\).
functions, but used the old equations from the first two versions of the paper. There is a small effect on the derivative functions when x = 0. In example with ELU: The old and new equation are only equal when \(\alpha\) = 1. Otherwise, when x = 0, f'(x) should be \(\alpha\), not 1.

One trusted source - due to one University module handed out an excerpt without citing the source which was the basis for an assignment to calculate a forward, backward, and forward propagation by hand, and creating a simple neural network, which both got graded - used the updated weights as basis for the backpropagation.

There was a larger accuracy dropFigure 15 when using multiple goroutines.

Although it was possible to see when to tweak the parameters to gain a higher accuracy with a single coreFigure 16, there has been found no practical use of these values for the correct implementation - the hyperparameters vary widely, so they have to be tuned differently.

Also mathematical sources are often not the best source for calculations in information systems. Math has been spared the problem of both errors due to overflow and underflow (except when using calculators or information systems). Also there is no need to optimize for memory or computation speed.

The problem with trusting the wrong data has been solved with further research from different sources and consulting a mathematician to check if the partial derivatives and all

Fig. 16: Accuracy with wrong parameters

Fig. 14: Benchmark after code changes

Fig. 15: Old Plot showing the accuracy decrease
formulas have been implemented correctly. The error has then been found very quick when checking against the standard reference [26].

### _Comment on Go_

Go is a wonderful language to write code. Implementation and testing of the neural network seemed to be easier than with other programming languages. But go also has some drawbacks (as does any language).

The main annoyance were the "unused imports" bugs. Sometimes only certain outputs are needed for testing which will get dropped by the developer immediately afterwards. It's good that the Go compiler sees these oversights as errors, even though they are a huge annoyance. A probably better way would be if unused imports won't be tested in a debug environment, only in production. But this would have additional drawbacks when Go is used in environments where code quality is not highly valued.

Another annoyance is the "sudden 'bad file descriptor' of doom". Sometimes it's just a "data reader error: file already closed". It was not possible to pin down what exactly causes the error, only that it affects the file as a whole. Not even deleting and creating a new file with the same name helps to overcome that error. Further testing is needed.

An additional observation that can ruin ones day is, that the Go compiler for some reason accesses trashed files, at least under Linux. There is no problem when files are overwritten by new files. But if a file gets deleted, and a new one inserted instead, Go sometimes seems to try to compile the deleted files, which can lead to hard to trace errors. If there's, in example, an error where the Go compiler expects an integer value, the code provides an integer value, but recently a file with the same function expecting a double value had been trashed, simply empty the trash bin.

Another "hard to debug except when you know it" part is: "panic: runtime error: invalid memory address or nil pointer dereference". This error occurs when the object has not been created with new(...). If it's further up in the code, i.e. some struct attribute, this error is not easy to find. When starting with Go that panic tells almost nothing about its nature.

Circular dependencies are not allowed. They can happen while refactoring code or when making some design mistake. It's good that Go does not permit them as they are a sign of bad software design.

The short variable declaration := is very handy. Go recognizes the type and assigns the value to the left hand variable. The best part: It won't break type safety, which prevents weird behavior.

With the test coverage profiler it is easy to see the current code coverage. There is also the possibility to create test heat maps and to show the test coverage in the browser with highlighting good, poor, or not covered code parts2.

Footnote 2: Go test coverage and html heatmap: [https://blog.golang.org/cover](https://blog.golang.org/cover)

There are memory and cpu profilers, and even a profiling tool3. It is easy to list the top cpu or memory consumers or show a profiling web. Therefore memory issues can be found easily, as well as slow code parts.

Footnote 3: Go profiling: [https://blog.golang.org/profiling-go-programs](https://blog.golang.org/profiling-go-programs)

Go uses function inlining which is a great method for speeding up code.

Goroutines are very lightweight4. As they're very efficient and only start to run if they get data from a channel, there's the probability of an application for parallelized neurons instead of only parallelized networks.

Footnote 4: Currently 2, 4, or 8 LB per Goroutine, depending on the version, i.e. [https://github.com/golang/go/issues/7514](https://github.com/golang/go/issues/7514)

It's easy to find and fix race conditions with Go as it comes with its own race detector.

## VI Conclusion and Future work

It has been learned how to use the programming language Go and about its parallel speedup possibilities. The main accomplishment of this work is to have managed to create a stable and fast neural network. The hardest part was to understand the mathematical concepts and ramifications behind neural networks and how to implement them software wise.

The main focus of this work was to see how the parallel speedup of a neural network behaves with the language Go. Due to time and resource restrictions only little derivations from the main focus were made. There are still ways left to make this neural network even more efficient, with higher accuracy, and so on. The current version could have some possible memory leaks. They will be fixed in a future version. As there will be further changes due to development and additional insights, the code will probably be refined and refactored in the future.

Some parts of the code are still untested - mainly file reading and writing. As they work as intended no additional effort has been made to get 100% test coverage in these areas. Here is room for improvement.

Optimization of the neural network would be the largest part of the future work. Currently it is only a simple network with Bias. It would be possible to implement momentum [29] and other artifacts to achieve higher accuracies. NADAM and other stochastic gradient descent optimization algorithms [28] could be implemented too.

Smaller changes will also include several options, in example if the user wants bias nodes, which error severity to log, and to choose different lambdas for the L1 and L2 Regularization in the elastic net. Adaptive learning rates [29] would be of interest too. Different loss functions, especially Cross Entropy Loss [30] will be implemented in the future.
There is an interest to look into Self-Normalizing Neural Networks [31].
