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
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.
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.
Nice Job! Very neat programming as well!
Finally, I was waiting for this. If someday I would need neuralnetworks, you will find me here.
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.
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!
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
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.
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.
Hi, I played around some more with the module and ran into a problem (again ).
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…
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.
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?
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).
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()
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.
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.
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]]
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 ) 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?
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 and it just makes the NPC or AI Smart?