Elasticsearch








Elasticserach에 Excel 데이터 입력하기 - JAVA API와 몇가지 설정


지난 시간에는 간단에서 Spring boot를 설정하면서 확인해야 했던 부분들을 중심으로 정리를 하였다.
일단 웹 프레이임워크가 갖추어졌으니 이제 시스템을 만들어가는 일만 남았다. 물론 파일 업로드, 엑셀 파싱 등의
기능들도 필요하지만 역시 가장 중요한 것은 Elasticsearch를 이용할 수 있게 해주는 API일 것이다.


지난 포스팅에서도 언급했지만 이미 Spring에는 Elasticsearch와 관련된 프로젝트가 있다. 하지만 안타깝게도
Spring Data Elasticsearch 프로젝트의 최신 버전도 아직은 Elasticsearch의 6.x 버전을 지원하지 못한다
(내가 이 작업을 시작하면서 검색했을 때는 Elasticsearch 2.4까지만 지원한다고 했었는데 그새 지원 버전이 조금
올라가긴 했다).


그래서 별도의 API 라이브러리를 참조하여 작업을 진행하였다.
물론 많은 API들이 존재하지만 오늘은 간단하게 Index 생성과 관련된 내용들만 살펴보도록 하겠다.


Client 연결


Index를 생성하기 위해서는 우선 Elasticsearch cluster의 노드에 접근을 해야 한다. API에서는 Client 인스턴스를
생성하여 연결한다. Client 클래스는 몇가지가 있는데 Low Level REST Client로 RestClient 클래스를 사용할 수
있고 이 RestClient를 wrapping한 RestHighLevelClient 클래스는 High Level REST Client라고 부른다.
여기에 다시 Indices(Elasticsearch 내부에서 관리하는 index들을 indices라 부른다)접근하기 위해
RestHighLevelClient를 한번 더 wrapping한 IndicesClient가 있고, 이 외에 TransportClient가 있다.



그런데 이 TransportClient는 조금 독특하게 HTTP가 아닌 TCP 프로토콜을 이용하며 따라서 사용하는 포트도
REST Client들이 기본 값을 기준으로 9200포트를 이용하는데 반해 TransportClient는 9300 포트를 이용한다.


TransportClient는 Elasticsearch 7.0에서 deprecate 예정이며 8.0에서는 제거될 것이라고 한다. 
TransportClient에 대한 자세한 내용은 아래 링크를 참조하도록 하자.


https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/client.html


처음에는 TransportClient를 이용하느라 고생을 좀 했다. 그러다가 deprecate 예정이라는 정보를 보고는 미련 없이
REST Client로 바꾸어 사용하기로 했다.


기본적으로 High Level REST Client인 RestHighLevelClient 클래스를 사용하게 되겠지만 그 전에 Low Level 
REST Client에서 중요하게 짚고 넘어가야 할 부분이 있다(어차피 RescClient의 builder를 통해 생성하니 당연한
이야기이겠지만...). 바로 다음 링크에 있는 내용들 때문이다.


https://www.elastic.co/guide/en/elasticsearch/client/java-rest/6.2/java-rest-low.html


내용을 간략하게 보자면 Low Level REST Client에는 load balancing이라든지 failover, 장애 노드에 대한
패널티 부여, 그리고 옵션 사항이지만 전체 클러스터에서의 노드 찾기 등 클러스터를 관리하기 위해 필요한 많은
기능들이 구현되어있다. 특히 load balancing의 경우 clietn 생성시 파라미터로 전달된 각 노드들을 round-robin
방식으로 접근하여 rquest를 보내게 된다. 자세한 내용은 아래 링크에서 확인할 수 있다.


https://artifacts.elastic.co/javadoc/org/elasticsearch/client/elasticsearch-rest-client/6.2.3/org/elasticsearch/client/RestClient.html


마지막으로 client 연결 시 애를 먹었던 부분이 X-pack을 설치한 후 Elasticsearch 접근 시 계정 인증이 필요하게
되었는데 이에 대한 처리를 하느라 고생을 좀 했다. 이 부분은 샘플 코드로 설명을 대신한다.


