From 7d630901fa10c29cf258f5c6b6dfaedb9b9741b2 Mon Sep 17 00:00:00 2001 From: Melmon Date: Sat, 1 Apr 2023 18:28:50 +0100 Subject: [PATCH] Arrrrgggg --- README.md | 8 +- chizuru.py | 73 ++++++++++++------ writeup/Drescher-DGD-dissertation-2022-23.tex | 50 ++++++++---- writeup/img/network_structure.png | Bin 0 -> 9062 bytes 4 files changed, 88 insertions(+), 43 deletions(-) create mode 100644 writeup/img/network_structure.png diff --git a/README.md b/README.md index 4363d67..fe83216 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -![I LOVE ROGUE](LOVEIT.png) +![Anya from SPY x FAMILY pointing at a television screen with Rogue displayed saying 'I like this show.'](LOVEIT.png) # Chizuru Chizuru is an AI that plays the 1980 computer game Rogue. -While this repository contains the code for the AI, it also contains the dissertation released alongside this code in `/writeup`. +While this repository contains the code for the AI, it also contains the dissertation released alongside this code in `writeup/`. You can learn more about Rogue on the [NetHack Wiki page](https://nethackwiki.com/wiki/Rogue_(game)) about it. @@ -12,10 +12,10 @@ This thing is designed to run in a Docker container. To do that, run these: docker build -t chizuru . docker run ``` -After that, it should be smooth sailing. +After that, it should be "smooth" sailing. ## Files -Chizuru saves its checkpoints to `czr.ckpt` and saves models to `czr.h5`. +Chizuru saves its training checkpoints to `czr-xxxx.ckpt` where `xxxx` is the epoch number. ## Bugs Probably infinite (although countably infinite). However, the distant screams of your PC running this model is *not* a bug. It's a feature. diff --git a/chizuru.py b/chizuru.py index 7d95ef4..ff9ff99 100644 --- a/chizuru.py +++ b/chizuru.py @@ -1,37 +1,56 @@ # This code is governed under the GNU General Public Licence v3.0. -"""This file contains the Chizuru class, a Rogue playing agent.""" -import os +# ██████╗██╗ ██╗██╗███████╗██╗ ██╗██████╗ ██╗ ██╗ +# ██╔════╝██║ ██║██║╚══███╔╝██║ ██║██╔══██╗██║ ██║ +# ██║ ███████║██║ ███╔╝ ██║ ██║██████╔╝██║ ██║ +# ██║ ██╔══██║██║ ███╔╝ ██║ ██║██╔══██╗██║ ██║ +# ╚██████╗██║ ██║██║███████╗╚██████╔╝██║ ██║╚██████╔╝ +# ╚═════╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ +# ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ +# ██╔══██╗██╔═══██╗██╔════╝ ██║ ██║██╔════╝ +# ██████╔╝██║ ██║██║ ███╗██║ ██║█████╗ +# ██╔══██╗██║ ██║██║ ██║██║ ██║██╔══╝ +# ██║ ██║╚██████╔╝╚██████╔╝╚██████╔╝███████╗ +# ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ +# +# An AI that plays Rogue. + +"""This file contains everything needed to run the Chizuru AI.""" +import os import tensorflow as tf -import numpy as np -import matplotlib.pyplot as plt ASCII_CHARNUM = 128 - -NUM_ITERATIONS = 20000 -BATCH_SIZE = 64 -LEARNING_RATE = 1e-3 -LOG_INTERVAL = 200 - ENVIRONMENT = "rogueinabox" +LOG_INTERVAL = 200 -CKPT_PATH = "training/czr.ckpt" +CKPT_PATH = "training/czr-{epoch:04d}.ckpt" CKPT_DIR = os.path.dirname(CKPT_PATH) +# Hyperparameters +NUM_ITERATIONS = 20000 +BATCH_SIZE = 64 +ALPHA = 1.0e-3 +BETA1 = 0.9 +BETA2 = 0.999 +EPSILON = 1.0e-8 +DECAY = 0.0 + CKPT_CALLBACK = tf.keras.callbacks.ModelCheckpoint( filepath=CKPT_PATH, save_weights_only=True, - verbose=1 + verbose=1, + save_freq=5*BATCH_SIZE ) def create_model(): - status_input = tf.keras.Input(shape=(16,)) + """Instantiates, compiles and returns the Chizuru model.""" + status_input = tf.keras.Input(shape=(64,)) inv_input = tf.keras.Input(shape=(64,)) - equip_input = tf.keras.Input(shape=(16,)) - map_input = tf.keras.Input(shape=(21, 79)) - crop_input = tf.keras.Input(shape=(9, 9)) + equip_input = tf.keras.Input(shape=(64,)) + map_input = tf.keras.Input(shape=(21, 79), dtype=tf.int32) + crop_input = tf.keras.Input(shape=(9, 9), dtype=tf.int32) status_net = tf.keras.layers.Embedding(ASCII_CHARNUM, 64)(status_input) status_net = tf.keras.layers.Dense(32, activation="relu")(status_net) @@ -47,17 +66,18 @@ def create_model(): equip_net = tf.keras.layers.Dense(16, activation="relu")(equip_net) map_net = tf.keras.layers.Embedding(ASCII_CHARNUM, 64, input_length=21 * 79)(map_input) - map_net = tf.keras.layers.Conv2D(32, (3, 3), activation="relu", input_shape=(21, 79))(map_net) + map_net = tf.keras.layers.Conv2D(64, (3, 3), activation="relu", input_shape=(21, 79))(map_net) map_net = tf.keras.layers.MaxPooling2D((2, 2))(map_net) - map_net = tf.keras.layers.Conv2D(64, (3, 3), activation="relu")(map_net) + map_net = tf.keras.layers.Conv2D(32, (3, 3), activation="relu")(map_net) map_net = tf.keras.layers.MaxPooling2D((2, 2))(map_net) - map_net = tf.keras.layers.Conv2D(64, (3, 3), activation="relu")(map_net) + map_net = tf.keras.layers.Conv2D(16, (3, 3), activation="relu")(map_net) crop_net = tf.keras.layers.Embedding(ASCII_CHARNUM, 64, input_length=9 * 9)(crop_input) crop_net = tf.keras.layers.Conv2D(64, (3, 3), activation="relu", input_shape=(9, 9))(crop_net) crop_net = tf.keras.layers.MaxPooling2D((2, 2))(crop_net) - crop_net = tf.keras.layers.Conv2D(32, (3, 3), activation="relu")(crop_net) + crop_net = tf.keras.layers.Conv2D(16, (3, 3), activation="relu")(crop_net) + # requires inputs with matching shapes except for the concatenation axis. Received: input_shape=[(None, 16, 16), (None, 64, 16), (None, 16, 16), (None, 1, 16, 64), (None, 1, 1, 32)] collected = tf.keras.layers.Concatenate()([status_net, inv_net, equip_net, map_net, crop_net]) # MLP after concat @@ -84,7 +104,7 @@ def create_model(): ) final_model.compile( - optimizer="adam", + optimizer=tf.keras.optimizers.Adam(), loss=tf.keras.losses.MeanSquaredError(), metrics=[tf.keras.metrics.SparseCategoricalAccuracy()] ) @@ -92,20 +112,27 @@ def create_model(): return final_model -def get_crop(map): +def get_crop(map: list[list[int]]): # TODO + """Returns a 9x9 crop of the given Rogue map surrounding the player.""" pass def save_checkpoint(model_sv: tf.keras.Model, epoch): + """Saves the model checkpoint with given epoch.""" model_sv.save_weights(CKPT_PATH.format(epoch=epoch)) + print("Epoch " + str(epoch) + " saved to " + CKPT_PATH.format(epoch=epoch) + "~") -def load_checkpoint(model_ld: tf.keras.Model): +def load_checkpoint(model_ld: tf.keras.Model, epoch): + """Loads a model checkpoint at a given epoch.""" model_ld.load_weights(CKPT_PATH) + print("File " + CKPT_PATH.format(epoch=epoch) + " loaded to current model~") if __name__ == "__main__": model = create_model() tf.keras.utils.plot_model(model, "stuff.png", show_shapes=True) + save_checkpoint(model, 0) + # †昇天† diff --git a/writeup/Drescher-DGD-dissertation-2022-23.tex b/writeup/Drescher-DGD-dissertation-2022-23.tex index d09b767..8b0111b 100644 --- a/writeup/Drescher-DGD-dissertation-2022-23.tex +++ b/writeup/Drescher-DGD-dissertation-2022-23.tex @@ -13,7 +13,7 @@ \begin{document} \title{chizuru-rogue: Deep Learning for Dungeon Crawling} - \author{Dylan G. Drescher \\[1ex] B.Sc. Computer Science - University of Bath} + \author{Dylan G. Drescher\\[1ex]Supervisor: Dr. Jie Zhang\\[1ex]B.Sc. Computer Science - University of Bath} \date{2022 - 2023} \maketitle @@ -31,7 +31,7 @@ \newpage - CHIZURU-ROGUE + \textbf{CHIZURU-ROGUE} submitted by Dylan G. Drescher @@ -48,9 +48,12 @@ \newpage \begin{abstract} + Video games is one of the most popular problem domains to tackle with reinforcement learning due to the interesting complexity that can arise from simple sets of rules that many games provide. + By training reinforcement learning models on video games and proving they are effective at solving problems, they can then be repurposed for other problems such as self-driving cars and healthcare. + In this article we introduce chizuru-rogue, which is a computer program designed to play the video game Rogue, a famous role-playing game that inspired the creation of the ``roguelike'' video game genre. - Rogue offers a unique problem to solve, requiring a player to solve a partially observable, randomly generated levels. - chizuru-rouge utilises a customised neural network that involves an LSTM to explore levels in Rogue, collect gold and reach the goal. + Rogue offers a unique problem to solve, requiring a player to solve partially observable, randomly generated levels. + chizuru-rouge utilises a customised neural network that involves an LSTM for long-term and short-term memory to explore levels in Rogue, collect gold and reach the goal of collecting the Amulet of Yendor. TensorFlow will be used as a framework to implement the reinforcement learning agent. TensorFlow is a Python library that provides tools to streamline development of deep learning models. @@ -84,7 +87,7 @@ % \vspace{5mm} - % This dissertation made use of Hex, the GPU Cloud in the Department of Computer Science at the University of Bath. + % This project made use of Hex, the GPU Cloud in the Department of Computer Science at the University of Bath. \end{center} \newpage @@ -166,7 +169,7 @@ Roguelike games are mainly characterised by challenging, turn based hack and slash gameplay, procedurally generated levels and permanent character death. \subsubsection{Objective}\label{subsubsec:objective} - In Rogue, your objective is to descend the Dungeon of Doom to slay monsters, collect gold coins, retrieve the Amulet of Yendor and escape the dungeon with it alive. + In Rogue, your main objective is to get a high score by descending the Dungeon of Doom to slay monsters, collect gold coins, retrieve the Amulet of Yendor and escape the dungeon with it alive. The game is turn based, which means the player can spend as long as they want thinking their next move before the game processes the environment. Figure~\ref{fig:rogsc} depicts an example screenshot of the game. @@ -179,8 +182,9 @@ \subsubsection{Environment}\label{subsubsec:environment} - Every floor of the dungeon is a randomly generated level consisting of several rooms connected with corridors. + Every floor of the dungeon is a randomly generated maze consisting of several rooms connected with corridors. Rooms sometimes generate empty, but they may also generate populated with several items or enemies. + One of the rooms will contain the stairs that will let the player descend the dungeon, represented with the character \texttt{\%}. When the player starts a new run, the player is placed in dungeon level 1 with some food, a mace, basic armour, a bow and arrows. Rogue's environment is partially observable. @@ -272,9 +276,15 @@ \subsection{Policy Optimisation}\label{subsec:policy-optimisation} \subsection{Neural Network}\label{subsec:neural-network} + The neural network processes the inputs in a separate subnetwork, which is then concatenated, fed through an LSTM, and a multilayer perceptron network to produce the final output. + Figure~\ref{fig:netwk} visually shows the structure of the Chizuru neural network. - - + \begin{figure}[t] + \caption{The structure of the Chizuru neural network *OUTDATED.} % TODO outdated + \centering + \includegraphics[scale=0.5]{network_structure} + \label{fig:netwk} + \end{figure} \section{Implementation}\label{sec:implementation} @@ -315,12 +325,20 @@ \medskip \appendix - \section{Methods} - \subsection{Neural Network} - \subsection{State Representation} - \subsection{Reward Representation} - \subsection{Hyperparameters} - \section{Results} - \section{Data} + \section{Methods}\label{sec:methods} + + \subsection{Neural Network}\label{subsec:neural-network2} + + \subsection{State Representation}\label{subsec:state-representation} + + \subsection{Reward Representation}\label{subsec:reward-representation} + + \subsection{Hyperparameters}\label{subsec:hyperparameters} + + + \section{Results}\label{sec:results} + + + \section{Data}\label{sec:data} \end{document} diff --git a/writeup/img/network_structure.png b/writeup/img/network_structure.png new file mode 100644 index 0000000000000000000000000000000000000000..0cd5e978e7e462c056eb09334a0da48bc13b15e2 GIT binary patch literal 9062 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%0A8{o0r*`|RIXHdv>{%0^l(o=SvpGRl>0bmn; zrBVqejo%C9ef#;=@ad`FZ!AIMoRF)-!D8>Adm&wzPvy2jJswi*i%w)T)g^A7&nB|k zL%q2aal)z3vWU0qY=k{#`R|BgXbUrD-nrc>OkFj@REXSxTJ$l#dVkleKoREh>dfhG zf4bODVv_GmLyK328SQc`&{$Fho07WBvoL-EN)eUMle@E2iH?( zBBDxhMXE7D6C(G#M-G*Wy1HCK7`isYs%mQu94&`TN>RndFWp1}5gG+!JhOuwvey=( z+X*Tl_wS1!9U7e-A>P~5gVIWa`h5w_vU!Z|LwhRO=_N8nJ&F#c>xf+-&7j0x71527qudAt-K7bjS0m9TV0} zK@fH#bg>w3$#};@y5mom%?;1YsDtMMk27E=<_Iga1bhFk(d?&uI`XS8S5G1%Bb)t? z$3$$G(VezO<`d2mglRE?QV$Kf4A2a7A_mwh6aAB?;8SSsrmkzZwV=!J+{O7Z8^TAy z(?$L4IScQCsOdCddB4iO`)cKjFr>!+441_XXPyJzOt-zzTa}UChCNpA5xYlvvAPf< zie)@NlmJrjHsWY|rkB=#HNzX7A;s2Q*6#`Ibv7po=R=Xit$*+8zLy%10-p*f{GFQN zsiW`{?P;oLI4{9vxLYOZu|hJiKZVLPycBeEDi+vyD48~9$M(QaIGq)W(!SY#{#RYQ zo0-KJZ>Kjf@Em4*VP%B<`s9Gy)8n%rtpqH7`~6~b0sHFxWy*ka%n#MGP;QRa6oZS1 z-g#7DqmjG4QG?2g${gJvkiA$=hK`-TqEdUq%s3?JpZ9h!8<>NX$r1Jjb@ta7I)TZa z3o{*MvB9&K6QU_KrFU0lFB+<5Y`yqYDzx~#O-$G`K`4Cp4HaY0-OFo zrYy?;*!dQ3K?qJDnT&y&Fi~TBB}lCQQACnc=wp+VPlq*nN1uXr;n2;=8i%vvTz45; z>^1Vl46*NMQmsS;g6ZTCcv>s%**0>=e)5OK1itM~Zk9v%t@0XoKoNxBX5I#92p~^r z_$;O;CTQvDr)nIMS6=4Krnm6G=B^FY&OJy&LeeR}Rt#-e+2{W~a3h5=ONEPzE4K?v z2ae$5gZTyKDoX3fzT3dly`BmiSoJ?xd!JsgPBS5~M%G7x5ESP0 zyU^HX%a51Br51aXF39v9t)c>*gVX&iViA_laiKXV8@SF!SInY6Un$;LiE=14x z{sZ+X!;jk=`UX{-xr)n%w?T>aQ{4Ar+=|0Xsd7e=7>>(P_!qlc$H7|8nf^0-*zYq- z059tp8iAUX4>&n9F^S`omlFX8s~-uiy1%NMmIh6~8==ngD83sxJj9!pX39}ZtA;dpcF+C~=`L z?wa)kCbFxB&_K`Q?U~z5(i0OC@Al3{Mn*b1!n|DaTjkRdGAKQI$K*FiW7Ya1;dHUd z<-hJ4_&Sz$>5~?Hogb~-D!#j2r>}?mRICk~wfj{cT0(%+KM5bMi(5rT7%blkG>i)b zS2sXLY$TMqz~DeW#(-n^=>FLbE=x|O25;5Y{6JOw6(QmYHB(}7fne?IV4DK zg27bAOHjJ{v#6Eq?n#-ISDt_ia8waO%*3&}3SY5lG3DYK#Q0QoeyxfoeS(iK=_Ie$ zG{?llM%c0sn%eUFjy4;EqY|fNo~pA_9!mG(1y4&oPc{}8XILp~JPtZ{Lm3yb@spW& z(ctATYSZVp7m35)FhAD3ytDGJMAL$HVmGO_y1a%*#Z2id8)~J za8U`r6cW>w* z%J$neC!WO^g;uooYOE)Ce5XvOq8Ng^Ca_mfI2?$`$2)BIA&erINowJcK)!zz7CKo2 z8t4s6v_n#N4dp3X=A|H=Qg|XI1e)vnaYS=c!+IFAqWcc~qFxwC2B2SorEhImTlPTacTrXwU%Z>JuO*AH=-vx3^ zlpmqjAa;C#>%3TBXc|=Gj$N$RO))Zm8g|><(`}YrqLbhuV?fk2lj08?rRXhfD%8dK znPhp1Ds@RlB`P&K>!vP3Pa|*KD!zF<3tPP+SxDR}Fxvd?d9Krpm*l5V6T(I#LJ`f& z%|+x_Qg*7RR?Lz2$dX$c5?#i6UtAo%)*+kojOrSJmr$u7ZNts>Y73ay8THQrc3Mb) zueO9mvHF=2r#ye_^ErLO0*NP&jSNx zT!!&y$2Y}Pbc(3R{{v$>$c;*`_QaMJ{jtTS_gdYe%J>cjB)Cb3?A~ug?Tp(6KUHTM z*ZU>n5Ffk>uh;Jo$+Aay*mRER1^5nn{N1XSP*#){NFyCo+L2`tfKob>vfryO#9R@U zfR7@oUh4pUr@eSE-P#oZ)W*kOAA~F-sqxfkmPQf#s4FvHE;d2x1DmGLz+m*X6^DZw zIaRsKFp>*kT`v{eztxUv${+4fpP=p};vr|(YYcjn3m&tp@ic~3iN$;V1w zOaF$dG8edg096;pgq4Lx)yG7@4>|Yk3@334ciY<9p1x&bq@%NMI2KwcW`cWpf!c`Q zC^*-pEsAp#2z7w98Gwrip7Q?L znkVbk#5s61$}&`j4Ei%gq_HuqDbr=TUfXfTP<^p>eU5KoOqkbCW_|4EPgejG92~r8 z{%dlpI!+lC8)oHoCVB>#X6lL)P2xx=Qc#E>R-lUQYWNefrgUp-gq>%{{JVW61?3B} zL@(?n!`ouUwHS&)obtdL%Qf>M&6FoYZysh~HauM!Rx=J@wXDFMMx{IIG!*|K(I26v z93P?~ni3TXqEi~NHAY+TUN?2#n_of>K42p42Kh`Dqk(m823We9jR>i<7b{@*<1~(S z&-TCEIwrne)Nk}Wv|oPN3i&(nv}`(~R<|)x{_$(Yu1Ble?f1-|MfY3K9(>D{bO=b9 z#+%2~yX1{q_&fdI&2!&hFlB^y;YXHw4?42JW+Tg!@G0xcH$~)V_Mqn#+E@)CDPLaY zm|E(MkL5{=Bcqv+EqBY?gj?G|LgX^|#yLuor#DHt3&Bclt!hP=6glX<#uh@ph!`Vb zN4ZJ?9&}Zc^mq=CJTvl3tPc&u0)ENl)KsK$&yEWUe}t*Bb@=%LUSVW4Uw9CWtkWEp zPYxKQM)72u3rXqY9zuQQua%Y&%c=VHZ=UpFM-UJo&I=E^+399xyV`xP-JsTKN&VBe zN?D^%oTXjVT#+7;f|OD*v9Xmbae!Zw@~P#ZqLXQAa*`!G5D4-Cj{~BJd&|Ack1KBg z2ERHk>sO`IO-dXb$PA#k%mw&7L4&^0<8`z!r!}t7uDJ)eF?1HxA+YE;%y5xRvGQh0 zUuA7=htb_TN*;CD%i=9fg_Tb)aYB#UW-1U&oNNofotGZ8yU-pJ69a@Y&5>Tu?+#C^ zFQk*je*F7d)<=?fO!xE-E2=jWvWb@0bk#XEIP_jj&q)uu8}%f6K5Hg0H;ZFtdMEZ; z+QW+Yf7JQ#2Ia_Qz%gx^7(|=&Qi(GK*sOwUJGRqfUQ66uWy@wJT(~H(<6M_qsD0Zg z9`)I{{XT+d-}6rFRUv>9^v?9Z9|LO#?6%WvFpgT*Pmc=@232F@vwy# z^kjZ)Y-~?-1{EB9agHzcFZkn~mtzfei{tBq&s={5d(Whv8aFQ{nX7;^GBTnoEb~s= z+&Ep_A-zrIWHfHxw8>n!2T&-KPt;2zH92d7sL)=;lT7TsICoH7xQ^%+-u)k};G+8b z2CfK)tTEcsl)#&P<7>bh;2#nz1c8(QN&UZOI2aLL%Jcsxx!^@LDd$3xxxH}MDIRcK zFHI2W^gmN0#*7{;h?fl}gFtkE{Y|f_sevH^_2BjGQJ{kxr2_vZQqTfh!ey>SN!>r8 z(2gviJ>Xd9;r$(+`p-uH=ROJ){>j(ygTo~+^Cg~zYT z1s|jRH<{u;@+$tj+A8_qU1rn+V&}lo+>XD{Z)12`WNt1CB+h^!iW=ZDg=eq+57q-( ArT_o{ literal 0 HcmV?d00001 -- 2.45.2