Neural Network Library 2.0

A little under a year since my last release, it looks like it’s that time again.

With more neural network knowledge and understanding of OOP, I was able to build a worthy successor to my first library, found here.
Other than being made professionally rather than for practice, the new library boasts a high-quality documentation website, trial LSTM neural networks, as well as a Github repository.

What is this library?

For those unfamiliar with my old library or machine learning in general, it is a programming science based on creating programs that, in their own way, mimic a biological brain. Neural networks allow for the creation of low-level AI that can perform complex tasks that are too difficult to code or AI that can predict and extrapolate based on the given data. This ranges from text generation to self-driving vehicles and brainwave visualization. Though this library cannot work with images, it can provide most of the functionality you will want from a neural network on Roblox.

The base goal of this library is to educate young programmers about machine learning on Roblox as there are no other libraries like this, but I also mean to provide developers with a valuable tool for their own goals.

How do I use it?

To get a proper introduction to the library, learn about neural networks, installation instructions, see the documentation, and see the example code for the library, just visit the Github website here!

Roblox Neural Network Library Website [CLICKY]

For the library itself, go ahead and grab it here!
https://www.roblox.com/library/5951897165/Roblox-Neural-Network-Library-V2-0

And if you want to review the code, check out the Github repository!

If you haven’t noticed, this time, I chose to place as much information on the website rather than this article. This gives me more control and I can place as much content in there without bloating the article.

Hope you enjoy the library! Make sure to let me and anyone who finds this library know of anything cool you make down below!

Disclaimers/Side-notes

  • The LSTM networks are ‘trial’ because I do not have the experience to confirm if they are functional. All the math is right but I wasn’t able to get them to work well. Feel free to try them yourself!
  • I realized a bit too late that if I made the package structure based around a module script container instead of a folder, it would allow the user to just require() the latest version of the library without having to manually check the comment section or the Github for changes and replace their folder. Another major reason for me to redo my OOP implementation for the 3rd iteration of the library if enough people actually care about this one.
  • I do not plan to update this library much after this release other than to patch bugs. I already heavily dislike some core choices I made in development and also want to redo the OOP implementation as I have more experience to do it properly now.
288 Likes

Unfortunately, there is far too much of a difference between their structures. The first-gen networks exclusively use arrays and don’t even offer named inputs/outputs, which is required for the second-gen networks. Speaking of, I should probably add to the documentation as to how you actually save the networks…

9 Likes

Ah yes another great library! Although my main concern is that since this library is oop based, won’t the overall performance go down? (Previous library was a simple function call and passing the array to needed calls, which I am guessing is going to run faster than this).

6 Likes

Yes, this library does run noticeably slower. However, this difference isn’t big enough to really affect you as the benefits from the OOP structure far outweigh the losses. It’s like going from machine code to a compiled language. The machine code will be ridiculously fast, but you’ll save your sanity by using the compiled language.

8 Likes

Ah alright. Thank you for clarification! I will be testing around with this new library too and see what i can create with :smiley:

3 Likes

Finally it is done :smiley:
i must say thanks for making this library

3 Likes

Added network saving using the serialization functions from my OOP implementation. These networks produce far more characters than first-gen networks but it should be fine.
A network with 3 layers (as much as you will ever need) and 23 nodes per layer (ditto) barely makes it under 200k characters, the maximum for StringValues. If your network is somehow bigger than that and still functions on Roblox, you can just save it in the Datastore instead, which has a character limit of 4 million characters.

4 Likes

It may be a good idea to add a link from the old post to this new post, as the old post does not mention the new library, it only mentions that is obsolete.

3 Likes

Added the notice. I thought of doing it earlier but the interest for this library seems to have withered over time and it didn’t seem like it was worth doing.

3 Likes

How do i do this? Could you make a video or a Tutorial with pictures on how to. What do I do with the script? Put it in the seat? @Kironte

4 Likes