public static RestHighLevelClient newRestHighLevelClient() {
 // X-pack 설치 시 아래와 같이 자격 증명을 해주어야 한다. user와 password에 각각 X-pack을 통해 설정한
 // ID와 비밀번호를 입력하면 되는데 ID는 보통 elastic이고 비밀번호는 자동 생성된 값이다. 
	final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
	credentialsProvider.setCredentials(AuthScope.ANY,
		        new UsernamePasswordCredentials(user, password));
		
	RestHighLevelClient client = new RestHighLevelClient(
		RestClient.builder(
			new HttpHost(hostData1, Integer.valueOf(httpPort), "http"))
		        .setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
		            @Override
		            public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) {
		                httpClientBuilder.disableAuthCaching(); 
		                return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
			}
	 }));

	return client;
}



API 구현


아직까지는 Excel 데이터를 Elasticsearch로 입력하는 기능만을 구현하였기에 실제로 사용하는 API는 Create Index API(Index 생성 시 사용)와 Index API(Index를 이용하여 데이터를 입력하는 작업에 사용) 뿐이다.


Elasticsearch의 JAVA API들은 모두 2가지 종류가 있는데 바로 synchronous와 asynchronous 방식이다.
익히 알고 있듯이 synchronous는 요청을 한 후 그 결과를 리턴받은 후 프로세스가 진행되지만 asynchronous의
경우 요청후 바로 다음 프로세스가 진행되며 요청한 프로세스에 대한 결과는 별도로 구현된 listener에 의해 처리된다.
따라서 asynchronous API를 구현하는 경우에는 listener를 구현한 후 이 listener를 파라미터로 전달해야 한다. 


나같은 경우 처음에 asynchronous 방식을 알지 못한 상태에서 Elasticsearch API들을 모두 util성 클래스에
static 메소드로 구현을 했는데 아무래도 한 번 뒤집어 엎어야겠다...ㅠ.ㅠ


각 API 구현은 아래 링크의 예제를 거의 그대로 사용하였다.


https://www.elastic.co/guide/en/elasticsearch/client/java-rest/6.2/java-rest-high-create-index.html

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/6.2/java-rest-high-document-index.html


전체적인 흐름은 우선 기존에 생성된 Index가 없는 경우에는 새로운 Index를 생성하도록 하고 이미 생성된 Index가
있는 경우에는 기존 Index와 type을 select 박스를 통해 선택하여 데이터를 입력하거나 아니면 새로운 Index를
생성하는 작업부터 시작하는 것을 선택하도록 하였다.


새로운 Index 생성 시에는 다음과 같은 파라미터를 입력받는다(아직 validation 체크 기능은 없다...-.-).


  1. Index 명
  2. alias
  3. type
  4. shard 수
  5. replica 수
  6. mapping 정보


Index가 생성되고 나면 생성된 Index들과 type들을 선택하여 데이터를 업로드 하는 화면으로 전환된다.
파일 업로드 기능을 통해 엑셀 파일을 업로드 하면 되는데 파일만 업로드한 후 나중에 데이터를 입력할 수도 있고
파일 업로드가 끝나면 바로 데이터 입력이 시작되도록 할 수도 있다.


문제는 데이터의 양이다.


이전 포스팅에서 말한 것처럼 현재 작업을 하려는 데이터는 대략 18개의 열과 50만개의 행으로 구성된 엑셀 파일이다.
가급적이면 다른 전처리(데이터 정제 작업 제외) 없이 한 번에 입력하기를 원하지만 웬만한 시스템이 아니면 입력 중
OOM을 맞닥뜨려야만 했다(물론 개발자 PC로써도 사양은 좀 낮았다...ㅠ.ㅠ).


