이번 시간에는 아두이노로 직접 구현하도록 하겠습니다. 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=
가위를 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)를 '바위'로 하였다.
- 플레이어가 가위(S_button), 바위(R_button), 보(P_button) 셋 중 하나의 버튼을 누른다.
- 인공지능은 현재 상태에 대하여 Q함수를 통해 취해야 할 행동(가위, 바위, 보 중 하나)를 action으로 리턴한다.
- 플레이어가 누른 해당 버튼의 상태를 다음 상태(next state)로 한다.
- 플레이어가 누른 다음 상태와 행동 결과 값을 이용하여 보상(reward)를 계산한다.
- 계산 결과 값을 이용하여 학습을 진행한다. 과적합 방지를 위해 ai가 3회 이상 이기면(winning > 3) 학습을 중지한다.
- 플레이어가 누른 다음 상태(next state)를 현재 상태(state)로 바꾼다.
- 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. 시뮬레이션
로그인 → 시뮬레이션 클릭 → 시뮬레이션 시작을 클릭 하면 해 볼 수 있다.
일정한 패턴 (가위 → 바위 → 보 반복)을 하면 AI가 학습을 완료한다. 이 때 패턴을 바꾸어 보자(바위 → 가위 등)
몇 번 지나다 보면 다시 패턴을 학습한다.
그러나 너무나도 간단하게 구현한 인공지능이므로 계속 바꾸다 보면 학습이 제대로 되지 않는다. 이 때 reset을 눌러준다.
끝!
'아두이노 + 인공지능' 카테고리의 다른 글
아두이노 강화학습(가위바위보AI) 2 - Q learning (0) | 2024.01.26 |
---|---|
아두이노 강화학습(가위바위보AI) 1 - 강화학습이란? (0) | 2024.01.25 |