안녕하세요 Web of Think 입니다.

몇 년만에 작성하는 포스트인지 모르겠네요.

 

대략 5년여 전부터 web technology 기반으로 사물인터넷 세상(IoT world)이 구현될 것이라는 상상을 블로그에 적어 내려가기 시작했습니다. 이후로 많은 시간이 지나 관심사도 많이 변했지만, 컨넥티드 사물들(connected things)를 실생활에서 만날 기회가 생길 때마다 이렇게 다시 글을 쓰게 되네요.  

 

오늘은 제 인생의 첫 차와 예약 구매로 아직 제품이 도착하지 않은 블랙박스, 그리고 NB-IoT 모듈에 관한 이야기를 적어 보려고 합니다.

 

 

가장 먼저 필요한 준비물은 흔하게 볼 수 있는 연결끊긴 이동형 기기(unconnected mobile device) 입니다. (아래 사진은 관련이 있습니다) 

내가 처음 산 반자동으로 움직이는 쓸데없이 비싼 기기 -  My first (semi-)automobile (K5 Lpi 모델)

테슬라조차 아직까지 완전하 자동으로 움직이지 않지만 자동차라는 별명으로 알려진 이 기기는 놀랍게도 이미 백 년 전에 1.5시간 만에 완성시킬 수 있는 생산 최적화(e.g., 모델-T)를 이뤄 냈습니다. Oh My Ford! 그럼에도 불구하고, 원더키디 2020년 현재 코로나19와 같은 소소한(?) 이유로 배송하는 데만 일주일이 넘게 걸리는 마법(?)을 보여줍니다. 

대중교통 운행에 확실히 영향을 받은 것 같습니다. (참고: 구글맵)

 

사족이지만 더욱 놀라운 사실(!)은 이 기기를 구매하는 대부분의 사람들은 몇 가지에 불과한(?) 옵션 만을 선택할 수 있음에도 불구하고, 제품을 받기까지 길게는 7주까지 기다리는 인내력을 발휘한다고 하더군요. 전 어떻게든 빠르고 싸게 사겠다는 욕망에 영맨의 제고 관리 프로그램에서 우연히 발견한 하나뿐인 재고(?) 신차를 줍줍하였습니다. 이 기기의 추가 옵션에 블랙박스나 네비처럼 커넥티드 서비스(connected service)를 끼워팔기(bundling)하지만, 제가 7주의 인내력을 가질 수 없는 상황이기도 하고 비싸기도 해서 과감히 제외하고 요즘 신차 기본 옵션이라 불리는 슬기 운전(드라이브 와이즈) 구성만을 장착해 둔 것을 장바구니에 넣었습니다.     

 

오랫만에 글을 쓰니 (사실은 차를 처음 사보니) 사설이 길었네요. 양해 바랍니다;; 

 

두 번째 준비물은 어쩌다 찾은 할인 예약 구매한 제품인 아이나비 FXD5000 입니다.  (아래 광고 사진 및 야간 촬영 영상)

 

선택의 배경은 이렇습니다. 장기렌트로 빌려 타던 차량에는 (영맨이 서비스로 부착해 준) 어떤 블랙박스가 있었는데 촬영 품질이나 사용성 모두 너무 나빠서 차를 구매하면 절대 번들링하지 않겠다고 생각했습니다. 그래서, 서비스를 마다하고 직접 알아보았는데 블랙박스 제품들의 촬영 품질과 사용성을 검토해보니 품질이 조금 좋아질 수록 가격이 많이 오르더군요. (귀차니즘과 후회의 연속) 그래서 아직 변변한 사용기조차 없지만 제작사가 공개한 주행 영상이 보여주는 품질만 보고 예약 구매에 낚길 수 밖에 없었습니다. 

 

사실 블랙박스의 품질보다도 개인적인 바램이 자동차가 여전히 스스로 움직이지 못하더라도, 스마트폰으로 간접적인 대화라도 할 수 있었으면 했습니다. 특히 스마트 앱의 핵심은 유지보수라는 점에 착안해서 소프트웨어(도) 직접 만드는 금방 망하지 않을 것 같은 브랜드의 블랙박스를 선택하게 된 거죠.

 

FXD5000 언박싱입니다. 예약 주문 배송 건이라 출발 메시지는 늦었지만 정말 신속하게 배송되었네요.

예약 구매라서 GPS 안테나와 무료 설치 쿠폰이 딸려 옵니다.
패키지 전면 샷입니다. 파검 듀얼 톤인 패키지 디자인은 밋밋하고 작은 글자은 잘 안 보입니다. 아이나비 connected 라는 우측 상단의 브랜드(?)에 맞지 않게 실제 connected 는 옵션입니다 (connectable?). 잘 안보이지만 스펙을 적은 문단을 마지막을 보면 별매라고 적혀 있네요.
박스를 열면 정말 카메라처럼 생긴 블랙박스가 한켠에 자리 잡고 있습니다. 

 

마지막 준비물로 이번 포스트의 가장 핵심 모듈이지만 의외로 별매인 '커넥티드 PKG' 입니다.

앞서 블랙박스 예약 구매 할인 혜택으로 아낀 비용을 투자해서 2년 간 NB-IoT 통신 요금을 포함한 핵심 장치를 위메프에서 다나외 최적가인 6만원 미만으로 구매하였습니다. 이 장치를 달아야만 '아이나비 커넥티드'라는 모바일 앱으로 원격 접속이 가능해 진다네요. 

이 놀라운 물건은 LG U+에서 제공하는 통신망에 연결할 수 있는 유심을 포함한 일종의 모뎀에 불과한데 무료 기간인 2년이 지나고 계속 사용하면 매년 2만원 정도의 통신료가 청구됩니다. (아니면 그냥 GPS로 동작) 통신망의 빠른 발전으로 스마트폰 구매 주기가 2년 여에 불과했었는데 이런 통신 장치 역시 이런 방식으로 빠르게 발전해 갈지가 제 관전 포인트입니다.

이 장치와 연동되는 소물인터넷 통신망의 대역폭에 맞춰 가격과 앱이 제공하는 기능 - 고속(Pro)과 저속(Standard) -이 꽤 다릅니다. 자세한 내용은 판매자의 블로그 링크에 잘 정리되어 있네요.

역시 곁다리이지만 여기에 쓰인 NB-IoT 기술과 M2M 네트워크 기술들에 대한 내용은 이 블로그에서 잘 정리하고 있습니다. 

 

이것이 바로 NB-IoT 통신 동글 standard 버전 입니다.

 

이제 본격적으로 조립기를 작성해야 할 시점이지만 블랙박스 설치는 관심사가 아니므로(?) 윈도우 틴팅을 하면서 겸사 겸사 설치를 위탁했네요..;;;

거의 루마 버텍스 900에만 있는 투명도 80 필름으로 전면 틴팅을 했는데도 지하 주차장 때문인지 잘 보이지 않네요. 가운데 흰 빛이 블랙박스가 동작하고 있음을 나타냅니다.

 

가까이서 찍으니 다행히(?) 잘 보입니다. 

이제 본격적인 설치를 해봅니다.

패키지에는 통신용 유심과 통신 모듈이 포함되어 있습니다.

NANO USIM 칩을 통신 모듈 후면에 장착하고 GPS 연결 단자와 연결하면 끝인 줄 알았습니다.

이제 등록 절차를 통해 단말기와 스마트폰을 연동시켜야 합니다.

블랙박스 설명서에서 커넥티드 모듈에 대한 설명이 부족하여 몇 가지 의구심이 생기는 문제 단계에 진입했습니다.

  • 검색 결과에 따르면, 이전 제품들에서는 GPS 신호 픽토그램(pictogram)과 네트워크 연결 픽토그램이 각각 표시되는 것 같은데 이 제품은 GPS 신호로 보이는 픽토그램만 계속 점멸됩니다. 네트워크 연결 픽토그램이 나타나지 않으니 커넥티드 모듈 혹은 유심칩 연결에 문제가 있는 건지 모르겠네요.
  • 설명서에 따르면 몇 분 내에 네트워크에 연결된다고 하는데 생각보다 꽤 걸리더군요. 아마 지하 주차장에서의 통신 상황이 여의치 않아 그럴 수 있겠다는 생각이 듭니다.
  • 등록하라는 팝업 창이 나타났는데 확인을 누르니 다시 이전 상태로 돌아 갑니다. 여전히 네트워크 연결 픽토그램이 보이지 않아서 네트워크에 연결된 건지 알기 어렵습니다. 

등록은 아이나비 커넥티드 앱에서 진행해야 합니다.

아이나비 커넥티드 앱을 스마트폰에 설치하고 로그인하면 위와 같이 등록할 수 있는 메뉴가 나타납니다. 블랙박스 등록 버튼을 누르면 유심칩에 기재된 전화번호(?)를 입력해 등록을 진행할 수 있습니다. 등록 절차는 스마트 폰에서 요청하고 블랙박스 디스플레이에서 확인하는 블루투스 같은 프로세스입니다.

블루투스와 확실히 다른 점(?)은 근거리가 아닌 원거리 통신 프로토콜(NB-IoT)을 쓴다는 점이죠.  

 

여기서 블랙박스의 다양한 사용사례(?)에 대한 의문점들이 떠오릅니다.

  • 블랙박스 등록 시, 앱에서 로그인한 계정만 블랙박스에 연결할 수 있는가? 다시 말하면 여러 계정이 하나의 블랙박스를 공유할 수 없나?
  • 블랙박스 등록 계정을 해지하고 다시 등록할 수 있는가?
  • 블랙박스의 촬영 데이터 사용자 구분 및 개인정보 보호가 가능한가?  

첫 번째, 패밀리 카처럼 운전자가 두 명 이상일 경우, 가족 계정이라는 개념이 필요합니다. 즉, 사건 사고  발생 시 알람을 공유할 수 있으면 보다 빠른 조치가 가능할 것으로 보입니다.

반면 두 번째는 사용 중인 블랙박스를 재판매하거나 이전하는 경우입니다. 스마트폰이 그러하듯 여기에도 초기화라는 개념이 필요합니다.

세 번째는 앞서 든 두 가지의 사례의 확장인데 운전정보보호 개념입니다. 운전 중에 블랙박스로 촬영된 영상이 동선을 노출하기 때문에 프라이버시에 민감한 분들을 위해 이를 보호할 방법이 필요합니다. 더불어 앞서 다중 운전자의 사례처럼 기존의 영상 구분 방법 외에도 운전자에 따라 촬영 데이터가 분리될 필요성도 있습니다. 

쓰다보니 많이 엇나간 듯 하지만 한줄 정리하면, 커넥티드 기기가 되면, 기존에 없던 사용 사례들이 가능해지고 이에 따른 고려사항들이 생긴다는 것입니다. 유선 전화기와 데스크톱 PC에서 진화하여 스스로는 못 움직이지만 움직이게 된 스마트폰 역시 그런 과정을 통해 오늘에 이른 것입니다.

 

스마트폰, 블랙박스와 통하다.

 

다시 사용기로 돌아와서, standard 버전에서는 위와 같이 블랙박스 전원 on/off 버튼, 차량 배터리 전압 표시, 그리고 현재 날씨 정보가  제공됩니다. '나의 주행'이나 '실시간' 감시와 같은 고급(?) 기능은 pro 버전을 쓰라고 되어 있고요. 그리고 standard 버전은 충격이나 충돌 이벤트 시 전면 카메라의 스틸샷(still shot)이 핸드폰에 전송됩니다. 문을 닫았을 뿐인데도 충격 이벤트가 오네요. 정밀도(precision)보다 재현율(recall)에 맞춰 셋팅된 것으로 보입니다. 아무래도 블랙 박스를 달았는데 사건 사고 발생 시에도 기록이 없다면 소송감(?)일테니 말이죠. 

 

스마트 폰 앱은 pro 버전 사용자의 사용자 경험에 최적화한 것 같습니다. 차라리 standard 버전을 기준으로 설계하고 pro 버전은 별도 앱 혹은 옵션에서 확장할 수 있도록 하는게 낮지 않을까 싶네요. 나를 기준으로 맞춰주세요.  

