지금 다니고 있는 회사에 MLOps Engineer 포지션으로 8월 9일에 입사하여 11월 19일에 수습기간 평가를 마치고 정직원이 되었다.

 

비전공자로 프로그래밍을 배우기 시작하여 정확히 1년 3개월 만에 취업에 성공하였다.

목표였던 올해 안에 취업하기도 달성해서 너무 좋다.

 

사실 적어도 2년 정도는 생각하고 있었고, 가능하면 올해 취업하고 싶었기 때문에 열심히 한 결과와 운이 꽤 따라줬던 것 같다.

 

42서울을 통해서 CS 기초지식과 문제를 해결하는 방법, 동료들과 함께 하는 방법을 배웠고, 아이펠을 통하여 배운 머신러닝/딥러닝에 대한 지식을 내가 하고자 하는 직무에 잘 녹여냈던 것 같다.

 

처음 합격 연락을 받고 입사하기 전에는 아무것도 모르는 내가 과연 회사에서 개발자로 일을 할 수 있을까? 라는 고민을 많이 했었다. 다행히 회사도 역시 사람이 사는 곳이었다. 오히려 처음 회사 코드를 봤을 땐, 이런 코드로도 회사가 돌아가는 구나 라는 생각이 들 정도였다. 물론 나는 삽질과 팔짱끼고 모니터만 쳐다보고 있는 매일이지만, 지난 달의 내가 해내지 못한 걸 이번 달의 나는 해내고 있는 것을 보면 내가 성장하고 있다는 것을 느낄 수 있다.

 

3개월간 일하면서 느낀 것은 일단 이 일이 내 적성에 잘 맞는다는 것이고, IT 기업이라고 해서 꼭 코드로만 문제를 해결할 필요는 없다는 것을 느꼈고, 사람 간의 의사소통이 중요하다고 느꼈다.

 

실제로 내가 짠 코드는 그렇게 많지 않을 것이다. 오히려 코드를 짜는 시간보다 같이 일하는 사람과 대화하고, 문제를 해결하는 방법에 대해 논의하는 시간이 더 많았을 것이다. 영업직을 했던 경험 덕분인지, 내가 생각했을 땐 의사소통에 큰 문제는 없었다고 생각한다.

 

3개월의 수습기간을 마치고 받은 평가에서는 대체로 긍정적으로 평가를 받아 무사히 정직원이 될 수 있었다. 평가 중 기억나는 평가가 있는데, 나를 처음 봤을 땐 남들의 이야기를 듣기 보다 내 의견을 더 피력하는데 주력하는 사람이었으나, 지금은 그런 부분이 빠지고 있어서 점점 더 협업하기 좋은 사람이 되어가고 있다는 평가였다. 수습기간 동안 내가 잘 하고 있는 게 맞나 싶었는데 다행히 잘 하고 있었던 것 같다.

 

지금 일하고있는 회사는 분위기나 문화, 같이 일하는 사람들이 너무 좋아서 지금 상태가 계속 유지된다면 회사가 계속 성장하여 상장하는 날 까지 계속 일해보고 싶다. 솔직히 회사 아이템에 대한 부분은 아직 잘 모르겠지만, 모든 스타트업이 다 그런거겠지 생각하고 재미라도 있으니 아무렴 뭐 어때.

 

갑자기 생각나서 두서 없이 적은 글이라 나중에 보면 무슨 말을 써놨는지도 모를 것 같다.

 

잠이나 자야지

'잡담' 카테고리의 다른 글

2021년 회고  (0) 2022.01.01
1년간의 회고  (0) 2021.05.20
개발자로서의 첫 이력서 작성기  (0) 2021.03.07

설치 환경

  • Windows 10 WSL2
    • Ubuntu : 20.04 LTS
    • Docker : v20.10.6
    • kuberenetes, kubectl : v1.19.7
    • Kubeflow : kfctl_k8s_istio.v1.0.0.yaml
    • kfctl: v1.2.0-0-gbc038f9
  • 단일 사용자가 로컬에서만 사용할 경우 : kfctl_k8s_istio
  • 여러 사용자가 사용해야 해서 인증이 필요한 경우 : kfctl_istio_dex

(istio_dex를 사용할 경우) kubernetes 설정 수정

Kubeflow를 설치하기 위해서는 kubernetes API server를 수정해야 하는데, Docker Desktop을 통해 kubernetes를 사용하고 있기 때문에 설정 파일이 로컬에 존재하지 않는다.

$ cat /etc/kubernetes/manifests/kube-apiserver.yaml cat: /etc/kubernetes/manifests/kube-apiserver.yaml: No such file or directory

따라서, 다음 방법을 이용하여 API server를 수정한다.

# Edit kube-apiserver.yaml in docker-desktop 
# docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh 
# vi /var/lib/kubeadm/manifests/kube-apiserver.yaml 
# ADD FOLLLOWING: spec.containers.command 
# - --service-account-signing-key-file=/run/config/pki/sa.key 
# - --service-account-issuer=kubernetes.default.svc 
# - --feature-gates=TokenRequest=true # - --feature-gates=TokenRequestProjection=true

sa.key 파일이 해당 위치에 없을 수도 있으므로 /etc/kubernetes/pki/sa.key 등에 있다면 해당 경로에 맞춰 내용을 수정한다.

kfctl 설치

wget https://github.com/kubeflow/kfctl/releases/download/v1.2.0/kfctl_v1.2.0-0-gbc038f9_linux.tar.gz
tar -xvf kfctl_v1.2.0-0-gbc038f9_linux.tar.gz
sudo mv kfctl /usr/local/bin/
rm kfctl_v1.2.0-0-gbc038f9_linux.tar.gz
kfctl version # kfctl_v1.2.0-0-gbc038f9

/usr/local/bin 으로 이동시키지 않을 경우 $PATH에 추가해서 사용할 수도 있다.

Kubeflow 설치

  • k8s_istio
wget https://raw.githubusercontent.com/kubeflow/manifests/v1.2-branch/kfdef/kfctl_k8s_istio.v1.2.0.yaml
kfctl apply -f kfctl_k8s_istio.v1.0.0.yaml -V
  • istio_dex
wget https://raw.githubusercontent.com/kubeflow/manifests/v1.2-branch/kfdef/kfctl_istio_dex.v1.2.0.yaml
kfctl apply -f kfctl_istio_dex.v1.0.0.yaml -V

설치가 완료되면 port-forward 명령어로 8080 포트로 접속할 수 있도록 한다.

kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80

istio_dex의 경우 초기 아이디와 비밀번호는 admin@kubeflow.org / 12341234 이다.

