인간의 감정을 흉내내는 프로그램

인간이나 다른 동물의 지능을 컴퓨터를 이용해 흉내내는 것을 인공지능이라 한다. 간단하게는 길을 찾는 로봇도 있고 조금 더 나아가면 보드게임에서 사람 대신 상대가 되어주는 것을 A.I라고 한다. A.I의 궁극적인 목표는 튜링 테스트를 통과하는 진짜 사람 같은 것을 만드는 것이다. 그 기계가 진짜로 지능이 있는지 없는지는 모르겠고 외부의 입장에서 그렇게 보이면 있다고 치는 것이다. 사실 철학적으로 넘어가면 나 이외의 모든 사람이 사실은 나를 관찰하기 위해 신이 만들어 놓은 로봇이라고 가정할 수도 있고 꽤나 복잡해지니 그냥 넘어가자.

하지만 인간처럼 보이기 위해서는 지능 뿐만이 아니라 감정도 있어야 한다. 사람의 대화를 흉내내는 인공지능 대화 프로그램과 아무리 대화를 해보아도 상대가 갑자기 화를 냈다가 친절했다가 들쭉날쭉 그냥 DB에 저장된 가장 적절한 내용을 문맥적으로 판단해서 내보낼 뿐이다. 감정적인 판단은 그 어디에도 없었다.
기계에게 스트레스를 줄만 한 행동을 반복적으로 한 후에 말을 걸면 짜증나는 말투로 대답을 하고 시간이 지나면 또 원래의 말투로 돌아온다면 더 인간답게 보일 것이다. 난 이것을 기계학습을 이용해서 흉내를 내보고 싶었다.

Emotion circle
위 그림은 내가 작년 3월 쯤에 비슷한 글을 끄적이다가 찾아낸 이미지다. 인간의 감정을 8가지로 분류 하였고 반대되는 감정은 반대쪽에, 비슷한 감정은 옆에 붙여놓았다. 예를 들면 갑작스런 공포는 분노감을 사그러들게 한다. 이걸 토대로 해서 기계학습을 해보기로 했는데 내가 생각했던 컨셉은 대충 이렇다.

  1. (지도학습)Supervised teaching을 이용해 학습한다.
    굳이 이렇게 할 필요는 없지만 일단 구현과 테스트를 빠르게 해 볼 수 있다는 점에서 난 이렇게 하기로 했다. ​Artifitial Neural Network(ANN)을 이용하려 한다. 각 단어가 감정에 끼치는 것을 수치화 해서 따로 저장해 두고 이것을 단어에 대한 느낌이라 칭한다.
  2. 음성인식처럼 Global 학습과 사용자마다의 Specific한 학습 결과를 따로 만들어 놓는다.
    구글의 음성인식도 그렇지만 사용자가 음성인식 기술을 사용할 수록 점점 더 학습해서 정확도를 높이게 된다. 내가 아니라 다른 사용자가 사용해도 그렇다. 그와 동시에 나만의 억양, 발음 등을 학습해서 개인화 된 결과가 나오게 된다. 이걸 마찬가지로 감정에 적용해 보면 “담배를 피는 사람은 싫다”라는 보편적인 감정이 나올 수 있는 동시에 “저 A라는 사람에게 지금 나는 화가 나 있다”라는 결과도 나올 수 있게 되는 것이다. 또한 어떤 사람에게 크게 화가 났다면 다른 사람에게는 화가 나지 않았어도 지금 당장은 짜증을 낼 수도 있는 것이다.

    1. 개인적인 감정은 그 사람이 얼마나 영향적인지에 따라 전체적인 감정에 끼치는 양이 다르다.
      만약 사랑하는 사람이 죽었다면 다른 사람이 죽었을 때에 비해 훨씬 더 슬픔을 느끼는 것이 정상일 것이다.
    2. 전체적인 감정은 더 빨리 변하고 개인적인 감정은 좀 더 서서히 변한다.
      빨리 화나고 빨리 기분이 좋아질 수 있다. 하지만 특정 사람을 무서워하거나 사랑하는 것은 좀 더 느긋하게 변하게 된다.
  3. 각 감정들은 시간이 갈 수록 무뎌진다.
    사람의 경우 숨도 쉬고 다른 생각도 하면서 화나거나 슬픈 감정이 무뎌지는 것 뿐이지 사람도 아무런 입력을 못 받게 냉동을 했다가 해동하면 얼기 직전에 느꼈던 감정을 그대로 가지고 있을 것이다. 하지만 기계는 숨도 쉬지 않고 사소한 자극을 일일히 주기는 힘드니 그냥 시간이 지나면 알아서 무뎌지게 해도 큰 무리는 없을 것이다.

