본문 바로가기
아두이노 + 인공지능

아두이노 강화학습(가위바위보AI) 3 - 아두이노

by Majestyblue 2024. 1. 26.

이번 시간에는 아두이노로 직접 구현하도록 하겠습니다. Tinkercad의 회로를 이용하여 구현했는데, Q 학습을 구현한 핵심 코드 설명 위주로 진행하겠습니다.

 

1. Q 테이블

이전시간에 구현했던 Q테이블 입니다.

 

 

아두이노 코드로는 아래와 같이 3x3 행렬로 구현할 수 있습니다. Q(s, a)를 Q[s][a]로 표현한 것입니다.

/* 
가위(Scissors): 0, 바위(Rock): 1, 보(Paper): 2
행은 상대편의 상태(state), 
열은 AI의 행동(action)을 나타낸다.
가위 {가위, 바위, 보}
바위 {가위, 바위, 보}
보   {가위, 바위, 보}
  */
float Q_table[3][3] = {
    {0.0, 0.0, 0.0},
    {0.0, 0.0, 0.0},
    {0.0, 0.0, 0.0}
};

 

 

2. 행동(action) 구현하기

상태 stats에서 행동 action은 Q함수의 상태 s에서의 최대 Q값을 선택한다고 하였습니다. 참고로 코드 작성은 chatGPT의 도움을 많아 받았습니다. (파이썬만 10년 가까이 하다 보니 메인 언어였던 C, C++과 C#을 다 잊어버렸습니다...)

/*
특정 행(row)를 입력으로 받아 해당 행에서 가장 큰 값을 가진 
열의 인덱스(maxIndex)를 반환한다. 초기 maxValue는 해당 행의 
첫 번째 값으로 설정하고, 그 이후로 행의 모든 값을 순회하면서 
가장 큰 값을 찾아 그 인덱스를 반환한다.
즉 플레이어의 상태를 입력하면 ai의 액션(가위바위보)를
출력하는 것이다.
*/
int get_action(int row) {
  int maxIndex = 0;
  float maxValue = Q_table[row][0];

  for (int i = 1; i < 3; i++) {
    if (Q_table[row][i] > maxValue) {
      maxValue = Q_table[row][i];
      maxIndex = i;
    }
  }

  return maxIndex;
}

 

 

3. 보상(reward) 구현하기

이기면 +1, 비기거나 지면 -1을 구현하였습니다. 아래 블로그를 참고하여 제작하였습니다.

https://m.blog.naver.com/PostView.naver?blogId=nms200299&logNo=221377198830&proxyReferer=

 

[C언어] 가위바위보 게임 (if 대신 switch 사용)

어제 부산 벡스코에서 열린 '2018 SW교육페스티벌'에 참여했는데 코드클럽(Codeclub) 부스에서 HTML...

blog.naver.com

 

가위를 0, 바위를 1, 보를 2로 두었을 때 숫자 연산을 통하여 아래와 같이 누가 이겼는지 알 수 있다.

 

/*
외부(you)의 입력과 AI의 판단(ai)을 이용하여 누가 이겼는지 판별해주는
함수 switch case로 작성함. sum = you-ai로 정의하여 결과에 따라
누가 이겼는지 알 수 있다. 
you가 이겼다면 1, 비기거나 ai가 이겼다면 -1을 리턴한다.
*/

float result(int ai, int you){
  int sum = you - ai;
  int reward = -1;
  lcd_1.clear();
  lcd_1.setCursor(0, 0);
  lcd_1.print((String)"ai:" + getName[ai] + ", you:" + getName[you]);
  lcd_1.setCursor(0, 1);
  switch(sum){
    case 0:
    lcd_1.print("Draw!");
    break;
    case 1:
    lcd_1.print("You win!");
    break;
    case -2:
    lcd_1.print("You win!");
    break;
    case -1:
    lcd_1.print("AI win!");
    reward = 1;
    break;
    case 2:
    lcd_1.print("AI win!");
    reward = 1;
    break;
  }
  return reward;
}

 

 

4. 학습(learn) 구현하기

학습은 이전 시간에도 언급하였던 Q 함수 업데이트 규칙을 이용한다.

void learn(int state, int action, float reward){
    Q_table[state][action] = Q_table[state][action]+reward;
}

 

 

 

5. 게임 진행 

아두이노의 void loop 문에서 실행할 게임 진행 loop의 핵심 코드이다. 순서는 다음과 같다.

먼저 초기 상태(state)를 '바위'로 하였다.

  1. 플레이어가 가위(S_button), 바위(R_button), 보(P_button) 셋 중 하나의 버튼을 누른다.
  2. 인공지능은 현재 상태에 대하여 Q함수를 통해 취해야 할 행동(가위, 바위, 보 중 하나)를 action으로 리턴한다.
  3. 플레이어가 누른 해당 버튼의 상태를 다음 상태(next state)로 한다.
  4. 플레이어가 누른 다음 상태와 행동 결과 값을 이용하여 보상(reward)를 계산한다.
  5. 계산 결과 값을 이용하여 학습을 진행한다. 과적합 방지를 위해 ai가 3회 이상 이기면(winning > 3) 학습을 중지한다.
  6. 플레이어가 누른 다음 상태(next state)를 현재 상태(state)로 바꾼다.
  7. 1 ~ 6까지 계속 반복한다.
if (S_button == HIGH || R_button == HIGH || P_button == HIGH ){
    int action = get_action(state); // ai select
    if (S_button == HIGH){
      next_state = 0;
    }
    else if (R_button == HIGH){
      next_state = 1;
    }
    else if (P_button == HIGH){
      next_state = 2;
    }
    float reward = result(action, next_state);
    if(reward==1){
      if(winning <3){
        learn(state, action, reward);
        winning += 1;
      }
      else{
      }
    }
    else{
      learn(state, action, reward);
      winning = 0;
    }
    state = next_state;
    prt_Q_table();
  }

 

 

6. 아두이노 회로 및 전체 코드

 

#include <LiquidCrystal_I2C.h>
#include <Wire.h>

int Flag = 0;

// C++ code
LiquidCrystal_I2C lcd_1(0x27, 16, 2);

/* 
가위(Scissors): 0, 바위(Rock): 1, 보(Paper): 2
행은 상대편의 상태(state), 
열은 AI의 행동(action)을 나타낸다.
가위 {가위, 바위, 보}
바위 {가위, 바위, 보}
보   {가위, 바위, 보}
  */
float Q_table[3][3] = {
    {0.0, 0.0, 0.0},
    {0.0, 0.0, 0.0},
    {0.0, 0.0, 0.0}
};

// 초창기 상대편 상태는 바위(1)이라고 하자.
int state = 1;
int next_state = 0;

// 3연승하면 훈련 중지(과적합 방지)
int winning = 0;

// 숫자를 가위바위보 약어 이름으로 바꾸기
char* getName[3] = {"S", "R", "P"};

// 버튼 눌렸는지 확인하기
bool S_button = 0;
bool R_button = 0;
bool P_button = 0;
bool Reset_button = 0;

void setup()
{
  lcd_1.init();
  lcd_1.backlight();
  Serial.begin(9600); 
  // 4,5,6 번은 각각 가위,바위,보, 8번은 Q_table 리셋
  pinMode(4, INPUT);
  pinMode(5, INPUT);
  pinMode(6, INPUT);
  pinMode(8, INPUT);
  lcd_1.begin(16, 2);
  lcd_1.setCursor(0, 0);
  lcd_1.print("Hi ^^");
  lcd_1.setCursor(0, 1);
  lcd_1.print("Let's play game!");
}

/*
특정 행(row)를 입력으로 받아 해당 행에서 가장 큰 값을 가진 
열의 인덱스(maxIndex)를 반환한다. 초기 maxValue는 해당 행의 
첫 번째 값으로 설정하고, 그 이후로 행의 모든 값을 순회하면서 
가장 큰 값을 찾아 그 인덱스를 반환한다.
즉 플레이어의 상태를 입력하면 ai의 액션(가위바위보)를
출력하는 것이다.
*/
int get_action(int row) {
  int maxIndex = 0;
  float maxValue = Q_table[row][0];

  for (int i = 1; i < 3; i++) {
    if (Q_table[row][i] > maxValue) {
      maxValue = Q_table[row][i];
      maxIndex = i;
    }
  }

  return maxIndex;
}


/*
외부(you)의 입력과 AI의 판단(ai)을 이용하여 누가 이겼는지 판별해주는
함수 switch case로 작성함. sum = you-ai로 정의하여 결과에 따라
누가 이겼는지 알 수 있다. 
you가 이겼다면 1, 비기거나 ai가 이겼다면 -1을 리턴한다.
*/

float result(int ai, int you){
  int sum = you - ai;
  int reward = -1;
  lcd_1.clear();
  lcd_1.setCursor(0, 0);
  lcd_1.print((String)"ai:" + getName[ai] + ", you:" + getName[you]);
  lcd_1.setCursor(0, 1);
  switch(sum){
    case 0:
    lcd_1.print("Draw!");
    break;
    case 1:
    lcd_1.print("You win!");
    break;
    case -2:
    lcd_1.print("You win!");
    break;
    case -1:
    lcd_1.print("AI win!");
    reward = 1;
    break;
    case 2:
    lcd_1.print("AI win!");
    reward = 1;
    break;
  }
  return reward;
}

/* 
Q_table 값을 출력하는 함수
Q_table 업데이트를 확인할 것이다.
*/
void prt_Q_table(){
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      Serial.print(Q_table[i][j]);
      Serial.print(" ");
    }
    Serial.println();
  }
}