차량에 설치된 블랙박스의 기능과 UI는 크게 도드라지지도 떨어지지도 않는 것 같습니다. full HD 영상이지만 디스플레이의 터치 반응 속도도 빨라서 블랙박스에서도 빠르게 촬영된 이벤트들을 검토해볼 수 있었습니다.

 

지금까지 긴 글 읽어 주셔서 감사합니다. 총평은 자잘한 아쉬움(?)이 있긴 하지만 오래된 아이나비 브랜드에 걸맞는 완성도를 보여준다고 할 수 있을 것 같네요. 더욱 진화되어 connectable이 connnected one에서 connected all이 되면 좋겠네요.  

역설적이게도 보험처럼 돈 낭비(?)가 되겠지만 부디 아무런 사건 사고 없이 블랙박스를 뒤질 일 없이 안전 운행하길 기원해 봅니다.  

     

  

p.s. 앞서 밝힌 것과 다르게 글을 쓰는 동기가 순수하지만은 않아 늦었지만 양심 고백(?)합니다. 사실 예약구매 사은품 조건으로 후기 작성 시 주유권(충전도 가능) 제공 이벤트가 있길래 이렇게 후기도 아닌데 서둘러 글을 썼네요. 놀랍게도!! 이벤트 담당자 분께서 업데이트 약속을 믿고 미리 모바일 상품권을 보내 주셨습니다!!! ㅠ.ㅠ 따라서, 전 아니지만 다른 유명 블로그들처럼 이 포스트는 아이나비의 사은품 후원을 받았음을 밝힙니다.         

피아노를 하나 산 김에 구매 후기를 남기려 블로그에 글을 쓰기 시작합니다.

(로그인을 하니 휴먼계정이라고 나와서 일년 넘게 로그인조차 안한 방치 블로거였음을 깨닫습니다. ^^;)

 

저는 어릴 적 부모님 등쌀에 못이겨 피아노를 배우긴 했으나 숙련을 위해 반복하는 것을 힘들어서 서둘러 포기했었습니다.

음악을 매우 좋아하지만 잘 하지 못해서 "연주는 하는 것이 아니라 듣는 것"으로 여기며 살아왔습니다.

그래서 피아노라는 물건은 정작 제겐 필요없었습니다. 

 

시간이 흘러 마냥 어릴 것 같았던 제 아이들도 커서 피아노를 배우기 시작했습니다.   

첫째인 딸 아이가 피아노 학원을 다니면서 익힌 자신의 연주(?) 실력을 보여주며 집에서도 더 연습하고 싶다는 의지를 피력하길래 몇차례 아내의 요구에도 미뤄두었던 피아노를 저질렀습니다.

 

가격, 방음 및 공간의 문제 등으로 피아노보다 디지털 피아노를 우선적으로 고려했지만, 그럼에도 불구하고 피아노의 감성(생김새, 울림, 키 감 등)은 갖추길 원했습니다. 아이가 원하는 깔끔한 색깔을 제공하는 모델인 것도 중요한 요소였습니다. 따라서, 가격대가 조금 비싸더라도 계속 치고 싶은 피아노를 구매하려고 했습니다.

 

이번에 구매한 KAWAI CN-27 디지털 피아노(Premium Satin White 모델)는 입문용 수준이지만 피아노의 감성을 충분히 표현하는 제품으로 보였습니다. 특히 피아노 키감을 흉내내기 위해 적용한 RH-III 건반 구조는 역학적으로도 매우 흥미로웠습니다. 실제 눌러보았을 때 느껴지는 감성은 "진정한 터치, 아름다운 사운드, 감동적인 기능, 뛰어난 품질"이라는 선전문구 같이 진정 피아노처럼 동작하는 피아노이길 바라는 마음이 구현된 디지털 피아노가 아닌가 싶습니다.

 

처음 살펴본 100만원 이하 모델들은 가격을 낮추기 위해 피아노라기 보다 키보드의 느낌이 나는 구성이지만 이 제품은 피아노의 외관과 의자, 키 터치감 및 페달까지 피아노의 감성을 완벽하게 구현하고 있습니다.

 

 

 

제가 피아노를 칠 수 없기 때문에 내장된 체르니 악보를 자동 연주 시킨 것을 핸드폰으로 촬영해 보았습니다.

영상의 소리가 좋지 않아서 음성 녹음으로 저장한 파일도 첨부해 봅니다. 궁금하신 분은 들어보세요.       

cn-27_auto_play.m4a
0.26MB

 

마지막으로 딸 아이가  연습하는 장면입니다.

 

딸아이가 아빠와는 다르게 스스로 원하는 연주하면서 소소한 행복함을 느끼기를 희망합니다.

제게 그랬듯 음악이 인생의 좋은 동반자가 되어주길 소망합니다.   

 

자세한 구매 관련 정보는 http://peacemusic.kr/product/detail.html?product_no=4392&cate_no=832&display_group=1 를 참고하시기 바랍니다.

 

자녀에게 혹은 스스로에게 음악을 선물해주고픈 분들에게 조금이나마 도움이 되는 후기가 되었기를 바랍니다.

읽어주셔서 감사합니다. 

안녕하세요.

정말 오랫만에 블로그를 업데이트하네요. 


이번에 소개하는 논문인 조건부 뉴럴 프로세스(CNP)는 구글 딥마인드 팀에서 ICML 2018에 제출한 논문으로써 기존 Gaussian Processes 가 kernel engineering이 필요한 반면 CNP는 이 제약이 없이 데이터셋에 내재된 특성을 학습하여 확률 모델링(probabilistic modeling)을 수행할 수 있어 베이지안 최적화 (Bayesian Optimization)을 위한 방법으로 사용할 수 있을 것이라는 기대를 가지고 읽은 논문입니다.


읽고 보니 이 방법은 요즘 관심이 집중되고 있는 연구주제인 '메타러닝 (meta-learning)'과 일맥상통하며 논문의 실험에도 메타러닝에 사용된 데이터셋이 벤치마크로 사용되었더군요.  

아래 튜토리얼 코드는 논문을 쉽게 이해할 수 있도록 제공한 노트북으로 코드를 자세히 이해하는 차원에서 한글로 번역해 보았습니다.

관심있는 분들에게 도움이 되길 바랍니다.   





Copyright 2018 Google LLC

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


1차원 회귀 문제를 위한 조건부 뉴럴 프로세스 (CNP)

Conditional Neural Processes (CNPs)는 Generative Query Networks (GQN) 의 일환으로 장면 표현방식(scene rendering)을 넘어서 자체의 훈련 방식을 확장한 문제들 (예, 회귀/분류 문제)에 적용하기 위해 등장했습니다.

단 하나의 함수를 근사(approximate)하는 일반적인 뉴럴넷과 다르게 CNP는 함수들의 분포를 근사하기 위해 학습합니다. 그 결과, 평가 시에 CNP는 적은 수의 관찰 결과들을 제공했을 때 학습된 분포에서 어떤 함수나 근사할 수 있게 됩니다. 더불어, CNP는 데이터셋에서부터 이런 예측이 가지는 불확실성을 평가할 수 있도록 학습하고 관찰 수가 증가하게 되면 이 불확실성은 줄어들고 예측력이 향상됩니다.

여기서는 CNP가 어떻게 다른지를 설명하고 결과 모델을 랜덤 함수들로 생성한 데이터셋으로 학습한 1차원 회귀 문제에 적용해 봅니다.

CNP에 대한 궁금한 점이 있거나 이 문서에 대한 피드백이 있다면 garnelo@google.com 으로 연락주시기 바랍니다..

CNP 구현

먼저 이 코드를 수행하기 위해 필요한 패키지들(numpy, tensorflow, mathplotlib 등)을 임포트합니다.

In [0]:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import collections

학습 데이터

CNP의 가장 핵심 요소는 테스트 시점에서의 유연성입니다. 이 알고리즘은 모든 범위의 함수들을 모델링할 수 있고 관찰 데이터 수가 증가하면 알고리즘의 예측 범위를 줄여 갑니다.이 데이터셋으로 CNP 학습 방식의 결과로써 이런 행태가 나타날 것입니다.

보편적인 기계학습에서 그렇듯 한 함수에서 얻은 관찰 값들을 사용해서 학습하기 보다는 내재된 특성을 공유하는 서로 다른 함수들을 구성해서 만든 데이터를 사용할 예정입니다. 위의 그림은 이를 도식화한 것입니다. 왼쪽 그림은 전통적인 학습 방식에 관련된 그림인데, 회색 선은 한 개의 내재된 정답 함수(a single underlying ground truth function)을 나타내고, 여러 다른 색으로 표시한 점들은 매번 학습할 때마다 이 함수에서 나온 적은 수의 샘플들을 표현한 것입니다.

오른쪽 그림은 뉴럴넷을 학습할 때 사용할 수 있는 데이터셋의 예시를 보여줍니다. 이 데이터셋에는 함수 하나 대신에 모델링하기를 원하는 함수 클래스에 속한 많은 수의 함수들로 구성되어 있습니다. 매 반복(iteration)마다, 데이터셋에서 임의로 하나의 함수를 선택하고, 학습하기 위해 그 함수에서 관찰 값 몇 개를 뽑습니다. 그리고 다음 반복 시점에는 이 함수를 되돌려 놓고 데이터셋에서 새로운 함수 하나를 뽑은 후 학습 데이터를 선택하기 위한 새로운 함수로 사용합니다. 이런 데이터셋의 형태는 모델이 특정 함수에 과적합(overfit)되지 않도록 막고, 그 대신 함수들의 분포를 학습하게 됩니다. 또한 이런 계층적인 데이터셋 아이디어가 현재의 메타러닝 방법들의 핵심에 맞닿아 있습니다.

다음과 같은 데이터셋들이 예시가 될 수 있습니다:

  • 세계 여러 도시들의 시간에 따른 온도 변화를 표현하는 함수들
  • 다양한 인간의 걸음 걸이에 대해 모션 캡쳐로 생성한 데이터셋
  • 이 예시처럼 어떤 한 커널을 이용해 가우시안 프로세스(GP)로 생성한 여러 함수들

이 예시에서는 GP를 선택해서 데이터를 만들었는데 이 방식은 어떤 내재적인 특성들(여기에서는 커널 함수)을 공유하는 부드러운 곡선들을 샘플링하기 쉬운 방식으로 구성되어 있기 때문입니다. 덧붙여 이 예제를 위한 데이터 생성 이외에 뉴럴넷으로 구현되는 뉴럴 프로세스는 커널 또는 GP를 이용하지 않습니다.

데이터 생성기

다음 장에는 함수들의 데이터셋을 생성하기 위해 GP로 학습 및 평가 데이터셋을 생성하기 위한 코드를 제공합니다. 다음에 설명하겠지만, CNP는 매 반복마다 두 가지의 부분집합을 사용하는데 하나는 문맥(context)을 제공하기 위함이고 남은 하나는 목표(target)를 위함입니다. 우리는 새로운 추가적인 데이터 값들로써 문맥 정보 값을 목표 값과 함께 포함시키는 것이 실질적으로 훈련에 도움이 되는 것을 발견했습니다. 이 데이터 생성기는 이렇게 생성 데이터를 두 집단으로 나누고 올바른 포멧으로 제공합니다.

In [0]:
# The CNP takes as input a `CNPRegressionDescription` namedtuple with fields:
#   `query`: a tuple containing ((context_x, context_y), target_x)
#   `target_y`: a tesor containing the ground truth for the targets to be
#     predicted
#   `num_total_points`: A vector containing a scalar that describes the total
#     number of datapoints used (context + target)
#   `num_context_points`: A vector containing a scalar that describes the number
#     of datapoints used as context
# The GPCurvesReader returns the newly sampled data in this format at each
# iteration

CNPRegressionDescription = collections.namedtuple(
    "CNPRegressionDescription",
    ("query", "target_y", "num_total_points", "num_context_points"))