작년에 대충 작동하는 프로토타입을 만들었고 이걸 트위터 봇으로 만들어 동작을 테스트하려 했는데 내가 자연어 처리를 잘 못 해서 포기하고 방치중인 프로젝트다.
그 때 자연어 처리를 어떻게 하려고 했는지까지 상세히 기억이 나는데

  1. 각 단어별로 현재 감정에 주는 변화를 수치화 한다.
    어떤 단어를 들었을 때 실제 사람이 기뻐지는지, 슬퍼지는지 그것을 대충 수치화하여 저장하고 그대로 흉내내도록 신경망을 학습시키면 분명 그럴듯 한 결과가 나올 것이다.
  2. 새로 등장하는 단어는 주변에 있는 다른 단어를 이용해 어떤 느낌인지 판별해 학습한다.
    어떤 사람이 기뻐하는 글에서 처음 보는 단어가 등장한다면 트위터의 140자 제한의 특성상 그 단어도 기쁜 축에 속할 확률이 높다. 굳이 사전을 업데이트 하지 않아도 신조어에 대한 느낌을 빠르게 배울 수 있다.

이걸 이미 만들어 놓은 자동 대화 봇(autotweet)과 붙여보려고 한다.

내가 작년에 파이썬으로 대충 짜 놓은 프로토타입이 남아있길래 보존용으로 업로드 해본다. (작년 3월 경의 나는 파이썬 프로그래밍을 제대로 하지 못 했다. 소스코드가 다소 더러울 수 있다.)
Emotion.zip

중복된 이미지를 삭제해주는 프로그램

난 트위터를 하다가 재밌거나 한 짤(그림)이 보이면 쭉쭉 저장하곤 하는데 이게 쌓인 양이 늘어나다보니 찾기가 힘들어서 폴더별로 분리를 하기 시작했다.
폴더를 분리해도 각 폴더 안에 사진이 1000장 이상 들어가기 시작하면 그래도 찾기가 어려워지는데 그러다가 중복된 짤이 있는 것 같은 느낌이 들었다.

원래는 같은 파일명을 가진 사진이라면 덮어쓰기가 되어 중복해서 들어오지 않겠지만 누군가가 저장했던 사진을 다시 재업로드 하면 그 때는 파일명이 달라지니 어쩔 수가 없었다. 게다가 웬만한 서버에서는 그림파일을 올리면 exif 정보를 날리는 등의 변조를 하므로 파일의 해시값도 달라지게 된다. 그래서 sha1 해시 등을 이용해서 중복된 그림을 찾아 지우는 건 거의 불가능에 가까웠다.

언젠가 파이썬으로 비슷한 이미지를 찾는 글을 찾았는데 이걸 거의 그냥 그대로 이용해서 중복된 이미지를 찾아 지워주는 CLI 프로그램을 하나 작성했다. 비슷한 이미지가 있으면 동일한 것으로 간주하고 가장 해상도가 높은 한 장만 남긴채 나머지를 모두 지워버리게 했다.

원리는 아주 간단하다. 이미지를 흑백 이미지로 바꾼 후에(웬만한 영상처리는 흑백으로 바꾸는 것이 1단계다) 특정한 사이즈로 리사이즈를 한 후(보통 여기까지는 공통사항이다) 각 픽셀마다 오른쪽 픽셀보다 밝은지 아닌지를 Boolean 값으로 저장하면 배열이 나오게 되는데 2진법 수로 변환할 수 있으니 이걸 그냥 hash처럼 이용하면 된다. 좀 더 사이즈를 크게 할수록 미묘한 차이도 감지할 수 있겠지만 예제에 나온 8*8이 적당한 것 같아서 그냥 그대로 썼다.