유저를 추가하는 방법에 대해서는 document에 나와있다.

localhost:8080 으로 접속하여 Kubeflow Dashboard에 접속했다면 설치 완료!

설치 완료 후 초기 컨테이너 image를 pull하고 running 하느라 시간이 다소 걸릴 수 있다. kubectl get pods -n kubeflow 에서 모든 pod 가 생성되어 동작할 때 까지 기다린다.

  • 1.2.0 버전에서 pipeline으로 바로 전송 시 에러가 발생하여 1.0.0 버전을 설치하였다.

 

첫 순위권!

  이번에 아이펠 진행하면서 EDA 하는 방법에 대해 조금 더 자세히 학습했었기 때문에 배운 걸 이용해서 데이콘에 참가해봤다. 처음엔 아무런 EDA도 하지 않은 데이터에 RandomForest로 baseline을 잡고 시작했는데 생각보다 좋은 점수가 나온 것 같다. 모델은 당연히 lightgbm을 사용하였고 이전에 kaggle 대회 연습하면서 optuna를 사용해봤기 때문에 이번에도 역시 optuna를 이용하여 hyperparamter를 tuning 하였다.

  점수를 더 올리려면 train data가 더 필요하거나 feature engineering을 해야 할 것 같은데 아직 많이 경험해보질 않아서 어떤 feature를 조합해야 할지 감이 잘 오질 않는다. 시간이 될 때 kaggle의 다른 비슷한 대회를 참고해서 힌트를 얻어봐야 겠다.

  아직 대회 초반이라 분명히 아래로 밀릴 순위겠지만... 그래도 주말 내내 대회에 매달려서 10등 안에 들어본 것으로 만족한다. 이 기세를 유지해서 상금도 탈 수 있었으면 좋겠다 ㅎ

이전부터 뭐든 실제로 서비스를 만들어보는 것이 중요하다고 생각하여 아이펠을 진행하면서도 틈틈이 프로젝트를 위한 공부를 따로 하고 있었다. 그러다가 GAN을 이용한 노드를 진행했을 때, '이거다!' 하고 느낌이 와서 실제로 프로젝트로 진행해보기로 했다.

우선 기존에 아이펠 노드에서 진행한 모델은 내가 잘못 한건지, 노드가 잘못된건지 학습은 되는데 어딘가 이상한 결과물이 나와서 텐서플로 공식 튜토리얼을 보고 다시 모델을 만들어보았다.

실패한 학습 결과

데이터셋은 kaggle의 Sketch2Pokemon 데이터셋을 학습시켰다. train 이미지가 830장 밖에 되질 않았고, test 이미지도 train 이미지에 있는 중복 이미지여서 데이터가 부족하긴 했지만, 일단 서비스로 배포해보는 것에 의의를 두기로 했다.

프로젝트 진행은 다음과 같이 이루어졌다.

  • 2021-03-28 : 프로젝트 시작
  • 2021-03-28 ~ 2021-03-29 : 첫 번째 모델 학습 및 결과 확인
  • 2021-03-29 ~ 2021-03-30 : 두 번째 모델 학습 및 결과 확인
  • 2021-03-30 ~ 2021-04-01 : 모델 서비스화 진행 중
  • 2021-04-02 : Ainize로 배포 완료

기존과의 차이점이라고 한다면 이미지를 Normalize 해주는 시점이 조금 다르다는 것이다. 아마 이 부분에서 문제가 생겨서 위와 같은 이미지가 학습된 것으로 보인다. 또한, 이미지 데이터를 다룰 때에는 항상 Normalize, Denormalize에 신경써야 하는 것 같다. 어떤 Activation Function을 사용하느냐에 따라 Normalize 범위를 [-1, 1]로 할지, [0, 1]로 할지가 결정되기 떄문이다. 이번에 사용된 모델에서는 tanh를 Activation Function으로 사용했기 때문에 [-1, 1]의 범위에서 Normalize 해주었다.

우선 시작부터 1000 epoch 정도를 돌려보고 시작하기로 했다.

  • 학습 과정을 시각화 해본 결과, 400 epoch 정도 부터 큰 변화가 없는걸로 보아 local minima에 빠졌거나 학습이 종료된 것 같다.
  • 750 epoch 의 checkpoint를 불러와서 기울기를 1/10으로 줄여서 50 epoch 정도 더 학습을 시켜보았으나 큰 변화는 없는 것 같다.
  • 750 epoch 의 checkpoint를 불러와서 기울기를 10배로 늘려서 50 epoch 정도 더 학습을 시켰더니 오히려 원하지 않는 방향으로 학습이 진행되었다.

750 epoch에서의 learning rate 테스트는 자원이 남아서 학습이 종료됐을 때 learning rate를 조절하여 다시 학습시키면 어떻게 될까 궁금해서 진행해봤다.

좌 : Dicriminator loss / 우 : Generator loss
Train set에 있는 이미지 학습 결과

모델을 학습해본 결과, 두 Loss를 비교해봤을 때, disc_loss는 감소하고 gen_loss는 증가하는 것으로 보아 generator가 discriminator를 속이기 위한 이미지를 잘 생성하지 못하는 것으로 판단된다. 실제로 train과 test에 없는 전혀 새로운 이미지를 입력했을 때, 다음과 같이 잘 생성하지 못하는 결과를 보여줬다.

뒤틀린 황천의 피카츄

아무래도 기존에 있는 이미지는 원본이랑 비슷한 정도로 생성하는 것으로 보아 train set에 overfitting 된 것 같다. 추가로 다른 데이터셋을 더 활용하거나, 모델의 구조를 바꾸어서 다시 학습시켜봐야겠다.

 

프로젝트를 진행하면서 많은 문제점과 어려움을 겪었다. 우선 웹페이지에 이미지를 하나 띄우는 것 까진 쉬웠지만 생성된 이미지를 다시 화면에 출력하는 것이 이렇게 어려운 작업이 될 줄은 몰랐다. 프론트엔드 개발자들 대단해...

겪었던 문제점들을 정리해보면 다음과 같다.

  • 프론트엔드/백엔드 지식이 전혀 없어서 HTML부터 공부해서 페이지를 제작함
  • Inference 결과 tensor object로 반환되어 해당 object를 바로 웹에서 rendering 가능한 방법을 찾다가 결국 이미지로 저장하여 rendering 하는 방식으로 진행
  • 페이지는 정상적으로 만들어졌는데 계속해서 기존의 이미지를 가져옴 -> 브라우저 쿠키 문제
  • model의 크기가 100MB가 넘어가서 git lfs를 이용하여 git에 업로드 하였으나 Ainize에서 불러오지 못하는 문제가 발생
  • heroku에서 배포를 시도하였으나 메모리 용량 부족(500MB)으로 배포 실패
  • 기본 git lfs 용량을 모두 사용하여 wget으로 바로 다운로드 가능한 무료 웹서버를 찾아보았지만 당장 쓸 수 있는 게 없어서 개인 웹서버 이용하여 모델 다운로드 후 Ainize로 배포