class GPCurvesReader(object):
  """Generates curves using a Gaussian Process (GP).

  Supports vector inputs (x) and vector outputs (y). Kernel is
  mean-squared exponential, using the x-value l2 coordinate distance scaled by
  some factor chosen randomly in a range. Outputs are independent gaussian
  processes.
  """

  def __init__(self,
               batch_size,
               max_num_context,
               x_size=1,
               y_size=1,
               l1_scale=0.4,
               sigma_scale=1.0,
               testing=False):
    """Creates a regression dataset of functions sampled from a GP.

    Args:
      batch_size: An integer.
      max_num_context: The max number of observations in the context.
      x_size: Integer >= 1 for length of "x values" vector.
      y_size: Integer >= 1 for length of "y values" vector.
      l1_scale: Float; typical scale for kernel distance function.
      sigma_scale: Float; typical scale for variance.
      testing: Boolean that indicates whether we are testing. If so there are
          more targets for visualization.
    """
    self._batch_size = batch_size
    self._max_num_context = max_num_context
    self._x_size = x_size
    self._y_size = y_size
    self._l1_scale = l1_scale
    self._sigma_scale = sigma_scale
    self._testing = testing

  def _gaussian_kernel(self, xdata, l1, sigma_f, sigma_noise=2e-2):
    """Applies the Gaussian kernel to generate curve data.

    Args:
      xdata: Tensor with shape `[batch_size, num_total_points, x_size]` with
          the values of the x-axis data.
      l1: Tensor with shape `[batch_size, y_size, x_size]`, the scale
          parameter of the Gaussian kernel.
      sigma_f: Float tensor with shape `[batch_size, y_size]`; the magnitude
          of the std.
      sigma_noise: Float, std of the noise that we add for stability.

    Returns:
      The kernel, a float tensor with shape
      `[batch_size, y_size, num_total_points, num_total_points]`.
    """
    num_total_points = tf.shape(xdata)[1]

    # Expand and take the difference
    xdata1 = tf.expand_dims(xdata, axis=1)  # [B, 1, num_total_points, x_size]
    xdata2 = tf.expand_dims(xdata, axis=2)  # [B, num_total_points, 1, x_size]
    diff = xdata1 - xdata2  # [B, num_total_points, num_total_points, x_size]

    # [B, y_size, num_total_points, num_total_points, x_size]
    norm = tf.square(diff[:, None, :, :, :] / l1[:, :, None, None, :])

    norm = tf.reduce_sum(
        norm, -1)  # [B, data_size, num_total_points, num_total_points]

    # [B, y_size, num_total_points, num_total_points]
    kernel = tf.square(sigma_f)[:, :, None, None] * tf.exp(-0.5 * norm)

    # Add some noise to the diagonal to make the cholesky work.
    kernel += (sigma_noise**2) * tf.eye(num_total_points)

    return kernel

  def generate_curves(self):
    """Builds the op delivering the data.

    Generated functions are `float32` with x values between -2 and 2.
    
    Returns:
      A `CNPRegressionDescription` namedtuple.
    """
    num_context = tf.random_uniform(
        shape=[], minval=3, maxval=self._max_num_context, dtype=tf.int32)

    # If we are testing we want to have more targets and have them evenly
    # distributed in order to plot the function.
    if self._testing:
      num_target = 400
      num_total_points = num_target
      x_values = tf.tile(
          tf.expand_dims(tf.range(-2., 2., 1. / 100, dtype=tf.float32), axis=0),
          [self._batch_size, 1])
      x_values = tf.expand_dims(x_values, axis=-1)
    # During training the number of target points and their x-positions are
    # selected at random
    else:
      num_target = tf.random_uniform(
          shape=(), minval=2, maxval=self._max_num_context, dtype=tf.int32)
      num_total_points = num_context + num_target
      x_values = tf.random_uniform(
          [self._batch_size, num_total_points, self._x_size], -2, 2)

    # Set kernel parameters
    l1 = (
        tf.ones(shape=[self._batch_size, self._y_size, self._x_size]) *
        self._l1_scale)
    sigma_f = tf.ones(
        shape=[self._batch_size, self._y_size]) * self._sigma_scale

    # Pass the x_values through the Gaussian kernel
    # [batch_size, y_size, num_total_points, num_total_points]
    kernel = self._gaussian_kernel(x_values, l1, sigma_f)

    # Calculate Cholesky, using double precision for better stability:
    cholesky = tf.cast(tf.cholesky(tf.cast(kernel, tf.float64)), tf.float32)

    # Sample a curve
    # [batch_size, y_size, num_total_points, 1]
    y_values = tf.matmul(
        cholesky,
        tf.random_normal([self._batch_size, self._y_size, num_total_points, 1]))

    # [batch_size, num_total_points, y_size]
    y_values = tf.transpose(tf.squeeze(y_values, 3), [0, 2, 1])

    if self._testing:
      # Select the targets
      target_x = x_values
      target_y = y_values

      # Select the observations
      idx = tf.random_shuffle(tf.range(num_target))
      context_x = tf.gather(x_values, idx[:num_context], axis=1)
      context_y = tf.gather(y_values, idx[:num_context], axis=1)

    else:
      # Select the targets which will consist of the context points as well as
      # some new target points
      target_x = x_values[:, :num_target + num_context, :]
      target_y = y_values[:, :num_target + num_context, :]

      # Select the observations
      context_x = x_values[:, :num_context, :]
      context_y = y_values[:, :num_context, :]

    query = ((context_x, context_y), target_x)

    return CNPRegressionDescription(
        query=query,
        target_y=target_y,
        num_total_points=tf.shape(target_x)[1],
        num_context_points=num_context)

조건부 뉴럴 프로세스

CNP에서의 전방 학습 경로(forward pass)를 다음과 같이 표현할 수 있습니다:

drawing

위의 도식에서 표현된 것처럼 CNP는 문맥 데이터 값인 (x, y)i 쌍을 인코더(encoder) 를 통과시키고 통합기(aggregator)를 이용해 결합시킨 후, 독립적인 표현 값(representations) ri를 얻습니다. 결과적으로 표현된 r에는 목표 값 xT 의 위치가 섞여서 목표 위치에서 현재 예측치에 대한 불확실성에 대한 측정치로써 y의 평균 예측 값을 함께 반환하는 디코더(decoder) 를 지납니다.

따라서 CNP를 구현하는 것은 크게 세 가지 구성요소를 개발하는 것입니다:

  • 인코더
  • 통합기
  • 디코더

이 세 부분에 대한 상세한 설명은 아래의 코드와 함께 있습니다.

인코더

모든 맥락 데이터들 사이에 인코더 e 가 공유되어 있고 이 인코더는 작은 층 수를 지닌 다층 퍼셉트론(MLP)으로 구성되어 있습니다. 이 예제에서는 4층으로도 충분하지만, 코드에서는 나중을 위해 encoder_output_sizes 변수를 통해 그래프를 만드는 시점에서 계층의 수를 바꿀 수도 있습니다. 각각의 문맥 쌍 (x, y)i 은 인코딩 이후 개개의 표현 값 ri의 결과로 나타납니다. 이 표현 값들을 통합기 a를 이용해 모든 문맥 값들을 아울러 하나의 표현 값 r으로 결합됩니다.

여기에서는 통합기 a 를 인코더에 포함시켜서 구현했는데, 모든 데이터들의 평균을 취하는 것만 고려했기 때문입니다. 통합기가 생성한 표현 값 r에는 모든 문맥 데이터 값들에 의해 제공되어진 숨겨진 익명함수 f 에 관한 정보를 가지고 있습니다.

In [0]:
class DeterministicEncoder(object):
  """The Encoder."""

  def __init__(self, output_sizes):
    """CNP encoder.

    Args:
      output_sizes: An iterable containing the output sizes of the encoding MLP.
    """
    self._output_sizes = output_sizes

  def __call__(self, context_x, context_y, num_context_points):
    """Encodes the inputs into one representation.

    Args:
      context_x: Tensor of size bs x observations x m_ch. For this 1D regression
          task this corresponds to the x-values.
      context_y: Tensor of size bs x observations x d_ch. For this 1D regression
          task this corresponds to the y-values.
      num_context_points: A tensor containing a single scalar that indicates the
          number of context_points provided in this iteration.

    Returns:
      representation: The encoded representation averaged over all context 
          points.
    """

    # Concatenate x and y along the filter axes
    encoder_input = tf.concat([context_x, context_y], axis=-1)

    # Get the shapes of the input and reshape to parallelise across observations
    batch_size, _, filter_size = encoder_input.shape.as_list()
    hidden = tf.reshape(encoder_input, (batch_size * num_context_points, -1))
    hidden.set_shape((None, filter_size))

    # Pass through MLP
    with tf.variable_scope("encoder", reuse=tf.AUTO_REUSE):
      for i, size in enumerate(self._output_sizes[:-1]):
        hidden = tf.nn.relu(
            tf.layers.dense(hidden, size, name="Encoder_layer_{}".format(i)))

      # Last layer without a ReLu
      hidden = tf.layers.dense(
          hidden, self._output_sizes[-1], name="Encoder_layer_{}".format(i + 1))

    # Bring back into original shape
    hidden = tf.reshape(hidden, (batch_size, num_context_points, size))

    # Aggregator: take the mean over all points
    representation = tf.reduce_mean(hidden, axis=1)

    return representation

디코더

표현 값 r 를 학습을 통해 얻은 후 각각의 목표 값 xt 를 합쳐서 디코더 d를 통해 전달합니다. 인코더 e 처럼 모든 목표 데이터 값들 사이에 디코더 d가 공유되어 있고 이 디코더는 decoder_output_sizes 변수로 계층 수가 결정되는 작은 MLP로 구성되어 있습니다. 디코더는 모든 목표 값들 xt에 대한 평균 μt 과 분산 σt을 출력합니다. 예측된 평균 μt과 분산 σt으로 결정되는 가우시안 (Gaussian)에서의 정답 값 yt의 로그 우도 (log likelihood)를 사용해 CNP를 학습합니다. 여기 구현된 버전은 붕괴 현상(collapsing)를 막기 위해 분산 σt 를 0.1에서 잘라 버립니다.

In [0]:
class DeterministicDecoder(object):
  """The Decoder."""

  def __init__(self, output_sizes):
    """CNP decoder.

    Args:
      output_sizes: An iterable containing the output sizes of the decoder MLP 
          as defined in `basic.Linear`.
    """
    self._output_sizes = output_sizes

  def __call__(self, representation, target_x, num_total_points):
    """Decodes the individual targets.

    Args:
      representation: The encoded representation of the context
      target_x: The x locations for the target query
      num_total_points: The number of target points.

    Returns:
      dist: A multivariate Gaussian over the target points.
      mu: The mean of the multivariate Gaussian.
      sigma: The standard deviation of the multivariate Gaussian.
    """

    # Concatenate the representation and the target_x
    representation = tf.tile(
        tf.expand_dims(representation, axis=1), [1, num_total_points, 1])
    input = tf.concat([representation, target_x], axis=-1)

    # Get the shapes of the input and reshape to parallelise across observations
    batch_size, _, filter_size = input.shape.as_list()
    hidden = tf.reshape(input, (batch_size * num_total_points, -1))
    hidden.set_shape((None, filter_size))

    # Pass through MLP
    with tf.variable_scope("decoder", reuse=tf.AUTO_REUSE):
      for i, size in enumerate(self._output_sizes[:-1]):
        hidden = tf.nn.relu(
            tf.layers.dense(hidden, size, name="Decoder_layer_{}".format(i)))

      # Last layer without a ReLu
      hidden = tf.layers.dense(
          hidden, self._output_sizes[-1], name="Decoder_layer_{}".format(i + 1))

    # Bring back into original shape
    hidden = tf.reshape(hidden, (batch_size, num_total_points, -1))

    # Get the mean an the variance
    mu, log_sigma = tf.split(hidden, 2, axis=-1)

    # Bound the variance
    sigma = 0.1 + 0.9 * tf.nn.softplus(log_sigma)

    # Get the distribution
    dist = tf.contrib.distributions.MultivariateNormalDiag(
        loc=mu, scale_diag=sigma)

    return dist, mu, sigma

모델

이제 지금까지 정의한 CNP의 세 가지 구성요소(인코더, 디코더, 통합기)를 하나의 모델로 조립해 봅시다. 근본적으로 이 모델은 오직 두 가지 주된 방법만 포함하면 됩니다:

  1. 목표의 예측 모델 내의 숨은 진리(ground truth) 값의 로그 우도를 반환하는 방법인데 이 방법을 훈련 과정에서 손실 함수(loss function)으로 취급할 것입니다.
  2. 테스트 시점(test time)에서 CNP를 질의하거나 평가하기 위해서 목표 지점에서의 예측 평균과 분산을 반환하는 방법입니다. 이 방법은 앞서 언급한 방법과 다르게 숨은 진리 목표 값에 의존하지 않아야 해서 따로 정의될 필요가 있습니다.
