*필자는 자연어 처리 전문가가 아니라 말하는 감자입니다. 틀린 내용이 있다면 너그러히 이해하고 알려주세요.
반갑읍니다. 꼬맨틀 사골을 우리는 것도 이번 글로 마지막입니다. 사족으로 전처리 과정을 직접 돌려보신 분들은 아마도 '고작 꼬맨틀 하나 풀자고 이런짓까지 해야하나...?' 라는 생각이 드실텐데 저도 그랬읍니다. 긴 글 읽기는 귀찮고 결과 코드만 보고 싶은 분들을 위해서 코드만 따로 올려둡니다.
* 소스코드 다운로드에서 zip 파일을 다운, 압축 해제 하시고 google colab 환경 혹은 local 환경에서 실행시켜 보시면 사용하실 수 있을 것 같습니다. 자잘한 사용법 및 오류는 귀찮으니 다루지 않으려는데 혹시 안되시면 댓글 남겨주시면 시간이 나면 답변하겠습니다.
본 글에서 다루는 코드는 제 github repo의 'test.ipynb' 파일입니다.
import pickle
import word2vec
import numpy as np
with open('data/valid_nearest.pkl', 'rb') as f:
words = pickle.load(f)
f = open('./data/secrets.txt', encoding='utf-8')
secs = f.readlines()
f.close()
sec_vec = ([], [])
for w in secs:
sec_vec[0].append(w.strip())
sec_vec[1].append(words[1][words[0].index(w.strip())])
위 코드에서는 전 게시물에서 정리해둔 단어-벡터 자료인 'valid_nearest.pkl'을 가지고 와서 words라는 변수에 저장해뒀습니다. 한 번 데이터의 shape을 찍어보면 59118개의 단어가 0번 Index에 존재하고, 1번 index에는 59118개의 300 차원 벡터, 즉 59118*300 형태의 matrix가 들어있는 것을 확인할 수 있습니다.
그 아래에서는 './data/secret.txt'은 정답으로 출제될 법한 단어들을 모아둔 텍스트 파일입니다. 꼬맨틀 제작자 분들도 '두 글자 이상의 단어만 출제되지만 추측을 위해서 한 글자 단어를 입력하는 것은 가능하다'라고 이야기 하는 것을 보면, 전체 단어 집합과 정답으로 출제되는 단어의 집합이 다르다는 것은 알 수 있습니다. 'secret.txt'가 그 정답으로 출제되는 단어들의 집합입니다. 아래 for loop에서는 정답 단어 집합에 속한 단어의 vector들을 앞서 정리한 전체 단어 집합에서 가지고 와서 sec_vec이라는 변수에 pair를 맞추어 저장해둡시다.
in_word = input('Enter a word: ')
sim = float(input("Enter similarity: "))
input_vec = sec_vec[1][sec_vec[0].index(in_word)]
sim_mat = np.ndarray(4650)
for idx, word in enumerate(sec_vec[0]):
sim_mat[idx]=word2vec.cosine_similarity(np.array(sec_vec[1][sec_vec[0].index(word)]), input_vec)*100
gaps = abs(sim_mat-sim)
idxs = np.where(gaps<0.01)[0]
for idx in idxs:
print(sec_vec[0][idx])
이제 본격적으로 정답 맞추기를 해봅시다! 기본적으로 꼬맨틀은 '단어를 입력하면 유사도를 알려준다'는 기본적인 가정이 있습니다. 따라서 저희는 치팅(attack)을 하기 위해서 꼬맨틀 게임이 제공하는 '단어 입력(Query)->유사도 알려줌(Return)'을 치팅의 기본 정보로 활용할 수 있습니다.
2022/12/20의 문제를 예시로 한 번 정답을 맞춰봅시다. 우선 꼬맨틀에 접속해 아무 간단한, 일반적인 단어를 입력합시다. '단어'라는 단어를 입력하니 매우 운이 좋게도 이것이 38.12%의 유사도로 75등에 rank된 단어 임을 알 수 있었습니다. 자 그렇다면, 우리는 다시 코드로 돌아와서 이 '단어'라는 단어와, 유사도 38.12를 프로그램에 입력합니다. 이는 각각 변수 in_word와 sim에 저장됩니다. 저희는 이미 대부분의 단어들에 대해서 임베딩된 vector에 대한 정보를 가지고 있기 때문에, 입력한 단어의 임베딩 vector를 가지고 와서 input_vec에 저장해 둡시다. 이후 정답으로 나올 수 있는 모든 단어들의 vector들과 입력한 단어의 vector 사이의 유사도를 구해 sim_mat이라는 변수에 저장해둡니다. 꼬맨틀에서 사용하는 코사인 유사도를 사용합니다.
이제 저희는 '단어'라는 단어와 정답 집합에 있는 모든 단어들의 유사도를 가지고 있으니, 38.12%만큼 비슷한 단어만 찾아내면 됩니다! 전체 유사도 행렬에서 유사도를 빼주고, 꼬맨틀에서는 소수점 아래 두 번째 자리까지의 정확도로 유사도를 알려주니 반올림을 고려하여 0.015보다 작은 단어들을 찾으면 정답은 반드시 그 중에 있게 됩니다.
실제로 코드를 실행해보니 해당 날짜의 정답인 '의미'가 제대로 찾아진 것을 볼 수 있었습니다.
그러나 첫번째 단어가 이번 처럼 유사도가 높지 않다면 어떻게 될까요? 실제로 유사도가 낮은 단어의 경우 정답일 법한 단어가 여러개가 나올 수 있습니다. 이런 경우에는 한 번 더 단어를 입력하여서 두 개의 조건을 모두 만족시키는 단어를 찾아야 합니다. 많은 단어에 대해서 실험해보지는 않았으나, 단어가 세 개까지 필요한 경우는 없었습니다. 넉넉 잡아 최악의 경우 세번 단어를 입력해보면 정답을 알 수 있다는 것이지요.
그렇다면 한 번도 단어를 입력하지 않고 한 방에 단어를 맞추는 것은 불가능할까요? 가능은 합니다. 바로 당일 알려주는 가장 유사한 단어의 유사도, 그리고 10번째로 유사한 단어의 유사도를 활용하는 것이지요. 해당 방식은 코드를 짜지는 않았지만 구현은 간단하지만 안봐도 런타임이 꽤나 많이 걸릴 것 같아(59118*4650 번 단어의 유사도를 계산하고 정렬해서 가장 높은 유사도와 10번째로 유사한 단어를 찾아야 함) 이번 글에서는 코드를 직접 짜지는 않겠습니다.
1. 정답으로 나올 수 있는 단어 4650개와 전체 단어 59118개 사이의 유사도를 모두 계산합니다.
2. 4950개의 정답 후보 단어들에 대해서 유사도를 모두 내림차순으로 정렬합니다.
3. 가장 높은 유사도를 가지는 단어는 자기자신이니 이를 제외하고 2등을 한 단어의 유사도가 당일 공개된 유사도와 똑같은 단어들을 찾습니다.
4. 사실 a란 단어와 b가 n% 유사하다는 것은 곧 b라는 단어가 a라는 단어와 n% 유사하다는 의미이니, 사실 아무리 운이 좋아도 정답일 수 있는 단어는 2개 이하로 좁히지 못합니다(2등인 단어가 정답 후보군에 없는 경우 바로 정답을 얻을 수는 있지만 특수한 케이스입니다).
5. 그러므로 본인을 제외하고 가장 유사한 단어와의 유사도가 공개된 것과 같은 단어들 중, 10번째(본인을 제외하니 사실은 11번째)로 유사한 단어의 유사도가 비슷한 단어를 찾아내면 현실적으로 하나의 단어만이 남게 될 것입니다.
재미 삼아서 시작한 꼬맨틀인데 과몰입 때문에 소스 코드도 까고 게시글도 세 개나 작성했네요. 굳이 굳이 이렇게 까지 치팅해가면서 문제를 풀 것은 아니지만, 이렇게 시스템을 공격할 수 있다는 재미있는 사실을 배우고 갑니다. 이제 쓸 데 없는 컨텐츠는 그만하고 가서 공부나 하겠습니다. 진짜 끗!
'호에에엥? > Side Project' 카테고리의 다른 글
꼬맨틀로 NLP(자연어 처리) 찍먹하기 (2) - 꼬맨틀 치팅 프로그램 만들기(데이터 전처리 과정) (0) | 2022.12.19 |
---|---|
꼬맨틀로 NLP(자연어 처리) 찍먹하기 (1) - 꼬맨틀의 단어 유사도 측정의 원리와 유사도가 이상한 이유! (2) | 2022.12.09 |