Kód ellenőrzés AI-jal

Kutatást végzek arról, hogyan tudnám tovább növelni a fejlesztő kényelmét és hatékonyságát a támogató platformommal. Mivel 1 éve foglalkozok neurális hálózatokkal, úgy döntöttem hogy megpróbálom jósoltatni a hibákat és regressziót keresni egy mesterséges intelligencia betanításával. (Ugye az AI egyik részhalmaza a Machine learning, és annak egy részhalmaza a neurális háló, és ezen belül is hatalmas a terület (deep learning, LSTM, attention stb). Nos, ez utóbbira hivatkozok csak röviden mint AI).

Egyelőre Livescript-el (LS) kísérletezek. Ugye ez Javascript-re (JS) fordul. Megnéztem hogy van-e a fordítójának olyan kapcsolója, amely bytecode-ot vagy egyéb más strip-elt struktúrát tud kidobni. Éppen van:


npm install livescript

lsc --help

...
  -l, --lex       print the tokens the lexer produces
  -t, --tokens    print the tokens the rewriter produces
  -a, --ast       print the syntax tree the parser produces

Task nevű példaprogramom kódját konvertálom AI-jal megetethető formába (csak simán a script tag-ek közti részt kimásolom fájlba és beadom lsc-nek, és azt konvertálom tovább). Lássuk a 3 különböző struktúrát (csak részletet mutatok, ebből már látszik megfelelően):

LEX kimenete:


NEWLINE:\n ID:print ASSIGN:= PARAM(: )PARAM: ->
INDENT:4 ID:request { ID:table : STRNUM:"task" } ,
PARAM(:( ID:respond )PARAM:) ->
INDENT:4 IF ID:respond DOT:. ID:data ?
INDENT:4 ID:_out DOT:. ID:innerHTML ASSIGN:= STRNUM:""
NEWLINE:\n FOR: ID:x IN: ID:respond DOT:. ID:data DOT:. ID:text
INDENT:4 ID:_out DOT:. ID:innerHTML ASSIGN:+= ID:x +-:+ STRNUM:"<br>"

TOKENS kimenete:


