Neural Network Library 2.0

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.

11 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

Sorry for replying so late, finals week.

I see I forgot to make :Save() automatically convert the save into a string. 1 missing boolean right up; you can remove the :JSONEncode() now.
The reason the saved network is doing so bad is that you’re saving the template network you created before training. You’re saving the network that was never trained to begin with.

About your pull request; the point of it is to allow simultaneous testing, right? Well, GeneticAlgorithm already has this ability. When processing the generation, you can either supply a ScoreFunc when creating the GeneticAlgorithm that will score the networks for you, or, you can supply an array of scores whose indexes match up with the network indexes in the population, and whose values match up to the score that the network should have. Sorry for it not being evident, I will update the documentation to reflect this.

4 Likes

Hi, thanks for fixing the bug with saving. I don’t understand what you mean with how the GeneticAlgorithm already has that feature. The function I added can simultaneously test an entire generation with a specified interval between each member of that generation. That way the testing goes alot faster as the network doesn’t have to wait until one car finishes simulating to send the next one. What code can I use to implement this with the GeneticAlgorithm?

4 Likes

All you have to do is create the function that scores the given network (exactly the same as ScoreFunc), but you don’t supply it when creating GeneticAlgorithm. Instead, keep the ScoreFunc field empty, as it is by default. This will let the GeneticAlgorithm know that you are the one supplying it with the scores, and it is not to calculate them.
Then, you test all of the networks using your scoring function manually. Since you have control over what networks are scored and when, you can score them all at once, and then do :ProcessGeneration(scoreArray).

4 Likes

I tried implementing this, but I can’t figure out how I can get the GeneticAlgorithm to use the trained network. The cars seem to only change their behavior when the generation has fiinished.
This is part of my code:

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

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

local scoreTable = {}
local generations = 50	-- Number of generations to train network with
local firstRun = true
for _ = 0, generations do
	for index = 1, populationSize+1 do
		spawn(function()
			local startTime = os.clock()
			local clone = game:GetService("ServerStorage").Car:Clone()
			clone.RemoteControl.MaxSpeed = 200

			-- Parent to workspace and then setup Scripts of car
			clone.Parent = workspace

			local score = 0
			local bool = true
			local checkpointsHit = {}
			for _, v in pairs(clone:GetDescendants()) do
				if v:IsA("BasePart") and v.CanCollide == true then
					v.Touched:Connect(function(hit)
						if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
							bool = false
						elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then	-- Give extra points when car reaches checkpoint
							local numHit = tonumber(hit.Name)
							score += (numHit * 2)
							checkpointsHit[numHit] = hit
						end
					end)
				end
			end
			while bool do
				local distances = getRayDistances(clone)		-- Get Distances of rays
				local output
				if firstRun then
					output = tempNet(distances)				-- Get output of NN with input distances
				else
					local population = geneticAlgo:GetPopulation()
					local net = population[1].Network
					output = net(distances)				-- Get output of NN with input distances
				end

				-- Set steering direction to direction of NN
				clone.RemoteControl.SteerFloat = output.steerDirection
				-- Set speed of car
				--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300

				-- Check if this simulation has been running for longer than x seconds
				if os.clock() > startTime + 90 then
					score -= 40	-- Punish algorithm
					break
				end
				wait()
			end
			
			score += (os.clock() - startTime)/2		-- Increment score based on time alive (longer is better)
			print("Exit score: "..math.floor(score*100)/100)

			clone:Destroy()
			scoreTable[index] = score
		end)
		wait(1)
	end
	-- Wait until generation finished
	repeat
		wait(1)
	until #scoreTable >= populationSize
	
	geneticAlgo:ProcessGeneration(scoreTable)
	scoreTable = {}
	firstRun = false
end

Also, would this save the right (best) network?
local save = geneticAlgo:GetBestNetwork():Save()

4 Likes

