[TensorFlow] 대항해시대 온라인 시세 공유 도우미 (1)

국내에서 서비스하는 대항해시대 온라인의 최악의 시스템은 랜덤 그레이드 옵션이 아닐까 생각합니다. 그런데 어쩌다 보니 글로벌판에서 경험치,스킬숙련도,명성 최대 4배 이벤트를 하는 것을 보고, 글로벌판으로 갈아탔지요. 그리고 foxytrixy 를 이용해 오픈 채팅방에서 시세 공유하는 것을 보고 스크린샷에서 바로 정보를 가져올 수 있으면 좋겠다는 생각에 시작했습니다. 때마침 머신러닝 교육을 갔다와서 어디 적용할 만한 것이 없을까 고민하고 있었고요.

처음에는 게임의 해상도마다 이미지 크기가 조금씩 다를테니 이걸 같은 크기로 리사이즈 하고 이런 데이터들을 모아서 학습을 시키면 모든 해상도에서 정보를 인식할 수 있을꺼라 생각했습니다. 그런데 이 옛날 게임은 해상도가 달라도 "Market Rates" 보는 창의 크기는 동일한 사이즈였습니다.

머신러닝을 적용하는 것이 큰 의미가 없어지긴 했지만, 그래도 반투명한 부분 때문에 충분히 도움이 되지 않을까 생각하면서 진행해 봤습니다.

여기서는 Python + Tensorflow 를 이용해서 제가 데이터를 모으고, 신경망을 구성한 것을 보여드리고자 합니다.

일단 대항해시대 온라인의 Market Rates 화면입니다.

창의 왼쪽에는 교역품 시세, 오른쪽에는 인근 도시의 시세입니다.

일단 왼쪽 교역품 항목에 집중해 봅시다.
화면에서 교역품 정보를 알기 위해서는 그림을 보거나 이름을 보거나 둘 중 하나가 될 텐데요, 저는 이미지를 택했습니다. 웬지 이름 보다는 더 식별을 잘 할 것 같아서 말이죠.

스크린샷에서 교역품 이미지를 확인해 보니, 이 이미지는 배경화면의 영향을 받지 않습니다. 그래도 머신러닝 시작한 김에 계속 진행합니다.

대항해시대 온라인의 교역품 종류는 https://ssjoy.org/ 사이트 기준으로 대략 600 가지가 넘는 것으로 보입니다. 처음에는 이 모든 교역품의 이미지를 스크린샷으로 부터 가져와서 데이터를 만들기는 어려워 사람들이 시세를 많이 확인하는 품목 몇 종류만 하려고 했습니다.

하지만 대항해시대 온라인을 위한 훌륭한 사이트가 또 하나 있으니, 바로 대항해시대 두부(http://uwodb.ivyro.net/)입니다.

(잠시 머신러닝과 관련없는 이야기로 넘어갑니다)
여기에서는 교역품 목록에 교역품 이미지가 같이 적혀 있습니다. 오호... 이걸 이용해 보죠.
이를 위해서는 웹 크롤링에 사용하는 Beautiful Soup를 이용하였습니다.

아래와 같은 소스로 이미지를 다운받아, 교역품 이름으로 변경하여 저장합니다.
Market Rates 스크린샷을 보면 교역품 이미지에 갯수가 적혀 있습니다.
그래서 이 영역을 제외하고, 테두리도 잘라내고 Cropping 하여 저장합니다.

소스파일

(다시 머신러닝 이야기로 돌아와서)
자, Input과 Output Data를 생각해 보겠습니다.
Cropping된 교역품 이미지의 크기는 42 x 24 사이즈고 RGB 모드로 저장되어 있습니다. 그래서 처음 Input은 42 * 24 * 3 = 3024 크기의 1차원 배열로 만들고, shape 를 변경해 주었습니다.
Output은 각각의 교역품을 알아내야 하므로, 대략 600종 이상의 교역품을 다 담을 수 있는 2의 지수승 중 하나 택했습니다.
keep_prob 는 Dropout을 위한 holder입니다.

------------
# files : 이미지 파일 리스트
# output : 저장할 세션 이름
def run_machine_learning(files, output):
    im = Image.open(files[0])

    X = tf.placeholder(tf.float32, [None, len(im.tobytes())])
    X_shaped = tf.reshape(X, [-1, 42, 24, 3])

    category_count = 1024
    Y = tf.placeholder(tf.float32, [None, category_count])

    keep_prob = tf.placeholder(tf.float32)

------------

그리고 신경망을 구성하였습니다.

-----------
    # Convolutional Layer1 : 2x2의 필터사이즈와 3개의 채널, 16개의 필터를 지정
    # 자주 보는 손글씨 예제와는 달리, 이 이미지는 RGB 3개의 컬러채널을 가지고 있으므로, shape의 3번째 값이 1이 아닌 3을 넣습니다.
    # Pool Layer1 에서는 2x2 Max Pool 적용
    W1 = tf.Variable(tf.random_normal([2, 2, 3, 16], stddev=0.01))
    L1 = tf.nn.conv2d(X_shaped, W1, strides=[1, 1, 1, 1], padding='SAME')
    L1 = tf.nn.relu(L1)
    L1 = tf.nn.max_pool(L1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # Convolutional Layer2 : 2x2의 필터사이즈와 16개의 채널(Layer1 을 거치고 나온 결과), 32개의 필터를 지정
    # Pool Layer2 : 3x3 Max Pool 적용. 원래 이미지가 42x24 -> 21x12 가 되었는데, 이건 2로 나눠지지 않아서, 3으로 나누도록 함.
    W2 = tf.Variable(tf.random_normal([2, 2, 16, 32], stddev=0.01))
    L2 = tf.nn.conv2d(L1, W2, strides=[1, 1, 1, 1], padding='SAME')
    L2 = tf.nn.relu(L2)
    L2 = tf.nn.max_pool(L2, ksize=[1, 3, 3, 1], strides=[1, 3, 3, 1], padding='SAME')

    # Full Connected Layer : Output 의 종류가 1024 이므로 그것보다 더 적당하다고 생각하는 값을 선택
    # 또한 Output 은 1차원 배열이 되어야 하므로, 1차원 배열로 reshape
    # 여기에서 Dropout 도 적용
    W3 = tf.Variable(tf.random_normal([7 * 4 * 32, 4096], stddev=0.01))
    L3 = tf.reshape(L2, [-1, 7 * 4 * 32])
    L3 = tf.matmul(L3, W3)
    L3 = tf.nn.relu(L3)
    L3 = tf.nn.dropout(L3, keep_prob)

    # 4096 개의 뉴런을 Output 크기로 변환
    W4 = tf.Variable(tf.random_normal([4096, category_count], stddev=0.01))
    model = tf.matmul(L3, W4)

    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=model, labels=Y))
    optimizer = tf.train.AdamOptimizer(0.001).minimize(cost)