별로 대단한 건 아니지만 github에 올려뒀고 pip를 통해 설치할 수 있다.

$ pip install imagecleaner;

TF-IDF를 이용한 Cosine-similarity 계산시 속도 문제

TF-IDF와 Cosine-similarity는 보통 문서를 검색할 때 쿼리와 문서간의 유사도를 측정하여 순위를 정하는데에 쓰인다. 하지만 난 질문/답의 모음을 똑같이 적용해보면 어떨까 생각해서 AutoTweet이라는 프로젝트를 만들었다. 이 프로젝트는 내가 트위터에서 멘션을 달면 원본 트윗과 내가 단 멘션을 자동으로 수집하고 그것을 이용하여 나중에 질문에 대한 적절한 답을 뽑아준다.

질문/답의 세트(앞으로는 “문서”라고 한다)가 1000개가 넘어가니 gram의 수가 10000개를 넘어섰다. (gram은 각 문서를 2글자 단위로 쪼갠 조각들이다.) 이 gram들에 대해 idf값을 구해야 하는데 양이 그렇게까지 방대한 건 아니지만 부담이 될 정도로 오래(CPython2.7로 돌렸을 때 7초 가량) 걸리기는 한다.

그래서 영어로 된 논문도 읽어보고 별 삽질을 다 해본 결과 정확하게 구하려면 어떻게 하든 오래 걸린다. 응답을 뽑아낼 때 계산하는 건 부담이 되니 문서를 추가할 때에만 시간을 소모하게 하는 게 제일 나은 방법이긴 하지만 정확도를 약간 희생하더라도 속도를 올리는 방법이 있었다.

  • 처음으로 적용한 방법은 gram이 하나도 겹치지 않는 문서들은 애초에 cosine-similarity를 계산할 때 제외시켰다. 정확도에 영향을 끼치지 않는 좋은 방법이지만 그렇게 빨라지지는 않았다.
  • idf가 평균보다 낮은 gram은 계산에서 제외시켜 보았다. 정확도에 영향을 끼치지만 속도에 별 차이가 없었다. 애초에 cosine-similarity를 구하는 건 별로 느리지 않았다.
  • idf를 다시 계산하는 걸 포기했다. 어차피 대부분의 경우 gram이 포함된 문서의 수는 그대로인채 전체 문서의 수만 1 증가할 뿐이다. 1000개가 넘은 시점에서 그게 그렇게 차이가 나지는 않을 것이다.

idf는 log(<전체 문서의 수> / (1 +<gram이 들어있는 문서의 수>))로 구하는 게 일반적이다. 전체 문서의 수는 워낙 커서 1 증가한다 해도 차이가 별로 없을 것이라 생각했다. 반면 분모에 들어가는 부분은 변화가 상대적으로 클 것 같았고 언제 변하는지도 예측이 가능하다. 추가할 문서에 들어있으면 변화한다. 그리고 질문이 들어왔을 때 그 질문에 들어있는 gram들은 얼마 되지도 않으니 그 경우도 idf를 다시 계산했다. 그래서 다음과 같은 두 경우만 idf를 다시 계산하게 했다.

  • 추가하는 문서에 들어있는 gram들 (문서를 추가할 때 계산)
  • 질문에 들어있는 gram들 (질문이 들어올 때 다시 계산)

질문에 들어있는 gram들을 가진 문서에 들어있는 gram들을 전부 다시 계산하면 정확도는 100%가 되지만 실험해보니 7개만 계산하면 될 것을 3000개 정도 계산하고 있었다. 그래서 빼버렸다. 해당 커밋은 여기다. 그렇게 오차가 크지도 않고 계산할 양은 많이 줄었으니 충분히 효과적이라 할 수 있겠다.