레이블이 테트리스 강화학습인 게시물을 표시합니다. 모든 게시물 표시
레이블이 테트리스 강화학습인 게시물을 표시합니다. 모든 게시물 표시

2017년 12월 18일 월요일

④테트리스 강화학습 - DQN으로 50시간 학습시키기

Github https://github.com/lyzqm123/Tetris-Reinforcement-Learning-with-Python3-keras

이전 포스팅에서 부족한 설명을 이 포스팅에서 약간 메꾸고 시작하겠다.

우선 강화학습의 원리는 간단하다.
어떤 환경(environment)이 주어지고 그 환경에서 학습하는 인공지능(agent)이 
현재 상태(state)에서 보상(reward)을 받아 행동(action)해 실력이 느는 방식이다.

여기서 해결해야할 아주 중요한 문제가 있다.
바로 '순차적 행동 결정 문제'이다.

이건 내가 기존의 오목 로봇 프로젝트에서 직접 겪었던 문제다.
agent는 현재 state에서 다음 결정을 위해 action을 취해야한다.

하지만 이 결정을 어떻게 내려야 되는가?
만약 현재 state를 수치화할 수 있는 명백한 요건이 있다면 해결되겠지만  
바둑, 오목, 테트리스에서 현재 state를 완벽하게 수치화를 판단하긴 굉장히 힘들것이다.
여기서는 이러한 문제점을 해결하기위해 Markov Decision Process(MDP)를 사용한다.

MDP는 한마디로 순차적 행동 결정문제를 해결하기위해 문제를 수학적으로 정의한 것이다.
MDP에는 앞서말한 state, action, reward가 포함되있고 추가로 정책(policy)로 구성된다.
이 프로젝트에서 MDP는 다음과 같이 될것이다. (물론 블록은 일정 시간마다 자동으로 내려간다.)

MDP를 해결했다는 것은 결국 agent가 가장 좋은 policy를 얻었다는 소리다.

이제 (Deep Q Network)DQN에대해 알아보자.
DQNDeepMind에서 사용된 알고리즘이다.
Q Learning은 오프폴리시(현재 행동하는 정책과는 독립적으로 학습함)알고리즘이다.
Q Learning에서 행동은 $\varepsilon$-greedy policy에 의해 결정된다.
(이게 무엇이냐면 강화학습의 중요 문제중 '탐색-이용의 딜레마(Explore-Exploit dilemma)'가 있다.
현재 우리가 학습한 방법을 고수(exploit)하면 탐험(explore)을 해서 얻을 수 있는 방법보다 보상을 덜 받을 수도 있다.
이것을 해결하기위해 $\varepsilon$의 확률을 사용하여 탐험을 시킨다.)

Q Learning과 인공신경망을 사용한게 DQN이다.
여기서 이 둘 사이를 이어주기위해 경험 리플레이(Experience Replay)를 사용한다.
리플레이 메모리에 지속적으로 $s$,$a$,$r$,$s'$를 넣어주고 무작위로 추출하여 
인공신경망 업데이트에 사용한다. (각각 state,action,reward,next state이다.)
나는 리플레이 메모리를 20000 Byte크기로 제한하였다.

그리고 Q Learning의 핵심 수식 큐함수 업데이트 공식은 다음과 같다.
$Q(S_{t},A_{t}) \leftarrow Q(S_{t},A_{t})+\alpha(R_{t+1}+\gamma \underset{a'}{max} Q(S_{t+1},a')-Q(S_{t},A_{t}))$
간단하게 설명하자면 다음 행동으로부터 큐함수의 가장 큰 값을 $\gamma$만큼 감소시키고 행동으로 얻은 
보상과 현재 큐함수의 값을 $\alpha$만큼 감소시켜 현재 큐함수와 더한값으로 업데이트 시키는 것이다. 