모델을 만들어서 학습시키고 실행 가능한 python 파일로 정리하는 것 까진 어렵지 않았는데 웹페이지를 구축하는 게 가장 어려웠다. 단순 텍스트 데이터가 아닌 이미지 데이터를 웹에서 띄우는 방법을 몰라 html을 공부해서 겨우 띄울 수 있게 되었는데 브라우저 캐시 때문에 정상작동 하는 걸 오작동으로 인식하여 다른 방법을 찾았다. 결국 프론트엔드를 공부하고 있는 sunhpark에게 도움을 요청하여 js로 웹페이지를 좀 더 깔끔하게 만드는 데 성공했다.

 

로컬에서 테스트 할 땐 잘 되었는데 막상 배포하려고 하니 적절한 서비스가 없었다. 우선 git에 모델 용량이 100MB를 초과해서 git lfs를 이용하여 Ainize로 배포를 시도하였으나 디버깅 하다가 git lfs 무료 사용량을 전부 사용하여 다른 서비스를 찾아보았다. heroku로 배포를 시도했을 땐 메모리 용량 부족으로 실패하였다.

 

마지막 남은 EC2는 1년만 무료여서 최후의 보루로 남겨두고 Ainize로 배포해보기 위하여 무료 파일 서버 서비스를 찾아보았지만 마땅한 게 없어서 결국 임시로 웹서버를 만들어서 Ainize에서 Dockerfile을 빌드할 때 모델을 받아올 수 있도록 하였고, 결국 성공하였다.

 

구글 드라이브나 네이버 클라우드 같은 곳에 파일을 업로드해서 wget으로 다운로드 받는 방법도 생각했었다. 그러나, wget으로 직접 다운로드 가능한 링크들이 아니어서 다른 방법을 찾았던건데, 나중에 찾아보니 구글 드라이브에 올린 파일도 wget으로 받을 수 있는 방법이 있었다. 당시엔 배포에 정신이 팔려 있어서 검색해볼 생각도 못 했나보다.

 

이번 프로젝트를 진행해보면서 왜 직접 해보는 것이 중요한지 확실하게 깨달았다. 아주 작은 토이 프로젝트였지만 어떻게 보면 프론트엔드와 백엔드 두 부분 모두 조금이나마 경험해볼 수 있었고, 직접 모델 설계부터 서비스까지 배포한 것은 MLOps라고 볼 수도 있지 않을까?

 

다음번엔 서버에 데이터 저장도 하고, 실제로 아마존 EC2나 GCP, Azure 같은 서비스를 이용하여 배포도 해보고, 좀 더 재미있는 아이디어를 가지고 팀원도 모아서 프로젝트를 진행해보도록 해야겠다.

 

해당 서비스는 다음 링크에서 테스트해볼 수 있다.


Run on Ainize

'AI' 카테고리의 다른 글

Inference time은 CPU와 GPU 중 뭐가 더 빠를까?  (0) 2021.05.13
auto-painter 진행 상황  (0) 2021.05.11
[Monthly Dacon 14] 첫 in 10등!  (1) 2021.04.12
Heroku로 딥러닝 모델 배포하는 방법  (0) 2021.04.02
VGGNet 구현해보기  (0) 2021.03.27

은 포기하는 게 좋다.

 

scikit-learn과 같은 라이브러리를 이용해서 만든 머신러닝 모델들은 가능할 지도 모르겠지만,

내가 시도했던 GAN 모델 중 하나인 U-Net구조의 Pix2Pix 모델만 해도 Heroku에서 배포가 불가능 했다.

로그를 확인해보면 알겠지만, heroku에 할당되는 container에는 약 500MB의 메모리가 할당되는 것 같다. 그러나 딥러닝 모델을 돌리기엔 턱없이 부족한 메모리이기 때문에 모델을 불러오는 과정에서 이미 메모리 부족으로 에러가 발생한다. 따라서 딥러닝 모델을 배포하려면 Ainize나 아마존 EC2와 같은 클라우드 서비스를 이용하는 것이 좋을 것 같다.

개요

VGGNet은 2014년 ILSVRC(ImageNet Large Scale Visual Recognition Challenge)의 Classification+Localization 분야에서 1, 2등을 했을 만큼 당시에 Classification 분야에서 좋은 성능을 낸 대표적인 모델 중 하나이다. VGG는 Visual Geometry Group의 약자로 연구팀 이름으로, VGG뒤의 숫자는 신경망 모델의 깊이(레이어 수)를 나타낸다.

모델의 구조

  • Input : 224 x 224 RGB image
  • 이미지 전처리 : train set의 RGB 평균값을 각 픽셀로 부터 계산하여 빼줌
  • filter : 3 x 3, 1 x 1(input channel 에서 linear transformation 할 때)
  • stride = 1, padding = same
  • max-pooling : 2 x 2, stride = 2
  • 3개의 FC Layer : 4096 → 4096 → 1000(classification, softmax)
  • activation function : ReLU
  • total 144M parameter(VGG-19)

VGG Architecture

모델의 구성

아래의 표 1에서 볼 수 있듯이, A-E라는 이름의 모델들을 11개의 layer(8 conv. + 3FC layers) 부터 최대 19개의 layer(16 conv. + 3FC layers)까지 모델의 깊이를 변화시켜가며 실험을 진행하였다.

VGGNet은 이전에 7x7의 filter를 사용하는 모델들과 달리 상하좌우의 feature를 추출할 수 있는 가장 작은 크기의 filter인 3x3을 사용하였다. filter의 크기를 줄이는 대신 layer를 늘려 7x7 filter와 동일한 receptive field를 갖도록 했다.

출처 : Research Gate

3x3 filter를 사용함으로써 얻는 이점은 다음과 같다.

  1. 결정 함수의 비선형성 증가 7x7 filter를 사용했을 땐 activation function을 한 번 밖에 통과하지 않지만, 3x3 filter의 3개 layer를 사용했을 땐 activation function을 3번 통과하므로 함수의 비선형성이 증가하게 된다. 따라서 모델이 더 복잡한 특징도 추출할 수 있게 된다.
  2. 학습 파라미터 수의 감소 7x7 filter를 사용한 C채널의 학습 파라미터 수는 $7^2C^2$로 총 $49C^2$이다. 그러나 3x3 filter를 사용한 3개 layer의 학습 파라미터 수는 $3(3^2C^2)$으로 총 $27C^2$이다. 7x7과 3x3의 3layer는 동일한 receptive field를 가지지만 parameter 수는 약 81% 감소했다. 따라서 학습시간에 있어서 더 유리하게 된다.

