Elasticserach에 Excel 데이터 입력하기 - 기본 설정과 적용 라이브러리


올해에는 선택과 집중을 분명히 하기로 했는데…제 버릇 개 못준다더니…어김없이 또 여기저기를 들쑤시기 시작했다…-.-


Hadoop이니 Hbase니 Spark니 잔뜩 설치해놓고는 다시 Elasticsearch에 관심을 갖게 된 것이다. 어떤 것인지 한 번
설치나 해보자고 시작한 것이 쉬운 설치 방법과 사용법에 혹해서 더 깊은 내용을 알고 싶어진 것이다. 마침 분석해보고
싶은 데이터가 있어 이참에 한번 Elasticsearch를 이용해보자고 마음먹었다.


현재까지 진행된 작업은 spring boot 기반의 웹 시스템을 통해 Excel파일을 업로드하면 이를 파싱하여 
Elasticsearch로 입력하고 업로드된 파일들은 별도로 관리 가능하도록 만든 것이다. 앞으로 3차례에 걸쳐 이 개발 
과정을 정리해보도록 하겠다.


발단


2월 경…새 집으로 이사를 좀 하는게 어떨까 싶어 새 집을 물색하였다. 그리고 기왕지사 옮기는 것, 가급적이면
앞으로 집 값도 좀 올라 주면 좋을 것 같다는 생각이 들었다. 하지만 부동산이라고는 쥐뿔도 모르는 상태에서 어디
그게 쉬운일이던가…그냥 새 집은 새 집이고 마침 어떤 데이터로 빅데이터나 AI를 공부해보나 하던 참이라 부동산
데이터를 사용해보면 어떨까 하는 생각이 들어 공공 데이터 포털에서 부동산 관련 데이터를 모으기 시작했다.


처음 모은 데이터는 1996년 부터 2017년까지의 공시지가 데이터였다. 그리고 첫 난관이 시작되었다.
데이터는 모았는데 이 데이터를 어떻게 Elasticsearch에 넣어야 할지 방법을 몰랐던 것이다. 그렇게 Excel
데이터를 Elasticsearch로 입력하는 방법을 찾다가 가장 적절해 보이는 솔루션으로 찾은 것이 excelastic이라는
vert.x 기반의 stand alone 애플리케이션이었다.


그런데 말입니다…안타깝게도 이 애플리케이션도 문제가 있었다. 클라이언트 PC 및 Elasticsearch가 설치된 서버의
사양과도 관계가 있겠지만 10만 건 정도 입력을 시도하면 여지없이 OOM이 발생하여 정상 입력이 되지 않았다.
시행착오를 거쳐 확인한 안정적인 입력 건수는 약 5만 건 정도였다. 공시지가 데이터가 년도당 약 50만 건의 데이터가
있는데 이 파일을 Elasticsearch로 입력하려면 파일을 10개로 쪼개는 작업을 해주어야 한다는 말이다. 이 작업 조차
웬만한 PC에서는 쉽지 않다. 내 맥미니가 i5(2.5GHz)에 RAM 16Gb인데도 50만 건 들어있는 Excel 파일을 열어서
5만 건씩 10개로 쪼개다보면 버벅거리기가 일쑤였다.


그래서 목마른 놈이 우물을 판다고…직접 하나 만들기로 했다. 그리고 기왕 만드는 김에 이것저것 기능을 좀 추가해보자
했는데 마침 또 회사에서 KMS를 Elasticsearch 기반으로 만들면 어떻겠냐는 이야기가 나와 겸사겸사 함께 진행해
보기로 했다. 


관련 기술들


이 작업에 사용된 기술들은 다음과 같다.


  1. Spring boot 2.0.0
  2. jQuery + bootstrap (UI는 AdminLTE라는 오픈소스 사용)
  3. Elasticsearch 6.2.1 ( + X-Pack)
  4. MySQL 5.6.38
  5. Spring Tool Suite 3.9.2


각 기술의 세세한 부분보다는 작업을 진행하면서 어려움을 겪었던 부분들 혹은 편리했던 기능들에 대한 팁 수준의
정리를 진행하고자 한다.


Spring boot로 삽질하기


작년까지는 현재 일하는 곳의 업무 시스템 개발을 위해 Spring으로 2차례 정도 가벼운 웹 시스템을 개발한 적이 있다.
그 과정에서 Spring boot를 개인 프로젝트에 사용한 적은 있지만 잠깐 건드려보다가 방치되고 말았다. 그리고는 이번에
다시 Spring boot를 이용하기로 했다. 마음같아서는 마이크로서비스에 대한 공부도 곁들여 하면서 구현을 해보고
싶었으나 너무 학습의 범위가 넓어질 것 같아 그냥 Spring을 쓰듯 Spring boot를 쓰기로 했다…-.-


STS에서 서버(Tomcat) 사용하기


경력에 걸맞지 않은 초보적인 실수가 참 많다…ㅠ.ㅠ 늘 겪는 실수 중 하나가 프로젝트를 생성한 후 STS에서 바로
서버 연결하여 실행하는 부분인데 이번에도 여지없이 프로젝트를 생성하고 나니 프로젝트의 서버 설정이 뭔가 이상하다.


서버를 선택할 수 있는 화면이 나오지 않고 “This project is not associated with any server”라는 문구가 보인다.


이 것은 프로젝트 생성 처음 설정에서 packaging 항목을 jar로 선택한 결과이다. jar로 선택한 경우 Stand alone
프로젝트로 판단하여 외부 서버와 연결하는 설정이 나타나지 않는다. 




packaging을 war로 하면 서버 설정 창에서 Tomcat 등의 was와 연결이 가능해진다.




Security 사용


프로젝트를 생성한 후 기본적인 REST API를 구현하였고 테스트를 위해 STS 내에서 Tomcat을 구동시켜 브라우저를
통해 URL을 호출하여보았다. 그런데…난데없이 계정 입력창이 뜨는 것이 아닌가?


Spring security


확인 결과 이 것은 프로젝트 생성 시 의존성 설정 부분에서 Security를 체크했기 때문이었다. 



Security를 체크함으로 해서 많은 부분에 영향을 받았다. 파일 업로드, iframe 사용 등에서도 문제가 생겨 확인해보면
모두 Security 관련 설정 때문이었다. 계정 로그인 처리, 파일 업로드 문제, iframe 사용과 관련된 각각의 내용들을
모두 확인 후 적용하였으나 아직은 잘 모르는 부분이 많기에 아래 코드로 Security는 bypass하는 수준에서 적용을
마무리 하였다.


@Override
public void configure(WebSecurity web) throws Exception {
	// TODO Auto-generated method stub
	super.configure(web);
		
	web.ignoring().antMatchers("/**");
}


위 코드는 JAVA config 설정을 이용할 경우 WebSecurityConfigurerAdapter를 상속받은 config 클래스를 
생성하여 코딩하면 된다. Spring (boot)에서 Security를 사용하는 방법은 아래 링크를 참조하였다.


https://spring.io/guides/gs/securing-web/


DB 연결 설정


Spring을 이용하는 경우에는 보통 다음과 같은 과정을 거쳐 DB를 연결하고 CRUD 작업을 진행하였다.


  1. DataSource 처리를 위한 Config 클래스 생성
  2. 필요에 따라 properties 파일에 DB 연결정보 추가
  3. Service 인터페이스와 그 구현 클래스 생성
  4. Controller 클래스에서 의존성 주입을 통해 Service에 선언한 CRUD 메서드를 이용하여 작업


처음엔 이 과정만 생각하고 진행하다가 꽤 심한 삽질을 했다. 내가 ORM과 관련하여 JPA를 사용하도록 설정한 것을
깜빡 한 것이다. JPA를 이용할 경우에는 1번과 3번의 과정이 필요없다. application.properties에 DB 연결 설정만
등록하면 바로 DB와 연결이 되며 Entity 클래스와 Repository 인터페이스를 구현하여 사용하면 된다.


JPA를 통한 MySQL 연동은 아래 두 곳의 사이트에서 도움을 받았다.


https://docs.spring.io/spring-data/jpa/docs/2.0.5.RELEASE/reference/html/

https://www.callicoder.com/spring-boot-rest-api-tutorial-with-mysql-jpa-hibernate/


Elasticsearch API 사용


Spring 프로젝트 중에도 Spring Data Elasticsearch라는 관련 프로젝트가 있다. 관련 링크는 아래와 같다.


https://projects.spring.io/spring-data-elasticsearch/


하지만 내용을 살펴보면 알겠지만 가장 최신 버전의 릴리즈도 Elasticsearch 5.5.0까지만 지원을 한다.
아래 링크를 보면 Spring Data Elasticsearch의 각 버전과 그 버전에서 지원하는 Elasticsearch 버전이
정리되어 있다.


https://github.com/spring-projects/spring-data-elasticsearch


하지만 나는 이미 Ealsticsearch 6.2.1 버전을 설치한터라 안타깝게도 Spring Data Elasticsearch는
사용하지 못하고 별도로 6.2 버전대의 라이브러리를 pom.xml 파일에 다음과 같이 추가하였다.


<dependency>
	<groupId>org.elasticsearch</groupId>
	<artifactId>elasticsearch-core</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch</groupId>
	<artifactId>elasticsearch-hadoop-mr</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch</groupId>
	<artifactId>elasticsearch-spark-20_2.11</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch.client</groupId>
	<artifactId>elasticsearch-rest-high-level-client</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch</groupId>
	<artifactId>elasticsearch-hadoop</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch.client</groupId>
	<artifactId>elasticsearch-rest-client</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch</groupId>
	<artifactId>elasticsearch</artifactId>
	<version>6.2.2</version><!--$NO-MVN-MAN-VER$-->
</dependency>
<dependency>
	<groupId>org.elasticsearch.client</groupId>
	<artifactId>transport</artifactId>
	<version>6.2.2</version>
</dependency>
<dependency>
	<groupId>org.elasticsearch.plugin</groupId>
	<artifactId>transport-netty4-client</artifactId>
	<version>6.2.2</version>