In [0]:
class DeterministicModel(object):
  """The CNP model."""

  def __init__(self, encoder_output_sizes, decoder_output_sizes):
    """Initialises the model.

    Args:
      encoder_output_sizes: An iterable containing the sizes of hidden layers of
          the encoder. The last one is the size of the representation r.
      decoder_output_sizes: An iterable containing the sizes of hidden layers of
          the decoder. The last element should correspond to the dimension of
          the y * 2 (it encodes both mean and variance concatenated)
    """
    self._encoder = DeterministicEncoder(encoder_output_sizes)
    self._decoder = DeterministicDecoder(decoder_output_sizes)

  def __call__(self, query, num_total_points, num_contexts, target_y=None):
    """Returns the predicted mean and variance at the target points.

    Args:
      query: Array containing ((context_x, context_y), target_x) where:
          context_x: Array of shape batch_size x num_context x 1 contains the 
              x values of the context points.
          context_y: Array of shape batch_size x num_context x 1 contains the 
              y values of the context points.
          target_x: Array of shape batch_size x num_target x 1 contains the
              x values of the target points.
      target_y: The ground truth y values of the target y. An array of 
          shape batchsize x num_targets x 1.
      num_total_points: Number of target points.

    Returns:
      log_p: The log_probability of the target_y given the predicted
      distribution.
      mu: The mean of the predicted distribution.
      sigma: The variance of the predicted distribution.
    """

    (context_x, context_y), target_x = query

    # Pass query through the encoder and the decoder
    representation = self._encoder(context_x, context_y, num_contexts)
    dist, mu, sigma = self._decoder(representation, target_x, num_total_points)

    # If we want to calculate the log_prob for training we will make use of the
    # target_y. At test time the target_y is not available so we return None
    if target_y is not None:
      log_p = dist.log_prob(target_y)
    else:
      log_p = None

    return log_p, mu, sigma

그림 함수 (Plotting function)

PLOT_AFTER 반복 시마다 중간 예측치를 그리는 함수를 정의합니다. 숨은 진리 곡선은 검은 점선으로 표시되고 모델에 투입되는 문맥 값들은 이 곡선 상의 검은 점들처럼 표현됩니다. 모델의 예측 평균과 분산은 -2와 2 간격 사이의 목표 값들의 범위를 위해 파란색으로 표시됩니다.

In [0]:
def plot_functions(target_x, target_y, context_x, context_y, pred_y, var):
  """Plots the predicted mean and variance and the context points.
  
  Args: 
    target_x: An array of shape batchsize x number_targets x 1 that contains the
        x values of the target points.
    target_y: An array of shape batchsize x number_targets x 1 that contains the
        y values of the target points.
    context_x: An array of shape batchsize x number_context x 1 that contains 
        the x values of the context points.
    context_y: An array of shape batchsize x number_context x 1 that contains 
        the y values of the context points.
    pred_y: An array of shape batchsize x number_targets x 1  that contains the
        predicted means of the y values at the target points in target_x.
    pred_y: An array of shape batchsize x number_targets x 1  that contains the
        predicted variance of the y values at the target points in target_x.
  """
  # Plot everything
  plt.plot(target_x[0], pred_y[0], 'b', linewidth=2)
  plt.plot(target_x[0], target_y[0], 'k:', linewidth=2)
  plt.plot(context_x[0], context_y[0], 'ko', markersize=10)
  plt.fill_between(
      target_x[0, :, 0],
      pred_y[0, :, 0] - var[0, :, 0],
      pred_y[0, :, 0] + var[0, :, 0],
      alpha=0.2,
      facecolor='#65c9f7',
      interpolate=True)

  # Make the plot pretty
  plt.yticks([-2, 0, 2], fontsize=16)
  plt.xticks([-2, 0, 2], fontsize=16)
  plt.ylim([-2, 2])
  plt.grid('off')
  ax = plt.gca()
  ax.set_axis_bgcolor('white')
  plt.show()

조건부 뉴럴 프로세스 수행

지금까지의 과정을 통해 데이터셋 뿐 아니라 모델 및 그 구성요소들로 그래프를 만들 수 있는 모든 것이 준비되었습니다. 하지만, 학습을 시작하기 전에 몇 가지 변수들을 더 설정해야 합니다:

  • TRAINING_ITERATIONS - 학습 과정에서 얼마나 많은 횟수의 반복을 할 지를 결정. 매 반복마다 GP를 통해 함수들의 새로운 한 묶음(batch)를 샘플링해서 곡선의 일부 값들을 문맥 값 (x, y)C으로 선정하고 또 다른 일부는 목표 값 (x, y)T 으로 선정합니다. 주어진 문맥 하에서 목표 값들의 평균과 분산을 예측하고 모델을 개선하기 위해 손실 값으로 진리 값의 로그 우도를 이용합니다.
  • MAX_CONTEXT_POINTS - 훈련 과정에서 사용할 문맥 값들의 최대 갯수를 결정 . 문맥 값들의 갯수는 매 반복마다 3과 MAX_CONTEXT_POINTS 사이의 임의의 값을 고르게 됩니다.
  • PLOT_AFTER - 중간 결과 값을 얼마나 자주 그릴지를 결정.
In [0]:
TRAINING_ITERATIONS = int(2e5)
MAX_CONTEXT_POINTS = 10
PLOT_AFTER = int(2e4)
tf.reset_default_graph()

훈련 데이터와 테스트 데이터 모두를 위해 데이터셋 리더(dataset reader)를 그래프에 포함시켰습니다. 앞서 언급한 것처럼 데이터셋은 이 실험을 위해 매 반복마다 GP에서 새로 추출한 함수 값들로 구성되어 있습니다. 이 경우에 훈련 데이터와 테스트 데이터의 주된 차이는 실험 셋에는 예측을 위해 적은 수의 목표 값들만을 포함시켰으나 테스트 셋에는 모든 곡선을 그릴 수 있도록 더 많은 목표 값들을 포함시켰다는 것입니다.

In [0]:
# Train dataset
dataset_train = GPCurvesReader(
    batch_size=64, max_num_context=MAX_CONTEXT_POINTS)
data_train = dataset_train.generate_curves()

# Test dataset
dataset_test = GPCurvesReader(
    batch_size=1, max_num_context=MAX_CONTEXT_POINTS, testing=True)
data_test = dataset_test.generate_curves()

이제 그래프에 모델을 추가하고 초기화 루틴(initializer)과 훈련 반복 횟수 (train step)를 정해서 마무리 합니다.

In [0]:
# Sizes of the layers of the MLPs for the encoder and decoder
# The final output layer of the decoder outputs two values, one for the mean and
# one for the variance of the prediction at the target location
encoder_output_sizes = [128, 128, 128, 128]
decoder_output_sizes = [128, 128, 2]

# Define the model
model = DeterministicModel(encoder_output_sizes, decoder_output_sizes)

# Define the loss
log_prob, _, _ = model(data_train.query, data_train.num_total_points,
                       data_train.num_context_points, data_train.target_y)
loss = -tf.reduce_mean(log_prob)

# Get the predicted mean and variance at the target points for the testing set
_, mu, sigma = model(data_test.query, data_test.num_total_points,
                     data_test.num_context_points)

# Set up the optimizer and train step
optimizer = tf.train.AdamOptimizer(1e-4)
train_step = optimizer.minimize(loss)
init = tf.initialize_all_variables()

이제 모델을 훈련시킬 모든 준비가 끝났습니다! 아래 코드는 훈련 과정 중에 중간 예측 값들의 일부를 출력해서 어떻게 모델이 변해 가는지 볼 수 있게 했습니다.

PLOT_AFTER 반복 시마다 예측 분포 하에서 진리 목표 값의 로그 우도에 해당하는 손실 값을 출력하였습니다. 모델이 학습된다면 이 손실 값은 줄어들어야 합니다. 더불어, 진리 곡선에 비교하여 이 반복 지점에서의 CNP가 만들어낸 맥락 데이터 값들과 모델의 예측력을 도식화하게 됩니다.

In [0]:
with tf.Session() as sess:
  sess.run(init)

  for it in range(TRAINING_ITERATIONS):
    sess.run([train_step])

    # Plot the predictions in `PLOT_AFTER` intervals
    if it % PLOT_AFTER == 0:
      loss_value, pred_y, var, target_y, whole_query = sess.run(
          [loss, mu, sigma, data_test.target_y, data_test.query])

      (context_x, context_y), target_x = whole_query
      print('Iteration: {}, loss: {}'.format(it, loss_value))

      # Plot the prediction and the context
      plot_functions(target_x, target_y, context_x, context_y, pred_y, var)
Iteration: 0, loss: 1.58132648468
Iteration: 20000, loss: 0.670665860176
Iteration: 40000, loss: 0.211461827159
Iteration: 60000, loss: 0.109301753342
Iteration: 80000, loss: 0.259903162718
Iteration: 100000, loss: 0.342098742723
Iteration: 120000, loss: -0.0120964311063
Iteration: 140000, loss: 0.0102357501164
Iteration: 160000, loss: 0.538827002048
Iteration: 180000, loss: 0.111666142941


안녕하세요.


최근 일 년간은 딥러닝에서의 초모수(hyperparameter) 최적화 방안 연구에만 매진하다보니 블로그를 방치해 두었네요.

그럼에도 불구하고 방문해 주셨던 분들께 감사를 표합니다.

  


일년만에 다시 아이들을 위한 소프트웨어 강연인 소.물.이 있어 

이렇게 발표 자료를 공유하는 겸사 겸사 블로그에 기록을 남깁니다.


강연의 요약은 다음과 같습니다:


일자: 2018년 5월 26일 오후 3시

장소: 태장마루 도서관

신청: https://www.suwonlib.go.kr/tm/html/03_culture/culture_view.asp?bd_seqn5=8928&lib_code_cul=TM


제목: 기계는 어떻게 생각할까? (How do Machines Think?)


생각하는 기계를 만들고 싶다는 생각은 언제 시작했을까요? 오늘날 인공지능이라고 부르는 기계들은 어떻게 동작하는 걸까요? 생각하는 기계는 과연 어떻게 우리의 미래를 바꿀까요? 꼬리를 무는 호기심에 아이들이 열어갈 희망찬 미래를 기대하며 재밌게 이야기하고자 합니다.

 





[Cloud ML Tutorial]:
Training CIFAR-10 Estimator Model with TensorFlow
for Hyperparameter Optimization



ABSTRACT

CIFAR-10 이미지 데이터를 분류하는 ResNet 모델을 텐서플로우(tensorflow) 고차원 API로 작성한 레퍼런스 코드를 분석하고 이 모델을 CPU, GPU, 구글 클라우드 플랫폼(GCP)의 기계학습 엔진(ML 엔진) 서비스에서 학습하는 방법을 기술한다. GCP에서 딥러닝의 초모수를 찾아주는 자동화 서비스인 초모수 미세조정(hyperparameter tuning) 기능을 사용하고 분산 환경을 이용해 효과적인 초모수 최적화 방법에 대한 논의한다.



INTRODUCTION

텐서플로우는 구글에서 개발한 오픈소스 딥러닝 프레임워크로써 다양한 프로그래밍 언어에서 텐서(Tensor)를 처리할 수 있는 API를 이용해 데이터 흐름 형태의 모델(Dataflow-like model)을 구현하면, 다양한 형태의(heterogeneous) 하드웨어에서 분산 처리할 수 있는 기능을 제공한다. 텐서는 임의의 다차원의 배열을 지칭하는데, 일반적인 컴퓨팅 기기에서는 메모리에 올려서 처리하기 힘들 정도로 매우 큰 크기(big data)를 갖거나 행렬 분해(matrix decomposition) 등의 연산을 처리하는데 오랜 시간이 걸리기 때문에 딥러닝을 활용하는데 걸림돌이 되었다. 텐서플로우는 이런 대용량 데이터를 효과적으로 다룰 수 있는 최적화 알고리즘을 제공할 뿐 아니라, 상이한 기기들로 이뤄진 분산 시스템에서도 모델을 효과적으로 학습하거나 활용할 수 있도록 설계하여 연구 뿐 아니라 실제 서비스에도 활용할 수 있다.


