A DQN Tensorflow.js megvalósítása a megerősítési tanulásban

„Gyakorold, amit tudsz, és segít tisztázni, amit most nem tudsz”
Rembrandt

Áttekintés

A Mnih és munkatársai által javasolt Deep Q-Network. A [2015] számos mélyen megerősítő tanulási algoritmus kiindulópontja és építési pontja. A felszíni egyszerűsége ellenére azonban néhány kihívást jelent a megvalósítás során, illetve a problémák megoldása során.

Ez a cikk a "Tensorflow.js"-t fogja használni megvalósítási technológiaként, a nyilvánvaló okok miatt:
- Minél több dolgot kell telepítenie, annál valószínűtlenebb, hogy erőfeszítéseket tesz a valódi algoritmus kódolására.
- A Javascript egy szép és népszerű nyelv, és nagyon intuitív azok számára, akik ismerik a C++-t, a C#-t, a Java-t stb.
- A demó megosztása az interneten egyszerű és egyértelmű.

Az algoritmus

Mint mondtam, az algoritmus önmagában meglehetősen egyszerű, az alábbi képen foglalható össze

Az alábbi lépések leírják, hogyan működik az algoritmus valójában:

1- Hozzon létre Q-Network és Target-Network
2- Töltse fel az Experience Buffert adatokkal a Q-Network segítségével
3- Ismételje meg a következő lépéseket megfelelő számú alkalommal
4- Szerezzen be egy véletlenszerű minta az Experience Bufferből
5- A minta bemenete a Q-hálózatba és a célhálózatba
6- Használja a célhálózat kimenetét a Q-Network betanításához (azaz a A célhálózat a Q-Network címkéinek szerepét fogja betölteni egy szabványos felügyelt tanulási forgatókönyvben.
7- Alkalmazza a feltárási/kihasználási stratégiát (pl.: Epsilon Greedy)
8- Ha az Exploration van kiválasztva, akkor generáljon véletlenszerű művelet, különben Ha a kihasználás van kiválasztva, akkor adja meg az aktuális állapotot a Q-Network-nek, és következtessen a műveletre a kimenetből.
9- Alkalmazza a műveletet a környezetre, megkapja a jutalmat és az új állapotot
10- Tárolja a régi állapotot, műveletet, jutalmat és új állapotot az élménypufferben (más néven Replay Memory)
11- Néhány epizód után másolja át a súlyokat a Q-Networkből a Target-Networkbe

Miért két hálózat?

Nyilvánvaló, hogy vajon miért van szükségünk két hálózatra?
Valójában a Q tanulási képlet a következő:

Észrevesszük, hogy ahhoz, hogy Q(S, A) elérje végső értékét, majdnem egyenlővé kell válnia R’ + γ max Q(S’,a) értékkel. Tehát ahhoz, hogy Q(S, A) R' + γ max Q(S',a) felé haladjon, a problémát valamilyen felügyelt tanulássá alakítjuk át, ahol becsüljük Q(S,A) és R' + γ max Q értéket. (S',a) lesz a 'címke'.
A Q(S,A) becsléséhez használt hálózat súlyai ​​a Loss függvény és a visszaterjesztés kombinációjával frissülnek.

De a max Q(S',a)-t nem ismerjük, egyszerűen megbecsüljük ugyanazon neurális hálózat segítségével, azonban ennek a hálózatnak a súlyai ​​folyamatosan változnak, ami különböző max Q(S',a) értékeket ad. minden iteráció.
Ez a Q(S, A) becslése egy mozgó célpont üldözésére.

A probléma megoldására létrehozunk egy azonos neurális hálózatot, amelyet célhálózatnak nevezünk, míg a Q(S,A) kiszámításához szükséges első hálózatot Online hálózatnak nevezzük. A Target hálózat célja, hogy egy ideig fix súlyokkal rendelkezzen, hogy stabilizálja a max Q(S’,a) számítását. Ezután néhány iteráció után a Target hálózat frissíti a súlyait az Online hálózat súlyainak egyszerű másolatával, amelyet az utóbbinak volt alkalma megtanulni.