신경망은 다양한 종류가 있지만 DeepMind처럼 Convolution Neural Network(CNN)을 사용했다.
이때 처음 알았는데 이미지자체를 학습시키는 방법이였다.
이미지는 RGB채널이 있는데 굳이 색을 넣어 학습시킬 필요가없으므로 
흑백으로 전처리하고 신경망에 넣는다.

나는 애초에 20X8배열에 테트리스를 저장해서 이 배열로 keras에 맞는 크기인 
84X84로 확장시켜 넣어줬다. 
또한 책에서와 유사하게 세 개의 컨볼루션 층을 쌓고 50개의 노드 층을 형성시켰다.
(책에서는 512개의 노드 층을 형성시켰는데 컴퓨터가 좋지않아 줄였다)

이렇게 프로젝트를 구성하였고 50시간을 학습시킨 결과는 위의 영상에 나와있다.

위의 그래프는 큐함수 가장 큰 값의 평균그래프이고 아래는 점수 그래프이다.
점수그래프 또한 위의 그래프처럼 나올줄 알았는데 학습이 온전하게 되진 않은거 같다.
아래서부터 6칸 위로부터는 사실상 제대로 학습되진 않았다.
아마도 explore을 더 많이 하고 더 심층 신경망을 사용하고 좋은보상을 써야 해결될것 같다.

다음에는 온전한 테트리스 강화학습을 진행하도록 하겠다.



2017년 12월 14일 목요일

③테트리스 강화학습 - 테트리스 환경 만들기 (Python3)

Github https://github.com/lyzqm123/Tetris-Reinforcement-Learning-with-Python3-keras
책이 도착했다.
책내용이 알차고 쉽게 알려줘서 좋은 것 같다.

앞서서는 C++로 프로젝트를 진행하려 했으나 Python의 유틸성과 keras연동이 매우 쉽고
책또한 Python으로 설명이 되어있어 Python으로 바꾸었다.

강화학습의 종류는 꽤 많이 있는데 내가 할 강화학습은 Deep Q Learning(DQN)과 
Convolution Neural Network(CNN)으로 진행을 할 것이다.

책에서는 DeepMind의 브레이크아웃 게임을 가지고 설명이 되어있는 부분이 있다.

Python에는 유틸기능이 기본적으로 많이 깔려있는데 C++로는 콘솔창에서 직접 출력하며 그려준 반면
Python으로는 tkinter 모듈을 이용해 GUI를 쉽게 그려서 좀 더 화려하게 만들 수 있다.
사실 화려한것이 목적이아니라 CNN을 위해 이미지로 학습시키기 위해 사용한 것이다.

강화학습이다 보니 환경에 대한 보상이 어느정도 필요하다.
이 보상정도를 잘 맞추기위해서는 전문적인 지식이 필요하지만 
어쩔수없이 대략적으로 만들어보려한다.

보상은 3가지로 만들었다.
1. 특정 높이에 해당하는 위치에 블럭이 쌓일 때
테트리스의 배열은 20X10으로 정해주었다.
테트리스의 배열은 20X8로 수정 ($\varepsilon$이 클 경우 가로 10일 때 한줄채우기가 잘 안됨)

여기서 높이를 $h$라 두고 그 높이에 $n$개가 쌓였을 때 보상을 $f(h^2)$*$n$으로 주었다.
여기서 $f(h)$ 함수는 테트리스 배열의 높이를 [0, 2]로 차등분배한 값을 반환하는 함수이다. 
$f(h^{2})$함수는 $h^{2}$*$0.0008276$를 반환하는 함수 (보상의 기준을 낮춤)


차등 분배는 높이 3부터 시작하였고 제곱의 형태이다 보니 마지막은 4에 가까워진다.
즉, 마지막 줄을 다 채운다면 약 4*10인 40점이 부여된다.
즉, 마지막 줄에 한칸이 쌓인다면 약 0.3점 정도가 부여된다.
학습을 보다 아래쪽에 많이 쌓게끔 하면 기본적인 테트리스플레이가 나올것 같았다.