NEWLINE:\n ID:print ASSIGN:= PARAM(: )PARAM: ->
INDENT:4 ID:request CALL(: { ID:table : STRNUM:"task" } ,
PARAM(:( ID:respond )PARAM:) ->
INDENT:4 IF ID:respond DOT:. ID:data ?
INDENT:4 ID:_out DOT:. ID:innerHTML ASSIGN:= STRNUM:""
NEWLINE:\n FOR: ID:x IN: ID:respond DOT:. ID:data DOT:. ID:text
INDENT:4 ID:_out DOT:. ID:innerHTML ASSIGN:+= ID:x +-:+ STRNUM:"<br>"

AST kimenete:


Assign  =
  Var print
  Fun
    Block
      Chain
        Var request
        Call
          Obj
            Prop
              Key table
              Literal "task"

AST tűnik használhatóbbnak. Szerintem meg tudom írni általánosra, hogy más compile output-ot is meg lehessen etetni az elemző megoldással más nyelvek esetén.

Írtam egy Ruby kódot, mellyel kikapom a fenti AST kimenetből az első szót (mely nem tartalmaz fejlesztő által megadott saját elnevezést) és tömbbe rendezem. Majd a tömb egyforma elemeit átszámozom 0-tól felfelé addig növelve az értéket, amíg van új kulcsszó. Kód alább:


a = `node_modules/livescript/bin/lsc --ast file`.split("\n")

b = a.map{|x| x.strip[/^[a-zA-Z]+/] }.compact

c = {}
i = 0

b.each {|x|
	if not c[x]
		c[x] = i
		i += 1
	end
}

d = b.map{|x| x = c[x] }

p b, d, i

A kimenete így néz ki:

["Block", "Assign", "Var", "Fun", "Var", "Block", "Return", "Var", "Assign", "Var", "Fun", "Block", "Chain", "Var", "Call", "Obj", "Prop", "Key", "Literal", "Fun", "Var", "Block", "If", "Existence", "Chain", "Var", "Index", "Key", "Block", "Assign", "Chain", "Var", "Index", "Key", "Literal", "For", "Var", "Chain", "Var", "Index", "Key", "Index", "Key", "Block", "Assign", "Chain", "Var", "Index", "Key", "Binary", "Var", "Literal", "Assign", "Var", "Fun", "Var", "Block", "If", "Binary", "Chain", "Var", "Index", "Key", "Literal", "Block", "Assign", "Var", "Chain", "Var", "Index", "Key", "If", "Binary", "Var", "Literal", "Block", "Chain", "Var", "Call", "Obj", "Prop", "Key", "Literal", "Prop", "Key", "Literal", "Fun", "Var", "Block", "Chain", "Var", "Call", "Block", "Chain", "Var", "Call", "Obj", "Prop", "Key", "Literal", "Prop", "Key", "Obj", "Prop", "Literal", "Var", "Fun", "Var", "Block", "Chain", "Var", "Call", "Assign", "Chain", "Var", "Index", "Key", "Literal", "Chain", "Var", "Call"]

[0, 1, 2, 3, 2, 0, 4, 2, 1, 2, 3, 0, 5, 2, 6, 7, 8, 9, 10, 3, 2, 0, 11, 12, 5, 2, 13, 9, 0, 1, 5, 2, 13, 9, 10, 14, 2, 5, 2, 13, 9, 13, 9, 0, 1, 5, 2, 13, 9, 15, 2, 10, 1, 2, 3, 2, 0, 11, 15, 5, 2, 13, 9, 10, 0, 1, 2, 5, 2, 13, 9, 11, 15, 2, 10, 0, 5, 2, 6, 7, 8, 9, 10, 8, 9, 10, 3, 2, 0, 5, 2, 6, 0, 5, 2, 6, 7, 8, 9, 10, 8, 9, 7, 8, 10, 2, 3, 2, 0, 5, 2, 6, 1, 5, 2, 13, 9, 10, 5, 2, 6]

16

Ez a legalsó szám azt mutatja, hogy hányféle paranccsal találkoztunk.

Egy RNN (Recursive Neural Network) idegrendszert próbálok majd betanítani LSTM-et használva (Long Short Term Memory). Kutatásaim alapján az az elsődleges sejtésem, hogy megfelelő tanítási halmaz után ki tudhat mutatni a fejlesztőnek anomáliákat a kódjával kapcsolatban, vagy egyéb, magasabb szinten képzett minta illesztési eltéréseket. Ez az a rész amit nem tudunk felfogni, mivel rendkívül nagy a kombinációs halmaz és ebből kell kiválasztani az eljárással lehetséges eseteket.

Persze kérdés a bemeneti, rejtett és kimeneti réteg nagysága. A kísérleteket egyforma értékkel fogom kezdeni (x = h = y = N), akár egy genetikai algoritmust alkalmazva, próbálgatva hogy milyen struktúra hozza a jobb eredményt. Viszont ehhez létre kell hoznom egy tanítási adatbázist. Nem lesz olyan kis meló megtalálni a működő kombinációt egy teljesen teszteletlen és ismeretlen paraméterekkel rendelkező adhoc saját készítésű adathalmazzal (még jó, mert ha ismerném milyen paraméter kell, akkor magát az adatbázist is tudnám tesztelni hogy megfelelő mennyiségű és minőségű-e a tanításhoz, 22-es csapdája). Itt rendkívül sok a lehetséges kutatási irány mind a machine learning algoritmusok tekintetében, mind az LSTM típust illetően (hogyan adagolom az adatot szekvenciálisan a fix méretű vektoroknak, vagyis mekkora szeletekben - illetve a "forget-gate"-et mikor aktiválom - például minden "block" parancs után, hogy csak kód blokkokat értelmezzen és ne legyen átfedés - de nem biztos hogy kell, mert ha elég intelligens, akkor úgyis látja).

A jelzést a hibáról a fejlesztő felé úgy tervezem megoldani, hogy az ACE editor (amit használok szerkesztő felülethez, remek munka C9-től) által tartalmazott program kódot kijelölöm vízszintesen a fejlesztőnek, így mentés után látja, hogy az AI szerint hol érdemes nézelődni anomáliák után. Valszeg egy pici gombot (link) teszek be balra a "diff" gombom mellé a szerkesztő felületen (valami marketinges "ai" névvel, még meglátom).

Egyelőre folytatom a munkát..

Hozzászólások

Közben azon gondolkodok, hogy lehet JS kód elemzéssel jobban járnék, mert egyrészt mindegyik dialektust erre fordítom a platformomon, másrészt github-ról tudnék lehúzni sok kódot, például NodeJS-re írtat.

Nem tudom pontosan mi a celod ezzel az elemzessel, de en szemely szerint kicsit mas iranyba mennek. Tapasztalatom szerint az AST-t nem celszeru kilapitani, ha mar egyszer van egy szep fa strukturad, a lapitas rovid tavon problemakhoz fog vezetni. Ennek a ket kodnak az AST-je kulonbozik ugyan, de ugyan arra a szamsorra fog fordulni:


if (condition) {
  a = 1;
}
b = 2;

// AST(?):

If
  Var
  Block
    Assign
      Var
      Literal
Assign
  Var
  Literal

if (condition) {
  a = 1;
  b = 2;
}

// AST(?):

If
  Var
  Block 
    Assign
      Var
      Literal
    Assign
      Var
      Literal

Mindketto kodja valami ilyesmi lesz az algoritmusoddal: 0 1 2 3 1 4 3 1 4
Ugye az AST nyelvtan fuggo, ugyhogy nem biztos hogy pont ezt kapod, de a lenyeg az, hogy fontos tudni a block vege hol van. Mivel ezt figyelmen kivul hagyod, lenyegesen kulonbozo kodokat tekinthet ugyan annak a neuralis halod.

Tapasztalatom szerint jobb megkozelites, ha az AST-t egy grafnak tekinted es azt reprezentalod valahogy. Peldaul a melyseget hozzacsapod prefixkent, vagy ilyesmi. Pl a fenti esetben ez ilyen kodsorokat fog eredmenyezni:
1. eset: 0-0 1-1 1-2 2-3 3-1 3-4 0-3 1-1 1-4
2. eset: 0-0 1-1 1-2 2-3 3-1 3-4 2-3 3-1 3-4

Tovabba (megint csak attol fuggoen, hogy mi a celod) fontos lehet, hogy milyen valtozokon tortenik a muvelet. Peldaul ha a neuralis haloddal fel akarod ismertetni a buborek rendezes implementaciojat vagy ilyesmi, akkor ugyan a valtozok neve nem szamit, de az igen, hogy ugyan az a valtozo van hivatkozva a masodik, nyolcadik meg tizedik utasitasban is. Ilyen esetben erdemes az AST-ben levo valtozo/fuggvenynev referenciakat feloldani, es egysegesiteni, igy ket kod osszehasonlitasakor az elnevezesek nem fognak szamitani. Itt figyelni kell arra is, hogy pl az "i" valtozo gyakori ciklusvaltozo ugyhogy a nevfeloldasnal szamit a scope.

Ha a nevfeloldas is megvan, akkor mar eleg kozel vagy ahhoz hogy legyen egy teljes control flowd a programrol (az exceptionok es egyeb implicit ugrasok hianyoznak).

Szerintem ez az, amivel csodakat lehetne tenni ha van egy jol betanitott neuralis halod :)

