Machine Learning (AI) model in Roblox - Number Recognition

A basic machine-learning model

With the recent take-off of AI and machine-learning models such as OpenAI I decided to research into the basic concepts of neural-networks and how they work

I ended up using all the acquired knowledge practically to create a basic example of how machine-learning can be implemented in Roblox

My model

Simple explanation of how it works:

A model which is able to distinguish patterns on a canvas and guess the drawn number (in the range zero to nine)

The model has to be trained (fed information on existing drawings) to be able to become as accurate as possible e.g shown images of the number “1” and corrected every guess if it guesses wrong

Complex analysis:

A single-layer perceptron (multi-neuron compatibility) which leads to an evidenced output if whether the number drawn is the correct number the AI thinks it is

My model utilises core concepts from neural networks such as weights, biases, sigmoid, gradient-descent etc. (3Blue1Brown has a really good playlist on these concepts)

Example

  • Untrained model with no previous info thinks “1” is a “9” or “2”


In the end this model would really need a lot of training (hundreds of thousands of images) to become extremely accurate but I had a really fun time making this.

If you have any questions or comments please don’t hesitate to ask below!

8 Likes

Is machine learning really that simple?

1 Like

It would be cool to see this expanded to be able to process multi-digit numbers. That would probably be a ton more training though.

2 Likes

I am glad that you were able to understand my explanation. :rofl:

Personally speaking, the concept of machine-learning is quite straight-forward with a machine literally learning from data given to it. For example, if I show a machine 100 pictures of different cats then give it a new random picture of a different cat it should be able to distinguish if the image provided is a cat.

However, the core mathematics and in-depth explanation of how the machine actually learns to pick up on whether the image provided is in fact a cat becomes way more complicated as you keep learning. I would do a horrible job explaining how it does so; therefore, I highly recommend checking out 3Blue1Brown’s playlist on Neural Networks for an in-depth explanation!

2 Likes

Are you using a pre-existing libraries like DataPredict or did you build the model yourself from scratch?

1 Like

I built the model up from scratch, no modules were used, but I looked at your post and that’s an amazing resource you’ve created! 3Blue1Brown’s introduction to Neural Networks really helped with explaining the logic behind a basic ML model that’s what I used as a reference to code this up.

All I did was make a single-layer perceptron class that could take multiple inputs and give out one output.

1 Like

Not really, I also learned the math from scratch from 3blue1browns videos but I also anyalized other python modules. Machine learning is organized in layers and a single layer is easier to compute for but multiple layears are harder as it requires more math calculations, a perceptron is relativley simple because it only has 1 layer while other nueral networks can have 10-100s of layers.

Think of it like this, each layer breaks down something complex into something simpler so

The pixels of a number → The edges → The parts of the number → The final number

1 Like

I wrote this in js which is kind of what I explained:


//  ---
// |   |
//  --- \
//       \
//  ---   \ ---
// |   |---|   |
//  ---   / ---
//       /
//  --- /
// |   |
//  ---
//
// Nueral Network Library
// Because I'm bored

function sigmoid(z) {
    return 1 / (1 + Math.exp(-z));
}

var nueronThing = -5
var amountAdd = 0

class Nueron {
    constructor(previousNuerons) {
        var weights = []
        for (var i = 0; i < previousNuerons; i++) {
            nueronThing = nueronThing + (nueronThing/5 )
            weights.push()
        }
        this.weights = weights
        this.bias = 0
    }
    updateBias(bias) {
        this.bias = bias
    }
    setInput(input) {
        this.activiation = input
    }
    updateWeights(weights) {
        this.weights = weights
    }
    getWeights() {
        return this.weights
    }
    computeAcitivation(previousNuerons) {
        var weights = this.weights
        var activiation = 0
        previousNuerons.forEach(function(nueron,index) {
            activiation = activiation + (nueron.activiation*weights[index])
        })
        this.withoutSigmoid = (activiation-this.bias)
        activiation = sigmoid(activiation-this.bias)
        this.activiation = activiation
        return activiation
    }
}

function softmax(arrayActiavtion) {
    var activationThing = []
    var activationSum = 0
    var activiations = []
    arrayActiavtion.forEach(function(activated) {
        activationThing.push(Math.exp(activated))
        activationSum = activationSum + Math.exp(activated)
    })
    activationThing.forEach(function(item,index) {
        activiations.push((item/activationSum))
    })
    return activiations
}