Sorry, @M0ADM0_DEV I think you misplaced your expectations a little too far this is a module which does the maths of a neural network from what I skimmed through watching videos. From there it’s your job to somehow compile it into a script that will fit your expectations of a self-driving car. For example here is a picture on the maths of backpropagation which allows the neural network to “learn” from this neat data science article I found.

And then here are the functions of the module which does the math for you from the documentation of the single network code:

--To backpropagate, you need to get the network's backpropagation.
local backProp = net:GetBackPropagator()
--...stuff
    backProp:CalculateCost(coords,correctAnswer)
--...more stuff
    if generation % numOfGenerationsBeforeLearning == 0 then
        backProp:Learn()
    end

Props to @Kironte for the amazing documentation really makes me want to follow suit for my own resource. Hopefully, I’ll get the time and opportunity to try it for my own game.

12 Likes

As @dthecoolest has said, this library is meant to simplify machine learning but in no way does it do everything for you. It takes some experience (or luck tbh) to implement the neural networks to an application in an effective way.
To start off, I suggest you look over the example code on the documentation website. The code there demonstrates what a functioning application looks like.

6 Likes

Nice Job! Very neat programming as well!

4 Likes

Finally, I was waiting for this. If someday I would need neuralnetworks, you will find me here.

4 Likes

Thanks for making this.
The problem I am having is that I can’t seem to figure out how to work with it? The examples on GitHub are randomly based (if I see that correctly), so how would I train a NN for say making a car that can dodge obstacles (or a NN with non-random inputs)? I know how to get readings of distance using RayCasts and also found this post of you @Kironte:

but:

  • How would I put those readings into the input nodes of the network?

  • How can I train the network while visually seeing the car move through the world? (If that’s the best way to do it)

  • What is a ScoreFunction and how do I use it?

  • You use a cubic function to test the network with in your example, what kind of functions are available for more than 2 inputs?

  • How can I save/run the NN once it is trained?

Sorry for the many questions, and thanks again for this amazing resource.

7 Likes

The 2 examples have examples of all of your questions other than saving. Incase you missed the API Documentation (next to the ‘Home’ tab at the top of the screen), each class’s functions are thoroughly documented.

You would either use the :SetInputValues() function on the neural network manually, or just call the network (i.e neuralNetwork() ) while passing the input values. The input values are just a dictionary where the key is equal to the input node’s name and the value is, well, the value you want it to have.


The network doesn’t have to be trained all at once. You can see in the examples how the training is done bit by bit, depending on your preference. I can’t give a proper example but imagine the network is just ran live: it receives a set of inputs and the outputs are used to adjust the car’s speed or steer, and the network is ran again a short while later. This takes a bit of practice and you will need to look up some extra material to learn how to do it properly.

As seen in example #2, the ScoreFunction is just a function that takes in the network as it’s only input, and outputs it’s numerical score. The score is treated differently depending on your genetic algorithm’s settings (i.e high score = bad or high score = good).

Literally anything. If you want the network to detect whether or not an enemy with the 5 given stats is a threat, you have 5 inputs. If you want the network to drive a car, you will most definitely have more than 2 inputs. The inputs are simply the bits of named information the network has access to. I chose the cubic function just because it is very simple and a good example that anyone can run.

For this, you use the :Save() function. When ran on the network, it will return a single string that is a representation of the network; this is your save ‘file’. This string can then be saved anywhere a string is accepted, like a datastore. Once you feel like working on the network again, just load up the string from the datastore and use the .newFromSave() constructor for the neural network. This will return the network that is saved within the string.


Hope this answers all your questions!

6 Likes

Hey, thanks for your answer. I think I have a better understanding of how to use the module now. I created a test place with a car that tries to run a course without running into walls:
NNTest.rbxl (104.5 KB)
But the car doesn’t really seem to learn anything at all when I let the algorithm process 100 generations of 30 population each. It just seems as if it randomly steers instead of actually learning. Am I doing something wrong here? Also :Save() doesn’t return a string, as stated in the documentation. It just returns a table (that I encoded to a string using HttpService:JSONEncode). Is it normal that I only get around 15 fps when running the algorithm?