You’re still using tempNet in your code. The template network should only be used as a template for GeneticAlgorithm creation and never again.

Why does one of your loops have “populationSize+1”? If you have 15 networks for your population, go past that will give you nil values.

Yes. Just make sure that you have the updated library and that should give you the string of the best network in the population.

4 Likes

Hi, I think I’m very close to actually succeeding here but there’s still one thing I can’t wrap my head around. I am now trying to use your method of training a generation simultaneously, but there are sometimes errors with the sorting of the scores.
This is the code I’m using:

local PhysicsService = game:GetService("PhysicsService")
local Package = game:GetService("ReplicatedStorage").NNLibrary
local FeedforwardNetwork = require(Package.NeuralNetwork.FeedforwardNetwork)
local ParamEvo = require(Package.GeneticAlgorithm.ParamEvo)

local clock = os.clock()
local collisionGroupName = "CarCollisionsDisabled"

-- Setup CollisionGroup for cars
PhysicsService:CreateCollisionGroup(collisionGroupName)
PhysicsService:CollisionGroupSetCollidable(collisionGroupName, collisionGroupName, false)

-- Function to activate scripts inside of car
local function setupCar(car)
	car.RemoteControl.Torque = 1000
	car.RemoteControl.Throttle = 5
	car.RemoteControl.TurnSpeed = 30
	for _, item in pairs(car:GetDescendants()) do
		if item:IsA("BasePart") or item:IsA("UnionOperation") then
			PhysicsService:SetPartCollisionGroup(item, collisionGroupName)
		end
	end

	-- Create ray visualizers
	local ray = Instance.new("Part")
	ray.CanCollide = false
	ray.Massless = true
	ray.CFrame = car.RaycastPart.CFrame
	ray.Size = Vector3.new(1, 1, 1)
	ray.Color = Color3.fromRGB(255, 0, 0)
	ray.Name = "frontRay"
	ray.Parent = car
	local weld = Instance.new("WeldConstraint")
	weld.Part0 = car.RaycastPart
	weld.Part1 = ray
	weld.Parent = ray
end

-- Setup car
local carSource = game:GetService("ServerStorage").Car
setupCar(carSource)

