Our customer AimoPark’s earlier infrastructure was mostly hosted in AWS Elastic Beanstalk. At that point, the version control platform was GitHub & CircleCI was used to offer some CI/CD steps, though some of them were still manual.

AimoPark had decided that they needed to renew their infrastructure quite and lot. We opted to get rid of the Elastic Beanstalk entirely and going for containerized microservices with Kubernetes on EKS for the container orchestration. We would be developing both new microservices & migrating the older applications to Kubernetes as well. They also had a need for a modern deployment pipeline for all their new Kubernetes apps.

Starting phases

We initially left the old AWS accounts with the Elastic Beanstalk envs as they were and made new ones for the new infrastructure. The older AWS accounts would eventually be cleaned & removed after we’d be done with migrations.

We built the new infra mostly using Terraform (and a complementary tool called Terragrunt), with the bot users & roles used to run Terraform created with CloudFormation StackSets to escape some chicken & egg problems. From the ground up we wanted to run Terraform only from CI/CD as well just like any other code. Ended up initializing a couple of infra-specific repos on GitHub & building first iterations of the pipelines with Circle CI.

At the same time we’d been looking for artifact store solutions. We ended up going with GitLab.com because it could provide us with version control platform, artifact store & CI/CD platform (among others) all in the same SaaS product. Migrated all the repos from GitHub to GitLab & started work on the new GitLab CI pipelines, loosely based on the CircleCI pipelines that we’d had before. They’ve gone through a bunch of iterations but the GitLab CI pipelines are what we’re still using.

Defining the pipelines

We’ve broken our pipelines down to some common components & dedicated a repository for them. For instance we’ve got a pipeline for building Docker images and pushing them to AWS ECR and another for Kubernetes deployments. We’ve also got pipelines for deploying infra-as-code with Terraform & couple of other pipelines as well. These separate pipelines can be mixed & matched with each other to a point and modified with app-specific custom jobs as well.

In the end this allows us to have very brief GitLab CI definitions in application repos. For example for Java services we can just have something like this example belowo and it’ll do all the necessary basic things. The referenced java_service_ci.yml uses both the ECR- & kubectl-pipelines, in addition to having some common Java-specific things like Maven stuff & SonarCloud code analysis.

The workflow

So here’s how the most common workflow for us works. Developers always work in feature-branches that are eventually merged to the main branch. When developer has pushed some commits & opens a Merge Request in GitLab the pipeline automatically starts working.

First it builds things like the final Kubernetes definition .yamls (with all the Deployments, Services etc. resources) using a tool called kustomize. With it we can have things like base configurations that are the same for all envs and overlay configurations that can be used to further add or modify things depending on the env. After the .yamls are built we run some validations against them with kubectl & some static code analysis with kube-score to improve security & resilience.

For getting environmental variables in the .ymls we had to do some envsubst trickery:

When those jobs pass the pipeline continues & builds the Docker image & pushes it to a registry hosted on Amazon ECR, tagged as a review-image. ECR does image vulnerability scanning on push.

To connect to the EKS cluster & other AWS services the pipeline uses a separate CICD-user with IAM role assuming & limited access rights. Early on we made some AWS Lambda functions to automatically rotate the CI/CD access keys very often for additional security.

Review environments

The pipeline then leverages a GitLab CI feature called Review App and creates a new namespace appended with the Merge Request id on our EKS dev cluster. The full app is deployed using an open-source tool Krane. It uses kubectl under the hood, makes sure that the deployment is successful and prints the relevant events & container logs if the job is failed. Naturally, with new pushes, the full deployment to the review namespace is repeated.

The deployment jobs themselves are pretty simple:

Developers can then test the app against all the resources on dev environment before merging. After merging or closing the MR the review namespace & all resources in it are mercilessly destroyed by the pipeline.

When merged another pipeline is started and it does much the same things as before, but this time deploys the app to the main namespace on the EKS dev cluster. Additional jobs are used to deploy the app to EKS clusters on staging & production accounts and these can behind manual approval steps depending on the app.

Final thoughts

Our development teams have ownership over their microservices and are responsible for doing all the deployments & maintaining the Kubernetes configurations etc. by themselves.

For debugging purposes, developers have edit rights to the development cluster, but for other clusters access any edit (& thus deployment) rights are limited to just the CICD and a small number of our AWS admins.

As things stand all of our EKS deployments are done through the CICDs. It has worked mostly very well for us, though sometimes it’s hard to tell which versions have been deployed to which env or coordinate deployments for multiple services. In the future, we might be interested in looking into some GitOps deployment tools like ArgoCD or Flux.

Toni Mauno

Toni Mauno is a Cloud Specialist at Gofore, with a background in software engineering. He is currently working mostly with AWS and DevOps projects, building infra-as-code solutions, working on CI/CD pipelines and backend services, among other things.

Linkedin profile

Do you know a perfect match? Sharing is caring

New technologies have taken over the industry and caused people-focused DevOps to fade into the background. Culture plays a big role in software development, and right now, the industry needs a big dose of real DevOps culture.

One of the main problems in today’s software industry is the excessive focus on technology. This development is closely tied to historical factors. In the early days of DevOps, we did everything ourselves. Nowadays there’s an app for everything, which has lead to a more technology-focused approach in software development.