모델 학습

VGGNet은 다음과 같은 최적화 방법을 사용하여 모델을 학습시켰다.

  • mini-batch gradient descent
  • Momentum (0.9)
  • batch_size = 256
  • weight decay (L2 = 5e10-4)
  • Dropout (0.5)
  • Initial learning rate = 0.01
  • 모델을 학습하면서 validation set accuracy가 증가하지 않을 때 learning rate를 1/10으로 감소시켰으며, 370K iterations (74 epochs)를 진행하는 동안 3번의 learning rate 감소가 이루어졌다.
  • Initial weight는 N(0, 0.01^2)을 따르는 정규분포에서 sampling
  • Initial bias = 0
  • 고정된 224x224의 image를 얻기 위해 train image를 random하게 crop하거나 rescale하였고, 더 나아가 augmentation을 위해 random하게 수평으로 flip하거나 RGB color를 shift 하였다.

평가

  1. local response normalization (A-LRN network)을 사용한 것은 큰 효과가 없었다. 따라서 더 깊은 레이어(B-E)에서 normalization을 사용하지 않았다.
  2. ConvNet의 깊이가 증가함에 따라 classification error가 감소하는 것을 확인할 수 있었다. 해당 Dataset에서는 19개의 layer로도 error rate가 saturated 되었지만, 더 큰 Dataset에서는 더 깊은 layer가 유용할 수 있다.

결론

  • ConvNet에서 layer의 깊이를 증가시키면 classification accuracy를 향상시키는 데 도움이 되며, ImageNet challenge dataset에 대한 state-of-the-art 성능을 얻을 수 있음을 증명하였다.

해당 모델을 코드로는 구현 해보았으나, 실제로 학습을 시켜보진 않았다. 150GB나 되는 ImageNet Dataset을 받아서 학습을 시킬 엄두가 나질 않았기 때문이다. ImageNet을 학습시키는 데 걸리는 시간은 P100 256개를 병렬로 연결해도 1시간이나 걸리는 것으로 알고 있는데, 단일 GPU를 사용하면 아마 몇 주 단위의 시간이 소요될 것이기 때문에 어쩔 수 없었다.

 

이 논문을 봤을 때, 딥러닝은 역시 컴퓨팅 파워가 중요하다고 생각했다. 과연 2014년 이전에는 deep network에 대한 인식이 없었을까? 물론 이전에도 deep network를 위한 시도는 있었으나 컴퓨팅 파워의 부족으로 ILSVRC의 dataset를 학습시킬 수 없었기 때문에 할 수 없었던 것으로 보인다. 분명 2021년인 지금도 엄청나게 깊은 deep network에 대한 시도는 계속 되고 있을 것이라고 생각한다. 다만 해당 network를 학습시킬 만한 컴퓨팅 파워가 부족하기 때문에 해당 network의 성능을 확인할 수 없을 뿐이라고 생각한다. 만약 딥러닝의 'deep'이 정말로 우리가 생각도 못할 만큼 'deep' 해야 한다면? 지금의 딥러닝 모델들이 이제 겨우 시작이라고 한다면?

 

실제 구현 코드는 깃허브

Copy of 6. 작사가 인공지능 만들기

학습목표


  • 인공지능이 문장을 이해하는 방식에 대해 알아본다.
  • 시퀀스에 대해 알아본다.

시퀀스(Sequence)

시퀀스는 데이터에 순서(번호)를 붙여 나열한 것이다. 시퀀스의 특징은 다음과 같다.

  • 데이터를 순서대로 하나씩 나열하여 나타낸 데이터 구조이다.
  • 특정 위치(~번째)의 데이터를 가리킬 수 있다.

문장은 각 단어들이 문법이라는 규칙을 따라 배열되어 있기 때문에 시퀀스 데이터로 볼 수 있다.

문법은 복잡하기 때문에 문장 데이터를 이용한 인공지능을 만들 때에는 통계에 기반한 방법을 이용한다.

순환신경망(RNN)

나는 공부를 [ ]에서 빈 칸에는 한다 라는 단어가 들어갈 것이다. 문장에는 앞 뒤 문맥에 따라 통계적으로 많이 사용되는 단어들이 있다. 인공지능이 글을 이해하는 방식도 위와 같다. 문법적인 원리를 통해서가 아닌 수많은 글을 읽게하여 통계적으로 다음 단어는 어떤 것이 올지 예측하는 것이다. 이 방식에 가장 적합한 인공지능 모델 중 하나가 순환신경망(RNN)이다.

시작은 <start>라는 특수한 토큰을 앞에 추가하여 시작을 나타내고, <end>라는 토큰을 통해 문장의 끝을 나타낸다.

언어 모델

나는, 공부를, 한다 를 순차적으로 생성할 때, 우리는 공부를 다음이 한다인 것을 쉽게 할 수 있다. 나는 다음이 한다인 것은 어딘가 어색하게 느껴진다. 실제로 인공지능이 동작하는 방식도 순전히 운이다.

이것을 좀 더 확률적으로 표현해 보면 나는 공부를 다음에 한다가 나올 확률을 $p(한다|나는, 공부를)$라고 하면, $p(공부를|나는)$보다는 높게 나올 것이다. $p(한다|나는, 공부를, 열심히)$의 확률값은 더 높아질 것이다.

문장에서 단어 뒤에 다음 단어가 나올 확률이 높다는 것은 그 단어가 자연스럽다는 뜻이 된다. 확률이 낮다고 해서 자연스럽지 않은 것은 아니다. 단어 뒤에 올 수 있는 자연스러운 단어의 경우의 수가 워낙 많아서 불확실성이 높을 뿐이다.

n-1개의 단어 시퀀스 $w_1,⋯,w_{n-1}$이 주어졌을 때, n번째 단어 $w_n$으로 무엇이 올지 예측하는 확률 모델을 언어 모델(Language Model)이라고 부른다. 파라미터 $\theta$로 모델링 하는 언어 모델을 다음과 같이 표현할 수 있다.