CIFAR-10은 캐나다 고등연구소(CIFAR)에서 이미지 분류(image classification problem) 문제 해결을 위해 수집해 정리한 데이터(labeled data)로 다양한 기계 학습(machine learning) 알고리즘의 성능을 비교하기 위한 벤치 마크(benchmark)로 빈번히 사용된다. 이 데이터 셋에는 비행기, 자동차, 배, 트럭, 새, 고양이, 사슴, 개, 개구리, 말 열 가지 종류의 사진이 각각 6천 장 씩 32x32 크기의 RGB 컬러 이미지(color image)로 편집되어 있다. 대부분 이중 5만 장을 학습 데이터(training data)로 만 장을 평가 데이터(test data)로 나눠 사용하는데 일반화 오류(generalization error)를 줄이기 위해 평가 데이터는 절대 알고리즘을 학습하는 데 사용하지 않고 오직 성능을 실험하는데만 사용한다. 필요한 경우 평가 데이터의 일부를 다시 유효성 확인 데이터(validation data)로 분리하여 교차 검증를 통해 모델의 과적합(overfitting)을 막거나 알고리즘의 초모수(hyperparameter)를 최적화하는데 사용하기도 한다.


여기서는 튜토리얼로 공개된 텐서플로우 레퍼런스 모델의 일부인 CIFAR-10 estimator를 상세 분석해 텐서플로우의 고차원 API를 통해 모델을 생성하는 방법과 분산 환경을 포함한 다양한 컴퓨팅 기기에서 학습을 수행하기 위한 방법을 이해하고자 한다. 이후로 관련 연구로써 학습 모델로 사용된 깊은 신경망(deep neural network - DNN)의 하나인 ResNet과 초모수 최적화 기법 등 딥러닝의 실제적 적용을 위해 필요한 기법들을 설명하고 튜토리얼 코드에 대해 구조 분석과 상세 분석을 수행한다. 해당 코드를 CPU, GPU, ML 엔진 등에서 각각 수행한 결과를 비교하고 학습 데이터에 최적화된 모델을 찾기 위한 초모수 최적화 알고리즘을 클라우드 환경을 이용해 수행하는 방법을 모색한다.



기계 학습은 주어진 과제 T를 학습하기 위해 경험 혹은 데이터 E 로 목표 성능 P 을 달성하기 위한 계산 통계학적 알고리즘(Tom Mitchell, 1997)이다. 이중 딥러닝이라고 불리는 심층 신경망(deep neural network)은 뉴런(neuron)이라고 불리는 비선형 처리 유닛(nonlinear processing units)으로 구성된 계층(layer)를 겹겹히 쌓고 학습 과정에서 알고리즘을 통해 뉴런들의 가중치(weights)를 적절히 업데이트되도록 만들어 데이터의 특성(feature)를 자동으로 학습해 가는 표현 학습(representation learning) 알고리즘의 하나이다. 표현 학습은 고차원의 데이터에서 정보를 인식하는 데 필요한 수준에 알맞는 저차원 표현(representation)으로 변환하는 정보 처리 알고리즘이다. 최근 활발하게 논의되고 있는 인공지능(AI)은 포괄적인 측면에서 추론(reasoning), 지식(knowledge), 계획(planning), 자연어 처리(natural language processing), 인지(perception) 및 물체 제어(manipulating objects)를 기계 스스로 할 수 있도록 만든 것을 의미하나, 엄밀히 말해서는 기계를 통해 정보를 처리할 수 있도록 만든 모든 알고리즘을 의미한다.


ResNet은 마이크로소프트 연구소에서 고안한 극단적으로 깊은 DNN 구조(extremely deep architecture)로써 2015년 이미지 구별 문제를 위한 벤치마크인 이미지넷(ImageNet)에서 무려 152층으로 구성한 아키텍쳐가 최고 성능을 보인 바 있다. 이로써 DNN이 더 깊어질 수록 더 좋은 성능을 가질 것이라는 기대를 가지게 만들었다. 일반적으로 DNN이 깊어질 수록 미분 값 상실(vanishing gradients) 등의 문제 때문에 학습하기 힘들어 져서 적절한 가중치 초기값 설정(weight initialization) 및 정칙화(regularization) 기법이 필수적이다. 이는 주어진 과제(task)와 학습 데이터의 특성에 따라 크게 변하는 초모수(hyperparameter) - 모수(parameter)를 학습하기 위해 필요한 모수라는 의미 - 으로 적절히 최적화하지 않으면 목표 성능을 달성하기 힘들다.


딥러닝을 포함한 대부분의 기계학습 알고리즘으로 적절한 모델을 학습하는 절차는 다음과 같다. 먼저 데이터의 특성과 알고리즘에 대한 이해를 바탕으로 기계 학습 모델을 선정하고 관련된 초모수의 초기 값을 정한다. 학습할 데이터를 학습 데이터, 유효성 확인 데이터 및 평가 데이터로 적절히 나누는데 학습할 데이터의 양이 부족할 경우 데이터 증강(data augmentation)을 수행해 데이터를 늘리거나 k겹 교차검증(k-fold cross validation)으로 실험을 반복하여 결과에 대한 통계적 신뢰도를 높여야 한다. DNN의 경우 보통 데이터를 잘게 쪼개 경사 하강법(mini batch gradient descent)을 반복 수행하면서 과적합이 발생하지 않는 횟수에서 학습을 마치고 평가 데이터로 최종 성능을 평가한다. 이때 목표 성능을 만족하지 못할 경우 모델 선정 혹은 초모수 설정(hyperparameter tuning)후 이전의 과정을 반복한다.


Cloud ML 엔진은 구글 클라우드 플랫폼(Google Cloud Platform, GCP)에서 제공하는 서비스 형태의 플랫폼(PaaS)의 하나인 기계학습 플랫폼이다. GCP는 구글의 데이터센터 인프라를 기반으로 한 서비스 형태의 플랫폼(PaaS)으로써 빅데이터 저장, 처리, 분석 및 기계학습을 수행할 수 있는 기능을 제공하는 클라우드 서비스이다. 모든 기능은 가상 기기(Virtual Machine, VM) 형태로 필요할 때만 동작하고 Open API를 통해 생성, 제어, 제거가 가능하다. ML 엔진은 텐서플로우로 작성한 모델을 다양한 VM에서 학습할 수 있는 온라인 분산 처리 기능을 제공한다. 하둡(hadoop)과 같은 여타의 분산 처리 시스템과 같이 일(job)을 생성해 전달하면 작업이 처리된다. 이 기능을 사용하면 서비스의 성능 목표에 따라 규모를 확장하거나 속도를 향상시킬 수 있으나 제대로 사용하기 위해서는 클라우드 컴퓨팅에 대한 이해가 필수적이다.



CODE ANALYSIS

깃허브를 통해 공개된 CIFAR-10 데이터에 대한 튜토리얼 프로젝트는 두 종류가 있는데 하나는 저수준 API를 사용하고 나머지는 고수준 API를 사용해 구현한 것이다. 이 포스팅에서는 두 번째 프로젝트을 기준으로 설명한다. 첫 번째 프로젝트은 CPU와 GPU 및 multiple GPUs를 활용해 각각 학습할 수 있으나 ML 엔진에서 학습하는 기능을 제공하지 않는다. 공식 홈페이지에서 상세한 설명을 제공하고 있으니 참고하기 바란다.


High Level Analysis

분석 대상 프로젝트는 파이썬 스크립트 파일(.py)로 구성되어 있으며 파일 별 역할은 아래와 같다.




파일명 역할 기타
generate_cifar10_tfrecords.py CIFAR-10 다운로드 후 데이터를 tfrecords 형태로 변환 파이썬 2.7.x 에서만 정상 수행
cifar10.py CIFAR-10 데이터 가공
cifar10_main.py 학습 및 평가 수행 명령줄 변수로 데이터 위치 등을 전달 필요
cifar10_model.py ResNet 모델 생성
cifar10_utils.py 병렬 학습 및 로깅
model_base.py ResNet 구성요소



각 파일 별 의존성(dependency)은 아래의 그림과 같다. 필수적으로 텐서플로우의 버전은 1.3 이상으로 설치되어야 한다. tfrecords 형태로 데이터를 변환하기 위한 코드는 cPickle 모듈을 참조하는데 이 모듈은 파이썬 3에서는 제거되어 파이썬 2에서만 동작한다. 만일 파이썬 3 버전에서 수행해야만 한다면 정상동작하지 않을 수 있다.


클래스 구조 및 상속 관계는 다음과 같다. 

Cifar10DataSet 클래스는 cifar10.py 에 정의되어 있고 tfrecords 데이터의 경로를 받아 그 일부를 make_batch() 함수의 결과 값으로 전달한다. 

use_distortion 속성으로 데이터 증강(data augumentation) 수행 여부를 결정할 수도 있다. 


model_base.py 에 정의되어 있는 ResNet 클래스는 추상 클래스의 역할을 하고 ResNet의 구성요소인 convolution, batch normalization, ReLU, fully connected layer, averge pooling unit들을 정의하고 있다. 

cifar10_model.py 에서 *ResNetCifar10 클래스가 이를 상속 받아서 forward_pass()에서 네트워크를 구성한다.



Detailed Analysis

지금부터는 학습이 실행될 때 호출 흐름과 API 사이의 참조하는 흐름에 따라 상세하게 살펴본다.


High level API call flow

시작 지점(entry point)은 cifar10_main.pyargparse.ArgumentParser 클래스를 사용해 명령 줄에서 입력 받아야 하는 모수들을 정의하고 해당 모수 값들을 main() 함수에 전달한다.

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    # omitted  
    parser.add_argument(
        '--eval-batch-size',
        type=int,
        default=100,
        help='Batch size for validation.')

    # omitted 
    main(**vars(args))


main() 함수에서는 그래프 모델을 수행할 세션(session) 설정 후 고수준 API인 learn_runner.run() 함수를 호출한다.

tf.contrib.learn.learn_runner.run(
  experiment_func, 
  run_config,
  hparams)

주의할 점은 learn_runner.run() 함수의 첫번째 모수로 함수 이름이 전달되는 데 이때 또다른 내부 함수(_resnet_model_fn()) 객체를 전달하는 구조이다. 파이썬이 First-class function를 지원하기 때문애 가능한 패턴이다. 

이 패턴의 기능에 대해 상세하게 설명하면 해당 모수의 값으로 정의한 함수를 전달하면 해당 함수가 필요한 시점에서 임의의 또다른 모수 값을 이용해 실행할 수 있게 된다.


experiment_functf.contrib.learn.Experiment() 클래스 객체를 반환해야하는 제약 조건이 있다. 

tf.contrib.learn.Experiment() 클래스 객체를 생성할 때 학습 및 평가에 필요한 데이터를 전달하는 함수 및 학습 반복 횟수 등을 정의해야 한다. 첫번째 모수인 classifier 에는 tf.estimator.Estimator() 클래스 객체를 전달해야 한다. 

이 클래스 객체를 생성할 때 초모수 최적화 시 필요한 값들을 전달해야 한다. tf.estimator.Estimator() 클래스 객체의 첫번째 생성자 모수로 model_fn 가 필요한데 이 모수에 전달될 함수는 tf.estimator.EstimatorSpec() 객체를 반환해야 한다. 

결국 tf.estimator.EstimatorSpec() 클래스 객체를 생성할 때 정의한 그래프 모델 및 목적 함수 등의 중요한 모수들이 전달된다.


Model training flow

지금까지 흐름이 cifar10_main.py 의 _resnet_model_fn() 에 구현되어 있다. 여기서 핵심적인 코드를 간추려서 살펴보자.

    def _resnet_model_fn(features, labels, mode, params):
        """Resnet model body.

        """

        for i in range(num_devices):
            with tf.variable_scope('resnet', reuse=bool(i != 0)):
                with tf.name_scope('tower_%d' % i) as name_scope:
                    with tf.device(device_setter):
                        loss, gradvars, preds = _tower_fn(
                            is_training, weight_decay, tower_features[i], tower_labels[i],
                            data_format, params.num_layers, params.batch_norm_decay,
                            params.batch_norm_epsilon)                

        # Device that runs the ops to apply global gradient updates.
        with tf.device(consolidation_device):

            num_batches_per_epoch = cifar10.Cifar10DataSet.num_examples_per_epoch(
                'train') // (params.train_batch_size * num_workers)
            boundaries = [ omitted ]
            staged_lr = [ omitted ]

            learning_rate = tf.train.piecewise_constant(tf.train.get_global_step(),
                                                        boundaries, staged_lr)
            # Create a nicely-named tensor for logging
            learning_rate = tf.identity(learning_rate, name='learning_rate')

            optimizer = tf.train.MomentumOptimizer(
                learning_rate=learning_rate, momentum=momentum)