</dependency>
<!-- Elasticsearch 설치 후 X-Pack을 설치했기 때문에 추가 -->
<dependency>
	<groupId>org.elasticsearch.client</groupId>
	<artifactId>x-pack-transport</artifactId>
	<version>6.2.2</version>
</dependency>


현재 상태에서 모든 라이브러리가 다 필요한 것은 아니지만 추후 Hadoop이나 Spark와의 연동도 염두에 두고 있기에
그냥 함께 설치하였다.


Excel parsing


Excel 파일을 분석하는 것은 가장 많이 사용하는 POI 라이브러리를 사용하였다.
하지만 일반적으로 알려진 사용법으로는 벌써 이 단계에서 50만건을 처리하는데 OOM이 발생하였다.
해결책을 찾아야 했다. 게으른 개발자의 숙명으로 직접 코딩을 해야 하는 방법보다는 누군가 만들어놓은 라이브러리가
없을까를 우선하여 구글링을 하였다…ㅠ.ㅠ


역시나 세상에는 나같은 불쌍한 중생을 거둬 먹이는 천사같은 분들이 늘 존재한다. 마침 내가 필요로 하는 용도의
라이브러리가 똭! 눈에 띄였다. 이 라이브러리를 설치하여 사용하니 50만건을 OOM 없이 빠른 속도로 parsing해
주었다. 라이브러리는 pom.xml에 다음과 같이 추가하면 된다.


<dependency>
    <groupId>com.monitorjbl</groupId>
    <artifactId>xlsx-streamer</artifactId>
    <version>1.2.0</version>
</dependency>


라이브러리 소스는 아래 링크에서 확인할 수 있다.


https://github.com/monitorjbl/excel-streaming-reader


정리


가장 첫 단계로 Spring boot 및 java 프로그래밍 관련된 내용을 가지고 포스팅을 해보았다. 주 목적이 Spring boot나
java 프로그래밍이 아니므로 부족한 내용이 많겠지만 이와 관련해서는 더 자세하고 정확한 설명이 있는 사이트나 
블로그를 참조하는 편이 더 나을 것이다.


자꾸 요상한(?) 것들에 관심을 가지게 되면서 최종 롤인 iOS 개발자로서도 또 그 이전까지의 롤이었던 java개발자로서도
점점 역량이 떨어지는 것 같다…ㅠ.ㅠ 드문드문 Spring 또는 Spring boot를 접하다보니 별 것 아닌 일로 시간을 
허비하기 일쑤다. 그래도 불행 중 다행이랄까? 요즘은 워낙 양질의 자료를 다양한 경로로 쉽게 구할 수 있다보니 그럭저럭
이정도나마 하고 있지 않나 싶다.


다음 포스팅에서는 Elasticsearch의 JAVA API와 관련된 내용을 조금 더 상세하게 살펴보고 또 Elasticsearch의
설정 몇가지를 함께 알아보겠다.


전체 소스를 공유하려고 했는데 오늘 문득 프로젝트명, github의 레포지터리명, 프로젝트의 패키지명 등이 맘에 들지
않아 전체적으로 수정을 하고 있어 당장에는 힘들 것 같다. 이 시리즈가 마무리되는 시점에(이 글 포함 3개의 포스팅으로
계획 중) 전체 소스를 공유하도록 하겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

3D 프린터 개봉기 및 간단 사용기 - USEED Creator mini


2016년, 아두이노를 처음 접하면서 나에겐 신세계가 열렸다!
애초에 현재의 직업인 개발자를 선택한 이유 중의 하나도 새로운(?) 무언가를 만들어낸다는 점이었는데 아두이노는
실물을 만지고 만들어낸다는 점이 더 역동적이고 흥미를 느끼게 한 것이다. 그리고 항상 좋은 소프트웨어는 좋은
하드웨어와 결합했을 때 그 진가가 발휘된다는 생각을 갖고 있던 나에게 아두이노는 더없는 도전의 대상이었다.


그러나 여기에도 장벽이 있었으니…알맹이는 어찌어찌 만들만한 기술을 익혔는데 이 알맹이를 실용적으로 포장할만한
껍데기는 도무지 제대로 만들 수가 없었다. 커터와 자 그리고 하드보드를 이용해서 쌩 노가다도 해보았고 돈을 들여
MDF로 레이저 커팅도 해보았으나 마음에 쏙 드는 결과물은 얻을 수가 없었다. 특히나 외장 문제로 가장 힘들었던 것이
바로 4족 보행 로봇과 싱글콥터(프로펠러 1개짜리 드론)를 만들 때였다. 이 두 작업은 외장 부품을 제대로 만들지
못해서 현재 중단 상태라고 봐도 과언이 아니다(사실은 본업과 관련된 프로젝트 때문에 손을 못대고 있는 부분이 더 크다). 그렇게 나에게 3D 프린터 장만은 중요한 숙원 사업이 되었다. 


그리고 드디어 그 숙원 사업을 달성하게 되었다.
오늘은 간단하게 개봉기와 샘플 출력기를 적어볼까 한다.
이 글은 아무런 협찬도 받지 않았으며 생애 첫 3D 프린터인 만큼 비교 대상도 없어 장점과 단점에 대해서는
완전히 개인적인 판단임을 참고하기 바란다.


USEED Creator mini


사실 그 동안 3D 프린터를 장만하지 못했던 것은 어처구니 없게도 공간의 문제였다. 사실 이미 banggood.com 같은
해외 사이트에 보면 10만원대 초반 가격대의 제품도 심심치않게 찾아볼 수 있을 만큼 이미 3D 프린터의 가격 장벽은
거의 없다고 보아야 할 것이다. 물론 출력물의 품질은 큰 문제가 되니 꼼꼼히 따져볼 필요가 있다. 그렇기에 아무래도
너무 싼 가격의 제품은 꺼려질 수밖에 없기도 하다. 아래 이미지는 banggood.com에서 15만원 ~ 25만원 사이의
3D 프린터를 검색한 결과 중 일부이다.




어쨌든 나에게 중요한 것은 그 무엇도 아니고 “공간”이었다. 그러다가 발견한 것이 바로 USEED라는 업체의
Creator mini라는 제품이었다(크레이터 미니라고 읽는다). 이 제품을 처음 접한 곳은 페이스북의 타임라인에 
게시된 광고였다.


처음 호감을 갖게된 것은 탄탄해보이는 외관과 완성품이라는 점이었다. 저가 모델의 대부분이 직접 조립을 해야
하는 경우가 많은데 아무리 아두이노로 무언가를 만드는게 좋아도 이건 또 별개의 문제다…-.- 차라리 모든 부품을
직접 하나 하나 구매해서 만들면 또 모를까…(사실 다음 아두이노 프로젝트로 계획하고 있는 것이 로봇 Arm 형태의
3D 프린터이다)


그리고 제품 판매처의 사용자 후기에 올라온 샘플 출력 이미지도 한몫 했다. 3D 프린터 무경험자인 내가 보기엔
더없이 훌륭한 품질을 보여주었다. 비슷한 크기의 XYZ사의 다빈치 나노도 고려를 했지만 샘플 출력물의 품질이
영 맘에 들지 않아 고민을 했었는데 크레이터 미니의 출력물을 보고 마음을 굳히게 되었다. 물론 설정에 따라 품질은
달라지고 또 실물과 사진은 차이가 있겠지만…


Creator mini의 스펙은 판매처 이미지로 대신한다.


Creator mini



개봉기


역시나 제품 사이즈는 아담했다. 포장 박스의 사이즈도 작아 사무실에서 물건을 받고 집으로 가져오는데도 큰 부담이 
없었다. 집에 들고올 때 손잡이 만드느라고 박스에 테이프가 조금 지저분하게 붙어있다…^^;;




드디어 박스 개봉!
가장 위에는 작은 박스가 하나 있는데 여기에는 기본 제공되는 필라멘트와 각종 도구들 및 부품들이 들어있다.
도구와 부품은 측면을 막아주는 투명 아크릴판, 정면의 투명 아크릴 문, 필라멘트 거치용 부품, 핀셋, USB 메모리,
USB 케이블, SD카드 1장과 SD카드 어댑터, 전원 케이블과 어댑터 등이다.


Creator mini boxCreator mini 부품Creator mini 부품



부품 박스를 들어내면 완충재가 보이고 이 완충재를 걷어내면 드디어 아담한 3D 프린터 본체가 보인다.




그리고 사용법을 적은 프린트물 1권, 제품 안내서 1 장, USEED의 타 제품에 대한 안내 브로셔 1장이 들어있다.




이제 본체를 꺼내보자! 탄탄한 철제 프레임이 내구성에 대한 신뢰감을 주었다. 색상이 검은색이어서 그런지 겉보기에는
크 작은 크기와는 달리 꽤나 묵직하고 듬직해보였다. 각각 정면, 상단, 측면에서 찍은 사진들이다.


Creator mini frontCreator mini topCreator mini side



후면에는 필라멘트 거치대를 위한 홈들과 필라멘트가 이동할 튜브가 붙어있다. 부품박스에 있는 필라멘트 거치대 부품
2개를 원통형으로 조립한 후 후면의 홈에 꽂고 시계방향으로 돌리면 거치대 설치는 완료다. 그런데 거치대가 꼭 맞지
않고 조금 헐렁거려 살짝 불안하기도 하다. 이렇게 한 후 필라멘트를 걸고 튜브 안으로 필라멘트를 넣어 반대편으로
나온 필라멘트를 익스트루더에 꽂아주면 된다(간단한 작업이라 사진은 생략…^^;;;).


Creator mini back



측면 보호 판넬은 네 귀퉁이를 동봉된 핀으로 고정하면 되고 문은 경첩부위에 위에서 아래로 꽂아주면 끝이다.
이렇게 해서 모든 조립이 완료되었다.


Creator mini full setting



이렇게 완성된 프린터를 책상위 올려주었다. 책상 위에 올리니 정말로 딱 알맞은 사이즈의 프린터를 샀구나 하는
생각이 절로 났다. 원래 사용하지 않는 17인치 LCD 모니터가 있던 자리인데 치우고나니 답답했던 분위기도 가시고
여러모로 좋았다.