언어 모델은 어떻게 학습시킬 수 있을까? 언어 모델의 학습 데이터는 어떻게 나누어야 할까? 답은 간단하다. 어떠한 텍스트도 언어 모델의 학습 데이터가 될 수 있다. x_train이 n-1번째까지의 단어 시퀀스고, y_train이 n번째 단어가 되는 데이터셋이면 얼마든지 학습 데이터로 사용할 수 있다. 이렇게 잘 훈련된 언어 모델은 훌륭한 문장 생성기가 된다.

인공지능 만들기

(1) 데이터 준비


import re
import numpy as np
import tensorflow as tf
import os

# 파일 열기
file_path = os.getenv('HOME') + '/aiffel/lyricist/data/shakespeare.txt'
with open(file_path, "r") as f:
    raw_corpus = f.read().splitlines() # 텍스트를 라인 단위로 끊어서 list 형태로 읽어온다.

print(raw_corpus[:9])

데이터에서 우리가 원하는 것은 문장(대사)뿐이므로, 화자 이름이나 공백은 제거해주어야 한다.

# 문장 indexing
for idx, sentence in enumerate(raw_corpus):
    if len(sentence) == 0: continue   # 길이가 0인 문장은 스킵
    if sentence[-1] == ":": continue  # :로 끝나는 문장은 스킵

    if idx > 9: break

    print(sentence)

이제 문장을 단어로 나누어야 한다. 문장을 형태소로 나누는 것을 토큰화(Tokenize)라고 한다. 가장 간단한 방법은 띄어쓰기를 기준으로 나누는 것이다. 그러나 문장부호, 대소문자, 특수문자 등이 있기 때문에 따로 전처리를 먼저 해주어야 한다.

# 문장 전처리 함수
def preprocess_sentence(sentence):
    sentence = sentence.lower().strip() # 소문자로 바꾸고 양쪽 공백을 삭제

        # 정규식을 이용하여 문장 처리
    sentence = re.sub(r"([?.!,¿])", r" \1 ", sentence) # 패턴의 특수문자를 만나면 특수문자 양쪽에 공백을 추가
    sentence = re.sub(r'[" "]+', " ", sentence) # 공백 패턴을 만나면 스페이스 1개로 치환
    sentence = re.sub(r"[^a-zA-Z?.!,¿]+", " ", sentence) # a-zA-Z?.!,¿ 패턴을 제외한 모든 문자(공백문자까지도)를 스페이스 1개로 치환

    sentence = sentence.strip()

    sentence = '<start> ' + sentence + ' <end>' # 문장 앞뒤로 <start>와 <end>를 단어처럼 붙여 줍니다

    return sentence

print(preprocess_sentence("Hi, This @_is ;;;sample        sentence?"))

우리가 구축해야할 데이터셋은 입력이 되는 소스 문장(Source Sentence)과 출력이 되는 타겟 문장(Target Sentence)으로 나누어야 한다.

언어 모델의 입력 문장 : <start> 나는 공부를 한다
언어 모델의 출력 문장 : 나는 공부를 한다 <end>

위에서 만든 전처리 함수에서 를 제거하면 소스 문장, 를 제거하면 타겟 문장이 된다.

corpus = []

# 모든 문장에 전처리 함수 적용
for sentence in raw_corpus:
    if len(sentence) == 0: continue
    if sentence[-1] == ":": continue

    corpus.append(preprocess_sentence(sentence))

corpus[:10]

이제 문장을 컴퓨터가 이해할 수 있는 숫자로 변경해주어야 한다. 텐서플로우는 자연어 처리를 위한 여러 가지 모듈을 제공하며, tf.keras.preprocessing.text.Tokenizer 패키지는 데이터를 토큰화하고, dictionary를 만들어주며, 데이터를 숫자로 변환까지 한 번에 해준다. 이 과정을 벡터화(Vectorize)라고 하며, 변환된 숫자 데이터를 텐서(tensor)라고 한다.

def tokenize(corpus):
    # 텐서플로우에서 제공하는 Tokenizer 패키지를 생성
    tokenizer = tf.keras.preprocessing.text.Tokenizer(
        num_words=7000,   # 전체 단어의 개수 
        filters=' ',      # 전처리 로직
        oov_token="<unk>" # out-of-vocabulary, 사전에 없는 단어
    )
    tokenizer.fit_on_texts(corpus) # 우리가 구축한 corpus로부터 Tokenizer가 사전을 자동구축

    # tokenizer를 활용하여 모델에 입력할 데이터셋을 구축
    tensor = tokenizer.texts_to_sequences(corpus) # tokenizer는 구축한 사전으로부터 corpus를 해석해 Tensor로 변환

    # 입력 데이터의 시퀀스 길이를 일정하게 맞추기 위한 padding  메소드
    # maxlen의 디폴트값은 None. corpus의 가장 긴 문장을 기준으로 시퀀스 길이가 맞춰진다
    tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')  

    print(tensor,tokenizer)
    return tensor, tokenizer

tensor, tokenizer = tokenize(corpus)

print(tensor[:3, :10]) # 생성된 텐서 데이터 확인

# 단어 사전의 index
for idx in tokenizer.index_word:
    print(idx, ":", tokenizer.index_word[idx])

    if idx >= 10: break

src_input = tensor[:, :-1] # tensor에서 마지막 토큰을 잘라내서 소스 문장을 생성. 마지막 토큰은 <end>가 아니라 <pad>일 가능성이 높다.
tgt_input = tensor[:, 1:]  # tensor에서 <start>를 잘라내서 타겟 문장을 생성.

print(src_input[0])
print(tgt_input[0])

# 데이터셋 구축
BUFFER_SIZE = len(src_input)
BATCH_SIZE = 256
steps_per_epoch = len(src_input) // BATCH_SIZE

VOCAB_SIZE = tokenizer.num_words + 1 # 0:<pad>를 포함하여 dictionary 갯수 + 1

dataset = tf.data.Dataset.from_tensor_slices((src_input, tgt_input)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
dataset
  • 데이터셋 생성 과정 요약
    • 정규표현식을 이용한 corpus 생성
    • tf.keras.preprocessing.text.Tokenizer를 이용해 corpus를 텐서로 변환
    • tf.data.Dataset.from_tensor_slices()를 이용해 corpus 텐서를 tf.data.Dataset객체로 변환

(2) 모델 학습하기


이번에 만들 모델의 구조는 다음과 같다.

# 모델 생성
class TextGenerator(tf.keras.Model):
    def __init__(self, vocab_size, embedding_size, hidden_size):
        super(TextGenerator, self).__init__()

        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_size)
        self.rnn_1 = tf.keras.layers.LSTM(hidden_size, return_sequences=True)
        self.rnn_2 = tf.keras.layers.LSTM(hidden_size, return_sequences=True)
        self.linear = tf.keras.layers.Dense(vocab_size)

    def call(self, x):
        out = self.embedding(x)
        out = self.rnn_1(out)
        out = self.rnn_2(out)
        out = self.linear(out)

        return out