“When it comes to DevOps, people tend to first focus on the tools and then start looking for the processes to match them, when in fact in real DevOps it should be the other way around. You should create the right software development culture first and only then start thinking about the right processes and tools after that,” says Gofore’s DevOps architect Jani Haapala.

DevOps means teamwork

Real DevOps culture means dismantling information silos and combining functions such as development, maintenance and information security into one collaborative process. In today’s software development, the production volumes have increased and the cycles have accelerated. This has led to a situation where there is no distinct culture in the industry, especially among younger people. Additionally, the professional literature only focuses on processes, leaving the culture to play a secondary role.

Real DevOps means continuous improvement

 DevOps is not an organisational level management model, but rather a desire to produce world-class applications. It allows employees the freedom to implement solutions, but also gives them the responsibility to maintain them in production.

An important part of real DevOps is the DevOps ideology that encourages you to do better and find better ways of working together. With the right kind of culture, experts of different areas can better understand their role in the grand scheme of things.

“A safe working environment and the chance for employees to innovate and be proud of their achievements is an integral part of DevOps. Only after that should we look at which processes best support this and pick the tools to match,” Haapala says.

Aiming for sustainable software manufacturing

An uniform culture is a valuable thing that should be deeply engrained in a company. This also means writing things down, so that the culture will not only exist in the minds and methods of current employees and both current and future employees are able to make their mark on it.

Organisational culture is built on communication and collaboration. Shared methods, such as automating certain things or using test automation in development work, create a strong shared foundation for sustainable improvement.

Happy and healthy people generally tend to achieve better results. The goal of real DevOps is sustainable software manufacturing, which includes that the employees are able to focus on meaningful things that are important to the customer.

“In the future, holistic thinking will become more mainstream. Machine learning and artificial intelligence will become more relevant for DevOps, too, and these skills we certainly be useful in the near future,” predicts Haapala.

Gofore will organise webinars on Real DevOps this spring. The webinars are hosted by the expert consulted in the article, Jani Haapala.

For more information and registration, visit gofore.com/aitodevops

Original article published in Tivi Magazine 19.4.2021

Jani Haapala

Jani works as a DevOps architect at Gofore. He is passionate about measurement, feedback, and continuous automated quality feedback loops. Jani’s journey started from manual testing and has evolved to full-scale software development automation. Jani thinks that automation can help everybody and increase value in anything.

Linkedin profile

Do you know a perfect match? Sharing is caring

With the release of Dart 2.12 & Flutter 2, sound null safety is finally here! What are the benefits, and how do you migrate to null safety? It was all new for Jesper, who decided to read up on the subject and write a few blog posts (start with part 1) about what he learned.

Lists, Sets, and maps can both be non-nullable or nullable and contain non-null or null values.

Map behaves a bit differently than Lists and Sets, which is described in more detail below.

For a more detailed read, visit Google’s documentation: https://dart.dev/codelabs/null-safety.

List, Set

Here’s an informative image on Lists and Sets from Google docs, which is quite helpful:


List and Set examples:

List<String?> nullableValues = <String?>[‘Kitchen’, ‘Bedroom’, ‘Bathroom’];

List<String?>? nullableListAndNullableValues = <String?>[‘Kitchen’, ‘Bedroom’, ‘Bathroom’];

Set<String?> nullableValues = <String?>{‘Utensils’, ‘Glasses’, ‘Plates’};

Set<String?>? nullableSetAndNullableValues = <String?>{‘Utensils’, ‘Glasses’, ‘Plates’};


Here’s another informative image on Maps from Google docs, which is also quite helpful:

Map types have one special feature: a lookup may return a null value, as null is the value for a key which doesn’t exist in the map, and because of this, you can’t assign them to non-nullable variables.

A short example of this behaviour;

Map<String, int> categories = <String, int>{“Kitchen”: 1, “Bedroom”: 2};

int value1 = categories[“Kitchen”]; // ERROR – value is non-nullable

The value variable has to be nullable in order for the lookup to work:

int? value2 = categories[“Kitchen”]; // OK

You can also use the ! operator if you’re sure the lookup succeeds:

int value3 = categories[“Kitchen”]!; // OK

A much safer option would be to use the ?? operator, as so:

Map<String, int> otherCategories = <String, int>{“Kitchen”: 1, “Bedroom”: 2};

int value4 = otherCategories[“Kitchen”] ?? 0;

Required function parameters

One annoyingly behaving feature before Dart 2.12 is the @required keyword. You use it to annotate a specific parameter as required for a function, which takes optional parameters as well. Should the parameter be absent, then the application would throw some kind of error. The Dart analyzer will only warn you if you invoke that function without the required parameter instead of showing an error! This makes no sense, as you have explicitly indicated something as required. That should result in an error, not in a subtle warning.

With Dart 2.12, the parameters of a class constructor or function are required by default. And fortunately, @required has been replaced by required. The Dart analyzer will now show an error if a required parameter is absent, which is quite helpful especially in bigger codebases.

The three null safety principle

As Google puts it, Darts null safety support is based on three design principles:

  • Non-nullable by default. Unless you explicitly tell Dart that a variable can be null, it’s considered non-nullable. This default was chosen after research found that non-null was by far the most common choice in APIs.
  • Incrementally adoptable. You choose what to migrate to null safety, and when. You can migrate incrementally, mixing null-safe and non-null-safe code in the same project. We provide tools to help you with the migration.
  • Fully sound. Dart’s null safety is sound, which enables compiler optimizations. If the type system determines that something isn’t null, then that thing can never be null. Once you migrate your whole project and its dependencies to null safety, you reap the full benefits of soundness —- not only fewer bugs but smaller binaries and faster execution.

