Machine Learning with Lua

A few months ago I learned a little bit about machine learning and neural networks, and since then I’ve wanted to make one. Today that’s what I did.


HOW IT WORKS

The goal of this algorithm eventually to teach the algorithm to spell a specific word.
I have an algorithm which uses random numbers to generate a word of arbitrary length. Each letter in the alphabet is given a weight, and then a “trainer” function is checking the word generated by the algorithm, and adjusting the weights accordingly.

If you’d like a more detailed explanation, here it is:

Detailed Description

I first needed to start with a list containing all of the letters in the alphabet:

letters = {
	["A"] = 0.5,
	["B"] = 0.5,
	["C"] = 0.5,
	["D"] = 0.5,
	["E"] = 0.5,
	["F"] = 0.5,
	["G"] = 0.5,
	["H"] = 0.5,
	["I"] = 0.5,
	["J"] = 0.5,
	["K"] = 0.5,
	["L"] = 0.5,
	["M"] = 0.5,
	["N"] = 0.5,
	["O"] = 0.5,
	["P"] = 0.5,
	["Q"] = 0.5,
	["R"] = 0.5,
	["S"] = 0.5,
	["T"] = 0.5,
	["U"] = 0.5,
	["V"] = 0.5,
	["W"] = 0.5,
	["X"] = 0.5,
	["Y"] = 0.5,
	["Z"] = 0.5
}

In this Dict, the key is the letter, and the value is the weight the letter holds.

I then have a function, generateWord, which uses random numbers to generate the word:

local function generateWord()
	local word = ""
	local avg = getAverageWeight() --Function which iterates through and finds the average weight
	if avg < 0 then
		avg = 0
	end
	for _ = 1,wordLength,1 do
		local l, s = nil
		local p = 1
		repeat
			local r = math.round(math.random(1, 26))
			l, s = getLetter(r) --[[Function which gets the letter at the index.
			This necessary because it's a dictionary. This function just
			iterates through until it reaches the given value, then returns it.]]
			p += 1
		until (s > 0) --0 is arbitrarily the tolerance
		word = word..l
	end
	return word
end

At this point, I have a function which calls generateWord, and adjusts the weight.

local function goNext(closer:number, doAdjustWeight:boolean)
	if doAdjustWeight then
		adjustWeights(wo, closer) --Adjusting the weights
	end
	wo = generateWord() --Generating the word
end

The goNext function is then repeatedly called in a function called run:

local function run(doAdjustWeight:boolean)
	local i = 1
	wo = ""
	while wo ~= testLetter:rep(wordLength) do
		local c = count(wo, testLetter) --count's how many times the letter appears in the word
		if c then
			goNext(c * -1, doAdjustWeight)
		else 
			goNext(1, doAdjustWeight)
		end
		i += 1
	end
	return i
end

Now, the important part; Adjusting weights:

local function adjustWeights(w:string, val:number)
	for i,v in pairs(letters) do --Iterating through the letters
		local adjust = rand:NextNumber(0, t * 2) --[[ The adjustment to the 
		weights is random, this is to allow for small "mistakes" made
		when extra letters are positively adjusted because they are
		are in words with letters I want. ]]
		adjust *= val --[[ Multiplying by 'val', which in this case is the
		the count of the letter being trained for. ]]
		if w:find(i) then
			letters[i] = v - adjust
		end
	end
end

The actual script I’m running has way more than this, and I have it incorporated with a UI, but if you’re interested in testing it and/or adapting it, here is the full script:

Full Script
local letters = nil
local function setLetters()
	letters = {
		["A"] = 0.5,
		["B"] = 0.5,
		["C"] = 0.5,
		["D"] = 0.5,
		["E"] = 0.5,
		["F"] = 0.5,
		["G"] = 0.5,
		["H"] = 0.5,
		["I"] = 0.5,
		["J"] = 0.5,
		["K"] = 0.5,
		["L"] = 0.5,
		["M"] = 0.5,
		["N"] = 0.5,
		["O"] = 0.5,
		["P"] = 0.5,
		["Q"] = 0.5,
		["R"] = 0.5,
		["S"] = 0.5,
		["T"] = 0.5,
		["U"] = 0.5,
		["V"] = 0.5,
		["W"] = 0.5,
		["X"] = 0.5,
		["Y"] = 0.5,
		["Z"] = 0.5
	}
end
local iter = 0 --# of iterations
local t = 0.1 --Tolerance of the AI
local runs = 100
local iterations = 500
local wordLength = 3
local testLetter = "T"

local RS = game:GetService("RunService")
local UI = script.Parent
local Main = UI:WaitForChild("Main")
local Word = Main:WaitForChild("Word")
local Index = Main:WaitForChild("Index")
local Good = Main:WaitForChild("Good")
local Bad = Main:WaitForChild("Bad")
local rand = Random.new(os.time())
local wo = ""

local function getAverageWeight()
	local total = 0
	for i,v in pairs(letters) do
		total += v
	end
	return (total / 26)