embedding_size = 256 # 워드 벡터의 차원 수
hidden_size = 1024 # LSTM Layer의 hidden 차원 수
model = TextGenerator(tokenizer.num_words + 1, embedding_size , hidden_size)

# 모델의 데이터 확인
for src_sample, tgt_sample in dataset.take(1): break
model(src_sample)
# 모델의 최종 출력 shape는 (256, 20, 7001)
# 256은 batch_size, 20은 squence_length, 7001은 단어의 갯수(Dense Layer 출력 차원 수)

model.summary() # sequence_length를 모르기 때문에 Output shape를 정확하게 모른다.

# 모델 학습
optimizer = tf.keras.optimizers.Adam()
loss = tf.keras.losses.SparseCategoricalCrossentropy(
    from_logits=True,
    reduction='none'
)

model.compile(loss=loss, optimizer=optimizer)
model.fit(dataset, epochs=30)

위의 코드에서 embbeding_size 는 워드 벡터의 차원 수를 나타내는 parameter로, 단어가 추상적으로 표현되는 크기를 말한다. 예를 들어 크기가 2라면 다음과 같이 표현할 수 있다.

  • 차갑다: [0.0, 1.0]
  • 뜨겁다: [1.0, 0.0]
  • 미지근하다: [0.5, 0.5]

(3) 모델 평가하기


인공지능을 통한 작문은 수치적으로 평가할 수 없기 때문에 사람이 직접 평가를 해야 한다.

# 문장 생성 함수
def generate_text(model, tokenizer, init_sentence="<start>", max_len=20):
    # 테스트를 위해서 입력받은 init_sentence도 텐서로 변환
    test_input = tokenizer.texts_to_sequences([init_sentence])
    test_tensor = tf.convert_to_tensor(test_input, dtype=tf.int64)
    end_token = tokenizer.word_index["<end>"]

    # 단어를 하나씩 생성
    while True:
        predict = model(test_tensor)  # 입력받은 문장
        predict_word = tf.argmax(tf.nn.softmax(predict, axis=-1), axis=-1)[:, -1]   # 예측한 단어가 새로 생성된 단어 

        # 우리 모델이 새롭게 예측한 단어를 입력 문장의 뒤에 붙이기
        test_tensor = tf.concat([test_tensor, 
                                                                 tf.expand_dims(predict_word, axis=0)], axis=-1)

        # 우리 모델이 <end>를 예측했거나, max_len에 도달하지 않았다면  while 루프를 또 돌면서 다음 단어를 예측
        if predict_word.numpy()[0] == end_token: break
        if test_tensor.shape[1] >= max_len: break

    generated = ""
    # 생성된 tensor 안에 있는 word index를 tokenizer.index_word 사전을 통해 실제 단어로 하나씩 변환
    for word_index in test_tensor[0].numpy():
        generated += tokenizer.index_word[word_index] + " "

    return generated   # 최종 생성된 자연어 문장

# 생성 함수 실행
generate_text(model, tokenizer, init_sentence="<start> i love")

회고록

  • range(n)도 reverse() 함수가 먹힌다는 걸 오늘 알았다...
  • 예시에 주어진 train data 갯수는 124960인걸 보면 총 데이터는 156200개인 것 같은데 아무리 전처리 단계에서 조건에 맞게 처리해도 168000개 정도가 나온다. 아무튼 일단 돌려본다.
  • 문장의 길이가 최대 15라는 이야기는 <start>, <end>를 포함하여 15가 되어야 하는 것 같아서 tokenize했을 때 문장의 길이가 13 이하인 것만 corpus로 만들었다.
  • 학습 회차 별 생성된 문장 input : <start> i love
    • 1회차 '<start> i love you , i love you <end> '
    • 2회차 '<start> i love you , i m not gonna crack <end> '
    • 3회차'<start> i love you to be a shot , i m not a man <end> '
    • 4회차 '<start> i love you , i m not stunning , i m a fool <end> '
  • batch_size를 각각 256, 512, 1024로 늘려서 진행했는데, 1epoch당 걸리는 시간이 74s, 62s, 59s 정도로 batch_size 배수 만큼의 차이는 없었다. batch_size가 배로 늘어나면 걸리느 시간도 당연히 반으로 줄어들 것이라 생각했는데 오산이었다.
  • 1회차는 tokenize 했을 때 length가 15 이하인 것을 train_data로 사용하였다.
  • 2, 3, 4회차는 tokenize 했을 때 length가 13 이하인 것을 train_data로 사용하였다.
  • 3회차는 2회차랑 동일한 데이터에 padding 을 post에서 pre로 변경하였다. RNN에서는 뒤에 padding을 넣는 것 보다 앞쪽에 padding을 넣어주는 쪽이 마지막 결과에 paddind이 미치는 영향이 적어지기 때문에 더 좋은 성능을 낼 수 있다고 알고있기 때문이다.
  • 근데 실제로는 pre padding 쪽이 loss가 더 크게 나왔다. 확인해보니 이론상으로는 pre padding이 성능이 더 좋지만 실제로는 post padding쪽이 성능이 더 잘 나와서 post padding을 많이 쓴다고 한다.
  • batch_size를 변경해서 pre padding을 한 번 더 돌려보았더니 같은 조건에서의 post padding 보다 loss가 높았고 문장도 부자연스러웠다. 앞으로는 post padding을 사용해야겠다.

  오늘은 아이펠 첫 Exploration 노드를 진행하였다. 나는 이론보다는 실전파라 더 기대가 되는 날이었다. 매번 인터넷으로만 보던 딥러닝 모델을 내가 직접 구현해볼 수 있는 아주 좋은 기회였다. 물론 처음부터 모든 코드를 내가 짠 것은 아니지만, 지금은 어떻게 작동을 하는지 그 원리를 이해하고 익히는 것이 중요하다고 생각한다. 처음에는 노드를 열심히 따라가면서 코드를 하나하나 실행시켜 보는 것으로 시작해서, 가위바위보를 분류하는 모델은 앞서 모델링되어있는 자료를 참고하여 내가 직접 복붙(?)한 코드이다. 물론 MNIST를 이용한 숫자 분류와는 조금 다른 부분이 있어서 해당 부분만 수정할 줄 안다면 그다지 어렵지 않은 과제였다. 라고 생각했던 때가 있었지...

