우리가 단번에 떠올리는 인공지능은 이미지, 텍스트, agent와 같이 복잡한 문제를 해결하는 것으로 볼 수 있지만

갑자기 든 생각으로, 아주 기초적이고 간단한 문제를 딥러닝 모델을 활용해 풀어보려고 합니다.

 

 우선 아무 생각 없이 fc layer 몇 개 쌓고 했더니 답이 제대로 나오는 게 하나도 없어서 구글링을 통해 LSTM으로 구현해보았습니다.

 

 

먼저 계산 모델이 필요하겠죠? 풀코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from tensorflow.keras import layers
from tensorflow import keras
 
class Calc:
    def __init__(self):
        pass
 
    def build_model(self):
        n_numbers = 2
 
        model = keras.Sequential()
        model.add(layers.LSTM(6, input_shape=(n_numbers, 1), return_sequences=True))
        model.add(layers.LSTM(6))
        model.add(layers.Dense(1))
        model.compile(loss='mean_squared_error', optimizer='adam')
 
        return model    
cs

 

 

1
2
from tensorflow.keras import layers
from tensorflow import keras
cs

우선 model 시작을 위해 keras를 불러오고, layer를 쌓기 위해 layers를 import 합니다.

 

다음으로 계산 모델을 쉽게 관리하기 위해 클래스로 만들어 관리하였습니다.

1
2
3
4
5
6
7
8
def build_model(self):
    n_numbers = 2
 
    model = keras.Sequential()
    model.add(layers.LSTM(6, input_shape=(n_numbers, 1), return_sequences=True))
    model.add(layers.LSTM(6))
    model.add(layers.Dense(1))
    model.compile(loss='mean_squared_error', optimizer='adam')
cs

4번 줄을 시작으로 sequential을 생성하고

5,6,7번 줄과 같이 층을 add 합니다.

5번 줄의 인풋 사이즈가 2,1 인 이유는 우리가 알다시피 사칙연산할 때 숫자 두 개를 가지고 연산을 하기 때문에 2입니다!

6번 줄을 거쳐 7번 줄에선 아웃풋으로 1개를 뽑아줍니다. 이유는 당연하게도 사칙연산 후 결과는 한 개이기 때문입니다.

8번 줄을 통해 loss function과 optimizer을 정해줍니다. 로스로 mse를 선택했고 optim으로 adam을 정하였습니다.

 

1
return model
cs

모델을 리턴함으로써 학습을 진행할 수 있습니다.

 

 하지만 모델만 있다고 해서 학습이 되는 것은 아니고, 학습에 필요한 데이터가 필요합니다.

 우리가 필요한 데이터란 사칙연산에서 사용되는 입력 1, 입력 2, 기호, 결과 총 4개 컬럼이고, 보통 사칙연산 관련 데이터는  공유되어있는 게 없을 수밖에 없고, 따라서 직접 생성하는 게 더 빠르기 때문에 데이터를 생성하는 클래스를 다음과 같이 작성해보았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import random
import numpy as np
 