이 함수는 매우 복잡하고 많은 일들을 하고 있어 분석하기 매우 까다롭다. (Here lives a dragon


개략적으로 보면 학습을 분산처리하기 위해서 여러 개의 GPU에 각각의 모델을 만들어서 병렬적으로 학습해서 각각 미분 값(gradients)를 구한 후 합쳐서 반영하는 타워(tower) 패턴을 쓰고 있다.
결국 GPU 별로 모델을 만드는(build) 과정과 loss를 구하는 데이터 흐름 처리는 _tower_fn() 함수에서 확인할 수 있다.

def _tower_fn(is_training, weight_decay, feature, label, data_format,
              num_layers, batch_norm_decay, batch_norm_epsilon):
    """Build computation tower (Resnet).
    """
    model = cifar10_model.ResNetCifar10(num_layers)
    logits = model.forward_pass(feature, input_data_format='channels_last')
    tower_pred = {
        'classes': tf.argmax(input=logits, axis=1),
        'probabilities': tf.nn.softmax(logits)
    }

    tower_loss = tf.losses.sparse_softmax_cross_entropy(
        logits=logits, labels=label)
    tower_loss = tf.reduce_mean(tower_loss)

cifar10_model.ResNetCifar10 클래스 인스턴스를 생성할 때 입력받은 레이어의 개수 만큼 forward pass() 메소드에서 모델을 만드는 작업을 수행한다. 

아래 그림은 네트워크 아키텍쳐를 만드는 순서를 도식화한 것이다. 


여기서 cifar10_main.py를 명령줄 매개변수로 초모수인 레어어 개수에 해당하는 Residual Block를 만들게 된다. 



Residual Block은 버전 1.0과 2.0 그리고 병목(bottleneck) 2.0 버전이 있는데 기본 값은 지름길 연결(shortcut connection)를 위해 플랜 A를 쓰며 2개의 하위 계층을 갖는 버전 1.0이다. 

Residual Block 버전 1.0의 세부 구조는 다음 그림과 같다. 

해당 코드는 model_base.py_residual_v1() 메소드에서 구현되어 있으니 참고하기 바란다. 



METHODS


시작 함수에서부터 고수준 API를 사용해 수행되는 흐름을 통해 모델을 만드는 과정과 만든 모델을 분산해서 학습하는 과정을 튜토리얼 코드를 통해 상세히 살펴보았다. 지금부터 이 코드를 다양한 수행 환경에서 학습을 진행하는 법을 정리한다.


먼저 자신의 개발 환경에 텐서플로우를 설치한다. 

GCP를 사용하면 크롬 브라우저에서 Google Cloud Shell을 통해 미리 개발환경이 구성된 가상 머신(VM)에서 손쉽게 사용할 수 있지만 사용할 때 비용이 발생하는 유료 서비스인 만큼 소프트웨어 개발 도구(SDK)을 자신이 선호하는 개발 환경에 설치해서 사용한다. (이때, 필요한 파이썬(python) 버전은 2.7.x 로써 텐서플로우와 같이 사용하기를 원한다면 2.7.x 버전용 텐서플로우를 설치해야 함에 주의한다.)

더불어 GCP에 대한 이해가 부족하다면 먼저 참고문헌에 기재한 GCP 입문서를 읽기를 추천한다.


Create TFRecord Dataset

텐서플로우는 다양한 형식의 데이터셋을 읽어들일 수 있는 방법를 제공하지만 그중 TFRecords 형식을 표준으로 삼는다. 아래 명령으로 CIFAR-10 데이터를 온라인에서 다운로드 받아서 TFRecords 파일 포멧으로 변경할 수 있다. 

(단, 이 코드는 파이썬 2.7.x에서만 정상적으로 동작한다.)

MY_BUCKET=${PWD}/cifar-10-data
python generate_cifar10_tfrecords.py --data-dir=${MY_BUCKET}

ML 엔진에서 활용하기 위해서는 --data-dir= 값으로 ${MY_BUCKET} GCP Storage 경로를 입력하거나 해당 버킷에 생성된 파일들을 업로드해야 한다.

MY_BUCKET=gs://cifar-10-data/
python generate_cifar10_tfrecords.py --data-dir=${MY_BUCKET}


Training with CPU Only

텐서플로우 CPU 버전(버전 1.3 이상)이 설치된 환경에서는 GPU 없이 CPU 만으로도 다음과 같이 튜토리얼을 학습할 수 있다. 이 코드는 train-steps 수 만큼의 학습을 반복하고 검증(evaluation)을 수행한다. 학습에 오랜 시간이 걸리기 때문에 디버깅 용도로만 활용하는 것이 바람직하다.

python cifar10_main.py --data-dir=${MY_BUCKET} \
                       --job-dir=/tmp/cifar10 \
                       --num-gpus=0 \
                       --train-steps=100



Training with CPU & GPUs

텐서플로우 GPU 버전(버전 1.3 이상, NVIDIA GPU card 설치 및 CUDA, CuDNN 라이브러리 필요)이 설치된 개발환경에서는 다음과 같이 GPU를 사용해서 빠르게 학습할 수 있다. 아래 명령은 개발환경에 두 대의 GPU가 설치되어 있다고 가정한다. 설치된 GPU 수 이하의 num-gpus 수를 정해야 정상 동작한다. 참고로 특별한 설정이 없는 한 모든 GPU의 메모리를 텐서플로우가 할당해서 사용하기 때문에 GPU가 동작하지 않더라도 메모리가 없어 계산을 수행하지 않는 GPU를 쓸 수 없으니 주의 바란다.

python cifar10_main.py --data-dir=${MY_BUCKET} \
                       --job-dir=/tmp/cifar10 \
                       --num-gpus=2 \
                       --train-steps=10000



Training with ML Engine

이제 이 튜토리얼 코드를 GCP의 ML 엔진에서 돌려보도록 하자. 앞서 설명한 것처럼 GCP를 쓰기 위해서는 Google Cloud Shell에 접속하거나 자신의 개발 환경에 개발 도구(SDK)를 설치해야 한다. ML 엔진에서 학습을 수행하기 위해서는 gcloud 명령을 사용해야 한다.

GCP에서 학습 작업(Trainig Job)을 수행하는 것은 하둡이나 스파크와 같이 여타의 분산처리 시스템에서와 유사하게 분산 학습을 수행할 패키지(package)를 만들어서 서버에 제출(submit)하는 형태가 된다. 따라서 수행할 코드가 어떠한 컴퓨팅 기기에서도 동작할 수 있도록 코드를 개발한 개발 환경과 의존성을 없는 상태가 되어야 하고 필요하다면 패키지 내에 포함될 수 있도록 해야 한다. 이후 이것을 자세히 살펴보자.


먼저 빈번히 변경되는 입력 값을 환경변수로 정의하자.

MY_BUCKET=gs://cifar-10-data/
JOB_ID=cifar_train_test_1

MY_BUCKET 환경 변수는 TFRecords 파일들이 업로드된 GCP 스토리지를 가리킨다. 이 경로에 대한 적절한 읽기/쓰기 권한이 없으면 작업(job)이 실행된다하더라도 정상 동작하지 않는다. JOB_ID는 작업을 구분할 고유한(unique) 아이디가 된다. 기존에 썼던 값으로 다시 시도할 경우 오류가 발생하니 아이디 사용에 주의를 기울이기 바란다.

지금까지 살펴 본 코드를 ML 엔진에서 학습하는 예시로 아래의 명령줄을 살펴보도록 하자. gcloud ml-엔진 jobs submit training 명령 뒤에 상당히 많은 매개 변수들이 있는데, 빈 매개변수(--) 전까지의 매개변수는 ML 엔진를 위한 옵션이고 이후는 cifar10_main.py 의 명령 매개변수이다

gcloud ml-engine jobs submit training ${JOB_ID} \
    --package-path estimator/ \
    --module-name estimator.cifar10_main \
    --config estimator/cmle_config.yaml \
    --runtime-version 1.2 \
    --job-dir=$MY_BUCKET/model_dirs/${JOB_ID} \
    --region us-east1 \
    -- \
    --data-dir=$MY_BUCKET \
    --num-gpus=2 \
    --variable-strategy GPU \
    --train-steps=10000

이 명령줄의 package-path, module-name, config 옵션의 값을 살펴보자. 이 명령을 수행하는 경로에 /etimator 라는 폴더가 존재하고 이 폴더 안에 수행할 cifar10_main.py, cmle_config.yamlinit.py 파일들이 있다고 가정한다. (참고: init.py 파일은 해당 경로가 모듈임을 나타내는 빈 파일이다.)


config 옵션으로 기술한 cmle_config.yaml 파일은 학습 작업을 수행할 클러스터의 사양을 기술하고 있다.

trainingInput:
  scaleTier: CUSTOM
  masterType: complex_model_m_gpu
  workerType: complex_model_m_gpu
  parameterServerType: complex_model_m
  workerCount: 1

이 파일을 기술하는 상세한 방식은 Job Resource - Training Input를 참조하면 되는데 해당 문서의 내용은 YAML 형태가 아닌 JSON 형태로 기술되어 있기 때문에 주의를 요한다. 이 YAML 문서가 아닌 명령줄에 관련 정보를 입력할 수 있는데 앞서 학습 작업을 제출하는 명령줄의 runtime-version, job-dir, region 등의 매개변수 정보를 YAML에서도 기술할 수 있다. 

여기서 runtime-version은 텐서플로우의 버전을, job-dir은 학습이 수행하면서 사용할 클라우드 스토리지를 그리고 region은 학습할 클러스터 서버가 위치한 지역을 의미한다. (참고: region준비된 클러스터 서버의 종류과 비용등을 참고해서 결정하기 바란다.)



Hyperparameter tuning

고수준 API로 작성된 텐서플로우 모델을 사용하면 ML 엔진과 연계하여 자동으로 초모수 최적화를 수행하는 기능을 제공한다. 

이를 위해서는 최적화 목표(성능 극대화 혹은 에러 최소화), 최적화 시도 횟수 및 초모수 별로 형식(type), 탐색 범위(search range)와 스케일 형식을 앞서 소개한 YAML 파일에 추가해 기술한다.

trainingInput:
  scaleTier: CUSTOM
  masterType: complex_model_m_gpu
  workerType: complex_model_m_gpu
  parameterServerType: complex_model_m
  workerCount: 1
  hyperparameters:
    goal: MAXIMIZE
    maxTrials: 10
    maxParallelTrials: 1
    params:
    - parameterName: learning-rate
      type: DOUBLE
      minValue: 0.0001
      maxValue: 0.5
      scaleType: UNIT_LOG_SCALE

이 YAML 파일은 앞서 설정 파일에 최대 10회의 최적화 작업을 수행 해 성능이 최대화되는 값을 찾도록 초모수 설정 기능을 추가한 것이다. 

이때 parameterName의 값은 cifar10_main.py 의 명령 매개변수로써 해당 변수가 HParams 클래스 인스턴스에 전달되도록 구현되어 있어야 한다. 이 설정의 목표는 learning-rate(mini-batch로 학습을 수행할 때마다 가중치 변경-오류 역전파, error backprop-에 반영되는 비율)을 0.0001 부터 0.5 사이의 값으로 찾되 로그 스케일로 선정하라는 의미이다.

ML 엔진을 이용해 학습 작업을 수행한 이력은 GCP 콘솔에서 확인할 수 있다. 여기에서 작업의 유형, 작업 별 로그와 및 결과를 확인할 수 있다. 





Visualizing results with TensorFlow

각각의 학습 결과는 명령줄 매개변수로 전달한 job-dir에 다양한 형태의 로그(log)로 저장된다. 텐서보드(Tensorboard)는 이 정보를 대시보드로 표현한다. 텐서보드는 서버 애플리케이션으로 동작하기 때문에 log-dir 매개변수에 로그가 저장된 경로와 더불어 사용가능한 포트(port) 숫자를 전달해 주어야 한다.

tensorboard --logdir=$MY_BUCKET/model_dirs/${JOB_ID} --port=8080

이 명령줄을 수행하면 다음과 같이 화면에 출력되고 브라우저를 열어 주소 창에 아래 http://URL:8080 에 해당하는 경로를 입력하면 텐서보드가 수행된다. 만일 구글 클라우드 쉘에서 이 명령을 실행했다면 화면 우측 상단 도구 상자에서 '웹 미리보기'를 선택하면 된다.

TensorBoard 0.1.5 at http://URL:8080 (Press CTRL+C to quit)




RESULTS

지금까지 고수준 API로 작성한 ResNet 모델로 CIFAR-10 데이터 분류 문제를 학습하는 코드를 다양한 실행 환경에서 학습을 수행해 보았다. 지금부터는 학습 결과와 초모수 자동 선정 결과를 살펴보자.



Training result comparision

다음은 만번 반복 학습(--train-steps=10000)하고 변수 연산을 GPU로 하는 (--variable-strategy GPU) 조건에서 GPU 2대가 설치된 딥러닝용 PC와 GCP에서 ML 엔진을 통해 할당받은 VM에서 GPU 2대를 사용한 결과이다.


조건 GPU 사양 성능(정확도) 수행시간
딥러닝용 PC NVIDIA GeForce 1080 Ti 75.48% 5분 3초
ML 엔진 NVIDIA Tesla K80 77.28% 18분 17초



ML 엔진이 사용하는 GPU가 딥러닝용 PC의 GPU보다 훨씬 고급임에도 불구하고 학습 수행 시간이 무려 3.6배 더 오래 걸린 점이 흥미롭다. 실제 작업이 제출된 이후 할당되는 시간까지 포함한다면 4~5배 정도 더 오래 걸릴 가능성이 존재한다. 같은 조건으로 수행하였는데도 불구하고 정확도가 1.8% 차이가 난다는 점은 비교 실험을 할 때 큰 문제가 된다. 딥러닝 모델을 학습함에 있어 이 차이가 발생할 수 있는 요소가 다양해서 이를 이후에 논의해 보겠다.



Hyperparameter tuning result

다음 표는 learning-rate 에 대해 10회 초모수 조정을 한 결과이다. 첫번째 선택한 값(0.033)으로 학습한 결과가 앞서 기본 값(0.1)으로 학습했을 때보다 확연히 좋은 성능(84.33% > 77.28%)을 보여준다.



trials selected learning-rate accuracy
1 0.033436416 0.8433
2 0.000100123 0.578
3 0.499496502 0.5129
4 0.001547028 0.7313
5 0.007184366 0.7893
6 0.000340355 0.6591
7 0.14863852 0.7975
8 0.015507787 0.804
9 0.003373571 0.7527
10 0.000734705 0.7273



이 실험에서 첫번째 시행에서 선택한 learning-rate 값에서 두드러지게 좋은 결과를 얻어서 남은 시행에서는 그에 상응하는 결과를 얻지 못했다.



아래 그림은 이렇게 선택한 learning-rate의 값에 로그를 취한 값을 x축으로, 그때의 정확도 성능을 y축으로 표시한 것이다.



최적화 작업을 YAML 파일을 설정할 때 learning-rate를 0.0001 부터 0.5 사이의 값을 로그 스케일로 선택했기 때문에 로그를 취한 값으로 표현하면 -4 ~ -0.3 사이의 값을 선택하게 되는데 초모수를 선정할 때 주어진 범위에서 상당히 균일하게 시도(low discrepancy sampling) 해 보는 것을 알 수 있다.
또한 성능 추세를 볼 때 최적의 learning rate는 0.0155 ~ 0.1486 사이의 값에서 가장 좋은 성능이 나올 가능성이 있다.




DISCUSSIONS

고수준 API는 ML 엔진과 연계해 분산 학습과 초모수 최적화를 수행할 수 있는 기능을 제공한다. 기존 저수준 API를 이용해서 자신만의 개발 환경에서만 동작하는 코드를 작성하는 것은 그리 어려운 일이 아니다. 하지만, 클라우드의 다양한 학습 수행 환경에 모두 동작하는 코드를 작성하는 것은 매우 어려운 일이다. 따라서 분산 환경에서 대용량의 학습이 필요한 경우에는 고수준 API로 작성된 코드를 기반으로 코드를 구현하는 것이 좋다.

고수준 API를 제대로 활용하기 위해서는 딥러닝의 용어(terminology) 뿐 아니라 프로그래밍 패턴(pattern)과 관용구(idiom)에 대해 익숙해져야 한다. 특히 API로 쓰이는 클래스나 함수가 매개변수로 함수 객체를 전달하는 경우가 많고 그 객체를 생성하기 위해 또 다른 객체를 생성해야 하는 경우가 많아 코드의 흐름을 이해하는데 어려움이 있다. 핵심 API는 API 문서를 링크해 두었기 때문에 참조해 보길 바란다.


딥러닝 모델 성능에 대한 재현 가능성(reproducibility)을 최대한 높이기 위한 노력이 필요하다. 실험 결과가 보여주듯 같은 조건이라도 다른 기기에서 학습했을 때 정확도 성능에서 상당한 차이가 발생한다. 먼저 확률적 경사 하강법(SGD)과 분산 학습에 의한 정확도 손실(precision loss)가 존재한다. 또한 가중치를 초기화(random initialization) 할 때나 다른 정칙화 기법에 필요한 시드(seed) 값을 다를 수 있다. 이런 차이는 초모수 값을 최적화할 때 큰 장애물이 된다.


딥러닝 모델의 초모수 값이 정확도 성능에 크게 영향을 미친다. 딥러닝은 비선형 함수(activation function)를 통과하는 수많은 뉴런(neuron)의 가중치들을 목적 함수(objective function)로 최적화해서 성능을 높이는 기계 학습 알고리즘이다. 뉴런을 연결하는 방식, 계층 별 뉴런의 갯수나 계층의 깊이 등이 모두 초모수가 되고 이로써 학습할 수 있는 수용력(capacity)이 달라진다.

딥러닝 모델의 초모수 최적화 시행 횟수를 늘린다고 해서 좋은 결과(optimum)를 얻을 수 있다는 보장이 없다. 기계 학습의 보편적인(de facto) 최적화 시도법은 그리드 탐색(grid search)이었다. 하지만 기존 연구에 따르면, 임의 탐색(random search)으로 보다 나은 성능을 얻었는데 그 이유는 초모수 별로 성능에 미치는 영향이 다르기 때문이다. (J. Bergstra and Y. Bengio, 2012) 딥러닝의 경우 기존 기계학습 알고리즘보다 초모수의 수가 많다. 초모수의 수가 증가하면 초모수 간의 상관 관계(correlation)에 의한 상쇄효과가 발생한다. 더불어 시행 횟수를 늘려도 해(solution)를 찾기 힘든 또 다른 형태의 차원의 저주 문제(The curse of dimensionality)가 발생한다.


자동화 알고리즘을 이용해 딥러닝의 초모수를 찾거나 최적화하는 다양한 연구를 시도하는 중이다. 초모수의 수가 적고 탐색 공간(search space)이 비교적 연속(continous)일 경우, 찾고자 하는 모델 함수의 분포가 가우시안(Gaussian)을 따른다는 가정(Proior) 하에 관찰(observation) 결과 값 사이의 공분산(covariance)를 계산해서 초모수 값을 업데이트를 하는 GP(Gaussian Processes)가 대부분 좋은 성능을 보여주었으나 초모수의 수가 많고 불연속일 경우에는 완전히 실퍠(catastrophic failure)할 때가 있어 신뢰를 얻지 못했다.


최근에는 최고 성능을 갖는 구조를 찾는 수고를 자동화하기 위해 탐색 공간에 대한 제약(constraints)이 적은 강화학습(reinforcement learning)이나 우전 알고리즘 (genetic algorithm)으로 시도하고 있다. 그 결과 사람이 고안한 것과 유사하면서 더 좋은 성능을 갖는 네트워크 구조를 발견할 수 있었다. (Barret Zoph and Quoc V. Le, 2017)

이런 유형의 자동화 알고리즘 연구를 수행하기 위해서는 자동 생성된 초모수 설정 조건(hyperparameter vector)에 따라 모델을 학습하고 그 성능 결과를 사용해 다른 초모수 설정을 생성해야 한다. 이는 막대한 양의 계산 비용(computation cost)과 시간이 요구된다. ML 엔진처럼 분산 처리가 가능한 클라우드 서비스를 효과적으로 활용할 수 있어야 하는 이유이다.




ACKNOWLEDGEMENTS

이 포스팅은 구글 클라우드 팀의 지원으로 페이스북 텐서플로우 그룹를 통해 진행된 무료 크레딧 제공 이벤트를 통해 작성되었습니다. 특별히 텐서플로우에 관한 좋은 정보들과 더불어 ML 엔진 사용의 기회를 제공해 주신 조대협 님께 감사를 전합니다.


이 포스팅에 작성하는데 필요한 연구는 서울대학교 융합과학기술대학원 응용 데이터 과학 연구실에서 진행했습니다.
딥러닝의 수학적 원리를 이용한 최적화 알고리즘 연구에 관심있는 분들은 연락바랍니다.




REFERENCES


안녕하세요.


구글 클라우드 상에서 jupyter notebook 인터페이스로 Big Query 및 tensorflow 등의 강력한 도구를 통해 손쉽게 데이터 사이언스를 수행해 볼 수 있습니다.

아래는 구글 데이터랩에서 튜토리얼로 제공하는 금융 데이터 분석 예제를 한글로 번역한 것입니다. 관심 있는 분들에게 데이터 분석에 대한 인사이트를 줄 수 있을 것으로 기대합니다.



안녕하세요.


아래와 같이 "소프트웨어, 사람과 사물의 소통을 향하여"라는 주제로 소프트웨어 특강을 진행합니다.

  • 일시: 2017년 5월 27일 오후 3시
  • 장소: 수원시 영통구 태장마루 도서관
  • 대상: 초등학교 3~6학년과 학부모
  • 신청: 공지 참조


아래는 발표 자료입니다. 참고하시기 바랍니다.




안녕하세요.

정말 오랫만에 포스팅을 올리네요.


다음과 같이 윈도우10 환경에서 anaconda를 이용해 jupyter notebook 으로 tensorflow를 구현할 수 있는 환경 설치 가이드를 정리합니다.



Install latest anaconda windows  for python 3.x



 

- Create conda virtualenv with python 3.5:

 conda create -n tf python=3.5 anaconda

 

- Activate virtualenv:

activate tf

 

- Install tensorflow CPU:

 pip install tensorflow


- Exit virtualenv:

deactivate tf


Create jupyter notebook startup script

o    If serving from this local directory:  C:/tensorflow

o    Create batch file named as "run-jupyter.bat" which contains as below.

@echo off

cmd /k "cd /d C:\tensorflow & activate tf & jupyter notebook"


·         Double click to run "run-jupyter.bat" and enjoy your tensorflow coding on your browser




안녕하세요.


이번에는 Docker를 이용해 구축한 TensorFlow(이하 텐서플로우)의 학습된 정보 (training)를 Tensorboard(이하 텐서보드)로 시각화는 방법을 다뤄 보겠습니다.


앞서 소개한 바 있는 Docker로 Windows에서 텐서플로우 수행하기 포스트에서 생성한 도커 컨테이너에서는 버그로 인해 텐서보드가 수행되지 않지 않는 현상이 있어 불가피하게 새로운 컨테이너를 만들어야 합니다.

귀찮겠지만 다음과 같이 새로운 컨테이너를 만들도록 합시다.   


1. 0.9 버전의 텐서플로우 컨테이너 만들기


Docker Quickstart Terminal를 실행한 후 현재 실행 중인 컨테이너 정보를 확인합시다.


$ docker ps -a


기존에 설치한 컨테이너가 수행 중이라면 정지하고 새로운 이미지를 다운로드 받아 설치합니다.


$ docker stop {수행중인 컨테이너 이름} $ docker run -p 8888:8888 -p 6006:6006 -it b.gcr.io/tensorflow/tensorflow:r0.9-devel

b.gcr.io/tensorflow/tensorflow 다음 콜론(:) 구분자 다음은 태그를 지칭하고 여기서 r0.9-devel 은 0.9 개발자 버전을 의미합니다.

(글을 작성하는 시점에서 텐서플로우는 활발히 개발 중인 프로젝트임으로 다음 링크에서 최신 버전에 대한 태그를 확인하기 바랍니다.)


또한 -d 옵션은 포트 포워딩을 의미하는데 jupyter가 사용하는 기본 포트인 8888과 텐서보드가 사용하는 포트인 6006를 전달해서 외부(즉, windows) 상에서 접속할 수 있게 해 줍니다.


다운로드가 완료되면 새로운 컨테이너가 생성되고 기본 값으로 bash 가 실행됩니다.


2. jupyter 실행하기


root@{컨테이너 아이디}:/# cd /


루트 폴더로 이동하면 jupyter를 실행할 수 있는 shell script 가 준비되어 있습니다. 이전 이미지는 컨테이너 생성 후 바로 이 스크립트를 수행하도록 지정되어 있으나 이 개발자 버전에서는 사용자가 직접 이 스크립트를 수행해 주어야 합니다.

bash가 종료되더라도 jupyter가 종료되지 않도록 nohup 으로 수행하고 새로운 process로 띄우기 위해 & 옵션을 줍시다.


root@{컨테이너 아이디}:/# nohup ./run_jupyter.sh


이제 windows에서 브라우저로 다음 경로에 접속해 jupyter를 실행합니다: 

http://192.168.99.100:8888/


3. 텐서보드 실행하기


텐서보드는 jupyter처럼 웹서비스로 동작합니다. 따라서 이를 실행하기 위해 run_jupyter.sh 와 같은 run_tensorboard.sh 스크립트를 만들어 보도록 하겠습니다.


root@{컨테이너 아이디}:/# nohup tensorboard --logdir=/logs --port 6006 &


여기서 logdir은 텐서플로우 프로그램에서 지정하는 로그 경로를 의미하고 port 는 사용할 포트를 의미합니다.

이제, windows 의 브라우저로 텐서보드를 접속해봅시다: 

http://192.168.99.100:6006/


아직 텐서플로우로 학습한 바가 없어서 아무런 기록이 없습니다.

4. 샘플코드 수행하기

jupyter에서 새로운 ipython 파일을 생성하고 샘플코드를 입력합시다.


import tensorflow as tf

# reset everything to rerun in jupyter
tf.reset_default_graph()

# config
batch_size = 100
learning_rate = 0.5
training_epochs = 5
logs_path = "/logs"

# load mnist data set
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

# input images
with tf.name_scope('input'):
    # None -> batch size can be any size, 784 -> flattened mnist image
    x = tf.placeholder(tf.float32, shape=[None, 784], name="x-input") 
    # target 10 output classes
    y_ = tf.placeholder(tf.float32, shape=[None, 10], name="y-input")

# model parameters will change during training so we use tf.Variable
with tf.name_scope("weights"):
    W = tf.Variable(tf.zeros([784, 10]))

# bias
with tf.name_scope("biases"):
    b = tf.Variable(tf.zeros([10]))

# implement model
with tf.name_scope("softmax"):
    # y is our prediction
    y = tf.nn.softmax(tf.matmul(x,W) + b)

# specify cost function
with tf.name_scope('cross_entropy'):
    # this is our cost
    cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))