The first principle was explained in Part 1; you have to explicitly mark a variable nullable.

The second is something some of you might go for; mixing null-safe and non-null-safe code, nothing wrong with that. Third-party library developers have fortunately been quite active in migrating to null safety. That means that most of our favorite libraries are already null-safe at some capacity, and ready to be used in our next null-safe project.

The last principle is more practical, and what it’s all about; eliminating bugs, better performance, a more enjoyable developing experience, and so on. I strongly recommend that you opt into null safety in your next Flutter application.

In the last part, I’ll be covering migration to null safety. Thank you for reading!





Jesper Skand

Jesper has a great deal of experience in various mobile and full-stack developer roles. He has developed several different Flutter & Android apps and has currently three published apps on Google Play Store. During his spare time, Jesper enjoys time with his kids, coding, and outdoor activities.

Linkedin profile

Do you know a perfect match? Sharing is caring

Remember our Working on the move experiment? It’s finally time to share thoughts and tips on why and how to work on the move.

“The main surprise was and is – why haven’t I done this before!?” —Andres Allkivi, Software Architect

During the four weeks period, Goforeans worked on the move together impressive 400 hours! They logged together 690 entries into the Heiaheia app where we tracked hours moved while working, and shared well over 100 pictures among colleagues. Challenge was spread around the whole group and people were participating in all our cities from Madrid to Jyväskylä. Also, people from different roles took actively part in the campaign.

The most active Goforean during the campaign was test automation specialist Sergei Pavlov. He worked on the move during the campaign time impressive 52 hours which means a rough 2,5 hours every day. “I found it easier to pace my day and get more done when doing some exercises while working. Working remotely alone at home I easily spend all day sitting on my computer, when I get a walk or a jog during the day, I feel more fresh and relaxed after work,” he highlights.

Are you interested in working on the move, but don’t know where to start? Our experts share their tips:

Goforeans suggest you try these sports while working

Double the joy and take your furry friend with you when working on the move.
  • kettlebell weightlifting
  • walking
  • dancing
  • stretching
  • running
  • skiing
  • rubberband workout (tip: the rubberband can be fastened to the chair)

If you still need some motivation, check these

What happens in the future: do we continue working on the move? 

” I think this was  “the” starting point for my movement habit – done once, could happen twice “, Tero Vartiainen. 

We already know that people will work more on the hybrid model in the future. We strongly see that people should pay attention to the way they work especially when they are working from their homes. Going to the office, or some other working location and moving around during the day is much more active than staying at home. This leaves room for innovations that would activate the office worker during the day. We believe that simply taking breaks and pauses is not enough. The logic behind ‘working on the move’ is simple: when you move, you also activate your thinking muscles. When you move you are more productive and happier. Ta-daa! ?  “I’ll definitely continue working on the move,  and will try to find new ways to work on the move.”, Andres says and we suggest you follow his advice.

Nina Etelävuori

Nina is a Communications Strategist who works with Gofore's internal communications and employer brand. She thinks that these two tasks go well hand in hand to make sure that Gofore's recruitment marketing and employer brand correspond to the internal employee experience.

Do you know a perfect match? Sharing is caring

With the release of Dart 2.12 & Flutter 2, sound null safety is finally here!

I haven’t had the time to check it out in more detail, but now that I’ve read about the subject, I decided to write a few blog posts about it. I also created a simple Flutter application, which has non-null-safe & null-safe versions, to support this blog post. The blog posts are also meant to function as a kind of cheat sheet that can be used by anyone.

Sound null safety

Sound null safety itself isn’t a new concept, but now we can finally use it in Flutter, as long as Dart SDK minimum version is set to 2.12!

I’ll go through the basics here, but you can find the complete documentation at https://dart.dev/null-safety.

First of all, what does “sound” or “soundness” mean in a null safety context? Well, Google’s definition is that “if an expression has a static type that does not permit null, then no possible execution of that expression can ever evaluate to null”.

The principles that guided Google when they had to make null safety choices were threefold;

  1. Code should be safe by default
  2. null safe code should be easy to write
  3. the resulting null safe code should be fully sound

A good statement can be found from the docs: “It is not null that is bad, it is having null go where you don’t expect it that causes problems.

When you finally take the step to dive into sound null safety, types in your code are non-nullable by default. That means that variables can’t contain or be null unless you explicitly state so. 

With null safety enabled, the ”runtime null-dereference errors turn into edit-time analysis errors”.

That means that we can avoid unexpected runtime errors if a null value was passed somewhere it shouldn’t have been.

As an example, the following variables are non-nullable when opted for null safety:


int amountChecked = 0;

double amountCheckedPercentage = 0.0;

String amountCheckedLabel = “0.0%”;

int maxChallengesAmount = 0;

To make a variable nullable, we’ll simply insert the “?” keyword after type declaration;

String? title; // declare as nullable, as null indicates that something failed when setting title value


That might look familiar to many, as e.g. Swift, C#, and Kotlin use the exact same nullable declaration logic. It was done intentionally the same way in Dart too, to make it easier for developers to declare nullable types. Jumping between languages can sometimes be a hassle, so it’s convenient if they have as many similarities as possible.