Creator mini setting complete



평가


사실 장점이나 단점을 따지는 것이 무의미하게 느껴진다. 앞서 말한바와 같이 다른 기기를 사용해본 적이 없는 상태로
마땅히 비교할 대상도 없는데다가 단점으로 꼽을만한 것들이 대체로 제조사에서 ‘가정용, 교육용’으로 용도를 명시해서
판매하고 있는만큼 그 용도에서는 단점이 될 수 없다는 것이 내 생각이다. 따라서 몇가지 개인적으로 느낀점만 적어
보도록 하겠다.


가장 만족스러운 부분은 제조사의 서비스이다. 호불호가 갈릴 수도 있지만 택배사를 이용하지 않고 직접 직원이 배송을
하여 설치 지원도 해주고 4개정도의 출력물 샘플도 주고 네이버 톡을 이용하여 늦은시간까지 질문에 답변을 해주는 등
개인적으로는 매우 만족스러웠다.


앞에서도 계속 언급한 바와 같이 작은 사이즈는 공간 활용면에서 충분히 매리트가 있다. 다만 출력사이즈 역시
작으므로 이 점은 고려하여야 한다.


출력 품질은 개인적으로 매우 만족스럽다. 아래 이미지는 제조사에서 샘플 출력물로 제공해준 것 중 하나인데
꽤나 매끄러운 표면을 보여준다.




그리고 출력 시간과 소음인데…이 부분은 다른 제품과의 비교를 통해 평가해야 할 부분인 것 같아 간단히 샘플
출력 기준으로 테스트 내용만 적어보자면 위 사자의 경우 속도 25%로 하여 진행해보니 약 10시간 정도 걸릴 것 같고
(너무 오래 걸려서 프린팅 진행 중 중지시켜버렸다…-.-) 샘플로 들어있던 실사이즈 레고 캐릭터의 경우 속도를 50%로 
하여 약 4시간 정도 걸렸다. 자세한 내용은 출력기를 따로 포스팅하겠다. 소음은 개인차가 있을 것 같지만 나같이
무던한 성격인 경우 옆에 있어도 그냥저냥 참을만 하다^^;;


매뉴얼의 경우 품질이 다소 아쉬웠다. A4 용지에 인쇄한후 스테플러로 찍은 것으로 일단 인쇄된 사진이 알아보기
힘들었고 전체적으로 내용도 아쉬움이 있다. 최소한 전체 메뉴에 대한 설명이라도 있었으면 좋겠다. 모든 메뉴를
다 사용하는 것은 아니지만 그래도 눈에 보이는 메뉴들인데 각각이 어떤 기능을 하는지는 알아 두는 것이 좋지 않을까?
그밖에 실사용과 관련된 내용들은 유튜브에 동영상으로 잘 정리가 되어있어 이 점은 맘에 들었다.


그밖에 제품 디자인과 프레임은 크게 만족스럽지만 측면과 정면의 투명 아크릴은 조금 약해 보여 충격에 주의해야 할 
것같다.


슬라이서 프로그램으로는 Repetire라는 프로그램을 동영상과 매뉴얼로 설명하고 있으나 스펙에도 있듯이 CURA도
사용 가능하다.


정리


작년 말에 3D 프린터 구매가 힘들 것 같아 무료로 사용할 수 있는 공간을 알아본 후 성수 메이커스페이스에 교육
신청을 하고 이용해보려고 하였다. 그런데 교육 당일 갑자기 집에 우환이 생기는 바람에 사전 연락도 못한 채 교육을
불참하게 되었다. 그래서 더더욱 미련이 남았었는데…이렇게 구입을 하게 되었다.


아무리 명필이 붓을 가리지 않는다고는 하지만 커터와 자로 3D 프린터를 당해낼 수는 없는 것이다…ㅠ.ㅠ
하지만 좋은 도구라도 어떻게 사용하느냐에 따라 그 가치가 달라지는 것 역시 사실이다.


당분간은 본업과 관련된 공부로 아이들 장난감이 좀 뽑아줘야 할 것 같고 틈틈이 준비를 하여 올 하반기부터
본격적으로 그동안 미뤄두었던 로봇, 드론 등의 작업을 다시 시작하게 될 것이다. 3D 프린터를 이용하여 작업을
할 때마다 간단하게 포스팅을 하여 좀 더 많은 정보를 공유하도록 하겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

TensorFlow







목차



3소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10 ~ cifar10_train.py (이번 글)





소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


이제 가장 중요한 부분은 지나갔다.
생각해보면 전체적인 흐름을 먼저 살펴보고 세부적인 내용들을 분석했어야 할 것 같은데 순서가 거꾸로 되어버렸다.
아무래도 전체 포스팅을 마무리한 후 다시 한 번 되짚는 과정을 거쳐야 할 것 같다.


앞서 분석한 내용들은 모델을 구성하고 loss값을 생성하고 optimizer를 적용하는 구체적인 내용들이었다.
처음 딥러닝을 공부할 때는 각각의 단계가 거의 1줄 코딩이었던 것을 생각하면 이 소스는 매우 복잡해보인다.
그러나 세부적인 설정들이 더 추가되었을 뿐 근본적인 맥락은 다를 바가 없다.


자세한 내용은 복습 시간에 다시 살펴보고 오늘은 사용자와 인터페이스하는 소스를 살펴보도록 하자.


cifar10_train.py


소스 분석에 들어가기 전에 참고로 이 소스를 훈련시켰을 때의 정확도가 소스 첫머리의 주석에 표시되어있다.


accuracy



뭐 흙수저가 사용할 수 있을만한 장비는 아닌 듯하니 그냥 그런가보다 하고 넘어가자…-.-

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from datetime import datetime
import time

import tensorflow as tf

import cifar10


첫 3줄은 앞서도 보았듯이 python 2와 3의 호환을 위한 것이고 datetime과 time은 이름에서도 알 수 있듯이 날짜와
시간을 사용하기 위한 것으로 print를 하거나 수행 시간을 체크하기 위한 용도로 import 하였다. 마지막 2줄도 생략

FLAGS = tf.app.flags.FLAGS

tf.app.flags.DEFINE_string('train_dir', '/tmp/cifar10_train',
                           """Directory where to write event logs """
                           """and checkpoint.""")
tf.app.flags.DEFINE_integer('max_steps', 1000000,
                            """Number of batches to run.""")
tf.app.flags.DEFINE_boolean('log_device_placement', False,
                            """Whether to log device placement.""")
tf.app.flags.DEFINE_integer('log_frequency', 10,
                            """How often to log results to the console.""")


FLAG 역시 이전 포스팅에서 설명을 하였는데 그 아래 tf.app.flags.DEFINE_XXX로 지정한 이름으로 그 값을
사용할 수 있다. 즉, FLAG.train_dir은 '/tmp/cifar10_train’라는 값을 가지고 있게 된다. 두 번째 줄에 보면 학습
step을 1000000회로 설정하였다.


train()

# 학습을 실행시키는 함수
def train():
  """Train CIFAR-10 for a number of steps."""

# with tf.Graph().as_default() 문장은 지금까지 만들었던 모든 그래프 구성 요소(operation과 tensor들)을
# 하나의 전역 Graph 안에서 사용하겠다는 의미이다.  
  with tf.Graph().as_default():
# global_step은 학습의 step 카운트를 자동으로 관리해주는 tensor로 사용자가 별도로 step을 카운트
# 할 필요가 없이 이 global_step을 이용하면 된다.
    global_step = tf.train.get_or_create_global_step()

    # Get images and labels for CIFAR-10.
    # Force input pipeline to CPU:0 to avoid operations sometimes ending up on
    # GPU and resulting in a slow down.
# 학습을 수행할 장치를 지정. 첫 번째 CPU를 사용하도록 지정하고 있다. GPU를 사용하는 방법은
# cifar10_multi_gpu_train.py 소스를 참조하면 된다. 비록 multi gpu를 사용하는 소스지만...-.-
    with tf.device('/cpu:0'):
# 학습에 사용할 미니 배치 크기의 image와 label을 가져온다.
# 자세한 내용은 cifar10.py 소스의 distorted_inputs함수 참조
# http://mazdah.tistory.com/814
      images, labels = cifar10.distorted_inputs()

    # Build a Graph that computes the logits predictions from the
    # inference model.
# 학습 모델 생성. 자세한 내용은 cifar10.py 소스의 inference함수 참조
# http://mazdah.tistory.com/814
    logits = cifar10.inference(images)

    # Calculate loss.
# 손실값 계산. 자세한 내용은 cifar10.py 소스의 loss함수 참조
# http://mazdah.tistory.com/814
    loss = cifar10.loss(logits, labels)

    # Build a Graph that trains the model with one batch of examples and
    # updates the model parameters.
# 실제 학습을 수행할 operation 생성. 자세한 내용은 cifar10.py 소스의 loss함수 참조
# http://mazdah.tistory.com/814
    train_op = cifar10.train(loss, global_step)

# 아래 나오는 tf.train.MonitoredTrainingSession에 사용하기 위한 로그 hooker
# MonitoredTrainingSession.run() 호출에 대한 로그들을 hooking하는 역할을 한다.
# Pythons에서는 클래스 선언 시 ( )안에는 상속할 클래스를 지정한다. 즉, _LoogerHook 클래스는
# tf.train.SessionRunHook 클래스를 상속하여 만들어지게 되며 정의된 함수들은 이 클래스의
# 함수들을 Overriding해서 구현한 함수들이다.
    class _LoggerHook(tf.train.SessionRunHook):
      """Logs loss and runtime."""

# session을 이용할 때 처음 한 번 호출되는 함수
      def begin(self):
        self._step = -1
        self._start_time = time.time()

# run() 함수가 호출되기 전에 호출되는 함수
      def before_run(self, run_context):
        self._step += 1
        return tf.train.SessionRunArgs(loss)  # Asks for loss value.