# specify optimizer
with tf.name_scope('train'):
    # optimizer is an "operation" which we can execute in a session
    train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(cross_entropy)

with tf.name_scope('Accuracy'):
    # Accuracy
    correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    
# create a summary for our cost and accuracy
tf.scalar_summary("cost", cross_entropy)
tf.scalar_summary("accuracy", accuracy)

# merge all summaries into a single "operation" which we can execute in a session 
summary_op = tf.merge_all_summaries()

with tf.Session() as sess:
    # variables need to be initialized before we can use them
    sess.run(tf.initialize_all_variables())

    # create log writer object
    writer = tf.train.SummaryWriter(logs_path, graph=tf.get_default_graph())
        
    # perform training cycles
    for epoch in range(training_epochs):
        
        # number of batches in one epoch
        batch_count = int(mnist.train.num_examples/batch_size)
        
        for i in range(batch_count):
            batch_x, batch_y = mnist.train.next_batch(batch_size)
            
            # perform the operations we defined earlier on batch
            _, summary = sess.run([train_op, summary_op], feed_dict={x: batch_x, y_: batch_y})
            
            # write log
            writer.add_summary(summary, epoch * batch_count + i)
            
        if epoch % 5 == 0: 
            print "Epoch: ", epoch 
    print "Accuracy: ", accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels})
    print "done"

