From 2f4bf7475ee8e630142d35ed9aee7376b2fe95d5 Mon Sep 17 00:00:00 2001 From: "REDMOND\\sayanpa" Date: Fri, 21 Jul 2017 21:02:29 -0700 Subject: [PATCH] Added new example that uses the CNTK Keras backend to train a bird to fly through a cactus maze using reinforcement learning. --- .../FlappingBird_with_keras_DQN.py | 258 ++++++++++++++++++ .../FlappingBirdWithKeras/README.md | 72 +++++ .../FlappingBirdWithKeras/__init__.py | 11 + .../assets/sprites/background-black.png | Bin 0 -> 4030 bytes .../assets/sprites/base.png | Bin 0 -> 664 bytes .../assets/sprites/cactus-green.png | Bin 0 -> 16417 bytes .../assets/sprites/newbird-downflap.png | Bin 0 -> 1642 bytes .../assets/sprites/newbird-midflap.png | Bin 0 -> 1584 bytes .../assets/sprites/newbird-upflap.png | Bin 0 -> 1744 bytes .../FlappingBirdWithKeras/game/__init__.py | 9 + .../game/flappy_bird_utils.py | 89 ++++++ .../game/wrapped_flappy_bird.py | 227 +++++++++++++++ .../conda-linux-cntk-py27-environment.yml | 12 +- .../conda-linux-cntk-py34-environment.yml | 21 +- .../conda-linux-cntk-py35-environment.yml | 12 +- .../conda-linux-cntk-py36-environment.yml | 12 +- .../conda-windows-cntk-py27-environment.yml | 12 +- .../conda-windows-cntk-py34-environment.yml | 12 +- .../conda-windows-cntk-py35-environment.yml | 12 +- .../conda-windows-cntk-py36-environment.yml | 12 +- .../FlappingBird_with_keras_DQN_test.py | 60 ++++ Tools/samples.json | 9 + 22 files changed, 803 insertions(+), 37 deletions(-) create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/FlappingBird_with_keras_DQN.py create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/README.md create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/__init__.py create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/background-black.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/base.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/cactus-green.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/newbird-downflap.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/newbird-midflap.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/newbird-upflap.png create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/game/__init__.py create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/game/flappy_bird_utils.py create mode 100644 Examples/ReinforcementLearning/FlappingBirdWithKeras/game/wrapped_flappy_bird.py create mode 100644 Tests/EndToEndTests/CNTKv2Python/Examples/FlappingBird_with_keras_DQN_test.py diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/FlappingBird_with_keras_DQN.py b/Examples/ReinforcementLearning/FlappingBirdWithKeras/FlappingBird_with_keras_DQN.py new file mode 100644 index 000000000..b82027688 --- /dev/null +++ b/Examples/ReinforcementLearning/FlappingBirdWithKeras/FlappingBird_with_keras_DQN.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +from collections import deque +import json +import numpy as np +import os +import random +import requests +import skimage as skimage +from skimage import transform, color, exposure +from skimage.transform import rotate +from skimage.viewer import ImageViewer +import sys + +# Load the right urlretrieve based on python version +try: + from urllib.request import urlretrieve +except ImportError: + from urllib import urlretrieve + +import game.wrapped_flappy_bird as game + +from keras.models import model_from_json +from keras.models import Sequential +from keras.layers.core import Dense, Dropout, Activation, Flatten +from keras.layers.convolutional import Convolution2D, MaxPooling2D +from keras.optimizers import SGD , Adam + +GAME = 'bird' # the name of the game being played for log files +CONFIG = 'nothreshold' +ACTIONS = 2 # number of valid actions +GAMMA = 0.99 # decay rate of past observations +OBSERVATION = 320. # timesteps to observe before training +EXPLORE = 3000000. # frames over which to anneal epsilon +FINAL_EPSILON = 0.0001 # final value of epsilon +INITIAL_EPSILON = 0.1 # starting value of epsilon +REPLAY_MEMORY = 50000 # number of previous transitions to remember +BATCH = 32 # size of minibatch +FRAME_PER_ACTION = 1 +LEARNING_RATE = 1e-4 +NUMRUNS = 400 +PRETRAINED_MODEL_URL_DEFAULT = 'https://cntk.ai/Models/FlappingBird_keras/FlappingBird_model.h5.model' +PRETRAINED_MODEL_FNAME = 'FlappingBird_model.h5' + +img_rows , img_cols = 80, 80 +#Convert image into Black and white +img_channels = 4 #We stack 4 frames + +def pretrained_model_download(url, filename): + '''Download the file unless it already exists, with retry. Throws if all retries fail.''' + if os.path.exists(filename): + print('Reusing locally cached: ', filename) + else: + print('Starting download of {} to {}'.format(url, filename)) + retry_cnt = 0 + while True: + try: + urlretrieve(url, filename) + print('Download completed.') + return + except: + retry_cnt += 1 + if retry_cnt == max_retries: + raise Exception('Exceeded maximum retry count, aborting.') + print('Failed to download, retrying.') + time.sleep(np.random.randint(1,10)) + +def buildmodel(): + print("Now we build the model") + model = Sequential() + model.add(Convolution2D(32, 8, 8, subsample=(4, 4), border_mode='same',input_shape=(img_rows,img_cols,img_channels))) #80*80*4 + model.add(Activation('relu')) + model.add(Convolution2D(64, 4, 4, subsample=(2, 2), border_mode='same')) + model.add(Activation('relu')) + model.add(Convolution2D(64, 3, 3, subsample=(1, 1), border_mode='same')) + model.add(Activation('relu')) + model.add(Flatten()) + model.add(Dense(512)) + model.add(Activation('relu')) + model.add(Dense(2)) + + adam = Adam(lr=LEARNING_RATE) + model.compile(loss='mse',optimizer=adam) + print("We finish building the model") + return model + +def trainNetwork(model, args, pretrained_model_url=None, internal_testing=False): + print(args) + + # Check if pretrained model url is passed in else ignore + if not pretrained_model_url: + pretrained_model_url = PRETRAINED_MODEL_URL_DEFAULT + else: + pretrained_model_url = pretrained_model_url + + pretrained_model_fname = PRETRAINED_MODEL_FNAME + + # open up a game state to communicate with emulator + game_state = game.GameState() + + # store the previous observations in replay memory + D = deque() + + # get the first state by doing nothing and preprocess the image to 80x80x4 + do_nothing = np.zeros(ACTIONS) + do_nothing[0] = 1 + x_t, r_0, terminal = game_state.frame_step(do_nothing) + + x_t = skimage.color.rgb2gray(x_t) + x_t = skimage.transform.resize(x_t,(80,80)) + x_t = skimage.exposure.rescale_intensity(x_t,out_range=(0,255)) + + if internal_testing: + x_t = np.random.rand(x_t.shape[0], x_t.shape[1]) + + s_t = np.stack((x_t, x_t, x_t, x_t), axis=2).astype(np.float32) + + #In Keras, need to reshape + s_t = s_t.reshape(1, s_t.shape[0], s_t.shape[1], s_t.shape[2]) #1*80*80*4 + + if args['mode'] == 'Run': + OBSERVE = 999999999 #We keep observe, never train + epsilon = FINAL_EPSILON + print ("Now we load weight") + pretrained_model_download(pretrained_model_url, pretrained_model_fname) + model.load_weights(pretrained_model_fname) + adam = Adam(lr=LEARNING_RATE) + model.compile(loss='mse',optimizer=adam) + print ("Weight load successfully") + else: #We go to training mode + OBSERVE = OBSERVATION + epsilon = INITIAL_EPSILON + + t = 0 + while (True): + loss = 0 + Q_sa = 0 + action_index = 0 + r_t = 0 + a_t = np.zeros([ACTIONS]) + #choose an action epsilon greedy + if t % FRAME_PER_ACTION == 0: + if random.random() <= epsilon: + print("----------Random Action----------") + action_index = random.randrange(ACTIONS) + a_t[action_index] = 1 + else: + q = model.predict(s_t) #input a stack of 4 images, get the prediction + max_Q = np.argmax(q) + action_index = max_Q + a_t[max_Q] = 1 + + if ((args['mode'] == 'Run') and (t > NUMRUNS)): + break + + #We reduced the epsilon gradually + if epsilon > FINAL_EPSILON and t > OBSERVE: + epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / EXPLORE + + #run the selected action and observed next state and reward + x_t1_colored, r_t, terminal = game_state.frame_step(a_t) + + x_t1 = skimage.color.rgb2gray(x_t1_colored) + x_t1 = skimage.transform.resize(x_t1,(80,80)) + x_t1 = skimage.exposure.rescale_intensity(x_t1, out_range=(0, 255)) + + x_t1 = x_t1.reshape(1, x_t1.shape[0], x_t1.shape[1], 1).astype(np.float32) #1x80x80x1 + s_t1 = np.append(x_t1, s_t[:, :, :, :3], axis=3) + + # store the transition in D + + D.append((s_t, action_index, r_t, s_t1, terminal)) + if len(D) > REPLAY_MEMORY: + D.popleft() + + #only train if done observing + if t > OBSERVE: + #sample a minibatch to train on + minibatch = random.sample(D, BATCH) + + inputs = np.zeros((BATCH, s_t.shape[1], s_t.shape[2], s_t.shape[3])).astype(np.float32) #32, 80, 80, 4 + print (inputs.shape) + targets = np.zeros((inputs.shape[0], ACTIONS)).astype(np.float32) #32, 2 + + #Now we do the experience replay + for i in range(0, len(minibatch)): + state_t = minibatch[i][0] + action_t = minibatch[i][1] #This is action index + reward_t = minibatch[i][2] + state_t1 = minibatch[i][3] + terminal = minibatch[i][4] + # if terminated, only equals reward + + inputs[i:i + 1] = state_t #I saved down s_t + + targets[i] = model.predict(state_t) # Hitting each buttom probability + Q_sa = model.predict(state_t1) + + if terminal: + targets[i, action_t] = reward_t + else: + targets[i, action_t] = reward_t + GAMMA * np.max(Q_sa) + + # targets2 = normalize(targets) + loss += model.train_on_batch(inputs, targets) + + s_t = s_t1 + t = t + 1 + + # save progress every 10000 iterations + if t % 1000 == 0: + + print("Now we save model") + model.save_weights(pretrained_model_fname, overwrite=True) + with open("model.json", "w") as outfile: + json.dump(model.to_json(), outfile) + + # print info + state = "" + if t <= OBSERVE: + if internal_testing: + return 0 #0 means success + else: + state = "observe" + elif t > OBSERVE and t <= OBSERVE + EXPLORE: + state = "explore" + else: + state = "train" + + print("TIMESTEP", t, "/ STATE", state, \ + "/ EPSILON", epsilon, "/ ACTION", action_index, "/ REWARD", r_t, \ + "/ Q_MAX " , np.max(Q_sa), "/ Loss ", loss) + + print("Episode finished!") + print("************************") + +def playGame(args): + model = buildmodel() + trainNetwork(model,args) + +def main(): + parser = argparse.ArgumentParser(description='Description of your program') + parser.add_argument('-m','--mode', help='Train / Run', required=True) + args = vars(parser.parse_args()) + playGame(args) + +if __name__ == "__main__": + # CNTK auto detects the GPU and is able to optimally allocate resources + # Hence, these lines below are commented out. + # from keras import backend as K + #if K.backend() == 'tensorflow': + # config = tf.ConfigProto() + # config.gpu_options.allow_growth = True + # sess = tf.Session(config=config) + # K.set_session(sess) + main() diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/README.md b/Examples/ReinforcementLearning/FlappingBirdWithKeras/README.md new file mode 100644 index 000000000..5659284ff --- /dev/null +++ b/Examples/ReinforcementLearning/FlappingBirdWithKeras/README.md @@ -0,0 +1,72 @@ +# Flapping Bird using Keras and Reinforcement Learning + +In [CNTK 203 tutorial](https://github.com/Microsoft/CNTK/blob/master/Tutorials/CNTK_203_Reinforcement_Learning_Basics.ipynb), +we have introduced the basic concepts of reinforcement +learning. In this example, we show an easy way to train a popular game called +FlappyBird using Deep Q Network (DQN). This tutorial draws heavily on the +[original work](https://yanpanlau.github.io/2016/07/10/FlappyBird-Keras.html) +by Ben Lau on training the FlappyBird game with Keras frontend. This tutorial +uses the CNTK backend and with very little change (commenting out a few specific + references to TensorFlow) in the original code. + +Note: Since, we have replaced the game environment components with different components drawn +from public data sources, we call the game Flapping Bird. + +# Goals + +The key objective behind this example is to show how to: +- Use CNTK backend API with Keras frontend +- Interchangeably use models trained between TensorFlow and CNTK via Keras +- Train and test (evaluate) the flapping bird game using a simple DQN implementation. + +# Pre-requisite + +Assuming you have installed CNTK, installed Keras and configured Keras +backend to be CNTK. The details are [here](https://docs.microsoft.com/en-us/cognitive-toolkit/using-cntk-with-keras). + +This example takes additional dependency on the following Python packages: +- pygame (`pip install pygame`) +- scikit-learn (`conda install scikit-learn`) +- scikit-image (`conda install scikit-image`) + +These packages are needed to perform image manipulation operation and interactivity +of the RL agent with the game environment. + +# How to run? + +From the example directory, run: + +``` +python FlappingBird_with_keras_DQN.py -m Run +``` + +Note: if you run the game first time in "Run" mode a pre-trained model is +locally downloaded. Note, even though this model was trained with TensorFlow, +Keras takes care of saving and loading in a portable format. This allows for +model evaluation with CNTK. + +If you want to train the network from beginning, delete the locally cached +`FlappingBird_model.h5` (if you have a locally cached file) and run. After +training, the trained model can be evaluated with CNTK or TensorFlow. + +``` +python FlappingBird_with_keras_DQN.py -m Train +``` + +# Brief recap + +The code has 4 steps: + +1. Receive the image of the game screen as pixel array +2. Process the image +3. Use a Deep Convolutional Neural Network (CNN) to predict the best action +(flap up or down) +4. Train the network (millions of times) to maximize flying time + +Details can be found in Ben Lau's +[original work](https://yanpanlau.github.io/2016/07/10/FlappyBird-Keras.html) + +# Acknowledgements +- Ben Lau: Developer of Keras RL example for contributing the [code](https://yanpanlau.github.io/2016/07/10/FlappyBird-Keras.html) with TensorFlow backend. +- Bird sprite from [Open Game Art](https://opengameart.org/content/free-game-asset-grumpy-flappy-bird-sprite-sheets). +- Shreyaan Pathak: Seventh Grade student at Northstar Middle School, Kirkland, WA for creating and processing new game sprites. diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/__init__.py b/Examples/ReinforcementLearning/FlappingBirdWithKeras/__init__.py new file mode 100644 index 000000000..3610b2e09 --- /dev/null +++ b/Examples/ReinforcementLearning/FlappingBirdWithKeras/__init__.py @@ -0,0 +1,11 @@ +# ============================================================================== +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE.md file in the project root +# for full license information. +# ============================================================================== + +''' +CNTK Flappybird Game Env. +''' +# TODO: Remove * import +from .game import * \ No newline at end of file diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/background-black.png b/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/background-black.png new file mode 100644 index 0000000000000000000000000000000000000000..a81193243bd5194acb22b5bc8826ffcd2503a1ce GIT binary patch literal 4030 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU}E541Bxge->40wSkfJR9T^xl_H+M9WCijS zl0AZa85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#N(6GqNkcg59UmvUF z{9L`nl>DSry^7odpbiEGn+hu+GdHy)QK2F?C$HG5!d3~a!V1U+3F|8Y3;nDA{o-C@9zzrKDK}xwt{K19`Se86_nJR{Hwo<>h+i#(Mch>H3D2 zmX`VkM*2oZx=P7{9O-#x!EwNQn0$BtH z5O0c3d|4@L;p!@;Rg)35>WWo-U3d6^w7M9OOJ;z{BEr^zZjMPaaBbnQ!=` z)}W&{?b+Y`?hOo$rHz~SWlUgTWD0j^n8U=#G_!#r^-CM4fPjLzLxY2wf`jDVL?#xN z6TAWn6F4~n-128IFftY~vv8y^F-^HBt>)m+a0jS+RNH8fjHZ&&j5As&jFyw5RmW(x sINDMeZ9I)O3`g6_qumD@c8wbOC+4#JXMba34(d94y85}Sb4q9e0L?WRCjbBd literal 0 HcmV?d00001 diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/base.png b/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/base.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7779822f79885ff52f5ff8c9a8991408683988 GIT binary patch literal 664 zcmeAS@N?(olHy`uVBq!ia0y~yU~E}4sv&5Sa(k5C6L3C?&#~t zz_78O`%fY(kk47*5n0T@z;^_M8K-LVNi#4o>3X_2hE&A8y{+gMlPJ-4ac3;oGDY7D zZy16jU9Ml`eQ?o2?0{YbD1^W-$A` zFGFaIt6^^N&A!7%r3IZ(lB{Q&*z zXa0fUn}F}J3m7ieBZ*u9i`XNHM0^9Oi5FinJvFT!1j}-M>jgBtc+B*5)0sEtL9nm% zJ}Z}i?sohA>wl?VvW6Rf0j#yCvUJl+Ce{bNKo>^Iz2CRXfssjE!(q!Fo4Y$8OqB%; r6C0RZn79NuH5^7cXkoDF_FqPZb58ay!AdiMsfoeU)z4*}Q$iB}l8NA+ literal 0 HcmV?d00001 diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/cactus-green.png b/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/cactus-green.png new file mode 100644 index 0000000000000000000000000000000000000000..a1925969640460ff44255698ceee2b904255ec05 GIT binary patch literal 16417 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 literal 0 HcmV?d00001 diff --git a/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/newbird-midflap.png b/Examples/ReinforcementLearning/FlappingBirdWithKeras/assets/sprites/newbird-midflap.png new file mode 100644 index 0000000000000000000000000000000000000000..92376985ade2207943abebc01d6e4beca2181ce3 GIT binary patch literal 1584 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 m9$*OQ0!{&!`M>=y+dlxa@p+0z=Rd;$0000 -2 * PLAYER_HEIGHT: + self.playerVelY = self.playerFlapAcc + self.playerFlapped = True + #SOUNDS['wing'].play() + + # check for score + playerMidPos = self.playerx + PLAYER_WIDTH / 2 + for pipe in self.upperPipes: + pipeMidPos = pipe['x'] + PIPE_WIDTH / 2 + if pipeMidPos <= playerMidPos < pipeMidPos + 4: + self.score += 1 + #SOUNDS['point'].play() + reward = 1 + + # playerIndex basex change + if (self.loopIter + 1) % 3 == 0: + self.playerIndex = next(PLAYER_INDEX_GEN) + self.loopIter = (self.loopIter + 1) % 30 + self.basex = -((-self.basex + 100) % self.baseShift) + + # player's movement + if self.playerVelY < self.playerMaxVelY and not self.playerFlapped: + self.playerVelY += self.playerAccY + if self.playerFlapped: + self.playerFlapped = False + self.playery += min(self.playerVelY, BASEY - self.playery - PLAYER_HEIGHT) + if self.playery < 0: + self.playery = 0 + + # move pipes to left + for uPipe, lPipe in zip(self.upperPipes, self.lowerPipes): + uPipe['x'] += self.pipeVelX + lPipe['x'] += self.pipeVelX + + # add new pipe when first pipe is about to touch left of screen + if 0 < self.upperPipes[0]['x'] < 5: + newPipe = getRandomPipe() + self.upperPipes.append(newPipe[0]) + self.lowerPipes.append(newPipe[1]) + + # remove first pipe if its out of the screen + if self.upperPipes[0]['x'] < -PIPE_WIDTH: + self.upperPipes.pop(0) + self.lowerPipes.pop(0) + + # check if crash here + isCrash= checkCrash({'x': self.playerx, 'y': self.playery, + 'index': self.playerIndex}, + self.upperPipes, self.lowerPipes) + if isCrash: + #SOUNDS['hit'].play() + #SOUNDS['die'].play() + terminal = True + self.__init__() + reward = -1 + + # draw sprites + SCREEN.blit(IMAGES['background'], (0,0)) + + for uPipe, lPipe in zip(self.upperPipes, self.lowerPipes): + SCREEN.blit(IMAGES['pipe'][0], (uPipe['x'], uPipe['y'])) + SCREEN.blit(IMAGES['pipe'][1], (lPipe['x'], lPipe['y'])) + + SCREEN.blit(IMAGES['base'], (self.basex, BASEY)) + # print score so player overlaps the score + # showScore(self.score) + SCREEN.blit(IMAGES['player'][self.playerIndex], + (self.playerx, self.playery)) + + image_data = pygame.surfarray.array3d(pygame.display.get_surface()) + pygame.display.update() + #print ("FPS" , FPSCLOCK.get_fps()) + FPSCLOCK.tick(FPS) + #print self.upperPipes[0]['y'] + PIPE_HEIGHT - int(BASEY * 0.2) + return image_data, reward, terminal + +def getRandomPipe(): + """returns a randomly generated pipe""" + # y of gap between upper and lower pipe + gapYs = [20, 30, 40, 50, 60, 70, 80, 90] + index = random.randint(0, len(gapYs)-1) + gapY = gapYs[index] + + gapY += int(BASEY * 0.2) + pipeX = SCREENWIDTH + 10 + + return [ + {'x': pipeX, 'y': gapY - PIPE_HEIGHT}, # upper pipe + {'x': pipeX, 'y': gapY + PIPEGAPSIZE}, # lower pipe + ] + + +def showScore(score): + """displays score in center of screen""" + scoreDigits = [int(x) for x in list(str(score))] + totalWidth = 0 # total width of all numbers to be printed + + for digit in scoreDigits: + totalWidth += IMAGES['numbers'][digit].get_width() + + Xoffset = (SCREENWIDTH - totalWidth) / 2 + + for digit in scoreDigits: + SCREEN.blit(IMAGES['numbers'][digit], (Xoffset, SCREENHEIGHT * 0.1)) + Xoffset += IMAGES['numbers'][digit].get_width() + + +def checkCrash(player, upperPipes, lowerPipes): + """returns True if player collders with base or pipes.""" + pi = player['index'] + player['w'] = IMAGES['player'][0].get_width() + player['h'] = IMAGES['player'][0].get_height() + + # if player crashes into ground + if player['y'] + player['h'] >= BASEY - 1: + return True + else: + + playerRect = pygame.Rect(player['x'], player['y'], + player['w'], player['h']) + + for uPipe, lPipe in zip(upperPipes, lowerPipes): + # upper and lower pipe rects + uPipeRect = pygame.Rect(uPipe['x'], uPipe['y'], PIPE_WIDTH, PIPE_HEIGHT) + lPipeRect = pygame.Rect(lPipe['x'], lPipe['y'], PIPE_WIDTH, PIPE_HEIGHT) + + # player and upper/lower pipe hitmasks + pHitMask = HITMASKS['player'][pi] + uHitmask = HITMASKS['pipe'][0] + lHitmask = HITMASKS['pipe'][1] + + # if bird collided with upipe or lpipe + uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask) + lCollide = pixelCollision(playerRect, lPipeRect, pHitMask, lHitmask) + + if uCollide or lCollide: + return True + + return False + +def pixelCollision(rect1, rect2, hitmask1, hitmask2): + """Checks if two objects collide and not just their rects""" + rect = rect1.clip(rect2) + + if rect.width == 0 or rect.height == 0: + return False + + x1, y1 = rect.x - rect1.x, rect.y - rect1.y + x2, y2 = rect.x - rect2.x, rect.y - rect2.y + + for x in range(rect.width): + for y in range(rect.height): + if hitmask1[x1+x][y1+y] and hitmask2[x2+x][y2+y]: + return True + return False diff --git a/Scripts/install/linux/conda-linux-cntk-py27-environment.yml b/Scripts/install/linux/conda-linux-cntk-py27-environment.yml index edc6abaf3..679dcf674 100644 --- a/Scripts/install/linux/conda-linux-cntk-py27-environment.yml +++ b/Scripts/install/linux/conda-linux-cntk-py27-environment.yml @@ -16,15 +16,19 @@ dependencies: - pip=8.1.2=py27_0 - python=2.7.11=5 - pyyaml=3.12=py27_0 +- scikit-image=0.12.3=np111py27_1 +- scikit-learn=0.18.1=np111py27_0 - scipy=0.18.1=np111py27_0 - seaborn=0.7.1=py27_0 - setuptools=27.2.0=py27_0 - six=1.10.0=py27_0 - wheel=0.29.0=py27_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym[atari]==0.8.1 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/linux/conda-linux-cntk-py34-environment.yml b/Scripts/install/linux/conda-linux-cntk-py34-environment.yml index 44b712da2..850f66933 100644 --- a/Scripts/install/linux/conda-linux-cntk-py34-environment.yml +++ b/Scripts/install/linux/conda-linux-cntk-py34-environment.yml @@ -9,25 +9,28 @@ dependencies: - jupyter=1.0.0=py34_3 - matplotlib=1.5.3=np111py34_0 - numpy=1.11.2=py34_0 +- opencv=3.1.0=np111py34_1 - pandas=0.19.1=np111py34_0 - pandas-datareader=0.2.1=py34_0 - pillow=3.4.2=py34_0 - pip=8.1.2=py34_0 - python=3.4.4=5 - pyyaml=3.12=py34_0 +- scikit-image=0.12.3=np111py34_1 +- scikit-learn=0.18.1=np111py34_0 - scipy=0.18.1=np111py34_0 - seaborn=0.7.1=py34_0 - setuptools=27.2.0=py34_0 - six=1.10.0=py34_0 - wheel=0.29.0=py34_0 -- opencv=3.1.0=np111py34_1 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - - gym[atari]==0.8.1 - - pydot-ng==1.0.0 - - future==0.16.0 - easydict==1.6.0 - - scikit-image==0.12.3 + - future==0.16.0 + - gym[atari]==0.8.1 + - keras==2.0.6 + - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/linux/conda-linux-cntk-py35-environment.yml b/Scripts/install/linux/conda-linux-cntk-py35-environment.yml index 6fa9f4d62..a72c7e9c9 100644 --- a/Scripts/install/linux/conda-linux-cntk-py35-environment.yml +++ b/Scripts/install/linux/conda-linux-cntk-py35-environment.yml @@ -15,15 +15,19 @@ dependencies: - pip=8.1.2=py35_0 - python=3.5.2=0 - pyyaml=3.12=py35_0 +- scikit-image=0.12.3=np111py35_1 +- scikit-learn=0.18.1=np111py35_0 - scipy=0.18.1=np111py35_0 - seaborn=0.7.1=py35_0 - setuptools=27.2.0=py35_0 - six=1.10.0=py35_0 - wheel=0.29.0=py35_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym[atari]==0.8.1 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/linux/conda-linux-cntk-py36-environment.yml b/Scripts/install/linux/conda-linux-cntk-py36-environment.yml index 8a5120ae8..1e6f9e890 100644 --- a/Scripts/install/linux/conda-linux-cntk-py36-environment.yml +++ b/Scripts/install/linux/conda-linux-cntk-py36-environment.yml @@ -15,15 +15,19 @@ dependencies: - pip=9.0.1=py36_1 - python=3.6.0=0 - pyyaml=3.12=py36_0 +- scikit-image=0.12.3=np111py36_1 +- scikit-learn=0.18.1=np111py36_0 - scipy=0.18.1=np111py36_0 - seaborn=0.7.1=py36_0 - setuptools=27.2.0=py36_0 - six=1.10.0=py36_0 - wheel=0.29.0=py36_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym[atari]==0.8.1 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/windows/conda-windows-cntk-py27-environment.yml b/Scripts/install/windows/conda-windows-cntk-py27-environment.yml index 7d413aee9..285a9a2a4 100644 --- a/Scripts/install/windows/conda-windows-cntk-py27-environment.yml +++ b/Scripts/install/windows/conda-windows-cntk-py27-environment.yml @@ -16,15 +16,19 @@ dependencies: - pip=8.1.2=py27_0 - python=2.7.11=5 - pyyaml=3.12=py27_0 +- scikit-image=0.12.3=np111py27_1 +- scikit-learn=0.18.1=np111py27_0 - scipy=0.18.1=np111py27_0 - seaborn=0.7.1=py27_0 - setuptools=27.2.0=py27_1 - six=1.10.0=py27_0 - wheel=0.29.0=py27_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym==0.5.2 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/windows/conda-windows-cntk-py34-environment.yml b/Scripts/install/windows/conda-windows-cntk-py34-environment.yml index ffeeadceb..7d1c4dc05 100644 --- a/Scripts/install/windows/conda-windows-cntk-py34-environment.yml +++ b/Scripts/install/windows/conda-windows-cntk-py34-environment.yml @@ -15,15 +15,19 @@ dependencies: - pip=8.1.2=py34_0 - python=3.4.4=5 - pyyaml=3.12=py34_0 +- scikit-image=0.12.3=np111py34_1 +- scikit-learn=0.18.1=np111py34_0 - scipy=0.18.1=np111py34_0 - seaborn=0.7.1=py34_0 - setuptools=27.2.0=py34_1 - six=1.10.0=py34_0 - wheel=0.29.0=py34_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym==0.5.2 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/windows/conda-windows-cntk-py35-environment.yml b/Scripts/install/windows/conda-windows-cntk-py35-environment.yml index 0630a62b6..9ca4f5f23 100644 --- a/Scripts/install/windows/conda-windows-cntk-py35-environment.yml +++ b/Scripts/install/windows/conda-windows-cntk-py35-environment.yml @@ -15,15 +15,19 @@ dependencies: - pip=8.1.2=py35_0 - python=3.5.2=0 - pyyaml=3.12=py35_0 +- scikit-image=0.12.3=np111py35_1 +- scikit-learn=0.18.1=np111py35_0 - scipy=0.18.1=np111py35_0 - seaborn=0.7.1=py35_0 - setuptools=27.2.0=py35_1 - six=1.10.0=py35_0 - wheel=0.29.0=py35_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym==0.5.2 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Scripts/install/windows/conda-windows-cntk-py36-environment.yml b/Scripts/install/windows/conda-windows-cntk-py36-environment.yml index b7c1ae1a4..8672a1ed8 100644 --- a/Scripts/install/windows/conda-windows-cntk-py36-environment.yml +++ b/Scripts/install/windows/conda-windows-cntk-py36-environment.yml @@ -15,15 +15,19 @@ dependencies: - pip=9.0.1=py36_1 - python=3.6.0=0 - pyyaml=3.12=py36_0 +- scikit-image=0.12.3=np111py36_1 +- scikit-learn=0.18.1=np111py36_0 - scipy=0.18.1=np111py36_0 - seaborn=0.7.1=py36_0 - setuptools=27.2.0=py36_1 - six=1.10.0=py36_0 - wheel=0.29.0=py36_0 - pip: - - pytest==3.0.3 - - sphinx==1.5.4 - - sphinx-rtd-theme==0.2.4 - - twine==1.8.1 - gym==0.5.2 + - keras==2.0.6 - pydot-ng==1.0.0 + - pygame==1.9.3 + - pytest==3.0.3 + - sphinx-rtd-theme==0.2.4 + - sphinx==1.5.4 + - twine==1.8.1 diff --git a/Tests/EndToEndTests/CNTKv2Python/Examples/FlappingBird_with_keras_DQN_test.py b/Tests/EndToEndTests/CNTKv2Python/Examples/FlappingBird_with_keras_DQN_test.py new file mode 100644 index 000000000..48b72efff --- /dev/null +++ b/Tests/EndToEndTests/CNTKv2Python/Examples/FlappingBird_with_keras_DQN_test.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft. All rights reserved. + +# Licensed under the MIT license. See LICENSE.md file in the project root +# for full license information. +# ============================================================================== + +import numpy as np +import os +import platform +import sys +import pytest + +os.environ["SDL_VIDEODRIVER"] = "dummy" +os.environ['KERAS_BACKEND'] = "cntk" + +from cntk.device import try_set_default_device, gpu + +abs_path = os.path.dirname(os.path.abspath(__file__)) +example_dir = os.path.join(abs_path, "..", "..", "..", "..", + "Examples", "ReinforcementLearning", + "FlappingBirdWithKeras") +sys.path.append(example_dir) +current_dir = os.getcwd() +os.chdir(example_dir) + +def test_FlappingBird_with_keras_DQN_noerror(device_id): + if platform.system() != 'Windows': + pytest.skip('Test only runs on Windows, pygame video device requirement constraint') + from cntk.ops.tests.ops_test_utils import cntk_device + try_set_default_device(cntk_device(device_id)) + + sys.path.append(example_dir) + current_dir = os.getcwd() + os.chdir(example_dir) + + import FlappingBird_with_keras_DQN as fbgame + + # TODO: Currently the model is downloaded from a cached site + # Change the code to pick up the model from a locally + # cached directory. + model = fbgame.buildmodel() + args = {'mode': 'Run'} + res = fbgame.trainNetwork(model, args, internal_testing=True ) + + np.testing.assert_array_equal(res, 0, \ + err_msg='Error in running Flapping Bird example', verbose=True) + + args = {'mode': 'Train'} + res = fbgame.trainNetwork(model, args, internal_testing=True ) + + np.testing.assert_array_equal(res, 0, \ + err_msg='Error in testing Flapping Bird example', verbose=True) + + #TODO: Add a test case to start with a CNTK trained cached model + os.chdir(current_dir) + print("Done") + + + + diff --git a/Tools/samples.json b/Tools/samples.json index e0e6e865e..75e0ceed0 100644 --- a/Tools/samples.json +++ b/Tools/samples.json @@ -539,4 +539,13 @@ "type": ["Recipe"], "dataadded": "05/05/2017" } + { + "category": ["Reinforcement Learning"], + "name": "Flapping Bird with Keras", + "url":"https://github.com/Microsoft/CNTK/tree/master/Examples/ReinforcementLearning/FlappingBirdWithKeras", + "description": "Using CNTK Keras backend train an agent to navigate a bird through a cactus maze", + "language": ["Python"], + "type": ["Recipe"], + "dataadded": "07/21/2017" + } ]