Here’s the code I used which can also be found in the placefile (under ServerScriptService):

local HttpService = game:GetService("HttpService")
local Package = game:GetService("ReplicatedStorage").NNLibrary
local Base = require(Package.BaseRedirect)
local FeedforwardNetwork = require(Package.NeuralNetwork.FeedforwardNetwork)
local ParamEvo = require(Package.GeneticAlgorithm.ParamEvo)
local Momentum = require(Package.Optimizer.Momentum)

--If the training/testing is intensive, we will want to setup automatic wait() statements
--in order to avoid a timeout. This can be done with os.clock().
local clock = os.clock()

-- Function to activate scripts inside of car
local function ActivateScripts(Model)
	for _,Item in pairs(Model:GetDescendants()) do
		if Item:IsA("Script") then
			Item.Disabled = false
		end
	end
end

-- Function that casts rays from the car in five directions and returns the distances in a table
local function getRayDistances(car)
	-- Setup filterTable for rays to ignore (all parts of car)
	local filterTable = {}
	for _, v in pairs(car:GetDescendants()) do
		if v:IsA("BasePart") then
			table.insert(filterTable, v)
		end
	end
	-- Setup RayCastParams
	local rayCastParams = RaycastParams.new()
	rayCastParams.IgnoreWater = true
	rayCastParams.FilterType = Enum.RaycastFilterType.Blacklist
	rayCastParams.FilterDescendantsInstances = filterTable

	local bumperPos = car.Bumper.Position
	local FrontRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(0, 0, -100), rayCastParams)
	local FrontLeftRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(-100, 0, -100), rayCastParams)
	local FrontRightRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(100, 0, -100), rayCastParams)
	local LeftRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(-100, 0, 0), rayCastParams)
	local RightRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(100, 0, 0), rayCastParams)

	local pos1 = 1
	local pos2 = 1
	local pos3 = 1
	local pos4 = 1
	local pos5 = 1
	if FrontRay then
		pos1 = ((FrontRay.Position - bumperPos).Magnitude)/100
	elseif FrontLeftRay then
		pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude)/100
	elseif FrontRightRay then
		pos3 = ((FrontRightRay.Position - bumperPos).Magnitude)/100
	elseif LeftRay then
		pos4 = ((LeftRay.Position - bumperPos).Magnitude)/100
	elseif RightRay then
		pos5 = ((RightRay.Position - bumperPos).Magnitude)/100
	end
	--[[
	print("FrontRay: " .. tostring(pos1))
	print("FrontLeftRay: " .. tostring(pos2))
	print("FrontRightRay: " .. tostring(pos3))
	print("LeftRay: " .. tostring(pos4))
	print("RightRay: " .. tostring(pos5))]]

	return {front = pos1, frontLeft = pos2, frontRight = pos3, left = pos4, right = pos5}
end

-- Settings for genetic algorithm
local geneticSetting = {
	--The function that, when given the network, will return it's score.
	ScoreFunction = function(net)
		local car = game:GetService("ServerStorage").Car:Clone()
		car.Parent = workspace
		-- Setup car
		ActivateScripts(car)
		car.RemoteControl.MaxSpeed = 40
		car.RemoteControl.Throttle = 1
		
		local score = 0
		local bool = true
		for _, v in pairs(car:GetDescendants()) do
			if v:IsA("BasePart") then
				v.Touched:Connect(function(hit)
					if hit.Parent == workspace.Obstacles then
						bool = false
					end
				end)
			end
		end
		spawn(function()
			while bool do
				wait(0.5)
				score += 1
			end
			print("Exit score: " .. score)
		end)
		while bool do
			local distances = getRayDistances(car)
			local output = net(distances)
			print(output.steerDirection)
			car.LeftMotor.DesiredAngle = output.steerDirection
			car.RightMotor.DesiredAngle = output.steerDirection
			
			if os.clock()-clock >= 0.1 then
				clock = os.clock()
				wait()
			end
		end
		car:Destroy()
		return score
	end;
	--The function that runs when a generation is complete. It is given the genetic algorithm as input.
	PostFunction = function(geneticAlgo)
		local info = geneticAlgo:GetInfo()
		print("Generation "..info.Generation..", Best Score: "..info.BestScore/(100)^2*(100).."%")
	end;
}