# run() 함수가 호출된 후에 호출되는 함수
      def after_run(self, run_context, run_values):
        if self._step % FLAGS.log_frequency == 0:
          current_time = time.time()
          duration = current_time - self._start_time
          self._start_time = current_time

          loss_value = run_values.results
          examples_per_sec = FLAGS.log_frequency * FLAGS.batch_size / duration
          sec_per_batch = float(duration / FLAGS.log_frequency)

          format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
                        'sec/batch)')
          print (format_str % (datetime.now(), self._step, loss_value,
                               examples_per_sec, sec_per_batch))

# 분산 환경에서 학습을 실행할 때 사용하는 Session. 분산 환경에 대한 지원을 해준다.
# (Hook를 이용한 로그 관리, 오류 발생시 복구 처리 등)
    with tf.train.MonitoredTrainingSession(
        checkpoint_dir=FLAGS.train_dir,
        hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
               tf.train.NanTensorHook(loss),
               _LoggerHook()],
        config=tf.ConfigProto(
            log_device_placement=FLAGS.log_device_placement)) as mon_sess:
      while not mon_sess.should_stop():
# 드디어 마무리~ 학습 operation을 실제로 수행시킨다.
        mon_sess.run(train_op)



main(argv=None)

# CIFAR-10 데이터를 다운로드 받아 저장. cifar10.py 소스 참조
#  http://mazdah.tistory.com/814
cifar10.maybe_download_and_extract()

# 학습 수행 중의 로그를 저장할 디렉토리 생성. 기존에 동일 디렉토리가 있다면 삭제 후 생성.
if tf.gfile.Exists(FLAGS.train_dir):
  tf.gfile.DeleteRecursively(FLAGS.train_dir)
tf.gfile.MakeDirs(FLAGS.train_dir)

# 학습 시작
train()



정리


소스 길이에 비해 분석하는 데 너무 많은 시간이 걸렸다…ㅠ.ㅠ
지난 포스팅에서도 언급한 것처럼 매개 변수나 리턴값들이 모두 tensor 형태이고 TensorFlow의 API 문서에 있는
내용들이 수학적인 내용을 많이 포함하고 있어 다른 언어나 프레임워크의 문서를 읽는 해석하는 것에 비해 원문
해석도 꽤나 어려웠다.


포스팅한 내용에 부정확한 내용이 있을지도 모르겠기에 일단 CIFAR-10 예제 코드를 실제로 돌려보고
그 중간 로그나 결과 값들과 비교해가면서 다시 한 번 찬찬히 살펴볼 필요가 있을 것 같다. 그리고 추후에 이 소스에 
쓰인 API들을 별도로 정리해보겠다.


소스 중에는 아직 평가를 위한 cifar10_eval.py이 남아있는데 요건 우선 학습 관련 내용을 마무리하고 
진행해보도록 하겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^

Tensorflow









소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


지난 포스팅에서 살펴보았던 cifar10_input.py는 데이터를 불러와서 이미지를 임의 조작한 후 배치 사이즈 크기로
나누어 담아 리턴해주는 기능을 하였다. 전체 프로세스의 가장 첫 단계라고도 할 수 있다.


오늘 살펴볼 cifar10.py는 가장 핵심적인 소스로 모델과 네트워크를 구성하는 내용이 주를 이루고 있다.
그만큼 코드의 길이도 전체 소스 중 가장 길다.


차근차근 살펴보도록 하자.


cifar10.py

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

지난 포스팅과 마찬가지로 위 3줄의 import 문은 python 2와 3의 호환성을 위한 것이다.


# OS의 자원을 사용할 수 있게 해주는 모듈
import os
# 정규표현식을 사용할 수 있게 해주는 모듈
import re
# python interpreter에 의해 관리되는 변수나 함수에 접근할 수 있도록 해주는 모듈
import sys
# tar 압축을 핸들링할 수 있게 해주는 모듈
import tarfile

여러 다양한 기능을 사용할 수 있게 해주는 import문들이다. os, sys, tarfile 등은 원격으로 cifar10 데이터셋을
다운로드 받기 위해 쓰인다.


# six 모듈은 python 2와 3의 함수를 함께 사용할 수 있도록 해줌. urllib는 URL 관련 모듈로 역시
# 데이터 셋 다운로드에 사용된다.
from six.moves import urllib
#텐서플로우 모듈
import tensorflow as tf

# 지난 포스팅에서 살펴본 cifar10_input.py 참조
import cifar10_input

몇가지 모듈이 추가로 import 되었으나 대부분 CIFAR10 데이터 셋을 원격으로 다운로드 받기 위한 것으로 이미
별도로 데이터 셋을 다운로드 받아두었다면 무시해도 좋을 것이다.


소스 앞부분에 영문으로 중요한 함수에 대한 설명이 주석으로 달려있다. 일단 간단하게 그 내용을 살펴보면 
다음과 같다.


  • inputs, labels = distorted_inputs( )
    : 학습에 사용할 데이터를 불러온다. 이 함수 안에서 cifar10_input.py에 있는 distorted_inputs( )
    함수를 호출하여 처리한다.
  • predictions = inference(inputs)
    : 파라미터로 전달되는 모델(inputs)에 대한 추론을 계산하여 추측한다.
  • loss = loss(predictions, labels)
    : 해당 라벨에 대해 예측값에 대한 총 손실 값을 구한다.
  • train_op = train(loss, global_step)
    : 학습을 수행한다.


위의 4개 함수가 가장 핵심적인 내용이라 할 수 있다.
이제 전체 코드를 차근차근 살펴보자.


# tf.app.flags.FLAGS는 상수의 성격을 갖는 값을 관리하는 기능을 한다.
# tf.app.flags.DEFINE_xxx 함수를 이용하여 첫 번째 파라미터에 사용할 이름을 넣고
# 두 번째 파라미터에 사용할 값을 설정하면 이후 'FLAGS.사용할 이름' 형식으로 그 값을
# 사용할 수 있다. 아래 첫 번째 코드의 경우 FLAGS.batch_size라는 코드로 128이라는 값을
# 사용할 수 있다.
FLAGS = tf.app.flags.FLAGS

# Basic model parameters.
tf.app.flags.DEFINE_integer('batch_size', 128,
                            """Number of images to process in a batch.""")
tf.app.flags.DEFINE_string('data_dir', '/tmp/cifar10_data',
                           """Path to the CIFAR-10 data directory.""")
tf.app.flags.DEFINE_boolean('use_fp16', False,
                            """Train the model using fp16.""")

# Global constants describing the CIFAR-10 data set.
# cifar10_input.py에 정의되어있던 값들
IMAGE_SIZE = cifar10_input.IMAGE_SIZE  #24
NUM_CLASSES = cifar10_input.NUM_CLASSES  #10
NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN  #50000
NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = cifar10_input.NUM_EXAMPLES_PER_EPOCH_FOR_EVAL #10000


# Constants describing the training process.
# tf.train.ExponentialMovingAverage에서 사용할 가중치 값
MOVING_AVERAGE_DECAY = 0.9999     # The decay to use for the moving average.
# 학습 속도 감소 후의 epoch 수
NUM_EPOCHS_PER_DECAY = 350.0      # Epochs after which learning rate decays.
# 학습률 감소를 위한 변수
LEARNING_RATE_DECAY_FACTOR = 0.1  # Learning rate decay factor.
# 초기 학습률
INITIAL_LEARNING_RATE = 0.1       # Initial learning rate.


음…수학을 깊이 들어가긴 싫지만 얼레벌레 그냥 넘어가는 것도 그러니 몇 가지 개념은 좀 알아보고 가자.


Exponential Moving Average

우선 이동평균(Moving Average)라는 것은 특정 기간동안 내에 측정된 값의 평균을 의미한다.
이 이동평균에는 단순이동평균, 가중이동평균, 그리고 여기서 사용하는 지수이동평균이 있는데
이 지수이동평균은 가장 최근 값에 더 큰 가중치를 두어 평균을 계산하는 방식이라고 한다.


일단 위 코드 중 MOVING_AVERAGE_DECAY 이후의 설정들은 모두 학습률 조정을 위한 것으로
train( ) 함수에서 사용을 하게 된다. 기본적으로 학습이 진행됨에 따라 학습률을 기하급수적으로 감소시켜
나가는 방법을 취하고 있다. 자세한 내용은 train( ) 함수 설명에서 다시 한 번 분석해보자.


# If a model is trained with multiple GPUs, prefix all Op names with tower_name
# to differentiate the operations. Note that this prefix is removed from the
# names of the summaries when visualizing a model.
# 멀티 GPU를 사용하여 병렬 처리할 때 작업 이름을 구분하기 위한 구분자...언제 써볼 수 있을까...ㅠ.ㅠ
TOWER_NAME = 'tower'

# CIFAR-10의 데이터 경로
DATA_URL = 'https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz'


이제부터는 함수를 하나 하나 살펴보도록 하겠다.


_activation_summary(x)

# 이 함수는 전체적으로 각 레이어들을 텐서 보드에 시각화하기 위한 summary를 만드는 작업을 한다.
"""Helper to create summaries for activations.
  Creates a summary that provides a histogram of activations.
  Creates a summary that measures the sparsity of activations.
  Args:
    x: Tensor
  Returns:
    nothing
  """
  # Remove 'tower_[0-9]/' from the name in case this is a multi-GPU training
  # session. This helps the clarity of presentation on tensorboard.
  tensor_name = re.sub('%s_[0-9]*/' % TOWER_NAME, '', x.op.name)
  tf.summary.histogram(tensor_name + '/activations', x)
  tf.summary.scalar(tensor_name + '/sparsity',
                                       tf.nn.zero_fraction(x))


_variable_on_cpu(name, shape, initializer)

# 파라미터로 전달받은 값을 이용하여 CPU를 이용하여 처리할 변수를 생성
"""Helper to create a Variable stored on CPU memory.
  Args:
    name: name of the variable
    shape: list of ints
    initializer: initializer for Variable
  Returns:
    Variable Tensor
  """
# 0번째 CPU를 사용하겠다고 지정
  with tf.device('/cpu:0'):