-- Function that casts rays from the car in five directions and returns the distances in a table
local function getRayDistances(car)
	-- Setup RayCastParams
	local rayCastParams = RaycastParams.new()
	rayCastParams.IgnoreWater = true
	rayCastParams.FilterType = Enum.RaycastFilterType.Whitelist
	rayCastParams.FilterDescendantsInstances = {workspace.Walls}

	local bumper = car.RaycastPart
	local bumperPos = bumper.Position
	local dist = 250

	local FrontRay = workspace:Raycast(bumperPos, bumper.CFrame.RightVector * dist, rayCastParams)														-- Cast ray for front
	local FrontLeftRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, bumper.CFrame.LookVector.Z) * dist, rayCastParams)	-- Cast ray to frontLeft
	local FrontRightRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, -bumper.CFrame.LookVector.Z) * dist, rayCastParams)	-- Cast ray to frontRight
	local LeftRay = workspace:Raycast(bumperPos, bumper.CFrame.LookVector * dist, rayCastParams)														-- Cast ray to left
	local RightRay = workspace:Raycast(bumperPos, -bumper.CFrame.LookVector * dist, rayCastParams)														-- Cast ray to right

	local pos1 = 1
	local pos2 = 1
	local pos3 = 1
	local pos4 = 1
	local pos5 = 1
	if FrontRay then
		pos1 = ((FrontRay.Position - bumperPos).Magnitude) / dist
	end
	if FrontLeftRay then
		pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude) / dist
	end
	if FrontRightRay then
		pos3 = ((FrontRightRay.Position - bumperPos).Magnitude) / dist
	end
	if LeftRay then
		pos4 = ((LeftRay.Position - bumperPos).Magnitude) / dist
	end
	if RightRay then
		pos5 = ((RightRay.Position - bumperPos).Magnitude) / dist
	end

	--local calc = pos1*dist
	--local ray = car.frontRay
	--ray.Size = Vector3.new(calc, 1, 1)
	--ray.Position = Vector3.new(bumperPos.X + calc/2, ray.Position.Y, ray.Position.Z)
	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 startTime = os.clock()
		local clone = game:GetService("ServerStorage").Car:Clone()
		clone.RemoteControl.MaxSpeed = 200

		-- Parent to workspace and then setup Scripts of car
		clone.Parent = workspace

		local score = 0
		local bool = true
		local checkpointsHit = {}
		for _, v in pairs(clone:GetDescendants()) do
			if v:IsA("BasePart") and v.CanCollide == true then
				v.Touched:Connect(function(hit)
					if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
						bool = false
					elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then	-- Give extra points when car reaches checkpoint
						local numHit = tonumber(hit.Name)
						score += (numHit * 2)
						checkpointsHit[numHit] = hit
					end
				end)
			end
		end
		while bool do
			local distances = getRayDistances(clone)		-- Get Distances of rays
			local output = net(distances)					-- Get output of NN with input distances

			-- Set steering direction to direction of NN
			clone.RemoteControl.SteerFloat = output.steerDirection
			-- Set speed of car
			--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300

			-- Check if this simulation has been running for longer than x seconds
			if os.clock() > startTime + 90 then
				score -= 40	-- Punish algorithm
				break
			end
			wait()
		end
		
		score += (os.clock() - startTime)/2		-- Increment score based on time alive (longer is better)
		print("Exit score: "..math.floor(score*100)/100)

		clone: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;

	HigherScoreBetter = true;
	
	PercentageToKill = 0.4;
	PercentageOfKilledToRandomlySpare = 0.1;
	PercentageOfBestParentToCrossover = 0.8;
	PercentageToMutate = 0.8;
	
	MutateBestNetwork = true;
	PercentageOfCrossedToMutate = 0.6;
	--NumberOfNodesToMutate = 3;
	--ParameterMutateRange = 3;
}

local feedForwardSettings = {
	HiddenActivationName = "ReLU";
	OutputActivationName = "Tanh";
	--Bias = 0;
    --LearningRate = 0.1;
    --RandomizeWeights = true;
}

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

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

local function roundDecimals(num, places)
    places = math.pow(10, places or 0)
    num = num * places
    if num >= 0 then 
        num = math.floor(num + 0.5) 
    else 
        num = math.ceil(num - 0.5) 
    end
    return num / places
end

local scoreTable = {}
local generations = 30	-- Number of generations to train network with
for _ = 1, generations do
	for index = 1, populationSize do
		local newThread = coroutine.create(function()
			local startTime = os.clock()
			local clone = game:GetService("ServerStorage").Car:Clone()
			clone.RemoteControl.MaxSpeed = 200
			-- Parent to workspace and then setup Scripts of car
			clone.Parent = workspace

			local score = 0
			local bool = true
			local checkpointsHit = {}
			for _, v in pairs(clone:GetDescendants()) do
				if v:IsA("BasePart") and v.CanCollide == true then
					v.Touched:Connect(function(hit)
						if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
							bool = false
						elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then	-- Give extra points when car reaches checkpoint
							local numHit = tonumber(hit.Name)
							if numHit and typeof(numHit) == "number" then
								score += (numHit * 2)
								checkpointsHit[numHit] = hit
							end
						end
					end)
				end
			end

			-- Setup Algorithm
			local net = geneticAlgo:GetPopulation()[index].Network
			while bool do
				local distances = getRayDistances(clone)				-- Get Distances of rays
				local output = net(distances)	-- Get output of NN with input distances

				-- Set steering direction to direction of NN
				clone.RemoteControl.SteerFloat = output.steerDirection
				-- Set speed of car
				--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300

				-- Check if this simulation has been running for longer than x seconds
				if os.clock() > startTime + 90 then
					score -= 40	-- Punish algorithm
					break
				end
				wait()
			end
			clone:Destroy()
			
			score = score + (os.clock() - startTime)/2		-- Increment score based on time alive (longer is better)

			--print("Exit score: " .. score)
			scoreTable[index] = roundDecimals(score, 2)
		end)
		coroutine.resume(newThread)
		wait(0.5)
	end
	-- Wait until generation finished
	while #scoreTable < populationSize do
		wait(1)
	end
	print(scoreTable)
	
	geneticAlgo:ProcessGeneration(scoreTable)
	scoreTable = {}
	print(scoreTable)