2. 줄마다 블록이 모두 찰 때
당연히 한줄마다 블록이 찰 때 보상을 해줘야지 인공지능이 줄에 블럭을 넣어 깰 것이다.
$t$개의 줄을 깰 때 보상을 $t$*$10$으로 주었다.

3. 일정 시간이 지날 때 
테트리스를 오랜 시간 유지하기위해 일정시간이 지날 때 보상을 주는것을 생각했다.

하지만 시간측정을 코드가 돌아가는 시간을 측정하는데 학습시간이 오래되다보면 데이터가 많이 쌓여
코드가 돌아가는 시간이 앞에 보다 자연히 늘어날 것이다.
이렇게 되면 기존의 틀에 결함이 생길 수 있어 아직은 구현하지 않았다.



현재는 랜덤으로 블럭을 낙하하고 종료될때 환경에 대한 보상을 확인하는 용도로 촬영했다.

2017년 12월 1일 금요일

②테트리스 강화학습 - 테트리스 구현하기(C++)

Github https://github.com/lyzqm123/Tetris-Reinforcement-Learning
일단 강화학습을 하기위한 기본 환경이 필요하기 때문에 테트리스 게임을 구현해 줍니다.
총 개발시간은 7~8시간 정도 걸린것 같고 약간 수정해야하는 부분이 있긴합니다.

객체지향 공부를 중심적으로 프로그램을 구현했습니다.
확실히 알고리즘을 많이 하다보니 예전에 비해 구현력도 많이 늘었던것 같습니다.



#include "stdafx.h"
int main(){
    //커서 깜빡임 없앰
    CONSOLE_CURSOR_INFO curInfo;
    GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &curInfo);
    curInfo.bVisible = 0
    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &curInfo); 
    FILE *tetris_log = fopen("tetris_log.txt""w");
    int test = 1;
    while (1) {
        goCursor(046); printf("<Test Count> : %d", test);
        Tetris *tetris = new Tetris();
        int score = tetris->start();
        fprintf(tetris_log, "<%d> - score : %d\n", test++, score);
        delete tetris;
    }
    return 0;
}
cs
우선 main문입니다. 
Tetris 객체에서 무한히 게임을 돌리는 방식입니다.


#include "ScoreBoard.h"
#include "NextBlcokBoard.h"
#define BASE_Y_SIZE 30
#define BASE_X_SIZE 32
class Tetris {
public:
    Tetris();
    ~Tetris();
    Tetris(int ysize, int xsize);
    int start();
    bool PossibleToMove(Dir dir);        //방향키 입력받았을 때 가능한 지
    bool PossibleToRotate();            //현재 블록을 회전시킬 때 가능한 지
    void ArrayPaint();
    bool ArrayHorizon();
    void BlinkBlock(int cursor_y, int cursor_x);
    void ArrayEraseAndPull(int y);
    void MapErase();
    void MapPrint();
    bool isGameEnd();
private:
    const int map_ysize, map_xsize;
    void BasicPrint();
    Board *score_board;
    Board *next_block_board;
    Block curr_block, next_block;
    bool map[BASE_Y_SIZE][BASE_X_SIZE / 2 + 1];
};
cs
Tetris 클래스는 Board 객체 2개와 Block객체, 함수들을 가집니다.
Board 클래스는 ScoreBoard 클래스와 NextBlockBoard 클래스의 부모 클래스입니다.
각각 점수판, 다음 블록 판에대해 처리를 합니다.
Block 클래스는 블록에대한 정보를 관리합니다.


