SHOW:
|
|
- or go back to the newest paste.
1 | --@name NeuroEvolution Experiments | |
2 | --@author Szymekk | |
3 | --@shared | |
4 | ||
5 | if CLIENT then return end | |
6 | ||
7 | -------------------------------------- | |
8 | -- Neuron class | |
9 | -------------------------------------- | |
10 | ||
11 | local neuron = { } | |
12 | neuron.__index = neuron | |
13 | ||
14 | function neuron:init() | |
15 | self.inputs = { } | |
16 | self.weights = { } | |
17 | ||
18 | self.value = nil | |
19 | end | |
20 | ||
21 | function neuron:activationFunction(x) | |
22 | return 1 / (1 + math.exp(-x)) * 2 - 1 | |
23 | --return x | |
24 | end | |
25 | ||
26 | function neuron:addInput(n) | |
27 | self.inputs[#self.inputs + 1] = n | |
28 | self.weights[#self.inputs] = math.random() * 2 - 1 | |
29 | end | |
30 | ||
31 | function neuron:getOutput() | |
32 | if not self.value and #self.inputs > 0 then | |
33 | local sum = 0 | |
34 | ||
35 | for k, v in pairs(self.inputs) do | |
36 | sum = sum + v:getOutput() * self.weights[k] | |
37 | end | |
38 | ||
39 | self.value = neuron:activationFunction(sum) | |
40 | end | |
41 | ||
42 | return self.value | |
43 | end | |
44 | ||
45 | function neuron.new() | |
46 | local tbl = { } | |
47 | setmetatable(tbl, neuron) | |
48 | tbl:init() | |
49 | ||
50 | return tbl | |
51 | end | |
52 | ||
53 | -------------------------------------- | |
54 | -- Neural network class | |
55 | -------------------------------------- | |
56 | ||
57 | local network = { } | |
58 | network.__index = network | |
59 | ||
60 | function network:init() | |
61 | self.inputs = { } | |
62 | self.outputs = { } | |
63 | self.hiddenLayers = { } | |
64 | self.allNeurons = { } | |
65 | end | |
66 | ||
67 | function network:addInputs(count) | |
68 | for i = 1, count do | |
69 | local n = neuron.new() | |
70 | self.inputs[i] = n | |
71 | self.allNeurons[#self.allNeurons + 1] = n | |
72 | end | |
73 | end | |
74 | ||
75 | function network:addHiddenLayer(count) | |
76 | local layer = { } | |
77 | ||
78 | for i = 1, count do | |
79 | local n = neuron.new() | |
80 | layer[i] = n | |
81 | self.allNeurons[#self.allNeurons + 1] = n | |
82 | end | |
83 | ||
84 | self.hiddenLayers[#self.hiddenLayers + 1] = layer | |
85 | ||
86 | return layer | |
87 | end | |
88 | ||
89 | function network:addOutputs(count) | |
90 | for i = 1, count do | |
91 | local n = neuron.new() | |
92 | self.outputs[i] = n | |
93 | self.allNeurons[#self.allNeurons + 1] = n | |
94 | end | |
95 | end | |
96 | ||
97 | function network:setInputs(tbl) | |
98 | for k, v in pairs(self.inputs) do | |
99 | v.value = tbl[k] | |
100 | end | |
101 | end | |
102 | ||
103 | function network:resetValues() | |
104 | for k, v in pairs(self.allNeurons) do | |
105 | v.value = nil | |
106 | end | |
107 | end | |
108 | ||
109 | function network:connectNeurons() | |
110 | local allLayers = { self.inputs } | |
111 | for k, v in pairs(self.hiddenLayers) do | |
112 | allLayers[#allLayers + 1] = v | |
113 | end | |
114 | allLayers[#allLayers + 1] = self.outputs | |
115 | ||
116 | for i = 2, #allLayers do | |
117 | for k1, v1 in pairs(allLayers[i - 1]) do | |
118 | for k2, v2 in pairs(allLayers[i]) do | |
119 | v2:addInput(v1) | |
120 | end | |
121 | end | |
122 | end | |
123 | end | |
124 | ||
125 | function network:run(tbl) | |
126 | self:resetValues() | |
127 | self:setInputs(tbl) | |
128 | ||
129 | local outputValues = { } | |
130 | ||
131 | for k, v in pairs(self.outputs) do | |
132 | outputValues[k] = v:getOutput() | |
133 | end | |
134 | ||
135 | return outputValues | |
136 | end | |
137 | ||
138 | function network:dumpWeights() | |
139 | local weights = { } | |
140 | ||
141 | for k, v in pairs(self.allNeurons) do | |
142 | weights[k] = { } | |
143 | ||
144 | for k2, v2 in pairs(v.weights) do | |
145 | weights[k][k2] = v2 | |
146 | end | |
147 | end | |
148 | ||
149 | return weights | |
150 | end | |
151 | ||
152 | function network:loadWeights(weights) | |
153 | for k, v in pairs(weights) do | |
154 | for k2, v2 in pairs(v) do | |
155 | self.allNeurons[k].weights[k2] = v2 | |
156 | end | |
157 | end | |
158 | end | |
159 | ||
160 | function network.new() | |
161 | local tbl = { } | |
162 | setmetatable(tbl, network) | |
163 | tbl:init() | |
164 | ||
165 | return tbl | |
166 | end | |
167 | ||
168 | -------------------------------------- | |
169 | -- Evolution class | |
170 | -------------------------------------- | |
171 | ||
172 | local evolution = { } | |
173 | evolution.__index = evolution | |
174 | ||
175 | function evolution:init(net) | |
176 | self.net = net | |
177 | ||
178 | self.netsPerGeneration = 100 | |
179 | self.maxGenerations = 40 | |
180 | ||
181 | self.mutationNeurons = 5 | |
182 | self.mutationRate = 0.01 | |
183 | self.maxWeight = 5 | |
184 | end | |
185 | ||
186 | function evolution:start() | |
187 | return coroutine.create(function() | |
188 | self:yieldedEvolve() | |
189 | end) | |
190 | end | |
191 | ||
192 | function evolution:yieldedEvolve() | |
193 | self:onStart() | |
194 | ||
195 | local generation = 1 | |
196 | ||
197 | local lastBest | |
198 | local mutationRateFactor = 1 | |
199 | ||
200 | while true do | |
201 | print("Generation: " .. generation) | |
202 | ||
203 | self:onGenerationStart(generation) | |
204 | ||
205 | local nets = { } | |
206 | local weights = self.net:dumpWeights() | |
207 | ||
208 | for i = 1, self.netsPerGeneration do | |
209 | local updatedWeights = self:updateWeights(weights, mutationRateFactor) | |
210 | self.net:loadWeights(updatedWeights) | |
211 | ||
212 | local fitness = self:onUpdate(self.net) | |
213 | ||
214 | nets[i] = { fitness, updatedWeights } | |
215 | ||
216 | coroutine.yield() | |
217 | end | |
218 | ||
219 | coroutine.yield() | |
220 | ||
221 | local best = nets[1] | |
222 | for k, v in pairs(nets) do | |
223 | if v[1] < best[1] then | |
224 | best = v | |
225 | end | |
226 | end | |
227 | ||
228 | print("Best fitness: ", best[1]) | |
229 | ||
230 | if lastBest and lastBest[1] * 1 > best[1] then | |
231 | print("Not better than before") | |
232 | self.net:loadWeights(lastBest[2]) | |
233 | mutationRateFactor = mutationRateFactor + 0.5 | |
234 | continue | |
235 | end | |
236 | ||
237 | self.net:loadWeights(best[2]) | |
238 | ||
239 | lastBest = best | |
240 | ||
241 | mutationRateFactor = 1 | |
242 | ||
243 | self:onGenerationEnd(generation) | |
244 | ||
245 | if generation >= self.maxGenerations then | |
246 | break | |
247 | end | |
248 | ||
249 | generation = generation + 1 | |
250 | end | |
251 | ||
252 | self:onEnd() | |
253 | end | |
254 | ||
255 | -- Evolution begins, all preparation is done here | |
256 | function evolution:onStart() end | |
257 | ||
258 | -- Update function of current step, should return current fitness | |
259 | function evolution:onUpdate(net) end | |
260 | ||
261 | -- Before training individuals | |
262 | function evolution:onGenerationStart(n) end | |
263 | ||
264 | -- After best individual has been chosen | |
265 | function evolution:onGenerationEnd(n) end | |
266 | ||
267 | -- The network has evolved | |
268 | function evolution:onEnd() end | |
269 | ||
270 | function evolution:updateWeights(weights, factor) | |
271 | local updated = { } | |
272 | ||
273 | for k, v in pairs(weights) do | |
274 | updated[k] = { } | |
275 | ||
276 | for k2, v2 in pairs(v) do | |
277 | updated[k][k2] = v2 | |
278 | end | |
279 | end | |
280 | ||
281 | for i = 1, self.mutationNeurons do | |
282 | local k = math.random(1, #updated) | |
283 | ||
284 | if #updated[k] == 0 then | |
285 | i = i - 1 | |
286 | continue | |
287 | end | |
288 | ||
289 | local l = math.random(1, #updated[k]) | |
290 | updated[k][l] = math.clamp(updated[k][l] + (math.random() * 2 - 1) * self.mutationRate * factor, -self.maxWeight, self.maxWeight) | |
291 | end | |
292 | ||
293 | return updated | |
294 | end | |
295 | ||
296 | function evolution.new(net) | |
297 | local tbl = { } | |
298 | setmetatable(tbl, evolution) | |
299 | tbl:init(net) | |
300 | ||
301 | return tbl | |
302 | end | |
303 | ||
304 | -------------------------------------- | |
305 | -- Action | |
306 | -------------------------------------- | |
307 | ||
308 | chip():setMaterial("models/shiny") | |
309 | ||
310 | local net = network.new() | |
311 | net:addInputs(3) | |
312 | net:addHiddenLayer(3) | |
313 | --net:addHiddenLayer(3) | |
314 | net:addOutputs(1) | |
315 | net:connectNeurons() | |
316 | ||
317 | local evo = evolution.new(net) | |
318 | ||
319 | evo.netsPerGeneration = 10 | |
320 | evo.maxGenerations = 5000 | |
321 | ||
322 | evo.mutationNeurons = 8 | |
323 | evo.mutationRate = 0.2 | |
324 | evo.maxWeight = 5 | |
325 | ||
326 | local evoCoroutine = evo:start() | |
327 | ||
328 | local allPower = true | |
329 | ||
330 | function evo:onStart() | |
331 | print("Starting evolution") | |
332 | end | |
333 | ||
334 | function scaleInput(a) | |
335 | return a * 0.1 | |
336 | end | |
337 | ||
338 | function scaleOutput(a) | |
339 | return a * 10 | |
340 | end | |
341 | ||
342 | ||
343 | function evo:onGenerationStart() | |
344 | a, b = math.random(1, 10), math.random(1, 10) | |
345 | end | |
346 | ||
347 | startPos = chip():getPos() + Vector(0, 0, 15) | |
348 | ||
349 | color = Color(math.random(0, 255), math.random(0, 255), math.random(0, 255)) | |
350 | ||
351 | baseHolo = holograms.create(startPos, Angle(0, 0, 0), "models/sprops/cuboids/height12/size_2/cube_24x18x12.mdl", Vector(1, 1, 1)) | |
352 | baseHolo:setColor(color) | |
353 | traceHolo1 = holograms.create(startPos, Angle(0, 0, 0), "models/sprops/geometry/sphere_6.mdl", Vector(1, 1, 1)) | |
354 | traceHolo2 = holograms.create(startPos, Angle(0, 0, 0), "models/sprops/geometry/sphere_6.mdl", Vector(1, 1, 1)) | |
355 | traceHolo3 = holograms.create(startPos, Angle(0, 0, 0), "models/sprops/geometry/sphere_6.mdl", Vector(1, 1, 1)) | |
356 | ||
357 | local bestFitness = 0 | |
358 | ||
359 | function evo:onUpdate(net) | |
360 | allPower = false | |
361 | ||
362 | local position = startPos | |
363 | local angle = math.pi / 2 | |
364 | ||
365 | while true do | |
366 | position = position + Vector(math.cos(angle), math.sin(angle)) * 2 | |
367 | ||
368 | baseHolo:setPos(position) | |
369 | baseHolo:setAngles(Angle(0, angle / math.pi * 180 + 90, 0)) | |
370 | ||
371 | local tr1 = trace.trace(position, position + Vector(math.cos(angle), math.sin(angle)) * 100) | |
372 | local tr2 = trace.trace(position, position + Vector(math.cos(angle + math.pi / 4), math.sin(angle + math.pi / 4)) * 100) | |
373 | local tr3 = trace.trace(position, position + Vector(math.cos(angle - math.pi / 4), math.sin(angle - math.pi / 4)) * 100) | |
374 | ||
375 | traceHolo1:setPos(tr1.HitPos) | |
376 | traceHolo2:setPos(tr2.HitPos) | |
377 | traceHolo3:setPos(tr3.HitPos) | |
378 | ||
379 | if tr1.Fraction < 0.01 or tr2.Fraction < 0.01 or tr3.Fraction < 0.01 then | |
380 | break | |
381 | end | |
382 | ||
383 | local rawOutput = net:run({ scaleInput(tr1.Fraction), scaleInput(tr2.Fraction), scaleInput(tr3.Fraction) })[1] | |
384 | local scaledOutput = scaleOutput(rawOutput) | |
385 | ||
386 | --[[ | |
387 | if scaledOutput < -0.1 then | |
388 | angle = angle + 0.02 | |
389 | elseif scaledOutput > 0.1 then | |
390 | angle = angle - 0.02 | |
391 | end | |
392 | ]] | |
393 | ||
394 | angle = angle + scaledOutput * 0.1 | |
395 | ||
396 | coroutine.yield() | |
397 | end | |
398 | ||
399 | allPower = true | |
400 | ||
401 | local fitness = (position - startPos).y - (position - startPos).x / 2 | |
402 | ||
403 | if fitness > bestFitness then | |
404 | baseHolo:emitSound("resource/warning.wav") | |
405 | bestFitness = fitness | |
406 | end | |
407 | ||
408 | return fitness | |
409 | end | |
410 | ||
411 | function evo:onGenerationEnd() | |
412 | print("Generation ended") | |
413 | end | |
414 | ||
415 | function evo:onEnd() | |
416 | ||
417 | end | |
418 | ||
419 | hook.add("think", "", function() | |
420 | if allPower then | |
421 | while coroutine.status(evoCoroutine) ~= "dead" and quotaUsed() / quotaMax() < 0.1 do | |
422 | coroutine.resume(evoCoroutine) | |
423 | end | |
424 | else | |
425 | if coroutine.status(evoCoroutine) ~= "dead" and quotaUsed() / quotaMax() < 0.1 then | |
426 | coroutine.resume(evoCoroutine) | |
427 | end | |
428 | end | |
429 | end) | |
430 | ||
431 | --[[ | |
432 | chip():setMaterial("models/shiny") | |
433 | ||
434 | local net = network.new() | |
435 | net:addInputs(6) | |
436 | net:addHiddenLayer(3) | |
437 | net:addOutputs(3) | |
438 | net:connectNeurons() | |
439 | ||
440 | local evo = evolution.new(net) | |
441 | ||
442 | evo.netsPerGeneration = 15 | |
443 | evo.maxGenerations = 5000 | |
444 | ||
445 | evo.mutationNeurons = 8 | |
446 | evo.mutationRate = 0.05 | |
447 | evo.maxWeight = 5 | |
448 | ||
449 | local evoCoroutine = evo:start() | |
450 | ||
451 | local allPower = true | |
452 | ||
453 | function evo:onStart() | |
454 | print("Starting evolution") | |
455 | end | |
456 | ||
457 | function scaleInput(a) | |
458 | return a * 0.001 | |
459 | end | |
460 | ||
461 | function scaleOutput(a) | |
462 | return a * 1000 | |
463 | end | |
464 | ||
465 | local a, b | |
466 | ||
467 | function evo:onGenerationStart() | |
468 | a, b = math.random(1, 10), math.random(1, 10) | |
469 | end | |
470 | ||
471 | startPos = chip():getPos() + Vector(0, 0, 2) | |
472 | desiredPos = startPos + Vector(50, 30, 60) | |
473 | ||
474 | holo = holograms.create(desiredPos, Angle(0, 0, 0), "models/sprops/geometry/sphere_9.mdl", Vector(1, 1, 1)) | |
475 | ||
476 | function evo:onUpdate(net) | |
477 | allPower = false | |
478 | ||
479 | local fitness = 0 | |
480 | local nextEndTime = timer.realtime() + 2 | |
481 | ||
482 | chip():setPos(startPos) | |
483 | chip():setFrozen(false) | |
484 | chip():setVelocity(Vector(0, 0, 0)) | |
485 | chip():enableGravity(false) | |
486 | ||
487 | while timer.realtime() < nextEndTime do | |
488 | fitness = fitness + ((desiredPos - chip():getPos()):getLength() ^ 2) * timer.frametime() | |
489 | ||
490 | local relPos = chip():getPos() - desiredPos | |
491 | local vel = chip():getVelocity() | |
492 | local rawOutput = net:run({ scaleInput(relPos.x), scaleInput(relPos.y), scaleInput(relPos.z), scaleInput(vel.x), scaleInput(vel.y), scaleInput(vel.z) }) | |
493 | local scaledOutput = Vector(scaleOutput(rawOutput[1]), scaleOutput(rawOutput[2]), scaleOutput(rawOutput[3])) | |
494 | ||
495 | chip():applyForceCenter(scaledOutput) | |
496 | ||
497 | coroutine.yield() | |
498 | end | |
499 | ||
500 | allPower = true | |
501 | ||
502 | return fitness | |
503 | end | |
504 | ||
505 | function evo:onGenerationEnd() | |
506 | print("Generation ended") | |
507 | end | |
508 | ||
509 | function evo:onEnd() | |
510 | ||
511 | end | |
512 | ||
513 | hook.add("think", "", function() | |
514 | if allPower then | |
515 | while coroutine.status(evoCoroutine) ~= "dead" and quotaUsed() / quotaMax() < 0.6 do | |
516 | coroutine.resume(evoCoroutine) | |
517 | end | |
518 | else | |
519 | if coroutine.status(evoCoroutine) ~= "dead" and quotaUsed() / quotaMax() < 0.6 then | |
520 | coroutine.resume(evoCoroutine) | |
521 | end | |
522 | end | |
523 | end) | |
524 | ]] |