end

local save = geneticAlgo:GetBestNetwork():Save()
game.ServerStorage.NetworkSave.Value = save
print(save)

Here is the place file: Neural Network Test.rbxl (1.0 MB)

This is the error I’m getting:

ReplicatedStorage.NNLibrary.GeneticAlgorithm:195: attempt to compare nil and number  -  Server  -  GeneticAlgorithm:195

I think there is something wrong with my scoring system, but I’m not sure what. I tried debugging and found out that the ScoreTable sometimes returns “void” values? I don’t know how this is happening. Thanks.

4 Likes

The reason is due to the way array size is determined. Array size is equal to the greatest index in the array. If the 20th car hits a wall before other cars are finished, it will end the generation, leaving unfinished car scores as ‘void’, another word for nil (not sure why Roblox didn’t use ‘nil’ here but whatever).
Instead, you need to keep a counter for how many cars have finished, ‘numFinished’, and replace #scoreTable with it.
Whenever a car submits it’s score, just increment this counter by 1.
With this, 3 tiny lines of code are edited. Nothing else is changed.

local PhysicsService = game:GetService("PhysicsService")
local Package = game:GetService("ReplicatedStorage").NNLibrary
local FeedforwardNetwork = require(Package.NeuralNetwork.FeedforwardNetwork)
local ParamEvo = require(Package.GeneticAlgorithm.ParamEvo)

local clock = os.clock()
local collisionGroupName = "CarCollisionsDisabled"

-- Setup CollisionGroup for cars
PhysicsService:CreateCollisionGroup(collisionGroupName)
PhysicsService:CollisionGroupSetCollidable(collisionGroupName, collisionGroupName, false)

-- Function to activate scripts inside of car
local function setupCar(car)
	car.RemoteControl.Torque = 1000
	car.RemoteControl.Throttle = 5
	car.RemoteControl.TurnSpeed = 30
	for _, item in pairs(car:GetDescendants()) do
		if item:IsA("BasePart") or item:IsA("UnionOperation") then
			PhysicsService:SetPartCollisionGroup(item, collisionGroupName)
		end
	end

	-- Create ray visualizers
	local ray = Instance.new("Part")
	ray.CanCollide = false
	ray.Massless = true
	ray.CFrame = car.RaycastPart.CFrame
	ray.Size = Vector3.new(1, 1, 1)
	ray.Color = Color3.fromRGB(255, 0, 0)
	ray.Name = "frontRay"
	ray.Parent = car
	local weld = Instance.new("WeldConstraint")
	weld.Part0 = car.RaycastPart
	weld.Part1 = ray
	weld.Parent = ray
end

-- Setup car
local carSource = game:GetService("ServerStorage").Car
setupCar(carSource)