void learn(int state, int action, float reward){
    Q_table[state][action] = Q_table[state][action]+reward;
}

void Reset_Q(){
  for(int i=0; i<3; i++) {
    for(int j=0; j<3; j++) {
        Q_table[i][j] = 0.0;
    }
  }
}

  
void loop()
{
  S_button = digitalRead(4);
  R_button = digitalRead(5);
  P_button = digitalRead(6);
  Reset_button = digitalRead(8);
  
  if (Reset_button == HIGH){
    lcd_1.clear();
    lcd_1.setCursor(0, 0);
    lcd_1.print("Reset!");
    Reset_Q();
    delay(1000);
    lcd_1.setCursor(0, 0);
    lcd_1.print("Hi ^^");
    lcd_1.setCursor(0, 1);
    lcd_1.print("Let's play game!");
  }
    
  
  if (S_button == HIGH || R_button == HIGH || P_button == HIGH ){
    if(Flag==0) # 연속 버튼 눌림 방지 기능
    {
      Flag = 1;

      int action = get_action(state); // ai select
      if (S_button == HIGH){
        next_state = 0;
      }
      else if (R_button == HIGH){
        next_state = 1;
      }
      else if (P_button == HIGH){
        next_state = 2;
      }
      float reward = result(action, next_state);
      if(reward==1){
        if(winning <3){
          learn(state, action, reward);
          winning += 1;
        }
        else{
        }

      }
      else{
        learn(state, action, reward);
        winning = 0;
      }
    }
    
    state = next_state;
    prt_Q_table();
  }
  else{
    Flag=0;
  }

}

 

 

 

7. 시뮬레이션

https://www.tinkercad.com/things/2Rqt9MpLSnc-q-learning?sharecode=WdiBkfZVOT8Tfpd8wXl3YxYSDCmWfyF5b_zXSP0WdXc

 

Login | Tinkercad

 

www.tinkercad.com

 

로그인 → 시뮬레이션 클릭 → 시뮬레이션 시작을 클릭 하면 해 볼 수 있다.

일정한 패턴 (가위 → 바위 → 보 반복)을 하면 AI가 학습을 완료한다. 이 때 패턴을 바꾸어 보자(바위 → 가위 등) 

몇 번 지나다 보면 다시 패턴을 학습한다.

 

그러나 너무나도 간단하게 구현한 인공지능이므로 계속 바꾸다 보면 학습이 제대로 되지 않는다. 이 때 reset을 눌러준다.

 

끝!