MNIST 데이터셋(수기로 작성한 숫자를 모아 구분하는 데이터셋)을 이용해 간단한 소프트맥스 로지스틱스 회기 분류로 학습 후 평가하는 모델을 수행해 봅니다.

이때, 텐서보드를 위해 추가되는 코드는 다음과 같습니다.


  • logs_path = "/logs" : 텐서보드가 참조할 로그 정보가 저장될 경로를 지정
  • tf.scalar_summary("cost", cross_entropy)
    tf.scalar_summary("accuracy", accuracy)
    summary_op = tf.merge_all_summaries()

    : EVENTS 항목에 표시할 요약 정보로 cost와 accuracy를 지정
  • writer = tf.train.SummaryWriter(logs_path, graph=tf.get_default_graph())
    : SummaryWriter 개체를 생성해 로그 경로에 현재 그래프를 지정
  • _, summary = sess.run([train_op, summary_op], feed_dict={x: batch_x, y_: batch_y})
    : training과 summary operation을 수행
  • writer.add_summary(summary, epoch * batch_count + i)
    : minibatch 수행 시마다 결과 요약 저장

5. 텐서보드에서 결과 확인

텐서보드는 events, images, audios, graphs, histograms 항목으로 구성되어 있습니다.

이번 샘플 코드에서는 events 와 graphs 에 관한 정보만 저장됩니다.




events 항목에서 학습으로 인해 accuracy가 증가되면서 cost 가 주는 모습을 보여주는 도식을 볼 수 있으며, graphs 항목에서는 모델의 형태와 텐서의 흐름을 역동적으로 볼 수 있습니다.


5. 참고자료



안녕하세요. 


docker image를 이용해 windows PC에서 텐서플로우를 사용하는 방안 에 덧붙여 이번에는 클라우드 환경에서 텐서플로우 딥러닝 개발을 할 수 있는 방안을 검토해 본 결과를 공유합니다.

요약하면, 다음과 같습니다.

  • 아마존 웹 서비스 기반 
    • 장점: GPU를 활용한 컴퓨팅 파워로 방대한 데이터로 인해 학습 시간이 많이 소요될 경우 활용 가치가 높음
    • 단점: 시간 당 사용 비용 모델(성능에 따라 대략 시간 당 천원에서 4천원 꼴)로 모델링 검증 완료 후 효과적으로 활용 필요

  • 구글 클라우드 플랫폼 기반
    • 장점: 베타 버전이라 요금이 싸고 텐서플로우를 개발하는 주최가 구글이라 빠른 개선이 기대됨.
      60일 무료 기간을 이용해  온라인에서 놀이터(playground) 목적으로 모델링을 하거나 학습이 오래 걸리더라도 상관없는 경우에 활용할 가치가 많음
    • 단점: 지금까지는 GPU 기반 컴퓨팅을 활용할 수 없는 것으로 보이고 preview인 cloud machine learning 서비스가 오픈되면 재 검토 필요




다량의 컴퓨팅 능력을 요하는 텐서플로우의 특성 때문에 연구에 있어 컴퓨팅 기기의 수요가 많은데 클라우드 서비스를 통해 대여해서 사용할 수 있다는 점에 착안하여 다음 양대 클라우드 서비스를 필두로 조사를 시작했습니다.

  1. 아마존 웹서비스(AWS)
  2. 구글 클라우드 플랫폼 (GCP)

1. 아마존 웹 서비스로 텐서플로우 구축하기


전자의 경우, 인스턴스를 구축하고 직접 관련 서비스를 설치하는 IaaS 형태로 필요한 하드웨어를 직접 설정하거나 정해진 목적에 따라 marketplace를 통해 손쉽게 인스턴스를 구축할 수 있도록 제공합니다. 검색 결과 64 bit linux 기반에 GPU를 이용하는 아마존 머신 이미지 (AMI) 가 등록되어 있음을 찾을 수 있었습니다. 



현재 소프트웨어 사용료 없이 EC2 사용료만 부과되고 아마존은 지역(region)에 따라 사용료가 달라지는 특성 상 글을 작성하는 시점에서 가장 저렴한 지역 (미국 동부지역) 기준으로 시간당 0.65 달러 / 2.6달러이며 (한국 돈으로 환산하면 대략 천원 내외/ 4천원 내외 예상) 각각의 하드웨어 스펙은 다음과 같습니다.

가격 발생 부담 때문에 직접 사용해보고 평가하지는 못했는데 추후 각각의 성능을 검토해 볼 예정입니다.


2. 구글 클라우드 플랫폼으로 텐서플로우 구축하기

후자인 GCP는 AWS와 다르게 PaaS 나 SaaS 형태가 두드러지며 각각 Products 를 활용해 Solutions 을 구축하는 방법을 가이드 하는 형태로 제공합니다.


최근 소식에 따르면 구글에서 TensorFlow 를 활용한 클라우드 기계학습 플랫폼을 준비중인데 아직 limited preview 단계라 활용 사유를 적고 대기해서 선정되어야만 활용할 수 있어 아쉬웠습니다.


그렇다고 클라우드에서 텐서플로우를 사용할 수 없는 것만은 아닙니다. 아래 링크는 시계열 금융 데이터로 기계학습한 사례를 다루고 있는데 이때 GCP의 또다른 제품인 Cloud Datalab를 활용하고 있음을 알 수 있었습니다.

https://cloud.google.com/solutions/machine-learning-with-financial-time-series-data


이 서비스의 특징은 원 클릭으로 Google App Engine과 Google Compute Engine을 이용해 Google BigQuery, IPython 및 TensorFlow를 지원하는 jupyter server를 생성해 주는데 다음과 같이 VM 기본 설정이 그다지 좋지는 않습니다.

  • 머신유형: n1-standard-1 (CPU 1개에 4GB 의 메모리를 가진 Intel Haswell 플랫폼)
  • 100 GB 스토리지

AWS 에 비해 활용 측면과 가격 측면이 유리하다고 판단되는데 datalab의 경우 현재 베타 버전인 관계로 App Engine 가격 정책만 따른다고 명시되어 있어 부담없이 신규 인스턴스를 만들어 수행해 볼 수 있습니다. 자세한 사용법은 다음 링크를 참고하시면 됩니다.

https://cloud.google.com/datalab/docs/quickstart   

GCP는 현재 신규 가입 시 60일 간 무료 사용에 500달러를 추가 제공하고 있으니 이 기간동안 활용해 보면서 성능을 평가해보려고 합니다.

다음은 생성된 인스턴스로 동작하는 jupyter service 입니다.

기존 활용했던 docker image 기반의 설치형 jupyter와 가장 큰 차이점은 git 리포지토리와 연결 할 수 있도록 기본 제공한다는 점입니다. 클라우드 서비스의 특성 처럼 시간이 지날 수록 개선될 것으로 보입니다.

이후 이 서비스를 기반으로 학습 시간이 많이 소요되는 benchmark를 선정해 수행하고 수행시간을 비교해 보고자 합니다.

이상입니다.


P.S. 마지막으로 설치된 텐서플로우의 버전이 궁금해서 확인할 방법을 찾았으나 마땅히 검색되는 것이 없더군요. 흥미로운 점은 python 텐서플로우 모듈의 버전을 출력해 보면 0.8.0 기준으로 설치한 docker image와 Cloud Datalab 모두 0.7.1 로 출력됩니다.

갑자기 텐서플로우의 버전 관리 (versioning) 가 제대로 이뤄지고 있는지 의구심이 들었습니다. :-(

  


+ Recent posts