-- Function that casts rays from the car in five directions and returns the distances in a table
local function getRayDistances(car)
	-- Setup RayCastParams
	local rayCastParams = RaycastParams.new()
	rayCastParams.IgnoreWater = true
	rayCastParams.FilterType = Enum.RaycastFilterType.Whitelist
	rayCastParams.FilterDescendantsInstances = {workspace.Walls}

	local bumper = car.RaycastPart
	local bumperPos = bumper.Position
	local dist = 250

	local FrontRay = workspace:Raycast(bumperPos, bumper.CFrame.RightVector * dist, rayCastParams)														-- Cast ray for front
	local FrontLeftRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, bumper.CFrame.LookVector.Z) * dist, rayCastParams)	-- Cast ray to frontLeft
	local FrontRightRay = workspace:Raycast(bumperPos, Vector3.new(bumper.CFrame.RightVector.X, 0, -bumper.CFrame.LookVector.Z) * dist, rayCastParams)	-- Cast ray to frontRight
	local LeftRay = workspace:Raycast(bumperPos, bumper.CFrame.LookVector * dist, rayCastParams)														-- Cast ray to left
	local RightRay = workspace:Raycast(bumperPos, -bumper.CFrame.LookVector * dist, rayCastParams)														-- Cast ray to right

	local pos1 = 1
	local pos2 = 1
	local pos3 = 1
	local pos4 = 1
	local pos5 = 1
	if FrontRay then
		pos1 = ((FrontRay.Position - bumperPos).Magnitude) / dist
	end
	if FrontLeftRay then
		pos2 = ((FrontLeftRay.Position - bumperPos).Magnitude) / dist
	end
	if FrontRightRay then
		pos3 = ((FrontRightRay.Position - bumperPos).Magnitude) / dist
	end
	if LeftRay then
		pos4 = ((LeftRay.Position - bumperPos).Magnitude) / dist
	end
	if RightRay then
		pos5 = ((RightRay.Position - bumperPos).Magnitude) / dist
	end

	--local calc = pos1*dist
	--local ray = car.frontRay
	--ray.Size = Vector3.new(calc, 1, 1)
	--ray.Position = Vector3.new(bumperPos.X + calc/2, ray.Position.Y, ray.Position.Z)
	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 startTime = os.clock()
		local clone = game:GetService("ServerStorage").Car:Clone()
		clone.RemoteControl.MaxSpeed = 200

		-- Parent to workspace and then setup Scripts of car
		clone.Parent = workspace

		local score = 0
		local bool = true
		local checkpointsHit = {}
		for _, v in pairs(clone:GetDescendants()) do
			if v:IsA("BasePart") and v.CanCollide == true then
				v.Touched:Connect(function(hit)
					if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
						bool = false
					elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then	-- Give extra points when car reaches checkpoint
						local numHit = tonumber(hit.Name)
						score += (numHit * 2)
						checkpointsHit[numHit] = hit
					end
				end)
			end
		end
		while bool do
			local distances = getRayDistances(clone)		-- Get Distances of rays
			local output = net(distances)					-- Get output of NN with input distances

			-- Set steering direction to direction of NN
			clone.RemoteControl.SteerFloat = output.steerDirection
			-- Set speed of car
			--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300

			-- Check if this simulation has been running for longer than x seconds
			if os.clock() > startTime + 90 then
				score -= 40	-- Punish algorithm
				break
			end
			wait()
		end
		
		score += (os.clock() - startTime)/2		-- Increment score based on time alive (longer is better)
		print("Exit score: "..math.floor(score*100)/100)

		clone: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;

	HigherScoreBetter = true;
	
	PercentageToKill = 0.4;
	PercentageOfKilledToRandomlySpare = 0.1;
	PercentageOfBestParentToCrossover = 0.8;
	PercentageToMutate = 0.8;
	
	MutateBestNetwork = true;
	PercentageOfCrossedToMutate = 0.6;
	--NumberOfNodesToMutate = 3;
	--ParameterMutateRange = 3;
}

local feedForwardSettings = {
	HiddenActivationName = "ReLU";
	OutputActivationName = "Tanh";
	--Bias = 0;
    --LearningRate = 0.1;
    --RandomizeWeights = true;
}

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

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