How to make?


일반적으로 딥러닝 기술은 "데이터 준비 → 딥러닝 네트워크 설계 → 학습 → 테스트(평가)" 순으로 이루어진다.

1. 데이터 준비

MINIST 숫자 손글씨 Dataset 불러들이기


import tensorflow as tf
from tensorflow import keras

import numpy as np
import matplotlib.pyplot as plt

print(tf.__version__) # Tensorflow의 버전 출력

mnist = keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

print(len(x_train)) # x_train 배열의 크기를 출력

plt.imshow(x_train[1], cmap=plt.cm.binary)
plt.show() # x_train의 1번째 이미지를 출력

print(y_train[1]) # x_train[1]에 대응하는 실제 숫자값

index = 10000
plt.imshow(x_train[index], cmap=plt.cm.binary)
plt.show()
print(f'{index} 번째 이미지의 숫자는 바로 {y_train[index]} 입니다.')

print(x_train.shape) # x_train 이미지의 (count, x, y)
print(x_test.shape)

Data 전처리 하기


인공지능 모델을 훈련시킬 때, 값이 너무 커지거나 하는 것을 방지하기 위해 정수 연산보다는 0~1 사이의 값으로 정규화 시켜주는 것이 좋다.

정규화는 모든 값을 최댓값으로 나누어주면 된다.

print(f'최소값: {np.min(x_train)} 최대값: {np.max(x_train)}')

x_train_norm, x_test_norm = x_train / 255.0, x_test / 255.0
print(f'최소값: {np.min(x_train_norm)} 최대값: {np.max(x_train_norm)}')

2. 딥러닝 네트워크 설계하기

Sequential Model 사용해보기