# python의 3항 연산 FLAGS.use_fp16이 true이면 tf.float16을 사용하고 false이면 
# else 뒤의 tf.float32를 사용. 이 코드에서는 FLAGS.use_fp16를 false로 설정했으므로
# tf.float32를 사용하게 됨
    dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
# 파라미터로 전달된 변수가 이미 존재하면 재활용하고 존재하지 않으면 새로 만든다.
# 참고로 tf.Variable는 무조건 새로운 변수를 만든다. 자세한 사용법은 아래 링크 참조
# https://tensorflowkorea.gitbooks.io/tensorflow-kr/content/g3doc/how_tos/variable_scope/
    var = tf.get_variable(name, shape, initializer=initializer, dtype=dtype)
  return var


_variable_with_weight_decay(name, shape, stddev, wd)

# 위의 _variable_on_cpu(name, shape, initializer) 함수를 이용하여 정규화 처리를 한 변수를 생성.
"""Helper to create an initialized Variable with weight decay.
  Note that the Variable is initialized with a truncated normal distribution.
  A weight decay is added only if one is specified.
  Args:
    name: name of the variable
    shape: list of ints
    stddev: standard deviation of a truncated Gaussian
    wd: add L2Loss weight decay multiplied by this float. If None, weight
        decay is not added for this Variable.
  Returns:
    Variable Tensor
  """
# 데이터 타입 설정
# 세 번째 파라미터는 초기화 함수를 리턴하여 넘기는 것으로 truncated_normal_initializer는
# 정규분포 기반의 초기화 함수로 표준편차의 양 끝단을 잘라낸 값으로 새로운 정규분포를 만들어 
# 초기화 한다.
  dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
  var = _variable_on_cpu(
      name,
      shape,
      tf.truncated_normal_initializer(stddev=stddev, dtype=dtype))

# L2 정규화 처리를 위한 코드. wd(아마도 Weight Decay)값이 None이 아닌 경우 if문
# 안의 코드를 수행하여 정규화 처리를 하고 그래프에 추가한다.
# tf.nn.l2_loss는 전달받은 텐서의 요소들의 제곱의 합을 2로 나누어 리턴한다.
  if wd is not None:
    weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
    tf.add_to_collection('losses', weight_decay)
  return var


위 함수들은 실제로 학습을 진행하면서 결과 값을 예측하는 과정에 사용되는 함수들이다.
자세한 내용들은 올바른 예측을 하기 위한 알고리즘을 구성하는 수학적인 내용이 포함되어있어
당장에는 이해가 쉽지 않다. 예를 들어 tf.truncated_normal_initializer의 경우 정규분포
그래프에서 2개 이상의 표준편차를 제거한 값들로 새롭게 만들어진 그래프로 초기화 한다고 해석이
되는데 사실 내용자체도 이해가 되지 않고 더 심각한 것은 수학적 개념이 포함된 영어를 해석하자니
제대로 해석이 되었는지도 모르겠다…ㅠ.ㅠ 일단은 학습을 최적화 시키고자 하는 목적으로 이러한
장치들을 사용한다는 것만 알아두면 되겠다.


distorted_inputs()

# cifar10_input.py에 있는 같은 이름의 함수를 이용하여 학습할 데이터를 불러온다.
"""Construct distorted input for CIFAR training using the Reader ops.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  Raises:
    ValueError: If no data_dir
  """

# 데이터 경로가 지정되어있지 않으면 에러~
  if not FLAGS.data_dir:
    raise ValueError('Please supply a data_dir')

# 데이터 경로를 조합하여 최종적으로 사용할 이미지와 라벨을 가져옴
  data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
  images, labels = cifar10_input.distorted_inputs(data_dir=data_dir,
                                                  batch_size=FLAGS.batch_size)

# FLAGS.use_fp16 값이 true이면 이미지와 라벨 텐서의 요소들을 tf.float16 타입으로 형변환 한다.
# 하지만 코드에는 False로 지정되어있으므로 무시.
  if FLAGS.use_fp16:
    images = tf.cast(images, tf.float16)
    labels = tf.cast(labels, tf.float16)
  return images, labels


inputs(eval_data)

# 역시 cifar10_input.py에 있는 같은 이름의 함수를 이용하여 평가할 데이터를 불러온다.
# eval_data라는 파라미터가 추가된 것 외에는 distorted_inputs 함수와 내용 동일
"""Construct input for CIFAR evaluation using the Reader ops.
  Args:
    eval_data: bool, indicating if one should use the train or eval data set.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  Raises:
    ValueError: If no data_dir
  """
  if not FLAGS.data_dir:
    raise ValueError('Please supply a data_dir')
  data_dir = os.path.join(FLAGS.data_dir, 'cifar-10-batches-bin')
  images, labels = cifar10_input.inputs(eval_data=eval_data,
                                        data_dir=data_dir,
                                        batch_size=FLAGS.batch_size)
  if FLAGS.use_fp16:
    images = tf.cast(images, tf.float16)
    labels = tf.cast(labels, tf.float16)
  return images, labels


inference(images)

# 이 소스의 핵심으로 예측을 위한 모델을 구성하는 함수
"""Build the CIFAR-10 model.
  Args:
    images: Images returned from distorted_inputs() or inputs().
  Returns:
    Logits.
  """
  # We instantiate all variables using tf.get_variable() instead of
  # tf.Variable() in order to share variables across multiple GPU training runs.
  # If we only ran this model on a single GPU, we could simplify this function
  # by replacing all instances of tf.get_variable() with tf.Variable().
  #
  # conv1
# convolution 레이어 1
  with tf.variable_scope('conv1') as scope:
# 커널(필터) 초기화 : 5 X 5 크기의 3채널 필터를 만들며 64개의 커널을 사용한다.
    kernel = _variable_with_weight_decay('weights',
                                         shape=[5, 5, 3, 64],
                                         stddev=5e-2,
                                         wd=None)
    conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')


이 부분은 CNN의 핵심이며 가장 중요한 부분이므로 좀 더 상세하게 알아보자.
일단 필터(커널보다 친숙하므로 앞으로는 ‘필터’로만 표기하겠다. 또한 원칙적으로는 bias까지 +되어야 완성된
필터라 할 수 있으나 우선은 bias를 무시하고 생각해보자)가 하는 역할이 무엇인지부터 알아보면 말 그대로 
이미지에서 지정된 영역의 특징만을 ‘걸러내는’ 역할을 한다. 


그러면 어떤 방식으로 특징을 걸러내는가?
바로 머신러닝이나 딥러닝을 처음 배울때 배웠던 xW + b의 함수를 사용해서 처리한다. 일단 bias는 무시하기로
했으니 xW만 생각해본다면 입력받은 이미지에서 필터와 겹치는 부분을 x라 보고 해당 위치의 필터를 W라 보아
x1* W1 + x2 * W2 + … + xn * Wn이 되는 것이다. 만약 3 X 3 필터를 사용하였다면 아래와 같이 계산할 수
있다.


x1 * W1 + x2 * W2 + x3 * W3 + ... x9 * W9


여기에 만일 입력 채널(이미지의 색상 채널)이 3이라면 각 채널마다 위의 계산을 적용한 후 각 채널별 출력값을
최종 더해서 하나의 feature map을 만들게 된다. 결국 하나의 필터가 하나의 feature map을 만들게 되므로
만일 필터를 여러개 사용한다면 feature map의 개수도 필터의 개수와 동일하게 만들어지고 이 수가 곧 
feature map의 채널이 된다(그리고 이 각각의 채널에 bias를 +하게 된다). 


이 내용을 이해 못해 수없이 구글링을 했으나 적절한 자료를 찾지 못했는데 아래 이미지를 보고 쉽게 이해할 수 있었다.


CNN Filter feature map

이미지 출처 : http://taewan.kim/post/cnn/


이 코드를 가지고 계산을 해보면 24 X 24 크기의 3채널 이미지를 입력으로 받아 5 X 5 크기의 3채널 필터 64개를
사용하고 padding이 원본 크기와 동일한 출력이 나오도록 SAME으로 지정되었으므로 24 X 24 크기에 64 채널을
가진 출력이 나올 것이다. 여기에 배치 사이즈가 128이므로 최종 출력 텐서의 shape는 [128, 24, 24, 64]가 된다.


# 바이어스 초기화
# 채널의 개수가 64개이므로 bias도 64개 설정. biases는 64개의 요소가 0.0으로 채워진
# vector
    biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))

# 가중치 + 바이어스. biases는 conv의 마지막 차수와 일치하는 1차원 텐서여야 한다.
    pre_activation = tf.nn.bias_add(conv, biases)

# 활성화 함수 추가
    conv1 = tf.nn.relu(pre_activation, name=scope.name)

# 텐서 보드에서 확인하기 위한 호출
    _activation_summary(conv1)

  # pool1
# 풀링 레이어 1
# pooling은 간단하게 말해 이미지를 축소하는 단계로 필터로 주어진 영역 내에서 특정한 값(평균,최대,최소)을
뽑아내는 작업이다. 일단 최대값을 뽑는 것이 가장 성능이 좋다고 하여 max pooling을 주로 사용한단다.
# 이 코드에서는 필터 크기가 3 X 3이므로 이 영역에서 가장 큰 값만을 뽑아 사용한다. stride는 2를 사용한다.
  pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
                         padding='SAME', name='pool1')
  # norm1
# local response normalization라는 정규화 처리인데 ReLu 사용시 에러율 개선에 
# 효과가 있다고 하는데 이 부분은 좀 더 확인이 필요함
  norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
                    name='norm1')

  # conv2
# convolution 레이어 2
  with tf.variable_scope('conv2') as scope:
    kernel = _variable_with_weight_decay('weights',
                                         shape=[5, 5, 64, 64],
                                         stddev=5e-2,
                                         wd=None)
    conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
    biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.1))
    pre_activation = tf.nn.bias_add(conv, biases)
    conv2 = tf.nn.relu(pre_activation, name=scope.name)
    _activation_summary(conv2)

  # norm2
# local response normalization 2
  norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
                    name='norm2')
  # pool2
# 풀링 레이어 2
  pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1],
                         strides=[1, 2, 2, 1], padding='SAME', name='pool2')

  # local3