-----------

이제 학습을 시켜봅니다.
x_data 의 포맷은 X 의 shape 에 맞도록 맞춰야 하고,
y_data 의 포맷은 Y 의 shape 에 맞도록 해야 합니다.

-----------
    init = tf.global_variables_initializer()
    sess = tf.Session()
    sess.run(init)

    for epoch in range(150):
        total_cost = 0

        x_data = []
        y_data = []
        for i in range(len(files)):
            im = Image.open(files[i])
            raw_bytes = im.tobytes()
            x_data += [[float(x) for x in raw_bytes]]
            y_data += [create_one_hot_format(category_count, i)]

        _, cost_val = sess.run([optimizer, cost],
                            feed_dict={X: x_data,
                                       Y: y_data,
                                       keep_prob: 0.95})
        total_cost += cost_val

        print('Epoch:', '%04d' % (epoch + 1),
            'Avg. cost =', '{:.3f}'.format(cost_val))
        if cost_val < 0.001:
            break

    print('최적화 완료!')

-----------

정형화된 이미지라서 별도의 테스트용 데이터는 없습니다.

-----------
    is_correct = tf.equal(tf.argmax(model, 1), tf.argmax(Y, 1))
    accuracy = tf.reduce_mean(tf.cast(is_correct, tf.float32))
    sum = 0
    fail_data = []
    for i in range(len(files)):
        im = Image.open(files[i])
        raw_bytes = im.tobytes()
        x_data = [[float(x) for x in raw_bytes]]
        y_data = [create_one_hot_format(category_count, i)]
        result = sess.run(accuracy,
                        feed_dict = {X: x_data, Y: y_data, keep_prob: 1})
        if result == 0.0:
            input_image = files[i]
            determined_image = get_name_from_one_hot_format(files, y_data)
            fail_data.append([input_image, determined_image])
            #print("input, result =", input_image, determined_image)
        sum += result

    saver = tf.train.Saver()
    saver.save(sess, output)

    print('정확도:', sum / len(files))
    if sum / len(files) < 1.0:
        fail_image = Image.new("RGB", (84 + 1, 24 * len(fail_data) + 1))
        for j in range(len(fail_data)):
            print(fail_data[j][0], fail_data[j][1])
            fail_image.paste(Image.open(fail_data[j][0]), (0, j * 24))
            fail_image.paste(Image.open(fail_data[j][1]), (42, j * 24))
        fail_image.show()
        fail_image.save("./fail_image.png")

-----------

두부 사이트에서 받은 데이터를 그대로 이용해서 결과를 보면, 원하는 만큼의 정확도가 나오진 않습니다. (개인적으로 원하는 만큼의 정확도는 100% 입니다만...)