다수의 데이터를 한 번에 입력하는 작업인만큼 bulk API를 이용하여 작업을 하였다. 처음에는 전체 데이터를 입력
하도록 해보았으나 Elasticsearch에서 timeout이 걸리고 말았다. kibana로 확인해보니 데이터는 모두 입력 된 것
같은데 정상적으로 종료 처리가 되지 않았다. 결국 현재 내 시스템에서 안정적인 입력 량인 10만 건 단위로 나누어
bulk request를 보내도록 구현하였다. 이렇게 하니 50만 건 입력하는데 대략 3분 전후가 걸렸다.


Elasticsearch 설정


하지만 API 구현쪽에서만 처리한다고 모든 것이 해결되는 것은 아니었다.
사실 개발자로서 굳이 알아야 하나 하는 생각도 들긴 하지만 그래도 어렵지 않은 내용이니 아주 얕은 수준에서는
튜닝(이라고 말하기는 부끄럽지만...-.-)은 해주는 것이 좋을 것 같았다. 유일하게 해준 작업은 jvm.options 파일에서
Xms와 Xmx를 수정한 것이다. 파일 경로는 ${ELASTIC_HOME}/config/jvm.options이다.


다만 이렇게 heap size를 설정할 때 주의할 사항이 있는데 일단 heap size가 커지면 가용성은 좋아지지만 GC 수행
시간이 오래 걸리는 단점이 있고 Xmx의 경우 OS의 커널 시스템이 사용할 부분을 고려하여 전체 메모리의 50%를
넘지 않도록 권고하고 있다. 그밖에 compressed ordinary object pointers라는 조금은 전문적인 내용에 대한
권고사항이 있는데 이는 링크로 대신한다.


https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html


위 링크에 보면 heap size를 jvm.options 파일이 아닌 시스템 환경변수에 설정하는 방법도 나와있으니 참고하자.


다음으로는 Web을 통한 접근과 관련된 설정으로 시스템 구현 후 뭔가 허전하여 간단하게 Elasticsearch의 몇가지
정보를 확인할 수 있는 버튼을 추가하였다. 이 작업은 다음 번에 포스팅하겠지만 jQuery를 통해 직접 REST API를
호출하도록 하였는데, 이 때 몇가지 오류가 발생을 하였다. 웹에서 접근시 발생하는 오류를 막기 위해서는 설정 파일인
elasticsearch.yml 파일에 다음의 내용을 추가해주어야 한다.


http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-credentials: true
http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, Authorization"
http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE


당장에 운영 시스템을 관리할 것이 아니라면 그냥 이정도 설정이면 충분할 것이다.


정리


Elasticsearch의 API는 워낙 간단하게 구현되어있어서 달리 설명할 것도 없을뿐더러 오히려 공식 홈페이지에 더 잘
설명이 되어있기에 굳이 이 자리에서 다시 설명할 필요를 못느낀다. 실제로 구현한 내용도 몇가지 시스템에 특화된 
내용을 제외하고는 공식 홈페이지의 예제 snippet를 그대로 Copy & Paste한 수준이다.


어쨌든 50만건의 데이터를 파일 업로드 한 번으로 3분정도의 시간에 Elasticsearch로 입력할 수 있게 되어 나름
만족스럽다. 다만 경력 18년차의 코드로 보기에는 너무 형편없는 코드를 공개해야 하나 생각하니 부끄부끄할 뿐...*^^*


잠깐 삼천포를 좀 들르자면 사실 현재 실질적으로 가장 필요하고 또 공부하고 싶은 부분은 테스트 코드에 관한
부분이다. TDD든 아니면 단순 Unit Test든...테스트 코드도 없는 소스를 공개하려니 뭔가 알맹이는 홀랑 까먹고
껍데기만 올리는 기분이랄까? (거꾸로인가?)


기본적인 시스템 구현 내용은 이정도에서 마무리하고 마지막 포스팅에서는 짧게나마 jQuery에서 REST API를
호출하는 부분을 살펴보고 만들어진 시스템에 대해 설명하고 마치고자 한다. 지금은 형편없고 특정 목적을 위해
만들어진 시스템이지만 평생 목표로 다듬어가야겠다.

블로그 이미지

마즈다

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










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들을 모두
차근차근 번역해봐야겠다.

블로그 이미지

마즈다

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

티스토리 툴바