# fully connected layer 
  with tf.variable_scope('local3') as scope:
    # Move everything into depth so we can perform a single matrix multiply.
    reshape = tf.reshape(pool2, [FLAGS.batch_size, -1])
    dim = reshape.get_shape()[1].value
    weights = _variable_with_weight_decay('weights', shape=[dim, 384],
                                          stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
    local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
    _activation_summary(local3)

  # local4
# fully connected layer 2
  with tf.variable_scope('local4') as scope:
    weights = _variable_with_weight_decay('weights', shape=[384, 192],
                                          stddev=0.04, wd=0.004)
    biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
    local4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
    _activation_summary(local4)

  # linear layer(WX + b),
  # We don't apply softmax here because
  # tf.nn.sparse_softmax_cross_entropy_with_logits accepts the unscaled logits
  # and performs the softmax internally for efficiency.
# softmax layer
  with tf.variable_scope('softmax_linear') as scope:
    weights = _variable_with_weight_decay('weights', [192, NUM_CLASSES],
                                          stddev=1/192.0, wd=None)
    biases = _variable_on_cpu('biases', [NUM_CLASSES],
                              tf.constant_initializer(0.0))
    softmax_linear = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
    _activation_summary(softmax_linear)

  return softmax_linear


이 함수의 코드는 Convolutional layer > ReLu layer > Pooling Layer > Norm layer > Convolutional layer 
> ReLu layer > Norm layer > Pooling layer > Fully connected layer > Fully connected layer > 
Softmax layer의 순으로 구성이 되어있는데 이 중 Norm layer가 정확히 어떤 역할을 하는지는 아직 잘 모르겠다.
일단 ReLu를 보조하는 것 같은데 더 알아봐야겠다.


loss(logits, labels)

# 손실 값 계산을 위한 함수
# 아래 주석에서 보이듯 logits 파라미터는 inference() 함수의 리턴 값이고 labels는 distorted_input()
# 또는 input() 함수의 리턴 튜플 중 labels 부분이다. cross entropy를 이용하여 loss를 구한다.
"""Add L2Loss to all the trainable variables.
  Add summary for "Loss" and "Loss/avg".
  Args:
    logits: Logits from inference().
    labels: Labels from distorted_inputs or inputs(). 1-D tensor
            of shape [batch_size]
  Returns:
    Loss tensor of type float.
  """
  # Calculate the average cross entropy loss across the batch.
# 여기서는 sparse_softmax_cross_entropy_with_logits 함수가 사용되고 있는데
# softmax_cross_entropy_with_logits와의 차이라면 softmax_cross_entropy_with_logits
# 함수가 확률분포를를 따른다면 sparse_softmax_cross_entropy_with_logits는 독점적인 확률로
# label이 주어진다고 하는데...무슨 의미인지 잘 모르겠다...ㅠ.ㅠ 확인이 필요한 내용
  labels = tf.cast(labels, tf.int64)
  cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
      labels=labels, logits=logits, name='cross_entropy_per_example')
  cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
  tf.add_to_collection('losses', cross_entropy_mean)

  # The total loss is defined as the cross entropy loss plus all of the weight
  # decay terms (L2 loss).
  return tf.add_n(tf.get_collection('losses'), name='total_loss')


_add_loss_summaries(total_loss)

# 텐서 보드에 손실값 표시를 위해 손실 값에 대한 summary 추가하고
# 손실값들의 이동 평균을 구하여 리턴. 여기서 사용하는 이동 평균은 가장 최근 값에 가중치를 두는
# tf.train.ExponentialMovingAverage을 사용하여 구한다.
"""Add summaries for losses in CIFAR-10 model.
  Generates moving average for all losses and associated summaries for
  visualizing the performance of the network.
  Args:
    total_loss: Total loss from loss().
  Returns:
    loss_averages_op: op for generating moving averages of losses.
  """
  # Compute the moving average of all individual losses and the total loss.
  loss_averages = tf.train.ExponentialMovingAverage(0.9, name='avg')
  losses = tf.get_collection('losses')
  loss_averages_op = loss_averages.apply(losses + [total_loss])

  # Attach a scalar summary to all individual losses and the total loss; do the
  # same for the averaged version of the losses.
  for l in losses + [total_loss]:
    # Name each loss as '(raw)' and name the moving average version of the loss
    # as the original loss name.
    tf.summary.scalar(l.op.name + ' (raw)', l)
    tf.summary.scalar(l.op.name, loss_averages.average(l))

  return loss_averages_op


train(total_loss, global_step)

# 학습을 실행시키는 함수
"""Train CIFAR-10 model.
  Create an optimizer and apply to all trainable variables. Add moving
  average for all trainable variables.
  Args:
    total_loss: Total loss from loss().
    global_step: Integer Variable counting the number of training steps
      processed.
  Returns:
    train_op: op for training.
  """
  # Variables that affect learning rate.
# 미리 정의한 변수들을 이용하여 러닝 rate를 조정할 파라미터를 결정한다. 
  num_batches_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN / FLAGS.batch_size
  decay_steps = int(num_batches_per_epoch * NUM_EPOCHS_PER_DECAY)

  # Decay the learning rate exponentially based on the number of steps.
# 학습 step이 증가할 수록 러닝 rate를 기하급수적으로 감소시키도록 처리한다.
# tf.train.exponential_decay 함수는 아래 식의 결과를 리턴한다.
# INITIAL_LEARNING_RATE * LEARNING_RATE_DECAY_FACTOR ^ (global_step / decay_steps)
  lr = tf.train.exponential_decay(INITIAL_LEARNING_RATE,
                                  global_step,
                                  decay_steps,
                                  LEARNING_RATE_DECAY_FACTOR,
                                  staircase=True)
  tf.summary.scalar('learning_rate', lr)

  # Generate moving averages of all losses and associated summaries.
  loss_averages_op = _add_loss_summaries(total_loss)

# Optimizer 설정 및 텐서 보드에 표시하기 위한 summary 생성 후 추가
  # Compute gradients.
  with tf.control_dependencies([loss_averages_op]):
    opt = tf.train.GradientDescentOptimizer(lr)
    grads = opt.compute_gradients(total_loss)

  # Apply gradients.
  apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)

  # Add histograms for trainable variables.
  for var in tf.trainable_variables():
    tf.summary.histogram(var.op.name, var)

  # Add histograms for gradients.
  for grad, var in grads:
    if grad is not None:
      tf.summary.histogram(var.op.name + '/gradients', grad)

  # Track the moving averages of all trainable variables.
  variable_averages = tf.train.ExponentialMovingAverage(
      MOVING_AVERAGE_DECAY, global_step)
  variables_averages_op = variable_averages.apply(tf.trainable_variables())

# tf.control_dependencies 함수는 오퍼레이션간의 의존성을 지정하는 함수로 with와 함께
# 사용하면 파라미터로 전달된 오퍼레이션이 우선 수행된 뒤 다음 문장, 여기서는 with문 아래 있는
# train_op = tf.no_op(name='train')이 수행된다. 
  with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
    train_op = tf.no_op(name='train')

# 이미 알다시피 여기까지는 그저 그래프를 만든 것 뿐, 이제 tf.Session을 통해 run을 하면
# 이전까지 구성된 그래프가 실행된다. 실제로 실행시키는 내용은 cifar10_tranin.py에 들어있다. 
  return train_op


maybe_download_and_extract()

# 웹사이트로부터 CIFAR-10 데이터 셋을 다운로드 받아 사용할 경로에 압축을 풀게 하는 함수
# 이미 별도로 데이터 셋을 받아놓은 경우는 필요 없음
"""Download and extract the tarball from Alex's website."""
  dest_directory = FLAGS.data_dir
  if not os.path.exists(dest_directory):
    os.makedirs(dest_directory)
  filename = DATA_URL.split('/')[-1]
  filepath = os.path.join(dest_directory, filename)
  if not os.path.exists(filepath):
    def _progress(count, block_size, total_size):
      sys.stdout.write('\r>> Downloading %s %.1f%%' % (filename,
          float(count * block_size) / float(total_size) * 100.0))
      sys.stdout.flush()
    filepath, _ = urllib.request.urlretrieve(DATA_URL, filepath, _progress)
    print()
    statinfo = os.stat(filepath)
    print('Successfully downloaded', filename, statinfo.st_size, 'bytes.')
  extracted_dir_path = os.path.join(dest_directory, 'cifar-10-batches-bin')
  if not os.path.exists(extracted_dir_path):
    tarfile.open(filepath, 'r:gz').extractall(dest_directory)


정리


핵심적인 내용들이 대부분 들어있는 소스이다보니 잊어버린 내용 되찾으랴 또 생소한 API 확인하랴 시간이
많이 걸렸다.


단지 시간만 많이 걸린 것이면 그나마 다행이지만 꽤 많은 부분을 이해하지 못한다는 것은 참으로 난감한 일이
아닐 수 없다…ㅠ.ㅠ 그래도 기본적인 CNN의 흐름을 따라 어찌어찌 정리는 했지만 여전히 확인해야 할 내용들이
많이 남아있다. 특히나 API의 경우 기본적으로 파라미터나 리턴 값들이 텐서를 기반으로 하고 있는데다가 설명
또한 수학적인 내용이나 용어들을 포함하고 있다보니 java나 python 같은 프로그래밍 언어의 API 문서를
대하는 것과는 그 이해의 차원이 다르다.


일단 중요한 고비는 넘겼으니 다음 포스팅에서 학습을 진행하기 위한 메인 소스인 cifar10_train.py를
살펴보고 그 다음 마지막으로 cifar10_eval.py를 살펴본 후 이 소스 코드에 등장했던 API들을 모두
차근차근 번역해봐야겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^





목차

1. 소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10 ~ cifar10_input.py (이번 글)





소스코드로 텐서플로우 맛보기 : [CNN] CIFAR-10