end

local function getLetter(r)
	local l = 1
	for i,v in pairs(letters) do
		if l == r then
			return i, v
		else
			l += 1
		end
	end
	return nil
end

local function generateWord()
	local word = ""
	local avg = getAverageWeight()
	if avg < 0 then
		avg = 0
	end
	for _ = 1,wordLength,1 do
		local l, s = nil
		local p = 1
		repeat
			local r = math.round(math.random(1, 26))
			l, s = getLetter(r)
			p += 1
		until (s > 0) --The weight of the letter is greater than the avg subtracted by the tolerance
		word = word..l
	end
	return word
end

local function adjustWeights(w:string, val:number)
	for i,v in pairs(letters) do
		local adjust = rand:NextNumber(0, t * 2)
		adjust *= val
		if w:find(i) then
			letters[i] = v - adjust
		--else
			--letters[i] = v + adjust
		end
	end
end

local function concat(a, sep:string) --DELETE
	local s = ""
	for i,v in pairs(a) do
		s = s..tostring(i).."-"..tostring(v)..sep
	end
	return s
end

local function count(s:string, ...:string) --DELETE
	local c = 0
	for i,v in pairs(s:split("")) do
		for _,a in pairs({...}) do
			if v == a then
				c += 1
			end
		end
	end
	if c ~= 0 then
		return c
	end
end

local function goNext(closer:number, doAdjustWeight:boolean)
	if doAdjustWeight then
		adjustWeights(wo, closer) --Adjusting the weights
	end
	--iter += 1
	wo = generateWord()
	Word.Text = wo
	--Index.Text = iter
end

local function run(doAdjustWeight:boolean)
	setLetters()
	--iter = 0
	--for i = 1,iterations,1 do
	--	local c = count(wo, testLetter)
	--	if c then
	--		goNext(c * -1, doAdjustWeight)
	--	else 
	--		goNext(1, doAdjustWeight)
	--	end
	--	if wo == testLetter:rep(wordLength) then
	--		--print("Got it in "..i)
	--		return i
	--	end
	--end
	--return iterations
	local i = 1
	wo = ""
	while wo ~= testLetter:rep(wordLength) do
		local c = count(wo, testLetter)
		if c then
			goNext(c * -1, doAdjustWeight)
		else 
			goNext(1, doAdjustWeight)
		end
		i += 1
	end
	return i
end

local function testAi()
	local nAvg = 0
	local start = os.clock()
	for i = 1,runs,1 do
		Index.Text = tostring(i)
		nAvg += run(false)
		--RS.RenderStepped:Wait()
	end
	local nDelta = os.clock() - start
	--print("Switch")
	local yAvg = 0
	start = os.clock()
	for i = 1,runs,1 do
		Index.Text = tostring(i)
		yAvg += run(true)
		--RS.RenderStepped:Wait()
	end
	local yDelta = os.clock() - start
	print("Avg w/o weight adjustment (random): "..tostring(nAvg / runs)..". Took "..nDelta.."ms")
	print("Avg w/ weight adjustment (AI): "..tostring(yAvg / runs)..". Took "..yDelta.."ms")
end
testAi()

RESULTS

As part of the full script (which can be found in the detailed description), I wrote a test function which runs the algorithm repeatedly, with and without weights. Basically, one time it goes through and attempts to reach the goal through random chance, and the other time it is weighting based on the trainer and reaching the goal that way.

Through this test function, I have found that the algorithm reaches the goal much more quickly, and much faster.

Examples

In this example, I am training the algorithm to reach “TT”. The testing function runs the algorithm 200 times, 100 with solely random sets of letters, and 100 with weighting. These are the results:

Avg w/o weight adjustment (random): 615.57. Took 0.1963841999968281s
Avg w/ weight adjustment (AI): 85.61. Took 0.05844290000095498s
  • The average is around where statistics dictate it should be. 26^2 = 676. ~60 over the average that it took the random chance.
  • The “AI” reached the goal in 7.19039831795 times fewer iterations.
  • The “AI” was 3.36027472958 times faster.

In this example, I am training the algorithm to reach “TTT”. The testing function runs the algorithm 200 times, 100 with solely random sets of letters, and 100 with weighting. These are the results:

Avg w/o weight adjustment (random): 19678.97. Took 7.989467200000945s
Avg w/ weight adjustment (AI): 92.33. Took 0.07685309999942547s
  • The average is around where statistics dictate it should be. 26^3 = 17576. ~2000 under the average that it took the random chance.
  • The “AI” reached the goal in 213.137333478 times fewer iterations.
  • The “AI” was 103.957643869 times faster.

I’d love to know what people think, and if/how I could make it better or more efficient.

3 Likes

That looks really interesting! Do you plan to expand this idea in the future? There are some amazing things like drone or ant simulations based on AI/machine learning.
It has a lot of potential for games, you could use it for mobs based on the movement and reactions of the player.