class GetData:
    def __init__(self, sign):
        random.seed(1015)
        
        self.sign = sign
        self.res = list()
        self.label = list()
 
 
    def get_data(self):
        if self.sign == "+":
            for i in range(100):
                n1 = random.randrange(1100)
                n2 = random.randrange(1100)
                self.res.append([n1, n2])
                self.label.append(n1+n2)
            x, y = np.array(self.res), np.array(self.label)
            self.x = x.astype('float'/ float(100*2)
            self.y = y.astype('float'/ float(100*2)
            
            self.invert = lambda x : round(x*float(100*2))
 
            return self.x, self.y
cs

 사칙연산인 +, -, x, / 총 4개에 대하여 구해야 하기 때문에 클래스를 만들어 각 케이스마다 부가적으로 구현하였지만 설명은 + 메소드 하나만 해도 다른 기호들은 계산방식만 조금 다르기 때문에 +로 진행하겠습니다.

 

16.  n1 = random.randrange(1100)

17.  n2 = random.randrange(1100)

 16, 17라인에서 볼 수 있듯, 두 개의 무작위 수를 고르고 연산 결과까지 계산하여 데이터를 만들고 결과적으로 학습할 데이터와 레이블 두 묶음을 리턴해줍니다.

 

21.  self.x = x.astype('float'/ float(100*2)

22.  self.y = y.astype('float'/ float(100*2)

 21,22 라인의 의미는 학습을 진행할 때 정규화를 진행해주는 것과 의미가 동일합니다. 정확한 정규화 과정은 아니지만 어느 정도 데이터의 크기를 줄여 예측을 하는 데 있어 이점을 불러오게 됩니다.

 

24.  self.invert = lambda x : round(x*float(100*2))

 따라서 학습 완료 후 평가를 하기 위해 실제 값에 대하여 연산을 진행해야 하기 때문에 24라인의 self.invert 함수를 통해 원래 값으로 복원 후 mse를 계산하게 됩니다.

 

나머지 -,*,/ 기호에 관한 함수는 직접 만들어봐도 좋을 것 같습니다.

급하신 분은 아래 코드를 사용하시면 됩니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def get_data(self):
        if self.sign == "+":
            for i in range(100):
                n1 = random.randrange(1100)
                n2 = random.randrange(1100)
                self.res.append([n1, n2])
                self.label.append(n1+n2)
            x, y = np.array(self.res), np.array(self.label)
            self.x = x.astype('float'/ float(100*2)
            self.y = y.astype('float'/ float(100*2)
            
            self.invert = lambda x : round(x*float(100*2))
 
            return self.x, self.y
        elif self.sign == "-":
            for i in range(100):
                n1 = random.randrange(1100)
                n2 = random.randrange(1100)
                self.res.append([n1, n2])
                self.label.append(n1-n2)
            x, y = np.array(self.res), np.array(self.label)
            self.x = (x.astype('float')+float(100)) / float(100*2)
            self.y = (y.astype('float')+float(100)) / float(100*2)
            
            self.invert = lambda x : round(x*float(100*2)-float(100))
 
            return self.x, self.y
        elif self.sign == "/":
            for i in range(100):
                n1 = random.randrange(1100)
                n2 = random.randrange(1100)
                self.res.append([n1, n2])
                self.label.append(n1/n2)
            x, y = np.array(self.res), np.array(self.label)
            self.x = (x.astype('float')*float(100)) / float(100*2)
            self.y = (y.astype('float')*float(100)) / float(100*2)
 
            self.invert = lambda x : (x*float(100*2/ float(100)) # round x
 
            return self.x, self.y
        elif self.sign == "*":
            for i in range(100):
                n1 = random.randrange(1100)
                n2 = random.randrange(1100)
                self.res.append([n1, n2])
                self.label.append(n1*n2)
            x, y = np.array(self.res), np.array(self.label)
            self.x = (x.astype('float')/float(100)) / float(100*2)
            self.y = (y.astype('float')/float(100)) / float(100*2)
 
            self.invert = lambda x : round(x*float(100*2* float(100))
 
            return self.x, self.y
        else:
            import sys
            sys.exit(0)
cs

 

이제 데이터, 모델이 다 완성됐으니 학습하는 코드를 작성해보겠습니다.

1
2
3
4
from calc.calc_model import Calc
import random
import numpy as np
from calc.get_data import GetData
cs

먼저 모델과 데이터 불러오는 코드를 calc 폴더 안에 넣었기 때문에 1,4라인과 같이 작성하였습니다.

2번 라인은 왜 들어있는지 모르겠네요 필요 없습니다!

 

 

다음으로 모델을 불러오겠습니다.

1
2
3
4
5
num_epochs = 1000
 
model = Calc()
model = model.build_model()
#model.summary()
cs

학습시킬 에폭을 정하고, 

모델을 불러온 뒤, build_model 메소드를 통해 모델을 생성합니다.

5번 라인을 통해 모델의 구조를 확인해볼 수도 있습니다.

 

학습 코드는 생각 외로 너무 단순합니다. 데이터 불러오고, 학습시키고.

코드로 보여드리겠습니다~

1
2
3
4
5
6
7
8
for _ in range(num_epochs):
    customData = GetData(calc_sign)
    x, y = customData.get_data()
    x = x.reshape(10021)
 
    model.fit(x, y, epochs=30, batch_size=32, verbose=2)
 
print("Train Fin!")
cs

정말 짧게 학습 코드가 짜여지는 것을 볼 수 있습니다.

몇 에폭을 돌지 반복문 설정 후, 데이터를 생성하여 shape만 모델에 맞게 바꾼 뒤, 바로 모델에 넣어 학습을 진행합니다.

6번 라인의 model.fit의 인자로 epochs가 30인 이유는 해당 제일 바깥에 있는 반복문을 한 번씩 돌 때마다 새로운 데이터를 생성하여 학습을 진행하기 때문에 그 새로운 데이터를 한 번씩만 학습시키기엔 완전하지 않아서 30번 정도로 설정하였습니다.

 

학습결과를 평가하는 코드를 보여드리겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
testData = GetData(calc_sign)
test_data, test_label = testData.get_data()
test_data = test_data.reshape(100,2,1)
 
res = model.predict(test_data)
 
exp = [testData.invert(x) for x in test_label]
pred = [testData.invert(x) for x in res[:,0]]
 
from math import sqrt
rmse = sqrt(mean_squared_error(exp, pred))
print("RMSE :", rmse)
 
err_sum = 0
for i in range(len(exp)):
    err = exp[i] - pred[i]
    print("Real : {}, predicted : {}, err={}".format(exp[i], pred[i], (err)))
    err_sum += err
print("aver Err : {}".format(err_sum/len(exp)))
cs

테스트 데이터를 받아와 5번 라인과 같이 학습시킨 모델에 predict를 진행하여 결과를 담습니다.

7,8번 라인의 의미는 데이터 생성 메소드 설명할 때 작성해 놓았듯, mse계산을 위해 정규화 비스무리 처리해둔 것을 다시 원상복구 해놓는 것입니다.

15~18라인을 통해 모든 값의 에러를 측정하여 더한 뒤 결과로 내놓게 되면 다음과 같이 나옵니다!

 

많은 epoch으로 인해 오버피팅이 된것일까요?

 gpu를 통해 학습하면 금방 진행되는 것 같습니다. 제 결과를 보면 평균 에러가 0으로 나오네요 ㅋㅋ 보통 조금 차이 날 법도 한데 에러가 0으로 나왔습니다!

 학습 데이터와 테스트 데이터를 나누어 진행하였다 하더라도 데이터의 범위가 0부터 100 사이의 값이라 거의 모든 경우를 학습하여 오버 피팅이 된 것 같습니다.

 

모델을 저장해 나중에 사용하고 싶다면(그럴 일은 절대 없겠지만)

1
2
dic = {"+":"plus""-":"minus""*":"multiply""/":"divide"}
model.save('.pretrained/' + dic[calc_sign]+'_model.h5')
cs

위와 같이 작성해주시면 폴더 생성 후 모델을 세이브해두게 됩니다!

 

 곱셈 나눗셈도 진행해보면 재밌을 것 같네요!

곱셈 학습결과

 포스팅 마치고 곱셈도 돌려보았는데 엄청난 에러를 보이진 않네요. 어느정도 비슷한 값까진 가져오는 것을 볼 수 있습니다. 에러가 0인 결과도 몇몇 있긴 하네요.

 그래도 사칙연산하는 모델인데 정확도가 중요할 수도 있겠지만 여기서 정확도를 따지려면 그냥 계산기 돌리는게 낫겠죠? ㅋㅋ

 아무튼 재밌는 시간이었습니다!!

감사합니다!!

+ Recent posts