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.