local scoreTable = {}
local generations = 30	-- Number of generations to train network with
for _ = 1, generations do
	
	local numFinished = 0
	for index = 1, populationSize do
		local newThread = coroutine.create(function()
			local startTime = os.clock()
			local clone = game:GetService("ServerStorage").Car:Clone()
			clone.RemoteControl.MaxSpeed = 200
			-- Parent to workspace and then setup Scripts of car
			clone.Parent = workspace

			local score = 0
			local bool = true
			local checkpointsHit = {}
			for _, v in pairs(clone:GetDescendants()) do
				if v:IsA("BasePart") and v.CanCollide == true then
					v.Touched:Connect(function(hit)
						if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
							bool = false
						elseif hit.Parent == workspace.Checkpoints and not checkpointsHit[tonumber(hit.Name)] then	-- Give extra points when car reaches checkpoint
							local numHit = tonumber(hit.Name)
							if numHit and typeof(numHit) == "number" then
								score += (numHit * 2)
								checkpointsHit[numHit] = hit
							end
						end
					end)
				end
			end

			-- Setup Algorithm
			local net = geneticAlgo:GetPopulation()[index].Network
			while bool do
				local distances = getRayDistances(clone)				-- Get Distances of rays
				local output = net(distances)	-- Get output of NN with input distances

				-- Set steering direction to direction of NN
				clone.RemoteControl.SteerFloat = output.steerDirection
				-- Set speed of car
				--clone.RemoteControl.MaxSpeed = math.abs(output.speed) * 300

				-- Check if this simulation has been running for longer than x seconds
				if os.clock() > startTime + 90 then
					score -= 40	-- Punish algorithm
					break
				end
				wait()
			end
			clone:Destroy()
			
			score = score + (os.clock() - startTime)/2		-- Increment score based on time alive (longer is better)

			--print("Exit score: " .. score)
			scoreTable[index] = score
			
			numFinished += 1
		end)
		coroutine.resume(newThread)
		wait(0.5)
	end
	-- Wait until generation finished
	while numFinished < populationSize do
		wait(1)
	end
	print(scoreTable)
	
	geneticAlgo:ProcessGeneration(scoreTable)
	scoreTable = {}
	print(scoreTable)
end

local save = geneticAlgo:GetBestNetwork():Save()
game.ServerStorage.NetworkSave.Value = save
print(save)

--[[ * Code for running network
for i = 1, 20 do
	local clone = game:GetService("ServerStorage").Car:Clone()
	clone.RemoteControl.MaxSpeed = 200
	clone.Parent = workspace

	local bool = true
	for _, v in pairs(clone:GetDescendants()) do
		if v:IsA("BasePart") and v.CanCollide == true then
			v.Touched:Connect(function(hit)
				if hit.Parent.Parent == workspace.Walls then	-- Destroy car on hit of walls
					bool = false
				end
			end)
		end
	end
	while bool do
		local distances = getRayDistances(clone)	-- Get Distances of rays
		local output = tempNet(distances)			-- Get output of NN with input distances

		-- Set steering direction to direction of NN
		clone.RemoteControl.SteerFloat = output.steerDirection
		wait()
	end
	clone:Destroy()
end]]
7 Likes

Sorry to ask, but the sigmoid function shouldn‘t return a value between -1 and 1? And that ReLU is better because it returns a value between 0 and 1 or infinity.

Still, I just saw that it has his own website too, wich is amazing. If you want to do a short tutorial, consider to make one about “How to setup a own website with Mk Materials“ (No, really, I can‘t get it :smile:) But, thanks for put in much effort just to make our developpement easier, it‘s great tho have members in the community like you

P.S.: Why shouldn‘t we use TensorFlow? Isn‘t it compatible with Lua or is here one reason to use your module instead of TensorFlow‘s?

4 Likes

this basically generates the path for the AI like PathfindingService ?, does it detect any nearby objects and checks if it’s a player If not then that’s fine but if he knows then he is big :brain: and it just makes the NPC or AI Smart?

2 Likes