Ügyeljen néhány megvalósítási buktatóra

  • Bemenetek normalizálása. Lehet, hogy nyilvánvaló, de a kezdők könnyen elfelejthetik ezt a fontos funkciót. Ennek nagy következményei lennének a kimenetre, mivel egyes bemenetek nagy értéktartományt, míg mások kisebb értéktartományt tartalmazhatnak. Ez nem jelenti azt, hogy az utóbbiak kevésbé fontosak az előbbieknél. Bármilyen számítási szempontból is, a nagy értékek összetörik a kicsiket. Tehát ha furcsa értékeket lát valamilyen nyilvánvaló bemenetnél, jó reflex lehet ellenőrizni, hogy a bemenetet normalizálták-e.
  • A két neurális hálózat szinkronizálása. A DQN két neurális hálózatot használ, az egyik „online”, ami azt jelenti, hogy minden iterációnál kiszámítja a Q értéket, a másik hálózat, a „target”, kiszámítja a Q értéket, de stabil súlykészleten alapul.
    A „cél” ” hálózat az R + γ max Q(St+1) kifejezést jelenti. Az „online” és a „cél” hálózatok kimenete a Mean Squared Error (MSE) kiszámítására, majd az „online” hálózat súlyainak frissítésére visszaszaporítással történik.
    Bizonyos számú iteráció esetén a Az "online" hálózatot át kell másolni a "cél" hálózatba, hogy a "cél" megkapja az "online" hálózat által tanult tapasztalatokat.
    Itt a fontos érték az iterációk megfelelő értékének beállítása, amelyeknél a súlyok másolják. A túl kicsi értékek instabillá teszik a rendszert, a túl nagy érték pedig képtelenné teszi a rendszert a tanulásra. Tehát ha a dolgok nem a megfelelő irányba haladnak, fontos ellenőrizni ezt a paramétert.
  • Az élménypuffer feltöltése. A folyamat elején fontos az élménypuffer feltöltése oly módon, hogy az ügynök interakcióba lép a környezettel, és tárolja az aktuális állapotot, műveletet, jutalmat, következő állapotot, terminális állapotot az élménypufferben, hogy amikor a tanulási folyamat generál véletlenszerű minták ebből az élményből.
  • Mintavételi méret. Jó gyakorlat, ha a mintavételi méretet a tapasztalati puffer méretének a felére helyezzük, mert kis mintavételezés esetén a tapasztalatból való tanulás nem lesz hatékony, míg a nagy mintavétel (a maximum körül) olyan torzítást okoz a tanulásban, amelyet nem kíván. Például, ha önvezető autója van, és mivel az utak legtöbbször egyenesek, az egyenes út tapasztalataiból túlzott mintavételezés arra készteti az MI-t, hogy minimálisra csökkentse a kanyarodás hatását.

CartPole példa

A DQN klasszikus megvalósítása a CartPole játék, ahol az AI ügynöknek úgy kell mozgatnia a kocsit, hogy a rúd függőlegesen maradjon.
A rúd nem eshet 30°-nál nagyobb szögben a függőlegeshez képest, és a kocsi nem érheti el a széleket. A nyeremény akkor érhető el, amikor az AI-ügynök 300 jutalmat gyűjt össze.

A játék valódi megvalósítását a https://rl-lab.com/cartpole/ oldalon tekintheti meg

Az alábbiakban az algoritmus fő részének kódrészletei találhatók. Tartalmazza a fő hurkot, a playStep-et és a betanítási bázist a kötegmintán.

A train() metódus tartalmazza a képzés fő hurkát. Először inicializálja az ügynököt, és megtölti a visszajátszási memóriapuffert tapasztalattal, majd elkezdi a hurkolást: edzés, majd egy lépés a lejátszás.

train(agent, maxIterations, batchSize, gamma, learningRate, syncEveryFrames) {
  // init the agent
  agent.reset();
  // at first fill the replay memory with experience by playing
  // without any training
  for (let i = 0; i < agent.replayBufferSize; ++i) {
    agent.playStep();
  }
  // create optimizer for the training  
  const optimizer = tf.train.adam(learningRate);
  // loop until all iterations are done
  let counter = maxIterations;
  while (counter > 0) {
    // train the agent by extracting samples from replay memory and 
    // use them as input for the DQN   
    agent.trainOnReplayBatch(batchSize, gamma, optimizer);
    // after each training, play one step of the game    
    agent.playStep();
    if (agent.frameCount % syncEveryFrames === 0) {
       copyWeights(agent.targetNN, agent.onlineNN);
    }
    counter--;
  }
}