int Tetris::start() {
    double plus = 0;
    curr_block.SetKind(GetRandomData(06));
    next_block.SetKind(GetRandomData(06));
    dynamic_cast<NextBlcokBoard*>(next_block_board)->SetBlockInfo(next_block);
    next_block_board->BasicPrint();
    score_board->BasicPrint();
    BasicPrint();
    curr_block.Print();
    auto start_time = clock();
    while (dynamic_cast<ScoreBoard*>(score_board)->GetScore() < LIMIT_SCORE) {
        int key = 0;
        for (int n = 0;n < 5;n++) {
            if (_kbhit()) {
                key = _getch();
                if (key == 224) {
                    key = _getch();
                    switch (key) {
                    case LEFT:
                        if (PossibleToMove(Dir::Left)) curr_block.Move(Dir::Left);
                        break;
                    case RIGHT:
                        if (PossibleToMove(Dir::Right)) curr_block.Move(Dir::Right);
                        break;
                    case DOWN:
                        if (PossibleToMove(Dir::Down)) curr_block.Move(Dir::Down);
                        break;
                    case UP:
                        if (PossibleToRotate()) curr_block.Rotate();
                        break;
                    defaultbreak;
                    }
                }
                else if (key == SPACE) {
                    while (PossibleToMove(Dir::Down)) curr_block.Move(Dir::Down);
                    while (_kbhit()) _getch();
                    break;
                }
                while (_kbhit()) _getch();
            }
        }
        auto end_time = clock();
        if ((long double)(end_time - start_time) / (long double)CLOCKS_PER_SEC >= GAME_VELOCITY - plus) {
            start_time = clock();
            if (PossibleToMove(Dir::Down)) {
                curr_block.Move(Dir::Down);
            }
            else if (isGameEnd()) return dynamic_cast<ScoreBoard*>(score_board)->GetScore();
            else {
                curr_block.Print(1);
                ArrayPaint();
                while (ArrayHorizon()) {
                    dynamic_cast<ScoreBoard*>(score_board)->SetScore(PLUS_SCORE);
                    plus += DIFFICULTY_WEIGHT;
                }
                curr_block.SetKind(next_block.GetKind());
                next_block.SetKind(GetRandomData(06));
                dynamic_cast<NextBlcokBoard*>(next_block_board)->SetBlockInfo(next_block);
                next_block_board->DataPrint();
            }
        }
    }
    return LIMIT_SCORE;
}
cs
start함수에서 모든과정이 시작하며 반복되며 끝이납니다.
초기에 현재 블록과 다음 블록을 랜덤하게 정해주고 기본 그림들을 그려줍니다.
키보드 입력값이 존재하고 가능하다면 처리를 해줍니다.
일정 시간이 흐르면 자동으로 한칸씩 내려오도록 만들었는데
이것을 이용해서 게임 난이도를 조절합니다.

현재 게임점수와 난이도를 비례하도록 만들었습니다. 

① 테트리스 강화학습 - 프로젝트 소개

제가 강화학습에대해 알게된 시기는 2015년에 "오목 로봇 프로젝트"를 진행하면서 알게되었습니다.



이때 당시에는 알파고가 나오기 1년전이라 인공지능에 대해 그렇게 많은 관심이 없었습니다.
오목 인공지능을 만들기 위해 많은 인공지능 알고리즘을 찾아보다가 
아주 신박한 동영상을 youtube에서 발견했습니다.

내용의 출처를 보면 지금은 너무나도 유명한 회사인 GoogleDeepMind가 만든 동영상 이였습니다.
강화학습인 Q-Learning을 이용해 시간이 갈 수록 점차 똑똑해지는것을 알 수 있습니다.
(이 때 DeepMind를 처음 알게되었는데 왠만한 회사면 까먹었을것이지만 
인공지능 소프트웨어 회사는 처음봐서 기억에 남았습니다.)

이것을 보고 언젠간 배워서 써먹어봐야지라고 생각을 하고선 실천을 하지않았는데 
이번기회에 해보려고합니다.
동아리 선배중에 강화학습으로 자율주행 자동차만들기에 성공해서 공부한 책을 추천해주셨습니다.

이 책을보면서 한번 강화학습으로 테트리스를 학습시켜보도록 하겠습니다.
(돈이없어서 알바비가 들어오면 그 때 책을 사고 공부한 후 구현해서 포스팅을 할 예정입니다.)