(automatizalt forraskod elemzessel foglalkozok; neuralis halokat csak tavolrol lattam)

Ez jó amit mondasz (még csak ma kezdtem el vizsgálni a lehetőségeket), a mélység hozzácsapása jó lehet. Viszont ezeken kívül sok egyéb szempontot is figyelembe kell vennem. Például azt, hogy belátható erőforrás elég legyen a NN tanításához. Ugyanis ha túl nagy részletességű adatot teszünk bele, akkor nagyobb struktúra kell és exponenciálisan nő a szükséges számítási kapacitás.

Kérdés még hogy mekkora tanító db-t tudok összeállítani. Nyilván minél nagyobb annál jobb. De ha túl részletgazdag, akkor sokkal több kell belőle, különben a tanítás nem lesz jó és under fitting vagy over fitting lesz. Nem egyszerű.

Képeknél szoktak 16 x 16 pixelt beadni fekete fehérként. Azért mert NN elég jól tanul kicsi infóból is. Mondhatjuk hogy a tanítás minőségéhez képesti eredmény nagy tud lenni. Ha jól van összeállítva, akkor meglepően jól következtet és extrapolál nem tanult dolgokra a kevés megtanultból. Persze itt a kevés nagyon relatív, sok mindentől függ.

Tehát a célom az, hogy minél lejjebb vigyen az input részletgazdagságát de maradjon legalább akkora, amely még elég nagy kombinációs teret jelent ahhoz, hogy eléggé tudjon tanulni az AI.