local feedForwardSettings = {
	HiddenActivationName = "LeakyReLU";
	OutputActivationName = "Tanh";	-- We want an output from -1 to 1
}

-- Create a new network with 5 inputs, 2 layers with 4 nodes each and 1 output "steerDirection" (default settings)
local tempNet = FeedforwardNetwork.new({"front", "frontLeft", "frontRight", "left", "right"}, 2, 4, {"steerDirection"}, feedForwardSettings) --FeedforwardNetwork.newFromSave(game.ServerStorage.NetworkSave.Value)

-- Create ParanEvo with the tempNet template, population size (20) and settings
local geneticAlgo = ParamEvo.new(tempNet, 30, geneticSetting)

-- Run the algorithm one generation
geneticAlgo:ProcessGenerations(100)

-- Get the best network in the population
local net = geneticAlgo:GetBestNetwork()
local save = net:Save()
local stringSave = HttpService:JSONEncode(save)

print(stringSave)
game.ServerScriptService.NetworkSave.Value = stringSave
6 Likes

There are a few issues with the code.
The raycasting is incorrect as you’re giving the wrong vector as a direction. You also shouldn’t use if/else for checking the raycasts because if one of the rays doesn’t hit anything, all proceeding rays are ignored.
If the score is equal to the time the car exists, you should instead use os.clock() to time it instead of a while loop.
Running the car without a delay (other than the wait() every 0.1 seconds) results in way too much processing than necessary, causing the low framerate. This can be replaced with a single wait().
To speed up testing, the car should move and turn a bit faster.

Those are the only changes I made in the code to keep it as close to the original as possible. The one other thing I did was rename the front bumper of the car to “MainBumper” so you were raycasting from the correct thing. I wasn’t quite able to get the thing to turn in both directions correctly but that’s more due to the settings involved; figuring out the best network settings is as difficult as the math involved in the library.

local HttpService = game:GetService("HttpService")
local Package = game:GetService("ReplicatedStorage").NNLibrary
local Base = require(Package.BaseRedirect)
local FeedforwardNetwork = require(Package.NeuralNetwork.FeedforwardNetwork)
local ParamEvo = require(Package.GeneticAlgorithm.ParamEvo)
local Momentum = require(Package.Optimizer.Momentum)

--If the training/testing is intensive, we will want to setup automatic wait() statements
--in order to avoid a timeout. This can be done with os.clock().
local clock = os.clock()

-- Function to activate scripts inside of car
local function ActivateScripts(Model)
	for _,Item in pairs(Model:GetDescendants()) do
		if Item:IsA("Script") then
			Item.Disabled = false
		end
	end
end