class NueralNetwork {
    constructor(nueralSturcture, useSoftmax) {
        var nueralThing = []
        nueralSturcture.forEach(function(amount,index) {
            nueralThing.push([])
            var previousAmount = 0
            if (index > 0) {
                previousAmount = nueralSturcture[(index-1)]
            }
            for (var i = 0; i < amount; i++) {
                nueralThing[(nueralThing.length-1)].push(new Nueron(previousAmount))
            }
        })
        this.useSoftmax = useSoftmax
        this.structure = nueralThing
    }
    runNetwork(input) {
        var activiations = []
        var previousValue = []
        var totalAcitivations = []
        var withoutSigmoid = []
        var useSoftmax = this.useSoftmax
        var structure = this.structure
        this.structure.forEach(function(thing,index) {
            totalAcitivations.push([])
            withoutSigmoid.push([])
            thing.forEach(function(nueron,index2) {
                if (index == 0) {
                    nueron.setInput(input[index2])
                    totalAcitivations[index].push(input[index2])
                    withoutSigmoid[index].push(input[index2])
                } else {
                    nueron.computeAcitivation(previousValue)
                    totalAcitivations[index].push(nueron.activiation)
                    withoutSigmoid[index].push(nueron.withoutSigmoid)
                }
            })
            if (index == (structure.length-1)) {
                if (useSoftmax == true) {
                    var activationThing = []
                    thing.forEach(function(activated) {
                        activationThing.push(activated.activiation)
                    })
                    activiations = softmax(activationThing)
                } else {
                    thing.forEach(function(nueron) {
                        activiations.push(nueron.activiation)
                    })
                }
            } else {
                previousValue = thing
            }
        })
        return [activiations, totalAcitivations, withoutSigmoid]
    }
    exportWeightsAndBiases() {
        var weights = []
        var biases = []
        this.structure.forEach(function(layer,index) {
            if (index > 0) {
                weights.push([])
                biases.push([])
            }
            layer.forEach(function(nueron) {
                weights[(weights.length-1)].push(nueron.weights)
                biases[(biases.length-1)].push(nueron.bias)
            })
        })
        return [weights,biases]
    }
    learn(dataSet, epochs) {
        for (var epochIndex = 0; epochIndex < epochs; epochIndex++) {
            var weightUpdates = []
            var biasUpdates = []
            this.structure.forEach(function(_,index) {
                if (index > 0) {
                    weightUpdates.push([])
                }
            })
            this.structure.forEach(function(layer,index) {
                if (index > 0) {
                    biasUpdates.push([])
                    layer.forEach(function() {
                        biasUpdates[(index-1)].push(0)
                    })
                }
            })
            var totalAverageCost = 0
            for (var dataSetIndex = 0; dataSetIndex < dataSet.length; dataSetIndex++) {
                var output = this.runNetwork(dataSet[dataSetIndex][0])

                output[0].forEach(function(predicted,index) {
                    totalAverageCost = totalAverageCost + (((dataSet[dataSetIndex][1][index]-predicted)*(dataSet[dataSetIndex][1][index]-predicted)))
                })

                var lastLayerDerivative = []
                if (this.useSoftmax != true) {
                    output[0].forEach(function(predicted,index) {
                        lastLayerDerivative.push((2*(dataSet[dataSetIndex][1][index]-predicted))*(sigmoid(output[2][(output[2].length-1)][index])*(1-sigmoid(output[2][(output[2].length-1)][index]))))
                    })
                } else {
                    var softMaxArray = softmax(output[2][(output[2].length-1)])
                    output[0].forEach(function(predicted,index) {
                        lastLayerDerivative.push((2*(dataSet[dataSetIndex][1][index]-predicted))*(softMaxArray[index]*(1-softMaxArray[index])))
                    })
                }
                
                var nueralSturucture = this.structure
                
                nueralSturucture[(nueralSturucture.length-1)].forEach(function(nueron,index2) {
                    var weightsNew = []
                    nueron.weights.forEach(function(weight,index) {
                        var learningRateMultipledDerriviative = (output[1][(output[1].length-2)][index]*lastLayerDerivative[index2])
                        weightsNew.push(learningRateMultipledDerriviative)
                    })

                    //console.log('Layer ' + (nueralSturucture.length) + ' Nueron ' + (index2+1) + ' bias: ' + lastLayerDerivative[index2])

                    if (weightUpdates[(weightUpdates.length-1)][index2] == undefined) {
                        weightUpdates[(weightUpdates.length-1)].push(weightsNew)
                    } else {
                        var newWeights = []
                        weightUpdates[(weightUpdates.length-1)][index2].forEach(function(weightUpdate,index) {
                            newWeights.push((weightUpdate+weightsNew[index]))
                        })
                        weightUpdates[(weightUpdates.length-1)][index2] = newWeights
                    }

                    biasUpdates[(biasUpdates.length-1)][index2] = biasUpdates[(biasUpdates.length-1)][index2] + lastLayerDerivative[index2]
                })

                for (var i = (nueralSturucture.length-2); i > 0; i--) {
                    var layer = output[2][i]

                    var weightsOrganizedByOrder = []

                    nueralSturucture[(i+1)].forEach(function(nueron,index2) {
                        nueron.weights.forEach(function(weight,index) {
                            if (index2 == 0) {
                                weightsOrganizedByOrder.push([])
                            }
                            weightsOrganizedByOrder[index].push(weight)
                        })
                    })

                    var averageWeights = []

                    weightsOrganizedByOrder.forEach(function(weightArray,index2) {
                        var amount = 0
                        weightArray.forEach(function(weight,index) {
                            amount = amount + (weight*lastLayerDerivative[index])
                        })
                        averageWeights.push(amount)
                    })
      

                    var layerDeriviative = []

                    layer.forEach(function(data,index) {
                        layerDeriviative.push((sigmoid(data)*(1-sigmoid(data)))*averageWeights[index])
                    })
                    
                    lastLayerDerivative = layerDeriviative

                    nueralSturucture[i].forEach(function(nueron,index2) {
                        var weightsNew = []
                        nueron.weights.forEach(function(weight,index) {
                            var learningRateMultipledDerriviative = (output[1][i-1][index]*lastLayerDerivative[index2])
                            weightsNew.push((learningRateMultipledDerriviative))
                        })

                        //console.log('Layer ' + (i+1) + ' Nueron ' + (index2+1) + ' bias: ' + lastLayerDerivative[index2])
                        
                        if (weightUpdates[(i-1)][index2] == undefined) {
                            weightUpdates[(i-1)].push(weightsNew)
                        } else {
                            var newWeights = []
                            weightUpdates[(i-1)][index2].forEach(function(weightUpdate,index) {
                                newWeights.push((weightUpdate+weightsNew[index]))
                            })
                            weightUpdates[(i-1)][index2] = newWeights
                        }
    
                        biasUpdates[(i-1)][index2] = biasUpdates[(i-1)][index2] + lastLayerDerivative[index2]
                    })
                }
            }
            this.structure.forEach(function(layer,index) {
                if (index > 0) {
                    layer.forEach(function(nueron,index2) {
                        var newWeights = []
                        weightUpdates[(index-1)][index2].forEach(function(weightDerivative, index) {
                            newWeights.push((nueron.weights[index]-(weightDerivative*(0.1/dataSet.length))))
                        })
                        nueron.updateWeights(newWeights)
                        nueron.updateBias((nueron.bias-(biasUpdates[(index-1)][index2]*(1/dataSet.length))))
                    })
                }
            })
            console.log('epoch: ' + epochIndex + '; cost: ' + totalAverageCost + '; average cost: ' + (totalAverageCost/dataSet.length))
        }
    }
}

var fs = require('fs')

async function trainModel() {

    var NN = new NueralNetwork([5,16,16,10], false)

    var exported = NN.exportWeightsAndBiases()

    fs.writeFileSync('./weights2.json', JSON.stringify({data: exported[0]}))
    fs.writeFileSync('./biases2.json', JSON.stringify({data: exported[1]}))

    NN.learn([[[0,0,1,0,1],[0,0,0,0,0,0,0,0,1,0]],[[1,0,0,0,1],[0,0,1,0,0,0,0,0,0,0]]],100)

    console.log(NN.runNetwork(dataSetOragnzied[0][0])[0])

    var exported2 = NN.exportWeightsAndBiases()
    fs.writeFileSync('./weights.json', JSON.stringify({data: exported2[0]}))
    fs.writeFileSync('./biases.json', JSON.stringify({data: exported2[1]}))
}

trainModel()
1 Like