model = keras.models.Sequential()
model.add(keras.layers.Conv2D(16, (3,3), activation='relu', input_shape=(28,28,1)))
model.add(keras.layers.MaxPool2D(2,2))
model.add(keras.layers.Conv2D(32, (3,3), activation='relu'))
model.add(keras.layers.MaxPooling2D((2,2)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(32, activation='relu'))
model.add(keras.layers.Dense(10, activation='softmax'))

print(f'Model에 추가된 Layer 개수: {len(model.layers)}')

model.summary()

3. 딥러닝 네트워크 학습시키기

우리가 만든 네트워크의 입력은 (data_size, x_size, y_size, channel) 과 같은 형태를 가진다. 그러나 x_train.shape 에는 채널수에 대한 정보가 없기 때문에 만들어주어야 한다.

print("Before Reshape - x_train_norm shape: {}".format(x_train_norm.shape))
print("Before Reshape - x_test_norm shape: {}".format(x_test_norm.shape))

x_train_reshaped=x_train_norm.reshape( -1, 28, 28, 1)  # 데이터갯수에 -1을 쓰면 reshape시 자동계산됩니다.
x_test_reshaped=x_test_norm.reshape( -1, 28, 28, 1)

print("After Reshape - x_train_reshaped shape: {}".format(x_train_reshaped.shape))
print("After Reshape - x_test_reshaped shape: {}".format(x_test_reshaped.shape))

model.compile(optimizer='adam',
             loss='sparse_categorical_crossentropy',
             metrics=['accuracy'])

model.fit(x_train_reshaped, y_train, epochs=10)

10epochs 정도 돌려본 결과 99.8% 에 가까운 정확도를 나타내는 것을 확인하였다.

4. 모델 평가하기

테스트 데이터로 성능을 확인해보기


test_loss, test_accuracy = model.evaluate(x_test_reshaped,y_test, verbose=2)
print("test_loss: {} ".format(test_loss))
print("test_accuracy: {}".format(test_accuracy))

실제 테스트 데이터를 이용하여 테스트를 진행해본 결과, 99.1% 로 소폭 하락하였다. MNIST 데이터셋 참고문헌을 보면 학습용 데이터와 시험용 데이터의 손글씨 주인이 다른 것을 알 수 있다.

어떤 데이터를 잘못 추론했는지 확인해보기


model.evalutate() 대신 model.predict()를 사용하면 model이 입력값을 보고 실제로 추론한 확률분포를 출력할 수 있다.

predicted_result = model.predict(x_test_reshaped)  # model이 추론한 확률값. 
predicted_labels = np.argmax(predicted_result, axis=1)

idx = 0  # 1번째 x_test를 살펴보자. 
print('model.predict() 결과 : ', predicted_result[idx])
print('model이 추론한 가장 가능성이 높은 결과 : ', predicted_labels[idx])
print('실제 데이터의 라벨 : ', y_test[idx])

# 실제 데이터 확인
plt.imshow(x_test[idx],cmap=plt.cm.binary)
plt.show()

추론 결과는 벡터 형태로, 추론 결과가 각각 0, 1, 2, ..., 7, 8, 9 일 확률을 의미한다.

아래 코드는 추론해낸 숫자와 실제 값이 다른 경우를 확인해보는 코드이다.

import random
wrong_predict_list=[]
for i, _ in enumerate(predicted_labels):
    # i번째 test_labels과 y_test이 다른 경우만 모아 봅시다. 
    if predicted_labels[i] != y_test[i]:
        wrong_predict_list.append(i)

# wrong_predict_list 에서 랜덤하게 5개만 뽑아봅시다.
samples = random.choices(population=wrong_predict_list, k=5)

for n in samples:
    print("예측확률분포: " + str(predicted_result[n]))
    print("라벨: " + str(y_test[n]) + ", 예측결과: " + str(predicted_labels[n]))
    plt.imshow(x_test[n], cmap=plt.cm.binary)
    plt.show()

5. 더 좋은 네트워크 만들어 보기

딥러닝 네트워크 구조 자체는 바꾸지 않으면서도 인식률을 올릴 수 있는 방법은 Hyperparameter 들을 바꿔보는 것이다. Conv2D 레이어에서 입력 이미지의 특징 수를 증감시켜보거나, Dense 레이어에서 뉴런 수를 바꾸어보거나, epoch 값을 변경해볼 수 있다.

Title n_channel_1 n_channel_2 n_dense n_train_epoch loss accuracy
1 16 32 32 10 0.0417 0.9889
2 1 32 32 10 0.0636 0.9793
3 2 32 32 10 0.0420 0.9865
4 4 32 32 10 0.0405 0.9886
5 8 32 32 10 0.0360 0.9885
6 32 32 32 10 0.0322 0.9903
7 64 32 32 10 0.0325 0.9914
8 128 32 32 10 0.0320 0.9912
9 16 1 32 10 0.1800 0.9437
10 16 64 32 10 0.0322 0.9912
11 16 128 32 10 0.0348 0.9917
12 16 32 64 10 0.0430 0.9888
13 16 32 128 10 0.0327 0.9916
14 16 32 32 15 0.0427 0.9900
15 16 32 32 20 0.0523 0.9884
16 64 128 128 15 0.0503 0.9901

각각의 Hyperparameter 별로 최적의 값을 찾아서 해당 값들만으로 테스트해보면 가장 좋은 결과가 나올 것 이라고 예상했는데 현실은 아니었다. 이래서 딥러닝이 어려운 것 같다.

6. 프로젝트: 가위바위보 분류기 만들기

오늘 배운 내용을 바탕으로 가위바위보 분류기를 만들어보자.

데이터 준비


1. 데이터 만들기

데이터는 구글의 teachable machine 사이트를 이용하면 쉽게 만들어볼 수 있다.

  • 여러 각도에서
  • 여러 크기로
  • 다른 사람과 함께
  • 만들면 더 좋은 데이터를 얻을 수 있다.

다운받은 이미지의 크기는 224x224 이다.

2. 데이터 불러오기 + Resize 하기

MNIST 데이터셋의 경우 이미지 크기가 28x28이었기 때문에 우리의 이미지도 28x28 로 만들어야 한다. 이미지를 Resize 하기 위해 PIL 라이브러리를 사용한다.

# PIL 라이브러리가 설치되어 있지 않다면 설치
!pip install pillow   

from PIL import Image
import os, glob

print("PIL 라이브러리 import 완료!")

# 이미지 Resize 하기
# 가위 이미지가 저장된 디렉토리 아래의 모든 jpg 파일을 읽어들여서
image_dir_path = os.getenv("HOME") + "/aiffel/rock_scissor_paper/train/scissor"
print("이미지 디렉토리 경로: ", image_dir_path)

images=glob.glob(image_dir_path + "/*.jpg")  

# 파일마다 모두 28x28 사이즈로 바꾸어 저장
target_size=(28,28)
for img in images:
    old_img=Image.open(img)
    new_img=old_img.resize(target_size,Image.ANTIALIAS)
    new_img.save(img,"JPEG")

print("가위 이미지 resize 완료!")
# load_data 함수

def load_data(img_path, number):
    # 가위 : 0, 바위 : 1, 보 : 2
    number_of_data=number   # 가위바위보 이미지 개수 총합
    img_size=28
    color=3
    #이미지 데이터와 라벨(가위 : 0, 바위 : 1, 보 : 2) 데이터를 담을 행렬(matrix) 영역을 생성
    imgs=np.zeros(number_of_data*img_size*img_size*color,dtype=np.int32).reshape(number_of_data,img_size,img_size,color)
    labels=np.zeros(number_of_data,dtype=np.int32)

    idx=0
    for file in glob.iglob(img_path+'/scissor/*.jpg'):
        img = np.array(Image.open(file),dtype=np.int32)
        imgs[idx,:,:,:]=img    # 데이터 영역에 이미지 행렬을 복사
        labels[idx]=0   # 가위 : 0
        idx=idx+1

    for file in glob.iglob(img_path+'/rock/*.jpg'):
        img = np.array(Image.open(file),dtype=np.int32)
        imgs[idx,:,:,:]=img    # 데이터 영역에 이미지 행렬을 복사
        labels[idx]=1   # 바위 : 1
        idx=idx+1       

    for file in glob.iglob(img_path+'/paper/*.jpg'):
        img = np.array(Image.open(file),dtype=np.int32)
        imgs[idx,:,:,:]=img    # 데이터 영역에 이미지 행렬을 복사
        labels[idx]=2   # 보 : 2
        idx=idx+1

    print("학습데이터(x_train)의 이미지 개수는",idx,"입니다.")
    return imgs, labels

image_dir_path = os.getenv("HOME") + "/aiffel/rock_scissor_paper"
(x_train, y_train)=load_data(image_dir_path, 2100)
x_train_norm = x_train/255.0   # 입력은 0~1 사이의 값으로 정규화

print("x_train shape: {}".format(x_train.shape))
print("y_train shape: {}".format(y_train.shape))

# 불러온 이미지 확인
import matplotlib.pyplot as plt
plt.imshow(x_train[0])
print('라벨: ', y_train[0])

# 딥러닝 네트워크 설계
import tensorflow as tf
from tensorflow import keras
import numpy as np

n_channel_1=16
n_channel_2=32
n_dense=64
n_train_epoch=15

model=keras.models.Sequential()
model.add(keras.layers.Conv2D(n_channel_1, (3,3), activation='relu', input_shape=(28,28,3)))
model.add(keras.layers.MaxPool2D(2,2))
model.add(keras.layers.Conv2D(n_channel_2, (3,3), activation='relu'))
model.add(keras.layers.MaxPooling2D((2,2)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(n_dense, activation='relu'))
model.add(keras.layers.Dense(3, activation='softmax'))

model.summary()

# 모델 학습
model.compile(optimizer='adam',
             loss='sparse_categorical_crossentropy',
             metrics=['accuracy'])

model.fit(x_train_reshaped, y_train, epochs=n_train_epoch)

# 테스트 이미지
image_dir_path = os.getenv("HOME") + "/aiffel/rock_scissor_paper/test/testset1"
(x_test, y_test)=load_data(image_dir_path, 300)
x_test_norm = x_test/255.0   # 입력은 0~1 사이의 값으로 정규화

print("x_train shape: {}".format(x_test.shape))
print("y_train shape: {}".format(y_test.shape))

# 불러온 이미지 확인
import matplotlib.pyplot as plt
plt.imshow(x_test[0])
print('라벨: ', y_test[0])

# 모델 테스트
test_loss, test_accuracy = model.evaluate(x_test_reshaped, y_test, verbose=2)
print("test_loss: {} ".format(test_loss))
print("test_accuracy: {}".format(test_accuracy))

처음 100개의 데이터 가지고 실행했을 때 결과는 처참했다...

총 10명 분량을 train set으로 사용하고 test를 돌렸을 때 가장 잘 나온 결과!

오늘은 Layer를 추가하지 않고 단순히 Hyperparameter만 조정하여 인식률을 높이는 것을 목표로 했다. 우선 데이터가 부족한 것 같아서 10명 보다 더 많은 데이터를 추가해보면 좋을 것 같다. 아직 첫 모델이라 많이 부족했지만, 그래도 뭔가 목표가 있고 무엇을 해야 하는지 알게 되면 딥러닝이 조금 더 재밌어질 것 같다.

+ Recent posts