-- Function that casts rays from the car in five directions and returns the distances in a table
local function getRayDistances(car)
	-- Setup filterTable for rays to ignore (all parts of car)
	local filterTable = {}
	for _, v in pairs(car:GetDescendants()) do
		if v:IsA("BasePart") then
			table.insert(filterTable, v)
		end
	end
	-- Setup RayCastParams
	local rayCastParams = RaycastParams.new()
	rayCastParams.IgnoreWater = true
	rayCastParams.FilterType = Enum.RaycastFilterType.Blacklist
	rayCastParams.FilterDescendantsInstances = {car}
	
	local distance = 50
	
	local bumper = car.MainBumper
	local bumperPos = bumper.Position
	local bumperCFrame = bumper.CFrame
	
	local function getDirection(vec)
		local start = bumperPos
		local finish = (bumperCFrame * CFrame.new(vec)).Position
		return (finish - start).Unit * distance
	end
	
	local FrontRay = workspace:Raycast(bumperPos, getDirection(Vector3.new(0,0,-1)), rayCastParams)
	local FrontLeftRay = workspace:Raycast(bumperPos, getDirection(Vector3.new(-1,0,-1)), rayCastParams)
	local FrontRightRay = workspace:Raycast(bumperPos, getDirection(Vector3.new(1,0,-1)), rayCastParams)
	local LeftRay = workspace:Raycast(bumperPos, getDirection(Vector3.new(-1,0,0)), rayCastParams)
	local RightRay = workspace:Raycast(bumperPos, getDirection(Vector3.new(1,0,0)), rayCastParams)

	local pos1 = 1
	local pos2 = 1
	local pos3 = 1
	local pos4 = 1
	local pos5 = 1
	if FrontRay then
		pos1 = ((FrontRay.Position - bumperPos).Magnitude) / distance
	end
	if FrontLeftRay then
		pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude) / distance
	end
	if FrontRightRay then
		pos3 = ((FrontRightRay.Position - bumperPos).Magnitude) / distance
	end
	if LeftRay then
		pos4 = ((LeftRay.Position - bumperPos).Magnitude) / distance
	end
	if RightRay then
		pos5 = ((RightRay.Position - bumperPos).Magnitude) / distance
	end
	
	--print("FrontRay: " .. tostring(pos1))
	--print("FrontLeftRay: " .. tostring(pos2),FrontLeftRay)
	--print("FrontRightRay: " .. tostring(pos3),FrontRightRay)
	--print("LeftRay: " .. tostring(pos4),LeftRay)
	--print("RightRay: " .. tostring(pos5),RightRay)

	return {front = pos1, frontLeft = pos2, frontRight = pos3, left = pos4, right = pos5}
end

-- Settings for genetic algorithm
local geneticSetting = {
	--The function that, when given the network, will return it's score.
	ScoreFunction = function(net)
		local car = game:GetService("ServerStorage").Car:Clone()
		car.Parent = workspace
		-- Setup car
		ActivateScripts(car)
		car.RemoteControl.MaxSpeed = 70
		car.RemoteControl.Torque = 100
		car.RemoteControl.Throttle = 1
		car.RemoteControl.TurnSpeed = 50
		
		local startTime = os.clock()
		
		local bool = true
		for _, v in pairs(car:GetDescendants()) do
			if v:IsA("BasePart") then
				v.Touched:Connect(function(hit)
					if hit.Parent == workspace.Obstacles then
						bool = false
					end
				end)
			end
		end
		while bool do
			local distances = getRayDistances(car)
			local output = net(distances)
			local steeringDir = output.steerDirection
			--print(output.steerDirection)
			car.LeftMotor.DesiredAngle = steeringDir
			car.RightMotor.DesiredAngle = steeringDir
			
			wait()
		end
		
		local score = os.clock() - startTime
		
		print("Exit score: "..math.floor(score*100)/100)
		
		car:Destroy()
		return score
	end;
	--The function that runs when a generation is complete. It is given the genetic algorithm as input.
	PostFunction = function(geneticAlgo)
		local info = geneticAlgo:GetInfo()
		print("Generation "..info.Generation..", Best Score: "..info.BestScore)
	end;
	PercentageToKill = 0.8;
	ParameterNoiseRange = 0.01;
	ParameterMutateRange = 0.1;
}

local feedForwardSettings = {
	HiddenActivationName = "LeakyReLU";
	OutputActivationName = "Tanh";
}

-- Create a new network with 5 inputs, 2 layers with 4 nodes each and 1 output "steerDirection" (default settings)
local tempNet = FeedforwardNetwork.new({"front", "frontLeft", "frontRight", "left", "right"}, 2, 4, {"steerDirection"}, feedForwardSettings) --FeedforwardNetwork.newFromSave(game.ServerStorage.NetworkSave.Value)

-- Create ParanEvo with the tempNet template, population size (20) and settings
local geneticAlgo = ParamEvo.new(tempNet, 30, geneticSetting)