A playStep() metódus arra készteti az ügynököt, hogy csak egy lépésig játssza a játékot. Epszilon mohó házirendet használ, amely arra készteti, hogy az idő egy részében véletlenszerű műveletet válasszon, míg a többi időben azt a műveletet, amely a maximális Q értéket adja vissza.

// play one step of the game and store result in replay memory
playStep() {
...
  let action;
  let actionIndex = 0;
  const state = this.game.getState();
  if (Math.random() < this.epsilon) {
    // Pick an action at random.
    actionIndex = this.getRandomAction();
    action = ALL_ACTIONS[actionIndex];
  } else {
  // Greedily pick an action based on online DQN output.
  // use tf.tidy() to clean up after finishing
     tf.tidy(() => {
       const stateTensor =  getStateTensor(state);
       actionIndex = this.onlineNN.predict(stateTensor)
                              .argMax(-1).dataSync()[0];
       action = ALL_ACTIONS[actionIndex];
     });
  }
  // play one step of the game and get the results
  const result = this.game.step(action);
  // store experience in replay Memory
  this.replayMemory.append([state, actionIndex, result.reward,
                                 result.state, result.status]);
  // if terminal state, reset the game
  if (r.status != 0) {this.reset();}
}

A trainOnReplayBatch() kivon egy mintát a replayMemory-ból, és bemenetként használja a DQN-be, majd az átlagos négyzetes hibát veszteségfüggvényként használja a súlyok frissítéséhez.

// This method is used to train the online network by using batch
// samples from the memory replay
trainOnReplayBatch(batchSize, gamma, optimizer){
// Get a batch of examples from the replay buffer.
const batch = this.replayMemory.sample(batchSize);
//define the loss function
const lossFunction = () => tf.tidy(() => {
  // example[0] is the state
  // example[1] is the action
  // example[2] is the reward
  // example[3] is the next state
  // example[4] indicates if this is a terminal state
  const stateTensor = getStateTensor(batch.map(
                                     example =>   example[0]));
  const actionTensor = tf.tensor1d(batch.map(
                                   example => example[1]), 'int32');
  
  // compute Q value of the current state
  // note that we use apply() instead of predict
  // because apply() allow access to the gradient
  const online = this.onlineNN.apply(stateTensor, {training: true});
  const oneHot = tf.oneHot(actionTensor, NUM_ACTIONS);
  const qs = online.mul(oneHot).sum(-1);
  
  // compute the Q value of the next state. 
  // it is R if the next state is terminal
  // R + max Q(next_state) if the next state is not terminal
  const rewardTensor = tf.tensor1d(batch.map(
                                         example => example[2]));
  const nextStateTensor = getStateTensor(batch.map(
                                         example => example[3]));
  const nextMaxQTensor =  this.targetNN.predict(
                                         nextStateTensor).max(-1);
  const status = tf.tensor1d(batch.map(
                          example => example[4])).asType('float32');
  // if terminal state then status = 1 => doneMask = 0
  // if not terminal then status = 0 => doneMask = 1
  // this will make nextMaxQTensor.mul(doneMask) either 0 or not
  const doneMask = tf.scalar(1).sub(status);
  const targetQs = rewardTensor.add(
                           nextMaxQTensor.mul(doneMask).mul(gamma));
  
  // define the mean square error between Q value of current state
  // and target Q value
  const mse = tf.losses.meanSquaredError(targetQs, qs);
  return mse;
});
// Calculate the gradients of the loss function with respect 
// to the weights of the online DQN.
const grads = tf.variableGrads(lossFunction);
// Use the gradients to update the online DQN's weights.optimizer.applyGradients(grads.grads);
tf.dispose(grads);
}

Végül itt egy videó, amelyben a DQN ügynök játszik, és megnyeri a CartPole játékot, elérve a 300-as jutalmat.

Következtetés

A DQN az egyik legnépszerűbb Deep Reforcement Learning algoritmus. Először ért el emberfeletti szintű teljesítményt egy Atari játékon.
Az évek múlásával számos fejlesztésen esett át, így még mindig a legjobban teljesítő DRL-ügynökök közé tartozik.