Académique Documents
Professionnel Documents
Culture Documents
Nicolas Jouandeau
n@ai.up8.edu
mars 2019
1. Intelligence Artificielle
Table des matières
1 Introduction 3
3 Minimax et Alpha-beta 24
3.1 Minimax . . . . . . . . . . . . . . . 24
3.2 Negamax . . . . . . . . . . . . . . . 25
3.3 Alpha-beta (αβ) . . . . . . . . . . . . . 25
3.4 Minimax Tree Optimization (MMTO) . . . . . . . 26
3.5 Application aux arbres ternaires . . . . . . . . . 26
3.6 Application aux Dames . . . . . . . . . . . 28
3.7 Application aux Echecs . . . . . . . . . . . 28
3.8 Application à Breakthrough . . . . . . . . . . 28
3.9 Application à Kamisado . . . . . . . . . . . 28
5 Recherche enveloppée 40
5.1 Nested Monte Carlo Search (NMCS) . . . . . . . 40
5.2 Nested Rollout Policy Adaptation (NRPA) . . . . . . 43
6 Preuve 47
6.1 Proof Number Search (PNS) . . . . . . . . . . 47
6.2 Proof Number 2 Search (PN2S) . . . . . . . . . 47
6.3 Depth-First PNS (DFPNS) . . . . . . . . . . 47
6.4 Problème GHI . . . . . . . . . . . . . . 47
6.5 Variantes de PNS . . . . . . . . . . . . . 47
6.6 Application à Breakthrough . . . . . . . . . . 47
1
7 Parallélisation 48
7.1 A la racine (Root Parallel) . . . . . . . . . . 48
7.2 Aux feuilles (Leaf Parallel) . . . . . . . . . . 48
7.3 Dans l’arbre (Tree Parallel) . . . . . . . . . . 48
7.4 Parallel NMCS . . . . . . . . . . . . . 48
7.5 Distributed NRPA . . . . . . . . . . . . 48
7.6 PNS Randomisé (RP-PNS) . . . . . . . . . . 48
7.7 Parallel DFPNS . . . . . . . . . . . . . 48
7.8 Job-Level PNS (JLPNS) . . . . . . . . . . . 48
7.9 Parallel PN2S (PPN2S) . . . . . . . . . . . 48
7.10 Application au Morpion Solitaire . . . . . . . . 48
9 Apprentissage 50
9.1 Processus de décision Markovien . . . . . . . . 50
9.2 Heuristique et compromis . . . . . . . . . . 52
9.3 Modes de progression d’apprentissage . . . . . . . 53
9.4 Programmation dynamique . . . . . . . . . . 57
9.5 Application à Grid-world . . . . . . . . . . . 59
9.6 Apprentissage par renforcement . . . . . . . . . 66
9.7 Apprentissage profond . . . . . . . . . . . 70
9.8 Application aux jeux Atari 2600 . . . . . . . . . 70
9.9 Application à StarCraft-II . . . . . . . . . . 71
9.10 Application à Dota2 . . . . . . . . . . . . 113
9.11 Application au Blackjack . . . . . . . . . . . 114
10 Annexes 125
10.1 DataFrame de pandas en Python . . . . . . . . 125
2
1 Introduction
L’IA est un domaine identifié par les questions relatives à la compréhension et
la construction d’entités intelligentes ; la notion d’intelligence est associée au
processus de la pensée dont l’objectif est de percevoir, comprendre, prévoir,
manipuler un monde plus étendu que soi-même ; l’objectif de l’IA est de systé-
matiser et d’automatiser les tâches intellectuelles.
Les principaux domaines d’application concernés sont regroupés dans les sys-
tèmes décisionnels dont : 1) la planification, 2) la programmation d’agents et
de systèmes autonomes, 3) les jeux 4 , 4) le diagnostic, 6) la robotique et 7) la
compréhension des langages ; le présent document se focalise sur l’IA pour les
jeux.
3
jeux sont IJCAI 5 , ECAI 6 , AAAI 7 , SIGAI 8 , AISB 9 , AI magazine 10 , RFIA 11 ,
GDC 12 , CG 13 , ACG 14 , ICGA 15 , TCIAIG 16 , CIG 17 , CGW 18 , TAAI 19 , GIGA 20 .
4
2 Résolution par exploration
2.1 Recherche en profondeur
La recherche en profondeur est en pratique réalisée à profondeur limitée (i.e.
DLS, pour Depth Limited Search 21 ) ; Alg. 1 et 2 présentent une recherche DLS ;
dans ces deux algorithmes, la recherche s’arrête quand la profondeur maximale
est atteinte ou quand une solution est trouvée.
Les fonctions utiles sont les suivantes :
— h retourne la valeur heuristique de la position courante ;
— nextMoves retourne la liste des coups possibles à partir de la position
courante ;
— play joue un coup ;
— unplay déjoue un coup.
21. Sans la valeur limite DLS_MAX_DEPTH, DLS est une recherche en profondeur d’abord,
abrégée DFS pour Depth-First Search.
5
Dans Alg. 1, la position courante, notée current_s, est maintenue en variable
globale, qui implique l’utilisation des fonctions play et unplay pour maintenir
une position cohérente ; on utilise une table de hashage H pour connaître les
positions précédemment évaluées à plus petite profondeur ; la ligne 6 vérifie si
la position courante est dans H ou si elle a déjà été évaluée à une profondeur
plus grande (ci c’est le cas, il faut refaire la recherche en espérant trouver une
solution ; si la position courante a déjà été évaluée à plus petite profondeur, il
est inutile de poursuivre la recherche à partir de cette position) ; pour résoudre
un problème, on fixe current_s et on appelle DLS(0).
1 fonction DLS ( d ) :
2 if solved then return ;
3 H[current_s] ← d ;
4 if h (best_s) > h (current_s) then
5 best_s ← current_s ;
6 if current_s == W IN then solved ← true ; return ;
7 if current_s == LOST or d == DLS_MAX_DEPTH then return ;
8 M ← nextMoves ( ) ;
9 for each m in M do
10 play ( m ) ;
11 if current_s ∈/ H or H[current_s] > d then
12 DLS ( d + 1 ) ;
13 unplay ( m ) ;
14 if solved then break ;
6
Dans Alg. 2, la position courante, notée s, est une variable locale ; s0 est la
position suivante de s par application du coup m ; avoir une variable locale
permet d’éviter de déjouer les coups ; pour résoudre un problème à partir d’une
position p, on appelle DLS(p,0).
1 fonction DLS ( s , d ) :
2 if solved then return ;
3 H[s] ← d ;
4 if h (best_s) > h (s) then
5 best_s ← s ;
6 if s == W IN then solved ← true ; return ;
7 if s == LOST or d == DLS_MAX_DEPTH then return ;
8 M ← nextMoves ( s ) ;
9 for each m in M do
10 s0 ← play ( m , s) ;
11 if s0 ∈/ H or H[s0 ] > d then
12 DLS ( s0 , d + 1 ) ;
13 if solved then break ;
7
Alg. 3 ajoute la gestion de la solution dans le tableau solution, qui stocke les
commandes utilisées ; le tableau est de taille DLS_MAX_DEPTH ; la solution est
de longueur solution_size ; la taille de la solution solution_size est initialisée
à zéro ; quand la recherche en profondeur trouve une solution, solution_size
prend pour valeur la taille de la solution.
1 fonction DLS ( s , d ) :
2 if solution_size 6= 0 then return ;
3 H[s] ← d ;
4 if h (best_s) > h (s) then
5 best_s ← s ;
6 if s == W IN then
7 solution_size ← d ;
8 return ;
9 if s == LOST or d == DLS_MAX_DEPTH then return ;
10 M ← nextMoves ( s ) ;
11 for each m in M do
12 s0 ← play ( m , s) ;
13 if s0 ∈/ H or H[s0 ] > d then
14 solution [ d ] ← m ;
15 DLS ( s0 , d + 1 ) ;
16 if solution_size 6= 0 then break ;
1 DLS_MAX_DEPTH ← 10 ;
2 H←∅;
3 solution_size ← 0 ;
4 best_s ← p ;
5 DLS ( p, 0 ) ;
8
2.2 Recherche en largeur
La recherche en largeur est en pratique également réalisée à profondeur limitée
(i.e. BFS, pour Breadth First Search) ; Alg. 5 présente une recherche BFS ; à
chaque appel, une liste de positions L devient la liste des positions suivantes S,
desquelles sont exclues les positions précédemment évaluées via H.
1 fonction BFS ( L , d ) :
2 if solution_size 6= 0 then return ;
3 S←∅;
4 for each s in L do
5 M ← nextMoves ( s ) ;
6 for each m in M do
7 s0 ← play ( m , s ) ;
8 S ← S + (s0 , m) ;
9 L0 ← ∅ ;
10 for each (s, m) in S do
11 if s ∈/ H or H[s] > d then
12 solution[d] ← m ;
13 H[s] ← d ;
14 if h (best_s) > h (s) then best_s ← s ;
15 if s == W IN then solution_size ← d ; return ;
16 if s == LOST or d == BFS_MAX_DEPTH then return ;
17 L0 ← L0 + s ;
18 BFS ( L0 , d + 1 ) ;
Alg. 5: Recherche en largeur, à profondeur limitée ; la variable
BFS_MAX_DEPTH définit la profondeur maximale.
Une recherche correspond à BFS({p},0), avec {p} une liste contenant une posi-
tion p correspondant au problème à résoudre.
9
2.3 Recherche à profondeur itérative
La recherche à profondeur itérative, présentée dans Alg. 6, est avant tout une
recherche en profondeur ; c’est une recherche à profondeur limitée dont on aug-
mente itérativement la profondeur limite (i.e. IDS, pour Iterative Deepening
Search 22 ) ; la recherche résultante est équivalente à la recherche en largeur, ce
qui permet d’obtenir le chemin optimal vers la solution, tout en utilisant moins
d’allocation mémoire (car n’utilisant pas de liste de positions en cours et pas de
relations entre les positions pour obtenir la solution).
1 fonction IDS ( p ) :
2 solution_size ← 0 ;
3 best_s ← p ;
4 for depth in 1 to IDS_MAX_DEPTH do
5 H←∅;
6 DLS_MAX_DEPTH ← depth ;
7 DLS ( p , 0 ) ;
8 if solution_size 6= 0 then break ;
22. IDS est également abrégé IDDFS pour Iterative Deepening Depth-First Search.
10
2.4 Application au Taquin
Le Taquin est un jeu à un joueur en forme de damier 23 ; il est composé de blocs
et d’une case vide (notée *) ; l’objectif est d’ordonner les blocs par glissement à
la place de la case vide ; ci-dessous un Taquin 3x3 résolu :
1 2 3
4 5 6
7 8 *
Pour un problème donné, il n’existe pas toujours de solution (i.e. certaines solu-
tions ne sont pas atteignables sans démonter le jeu) ; pour savoir si un problème
possède une solution, il est possible de compter le nombre de valeurs mal pla-
cées après chaque bloc en suivant le parcours théorique possible de la case vide
(ce parcours est défini par les cases 1,2,3,6,5,4,7,8,* dans le Taquin résolu) ; si
la somme est paire, une solution existe ; ci-dessous un exemple de problème de
Taquin 3x3 :
1 2 3
4 * 5
6 7 8
Dans le problème ci-dessus, en suivant le parcours théorique de la case vide,
on obtient la séquence 1,2,3,5,*,4,6,7,8 ; on a des erreurs de placement pour les
valeurs 5 et *, qui possèdent respectivement une (le 4) et quatre (le 4,6,7 et 8)
valeurs mal placées ; ce qui fait 5 valeurs mal placées ; 5 étant impair, ce pro-
blème n’a pas de solution.
23. La taille de l’espace d’états d’un Taquin à n cases semble être égale à n!/2 ; la difficulté
(disons trois étoiles) du Taquin vient : 1) de la taille de l’espace d’états ; 2) des cycles possibles ;
3) et de la possibilité d’avoir des transpositions d’une position à des hauteurs différentes.
11
19 void print_board() {
20 printf("%d %d ", NBL, NBC);
21 for(int i = 0; i < NBL; i++)
22 for(int j = 0; j < NBC; j++) {
23 if(board[i][j] == NBL*NBC) printf("* ");
24 else printf("%d ", board[i][j]);
25 }
26 printf("\n");
27 }
28 bool can_move_U(int _i, int _j) {
29 if(_i <= 0) return false; else return true;
30 }
31 void move_U(int _i, int _j) {
32 board[_i][_j]=board[_i-1][_j]; board[_i-1][_j]=NBL*NBC;
33 }
34 bool can_move_D(int _i, int _j) {
35 if(_i >= (NBL-1)) return false; else return true;
36 }
37 void move_D(int _i, int _j) {
38 board[_i][_j]=board[_i+1][_j]; board[_i+1][_j]=NBL*NBC;
39 }
40 bool can_move_L(int _i, int _j) {
41 if(_j<=0) return false; else return true;
42 }
43 void move_L(int _i, int _j) {
44 board[_i][_j]=board[_i][_j-1]; board[_i][_j-1]=NBL*NBC;
45 }
46 bool can_move_R(int _i, int _j) {
47 if(_j>=NBC-1) return false; else return true;
48 }
49 void move_R(int _i, int _j) {
50 board[_i][_j]=board[_i][_j+1]; board[_i][_j+1]=NBL*NBC;
51 }
52 void set_next(int *_moves, int& _size, int& _line, int& _col) {
53 for(int i = 0; i < NBL; i++)
54 for(int j = 0; j < NBC; j++) {
55 if(board[i][j] == NBL*NBC) {
56 _line = i; _col = j; break;
57 }
58 }
59 _size = 0;
60 if(can_move_U(_line, _col)) { _moves[_size]=MOVE_U; _size++; }
61 if(can_move_D(_line, _col)) { _moves[_size]=MOVE_D; _size++; }
62 if(can_move_L(_line, _col)) { _moves[_size]=MOVE_L; _size++; }
63 if(can_move_R(_line, _col)) { _moves[_size]=MOVE_R; _size++; }
64 }
12
61 void play(int _move, int _line, int _col) {
62 if(_move == MOVE_U) move_U(_line, _col);
63 if(_move == MOVE_D) move_D(_line, _col);
64 if(_move == MOVE_L) move_L(_line, _col);
65 if(_move == MOVE_R) move_R(_line, _col);
66 }
67 void unplay(int _move, int _line, int _col) {
68 if(_move == MOVE_U) move_D(_line-1, _col);
69 if(_move == MOVE_D) move_U(_line+1, _col);
70 if(_move == MOVE_L) move_R(_line, _col-1);
71 if(_move == MOVE_R) move_L(_line, _col+1);
72 }
73 int rand_move() {
74 int next_moves[4];
75 int next_size;
76 int next_i_line;
77 int next_i_col;
78 set_next(next_moves, next_size, next_i_line, next_i_col);
79 int r = ((int)rand())%next_size;
80 play(next_moves[r], next_i_line, next_i_col);
81 return next_moves[r];
82 }
83 bool final_position() {
84 for(int i = 0; i < NBL; i++)
85 for(int j = 0; j < NBC; j++)
86 if(board[i][j] != i*NBC+j+1) return false;
87 return true;
88 }
89 std::string mkH() {
90 char strh[1024];
91 strh[0] = ’\0’;
92 for(int i = 0; i < NBL; i++)
93 for(int j = 0; j < NBC; j++) {
94 char stre[16];
95 sprintf(stre, "%d-", board[i][j]);
96 strcat(strh, stre);
97 }
98 return std::string(strh);
99 }
100 #endif
13
Le programme suivant présente la résolution DLS du taquin 3x3 :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <unordered_map>
4 #include "mytaq.h"
5 std::unordered_map<std::string, int> H;
6 int DLS_MAX_DEPTH = 30;
7 int* sol;
8 int sol_size;
9 void dls_solve(std::string _strboard, int _depth) {
10 if(sol_size != 0) return;
11 H[_strboard] = _depth;
12 if(final_position()) {
13 sol_size = _depth;
14 return;
15 }
16 if(_depth == DLS_MAX_DEPTH) return;
17 int next_moves[4];
18 int next_size = 0;
19 int next_start_line = 0;
20 int next_start_col = 0;
21 set_next(next_moves, next_size, next_start_line, next_start_col);
22 for(int j = 0; j < next_size; j++) {
23 play(next_moves[j], next_start_line, next_start_col);
24 std::string new_strboard = mkH();
25 std::unordered_map<std::string, int>::iterator ii = H.find(new_strboard);
26 if(ii == H.end() || ii->second > _depth) {
27 sol[_depth] = next_moves[j];
28 dls_solve(new_strboard, _depth+1);
29 }
30 unplay(next_moves[j], next_start_line, next_start_col);
31 if(sol_size != 0) break;
32 }
33 }
34 int main(int _ac, char** _av) {
35 sol_size = 0;
36 sol = new int[DLS_MAX_DEPTH];
37 board[0][0] = 1; board[0][1] = 2; board[0][2] = 3;
38 board[1][0] = 4; board[1][1] = 9; board[1][2] = 5;
39 board[2][0] = 6; board[2][1] = 7; board[2][2] = 8;
40 std::string strboard = mkH();
41 dls_solve(strboard, 0);
42 if(sol_size != 0) {
43 for(int i = 0; i < sol_size; i++) printf("%d ", sol[i]);
44 printf("\n");
45 } else { printf("-1\n"); }
46 H.clear();
47 return 0;
48 }
14
La solution retournée par le programme réalisant une recherche DLS est :
0 2 1 1 3 0 0 2 1 3 0 2 1 3 0 3 1 1 2 0 3 1 2 0 3 0 2 1 3 1
15
La solution retournée par le programme réalisant une recherche IDS est :
3 1 2 2 0 3 1 3 0 2 2 1 3 3
On note que la solution retournée par IDS est plus courte que la solution re-
tournée par DLS.
24. Dans un Taquin 3x3, on a 362880 positions, dont 181440 positions sans solution.
16
11 void generate(int _srand, int _n, int _nb_ite) {
12 srand(_srand);
13 board = new int[nbl*nbc];
14 for(int i = 0; i < _n; i++) {
15 shuffle_board(_nb_ite);
16 std::string strboard = mkH();
17 std::unordered_map<std::string, int>::iterator ii = H.find(strboard);
18 if(ii == H.end() && !final_position()) {
19 H.insert(std::unordered_map<std::string, int>::value_type(strboard, 1));
20 print_board();
21 }
22 }
23 delete[] board;
24 }
25 int main(int _ac, char** _av) {
26 nbl = 3; nbc = 3;
27 generate(1, 4, 100);
28 return 0;
29 }
Pour 100 problèmes 3x3, la taille moyenne des solutions est de 18, avec un
écart-type de 5 ; en utilisant une recherche IDS qui utilise DLS, on obtient un
temps de calcul moyen de 0.77sec avec un écart-type inférieur à 1.2sec ; en utili-
sant une recherche IDS qui utilise DLSbis , on obtient un temps de calcul moyen
de 0.38sec avec un écart-type inférieur à 0.3sec (calculs réalisés sur i7-6820HQ
CPU @ 2.70GHz).
17
2.5 Application au Solitaire
Le solitaire est un jeu à un joueur dont la forme diffère selon la variante et
la connexité du plateau ; dans le cas de la variante anglaise, c’est un plateau
4-connexe en forme de croix, sur lequel les pions se déplacent par saut au dessus
d’un pion ; sauter au dessus d’un pion, vers une position vide, supprime le pion
sauté du plateau ; l’objectif est de finir avec un seul pion sur le plateau et selon
les variantes, sur une position imposée ou libre ; ci dessous, avec des pions « o »,
les positions finales « $ » et les positions vides « + », un cas 4-connexe et un cas
3-connexe (avec la position initiale au dessus et la position finale en dessous 25 ) :
o o o + + +
o o o + + + + $
o o o o o o o + + + + + + + o o + +
o o o + o o o + + + $ + + + o o o + + +
o o o o o o o + + + + + + + o o o o + + + +
o o o + + + o o o o o + + + + +
o o o + + +
25. Ici, les positions finales sont initialement des positions sans pion mais ce n’est pas tou-
jours le cas.
26. L’identification de transpositions dans un problème peut se résumer à l’identification de
chemins différents menant à une position identique en partant d’une position commune.
18
Le fichier mypeg.h contient la représentation du plateau et ses manipulations
(jouer un coup, jouer aléatoirement) :
1 #ifndef MYPEG_H
2 #define MYPEG_H
3 #include <cstdio>
4 #include <cstdlib>
5 #include <vector>
6
7 #define OUT_OF_BOARD 0
8 #define FREE_POS 1
9 #define PIECE_POS 2
10
11 #define MOVE_U 0
12 #define MOVE_D 1
13 #define MOVE_L 2
14 #define MOVE_R 3
15
19 struct peg_t {
20 int board[49];
21 int nbl;
22 int nbc;
23 std::vector<int> move_pos;
24 std::vector<int> move_dir;
25
19
42 void load(char* _file) {
43 FILE *fp = fopen(_file, "r");
44 char *line = NULL;
45 size_t len = 0;
46 ssize_t nread;
47 if (fp == NULL) { perror("fopen"); exit(EXIT_FAILURE); }
48 int nbline = 0;
49 while ((nread = getline(&line, &len, fp)) != -1) {
50 if(((int)nread) == (nbc+1)) {
51 for(int i = 0; i < nbc; i++) {
52 if(line[i] == cpos[FREE_POS]) board[nbline*nbc+i] = FREE_POS;
53 if(line[i] == cpos[PIECE_POS]) board[nbline*nbc+i] = PIECE_POS;
54 }
55 nbline++;
56 }
57 if(nbline == nbl) break;
58 }
59 free(line); fclose(fp); init_moves();
60 }
61 std::string mkH() {
62 char strh[1024];
63 int strh_size = 0;
64 for(int i = 0; i < nbl; i++)
65 for(int j = 0; j < nbc; j++) {
66 if(board[i*nbc+j] == OUT_OF_BOARD) { /* nop */ }
67 else if(board[i*nbc+j] == FREE_POS) { strh[strh_size] = ’+’; strh_size++; }
68 else { strh[strh_size] = ’o’; strh_size++; }
69 }
70 strh[strh_size] = ’\0’;
71 return std::string(strh);
72 }
73 bool can_move_U(int _p) {
74 if(_p >= nbl*nbc || _p < 2*nbc) return false;
75 if(board[_p] != PIECE_POS) return false;
76 if(board[_p-nbc] != PIECE_POS) return false;
77 if(board[_p-2*nbc] != FREE_POS) return false;
78 return true;
79 }
80 void move_U(int _p) {
81 board[_p]=FREE_POS; board[_p-nbc]=FREE_POS; board[_p-2*nbc]=PIECE_POS;
82 }
83 bool can_move_D(int _p) {
84 if(_p < 0 || _p > ((nbl*nbc)-2*nbc)) return false;
85 if(board[_p] != PIECE_POS) return false;
86 if(board[_p+nbc] != PIECE_POS) return false;
87 if(board[_p+2*nbc] != FREE_POS) return false;
88 return true;
89 }
90 void move_D(int _p) {
91 board[_p]=FREE_POS; board[_p+nbc]=FREE_POS; board[_p+2*nbc]=PIECE_POS;
92 }
20
93 bool can_move_L(int _p) {
94 if(_p < 0 || _p >= nbl*nbc || (_p%nbc) <= 1) return false;
95 if(board[_p] != PIECE_POS) return false;
96 if(board[_p-1] != PIECE_POS) return false;
97 if(board[_p-2] != FREE_POS) return false;
98 return true;
99 }
100 void move_L(int _p) {
101 board[_p]=FREE_POS; board[_p-1]=FREE_POS; board[_p-2]=PIECE_POS;
102 }
103 bool can_move_R(int _p) {
104 if(_p < 0 || _p >= nbl*nbc || (_p%nbc) >= (nbc-2)) return false;
105 if(board[_p] != PIECE_POS) return false;
106 if(board[_p+1] != PIECE_POS) return false;
107 if(board[_p+2] != FREE_POS) return false;
108 return true;
109 }
110 void move_R(int _p) {
111 board[_p]=FREE_POS; board[_p+1]=FREE_POS; board[_p+2]=PIECE_POS;
112 }
113 void try_add_move_from(int _p) {
114 if(can_move_U(_p)) add_move(_p, MOVE_U);
115 if(can_move_D(_p)) add_move(_p, MOVE_D);
116 if(can_move_L(_p)) add_move(_p, MOVE_L);
117 if(can_move_R(_p)) add_move(_p, MOVE_R);
118 }
119 void try_add_move_to(int _p) {
120 if(can_move_U(_p+2*nbc)) add_move(_p+2*nbc, MOVE_U);
121 if(can_move_D(_p-2*nbc)) add_move(_p-2*nbc, MOVE_D);
122 if(can_move_L(_p+2)) add_move(_p+2, MOVE_L);
123 if(can_move_R(_p-2)) add_move(_p-2, MOVE_R);
124 }
125 void init_moves(){
126 for(int i = 0; i < nbc*nbl; i++) {
127 if(board[i] == FREE_POS) try_add_move_to(i);
128 }
129 }
130 void update_moves() {
131 move_pos.clear(); move_dir.clear(); init_moves();
132 }
133 int score() {
134 int ret = 0;
135 for(int i = 0; i < nbc*nbl; i++) if(board[i] == PIECE_POS) ret++;
136 return ret;
137 }
138 void play_move(int _pos, int _dir) {
139 if(_dir == MOVE_U) { move_U(_pos); }
140 else if(_dir == MOVE_D) { move_D(_pos); }
141 else if(_dir == MOVE_L) {move_L(_pos); }
142 else if(_dir == MOVE_R) {move_R(_pos); }
143 update_moves();
144 } 21
145 };
Le programme suivant utilise mypeg.h et résoud les problèmes de solitaire :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <string>
5 #include <unordered_map>
6 #include "mypeg.h"
7
22
Considérons le problème suivant :
1 ..ooo..
2 ..ooo..
3 ooooooo
4 ooo+ooo
5 ooooooo
6 ..ooo..
7 ..ooo..
23
3 Minimax et Alpha-beta
3.1 Minimax
L’algorithme Minimax correspond à l’imbrication mutuelle d’une fonction maxi
et d’une fonction mini, en commençant par la fonction maxi ( s , 0) pour une
position courante s. Alg. 7 présente la fonction maxi. Alg. 8 présente la fonction
mini. Ces deux fonctions utilisent 3 variantes de la fonction f d’évaluation d’une
position s pour le joueur root 27 :
— f’ ( s ) la variante de f pour une position sans mouvement possible 28 .
— fmini () la variante de f retournant la valeur minimale de f.
— fmaxi () la variante de f retournant la valeur maximale de f.
Les fonctions mini et maxi retournent le meilleur coup (i.e. best_m) et la
meilleure évaluation (i.e. best_v).
1 fonction maxi ( s , d ) :
2 if d == MINIMAX_MAX_DEPTH then return { ∅ , f ( s ) } ;
3 M ← nextMoves ( s ) ;
4 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
5 { best_m , best_v } ← { ∅ , fmini ( ) −1 } ;
6 for each m in M do
7 s0 ← applyMove ( s , m ) ;
8 v ← mini ( s0 , d + 1) ;
9 if v > best_v then
10 { best_m , best_v } ← { m , v } ;
11 return { best_m , best_v } ;
Alg. 7: Fonction maxi pour l’évaluation d’une position s.
1 fonction mini ( s , d ) :
2 if d == MINIMAX_MAX_DEPTH then return { ∅ , f ( s ) } ;
3 M ← nextMoves ( s ) ;
4 if size ( M ) == 0 then return { ∅ , f’ ( s ) } ;
5 { best_m , best_v } ← { ∅ , fmaxi ( ) +1 } ;
6 for each m in M do
7 s0 ← applyMove ( s , m ) ;
8 v ← maxi ( s0 , d + 1) ;
9 if v < best_v then
10 { best_m , best_v } ← { m , v } ;
11 return { best_m , best_v } ;
Alg. 8: Fonction mini pour l’évaluation d’une position s.
27. Le joueur root est celui dont la position est à la racine de l’arbre, position à partir de
laquelle on cherche le meilleur coup à jouer.
28. Pour une position terminale, il est courant d’avoir des évaluations complètes, ce qui
donne lieu à une évaluation f’ différente de l’ évaluation heuristique f.
24
3.2 Negamax
Alg. 9 correspond à un parcours Negamax, basé sur l’utilisation d’une fonction
d’évaluation f dépendant du tour de jeu, retournant la valeur max d’une position
pour le joueur courant. La variation f’ reste utile.
25
Alg. 11 présente une autre possibilité de parcours αβ, utilisant une variable lo-
cale AB représentant les valeurs des coupes α et β. En fonction de la profondeur,
les valeurs seront des coupes α ou des coupes β.
26
#i n c l u d e <c s t d l i b >
#i n c l u d e <c s t d i o >
#i n c l u d e < s t r i n g . h>
#i n c l u d e <deque>
s t r u c t S_t {
u n s i g n e d i n t myseed ;
u n s i g n e d i n t myiseed ;
i n t ∗ mydata = 0 ;
i n t myalloc = 0 ;
i n t mysize = 0 ;
i n t mydepth = 0 ;
// t r e e with f i x e d b r a n c h i n g f a c t o r
// o r d e r i n g data i n a s i n g l e s t r u c t u r e
// [ . . . , e v a l , p a r e n t , c h i l d r e n 0 , c h i l d r e n 1 , c h i l d r e n 2 , ... ]
// ID∗5 = e v a l o f ID node
// ID∗5+1 = p a r e n t i d o f ID node
// ID∗5+2 = c h i l d r e n 0 i d o f ID node
// ID∗5+3 = c h i l d r e n 1 i d o f ID node
// ID∗5+4 = c h i l d r e n 2 i d o f ID node
i n t myget ( i n t i , i n t d ) { r e t u r n mydata [ ( i ∗5)+d ] ; }
v o i d myset ( i n t i , i n t d , i n t v ) { mydata [ ( i ∗5)+d]=v ; }
i n t g e t _ v a l ( i n t i ) { r e t u r n myget ( i , 0 ) ; }
i n t g e t _ p a r e n t ( i n t i ) { r e t u r n myget ( i , 1 ) ; }
i n t g e t _ c h i l d r e n ( i n t i , i n t c ) { r e t u r n myget ( i , 2+c ) ; }
v o i d s e t _ v a l ( i n t i , i n t v ) { myset ( i , 0 , v ) ; }
v o i d s e t _ p a r e n t ( i n t i , i n t p ) { myset ( i , 1 , p ) ; }
v o i d s e t _ c h i l d r e n ( i n t i , i n t c i , i n t cv ) { myset ( i , 2+c i , cv ) ; }
i n t get_depth ( i n t i ) {
i n t p = g e t _ p a r e n t ( i ) ; i f ( p == −1) r e t u r n 0 ; r e t u r n 1+get_depth ( p ) ;
}
v o i d i n i t ( u n s i g n e d i n t _seed , i n t _max_nb_nodes ) {
myseed = _seed ; myiseed = _seed ;
m y a l l o c = _max_nb_nodes ;
i f ( m y a l l o c != 0 ) d e l e t e [ ] mydata ;
mydata = new i n t [ m y a l l o c ∗ 5 ] ;
memset ( mydata , −1, m y a l l o c ∗5∗ s i z e o f ( i n t ) ) ;
}
void destroy ( ) {
i f ( m y a l l o c != 0 ) { d e l e t e [ ] mydata ; m y a l l o c = 0 ; }
}
i n t a d d _ c h i l d r e n ( i n t _node , i n t _ c h i l d r e n _ i d ) {
i n t r e t = mysize ;
s e t _ c h i l d r e n ( _node , _ chi ld ren _i d , m y s i z e ) ;
s e t _ p a r e n t ( mysize , _node ) ;
m y s i z e ++;
return ret ;
}
v o i d p r i n t ( FILE∗ _fp ) {
f p r i n t f ( _fp , "# b f s s i z e %d depth %d\n " , mysize , mydepth ) ;
f p r i n t f ( _fp , "# ID VAL PARENT C0 C1 C3\n " ) ;
f o r ( i n t i = 0 ; i < m y s i z e ; i ++) {
f p r i n t f ( _fp , "%d %d %d " , i , g e t _ v a l ( i ) , g e t _ p a r e n t ( i ) ) ;
f o r ( i n t j = 0 ; j < 3 ; j ++) f p r i n t f ( _fp , " %d " , g e t _ c h i l d r e n ( i , j ) ) ;
f p r i n t f ( _fp , "\n " ) ;
}
}
};
27
/∗ g++ −s t d=c++11 minimax . cpp
∗/
i n t main ( i n t _ac , c h a r ∗∗ _av ) {
S_t T ; T . i n i t ( 1 , 1 0 ) ;
T. mysize = 1 ;
T. add_children (0 , 0 ) ; T. add_children (0 , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 1 , 0 ) , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 1 , 1 ) , 2 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 2 , 0 ) , 0 ) ;
T. add_children (2 , 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 0 ) , 1 0 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 1 ) , 1 1 ) ;
T . s e t _ v a l (T . a d d _ c h i l d r e n ( 6 , 2 ) , 1 2 ) ;
T. p r i n t ( stdout ) ;
T. destroy ( ) ;
return 0;
}
28
4 Monte-Carlo et Monte-Carlo Tree Search
Le principe de Monte-Carlo (MC) est de réaliser des moyennes à partir de plu-
sieurs parties ; dans le cas d’un jeu, les parties aléatoires sont appelées playout
ou rollout.
Pour un problème dans une position ou d’un état s, on peut réaliser cette évalua-
tion avec N itérations ; Alg. 12 présente la solution avec N itérations et Alg. 12
présente la solution avec calcul de l’écart-type.
1 sum ← 0 ;
2 for N times do
3 r ← playout ( s ) ;
4 sum ← rq + sum ;
5 rq ← sum/N ;
Alg. 12: Evaluation MC après N itérations.
1 sum ← 0 ;
2 sum2 ← 0 ;
3 for N times do
4 r ← playout ( p ) ;
5 sum ← r + sum ;
6 sum2 ← (r ∗ r) + sum2 ;
7 rp ← sum/N ;
p
8 σrp ← sum2/N − (rp ∗ rp ) ;
Alg. 13: Evaluation MC avec écart-type.
29
L’utilisation de MC pour évaluer une décision (i.e. la possibilité de choisir entre
plusieurs actions) implique dans le cas d’un jeu, de savoir jouer un coup aléa-
toirement, à partir d’une position, détecter la fin de partie, décider si une
position de fin de partie est gagnée et implicitement d’être capable de jouer
des parties aléatoires rapidemment pour établir des taux de réussite.
1 M ← nextMoves ( s ) ;
2 if size ( M ) == 0 then return P ASS ;
3 best ← first ( M ) ; max ← 0 ;
4 for each m in M do
5 s0 ← applyMove ( s , m ) ;
6 wi ← 0 ;
7 for N times do
8 r ← playout ( s0 ) ;
9 if r == W IN then wi ← wi + 1 ;
10 if wi > max then { best , max } ← { m , wi } ;
11 return best ;
Alg. 14: Programme MC pour l’évaluation d’un état s.
30
4.1 Monte Carlo Tree Search (MCTS )
La figure 2 29 présente les 4 étapes de construction d’une recherche MCTS. Ces 4
étapes appliquées itérativement permettent de construire un arbre de décision à
l’aide de statistiques établis itérativement ; au fil des itérations, les descentes sé-
lectionnent les meilleurs noeuds de l’arbre, les expansions ajoutent de nouveaux
noeuds dans l’arbre, les playouts ajoutent des résultats et les rétropropagations
permettent de prendre en compte les résultats dans les noeuds parent.
1 while not-interrupted do
2 s0 ← selection ( s ) ;
3 s00 ← expansion ( s0 ) ;
4 r ← playout ( s00 ) ;
5 backpropagate ( s00 , r ) ;
6 return bestNext ( s ) ;
Alg. 16: MCTS pour l’évaluation d’un état s.
31
à proximité de 0.4, N le nombre de playouts réalisés au nœud parent ;
Alg. 17 présente cette fonction en détails.
— La fonction expansion (Alg. 16 ligne 3) corresponds à l’étape d’expansion
de s0 en s00 ; Alg. 18 présente cette fonction en détails.
— La fonction playout (Alg. 16 ligne 4) correspond à l’étape de simulation ;
cette fonction est identique à la fonction de playout d’une recherche MC.
— La fonction backpropagate (Alg. 16 ligne 5) correspond à l’étape de
rétropropagation des scores dans l’arbre ; Alg. 19 présente cette fonction
en détails.
Cet algorithme est dit anytime dans le sens ou il est interruptible à tout ins-
tant pour nous retourner son meilleur résultat courant ; c’est un algorithme qui
construit itérativement un arbre à partir de playouts ; l’interruption peut être
en nombre d’itérations ou en temps ou réalisée par la découverte d’une solution.
1 N ← getN ( s ) ;
2 if N == 0 then return s ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then return s ;
8 new_eval ← uct ( s , m ) ;
9 if new_eval > max then {max, best} ← {new_eval, m} ;
10 s0 ← applyMove ( s , best ) ;
11 return selection ( s0 ) ;
Alg. 17: Fonction selection(s) de sélection à partir d’un nœud s.
1 N ← getN ( s ) ;
2 if N == 0 then makeMoves ( s ) ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then
8 s0 ← applyMove ( s , m ) ;
9 then return s0 ;
32
1 if parent ( s ) == ∅ then return ;
2 addScore ( s , score ) ;
3 return backpropagate ( parent ( s ) , score ) ;
Alg. 19: Fonction backpropagate (score) de rétropropagation d’un
score dans un noeud s.
1 N ← getN ( s ) ;
2 if N == 0 then makeMoves ( s ) ;
3 M ← nextMoves ( s ) ;
4 {max, best} ← {−1, ∅} ;
5 for each m in M do
6 N i ← getNi ( s , m ) ;
7 if N i == 0 then
8 s0 ← applyMove ( s , m ) ;
9 return s0 ;
10 new_eval ← uct ( s , m ) ;
11 if new_eval > max then {max, best} ← {new_eval, m} ;
12 s0 ← applyMove ( s , best ) ;
13 return selection-expansion ( s0 ) ;
Alg. 20: Fonction selection-expansion(s) à partir d’un nœud s.
1 while not-interrupted do
2 s0 ← selection-expansion ( s ) ;
3 r ← playout ( s0 ) ;
4 if r == W IN then backpropagate ( s0 , r ) ;
5 return bestNext ( s ) ;
Alg. 21: MCTS pour l’évaluation d’un état s.
33
4.2 Progressive Bias (PB)
La variante appelée Progressive Bias consiste à ajouter un terme de valeur
p des coups dans la formule U CT et en
heuristique en fonction des positions
appliquant la formule (Wi /Ni )+K log(N )/Ni +Hi /(1+Ni −Wi ) dans laquelle
Hi est une valeur précalculée.
34
Trois variantes d’hybridation sont possibles :
— La variante MCTS-MR (pour Minimax Rollout) précède les itérations du
playout par un parcours en largeur à profondeur prédéfinie. Si le parcours
en largeur détermine la valeur de la position courante, le playout est
interrompu et la valeur trouvée est rétropropagée.
— La variante MCTS-MS (pour Minimax Selection) précède l’étape de sé-
lection et d’expansion par un parcours en largeur quand le nombre de
visite d’un noeud dépasse un seuil prédéfini ; seul un seuil de 0 implique
la phase d’expansion ; l’étape de sélection et d’expansion est interompue
si le parcours en largeur permet d’évaluer la position courante.
— La variante MCTS-MB (pour Minimax Backpropagate) ajoute à l’étape
de rétropropagation une évaluation de noeuds voisins d’un noeud prouvé
perdant ou gagnant afin de rétropropager la preuve d’une victoire ou
d’une défaite dans le parent de ce noeud.
1 while not-interrupted do
2 {s0 , L1 , M1 } ← selection_RAVE ( s , ∅, ∅) ;
3 expansion_RAVE ( s0 ) ;
4 {r, L2 , M1 } ← playout_RAVE ( s0 , ∅, M1 ) ;
5 backpropagate_RAVE ( r , L1 , L2 , M1 ) ;
Alg. 22: RAVE pour l’évaluation d’un état s.
35
s) et H[s].AM AF _N [i] (abrégé AM AF _Ni en s), soit une table de valeurs
AM AF _W et une table de valeurs AM AF _N dans un état s, pour évaluer
les valeurs des liens vers les états suivants s0 ; de la même façon, H[s].N est
abrégé Ns et H[s].W est abrégé Ws ; la mise à jour de ces valeurs implique
la construction de trois listes : L1 la liste des états rencontrés pendant la sé-
lection et l’expansion, L2 la liste des états rencontrés pendant le playout et
M1 la liste des coups recontrés pendant la sélection, l’expansion et le playout ;
la prise en compte de coups issus de la partie playout de la descente 31 im-
plique que des états auront des valeurs AMAF positives et des valeurs N et W
nulles ; Alg. 23, 24, 25 et 26 présentent respectivement la sélection, l’expansion,
le playout et la rétropropagation de RAVE.
36
1 if H[s] == ∅ then
2 add ( H, s ) ;
3 {Ws , Ns } ← {0, 0} ;
4 for each m in nextMoves ( s ) do
5 add_move ( s, m ) ;
1 for each s in L1 do
2 H[s].N + + ;
3 if r == W IN then H[s].W + + ;
4 for each e in M1 do
5 if ∃ H[s].AM AFe then
6 H[s].AM AF _N [e] + + ;
7 if r == W IN then H[s].AM AF _W [e] + + ;
8 for each s in L2 do
9 for each e in M1 do
10 if ∃ H[s].AM AFe then
11 H[s].AM AF _N [e] + + ;
12 if r == W IN then H[s].AM AF _W [e] + + ;
37
4.6 Generalized RAVE (GRAVE)
L’algorithme GRAVE fixe un seuil d’utilisation des valeurs AMAF ; en dessous
de ce seuil, les valeurs AMAF d’un nœud deviennent celles du plus proche pa-
rent conformes à ce seuil ; les valeurs AMAF à prendre en compte sont évaluées
lors de la phase de sélection (Alg. 27) ; la valeur ref définit ce seuil, qui est à
comparer avec le nombre de playouts réalisés (i.e. Ns pour l’état s) ; les phases
d’expansion, de playout et de rétropropagation sont identiques à RAVE.
1 while not-interrupted do
2 {s0 , L} ← selection_GRAVE ( s , ∅, ref , s) ;
3 expansion_RAVE ( s0 ) ;
4 {r, L} ← playout_RAVE ( s0 , L ) ;
5 backpropagate_RAVE ( r , L) ;
Alg. 28: GRAVE pour l’évaluation d’un état s.
38
4.7 MAST, TO-MAST, PAST
L’algorithme MAST (pour Move-Average Sampling Technique) identifie les coups
les plus prometteurs ; les coups sont considérés indépendamment des états, pen-
dant les phases de sélection, d’expansion et de playout ; une table en mémoire
globale stocke le nombre de playouts réalisés avec le coups i (i.e. M AST _Ni )
et le nombre de playouts ayant menés à une victoire en utilisant le coup i (i.e.
M AST _Wi ).
La fonction d’évaluation des coups pendant la sélection devient :
eM ASTi /τ
mast(i) = Pn M ASTj /τ
j=1 e
39
5 Recherche enveloppée
5.1 Nested Monte Carlo Search (NMCS)
L’algorithme NMCS est défini par une fonction à boucles for imbriquées 32 ;
une fonction nested_MC appelle récursivement la fonction nested_MC de niveau
inférieur ; au dernier niveau (ici associé à level == 0), la fonction appelle sim-
plement un playout ; entre chaque niveau, on joue un coup ; cette imbrication
est réalisée à une profondeur définie par le premier appel à nested_MC.
Prenons pour exemple une variante du problème du solitaire qui se limite l’ob-
jectif de finir avec le moins de pièces possibles ; pour certains problèmes, il sera
possible de finir avec une seule pièce ; pour d’autres problèmes, avec des formes
de plateaux plus ésothériques, il sera possible de finir avec n pièces ; tout comme
au taquin (pour lequel on générait des problèmes en mélangeant aléatoirement
le taquin en partant d’un taquin résolu), on peut générer des problèmes au so-
litaire en partant de la solution finale et en déjouant des coups tant que c’est
possible ; partant du problème ainsi généré, dont on sait qu’il possède au moins
une solution de score n, il s’agit de retrouver une solution ; pour certains de ces
problèmes, finir avec moins de pièces (i.e. n − m) sera possible ; dans tous les
cas, finir avec une seule pièce correspond à une solution ; avec une ou n pièces,
une solution ne correspond pas forcément optimale, dans le sens ou ce n’est
pas forcément la séquence la plus courte permettant d’arriver à cette solution ;
ci-dessous un exemple de problème 4x4 avec une position finale à une pièce :
oooo ..$.
oooo ....
+oo+ ....
o+oo ....
1 M ← nextMoves ( s ) ;
2 if size ( M ) == 0 then return score ( s ) ;
3 min ← score ( s ) ;
4 for each m in M do
5 s0 ← applyMove ( s , m ) ;
6 if level == 0 then r ← playout ( s0 ) ;
7 else r ← nested_MC ( s0 , level − 1) ;
8 if r < min then min ← r ;
9 return min ;
Alg. 29: La fonction nested_MC(s, level) réalisant une recherche NMC
avec un niveau d’enroulement level.
32. Dans la famille des fonctions récursives, f est récursive enveloppée pour f (x) =
g(y, f (x0 )), f est récursive terminale pour f (x) = f (y, x0 ), f est imbriquée dans g pour
g(x) = h(y, f (f (x0 ))), f est à boucles imbriquées pour f (x) = f or[0 : n]{f (x0 )}, f est ré-
cursive imbriquée pour f (x) = h(y, f (f (x))) et enfin f et g sont mutuellement récursives
pour f (x) = g(x0 ) et g(x) = f (x0 ), avec g et h des fonctions différentes de f , y une variable
indépendante de x, et x0 une variable dépendante de x.
40
Alg. 29 retourne le nombre de pièces restantes sur la plateau pour le meilleur
coup trouvé ; l’appel aux playouts est réalisé au niveau défini par la valeur level ;
comme le score retourné dépend des playouts, deux appels à la fonction avec les
mêmes paramètres peuvent ne pas retourner le même résultat ; Alg. 30 permet
d’obtenir la meilleure valeur sur N appels à NMCS.
1 best ← score ( s ) ;
2 for N loops do
3 r ← nested_MC ( s , level ) ;
4 if r < best then best ← r ;
5return best ;
Alg. 30: La fonction anytime_NMC(s, level) permettant d’obtenir la
meilleure valeur sur N itérations de NMC ou sur un temps maximum
prédéfini.
41
Exemple d’utilisation de la fonction nested_MC pour la résolution d’un problème
de solitaire sur une grille 6x6 avec une solution à deux pierres ou moins :
1 genpeg_t P(6,6,2);
2 P.gen(1);
3 P.mp->print();
4
5 solvepeg_t S(*(P.mp));
6 S.init();
7
8 int level = 3;
9 P.mp->update_moves();
10 int res = S.nested_MC(*(P.mp), level);
11 printf("res %d \n", res);
Les résultats présentés dans la table 1 ont été réalisés sur des grilles de taille
variant de 4x4 à 7x7.
Les temps de calculs présentés dans la table 1 ont été obtenus avec un processeur
Intel(R) Xeon(R) E5-4610 @ 2.40GHz (4804 bogomips) ; avec DLS, les tests
sur 6x6 ont été interrompus après une centaine d’heures de calculs ; pour M CS,
N M CS et N M CSanytime , les tests prennent de quelques secondes à quelques
minutes ; la profondeur maximale de récursivité choisie est de 20.
42
5.2 Nested Rollout Policy Adaptation (NRPA)
Le principe de NRPA reprend le principe de NMCS en ajoutant 1) une mémo-
risation de la meilleure séquence pour orienter les playouts et 2) une série de
playouts pour augmenter les chances de bonne orientation des futurs playouts ;
une probabilité est associée à chaque coup ; les probabilités des coups de la
meilleure séquence sont renforcées à chaque itération ; Alg. 31 présente NRPA ;
la fonction playout joue la partie aléatoirement et retourne 1) le nombre de
pièces sur le plateau après le dernier coup et 2) la séquence jouée pendant ce
playout ; P est la politique de choix des coups ; un playout est réalisé à partir
d’une position s et en fonction de cette politique P ; la mise à jour de P est
réalisée par la fonction adapt, qui est appelée à chaque itération en prenant en
compte la meilleure séquence en cours.
1 if level == 0 then
2 return playout ( s ) ;
3 else
4 {min , best_seq} ← {score ( s ) , ∅} ;
5 for N times do
6 {r, seq} ← nested_RPA ( s, level − 1, P) ;
7 if r == 1 then return {1 , seq} ;
8 if r ≤ min then {min, best_seq} ← {r, seq} ;
9 adapt (P, best_seq) ;
10 return {min , best_seq} ;
Alg. 31: La fonction nested_RPA(s, level, N ) réalisant un parcours
NRPA avec N playouts à chaque niveau et avec MAJ de la politique P de
sélection des coups suivants.
43
s t r u c t pdp_t { // Posi , Dir , Proba
i n t p o s i ; i n t d i r ; i n t proba ;
};
struct rollout_policy_t {
s t d : : map<s t d : : s t r i n g , double> proba ;
pdp_t∗ i n p u t ;
int input_size ;
i n t nb_input ;
i n t sum ;
rollout_policy_t ( i n t _size ) {
input_size = _size ;
i n p u t = new pdp_t [ _ s i z e ] ;
nb_input = 0 ;
sum = 0 ;
}
~rollout_policy_t () {
i f ( i n p u t != 0 ) d e l e t e [ ] i n p u t ;
input = 0;
}
void i n i t ( ) {
sum = 0 ;
nb_input = 0 ;
}
v o i d c o n s i d e r e ( peg_move_t& _m) {
s t d : : map<s t d : : s t r i n g , double > : : i t e r a t o r i i = proba . f i n d (_m. t o _ s t r ( ) ) ;
i f ( nb_input == i n p u t _ s i z e ) {
f p r i n t f ( s t d e r r , " r o l l o u t _ p o l i c y _ t c o n s i d e r e ALLOC ERROR ! ! ! \ n " ) ;
return ;
}
i f ( i i != proba . end ( ) ) {
i n p u t [ nb_input ] . p o s i = _m. p o s i ;
i n p u t [ nb_input ] . d i r = _m. d i r ;
i n p u t [ nb_input ] . proba = ( i n t ) i i −>s e c o n d ;
sum += i i −>s e c o n d ;
} else {
i n p u t [ nb_input ] . p o s i = _m. p o s i ;
i n p u t [ nb_input ] . d i r = _m. d i r ;
i n p u t [ nb_input ] . proba = 1 ;
proba [_m. t o _ s t r ( ) ] = ( d o u b l e ) i n p u t [ nb_input ] . proba ;
sum += i n p u t [ nb_input ] . proba ;
}
nb_input ++;
}
peg_move_t s e l e c t ( ) {
i n t r = rand ()%sum ;
i n t s e l e c t e d = −1;
f o r ( i n t i = 0 ; i < nb_input ; i ++) {
i f ( r < i n p u t [ i ] . proba ) { s e l e c t e d = i ; b r e a k ; }
r −= i n p u t [ i ] . proba ;
}
i n t new_posi = 0 ;
i n t new_dir = 0 ;
i f ( s e l e c t e d < 0 | | s e l e c t e d >= nb_input ) s e l e c t e d = nb_input −1;
new_posi = i n p u t [ s e l e c t e d ] . p o s i ;
new_dir = i n p u t [ s e l e c t e d ] . d i r ;
r e t u r n peg_move_t ( new_posi , new_dir ) ;
}
v o i d adapt ( s t d : : v e c t o r <peg_move_t>& _l ) {
f o r ( i n t i = 0 ; i < ( i n t ) _l . s i z e ( ) ; i ++) {
d o u b l e c u r r e n t _ v a l = proba [ _l [ i ] . t o _ s t r ( ) ] ;
i f ( c u r r e n t _ v a l < 1 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 1 . 0 ;
44
else i f ( c u r r e n t _ v a l < 1 0 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 2 . 0 ;
else i f ( c u r r e n t _ v a l < 1 0 0 0 . 0 ) proba [ _l [ i ] . t o _ s t r ( ) ] += 0 . 5 ;
}
}
};
45
RP−>adapt ( h i s t o ) ;
}
};
r o l l o u t _ p o l i c y _ t ∗ peg_RP_t : : RP;
Les résultats présentés dans la table 2 ont été réalisés sur des grilles de taille
variant de 5x5 à 7x7.
Table 2 – Résultats de NRPA pour 10 problèmes avec 100 et 200 playouts par
niveau
Les temps de calculs présentés dans la table 2 ont été obtenus avec un proces-
seur Intel(R) Xeon(R) E5-4610 @ 2.40GHz (4804 bogomips). La profondeur
maximale de récursivité choisie est de 20.
Pour les problèmes 7x7, on obtient NRPA > NMCS ; NRPA résoud en moyenne 33 1.5
fois plus rapidemment que NMCS (cf support de cours NMCS). Pour les problèmes
plus faciles, NMCS trouve une solution plus rapidemment que NRPA. Les résultats
obtenus dépendent directement du nombre de playouts réalisés par niveau et du
niveau de « nesting » choisis.
33. Résoudre 9 ou 10 problèmes sur 10 est içi considéré comme équivalent. Des tests sur
100 problèmes permettraient de quantifier plus précisément la différence entre NMCS avec 100
playouts et NRPA avec 200 playouts et surtout la différence entre problèmes résolus avec NRPA
contre 10 avec NMCS du support de cours précédent. Un peu de tunning des paramètres
nb_playouts, depth et allure de la fonction adapt serait à faire pour améliorer les temps de
réponse du solveur de peg-solitaire.
46
6 Preuve
6.1 Proof Number Search (PNS)
6.2 Proof Number 2 Search (PN2S)
6.3 Depth-First PNS (DFPNS)
6.4 Problème GHI
6.5 Variantes de PNS
6.6 Application à Breakthrough
47
7 Parallélisation
7.1 A la racine (Root Parallel)
7.2 Aux feuilles (Leaf Parallel)
7.3 Dans l’arbre (Tree Parallel)
7.4 Parallel NMCS
7.5 Distributed NRPA
7.6 PNS Randomisé (RP-PNS)
7.7 Parallel DFPNS
7.8 Job-Level PNS (JLPNS)
7.9 Parallel PN2S (PPN2S)
7.10 Application au Morpion Solitaire
48
8 Problèmes à information incomplète
8.1 Théorie des jeux
La théorie des jeux présente les jeux sous l’angle des interactions entre les joueurs
et essaie de définir les stratégies optimales d’un jeu ; cette question, de définition
de stratégie optimale, vue sous l’angle des interactions, implique que maximi-
ser des gains et minimiser des pertes dépend des décisions des autres joueurs ;
cette question est considérée en théorie des jeux pour les jeux à coups simultanés.
Un jeu à deux joueurs est représenté en forme normale par une tableau à deux
dimensions (également appelé matrice des gains). Dans le jeu Roshambo 34 , les
deux joueurs choisissent un coup parmi les trois Pierre, Papier, Ciseaux ; on
notera R le coup Pierre, P le coup Papier et S le coup Ciseaux ; les joueurs
jouent simultanément ; le tableau 3 présente la résolution des décisions de chaque
joueur ; le gain du joueur J1 est la première valeur ; le gain du joueur J2 est la
deuxième valeur ; une victoire est notée 1 et une défaite est notée −1 ; quand J1
joue R et J2 joue S, on trouve « 1 ;-1 », donc J1 gagne et J2 perd.
Table 3
J2
R P S
R 0 ;0 -1 ;1 1 ;-1
J1 P 1 ;-1 0 ;0 -1 ;1
S -1 ;1 1 ;-1 0 ;0
Pour chaque case de la matrice de gain, si la somme des gains des joueurs est
nulle, le jeu est dit à somme nulle ; si la somme des gains des joueurs vaut une
valeur constante, le jeu est dit à somme constante et est reformulable en jeu à
somme nulle par soustraction de cette valeur constante à tous les gains.
49
9 Apprentissage
9.1 Processus de décision Markovien
Dans le cadre de la prise de décision, les processus de décision Markovien (MDP)
permettent de définir des politiques de décision et de les améliorer itérativement ;
dans un MDP, un environnement complètement observable est défini par un état
courant ; à partir de cet état, une action définit l’état suivant ; à une action est
associée une récompense ; un MDP est ainsi une suite d’états, définie par une
suite d’actions, associée à une suite de récompenses ; résoudre un MDP équi-
vaut à définir la suite des actions qui maximise la récompense totale ; on appelle
politique la fonction qui définit à chaque instant l’action à suivre dans chaque
état ; le principe d’un MDP s’appuie sur la propriété suivante : « considérant le
présent, le futur est indépendant du passé », qui s’écrit en considérant la pro-
babilité de l’état à t + 1 :
Un MDP est noté (S, A, P, R, γ) avec S l’ ensemble des états, A l’ensemble des
actions, P l’ensemble des probabilités de transition, R l’ensemble des récom-
penses immédiates, et γ un facteur de dépréciation tel que γ ∈ [0, 1].
Une récompense RaSS 0 , par application d’une action a, à partir d’un état S pour
obtenir un état S 0 peut être positive ou négative ; RaSS 0 appliquée à l’instant t
est abrégée rt ; en pratique rt est constaté à l’instant t + 1 (i.e. après application
d’une action at à l’instant t).
50
Plus généralement, on note une politique π et on a :
— π(St , at ) = πSt → at pour une politique Markovienne déterministe
— π(St , pa ) = πSt → [0, 1] pour une politique Markovienne aléatoire
— π(ht , at ) = πht → at pour une politique histoire-dépendante déterministe
— π(ht , pa ) = πht → [0, 1] pour une politique histoire-dépendante aléatoire
— π(St , at ) ⊆ π(St , pa )
— π(St , at ) ⊆ π(ht , at )
— π(St , pa ) ⊆ π(ht , pa )
— π(ht , at ) ⊆ π(ht , pa )
Pour évaluer une politique, on utilise le cumul espéré, qui correspond au cumul
des valeurs contenues dans une suite de récompenses de taille n ; on appelle
critère de performance le mode de cumul choisi, avec r0 la récompense obte-
nue dans l’état courant S0 , r1 la récompense dans l’état suivant, et rn−1 la
dernière récompense obtenue avant obtention d’un état terminal ; le critère de
performance peut être défini par :
— une somme de taille fixe avec k < n − 1, égale à {r0 + r1 + · · · + rk |S0 }
— une somme gamma-pondérée, égale à {r0 +γr1 +γ 2 r2 +· · ·+γ n−1 rn−1 |S0 }
— une somme totale, égale à {r0 + r1 + · · · + rn−1 |S0 }
— une moyenne, égale à {(r0 + r1 + · · · + rn−1 )/n|S0 }
Quand γ (le facteur de dépréciation) est proche de zéro, l’évaluation est à courte
échéance ; quand γ est proche de un, l’évaluation est à longue échéance.
51
9.2 Heuristique et compromis
Quand une fonction heuristique h (S,A) donne une valeur de l’action A à partir
de l’état S, on appelle approche gloutonne (ou encore approche greedy) le fait
de suivre le choix proposé par la fonction h pour trouver la meilleure solution.
Pour améliorer une approche gloutonne, il est possible de pondérer l’action pro-
posée par une heuristique associée à un choix aléatoire ; ces heuristiques s’ap-
pliquent avant l’état terminal, pour maximiser l’évaluation de la position termi-
nale ; plusieurs méthodes de pondération sont possibles, dont les plus courantes
sont d’alterner choix heuristique et choix aléatoire :
— Selon une fonction hN, utilisant une valeur N1 , comme le présente Alg. 32 ;
la valeur N1 diminue au fil de sa sélection ; après N1 appels à cette
fonction heuristique, N1 est nul et toutes les actions choisies deviennent
aléatoires.
— Selon une fonction -greedy, comme le présente Alg. 33, avec la pro-
babilité d’exploration (i.e. de sélection de la fonction randomMove).
— Selon une fonction softmax, comme le présente Alg. 34.
1 if N1 >= 0 then
2 N1 ← N1 − 1 ;
3 return argmaxA h(S, A) ;
4 else
5 return randomMove (S);
Alg. 32: Fonction hN définisant la meilleure action à partir d’un
état S par alternance entre valeur heuristique h et fonction aléatoire
randomMove ; cette solution dépend de la valeur initiale de N1 .
52
1 rnd ← random ( ) ;
2 M ← nextMoves ( S ) ;
3 best ← ∅ ;
4 sum ← 0 ;
5 for each m in M do
6 sum ← sum + eh(S,m) ;
7 for each m in M do
8 evalm ← eh(S,m) /sum ;
9 if rnd ≤ evalm then
10 best ← m ;
11 break ;
12 rnd ← rnd − evalm ;
13 return best ;
Alg. 34: Fonction softmax ; M définit l’ensemble des actions A possibles
à partir de S.
53
1 Ŝ ← ∅ ;
2 for N times do
3 seq ← π(random (S)) ;
4 Gt ← eval(seq) ;
5 for each s in seq do
6 if s ∈
/ Ŝ then (sums , Ns ) ← (Gt , 1) ;
7 else (sums , Ns ) ← (sums + Gt , Ns + 1) ;
8 Ŝ ← Ŝ ∪ seq ;
9 for each s in Ŝ do
10 scores ← sums /Ns ;
Alg. 35: Mode d’apprentissage MC.
1 sums ← 0 ;
2 for k times do sums ← sums + Gt ;
3 scores ← sums /k ;
Alg. 36: Abstraction de l’évaluation MC d’un état s.
1 scores ← 0 ;
2 for k times do scores ← scores + (Gt − scores )/k ;
Alg. 37: Abstraction de l’évaluation MC incrémentale d’un état s.
Pour des cas d’évaluation instable (i.e. ne donnant pas toujours le même ré-
sultat Gt pour deux séquences), il sera judicieux de pondérer le score avec
le dernier résultat Gt en remplaçant 1/k par une valeur α avec scores =
scores + α(Gt − scores ) ; ainsi on peut réduire ou augmenter l’influence les
nouveaux résultats Gt en augmentant ou en diminuant la valeur α autour de
1/k ; scores n’est plus une moyenne des scores obtenus dans ces cas ; ce mode
de calcul est adapté aux problèmes non-stationnaires (i.e. variant dans le temps).
54
L’apprentissage en mode TD est en fait paramétré avec λ qui définit la profon-
deur de l’estimation de TD-target ; Alg. 38 présente une abstraction de l’éva-
luation TD(0) d’un état s avec s = St , s0 = St+1 (autrement dit, s0 suivant de
s), α = k −1 , rets0 = Rt+1 , scores = V (St ) et scores0 = V (St+1 ).
1 scores ← 0 ;
2 for k times do scores ← scores + α(rets0 + γscores0 − scores ) ;
Alg. 38: Abstraction de l’évaluation TD(0) d’un état s.
Pour TD(0), on a :
TD-target = rets0 + γscores00
Pour TD(1), avec s00 suivant de s0 , on a :
1 scores ← 0 ;
2 for k times do
scores ← scores + α(rets0 + γ a π(a|S 0 )scores0 − scores ) ;
P
3
55
Les traces d’éligibilité correspondent au stockage des valeurs de retour des états
suivants de s0 (dans le cas de TD(2) : rets00 et rets000 ) ; Alg. 40 présente une abs-
traction de l’évaluation TD avec les traces d’éligibilité ; chaque MAJ concerne
les scores de tous les états ; les traces d’éligibilité de l’état S sont notées zs ; S
définit l’ensemble des états possibles.
1 for each S in S do
2 scores ← 0 ;
3 zs ← 0 ;
4 for k times do
5 S ← get_current_state ( ) ;
6 update ( zs ) ;
7 for each s in S do
8 scores ← scores + αzs (rets0 + γscores0 − scores ) ;
9 zs ← γλzs ;
zs + = 1
56
9.4 Programmation dynamique
Dans le cas d’espaces d’états complètement connus, dans lesquels il est possible
de procéder à rebours en partant de l’état final, les trois solutions courantes
pour la définition d’une politique optimale (notée π∗ ) sont :
— Par exécution de N simulations, selon le principe policy-evaluation
— Par exécution de N itérations de MAJ inter-dépendantes de politique et
d’évaluation, selon le principe value-iteration.
— Par exécution de N itérations de MAJ de politique toutes les k itérations
d’évaluation, selon le principe policy-iteration.
Dans le cas d’une politique prédéfinie, il est possible de l’évaluer avec N ité-
rations en suivant le principe policy-evaluation ; cette solution répond à des
questions de prédiction (autrement dit, quelle récompense vπ obtenir ou espérer
pour une politique π) ; pour une politique π, on calcule la valeur vπ ; les valeurs
des états sont initialisées à zéro ; les valeurs des états sont définies selon leurs
états précédents ; la valeur d’un état est MAJ selon la moyenne des états précé-
dents plus une récompense ; on définit ainsi v1 , v2 , . . . vN ; après N itérations,
les valeurs des états convergent vers vπ ; la convergence vers vπ est indépendante
de la politique π1 initialement choisie ; Alg. 41 présente ce calcul d’évaluation
d’une politique π dans un espace S ; à chaque itération, on applique une récom-
pense r ; à la fin de chaque itération, vi est dans S.
1 for N times do
2 S0 ← ∅ ;
3 for each s in S do
4 sum_score ← 0 ;
5 s0 ← s ;
6 P ← prev(s, π) ;
7 for each p in P do
8 sum_score ← sum_score + p.eval ;
9 s0 .eval ← r + sum_score/sizeof(P) ;
10 S 0 ← S 0 ∪ s0 ;
11 S ← S0 ;
Alg. 41: Principe policy-evaluation.
57
1 for N times do
2 S ← update-value (π, greedy) ;
3 π ← update-policy (S) ;
Alg. 42: Principe value-iteration.
1 for N times do
2 for k times do
3 S ← update-value (π, greedy) ;
4 π ← update-policy (S) ;
Alg. 43: Principe policy-iteration.
58
9.5 Application à Grid-world
Le problème Grid-world consiste en une grille (figure 3) avec des cases de sortie
(représentées en gris) et des cases (représentées en blanc) sur lesquelles il est
possible d’appliquer une commande haut-bas-gauche-droite ; en appliquant une
politique aléatoire sur chaque case, la question est de trouver la valeur de la
politique de déplacement aléatoire dans une telle grille ; ce qui équivaut à définir
la longueur moyenne des chemins permettant d’atteindre les cases de sortie.
59
14 void print() {
15 for(int i = 0; i < 16; i++)
16 printf("%.1f ", value[i]);
17 printf("\n");
18 }
19 void iterate() {
20 double tmp[16];
21 for(int i = 0; i < 16; i++) {
22 if(end_state[i]) {
23 value[i]=0.0;
24 } else {
25 int i_up = i-4; if(i_up<0) i_up=i;
26 int i_down = i+4; if(i_down>15) i_down=i;
27 int i_left = i-1; if((i%4)==0) i_left=i;
28 int i_right = i+1; if((i%4)==3) i_right=i;
29 tmp[i] = (value[i_up]+value[i_down]+
30 value[i_left]+value[i_right])/4.0-1.0;
31 }
32 }
33 memcpy(value, tmp, 16*sizeof(double));
34 }
35 void generate() {
36 init();
37 for(int i = 0; i < 1001; i++) {
38 if(i<=2 || i == 10 || i == 100 || i == 1000) {
39 printf("i:%d :: ", i); print();
40 }
41 iterate();
42 }
43 }
44 };
45 /* g++ -std=c++11 grid-world-policy-evaluation.cpp */
46 int main(int _ac, char** _av) {
47 grid_t G;
48 G.generate();
49 return 0;
50 }
La moyenne des valeurs est de 16 ; Pour chaque case, ces valeurs permettent
également de définir une politique optimale (présentée en figure 5) ; la politique
obtenue est partielle pour certains états (représentés en rouge dans cette figure).
60
Fig. 5 – Politique optimale π1000 par policy-evaluation sur Grid-world.
61
36 void update_value() {
37 double tmp[16];
38 for(int i = 0; i < 16; i++) {
39 if(end_state[i]) {
40 value[i]=0.0;
41 } else {
42 int i_up = i-4; if(i_up<0) i_up=i;
43 int i_down = i+4; if(i_down>15) i_down=i;
44 int i_left = i-1; if((i%4)==0) i_left=i;
45 int i_right = i+1; if((i%4)==3) i_right=i;
46 int N = 0;
47 tmp[i] = 0.0;
48 if(prev[i][UP]) { tmp[i] += value[i_up]; N++; }
49 if(prev[i][DOWN]) { tmp[i] += value[i_down]; N++; }
50 if(prev[i][LEFT]) { tmp[i] += value[i_left]; N++; }
51 if(prev[i][RIGHT]) { tmp[i] += value[i_right]; N++; }
52 tmp[i] /= N;
53 tmp[i] -= 1.0;
54 }
55 }
56 memcpy(value, tmp, 16*sizeof(double));
57 }
58 void update_policy() {
59 bool tmp[16][4] = {false};
60 for(int i = 0; i < 16; i++) {
61 if(!end_state[i]) {
62 double best_value = -DBL_MAX;
63 int i_up = i-4; if(i_up<0) i_up=i;
64 int i_down = i+4; if(i_down>15) i_down=i;
65 int i_left = i-1; if((i%4)==0) i_left=i;
66 int i_right = i+1; if((i%4)==3) i_right=i;
67 // compute best_value
68 if(prev[i][UP]) if(value[i_up] > best_value) best_value = value[i_up];
69 if(prev[i][DOWN]) if(value[i_down] > best_value) best_value = value[i_down];
70 if(prev[i][LEFT]) if(value[i_left] > best_value) best_value = value[i_left];
71 if(prev[i][RIGHT]) if(value[i_right] > best_value) best_value = value[i_right];
72 // compute the new policy
73 if(prev[i][UP]) if(value[i_up] == best_value) tmp[i][UP] = true;
74 if(prev[i][DOWN]) if(value[i_down] == best_value) tmp[i][DOWN] = true;
75 if(prev[i][LEFT]) if(value[i_left] == best_value) tmp[i][LEFT] = true;
76 if(prev[i][RIGHT]) if(value[i_right] == best_value) tmp[i][RIGHT] = true;
77 }
78 }
79 memcpy(prev, tmp, 16*4*sizeof(bool));
80 }
81 void generate() {
82 init();
83 for(int i = 0; i < 101; i++) {
84 if(i==1 || i == 100) {
85 print();
86 print_prev();
87 }
88 update_value();
89 update_policy();
90 }
91 }
92 };
93 /* g++ -std=c++11 grid-world-value-iteration.cpp */
94 int main(int _ac, char** _av) {
95 grid_t G;
96 G.generate();
97 return 0;
98 }
62
A la première itération, avec les appels à print() et print_prev, on obtient :
0.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 -1.0
-1.0 -1.0 -1.0 0.0
---- --L- UDLR UDLR
U--- UDLR UDLR UDLR
UDLR UDLR UDLR -D--
UDLR UDLR ---R ----
63
En ajoutant pour condition de terminaison, une égalité à 10−2 prêt de l’évalua-
tion vi ou de la politique πi , le programme correpondant à value-iteration est
modifié comme suit :
1 // ...
2 struct grid_t {
3 bool end_state[16] = {false};
4 double value[16] = {0.0};
5 bool prev[16][4];
6
7 bool value_stop_condition = false;
8 bool policy_stop_condition = false;
9 bool convergence_detected = false;
10
11 void init () { // ...
12 }
13 void print() { //...
14 }
15 void print_prev() { //...
16 }
17 void update_value() {
18 double tmp[16];
19 for(int i = 0; i < 16; i++) { //...
20 }
21 if(value_stop_condition) {
22 convergence_detected = true;
23 for(int i = 0; i < 16; i++) {
24 if(!(value[i]-1E-2 < tmp[i] && value[i]+1E-2 > tmp[i]))
25 convergence_detected = false;
26 }
27 }
28 memcpy(value, tmp, 16*sizeof(double));
29 }
30 void update_policy() {
31 bool tmp[16][4] = {false};
32 for(int i = 0; i < 16; i++) { //...
33 }
34 if(policy_stop_condition) {
35 convergence_detected = true;
36 for(int i = 0; i < 16; i++)
37 for(int j = 0; j < 4; j++) {
38 if(!(prev[i][j]-1E-2 < tmp[i][j] &&
39 prev[i][j]+1E-2 > tmp[i][j])) convergence_detected = false;
40 }
41 }
42 memcpy(prev, tmp, 16*4*sizeof(bool));
43 }
44 void generate() {
45 init();
46 if(value_stop_condition || policy_stop_condition) {
47 convergence_detected = false;
48 }
49 for(int i = 0; i < 10000; i++) {
50 update_value();
51 update_policy();
52 if(convergence_detected) { printf("convergence at %d\n", i); break; }
53 }
54 print();
55 print_prev();
56 }
57 };
58 //...
64
Dont l’appel devient dans le cas d’une condition d’arrêt sur l’évaluation vi :
1 int main(int _ac, char** _av) {
2 grid_t G;
3 G.value_stop_condition=true;
4 G.generate();
5 return 0;
6 }
Dans les deux cas, la convergence est atteinte à la quatrième itération, avec
l’évaluation vi et la politique πi pour (i = 3).
65
9.6 Apprentissage par renforcement
Dans le cas d’espaces d’états trop grands pour être complètement énumérés, les
quatres solutions courantes combinant modes de progression et principes des
MDPs pour établir une estimation de la politique à suivre pour résoudre un
problème de décision sont : 1) SARSA, 2) Q-learning, 3) SARSA-λ, et 4) le
double apprentissage.
66
L’apprentisage Q-learning reprend le principe de SARSA en séparant politique
apprise et politique suivie ; cette solution augmente la vitesse de convergence de
l’apprentissage ; la ligne 8 de Alg. 44 devient :
L’ensemble Q n’est pas toujours utile dans sa totalité ; selon les conditions défi-
nies par les états et les actions, certains états sont non atteignables ; pour éviter
de définir initialement l’ensemble Q, Alg. 45 présente une variante d’apprentis-
sage Q-learning dans lequel Q est défini incrémentalement.
1 for nb_episodes do
2 S ← initialize ( ) ;
3 if {S, randomMove ( ) } ∈
/ Q then
4 for each m in nextMove ( S ) do
5 update ( Q, {S, m}, random ( )) ;
6 A ← -greedy ( S, Q) ;
7 for nb_loop do
8 {S 0 , R0 } ← apply ( S, A) ;
9 if {S, A} ∈ / Q then
10 for each m in nextMove ( S ) do
11 update ( Q, {S, m}, random ( )) ;
12 A0 ← -greedy ( S 0 , Q) ;
13 q(S, A) ← q(S, A) + α ( R0 + γ maxa (q(S 0 , a)) − q(S, A)) ;
14 {S, A} ← {S 0 , A0 } ;
15 if S == terminal then break ;
67
Alg. 46 présente l’apprentissage SARSA-λ selon la formule originelle de l’esti-
mation T D(λ).
La fonction update appelée ligne 9 réalise une MAJ accumulative ou une MAJ
avec réinitialisation (conformément à sa définition page 56).
36. Les quatres valeurs courantes du critère de performance sont la somme de taille fixe, la
somme gamma-pondérée, la somme totale et la moyenne (cf. section 9.1) ; pour T D(2) avec
la somme gamma-pondérée pour critère de performance de la politique π appliquée à partir
de S 0 , on a : a π(a|S 0 )q(S 0 , a) = (R0 + γ R00 + γ 2 q(S 00 , A00 )).
P
68
Le double apprentissage propose d’apprendre deux fonctions d’évaluation Q1 et
Q2 simultanément pour réduire les écarts entre les valeurs estimées et les valeurs
optimales et ainsi accélerer la convergence de la politique vers la politique op-
timale ; ce principe s’applique à SARSA, au Q-learning et à SARSA-λ ; Alg. 48
présente ce principe appliqué à SARSA.
69
9.7 Apprentissage profond
9.8 Application aux jeux Atari 2600
70
9.9 Application à StarCraft-II
StarCraft-II (abrégé SC2) est un jeu de stratégie temps-réel dans lequel trois
races s’affrontent pour le contrôle de cartes ; les trois races sont les Terran,
les Protoss et les Zerg ; les cartes contiennent des minerais qui permettent de
construire des bâtiments et de produire des unités ; chaque unité possède une
capacité particulière ; les pages suivantes présentent 37 les unités de chaque race,
leurs particularités, leur coût en ressources, leur coût en support et leur temps
de production ; on note « attaque T » pour les unités pouvant attaquer les unités
terrestres adverses et « attaque A » pour les unités pouvant attaquer les unités
aériennes adverses ; les coûts sont en M, G et E, pour Minerai, Gaz et Energie ;
le support (abrégé Sup.) est noté en places dans les unités de ravitaillement (i.e.
les Supply depot pour les Terran, les Overlord pour les Zerg et les Pylon pour
les Protoss) ; une unité est caractérisée par des points de vie (abrégés Pv.), une
armure (abrégée Arm.), une vitesse de déplacement (abrégée Vit.), des dégats
par seconde (abrégés Dps.), un temps de cooldown 38 (abrégé Coold.), une portée
et une vision (abrégée Vis.) ; les temps sont en secondes ; les unités Protoss ont
en plus une caractéristique de shield (abrégé Shd.), dépendant d’une gestion
d’énergie.
Les unités sont regroupées en deux catégories : les unités légères (abrégées Leg.)
et les unités blindées (abrégées Bld.) ; selon ces deux catégories, les dégats infli-
gés peuvent varier.
La résolution d’une attaque dépend des unités et des cas ; dans le cas général,
les dommages minimum d’une attaque sont de 1 ; les dommages maximum sont
les dégats de l’unité attaquante moins l’armure de l’unité recevant l’attaque ;
pour une unité Protoss, on commence par prendre sur le bouclier (i.e. Shd.) ;
quand le bouclier n’est plus, on utilise le cas général 39 :
71
Les unités Terran
Le bâtiment Command center supporte les unités, stocke minerai et gaz, per-
met de construire le bâtiment Engineering bay et produit des unités SCV ;
les bâtiments Planetary Fortress et Orbital Command sont des évolutions du
Command center ; le Planetary Fortress attaque T et A ; le Orbital Command
peut produire l’unité MULE, demander un Extra supplies et réaliser des Scanner
Sweep ; le bâtiment Supply depot supporte les unités et permet de construire des
Barracks ; le bâtiment Refinery permet la récolte du gaz ; le Barracks permet
de construire les bâtiments Orbital command, Bunker, Factory, Ghost academy
et de produire les unités Marine, Marauder, Reaper et Ghost ; le Engineering
bay permet de construire les bâtiments Missile turret, Sensor tower, Planetary
fortress et de développer les améliorations Infantry weapons, Armors, Hi-sec,
Neosteel ; le Sensor tower possède une vision avec une portée de 12 ; le Missile
turret possède des attaque A avec une portée de 7, avec 39.3 de Dps., un Cool-
down de 0.6 et une vision de 11 ; le Factory permet de construire les bâtiments
Starport, Armory, Tactical nuke, et de produire les unités Hellion, Widow mine,
Siege tank, Cyclone, Hellbat et Thor ; le Ghost academy permet de produire les
unités Ghost et les capacités associées ; le Starport permet de construire le bâ-
timent Fusion core et de produire les unités Viking, Medivac, Raven, Banshee,
Liberator et Battlecruiser ; le bâtiment Armory permet de produire les unités
Thor, Hellbat et de développer Vehicule weapons, Vehicule skills, Ship weapons ;
72
le Fusion Core permet de produire l’unité Battlecruiser ; le Tech lab permet de
développer des technologies relatives aux unités et aux bâtiments ; le bâtiment
Reactor permet d’augmenter la capacité de production de certains bâtiments.
Les bâtiments Terran ont également la particularité d’être réparables par les
SCV.
Les unités peuvent voir leurs capacités augmentées avec les stimpack (abrégés
Spk.), les Infernal Pre-Igniter (abrégés Ipi.) ; les Spk. durent 11 sec et les at-
taques splash (abrégées Spl.) augmentent les dégats ; l’utilisation de Spk. coûtent
10 Pv. aux unités Marines et 20 aux unités Marauder.
La stratégie classique avec les Terran est de déplacer les bâtiments, de multiplier
les bases et de pratiquer l’harassement des adversaires sur la carte.
73
Unité Pv. Arm. Vit. Dps. Coold. Portée Vis.
SCV 45 0:1 3.9 5 1 0.1 8
MULE 60 0:1 3.9 - - - 8
Marine 45:55 0:1 3.1 9.8 : 11.4 0.6:0.8 5 9
(+ Spk.) +1.6 +5.9
Reaper 60 0:1 5.2 10.1 : 12.6 0.8 5 9
Marauder 125 1:2 3.1:4.7 9.3 : 10.4 0.7:1 6 10
(+Spk.) +1.4 +5.9
(vs. Bld.) +9.3 : 10.2
(vs. Bld. + Spk.) +14.1 : 16.9
Ghost 100 0:1 3.9 9.3 : 10.2 1.1 6 11
(vs. Leg.) +9.3 : 11
Hellion 90 0:1 6 4.5 : 5 1.8 5 10
(vs. Leg.) +3.4 : 4
(vs. Leg. + Ipi.) +6.2 : 6.7
Hellbat 135 0:1 3.2 12.6 : 14 1.4 2 10
(vs. Leg.) +0 : 0.7
(vs. Leg. + Ipi.) +8.4 : 9.1
Widow mine 90 0:1 3.9 125 - 5 7
(+Spl.) (+40)
(vs. Shd. (+Spl.)) +35(+25)
Siege tank 175 1:2 3.15 20.3 : 23 1 7 11
(vs. Bld.) +13.5 : 14.8
(siège) 0 18.7 : 20.5 2.1 13 11
(siège vs. Bld.) +14 : 14.5
Thor 400 2:3 2.6 65.9.2 : 72.5 0.9 7 10
(expl.) 11.2 : 13.1 2.1 10
(expl. vs. Leg.) +11.2 : 13.1
(impact.) 16.3 : 17.7 2.1
(impact. vs. Bld.) +7 : 7.9 2.1
Viking 135 0:1
(att. T) 3.1 16.8 : 18.2 0.7 6 10
(att. T. vs. Mec.) +11.3
(att. A) 3.8 14 : 15.4 1.4 9
(att. A. vs. Bld.) +5.6
Medivac 150 1:2 3.5:5.9 - - - 11
Liberator 180 0:1 4.7
(att. A) 65.8 : 70.2 1.1 10 : 14 10
(mode def. att. T) 7.8 : 9.2 1.3 5 13:17
Raven 140 1:2 3.9 - - - 11
Banshee 140 0:1 3.9:5.2 27 : 29.2 0.9 6 10
Battlecruiser 550 3:4 2.6
(att. T) 50 : 56.2 0.2 6 12
(att. A) 37.5 : 43.7 0.2 6 12
Auto-turret 150 1:3 - 31.2 0.6 6:7 7
74
Les unités Zerg
Pour chaque unité Larva, il est possible d’obtenir : un Drone ou deux Zergling
ou un Hydralisk ou un Roach ou un Infestor ou un Swarm Host ou un Ultralisk
ou un Overlord ou un Mutalisk ou un Corruptor ou un Viper ; les Larva appa-
raissent automatiquement autour des bâtiments Hatchery, Lair, et Hive ; leur
fréquence d’apparition est de une Larva toutes les 11 secondes ; le nombre de
Larva non transformée est limité à 3 ; pendant sa transformation, la Larva de-
vient un cocon, dont avec 200 Pv. et 10 d’armure (sauf pour le Baneling 50 Pv.
et 2 en Arm., pour Overser et Broodlord 2000 Pv. et 2 en Arm., pour le Ravager
100 Pv. et 5 en Arm. et pour le Lurker 100 Pv. et 1 en Arm. ; une Queen est
une unité produite par un bâtiment Hatchery après avoir construit un Spawning
75
pool ; la Queen possède le sort Spawn larva qui permet d’augmenter la produc-
tion de Larva ; le Brood lord est obtenu par évolution de Corruptor ; il apparait
avec des Broodling ; le Infested terran est obtenu par lancement d’un sort sur un
Terran par un Imphestor ; la transformation prend 3 secondes ; l’oeuf possède
70 Pv. et 2 d’armure ; le Nydus worm est constructible dans les parties visibles
de la carte, après construction d’un Nydus network.
76
les bâtiments Hive, Infestor et Swarm host ; le Hive permet de construire les
bâtiments Ultralisk caverne, Greater spire et de produire des unités Viper ; le
Greater spire permet de produire des unités Brood lord, et d’améliorer les unités
volantes ; le Ultralisk cavern permet de produire des unités Ultralisk.
77
Les attaques du Baneling sont suicidaires et leur explosion implique une aire
d’effet ; leur dommage dépend du type de la cible ; ainsi les dommages sur les
unités légères seront de 25 si une unité légère était la cible et seront de 15 si
la cible était un bâtiment proche ; chaque attaque est sujette à des bonus splash.
Les bâtiments Zerg produisent un mucus qui se répand sur la carte ; Roach,
Ravager, Hydralisk, Lurker, Swarm host, Locust, Infestor, Ultralisk obtiennent
un bonus en vitesse de 30% sur ce mucus.
Les caractéristiques du Changeling varient légèrement selon les unités qu’il copie.
La stratégie classique avec les Zerg est de réaliser des rush avec des groupes de
Zergling.
Le coût de l’unité Archon varie selon les cas : 1) avec 2 High templar, il est de
100M + 300G, 2) avec 1 High templar et 1 Dark templar, il est de 175M + 275G,
3) avec 2 Dark templar, il est de 250M + 250G.
Le coût des unités pour leur transport via Warp prism varie selon les unités :
il est de 1 pour une unité Probe, de 2 pour une unité Zealot, Sentry, Stalker,
78
Adept, High Templar, Dark Templar, de 4 pour une unité Immortal, Disruptor,
Archon, et de 8 pour une unité Colossus ; le Warp prism possède une capacité
de transport de 8.
Le bâtiment Nexus supporte les unités, stocke minerai et gaz, permet de construire
les bâtiments Gateway et Forge, et produit des unités Probe et Mothership ; le
bâtiment Pylon supporte les unités et fournit de l’énergie aux autres bâtiments
situés jusqu’à une distance de 9 ; sans énergie, les bâtiments Protoss sont hors-
service ; le bâtiment Assimilator permet la récolte du gaz ; le Gateway permet de
construire le bâtiment Cybernetics core, et de produire les unités Zealot, Stalker,
Sentry, Adept, High templar et Dark templar ; le Forge permet de construire le
bâtiment Photon cannon, et d’améliorer les unités terrestres ; le Photon cannon
est une défense T et A de 22.4 Dps ; la portée est de 7, la vision de 11, le Co-
oldown de 0.9 ; le Cybernetics core permet de construire les bâtiments Twilight
council, Stargate, Robotics facility, Shield battery et Warp gate, et de produire
les unités Sentry et Adept, et d’améliorer les unités volantes ; le Stargate permet
de construire le bâtiment Fleet beacon, et de produire les unités Phoenix, Oracle,
Void ray, Tempest et Carrier ; le Twilight council permet de construire les bâti-
ments Templar archives et Dark shrine ; le Robotics bay permet de produire les
unités Colossus et Disruptor ; le Shield battery permet de recharger les boucliers
des unités et des bâtiments plus rapidement ; le Fleet beacon permet de produire
les unités Carrier, Mothership et Tempest ; le Templar archives permet de pro-
duire les unités High templar, et Archon ; le Dark shrine permet de produire les
unités Dark templar, et Archon.
79
Unité Pv. Shd. Arm. Vit. Dps. Coold. Portée Vis.
Probe 20 20 0:1 3.9 4.7 1.1 0.1 8
Zealot 100 50 1:2 3.2 18.6 : 20.9 0.9 0.1 9
(+ Charge) 4.1:4.6
Stalker 80 80 1:2 4.1 9.7 : 10.5 1.3 6 10
(vs. Bld.) +3.7 :4.5
Sentry 40 40 1:2 3.2 8.4 : 9.8 0.7 5 10
Adept 70 70 1:2 3.5 6.2 : 6.8 1.1:1.6 4 9
(vs. Leg.) +7.5:8.1
High templar 40 40 0:1 2.6 3.2 : 4 1.2 6 10
Dark templar 40 80 1:2 3.9 37.2 : 41.3 1.2 0.1 8
Archon 10 350 0:1 3.9 20 : 22.4 1.3 3 9
(vs. Bio) +8:8.8
Observer 40 20 0:1 2.6:3.9 - - - 11:14
Warp prism 80 100 0:1 4.1:5.4 - - - 10
Immortal 200 100 1:2 3.1 19.2 : 21.1 1.1 6 9
(vs. Bld.) +28.9 : 31.8
Colossus 200 150 1:2 3.2 18.7 : 20.6 1.1 7:9 10
(vs. Leg.) +9.3 : 11.2
Disruptor 100 100 1:2 3.2 9
Phoenix 120 60 0:1 6 12.7 : 15.2 0.8 5:7 10
(vs. Leg.) +12.7
Void ray 150 100 0:1 3.5 16.8 : 19.6 0.4 6 10
(+ Prism.) -1.4 +11.2
(vs. Bld.) +28
Oracle 100 60 0:1 5.6 24.4 0.6 4 10
Tempest 300 150 2:3 2.6 12
(att. T) 17 : 18.7 2.4 10
(att. A) 12.7 : 14.0 2.4 15
(vs. Mass.) +9.3 : 10.2
Carrier 250 150 2:3 2.6 8 12
Interceptor 40 40 0:1 2.1 4.7 : 5.6 2.1 +2 7
Mothership 350 350 2:3 2.6 22.8 : 26.6 1.6 7 14
Stasis ward 30 30 0:1
Les unités Zealot peuvent accélerer avec la capacité Charge ; les Archon ont des
bonus contre les unités biologiques (abrégées Bio.) ; les Tempest ont des bonus
contre les unités massives (abrégées Mass.).
80
PySC2
Le kit PySC2 2.0.1 permet d’écrire en Python des IA jouant à SC2 ; ce kit per-
met le lancement de missions SC2, la récupération des perceptions (içi appelées
observations), la prise de décision (içi appliquée au travers d’actions) et la ges-
tion de replay ; ce kit contient également des problèmes à un joueur, appelés
minigames ; avec Python 3.5.2 et PySC2 2.0.1, on peut lancer un joueur vide
(présenté ci-dessous) dans un minigame :
python3 -m pysc2.bin.agent --map BuildMarines \
--agent empty.Simple
1 from pysc2.agents import base_agent
2 from pysc2.lib import actions
3
4 class Simple(base_agent.BaseAgent):
5 def step(self, obs):
6 super(Simple, self).step(obs)
7 return actions.FUNCTIONS.no_op()
Pour avoir la liste des options possibles et leur valeur par défaut :
python3 -m pysc2.bin.agent --helpfull
40. Le lecteur concerné pourra faire une doc txt des fonctions correspondants aux actions
avec une simple redirection : cmd > doc.txt.
81
Dans un joueur programme, on pourra visualiser la liste des actions possibles à
chaque instant avec :
1 for action in obs.observation.available_actions:
2 print(actions.FUNCTIONS[action])
Les types sont : screen pour un point sur l’écran ; minimap pour un point
sur la minimap ; screen2 pour un second point 41 sur l’écran ; queued pour
une possibilité d’exécution différée ; control_group_act pour une action de
groupe ; control_group_id pour un groupe ; select_point_act pour une ac-
tion d’une unité en un point ; select_add pour ajouter une unité à une sélection ;
select_unit_act pour une action d’une sélection ; select_unit_id pour une
sélection par id ; select_worker pour une action d’un mineur ; build_queue_id
pour une file d’attente de construction ; unload_id pour une sélection à déchar-
ger.
Les observations sont une abstraction des situations vues à l’écran et de données
supplémentaires (telles que le score et la reward ).
41. Dans PySC2, les fonctions n’acceptent pas deux arguments de même type ; cette restric-
tion semble provenir du principe qu’une fonction comportant deux arguments de même type
est source de confusion et de bug ; en imposant cette restriction, les concepteurs de PySC2
ont probablement pensé réduire les confusions entre les arguments de même type.
82
Système de coordonnées
Dans PySC2, les unités sont positionnées dans deux vues (i.e. deux repères) :
— le screen, correspondant à la vue principale ; la dimension du screen par
défaut est de 84 × 84 ; le screen correspond une vue d’une partie de
la minimap, donnée par la caméra ; pour déplacer la caméra, utiliser la
fonction move_camera.
— la minimap, correpondant à une vue complète de la carte, en basse ré-
solution, située en bas à gauche sur l’interface classique ; elle montre les
hauteurs du terrain, le brouillard, les unités, la partie correspondant à
la vue du screen ; pour les unités, on a le joueur associé avec son type
(ami/ennemi) ; la dimension de la minimap par défaut est de 64 × 64.
Dans le screen comme dans la minimap, le point de coordonnées (0,0) est en
haut à gauche ; les axes sont orientés comme suit :
*---Ox
|
Oy
Pour les observations 42 , les coordonnées 43 sont lues dans l’ordre (y,x) :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 unit_y, unit_x = (unit_type == units.Terran.SCV).nonzero()
On notera l’utilisation de la fonction nonzero qui permet de passer d’une ma-
trice de labels à une liste de coordonnées dans la matrice considérée.
Pour les actions, les coordonnées 44 sont données dans l’ordre (x,y) :
1 dest = [int(unit_x[0]), int(unit_y[0])]
2 return actions.FUNCTIONS.move_screen("now", dest)
42. Les observations sont données en features dont la définition dépend de la résolution du
screen et de la minimap ; de plus, le nombre de ces features dépend de la taille de la fenêtre du
screen ; la position précise d’une unité est définie par le barycentre des positions des features
de cette unité.
43. Les identifiants des unités considérées (içi SCV ) sont définies dans pysc2/lib/units.py.
44. Indiquer des coordonnées négatives est incohérent ; déplacer le screen en dehors de la
minimap est incohérent.
83
Observations
Les observations regroupent un ensemble de matrices et de vecteurs de données ;
Le contenu de cet ensemble est défini dans pysc2/lib/features.py ; le vecteur
score_cumulative contient 13 valeurs ordonnées comme suit :
— score : le score (qui cumule les reward 45 )
— idle_production_time : le temps de production inactive
— idle_worker_time : le temps inactif des unités de collecte de ressources
— total_value_units : la valeur totale des unités
— total_value_structures : la valeur totale des bâtiments
— killed_value_units : la valeur des unités tuées
— killed_value_structures : la valeur des bâtiments détruits
— collected_minerals : le minerai collecté
— collected_vespene : le gaz collecté
— collection_rate_minerals : le taux de collecte du minerai
— collection_rate_vespene : le taux de collecte du gaz
— spent_minerals : le minerai dépensé
— spent_vespene : le gaz dépensé
Après 200 tours dans le scénario Simple64, on obtient :
1 print(obs.observation["score_cumulative"])
[1110 12 0 600 400 0 0 60 0 403 0 0 0]
Ce qui correspond à un score de 1110, pour un temps de production inactive de
12, pour 60 de minerai collecté ; on accède également au minerai collecté comme
suit :
1 print(obs.observation["score_cumulative"]["collected_minerals"])
60
Pour obtenir la liste des features des SCV présents dans le screen, on utilise une
matrice des types des unités (içi unit_type), restreinte aux unités SCV (dans
unit_scv) puis convertie en coordonnées des unités du type SCV à l’aide de la
fonction nonzero :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 unit_scv = (unit_type == units.Terran.SCV)
4 unit_y, unit_x = unit_scv.nonzero()
45. Les reward sont les récompenses immédiates obtenues au cours des missions ; pour De-
featRoaches, les reward sont de -1 par Marine perdu et de +10 par Roach éliminé ; les valeurs
de reward sont visibles aux instants concernés via la variable obs.reward.
46. Dans la version 1.2 de PySC2, feature_screen était screen et feature_minimap était
simplement minimap.
84
Pour une unité, le nombre de features dépend du type de l’unité et de la ré-
solution de la vue considérée (i.e. le screen avec 84 × 84 ou la minimap avec
64 × 64) ; dans la mission DefeatRoaches (qui comporte initialement 9 Marines
et 4 Roaches), avec les résolutions par défaut, on a 81 features Marine et 46
features Roaches 47 :
1 _UNIT_TYPE = features.SCREEN_FEATURES.unit_type.index
2 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
3 m_y, m_x = (unit_type == units.Terran.Marine).nonzero()
4 r_y, r_x = (unit_type == units.Zerg.Roach).nonzero()
5 print("m:%d r:%d" %(len(m_x), len(r_x)))
m:81 r:46
Ci-dessous trois exemples de répartition des features pour des minerais (les
features des minerais sont représentés par des « x » et les espaces par des « . ».)
dans la résolution par défaut (i.e. 84 × 84 pour le screen et 64 × 64 pour la
minimap) :
......
..xx.. un minerai
.xxxx.
.xxxx.
..xx..
......
............
..xx....xx.. deux minerais espacés
.xxxx..xxxx.
.xxxx..xxxx.
..xx....xx..
............
.........
..xx.xx.. deux minerais serrés
.xxxxxxx.
.xxxxxxx.
..xx.xx..
.........
47. Pour des unités espacées, un Marine équivaut à 9 features et un Roach à 12 features ;
pour des unités serrées, ces valeurs peuvent diminuer.
85
Fig. 7 présente l’IHM du kit PySC2 à l’exécution de la mission DefeatRoaches ;
screen et minimap sont présentés dans la partie gauche ; la minimap est une
vue de l’ensemble de la carte, presentée dans un cadre en rouge ; la position
screen dans la minimap est identifié par un cadre blanc ; le screen est présenté
en détail au dessus, avec 4 Marines et 9 Roaches ; dans la partie droite de l’IHM,
différentes features 48 sont présentées ; Fig. 8 montre Fig. 7 dans SC2.
48. De gauche à droite, sur la première ligne : minimap height_map, minimap visibi-
lity_map, minimap creep, minimap camera, minimap player_id, et sur la deuxième ligne :
minimap player_relative, minimap selected, screen height_map, screen visibility_map, screen
creep, et sur la troisième ligne : screen power, screen player_id, screen player_relative,
screen unit_type, screen selected, et sur la quatrième ligne : screen unit_hit_points, screen
unit_hit_points_ratio, screen unit_energy, screen unit_energy_ratio, screen unit_shields, et
sur la dernière ligne : screen unit_shields_ratio, screen unit_density, screen unit_density_aa,
screen effects.
86
Simplifier les observations
Afin de simplifier la présentation des informations relatives aux unités présentes
dans le screen, il est possible d’activer l’option use_feature_units qui permet
d’obtenir une interprétation des
87
Missions, scénarios et épisodes
Les missions associées aux minigames sont définies dans docs/mini_games.md.
3 class SimpleAgent(object):
4 def __init__(self):
5 self.episodes = -1
6 self.steps_episode = -1
7 self.steps_game = -1
8 print("-- __init__")
9
10 def __del__(self):
11 print("-- __del__")
12
16 def reset(self):
17 self.steps_episode = -1
18 self.episodes += 1
19 print("-- -- reset")
20
Les fonctions __init__ et setup sont appelées une seule fois au début de l’en-
semble des épisodes ; la fonction __init__ initialise les variables locales et la
fonction setup définit les observations et actions possibles ; la fonction reset
est appelée au début de chaque nouvel épisode ; la fonction step est appelée à
chaque itération de simulation 49 ; la fonction __del__ est appelée à la fin d’un
scénario.
49. La fréquence d’appel de la fonction step est liée avec la valeur de step_mul ; pour un
step_mul de 1, selon les spécifications de PySC2, le simulateur appelle de 16 à 22 fois la
fonction step par seconde ; pour un temps de 10 secondes, on a un nombre théorique d’appels
à la fonction step compris entre 160 et 220 ; en pratique, on observe parfois des variations de
10 à 30 appels par seconde.
88
L’exécution de la mission DefeatRoaches avec 2 épisodes produit le scénario sui-
vant 50 :
python3 -m pysc2.bin.agent --map DefeatRoaches \
--agent print_steps.SimpleAgent --max_episodes 2
-- __init__
-- setup
-- -- reset
-- -- step 0 episode 0 (0)
-- -- step 1000 episode 0 (1000)
-- -- reset
-- -- step 2000 episode 1 (80)
-- -- step 3000 episode 1 (1080)
-- __del__
Les épisodes permettent de répéter une même mission, correspondant à une suite
d’appels aux fonctions reset et step, en suivant le schéma défini par Alg. 49.
1 call __init__ ;
2 call setup;
3 for nbepisodes do
4 call reset;
5 while episode not ended do
6 call step;
7 call __del__;
Alg. 49: Boucle d’exécution d’une mission.
5 class SimpleAgent(base_agent.BaseAgent):
6 def reset(self):
7 self.step_cnt = -1
8 def step(self, obs):
9 super(SimpleAgent, self).step(obs)
10 self.step_cnt += 1
11 time.sleep(1)
12 print("think %d" %(self.step_cnt))
13 return actions.FUNCTIONS.no_op()
50. Dans la mission DefeatRoaches, un épisode s’achève après 1920 steps ou quand les Ma-
rines sont morts ; comme le joueur print_steps laisse les Marines inactifs, les épisodes s’ar-
rêtent après 1920 steps ; avec un step_mul de 8, on a 8 fois moins d’appels à la fonction step
et les lignes "step 1000", "step 2000" et "step 2000" ne s’affichent plus.
89
Exercices
1. Scanner exhaustivement Simple64 avec la fenêtre définie par le screen
2. Scanner les positions clés de Simple64 avec la fenêtre définie par le screen
3. Envoyer un SCV dans chaque coin de Simple64
4. Envoyer trois SCV aux positions clés de Simple64
5. Envoyer trois SCV aux positions clés de Simple64 et scanner leur posi-
tion avec la fenêtre définie par le screen
6. Définir une stratégie défensive dans Simple64 (Terran, Protoss, Zerg)
7. Définir une stratégie défensive générique (Terran, Protoss, Zerg)
8. Définir une stratégie d’expansion dans Simple64 (Terran, Protoss, Zerg)
9. Définir une stratégie d’expansion générique (Terran, Protoss, Zerg)
10. Définir une stratégie de rush dans Simple64 (Terran, Protoss, Zerg)
11. Définir une stratégie de rush générique (Terran, Protoss, Zerg)
12. Résoudre MoveToBeacon
13. Résoudre CollectMineralShards
14. Résoudre FindAndDefeatZerglings
15. Résoudre DefeatRoaches
16. Résoudre DefeatZerglingsAndBanelings
17. Résoudre CollectMineralsAndGas
18. Résoudre BuildMarines
90
1ère solution pour MoveToBeacon
Cette solution utilise un FSM 51 à deux états.
6 #Functions
7 _MOVE_SCREEN = actions.FUNCTIONS.Move_screen.id
8
9 # Features
10 _PLAYER_NEUTRAL = features.PlayerRelative.NEUTRAL
11
12 def _xy_locs(mask):
13 y, x = mask.nonzero()
14 return list(zip(x, y))
15
16 #
17 # python3 -m pysc2.bin.agent --map MoveToBeacon \
18 # --agent simple_agent.SimpleAgent --max_episodes 1
19 #
20 class SimpleAgent(base_agent.BaseAgent):
21
Dans SC2, les unités neutres sont assez rares ; balises et mineraux sont des élé-
ments neutres.
Cette mission est composée de deux actions : la sélection des unités Marines et
leur déplacement vers le Beacon (i.e. une balise lumineuse).
91
2ème solution pour MoveToBeacon
Cette solution utilise le même FSM que la 1ère solution ; seule l’écriture du pro-
gramme diffère ; en réalisant les appels de fonction avec la procédure FunctionCall,
on obtient un message d’erreur indiquant les arguments attendus et les argu-
ments utilisés en cas de mauvaise utilisation ; ainsi FUNCTIONS.no_op() est rem-
placé par l’appel FunctionCall(_NO_OP, []).
5 #Functions
6 _NO_OP = actions.FUNCTIONS.no_op.id
7 _MOVE_SCREEN = actions.FUNCTIONS.Move_screen.id
8 _SELECT_ARMY = actions.FUNCTIONS.select_army.id
9
10 # Features
11 _PLAYER_RELATIVE = features.SCREEN_FEATURES.player_relative.index
12 _PLAYER_NEUTRAL = features.PlayerRelative.NEUTRAL
13
14 # Parameters
15 _NOT_QUEUED = [0]
16 _SELECT_ALL = [0]
17
18 #
19 # python3 -m pysc2.bin.agent --map MoveToBeacon \
20 # --agent simple_agent.SimpleAgent --max_episodes 1
21 #
22 class SimpleAgent(base_agent.BaseAgent):
23
92
1ère solution pour DefeatRoaches
La mission DefeatRoaches est un problème de micro-gestion 52 , dans lequel il
s’agit de définir la politique optimale pour les actions des unités contre des uni-
tés ennemis.
Dans cette mission, il s’agit d’affronter des Roaches à l’aide d’un ensemble de
Marines ; les Roaches sont des unités terrestres Zerg ; les Marines sont des unités
terrestres Terran ; par groupe de 4, des Roaches apparaissent face aux Marines ;
pour chaque victoire, les Marines obtiennent un renfort de 5 unités ; l’objectif
des Marines est de tenir le plus longtemps possible.
1 if state == 0 then
2 if select_all is available then
3 state ← 1 ;
4 return select_all ( );
5 else return noop ( );
6 if state == 1 then
7 {Y, X} ← getEnemies ( Obs ) ;
8 if X == ∅ then return noop ( );
9 target ← bestEnemy ( X , Y ) ;
10 return attack ( target );
Alg. 50: Une solution à deux états pour accomplir la mission Defea-
tRoaches.
52. Dans les jeux de stratégie temps-réel, la micro-gestion (i.e. micro-ing) concerne la ges-
tion des unités et la macro-gestion (i.e. macro-ing) concerne la gestion des batiments ; dans
SC2, il apparaît également des stratégies de découverte de la carte (i.e. scouting), d’ordre de
construction (i.e. build orders), d’épuisement par agression permanente (i.e. harassment), de
contrôle de positions clés sur la carte (i.e. map control) et de composition des squads (i.e.
unit composition).
93
2ème solution pour DefeatRoaches
Pour ajouter un peu de micro-gestion dans l’automate de la Fig. 9, en prenant
en compte les portées de tir et les temps de cooldown, il est possible de mainte-
nir les Marines à distance des Roaches, de façon à être hors de la portée de tir
des Roaches tout en gardant les Roaches à portée de tir des Marines ; ce com-
portement, qui vise à attaquer en reculant, appelé hit-and-run 53 , est défini par
Alg. 51, représenté Fig. 10 ; les Marines reculent quand ils sont à une distance
des Roaches inférieure à drun .
1 if state == 0 then
2 if select_all is available then
3 state ← 1 ;
4 return select_all ( );
5 return noop ( );
6 if state == 1 then
7 {Y, X} ← getPositions ( Obs ) ;
8 if X == ∅ then return noop ( );
9 A ← average ( X, Y ) ;
10 state ← 2 ;
11 return noop ( );
12 if state == 2 then
13 {Y 1, X1} ← getEnemies ( Obs ) ;
14 {Y 2, X2} ← getPositions ( Obs ) ;
15 if X1 == ∅ || X2 == ∅ then return noop ( );
16 if new_episode then
17 state ← 0 ;
18 return noop ( );
19 if dist (X1, Y 1, X2, Y 2) < drun then
20 return run ( A);
21 return attack ( X1, Y 1);
Alg. 51: Comportement hit-and-run à trois états pour accomplir la mis-
sion DefeatRoaches.
94
Ci-dessous les fonctions correspondants aux principales fonctions du compor-
tement hit-and-run de Fig. 10 ; la distance visant à tenir les Roaches hors de
portée est içi fixée à 19.7.
1 def reset(self):
2 super(SimpleAgent, self).reset()
3 self.state = 0
4 self.initial_marines_x = 0.0
5 self.initial_marines_y = 0.0
6 self.nb_roaches = 100
7
95
Evaluation MC de drun pour DefeatRoaches
Cette section présente une solution MC (cf. Alg. 13 page 29) pour l’évaluation
des valeurs de drun dans le comportement hit-and-run appliqué à la mission
DefeatRoaches ; le programme définissant le comportement hit-and-run est mo-
difié, en ajoutant la fonction set_d_run pour pouvoir redéfinir la valeur de drun
comme suit :
1 def reset(self):
2 self.d_run = 10.0
3
8 MISSION = "DefeatRoaches"
9 AGENT = defeat_roaches.SimpleAgent()
10 VISUAL = False
11 NB_DIST = 1
12 NB_TEST = 2
13 SMALLEST_DIST = 19.7
14 NB_STEPS = 1920
15
16 class EvalMcDrunDefeatRoaches(utils.TestCase):
17 step_mul = 1
18 steps = NB_STEPS/step_mul
19 max_episodes = 1
20 sum_scores = np.zeros(NB_DIST)
21 sum_scores2 = np.zeros(NB_DIST)
22 nb_scores = np.zeros(NB_DIST)
96
Selon la valeur de visualize, l’interface SC2 affiche le déroulement des scéna-
rios ; le nombre de scénarios correspond au nombre d’appels de run_loop avec
pour paramètre un programme joueur.
23 def setUp(self):
24 super(EvalMcDrunDefeatRoaches, self).setUp()
25 sc2_env.stopwatch.sw.enabled = False
26
27 def test(self):
28 with sc2_env.SC2Env(
29 map_name=MISSION,
30 visualize=VISUAL,
31 agent_interface_format=sc2_env.AgentInterfaceFormat(
32 feature_dimensions=sc2_env.Dimensions(
33 screen=84, minimap=64)),
34 step_mul=self.step_mul,
35 game_steps_per_episode=self.steps * self.step_mul) as env:
36 agent = AGENT
37 for j in range(0,NB_TEST) :
38 for i in range(0,NB_DIST) :
39 agent.set_d_run(float(SMALLEST_DIST+i))
40 run_loop.run_loop([agent], env, self.steps, self.max_episodes)
41 self.sum_scores[i] += agent.score
42 self.sum_scores2[i] += (agent.score*agent.score)
43 self.nb_scores[i] += 1
44 print("----------------------------------------")
45 print("dist sum_scores nb_scores => avg std_dev")
46 for i in range(0,NB_DIST) :
47 avg = float(self.sum_scores[i]) / float(self.nb_scores[i])
48 avg2 = float(self.sum_scores2[i]) / float(self.nb_scores[i])
49 std_dev = np.sqrt(avg2 - (avg*avg))
50 print("%d %d %d => %.2f %.2f" %(SMALLEST_DIST+i,
51 self.sum_scores[i],
52 self.nb_scores[i],
53 avg,
54 std_dev))
55 print("----------------------------------------")
56
57 if __name__ == "__main__":
58 absltest.main()
97
En utilisant le programme de test précédent (i.e. testDefeatRoaches.py), avec
les paramètres suivants,
MISSION = "DefeatRoaches"
AGENT = defeat_roaches.SimpleAgent()
VISUAL = False
NB_DIST = 20
NB_TEST = 10
SMALLEST_DIST = 10.0
NB_STEPS = 1920
on obtient les résultats présentés en Fig. 11, qui confirme que la meilleure valeur
de drun est proche de 19.
98
1ère solution pour CollectMineralsAndGas
La mission CollectMineralsAndGas consiste à définir la suite des actions per-
mettant d’optimiser la collecte de minerai et de gaz ; dans cette mission, on
dispose initialement de 12 SCV, un Command center, à proximité de 16 sources
de minerai et 4 sources de gaz ; les ressources initialement possédées sont de 50
minerais ; le Command center supporte 15 unités SCV ; pour supporter plus
d’untiés, il faut construire un Supply depot ; le temps est limité à 300 secondes ;
la mission consiste donc à choisir entre les actions suivantes : collecter du mine-
rai, collecter du gaz, construire un ou plusieurs SCV, construire un ou plusieurs
Supply depot et construire un ou plusieurs un Command center 54 .
Une première solution consiste à suivre la politique définie par Alg. 52 ; nbSCV
définit le nombre de SCV ; nbharvest_gather définit le nombre de SCV col-
lecteurs ; nb_trainSCV définit le nombre de SCV en cours d’entrainement ;
nbsupply_depot définit le nombre de Supply depot ; nb_buildsupply−depot définit
le nombre de Supply depot en cours de construction.
99
La politique définie dans Alg. 52 permet d’obtenir un score de 4645 ; après 6720
step, il y a 28 SCV tous impliqués dans la collecte de minerai, 2 Supply depot
constuits et 3 SCV en cours d’entrainement.
100
Les variables sont initialisées comme suit :
1 nb_scv = 12
2 nb_harvest_gather = 0
3 nb_supply_depot = 0
4 nb_scv_in_train = 0
5 nb_supply_depot_in_build = 0
6 prev_total_value_structures = 400
7 inactive_scv_selected = False
8 random_scv_selected = False
9 commandcenter_selected = False
La fonction update_units compte les SCV ; elle exploite le fait que les unités
de cette mission sont uniquement des SCV dont la valeur est de 50 :
1 def update_units(self, obs):
2 tvu = obs.observation["score_cumulative"]["total_value_units"]
3 new_nb_scv = int((tvu - self.prev_tvu)/50)
4 if new_nb_scv != self.nb_scv:
5 diff = new_nb_scv - self.nb_scv
6 self.nb_scv += diff
7 self.nb_scv_in_train -= diff
8 self.prev_tvu = tvu
La fonction update_buildings compte les Supply depot ; sachant que les ordres
se limitent à construire des Supply depot, elle observe la différence avec la valeur
précédente et recherche une différence de 100 (qui est la valeur d’un Supply
depot) ; la mémorisation de la valeur courante de total_value_structures est
imposée par la présence d’un Command center dont la valeur est de 400 :
1 def update_buildings(self, obs):
2 tvs = obs.observation["score_cumulative"]["total_value_structures"]
3 if tvs != self.prev_total_value_structures:
4 diff = (tvs - self.prev_total_value_structures)/100
5 self.nb_supply_depot += diff
6 self.nb_supply_depot_in_build -= diff
7 self.prev_total_value_structures = tvs
101
La fonction erode_with_min réalise une érosion sur une image de la taille du
screen :
1 def erode_with_min(screen):
2 output = []
3 for i in range(84):
4 output.append([0] * 84)
5 for i in range(1,83) :
6 for j in range(1,83) :
7 if(screen[i][j] == 1):
8 output[i][j] = int(min(screen[i][j], screen[i+1][j],
9 screen[i-1][j], screen[i][j+1],
10 screen[i][j-1]))
11 return output
55. La sélection d’un mineral à l’aide d’un pixel situé en bordure peut se solver par un échec.
102
La fonction train_scv fonctionne en deux temps ; dans le premier temps, il
faut sélectionner le Command center ; dans le deuxième temps, on sélectionne
un ordre de construction d’un SCV :
1 def train_scv(self, obs):
2 if self.commandcenter_selected == False :
3 if _SELECT_POINT in obs.observation["available_actions"]:
4 self.commandcenter_selected = True
5 self.inactive_scv_selected = False
6 unit_type = obs.observation["feature_screen"][_UNIT_TYPE]
7 cc = (unit_type == units.Terran.CommandCenter)
8 commandcenter_y, commandcenter_x = cc.nonzero()
9 cc_center_x = np.mean(commandcenter_x, axis=0).round()
10 cc_center_y = np.mean(commandcenter_y, axis=0).round()
11 target = [cc_center_x, cc_center_y]
12 return actions.FUNCTIONS.select_point("select", target)
13 if self.commandcenter_selected == True :
14 if _TRAIN_SCV_QUICK in obs.observation["available_actions"]:
15 self.nb_scv_in_train = self.nb_scv_in_train +1
16 return actions.FUNCTIONS.Train_SCV_quick("now")
17 return actions.FUNCTIONS.no_op()
103
2ème solution pour CollectMineralsAndGas
Dans le cas d’une mission correspondant à une recherche de séquence optimale
de constructions 56 , il est possible d’approximer le processus de décision à l’aide
d’un MDP (tel que défini section 9.1).
Un état est défini par un nombre de SCV et un nombre de Supply depot ; les
SCV sont soient en cours d’entrainement, soient inactifs, soient collecteurs de
minerai ; les Supply depot sont soient en cours de construction, soient construits ;
un état est donc défini comme suit :
1 new_state = [self.nb_harvest_gather, self.nb_scv, \
2 self.nb_supply_depot, self.nb_scv_in_train, \
3 self.nb_supply_depot_in_build]
Les actions sont définies comme suit :
1 ACTION_DO_NOTHING = ’donothing’
2 ACTION_ADD_HARVEST_GATHER = ’addharvestgather’
3 ACTION_TRAIN_SCV = ’trainsvc’
4 ACTION_BUILD_SUPPLY_DEPOT = ’buildsupplydepot’
5 my_actions = [ ACTION_DO_NOTHING, ACTION_ADD_HARVEST_GATHER, \
6 ACTION_TRAIN_SCV, ACTION_BUILD_SUPPLY_DEPOT, ]
Les actions ont un délai de répercution effective sur l’état courant :
— construire un SCV demande 272 steps
— collecter du minerai pour un SCV demande au moins 125 steps
— construire un Supply depot demande au moins 462 steps
1 stepdecision ← stepdecision + 1 ;
2 if stepdecision == 300 then
3 stepdecision ← 0 ;
4 R0 ← get_reward ( );
5 S 0 ← get_current_state ( ) ;
6 A0 ← -greedy (S, Q) ;
7 SARSA-update (S, A, R0 , S 0 , A0 ) ;
8 {S, A} ← {S 0 , A0 } ;
9 return apply_decision (A0 ) ;
10 return do noop ( );
Alg. 53: Construction de la fonction step.
104
La fonction apply_decision est implémentée comme suit :
1 def apply_decision(self, action):
2 self.do_add_harvest_gather = False
3 self.do_train_scv = False
4 self.do_build_supply_depot = False
5 if(action == ACTION_DO_NOTHING):
6 return
7 if(action == ACTION_ADD_HARVEST_GATHER):
8 self.do_add_harvest_gather = True
9 if(action == ACTION_TRAIN_SCV):
10 self.do_train_scv = True
11 if(action == ACTION_BUILD_SUPPLY_DEPOT):
12 self.do_build_supply_depot = True
Le décompte des SCV et des Supply depot est implémenté comme suit :
1 def update_units(self, obs):
2 tvu = obs.observation["score_cumulative"]["total_value_units"]
3 tvu_diff = tvu - self.prev_tvu
4 new_scv = int(tvu_diff/50)
5 if new_scv != 0:
6 self.nb_scv += new_scv
7 self.nb_scv_in_train -= new_scv
8 self.prev_tvu = tvu
9
105
La classe Qtable implémente l’apprentissage des évaluations des actions pos-
sibles à partir de chaque état selon l’apprentissage SARSA (cf. Alg. 53 page 66).
1 class QTable:
2 def __init__(self, actions, alpha=0.1, gamma=0.9, epsilon=0.9):
3 self.actions = actions
4 self.alpha = alpha
5 self.gamma = gamma
6 self.epsilon = epsilon
7 self.data = pd.DataFrame(columns=self.actions, dtype=np.float64)
8
La classe DataFrame de la librairie pandas (cf. annexes, section 10.1) est utilisée
pour maintenir la relation entre états et actions ; une action non sélectionnable
est associée à la valeur NaN ; dans la fonction add_state :
— x est le nombre de SCV collecteurs de minerai,
— y est le nombre de SCV,
— z est le nombre de Supply depot,
— a est le nombre de SCV en cours d’entrainement,
— b est le nombre de Supply depot en cours de construction,
— max_x est le nombre max de SCV collecteurs,
— max_y est le nombre de max de SCV (incluant les SCV en cours d’en-
trainement),
— max_z est le nombre max de Supply depot (incluant les Supply depot en
cours de construction),
— max_a est le nombre max de SCV en cours d’entrainement,
— max_b est le nombre max de Supply depot en cours de construction,
— l’action donothing est toujours possible (ligne 11),
— l’action addharvestgather dépend du nombre de SCV collecteurs (ligne
12),
— l’action trainsvc dépend du nombre de SCV en cours d’entrainement
et du nombre de SCV (ligne 14),
— l’action buildsupplydepot dépend du nombre de Supply depot en cours
de construction et du nombre de Supply depot déjà construits (ligne 16).
106
Les fonctions e_greedy et sarsa_update sont définies comme suit :
25 def e_greedy(self, state):
26 if self.epsilon < np.random.uniform():
27 new_actions = []
28 state_action = self.data.loc[state]
29 for x in range(0, len(state_action)):
30 if np.isnan(state_action[x]) == False:
31 new_actions.append(x)
32 action = np.random.choice(new_actions)
33 else:
34 state_action = self.data.loc[state]
35 action = state_action.idxmax()
36 return action
37
La table des correspondances entre états et actions est initialisée comme suit :
1 self.qtable = QTable(actions=list(range(len(my_actions))))
2 for x in range(0, 1+MAX_NB_HARVEST_GATHER):
3 for y in range(self.nb_scv, 1+MAX_NB_SCV):
4 for z in range(0, 1+MAX_NB_SUPPLY_DEPOT):
5 for a in range(0, 1+MAX_NB_SCV_IN_TRAIN):
6 for b in range(0, 1+MAX_NB_SUPPLY_DEPOT_IN_BUILD):
7 self.qtable.add_state(x,y,z,a,b, \
8 MAX_NB_HARVEST_GATHER, \
9 MAX_NB_SCV, \
10 MAX_NB_SUPPLY_DEPOT, \
11 MAX_NB_SCV_IN_TRAIN, \
12 MAX_NB_SUPPLY_DEPOT_IN_BUILD)
107
Fig. 13 présente l’évolution des scores avec l’apprentissage SARSA pour la mis-
sion CollectMineralsAndGas ; la progression du score se traduit par une amélio-
ration de la politique suivie ; à l’itération 90, le score obtenu est de 4335, avec
23 SCV dont 20 impliqués dans la collecte de minerai, 1 Supply depot construit
et 1 Supply depot en cours de construction.
Fig. 13 – Evolution des scores avec l’apprentissage SARSA sur 100 itérations.
Entre les itérations 70 et 100, le score repasse parfois encore autour de 2300 ;
cette limite correspond à la construction d’un Supply depot qui permet d’entrai-
ner plus de SCV pour les impliquer dans la collecte.
108
En utilisant un processus aléatoire, on peut utiliser les récompenses des actions
pour définir les valeurs des états ; dans ce cas, on définit la valeur d’un état par la
moyenne des sommes des récompenses établies par les actions suivants cet état ;
pour les deux premières séquences précédentes, en appliquant le modèle d’un
processus de récompense Markovien, avec γ = 0.5, on obtient les récompenses
suivantes :
— C1 C2 C3 Pass Sleep : V 1 = −2 − 2 ∗ 21 − 2 ∗ 14 + 10 ∗ 81 = −2.25
— C1 Flip Flip C1 C2 Sleep : V 1 = −2 − 1 ∗ 21 − 1 ∗ 41 − 2 ∗ 18 = −3
1 for each s in S do
2 sum_score ← 0 ;
3 for N times do
4 seq ← init (s) ;
5 seq ← random (seq, S, P) ;
6 sum_score ← sum_score+ eval (seq, R, γ) ;
7 scores ← sum_score/N ;
Alg. 54: Définition des valeurs des états par moyenne de récompenses
Markoviennes.
Avec le programme C/C++ suivant, les valeurs des états sont estimées en pre-
nant la moyenne des valeurs des scénarios aléatoires commençant par un état :
1 #include <cstdio>
2 #include <cstdlib>
3 #include <string.h>
4 #include <math.h>
5
6 #define gamma 0.5
7 enum {C1=0, C2, C3, Pass, Pub, Flip, Sleep};
8 double state_score[] = {-2, -2, -2, +10, +1, -1, 0};
9 const char* str_state[] = {"C1", "C2", "C3", "Pass", "Pub", "Flip", "Sleep" };
10
11 struct mk_t {
12 int size = 0;
13 int scenario[1024] = {0};
14
15 void next(){
16 double myrand = ((double)rand())/RAND_MAX;
17 if(scenario[size-1]==C1) {
18 if(myrand < 0.5) scenario[size] = C2; else scenario[size] = Flip;
19 size++; return;
20 }
21 if(scenario[size-1]==C2) {
22 if(myrand < 0.8) scenario[size] = C3; else scenario[size] = Flip;
23 size++; return;
24 }
109
25 if(scenario[size-1]==C3) {
26 if(myrand < 0.6) scenario[size] = Pass; else scenario[size] = Pub;
27 size++; return;
28 }
29 if(scenario[size-1]==Pass) {
30 scenario[size] = Sleep; size++; return;
31 }
32 if(scenario[size-1]==Pub) {
33 if(myrand < 0.2) scenario[size] = C1;
34 if(myrand < 0.6) scenario[size] = C2; else scenario[size] = C3;
35 size++; return;
36 }
37 if(scenario[size-1]==Flip) {
38 if(myrand < 0.1) scenario[size] = C1; else scenario[size] = Flip;
39 size++; return;
40 }
41 }
42 void generate() {
43 while(1) {
44 if(size==2014) break;
45 if(scenario[size-1]==Sleep) break;
46 next();
47 }
48 }
49 double score() {
50 double res = 0.0;
51 if(scenario[size-1]!=Sleep) return res;
52 res = state_score[scenario[0]];
53 for(int i = 1; i < size; i++) {
54 res += pow(gamma,(double)i)*state_score[scenario[i]];
55 }
56 return res;
57 }
58 };
59 /* g++ -std=c++11 journee-etudiante.cpp */
60 int main(int _ac, char** _av) {
61 for(int i = C1; i < Sleep; i++) {
62 double sum_score = 0.0;
63 for(int j = 0; j < 1000; j++) {
64 mk_t MK; MK.scenario[0]=i; MK.size=1; MK.generate();
65 sum_score += MK.score();
66 }
67 printf("score %s : %f\n", str_state[i], sum_score/1000);
68 }
69 return 0;
70 }
110
Avec le programme Racket suivant, les valeurs des états sont estimées en pre-
nant la moyenne des valeurs des scénarios aléatoires commençant par un état :
1 #lang racket
2 (define gamma 1.0)
3 (define C1 0)
4 (define C2 1)
5 (define C3 2)
6 (define PASS 3)
7 (define PUB 4)
8 (define FLIP 5)
9 (define SLEEP 6)
10 (define (eval-S s)
11 (cond ((equal? s C1) -2)
12 ((equal? s C2) -2)
13 ((equal? s C3) -2)
14 ((equal? s PASS) 10)
15 ((equal? s PUB) 1)
16 ((equal? s FLIP) -1)
17 (else 0)))
18 (define (next L)
19 (let ((f (first L)) (r (random)))
20 (cond ((equal? f C1)
21 (if(< r 0.5) (cons C2 L) (cons FLIP L)))
22 ((equal? f C2)
23 (if(< r 0.8) (cons C3 L) (cons FLIP L)))
24 ((equal? f C3)
25 (if(< r 0.6) (cons PASS L) (cons PUB L)))
26 ((equal? f PASS) (cons SLEEP L))
27 ((equal? f PUB)
28 (if(< r 0.2) (cons C1 L)
29 (if(< r 0.6) (cons C2 L)
30 (cons C3 L))))
31 (else (if(< r 0.5) (cons C2 L) (cons FLIP L))) )))
32 (define (generate L)
33 (if(equal? (first L) SLEEP) (reverse L) (generate (next L))))
34 (define (score L)
35 (define (f L ret ite)
36 (if(empty? L) ret
37 (f (rest L) (+ ret (* (eval-S (first L)) (expt gamma ite))) (+ 1 ite) )))
38 (f L 0 0))
39 (define (init s) (list s))
40 (define (n-generate n s)
41 (define (f n s sum)
42 (if(equal? n 0) sum
43 (f (- n 1) s (+ sum (score (generate (init s))))) ))
44 (/ (f n s 0) n))
45 (string-append "C1: " (real->decimal-string (n-generate 1000 C1) 2))
46 (string-append "C2: " (real->decimal-string (n-generate 1000 C2) 2))
47 (string-append "C3: " (real->decimal-string (n-generate 1000 C3) 2))
48 (string-append "PASS: " (real->decimal-string (n-generate 1000 PASS) 2))
49 (string-append "PUB: " (real->decimal-string (n-generate 1000 PUB) 2))
50 (string-append "FLIP: " (real->decimal-string (n-generate 1000 FLIP) 2))
111
En partant des récompenses supposées sur les actions, les programmes précé-
dents permettent de calculer une évaluation des récompenses obtenues dans les
états (présenté en figure 15 avec les résultats du programme C/C++) ; partant
de ces évaluations, on peut définir une politique optimale par recherche d’un
chemin maximisant les évaluations des états futurs (présenté en figure 16) ; la
propriété de Markov permet de ne tenir compte que de la valeur de l’état à
l’instant t + 1 pour définir la meilleure action partant d’un état à l’instant t ;
le chemin résultant est différent du chemin étabi par évaluation complète du
graphe ; cette différence est une conséquence des valeurs de récompense établies
à priori ; la recherche de ce chemin est de complexité linéaire.
Fig. 16 – Politique optimale π∗ par recherche d’un chemin maximisant les éva-
luations des états futurs ; en appliquant cette politique optimale, l’état Flip ne
comporte que des actions vers lui-même et est dit absorbant.
112
9.10 Application à Dota2
113
9.11 Application au Blackjack
1 /* blackjack avec 1 jeu de 52 cartes
2 * version americaine avec une carte cachee pour le croupier */
3 #include <cstdio>
4 #include <cstdlib>
5 #include <string.h>
6 #include <math.h>
7 #include <vector>
8 #include <string>
9 #include <algorithm>
10 #include <cfloat>
11 int card_val[13];
12 char get_card_number(int _card) { return 1+_card%13; }
13 int get_card_value(int _card) { return card_val[_card%13]; }
14 struct bj_t {
15 std::vector<int> all_cards;
16 std::vector<int> player_cards[2]; // player0 est le croupier
17 int player_sums[2]; // en comptant AS=1
18 int player_max_sums[2]; // en comptant AS=11
19 bj_t() {
20 for(int i = 0; i < 52; i++) all_cards.push_back(i);
21 std::random_shuffle( all_cards.begin(), all_cards.end());
22 for(int i = 0; i < 10; i++) card_val[i]=i+1;
23 for(int i = 10; i < 13; i++) card_val[i]=10;
24 }
25 void new_game() {
26 for(int j = 0; j < 2; j++) { // 2 players (croupier inclus)
27 player_cards[j].clear();
28 player_sums[j] = 0;
29 player_max_sums[j] = 0;
30 for(int i = 0; i < 2; i++) { // 2 cartes
31 player_cards[j].push_back(all_cards.back());
32 all_cards.pop_back();
33 int v = get_card_value(player_cards[j][i]);
34 player_sums[j] += v;
35 if(v == 1) v = 11;
36 player_max_sums[j] += v;
37 }
38 }
39 }
40 void twist(int _pid) { // pour player 0 et 1
41 player_cards[_pid].push_back(all_cards.back());
42 all_cards.pop_back();
43 int v = get_card_value(player_cards[_pid].back());
44 player_sums[_pid] += v;
45 if(v == 1) v = 11;
46 player_max_sums[_pid] += v;
47 }
48 // completer les cartes du croupier
49 // et calculer des sommes les + proches de 21
50 void finalize(){
51 int s1 = player_sums[1];
52 int s2 = player_max_sums[1];
53 if(s2 < 21 && s2 > s1) player_sums[1] = s2;
54 while(1) {
55 if(player_sums[0] > player_sums[1]) break;
56 if(player_max_sums[0] > player_sums[1]) break;
57 if(player_sums[0] <= 16 && player_max_sums[0] <= 16) twist(0);
58 else break;
59 }
60 s1 = player_sums[0]; s2 = player_max_sums[0];
61 if(s2 <= 21 && s2 > s1) player_sums[0] = s2;
62 }
114
61 bool win() {
62 if(player_sums[1] > 21) return false; // defaite car joueur > 21
63 if(player_sums[0] > 21) return true; // victoire car croupier > 21
64 if(player_sums[1] >= player_sums[0]) return true; // victoire aux points
65 return false; // defaite aux points
66 }
67 };
1 double stick_scores[10][22];
2 double twist_scores[10][22];
3 int stick_effectif[10][22];
4 int twist_effectif[10][22];
5 #define INITIAL_SCORE -1.0
6 #define INITIAL_EFFECTIF 1
7 void init_scores() {
8 for(int i = 0; i < 10; i++)
9 for(int j = 0; j < 22; j++){
10 stick_scores[i][j] = INITIAL_SCORE;
11 twist_scores[i][j] = INITIAL_SCORE;
12 stick_effectif[i][j] = INITIAL_EFFECTIF;
13 twist_effectif[i][j] = INITIAL_EFFECTIF;
14 }
15 }
115
12 // jouer contre le croupier
13 void play_game() {
14 bj_t BJ;
15 BJ.new_game();
16 std::vector<int> p_scores_seq; // scores du joueur
17 int c_card_seq = get_card_value(BJ.player_cards[0][0]); // carte visible du croupier
18 std::vector<int> actions_seq;
19 while(1) {
20 int decision = policy_stick_19(BJ);
21 if(decision == DO_STICK) {
22 actions_seq.push_back(DO_STICK);
23 p_scores_seq.push_back(BJ.player_sums[1]);
24 break;
25 } else { // DO_TWIST
26 actions_seq.push_back(DO_TWIST);
27 p_scores_seq.push_back(BJ.player_sums[1]);
28 BJ.twist(1);
29 }
30 if(BJ.player_sums[1] > 21) break;
31 }
32 BJ.finalize();
33 double reward = 1.0;
34 if(!BJ.win()) reward = -1.0;
35 for(int i = 0; i < (int)actions_seq.size(); i++) {
36 int card_id = c_card_seq-1;
37 int sum_id = p_scores_seq[i];
38 if(actions_seq[i] == DO_STICK) {
39 stick_scores[card_id][sum_id] += reward;
40 stick_effectif[card_id][sum_id] += 1;
41 } else { // action est DO_TWIST
42 twist_scores[card_id][sum_id] += reward;
43 twist_effectif[card_id][sum_id] += 1;
44 }
45 }
46 }
47 void normalize_scores_MC(double(*T) [22], int(*N) [22]) {
48 double min_score = DBL_MAX;
49 double max_score = -DBL_MAX;
50 for(int i = 0; i < 10; i++)
51 for(int j = 0; j < 22; j++) {
52 if(N[i][j] != INITIAL_EFFECTIF) T[i][j] = T[i][j]/N[i][j];
53 if(T[i][j] < min_score) min_score = T[i][j];
54 if(T[i][j] > max_score) max_score = T[i][j];
55 }
56 double ratio = fabs(min_score-max_score);
57 for(int i = 0; i < 10; i++)
58 for(int j = 0; j < 22; j++) {
59 if(N[i][j] == INITIAL_EFFECTIF) T[i][j] = 0.0;
60 else T[i][j] = (T[i][j] - min_score)/ratio;
61 }
62 }
63 void MC_evaluation(int _k){
64 init_scores();
65 for(int i = 0; i < _k; i++) play_game();
66 normalize_scores_MC(twist_scores, twist_effectif);
67 normalize_scores_MC(stick_scores, stick_effectif);
68 }
69 /* g++ -std=c++11 blackjack-MC.cpp */
70 int main(int _ac, char** _av) {
71 MC_evaluation(1000); // evaluation MC avec 1k playouts
72 }
116
Résultats de l’évaluation MC
Les figures 17 et 18 présentent les résultats de l’évaluation MC de la politique
policy_stick_19 ; ces matrices présentent les chances de succès dans la prise
de décision DO_STICK (i.e. passer) ou DO_TWIST (i.e. demander une carte) ; pour
des chances de succès de 100%, la case est bleue ; pour des chances de succès
nulle, la case est rouge ; pour prendre cette décision, on utilise en x, la carte
visible du croupier et en y, la somme des cartes du joueur ; l’évaluation MC est
plus précise avec 100k playouts qu’avec 1k playouts.
Les valeurs en y étant des sommes de cartes calculées avec des valeurs de 1 pour
les As, la forte probabilité de gain pour l’action DO_STICK autour de 10 sur la
figure 18 correspond à des séquences de cartes contenant un As.
117
Evaluation TD d’une politique
1 void play_game_TD() {
2 // ...
3 BJ.finalize();
4 double gamma = 0.9;
5 double reward = 1.0;
6 if(!BJ.win()) reward = -1.0;
7 for(int i = 0; i < (int)actions_seq.size(); i++) {
8 int card_id = c_card_seq-1;
9 int sum_id = p_scores_seq[i];
10 double TD_target = 0.0;
11 if(actions_seq[i] == DO_STICK) {
12 stick_effectif[card_id][sum_id] += 1;
13 double alpha = 1.0/(double)stick_effectif[card_id][sum_id];
14 if(i != ((int)actions_seq.size()) -1)
15 TD_target = 1.0+gamma*stick_scores[card_id][p_scores_seq[i+1]];
16 else TD_target = reward+gamma*reward;
17 stick_scores[card_id][sum_id] += alpha*(TD_target - stick_scores[card_id][sum_id]);
18 } else { // action est DO_TWIST
19 twist_effectif[card_id][sum_id] += 1;
20 double alpha = 1.0/(double)twist_effectif[card_id][sum_id];
21 if(i != ((int)actions_seq.size()) -1)
22 TD_target = 1.0+gamma*twist_scores[card_id][p_scores_seq[i+1]];
23 else TD_target = reward+gamma*reward;
24 twist_scores[card_id][sum_id] += alpha*(TD_target - twist_scores[card_id][sum_id]);
25 }
26 }
27 }
28 void normalize_scores_TD(double(*T) [22], int(*N) [22]) {
29 double min_score = DBL_MAX;
30 double max_score = -DBL_MAX;
31 for(int i = 0; i < 10; i++)
32 for(int j = 0; j < 22; j++) {
33 if(N[i][j] != INITIAL_EFFECTIF) {
34 if(T[i][j] < min_score) min_score = T[i][j];
35 if(T[i][j] > max_score) max_score = T[i][j];
36 }
37 }
38 double ratio = fabs(min_score-max_score);
39 for(int i = 0; i < 10; i++)
40 for(int j = 0; j < 22; j++) {
41 if(N[i][j] == INITIAL_EFFECTIF) T[i][j] = 0.0;
42 else T[i][j] = (T[i][j] - min_score)/ratio;
43 }
44 }
45 void evaluation_TD(int _k){
46 init_scores();
47 for(int i = 0; i < _k; i++) play_game_TD();
48 normalize_scores_TD(twist_scores, twist_effectif);
49 normalize_scores_TD(stick_scores, stick_effectif);
50 }
51 /* g++ -std=c++11 blackjack-TD.cpp */
52 int main(int _ac, char** _av) {
53 evaluation_TD(1000); // evaluation TD avec 1k iterations
54 }
118
Résultats de l’évaluation TD
Les figures 19 et 20 présentent les résultats de l’évaluation TD(0) de la politique
policy_stick_19.
119
Evaluation TD(λ) d’une politique
1 void play_game_TD_lambda() {
2 // ...
3 BJ.finalize();
4 double gamma = 0.9;
5 double reward = 1.0;
6 if(!BJ.win()) reward = -1.0;
7 for(int i = 0; i < (int)actions_seq.size(); i++) {
8 int card_id = c_card_seq-1;
9 int sum_id = p_scores_seq[i];
10 double TD_target = 0.0;
11 double ret_acc = 1.0;
12 double TD_error;
13 int last_id = p_scores_seq[(int)actions_seq.size()-1];
14 for(int j = i+1; j < (int)actions_seq.size()-1; j++) {
15 ret_acc *= gamma;
16 TD_target += ret_acc;
17 }
18 ret_acc *= gamma;
19 TD_target += (ret_acc*reward);
20 if(actions_seq[i] == DO_STICK) {
21 stick_effectif[card_id][sum_id] += 1;
22 double alpha = 1.0/(double)stick_effectif[card_id][sum_id];
23 if(i != ((int)actions_seq.size()) -1) {
24 TD_target += gamma*stick_scores[card_id][last_id];
25 } else {
26 TD_target += gamma*reward;
27 }
28 stick_scores[card_id][sum_id] += alpha*(TD_target - stick_scores[card_id][sum_id]);
29 } else { // action est DO_TWIST
30 twist_effectif[card_id][sum_id] += 1;
31 double alpha = 1.0/(double)twist_effectif[card_id][sum_id];
32 if(i != ((int)actions_seq.size()) -1) {
33 TD_target += gamma*twist_scores[card_id][last_id];
34 } else {
35 TD_target += gamma*reward;
36 }
37 twist_scores[card_id][sum_id] += alpha*(TD_target - twist_scores[card_id][sum_id]);
38 }
39 }
40 }
41 void evaluation_TD_lambda(int _k){
42 init_scores();
43 for(int i = 0; i < _k; i++) play_game_TD_lambda();
44 normalize_scores_TD(twist_scores, twist_effectif);
45 normalize_scores_TD(stick_scores, stick_effectif);
46 }
47 /* g++ -std=c++11 blackjack-TD-lambda.cpp */
48 int main(int _ac, char** _av) {
49 evaluation_TD_lambda(1000); // evaluation TD-lambda avec 1k iterations
50 }
120
Résultats de l’évaluation TD(λ)
Les figures 21 et 22 présentent les résultats de l’évaluation TD(λ) de la politique
policy_stick_19.
L’évaluation TD(λ) est plus fidèle à la réalité du blackjack que les évaluations
MC et TD dans le sens où : 1) l’évaluation TD(λ) donne plus d’importance à
l’action DO_STICK autour de la valeur 10 (correspondant à une main avec un
As) ; 2) l’évaluation TD(λ) pour des valeurs supérieures à 12 est similaire à
l’évaluation TD ; 3) les constats précedemment établis entre l’évaluation MC et
TD, restent valablent entre les évaluations MC et TD(λ).
121
Construction d’une politique avec VI et MC
L’objectif est de définir une politique de décision en choisissant l’action DO_TWIST
ou l’action DO_STICK pour une carte visible de croupier et une somme des cartes
du joueur ; pour chaque action, on maintient une valeur qui définit l’action la
plus importante ; dans la fonction policy_MC_stick, si stick_value est su-
périeure à twist_value, la décision est d’appliquer l’action DO_STICK ; alter-
nativement, l’action à appliquer est DO_TWIST ; la politique est initialisée avec
une décision d’appliquer DO_TWIST partout ; la MAJ de la politique incrémente
twist_value pour les premières cartes et incrémente stick_value pour la der-
nière carte en cas de victoire et l’avant dernière carte en cas de défaite ; la
politique est itérativement MAJ en suivant le principe value-iteration dans la
fonction play_game_value_iteration.
1 void init_MC_stick() {
2 for(int i = 0; i < 10; i++)
3 for(int j = 0; j < 22; j++) {
4 stick_value[i][j] = 0; twist_value[i][j] = 1;
5 }
6 }
7 void update_MC_stick(bool _new_win, int _c, int _s) {
8 if(_new_win) stick_value[_c][_s] ++;
9 else twist_value[_c][_s] ++;
10 }
11 int policy_MC_stick(int _p0_card, int _p1_sum) {
12 if(stick_value[_p0_card][_p1_sum] >= twist_value[_p0_card][_p1_sum])
13 return DO_STICK;
14 return DO_TWIST;
15 }
16 void play_game_value_iteration() {
17 bj_t BJ;
18 BJ.new_game();
19 std::vector<int> p_scores_seq; // scores du joueur
20 int c_card_seq = get_card_value(BJ.player_cards[0][0]); // carte visible du croupier
21 int card_id = c_card_seq-1;
22 while(1) {
23 int decision = policy_MC_stick(BJ.player_sums[1], card_id);
24 p_scores_seq.push_back(BJ.player_sums[1]);
25 if(decision == DO_STICK) break;
26 BJ.twist(1);
27 if(BJ.player_sums[1] > 21) break;
28 }
29 BJ.finalize();
30 if(BJ.win()) {
31 for(int i = 0; i < ((int)p_scores_seq.size())-1; i++)
32 update_MC_stick(false, card_id, p_scores_seq[i]);
33 int last_sum = p_scores_seq[((int)p_scores_seq.size())-1];
34 update_MC_stick(true, card_id, last_sum);
35 } else {
36 for(int i = 0; i < ((int)p_scores_seq.size())-2; i++)
37 update_MC_stick(false, card_id, p_scores_seq[i]);
38 if(((int)p_scores_seq.size()) > 1) {
39 int before_last_sum = p_scores_seq[((int)p_scores_seq.size())-2];
40 update_MC_stick(true, card_id, before_last_sum);
41 }
42 }
43 }
122
La figure 23 présente la politique définie (l’action DO_STICK étant en rouge et
l’action DO_TWIST étant en bleu) en utilisant les principes MC et value-iteration ;
la précision de l’évaluation augmente avec le nombre d’itérations ; la politique
construite suggère d’appliquer l’action DO_STICK pour des mains de valeurs 11
et de 21.
123
Si on regarde la convergence des valeurs de la politique définie par la fonction
policy_MC_stick, conditionnée par 100 itérations donnant des choix d’actions
identiques, la politique converge après 2178 itérations ; la figure 25 présente la
politique définie avec les valeurs retournées par la politique policy_MC_stick
et avec les ratios r comme défini précédemment.
124
10 Annexes
10.1 DataFrame de pandas en Python
La classe DataFrame de la librairie panda permet de définir des tableaux à deux
dimensions, indexés selon des clés.
1 my_actions = ["A","B","C",]
2 table = pd.DataFrame(columns=list(range(len(my_actions))), \
3 dtype=np.float64)
4 # ajouter un élément
5 new_element = [0,1]
6 table = table.append(pd.Series([0] * len(my_actions), \
7 index=table.columns, name=str(new_element)))
8 new_element = [0,3]
9 rand_val = []
10 for i in range(0, len(my_actions)):
11 rand_val.append(np.random.uniform())
12 table = table.append(pd.Series(rand_val, index=table.columns, \
13 name=str(new_element)))
14 # modifier une des valeur d’un élément
15 table.loc[str([0,1]), 2] = 0.1
16 # associer une valeur NaN
17 table.loc[str([0,1]), 1] = np.nan
18 # afficher
19 print(table)
0 1 2
[0, 1] 0.000000 NaN 0.100000
[0, 3] 0.850929 0.552237 0.320382
En considérant les valeurs comme les scores des actions A,B et C pour chacun
des états, notre table indique que l’action B (qui est l’action d’indice 1) n’est
pas possible dans l’état [0,1].
Il est important de ne pas ajouter dans la table deux fois le même élément, qui
sera la cause d’erreur à l’exécution.
Avant d’ajouter un élément dans la table, vérifier qu’il n’est pas déjà dedans :
1 new_element = [0,4]
2 if str(new_element) not in table.index:
3 print("%s is not in table" % (str(new_element)))
[0, 4] is not in table
125
Il est possible de récupérer la valeur d’une action d’un élément, la valeur max
d’un élément et l’indice de cette valeur max (autrement dit, le score de la
meilleure action d’un élément et l’indice de cette meilleure action) :
1 #lire une valeur d’un élément
2 new_element = [0,1]
3 val = table.loc[str(new_element), 2]
4 print("in %s at 2 : %f" %(str(new_element), val))
5 #obtenir la valeur max d’un élément
6 maxval = table.loc[str(new_element), :].max()
7 print("maxval: %f" %(maxval))
8 #obtenir d’index de la valeur max d’un élément
9 maxid = table.loc[str(new_element), :].idxmax()
10 print("maxid: %d" %(maxid))
in [0, 1] at 2 : 0.100000
maxval: 0.100000
maxid: 2
126