-- Run the algorithm one generation
geneticAlgo:ProcessGenerations(100)

-- Get the best network in the population
local net = geneticAlgo:GetBestNetwork()
local save = net:Save()
local stringSave = HttpService:JSONEncode(save)

print(stringSave)
game.ServerScriptService.NetworkSave.Value = stringSave

--Total number of test runs.
local totalRuns = 0
--The number of runs that were deemed correct.
local wins = 0

--[[
-- Run algorithm 100 times
for i = totalRuns, 100 do
	local car = workspace.Car
	-- Setup filterTable (all parts of car)
	local filterTable = {}
	for _, v in pairs(car:GetDescendants()) do
		if v:IsA("BasePart") then
			table.insert(filterTable, v)
		end
	end
	-- Setup RayCastParams
	local rayCastParams = RaycastParams.new()
	rayCastParams.IgnoreWater = true
	rayCastParams.FilterType = Enum.RaycastFilterType.Blacklist
	rayCastParams.FilterDescendantsInstances = filterTable
	
	local bumperPos = car.Bumper.Position
	local FrontRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(0, 0, -100), rayCastParams)
	local FrontLeftRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(-100, 0, -100), rayCastParams)
	local FrontRightRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(100, 0, -100), rayCastParams)
	local LeftRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(-100, 0, 0), rayCastParams)
	local RightRay = workspace:Raycast(bumperPos, bumperPos + Vector3.new(100, 0, 0), rayCastParams)
	
	local pos1 = 1
	local pos2 = 1
	local pos3 = 1
	local pos4 = 1
	local pos5 = 1
	if FrontRay then
		pos1 = ((FrontRay.Position - bumperPos).Magnitude)/100
	elseif FrontLeftRay then
		pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude)/100
	elseif FrontRightRay then
		pos3 = ((FrontRightRay.Position - bumperPos).Magnitude)/100
	elseif LeftRay then
		pos4 = ((LeftRay.Position - bumperPos).Magnitude)/100
	elseif RightRay then
		pos5 = ((RightRay.Position - bumperPos).Magnitude)/100
	end
	
	net({front = pos1, frontLeft = pos2, frontRight = pos3, left = pos4, right = pos5})
	
	-- Automatic wait
	if os.clock()-clock >= 0.1 then
		clock = os.clock()
		wait()
		--print("Testing... "..(x+400)/(8).."%")
	end
end

print(wins/totalRuns*(100).."% correct!")]]

I think I’ll try my hand at making a self-driving car example similar to the one ScriptOn did. Would be a much better demo than the cubic function stuff.

8 Likes

Thanks for taking the time to look through my code. I’m going to try playing a bit with the fixed script you provided. A self driving car example (or something similarly complex) would be really useful. I’ll make sure to post here if I get something useful out of it.

3 Likes

Hi, I played around some more with the module and ran into a problem (again :grinning:).
This is a piece of the code I use for training and saving the algorithm:

local tempNet = FeedforwardNetwork.new({"front", "frontLeft", "frontRight", "left", "right"}, 2, 4, {"steerDirection"}, feedForwardSettings)
local geneticAlgo = ParamEvo.new(tempNet, 30, geneticSetting)		-- Create ParamEvo with the tempNet template, population size and settings
-- Run the algorithm x generations
geneticAlgo:ProcessGenerationsInBatch(15, 1, 1)

local save = tempNet:Save()
local stringSave = HttpService:JSONEncode(save)
game.ServerStorage.NetworkSave.Value = stringSave
print(stringSave)

I’m not sure though if this is the right way to save the algorithm, as it performs way worse than the saved algorithm’s best generation when calling it with:

local tempNet = FeedforwardNetwork.newFromSave(game.ServerStorage.NetworkSave.Value)

This is really frustrating as it just seems as if all progress from training the network is lost and it has to start all over again from scratch.
Btw I’m also using a new function I made for training a generation in batch (to speed up the process). I created a Pull Request on GitHub. This shouldn’t have anything to do with this issue but who knows…

5 Likes