그래서 어떤 데이터를 제대로 판단하지 못하는가, 확인 해 보았습니다.
두부 사이트에서는 이미지를 대온 클라이언트로 부터 직접 추출해서 그런지 교역품 정보가 틀린 것이 있습니다. 어쨌든 같은 이미지가 있어서 좀 문제가 될 것 같아 삭제합니다.

Deer Antlers.png 와 Pepper.png 가 같군요. Deear Antlers 라는 교역품은 없으므로 삭제.
Aconite.png 와 Belladonna.png 도 같습니다. Aconite 라는 교역품도 없으므로 삭제.
Black Alchemy Ointment.png 와 White Alchemy Ointment.png 는 Cropping된 이미지에서는 구분이 안되네요. 실제 전체 이미지는 다릅니다. 교역품 시세 정보를 확인하는 목적에서는 필요없을 듯 하여 둘 다 삭제.

잘못 판단한 교역품 중, Sichuan Pepper_1.png 라는 이름이 있습니다.
파일을 보면 Sichuan Pepper.png, Sichuan Pepper_1.png 2개가 있는데, 사이트에 같은 이름으로 다른 이미지가 등록되어 있었습니다.
결론부터 말하자면 Sichuan Pepper_1.png 은 잘못된 이미지입니다.
 (Sichuan Pepper.png)

 (Sichuan Pepper_1.png)

(실제 산초 사진을 보시죠. 물론 게임에서도 산초에 해당 하는 이미지가 Sichuan Pepper_1.png 이 아닙니다만, 사진으로 봐도 구분을 할 수 있을 것 같습니다. -_-)

자, 이제 정리한 데이터를 가지고 다시 학습을 시켜 봅니다.

----
Epoch: 0148 Avg. cost = 0.069
Epoch: 0149 Avg. cost = 0.071
Epoch: 0150 Avg. cost = 0.069
최적화 완료!
정확도: 0.9767441860465116

-----


여전히 만족스럽진 않습니다. 잘 못 판단한 교역품을 다시 한 번 검토해 봅니다.
Bear Gall Bladders, Corn Oll, Dragon Bone, Rhubarb, Sesame Oil 이런 교역품도 없습니다. 삭제해 보겠습니다.

그리고 다시 학습 시작 또 몇가지 항목들은 제대로 인식하지 못하네요. 다시 한번 데이터를 검토해 봅니다.
Liquorice, 감초라는 군요. 이런 교역품도 없습니다. 게다가 몰약이랑 비슷하네요? 삭제.
Ox Gallstones, 우황 - 소의 결적을 말린 것? 이런 교역품도 본 적이 없는거 같아서 삭제.
Sappanwood, 다목 - 이것도 없는 교역품 같네요. 삭제.

그리고 다시 학습 시작.
Ephedra, Nard Oil 도 없는 교역품으로 삭제합니다.

또 다시 학습 시작.

 (마지막 Fail Image)
Belladonna, Salt는 실제 교역품이기는 하지만 거의 이용하지 않을듯 하므로 삭제합니다.

또 학습.
------
Epoch: 0147 Avg. cost = 0.002
Epoch: 0148 Avg. cost = 0.001
Epoch: 0149 Avg. cost = 0.003
Epoch: 0150 Avg. cost = 0.004
최적화 완료!
정확도: 1.0

------

와, 100% 정확도를 가지게 되었습니다.


첫 번째 글을 마치며...

처음에는 교역품 이미지 자체가 정형화된 데이터라서 머신러닝으로도 100% 인식이 잘 될꺼라 예상했습니다만, 의외로 95% 에서 더 올라가지 않았습니다.

CNN 대신 DNN 도 이용해보고(정형화된 이미지라 DNN이 더 나을거라는 생각에), Layer 갯수를 조정하거나 채널을 조정하고, 표준편차와 learning rate 조절도 해 보았습니다만, 인식에 문제를 일으키는 학습 데이터 자체를 정제하는 것이 제일 좋았습니다. 학습데이터를 정제하고 나니 DNN 으로도 100% 정확도가 나왔습니다.

얼마전에 들은 딥러닝 강의의 강사님이 이야기 한 것처럼, 데이터 전처리가 80 ~ 90% 라는 말이 생각나더군요.

하지만 저는 교역품 이미지 인식을 하기 위해, 이 머신러닝을 이용한 학습데이터를 이용하지 않고 이미지 유사도 분석을 통해 구현하려 할 것 같습니다. (더 간단하고 빠를거라 예상되네요)

다음 글은 Market Rates 숫자와 도시 이름 화살표 방향 등을 학습시키고 그 결과를 공유해 보겠습니다.

댓글

이 블로그의 인기 게시물

[게임개발 스토리] 장르/타입/조합 정보와 몇 가지 팁

[윈도우] 실행 중인 프로그램의 타이틀을 변경하는 유틸리티

Synergy 한글키 패치 공식 버전 적용 및 최종 정리