나름 직장 동료들과 열심히 공부를 하고 있고 또 이 딥러닝이라는 분야의 공부를 시작한지도 어언 1년이 다되간다.
하지만 한 때 유행했던 유머처럼 ‘딥러닝을 글로만 배웠어요~’인 상태이다보니 제대로 뭔가를 알고 있는 것인지
감조차 오지 않았다. 그래서 이제야 비로소 예제 코드를 돌려보기로 했다. 


다만 그저 샘플 소스를 다운로드 받고 실행하고 끝! 하는 것이 아닌 적어도 소스 코드가 어떤 의미인지는 알고
돌려보기로 했다. 그 시작으로 CNN쪽에 있는 CIFAR-10 예제를 대상으로 삼았다.


처음에는 함께 공부하는 직장 동료들과 직독직해 식으로 소스를 분석해보려고 했으나…
이런 상황을 ‘자만심 오졌다리~’라고 표현해야 하나…처음 import부터 막혀서 쩔쩔매다가 일단 내가
분석을 좀 하고 내용을 공유하기로 한 것이다.


이러한 형편이니 혹시라도 잘못된 내용이 있으면 따끔한 충고 부탁드린다…^^;;


cifar10_input.py


# sys.path 상의 가장 상위 모듈을 import 하는 것을 보장해 줌. 
from __future__ import absolute_import
# /연산자와 더불어 // 연산자 사용 가능, / 연산자는 실수형을 리턴, // 연산자는 몫 부분만 정수로 리턴
from __future__ import division
# print 함수에 ()를 사용할 수 있게 함
from __future__ import print_function


__future __의 의미 : Python 2에서 Python 3 함수를 사용할 수 있게 해줌
위의 3줄은 Python 2와 Python 3의 호환성을 위한 import이다.


# OS의 자원을 사용할 수있게 해주는 모듈
import os

# six(2 * 3)는 Python 2와 Python 3에서 차이나는 함수들을 함께 사용할 수 있게 해줌
# xrange는 3에서는 range
from six.moves import xrange  # pylint: disable=redefined-builtin
# 아기다리고기다리던 텐서플로우
import tensorflow as tf


데이터를 읽어들이기 위해 OS 자원을 사용하도록 해주고 range의 하위호환성을 위해 xrange를 import 했으며
마지막으로 텐서플로우를 import 함


IMAGE_SIZE = 24


32 X 32 사이즈의 이미지를 랜덤하게 24 X 24 사이즈로 Corp함으로써 전체 데이터셋의 크기가 커진다.


NUM_CLASSES = 10
NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = 50000
NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = 10000


CIFAR-10 데이터 셋 관련 상수로 총 10개의 클래스(비행기, 자동차, 새, 고양이, 사슴, 개, 개구리, 말, 배, 트럭)가
있으며 학습을 위한 데이터 50000건 테스트를 위한 데이터 10000건으로 구성된다.


이 파일에는 총 4개의 함수가 있으며 각각 다음과 같다.

  • read_cifar10(filename_queue) : 파일 이름 목록을 받아와 CIFAR-10의 바이너리 데이터를 읽고 파싱하여 단일 오브젝트 형태로 반환한다. 이 오브젝트에는 height, width, depth, key, label, uint8image 등의 필드가 있다.
  • _generate_image_and_label_batch(image, label, min_queue_examples, batch_size, shuffle) : image와 label들을 담은 배치용 queue를 만들어 리턴한다.
  • distorted_inputs(data_dir, batch_size) : 데이터셋 확대를 위한 이미지 왜곡 작업을 진행한다.
    read_cifar10 함수를 호출하여 그 리턴 값을 가지고 작업한다. 학습 시 사용.
  • inputs(eval_data, data_dir, batch_size) : 평가를 위한 input에 사용하며 역시 read_cifar10
    함수를 호출하여 사용하며 Crop 외에 다른 조작은 하지 않는다. 


이미 코드에 영문 주석이 다 있지만 추가로 한글 주석을 추가하며 알아보자.


distorted_inputs(data_dir, batch_size)

def distorted_inputs(data_dir, batch_size):
  """Construct distorted input for CIFAR training using the Reader ops.
  Args:
    data_dir: Path to the CIFAR-10 data directory.
    batch_size: Number of images per batch.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """

# os.path.join 함수는 전달받은 파라미터를 이어 새로운 경로를 만드는 함수
# 아래 코드는 이 함수에서 파라미터로 받은 data_dir 경로와 그 경로 아래에 있는
# CIFAR-10의 이미지 파일이 담긴 data_batch_1.bin ~ data_batch_5.bin의
# 5개 파일에 대한 전체 경로를 요소로 하는 벡터(텐서)를 만드는 것이다.
  filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
               for i in xrange(1, 6)]

# 만일 배열 내에 파일 경로가 없으면 에러 발생
  for f in filenames:
    if not tf.gfile.Exists(f):
      raise ValueError('Failed to find file: ' + f)

# string_input_producer 함수는 필수 파라미터인 첫 번째 파라미터에 string 타입의 요소로 만들어진 
# 텐서 타입을 받아서 각 요소 문자열로 구성된 Queue 형태로 리턴을 해준다.
  # Create a queue that produces the filenames to read.
  filename_queue = tf.train.string_input_producer(filenames)

  with tf.name_scope('data_augmentation'):
    # Read examples from files in the filename queue.
# 아래 설명할 read_cifar10 함수로부터 라벨, 이미지 정보 등을 포함한 
# CIFAR10Record 클래스 타입을 톨려받는다.
    read_input = read_cifar10(filename_queue)

# cast 함수는 첫 번째 인자로 받은 텐서 타입의 파라미터를 두 번째 인자로 받은
# 데이터 타입의 요소를 가진 텐서로 돌려준다.
    reshaped_image = tf.cast(read_input.uint8image, tf.float32)

    height = IMAGE_SIZE
    width = IMAGE_SIZE

    # Image processing for training the network. Note the many random
    # distortions applied to the image.

    # Randomly crop a [height, width] section of the image.
# tf.random_crop 함수는 첫 번째 파라미터로 받은 텐서타입의 이미지들을 
# 두 번째 파라미터로 받은 크기로 무작위로 잘라 첫 번째 받은 파라미터와 같은 rank의
# 텐서 형태로 돌려준다. 
    distorted_image = tf.random_crop(reshaped_image, [height, width, 3])

    # Randomly flip the image horizontally.
# 좌우를 랜덤하게 뒤집은 형태의 텐서를 돌려준다.
    distorted_image = tf.image.random_flip_left_right(distorted_image)

    # Because these operations are not commutative, consider randomizing
    # the order their operation.
    # NOTE: since per_image_standardization zeros the mean and makes
    # the stddev unit, this likely has no effect see tensorflow#1458.
# 밝기와 콘트라스트를 랜텀하게 변형시킨 텐서를 돌려준다.
    distorted_image = tf.image.random_brightness(distorted_image,
                                                 max_delta=63)
    distorted_image = tf.image.random_contrast(distorted_image,
                                               lower=0.2, upper=1.8)
# random_crop부터 random_contrast까지는 데이터 셋 확장을 위해 이미지를 임의 조작하는
# 과정이다.

    # Subtract off the mean and divide by the variance of the pixels.
# 이미지를 표준화 하는 과정인 듯한데...어려워서 패쓰~
    float_image = tf.image.per_image_standardization(distorted_image)

    # Set the shapes of tensors.
# 텐서의 shape 설정
    float_image.set_shape([height, width, 3])
    read_input.label.set_shape([1])

    # Ensure that the random shuffling has good mixing properties.
# 전체 테스트용 이미지의 40%, 즉, 총 50000개의 테스트 이미지 중 20000개를 사용
    min_fraction_of_examples_in_queue = 0.4
    min_queue_examples = int(NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN *
                             min_fraction_of_examples_in_queue)
    print ('Filling queue with %d CIFAR images before starting to train. '
           'This will take a few minutes.' % min_queue_examples)

  # Generate a batch of images and labels by building up a queue of examples.
# 배치 작업에 사용할 128개의 이미지를 shuffle하여 리턴함
  return _generate_image_and_label_batch(float_image, read_input.label,
                                         min_queue_examples, batch_size,
                                         shuffle=True)


read_cifar10(filename_queue)

"""Reads and parses examples from CIFAR10 data files.
  Recommendation: if you want N-way read parallelism, call this function
  N times.  This will give you N independent Readers reading different
  files & positions within those files, which will give better mixing of
  examples.
  Args:
    filename_queue: A queue of strings with the filenames to read from.
  Returns:
    An object representing a single example, with the following fields:
      height: number of rows in the result (32)
      width: number of columns in the result (32)
      depth: number of color channels in the result (3)
      key: a scalar string Tensor describing the filename & record number
        for this example.
      label: an int32 Tensor with the label in the range 0..9.
      uint8image: a [height, width, depth] uint8 Tensor with the image data
  """

# 이 함수의 리턴 값은 CIFAR10Record라는 class임 pass는 비어있는 클래스 선언 시 사용
# 이미 아는 바와 같이 텐서플로우의 Session.run이 실행되기 전까지는 비어있는 클래스이며
# Session.run이 실행된 이후에야 데이터 파일의 레코드들이 클래스에 들어가게 된다.
  class CIFAR10Record(object):
    pass
  result = CIFAR10Record()

# label_bytes는 말 그대로 라벨의 길이이고 1byte이다.
# result.height는 이미지의 높이
# result.width는 이미지의 넓이
# result.depth는 이미지를 구성하는 색상 채널
# image_bytes 결국 이미지를 구성하는 총 byte 수는 높이 * 넓이 * 색상 채널

  # Dimensions of the images in the CIFAR-10 dataset.
  # See http://www.cs.toronto.edu/~kriz/cifar.html for a description of the
  # input format.
  label_bytes = 1  # 2 for CIFAR-100
  result.height = 32
  result.width = 32
  result.depth = 3
  image_bytes = result.height * result.width * result.depth

# 모든 레코드는 라벨과 라벨에 해당하는 이미지로 구성되어있으므로  
# 전체 레코드 크기는 label_bytes + image_bytes로 고정
  # Every record consists of a label followed by the image, with a
  # fixed number of bytes for each.
  record_bytes = label_bytes + image_bytes