Az NN részéhez nem értek, de az AST egyszerűsíthető egy kis utófeldolgozással. Ha a mélységeket is számon tartod, akkor pl a Block elhagyható mert nem ad új információt, csak a parseolásnál segít egységbe rendezni az utasításokat. A while ciklus for ciklussá alakítható, az obj.property obj['property']-vé, stb.
Így egy "csomó", amúgy valid esettel nem kell foglalkozni.

A parseExpression csak kifejezéseket fog tudni parseolni a neve alapján, általános utasításokat nem. Pl. while(cond) {...} - ban csak a cond-ot.

Csak sokszor amire nem gondoljuk hogy információt hordoz, továbbra is hordozhat fontos összefüggéseket az AI számára. Ilyet lehet hogy nem veszek ki mert ha magas szinten kombinál és észrevesz kódolási szokásokat amely hibás, akkor jól jöhet.

Persze ez még teória. Gyakrolattá kell fordítani.

Kérdés az is hogy hogyan tanítom a hibás kódolásra. Ha nincsenek negatív mintáim, akkor azzal fogok próbálkozni, hogy a nagy átlag kódolási szokásához igazítom. Persze nem biztos hogy a nagy átlag jelenti a best practice-t, de a kezdőnek a semminél jobb indulás lehet.

Szerintem a melyseg helyett a blokkveget (meg az else kulcsszot) kellene belepakolni a tobbi kulcsszohoz hasonloan. A te modszereddel ugyanaz az algoritmus teljesen mas, ha beteszed egy elagazasba/ciklusba, vagy kiszervezed egy fuggvenybe. Igy viszont ugyanaz a szerkezete.

--
Any A.I. smart enough to pass a Turing test is smart enough to know to fail it. -Ian McDonald

Közben keresek parsert és úgy látom BabelJS Babylon parserját fogom használni JS-ből AST generáláshoz:


sudo npm install -g babylon

var b = require("babylon");
console.log(b.parse("var a = 5*6"));

Github repo:
https://github.com/babel/babel/tree/master/packages/babylon

Ahogy nézem, .parse() helyett jobban járok .parseExpression() függvénnyel. Kevesebb infó és gyorsabb parsolás:

...tries to parse a single Expression with performance in mind.

Egy "| grep type" meg is adja amit keresek.

Babylon AST spec-ből úgy látom hogy max 109 fajta típus van:

https://github.com/babel/babylon/blob/master/ast/spec.md

Kódjaim max 32 típust dobnak. Még meglátom hogy mekkora input réteget válasszak. Valszeg 1 inputot fogok 1 értéknek választani 0 vagy 1 értékkel, viszont az sem jó ha túl nagy az input réteg és a felső értékek nincsenek kihasználva. Részben mert több input nagyobb számítási kapacitást igényel, részben pedig azért mert eltolja a rejtett réteg súlyát.

Lehet hogy forgatni fogom körbe az inputot úgy, hogy maradékot képzek (N mod 32). Tehát a 32. értéket 0-nak veszem, 33-at 1-nek, 64-et megint nullának.

Megoldandó probléma még a hiba visszaképezése a forráskódra ami nem lesz egyszerű a sok absztrakció miatt. Azt tudni fogom hogy hányadik művelet típusról van szó és mi a neve (pl.: BreakStatement), de ennek a helyét meg kell mutatnom a kódban.