Using “late” keyword

The “seasonJourneyModel” variable that contains all the necessary data for the application shouldn’t be null, otherwise, we can’t do anything with our Flutter app. If we don’t make it null, then the Dart analyzer will complain about possible null problems. We know the variable will be initialized later and used only after that. But how do we tell the analyzer that it won’t be null?

The “late” keyword comes to the rescue! It does just what we want; allows us to initialize our variable at a later point, and won’t complain about possible null problems.


class Controller extends GetxController {

  // This variable contains all data related to current season journey

  late SeasonJourneyModel seasonJourneyModel;


  Future<void> _init() async {


    String jsonString = await rootBundle.loadString(“assets/seasonJourney.json”);

    seasonJourney = SeasonJourney.fromJson(json.decode(jsonString));





However, if we use the variable before it’s an initialization, then a “LateInitializationException” will be thrown during runtime, which ultimately crashes the app.

We can also use the final keyword late, and we don’t have to initialize that variable straight away. We can do that later at our choosing, but we can only assign it once, which is checked at runtime. Calling it twice will cause an exception to be thrown. Using final with late is a great way to model your state that gets initialized at some point and is immutable afterward.

late final SeasonJourneyModel seasonJourneyModel;

According to Google docs, the late keyword has two effects:

  • The analyzer doesn’t require you to immediately initialize a late variable to a non-null value.
  • The runtime lazily initializes the late variable. For example, if a non-nullable instance variable must be calculated, adding the late modifier delays the calculation until the first use of the instance variable.

Using variables and expressions

The Dart analyzer will generate errors when it discovers a nullable value somewhere where a non-null value is required (explicitly or implicitly). It can usually recognize nullable types in variables or expressions inside a function that can’t have a nullable value. 

It is important to handle null values when using nullable variables or expressions. Some possible ways to handle null values can be e.g. if statement or for example the ???. or ! operator;

Using ?? operator:

// value is set as object property if it’s not null, otherwise “unknownSeason”


title = seasonJourneyModel.title ?? “unknownSeason”;

Null check:

List<Widget> _getDrawerItems(Controller controller) {

  List<Widget> items = [];

  if (controller.seasonJourneyModel.chapters == null) {

    return items;



  return items; // Never null!


You should be a bit more careful with the ! operator, since it may throw an exception if the value is null, even the documentation says that ”
If you aren’t positive that a value is non-null, don’t use the ! operator”, so use it with care!

// This will throw an exception if possibleNullValue is null


int? possibleNullValue = 1;

int value = possibleNullValue!;

If you need to change the type of a nullable variable, you can just use the as typecast operator.

This example contains as operator to convert a num? to an int?:

print(possibleNullValue()); // prints “null”

int? possibleNullValue() {

  num? age;

  return age as int?;


If you change the function to non-nullable and remove nullable declaration from 
as operator, then an error will be thrown:


// throws error


int possibleNullValue() {

  num? age;

  return age as int;


When you’ve opted into null safety, you can’t use the member access operator (.) anymore, since the operand might be null. You can instead use the null-aware version of that operator, 


In the next part, I’ll be covering collections among other things. Stay tuned!


Jesper Skand

Jesper has a great deal of experience in various mobile and full-stack developer roles. He has developed several different Flutter & Android apps and has currently three published apps on Google Play Store. During his spare time, Jesper enjoys time with his kids, coding, and outdoor activities.

Linkedin profile

Do you know a perfect match? Sharing is caring

Neural network from scratch

When I was studying computer science at the University of Helsinki, I went to one machine learning course. During the first lesson, there was a prerequisites test, which I failed spectacularly, and gave up the course right there. After that, I did not look into neural networks for many years despite making my master’s thesis about optimisation algorithms in real-time strategy games, where they certainly could have been one viable solution.

Somewhat recently I was interested in stock trading. That inspired me to read a little about neural networks and I tried building one that predicts the stock market with Google’s open-source solution TensorFlow. Obviously, that did not work, but still, I learned the basics of TensorFlow, which is probably one of the best options to use if you want to get something real done. You only need to describe the network, give it a bunch of parameters and TensorFlow handles the actual implementation.

Some weeks ago I stumbled across these four great videos describing neural networks in a way that really increased my understanding of how they actually work. But still, I felt that the last piece was missing. To really understand what is happening and how I needed to implement one myself. All the way from scratch.

Recognising numbers – an approach with less math

For this project I chose probably the most standard schoolbook problem there is: recognising handwritten digits. This kind of image recognition task – while far from trivial – is one where artificial neural networks have been proven to work well countless times already. Also, large amounts of labeled training data is easily available.

If one wants to build an efficient neural network, the obvious and likely easiest way to go is to make heavy use of matrix operations and other linear algebra. I suppose that is one reason why python is a common language here, given its good library support for such things.

But that was not my goal. If I wanted to build a concrete understanding as possible of how the network works, I felt that for me personally, the math would be in the way, obscuring things, although I do see why many would disagree.

So instead I chose a more object-oriented approach to model the network, with a sprinkle of functional programming on top (don’t want to feel like a dinosaur after all, despite maybe having a minor 30’s crisis). My language of choice was Kotlin, which along with TypeScript is one of my clear favourites at the moment. I did not use any dependencies, everything is plain handwritten Kotlin.

Given that a random number generator would guess the digit correctly 10% of the time, I set my initial target to 15 % success rate, just to prove that the network is at least doing something right. As it turns out, this was rather pessimistic and to my surprise, the final success rate made it past 90%, and after adding some ad-hoc convolution layers it exceeded 96%. Let’s see how we got there!

What is it and how does it fly

First of all, I want to emphasise that this is not an area that I have extensively studied, so some of the terminology and theory here may be slightly inaccurate. But then again, the whole point of this post is to show that you can already get some pretty cool things done without spending a lot of time with your nose stuck in a book.

That out of the way, I will now first attempt to explain how an already-trained network functions. How does it get from a bunch of pixel values in the input image to a single number being the recognised digit?

Basic concepts

Most of this revolves around three very simple concepts, which are also at the center of my object-oriented model.

  1. A neuron, with a loose analogy to the brain, is a node in the network. Each neuron has an activation value; a number describing how active it is. You might also visualise the neuron as a light bulb and think of the activation as how brightly it is lit. Each neuron also has a value called bias, which could be thought of as a minimum power needed for the neuron to light up.
  2. Between neurons there are connections. Each connection has a weight, which describes the strength of the connection, i.e. how much the activation of the previous neuron affects the activation of the latter. Weight can also be negative, in which case when the input neuron gets brighter the output neuron gets dimmer and vice versa.
  3. While this may not really have an analogy with brains, usually the network is organised into layers. Each layer is simply a set of neurons. The very first layer is the input layer, the very last one is the output layer, and any layers between them are so-called hidden layers. In a most basic network, every neuron on every layer is connected to every neuron in the next layer. How many hidden layers there should be and how many neurons should each have is very difficult to know in advance. Mostly it is a matter of trial and error: keep trying different things until you get lucky and something gives good enough results.

Propagation from input to output

Now that we have laid out some terminology and described what the network consists of, let us next look into how it works and how it can be used.

First, we shall manually set the activation of each neuron on the input layer so that they somehow encode the input data. In this case, the input is a grayscale image of 28×28 pixels, each of them having a value of how bright/dark that pixel is. Our input layer will thus consist of 784 neurons, each of them associated with one pixel, and the activation value between 0.0 and 1.0 will be set to the brightness of that pixel.

Next, we will start moving on layer by layer. For every neuron, we will calculate its activation based on the connections from the previous layer. I will soon explain how exactly is each activation calculated.

Finally, we arrive at the output layer. In this classification problem where we are trying to recognise digits between 0 and 9, there are 10 different options. Thus our output layer should have 10 neurons. Once we have calculated an activation level for each of them, our best guess for the digit in the image is simply the index of the output neuron that has the highest activation.

Calculating activations

Calculating the activation of a neuron is done with a simple propagation function. At its core, it is just a weighted sum. You look at each neuron on the previous layer and multiply their activations with the weights of the associated connections. Then you just sum them up and finally add the bias of the neuron.

The last step we should do is to put the result through a so-called activation function. Here again, there are many alternatives that you can try, but the most classic option is a function called sigmoid. That function basically squishes the input from the previous step so that the end result is always between 0.0 and 1.0.

Class model and forward propagation implementation

Now we have a general understanding of the terminology. As mentioned, I chose to implement the network in an object-oriented model, so I know you must already be itching to see some UML diagrams. No worries, I got you, buddy!

So essentially there are just those three concepts we talked about – neurons, layers and connections – but they have been broken down a bit more. For example, every Neuron has activation, but OutputNeurons on the OutputLayer have no further output connections since it is the last layer. Therefore, the outputs field is on the TransmitterNeuron interface, which is implemented by InputNeuron and HiddenNeuron but not OutputNeuron. Connection has weight and ReceiverNeuron has bias. That is pretty much all.

With these constructs, after we have set the activations on InputNeurons based on some image, the matter of propagating the activations forward all the way to the OutputLayer only requires calling one function for every ReceivingLayer in order

which in turn only calls a simple eval function for each ReceiverNeuron

As discussed earlier, the activation is calculated simply by taking a weighted sum of the input activations, adding the bias, and then crushing the result through an activation function such as sigmoid.

How to train your network

As seen above, implementing the network is very simple if you somehow already have a set of values for every single weight and bias. Assuming our network has those 784 input neurons, 10 output neurons and for example just one hidden layer of 16 neurons, that already means there are 784 * 16 + 16 * 10 = 12 704 connections with weights, and 16 + 10 = 26 biases (neurons on input layer do not have bias). A total of 12 730 parameters. Making the network “recognise” something is all about finding values for these parameters that happen to cause certain inputs to produce correct outputs.

If we were to consider only one image, say one that represents digit 2, it would be rather easy to find some weights and biases even manually so that the output neuron associated with digit 2 would have activation close to 1.0 and every other output neuron would have activation close to 0.0. But that is of course not enough. We need to find values that simultaneously lead to correct answers with any of the thousands of training images representing different digits.

So how do we actually get those? The first idea might be to just randomly try different values until we find something that works. Unfortunately, since there are so many parameters, that would be way too slow. It would be like trying to navigate in a huge forest without a map or a compass while blindfolded. Fortunately we can devise sort of a compass by using a process called gradient descent.

Credit: https://xkcd.com/1838/

Gradient descent and backward propagation

If you really don’t like math, skip this chapter. 😉

First, we need to define a cost function, which tells us how badly the network behaves at the moment. The cost function typically used in neural networks is

C = (a – y)2,

where a is the activation value of an output neuron and y is the value we would have wanted it to be. That of course needs to be summed up overall output neurons and also averaged over sufficiently many training images. Now we have a single number to measure how bad the network is and we can improve the network by trying to minimise that.

Now imagine that you are standing on a hillside. If we think of the cost function as how high you are, that means you want to go down the hill and find the bottom of the valley. However, it is night and you can not see anything. In that case, you could try to feel the ground around yourself, find out the direction where the descent is steepest, take a step towards that direction and repeat. By using this algorithm you are at least guaranteed to soon end up at the bottom of some valley. That is the idea of gradient descent.

So we could just adjust the parameters a tiny bit and see which way the cost function decreases the most. Unfortunately, the number of parameters is still an issue. Basically, we are not standing on a normal hillside, but on one that is 12 730 dimensional, so there are just too many possible directions to feel around. So again we need to reach into the toolbox of mathematics to find something that helps us speed up the process.

While the cost function seems to be a function of the activation values, those, in turn, are functions of the network parameters. Therefore, the cost function is in the end just a function of all those different parameters, albeit rather complex ones, and it is possible to analytically find a partial derivative of the cost function with respect to each different parameter. Then in order to take a step towards the right direction, we simply evaluate the partial derivative for every parameter, multiply it with some small constant (a step size) and subtract it from the parameter’s current value. Now we have a working compass that tells us the direction to go!

I will not go too deep into the mathematics of how to derive those partial derivatives, but if you start from the last layers, it starts off quite simple. For example, with some high school mathematics, you can see that if

C = (a – y)2 = a2 – 2ay + y2

then the partial derivative of it in respect to a is:

dC/da = 2a – 2y = 2(a – y)

Next, we need to consider how for instance some weight w0 would affect a. That is, what is the partial derivative da/dw0. Once we have that we can use the chain rule to find the partial derivative of the cost function C with respect to the weight w0

dC/dw0 = da/dw0 dC/da

As I promised not to dive too deep into mathematics, I will not even try to show what is that da/dw0, but hopefully, you now have at least a very vague idea of how we would approach solving these derivatives. As you might have guessed, the derivatives get rather complex once you move further away from the output layer, since there are so many things that chain together. Fortunately, though, it turns out that once we have figured out all the partial derivatives of the last hidden layer LN-1, it is enough to find partial derivatives of the activations on that layer with respect to the parameters of the second last layer LN-2. Then we can chain those together to find the partial derivatives of the cost function with respect to the parameters on layer LN-2. This way we only have a limited amount of math to figure out, and then we just apply it recursively, moving back one layer at a time. This is called backward propagation.

Implementation of the training algorithm

It is quite possible that I derived some of the equations incorrectly, but be as it may, the end results are decent and with machine learning that is all that really matters, so I will dare to present my code that relies on those equations. If we take those for granted, my solution becomes quite simple.

First, we need to do a couple of small additions to our data model. To be able to average partial derivatives for weights and biases over multiple images and to keep track of partial derivatives by neuron activations when back-propagating we add these lists:

  • Connection
    • derivativesOfCostByWeightPerSample: List<Double>
  • ReceiverNeuron (i.e. HiddenNeuron and OutputNeuron)
    • derivativesOfCostByBiasPerSample: List<Double>
  • HiddenNeuron
    • derivativesOfCostByActivation: List<Double>

For OutputNeuron we also add a field desiredActivation: Double which will contain the expected activation value based on current image.

Then the training algorithm consists of these 8 relatively simple steps:

Step 1

Clear derivativesOfCostByBiasPerSample for every neuron and derivativesOfCostByWeightPerSample for every connection

Step 2

Clear derivativesOfCostByActivation for every HiddenNeuron

Step 3

Use a training image as input and propagate forward to set all activation values. Set desiredActivation for each OutputNeuron to 0.0 or 1.0 based on what digit the image represents.

Step 4

Call the train function on each OutputNeuron:

Here on line 4 we have the previously discussed derivative of the cost function. The train function starting on line 6 simply calculates some values and adds them to lists.

On line 7 we need the derivative of the activation function, which in the case of sigmoid is

Step 5

Call the train function on each hidden neuron on the last hidden layer. Then do the same for every neuron on the previous layer, and so on.

Here we again just calculate similar stuff and add the results into lists. On lines 8, 16 and 21 you get a taste of Kotlin’s nice functional API with the averageBy function. The lambda you give it is evaluated for each element of the list and then an average is calculated from those. Clean, concise and simple!

Step 6

Repeat steps 2-5 for at least some portion of the training images.

Step 7

Call the nudgeParameters function for each ReceiverNeuron, which will adjust all the parameters based on the previously calculated results.

Here on line 5 I chose to limit the bias always between -0.8 and 0.8.

Step 8

Repeat the steps 1-7 until the results no longer improve. That is all!

Online demo

For presentation purposes, I also wrote this demo application with React and Typescript. There you can draw a number yourself and the app will try to recognise it. The results are not quite as great as with the pre-made test data set, possibly due to differences in normalising the images, but still rather impressive in my opinion. How it works is that I implemented the forward propagation part of the network also in TypeScript and imported a pre-trained network from a JSON file that I had exported from the Kotlin version. The rest is just basic React and HTML5 stuff.

Feel the force, read the source

I challenge you to go through the same exercise yourself during some rainy weekend to make use of these times of isolation. I feel this was a very interesting and rewarding little project. However, you may also have a look at all the code in my full solution and use it as you wish: https://github.com/Joosakur/neural-network-test

Are you eager to learn something new? Are you interested in challenging tech projects? Check out also Join the Crew 

Joosa Kurvinen

Joosa is an experienced fullstack software developer with a very broad skill set. He is always eager to learn and try out new things, regardless of whether that is with backend, frontend, devops or architecture. He has an agile mindset and always strives after clean and testable code. Joosa graduated from the University of Helsinki and wrote his master's thesis on AI and optimization algorithms.

Linkedin profile

Do you know a perfect match? Sharing is caring

Our Gofore: Jani Haapala

I’m Jani Haapala and I work as a Quality Consultant and DevOps evangelist. I’m very fortunate since I do not get to do these things only during my working hours but I can use all my knowledge and learnings in my free time when I am coding my own megalomanic home automation project.

Gofore is an extremely warm and employee-focused company. Gofore makes a great deal of effort to make employees happy and satisfied with the work. There is a huge number of different groups that you can join to do things that you feel interested in. You can affect a lot that what kind of work you want to do. Gofore has also lots of opportunities and possibilities for self-development.

Tell us about your career and study background?

I have a background of being an electrician who got super-interested in technology and went to polytechnic to study telecom networks and coding. During the studies, I got an internship place from Ericsson and as a bonus, got a possibility to do my thesis work for them also. My thesis subject was selected to be “An automation solution for testing team needs”. That was the moment when testing and automation flies bit me… hard. From that moment on I have been extremely interested in testing, test automation, and lately infrastructure automation and automating software development processes. I stayed on Ericsson for about three years and switched to a company called Espotel to be a consultant, more specifically, test automation consultant. My first assignment was with Nokia Siemens Networks. That was the time that got me interested in consultant work since it is a profession whose role is to help others to succeed with your expertise. I stayed on Espotel for about three years until I found Qentinel.

Qentinel was focused on testing and test automation so it felt like a natural move, so I joined them. I have been now in Qentinel/Gofore as a Quality Consultant for a bit over ten years and I am still loving my company every day. My career has taken me from a manual tester into becoming a Lean and DevOps evangelist with a flavor of software development process automation specialist. I also feel that I am very fortunate since I do not get to do these things only during my working hours, but I can use all my knowledge and learnings in my free time when I am coding my own megalomanic home automation project.

What makes Gofore a great company for you?

Gofore is an extremely warm and employee-focused company. Gofore makes a great deal of effort to make employees happy and satisfied with the work. There is a huge number of different groups that you can join to do things that you feel interested in. You can affect a lot that what kind of work you want to do. Gofore has also lots of opportunities and possibilities for self-development.

I appreciate that the employer offers training opportunities such as Udemy online courses as well as libraries. Another important point is the opportunity to attend conferences to get new ideas. All of these have been things keeping my passion alive and made it possible that I have got to try new learnings in customer projects and also my perspective have evolved. And once you have learned and tried things enough and been able to use them in many contexts, it is natural to start helping colleagues as well.

What are the things you most likely tell your close circle about Gofore?

About all the enthusiastic people that Gofore has and all the things that these people do daily to make work fun and inspirational. Also, all the possibilities that you have to develop yourself.

What is the best Gofore memory?

There are lots of warm memories with all the lovely customers that I have been able to work but the Gofore memory, I have to say some of the many gatherings that we have had. For example, our Coding days where we have been together, coding with Legos and RasperryPi’s. Competing on various wacky coding challenges together and won some cool prizes too.

What is your favorite internal Slack channel at Gofore?

I am a huge photography fan and was so amazed to see that there is an own channel for us, so it has to be the #photograpy-gerho. ?

What are the technologies and tools you mostly work with?

I mostly work nowadays with Infrastructure as Code (IaC), DevOps, and CI/CD pipelines so the tooling and technologies come around that. Some of the key tools and technologies are AWS, Docker, Kubernetes, Jenkins, Python, RobotFramework, GithubActions, Linux.

How does your typical day look like? What excites you most in your daily work?

My calendar tends to get quite fast full of meetings. Meetings with various customers, sales support meetings, and team meetings. I do nowadays lots of designing, documenting, and coaching work also but I always try to reserve some time for the pure coding, experimenting, and building work. The most exciting and rewarding moments in my work are the situations where I manage to help somebody else to be happier on their work. That can be through coaching, solution design, or solution building that helps somebody else to be more efficient, do things more easily or enjoy one’s work more. And maybe learn something new also.

What would you like to do in the future at Gofore?

I would like to take the DevOps concept further on our company and make it more understandable for everybody. I would also like to help to create opportunities for everybody to have a good career path, to evolve and gain new skills. Especially in the areas of DevOps and Test Automation.

What kind of digital world do you want to build with others?

The digital world and digitisation are all about the speed and fast phase of ideas, competencies, and concepts that are filling our daily life. It is not enough anymore to study a profession and do that until you get retired. Nowadays there needs to be a humane and ethical way to deal with this digitisation fuzz and help people to stay ahead of it. This means that we need to create enthusiasm, opportunities, and learning paths for people to constantly learn new things and develop their skills. Having needed skills makes people feel needed and part of something greater, and that brings happiness.

For me example, I do not feel that I am working, I feel that I have a passion for the things that I do and as a bonus I get also paid to do those. I live and breathe the things that I do and the technologies that I use. I want that everybody can also feel this passion and get the same feeling and opportunity to be passionate. Everybody needs the possibility to be good at their passion.

It’s often said that digitalisation has changed every part of human life – how we live, how we work and how we interact with the world around us. But when you think about it, digitalisation alone can’t change anything. Without people, it’s all just hardware and code. That is why we want you to get to know our people, Goforeans. Read Our Gofore stories! ?

Are you interested in job opportunities at Gofore, read more about us, and check out the open positions gofore.com/en/join-the-crew/

Gofore Oyj

Do you know a perfect match? Sharing is caring

Test automation as a service

Testing and test automation have in the past been seen mainly as one part of software development. With the development of digitalisation, the topic has become familiar to almost every company. In many cases, testing and safeguarding processes that are critically important to businesses have come with the systems purchased, almost as a surprise to the purchaser. For example, when switching to cloud-based systems, it may not have been possible to predict how big and permanent the change in testing would be.

The situation is made more difficult by the fact that to the extent required, the topic is new to many companies – there is no existing testing culture or practices that can be scaled to meet changing needs. Test initiatives have been handled in the past under the guidance of a few system experts, and have often focused on very specific and known changes. No large-scale and regular regression testing has been performed in the company, or it has been done very rarely.

When system vendor version updates suddenly start running at least quarterly and all testing has to be done within a few weeks, however, previous practices and resources are no longer sufficient. The challenge is made more difficult by the fact that there are usually only a handful of people who know the business processes of the systems well enough, and these are largely business experts. They may have a good understanding of the processes to be tested, but in the long run they can’t be burdened any further with testing without it having a negative impact on their other productivity.

With the difficulties described and the increasing testing load, it can be quickly concluded that help is needed for testing. Moreover, test automation is more likely to be considered a more sensible approach than having a large group of testers.

Awareness of the need for test automation is often a long way from a situation where automation handles any whole part of the company’s testing needs. One clear bottleneck is identifying and acquiring the necessary resources. If it was previously difficult to outline the overall test and the amount of testing required without systematic work on the topic, outlining all the requirements of test automation is even more difficult. Suddenly, there should be answers to what kind of skills are needed, how much they are needed, how long they are needed for, where they come from, and how much they cost.

Typically, an automation deployment project involves some form of planning and preparation process. The scope of this is often directly proportional to the initial situation of the company in terms of testing. The end result is at the very least a test plan, sufficiently detailed use cases, and a technical analysis of the processes to be tested and their suitability for automation. In addition, test environments, users and data are required. Finally, it is decided how things will be prioritised, monitored, reported and responded to, and where all this will be managed. This is a lot of different things to know, even when shared by multiple people.

As technical performance is achieved, the need for different capabilities increases. The technical implementation of the project requires infrastructure experts, software developers and test automation experts. Alternatively, it is possible to choose a commercial product to handle some of the challenges.

Often, the amount of skills required is so difficult to determine in advance that after a procurement or recruitment decision, the company ends up seeking a mixture of consultants or individuals without knowledge of the actual need for skills. Recruitment processes are then unnecessarily cumbersome, time-consuming, and the progression of test automation from the deployment decision to the start-up phase can be several months – and several test cycles – away.

Testing as a Service

Imagine choosing a previous type of procurement model, for example, in terms of construction. The established operating models are ignored, and the necessary resources are calculated for the next three years with the aim of having the premises renovated. All the needs are successfully planned and procured for. Sure. Difficult, and most likely also quite slow, expensive and risky.

What if the setup is reversed – what if testing and test automation projects could be ordered and delivered like plumbing repairs? Or how about breaking it down into even smaller parts. Why buy an entire project at once if you only need a smaller single part to start with, one that brings things forward by a controlled amount at a time.

The customer buys it as a service, and the supplier makes sure that the customer always has the core skills needed at that moment in the current work phase of the project. Work and costs can be paced in an agile way with the most suitable schedule and capacity for the customer. In some situations, the service may be a few people who are constantly needed in the project, and the number can be scaled to suit the situation as necessary. In the second case, it could be one common human resource model where the work input is given today by a test manager, tomorrow by a cloud expert, and next week by a test automation developer. In the third case, for example, a fixed-length sprint of two weeks to one month can be agreed, in which the supplier jointly submits a work performance based on a defined workload estimate and is responsible for its implementation (= renovation). In all cases, the service scales up and down. According to the need and the situation.

Get agile test automation as a service from Gofore

Speed and agility are directly included in the Gofore operation model. Subscriber is not bound to anything more complex than a single kiosk purchase which can be, for example, a specific training course or the automation of a single use-case.

Invoicing is carried out only when the process is in progress, and the supplier ensures that the right kind of expert is always available. Instead of one person, there is always a bigger team behind the implementation, with all the necessary expertise needed at any point during the project.

Mika Lindh

Mika works as the Service Manager at Gofore Test Automation Hub. He is passionate about RPA and test automation. Mika has worked long as a Test Automation developer, consultant and he has long experience with ERP-systems.

Linkedin profile

Do you know a perfect match? Sharing is caring