# tf.FixedLengthRecordReader는 파일로부터 고정길이의 레코드를 출력해주는 클래스
# 생성 시 첫 번째 파라미터는 읽어올 레코드의 바이트 수
  # Read a record, getting filenames from the filename_queue.  No
  # header or footer in the CIFAR-10 format, so we leave header_bytes
  # and footer_bytes at their default of 0.
  reader = tf.FixedLengthRecordReader(record_bytes=record_bytes)

# Queue 타입(FIFO)의 자료 구조를 파라미터로 받아 그 안의 레코드로부터
# Key와 Value를 받아오는 처리. key는 레코드가 포함된 파일명과 index의 구성으로
# 되어있으며, value는 사용할 라벨과 이미지가 포함된 텐서임.
  result.key, value = reader.read(filename_queue)

  # Convert from a string to a vector of uint8 that is record_bytes long.
# byte 타입의 문자열을 숫자형 벡터로 변환. 첫 번째 인자는 문자열로 구성된 텐서이며
# 모든 요소들은 동일한 길이여야 함. 두 번째 인자는 변환할 데이터 타입
  record_bytes = tf.decode_raw(value, tf.uint8)

  # The first bytes represent the label, which we convert from uint8->int32.
# 첫 번째 인자로 받은 텐서를 두 번째 인자로 받은 데이터 타입으로 형변환 함.
# 즉, 아래 코드는 위에서 구성된 record_bytes에서 첫 번째 바이트를 가져와 int32
# 타입으로 변환하여 리턴한다. 따라서 result.label은 1바이트 크기의 int32 타입 요소를
# 갖는 벡터이다.
  result.label = tf.cast(
      tf.strided_slice(record_bytes, [0], [label_bytes]), tf.int32)

  # The remaining bytes after the label represent the image, which we reshape
  # from [depth * height * width] to [depth, height, width].
# tf.reshape는 첫 번째 파라미터의 shape를 두 번째 파라미터로 받은 형태로 바꾼다.
# 아래 코드의 첫 번째 인자는 record_bytes에서 첫 바이트인 라벨을 제외한 나머지
# 바이트(이미지 부분)를 가져와 [3, 32, 32] 형태의 shape로 바꾼다. 
  depth_major = tf.reshape(
      tf.strided_slice(record_bytes, [label_bytes],
                       [label_bytes + image_bytes]),
      [result.depth, result.height, result.width])
  # Convert from [depth, height, width] to [height, width, depth].
# tf.transpose는 첫 번째 파라미터로 받은 텐서의 각 차원 값을 두 번째 파라미터로 전달받은
# 순서로 바꾼 텐서를 리턴한다. 위의 depth_major의 shape는 [3, 32, 32]이다.
# 즉, shape의 0번째 요소는 3, 1번째 요소는 32, 2번째 요소는 32이다. 이 것을 두 번째
# 파라미터처럼 인덱스를 [1, 2, 0]로 바꾸는 것이므로 1 번째 요소인 32가 맨 앞으로, 다음으로
# 2 번째 요소인 32가 오고 0번째 요소인 3은 맨 마지막으로 가게 되는 것이다.
# 결국 최초에 [depth, height, width]의 순서가 [height, width, depth]가 된다.
  result.uint8image = tf.transpose(depth_major, [1, 2, 0])

# 테스트 코드 시작 ##############################################
# 원본 코드에는 없는 내용이지만 아래 코드를 이용하여 간단하게 데이터를 정상적으로 불러왔는지
# 확인할 수 있다. 아래 코드를 싫행하면 총 100개의 이미지가 10 X 10 형태로 배열된 1개의 이미지가
# 만들어지며, label, key, value 값을 확인할 수 있다.
# 이 코드를 사용하려면 matplotlib.pyplot을 import해야 한다.
 fig, ax = plt.subplots(10, 10, figsize=(10, 10))
  with tf.Session() as sess:
      coord = tf.train.Coordinator()
      threads = tf.train.start_queue_runners(coord=coord, sess=sess)

      for i in range(10):
          for j in range(10):
              print(sess.run(result.label), sess.run(result.key), sess.run(value))
              img = sess.run(result.uint8image)
              ax[i][j].set_axis_off()
              ax[i][j].imshow(img)

      dir = os.path.abspath("cifar10_image")
      plt.savefig(dir + "/" + "image")
      print(dir)

      coord.request_stop()
      coord.join(threads)
# 테스트 코드 끝 ############################################

  return result


_generate_image_and_label_batch(image, label, min_queue_examples, batch_size, shuffle)

"""Construct a queued batch of images and labels.
  Args:
    image: 3-D Tensor of [height, width, 3] of type.float32.
    label: 1-D Tensor of type.int32
    min_queue_examples: int32, minimum number of samples to retain
      in the queue that provides of batches of examples.
    batch_size: Number of images per batch.
    shuffle: boolean indicating whether to use a shuffling queue.
  Returns:
    images: Images. 4D tensor of [batch_size, height, width, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """
  # Create a queue that shuffles the examples, and then
  # read 'batch_size' images + labels from the example queue.
# 각각 배치를 생성하는 코드로 shuffle_batch는 무작위로 뒤섞은 배치를 생성하며
# batch는 입력 텐서와 레코드 순서가 동일한 배치를 생성한다. 배치 생성 시 16개의
# thread를 사용한다.
  num_preprocess_threads = 16
  if shuffle:
    images, label_batch = tf.train.shuffle_batch(
        [image, label],
        batch_size=batch_size,
        num_threads=num_preprocess_threads,
        capacity=min_queue_examples + 3 * batch_size,
        min_after_dequeue=min_queue_examples)
  else:
    images, label_batch = tf.train.batch(
        [image, label],
        batch_size=batch_size,
        num_threads=num_preprocess_threads,
        capacity=min_queue_examples + 3 * batch_size)

  # Display the training images in the visualizer.
# 텐서보드에서 이미지를 보여주긴 위한 코드
  tf.summary.image('images', images)

# 배치 과정을 거친 이미지와 라벨의 최종 shape는 각각 [128, 32, 32, 3]과 [128]이다.
  return images, tf.reshape(label_batch, [batch_size])


inputs(eval_data, data_dir, batch_size)

"""Construct input for CIFAR evaluation using the Reader ops.
  Args:
    eval_data: bool, indicating if one should use the train or eval data set.
    data_dir: Path to the CIFAR-10 data directory.
    batch_size: Number of images per batch.
  Returns:
    images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
    labels: Labels. 1D tensor of [batch_size] size.
  """

# eval_data 값에 따라 학습용 데이터를 불러올지 평가용 데이터를 불러올지 결정한다.
  if not eval_data:
    filenames = [os.path.join(data_dir, 'data_batch_%d.bin' % i)
                 for i in xrange(1, 6)]
    num_examples_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN
  else:
    filenames = [os.path.join(data_dir, 'test_batch.bin')]
    num_examples_per_epoch = NUM_EXAMPLES_PER_EPOCH_FOR_EVAL

# 이후 코드는 이미지 변형 (random_flip_left_right, random_brightness,
# random_contrast) 처리를 제외하고는 distorted_inputs(data_dir, batch_size)
# 함수와 동일하다.
  for f in filenames:
    if not tf.gfile.Exists(f):
      raise ValueError('Failed to find file: ' + f)

  with tf.name_scope('input'):
    # Create a queue that produces the filenames to read.
    filename_queue = tf.train.string_input_producer(filenames)

    # Read examples from files in the filename queue.
    read_input = read_cifar10(filename_queue)
    reshaped_image = tf.cast(read_input.uint8image, tf.float32)

    height = IMAGE_SIZE
    width = IMAGE_SIZE

    # Image processing for evaluation.
    # Crop the central [height, width] of the image.
    resized_image = tf.image.resize_image_with_crop_or_pad(reshaped_image,
                                                           height, width)

    # Subtract off the mean and divide by the variance of the pixels.
    float_image = tf.image.per_image_standardization(resized_image)

    # Set the shapes of tensors.
    float_image.set_shape([height, width, 3])
    read_input.label.set_shape([1])

    # Ensure that the random shuffling has good mixing properties.
    min_fraction_of_examples_in_queue = 0.4
    min_queue_examples = int(num_examples_per_epoch *
                             min_fraction_of_examples_in_queue)

  # Generate a batch of images and labels by building up a queue of examples.
  return _generate_image_and_label_batch(float_image, read_input.label,
                                         min_queue_examples, batch_size,
                                         shuffle=False)


정리


늘 어처구니 없는 실수가 따라다닌다.
CIFAR-10 홈페이지에 가면 다음과 같이 데이터 셋이 3가지 버전이 있다.

  • CIFAR-10 python version
  • CIFAR-10 Matlab version
  • CIFAR-10 binary version (suitable for C programs)


나는 Tensorflow가 python 기반으로 코딩이 되므로 당연히 python versiond을 받아야 한다고 생각했다.
그런데 python 버전을 사용하여 코드를 실행하다보니 뭔가 이상했다. 간간히 데이터를 제대로 불러왔는지
확인하기 위한 print문에 이상한 결과가 찍히는 것이다. CIFAR10Record 클래스의 멤버들에 대한 shape나
rank는 물론 중간에 시험삼아 100개의 이미지를 출력한 것도 모든 이미지가 깨져서 나왔다.



주말 2일을 고민하다가 문득 원래의 코드에는 파일명을 가져올 때 .bin이라는 확장자가 있었는데 내가 사용하는
데이터 파일에는 확장자가 없는 것을 발견했다. 그리고 겨우 내가 잘못된 버전의 데이터 셋을 받았다는 것을 
깨달았다…ㅠ.ㅠ


새로 받은 버전의 데이터 셋은 아래와 같이 이미지가 정상적으로 나왔다.



이제 겨우 파일 하나 분석해봤을 뿐인데 벌써 지친다…특히나 텐서라는 개념과 행렬 연산 그리고 Tensorflow의
지연 실행이라는 메커니즘은 정말 적응이 안된다…ㅠ.ㅠ 다음 포스팅에서는 cifar10.py 파일을 분석해보자.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^