앞에서 SRE의 주요 지표인 SLO/SLI의 개념에 대해서 설명하였는데, 그러면 실제 서비스에서는 어떻게 SLO/SLI를 정의하는지에 대해서 알아본다.
SLI는 사용자 스토리당 3~5개 정도가 적당하다. 사용자 스토리는 로그인, 검색, 상품 상세 정보와 같이 하나의 기능을 의미한다고 보면된다.
아래 그림과 같은 간단한 게임 서비스가 있다고 가정하자. 이 서비스는 웹사이트를 가지고 있고, 그리고 앱을 통해서 접근이 가능한데, 내부적으로 API 서비스를 통해서 서비스가 된다. 내부 서비스에는 사용자 랭킹(Rank ), 사용자 프로파일 (User profiles) 등의 서비스가 있다.
이 서비스에서 "사용자 프로필" 에 대한 SLI를 정의해보도록 하자.
SLI 지표 레퍼런스
앞에서 설명한 SLI 지표로 주로 사용되는 지표들을 되집어 보면 다음과 같다.
응답 시간 (Request latency) : 시스템의 응답시간
에러율 (Error rate%) : 전체 요청에서 실패한 요청의 비율
처리량(Throughput) : 일반적으로 초당 처리량으로 측정하고 TPS (Thoughput per second) 또는 QPS (Query per second)라는 단위를 사용한다.
가용성(availability) : 시스템의 업타임 비율로, 앞에서 예를 들어 설명하였다.
이런 지표들이 워크로드 타입에 따라 어떤 지표들이 사용되는지 정의해놓은 정보를 다시 참고해 보면 다음과 같다.
사용자에게 서비스를 제공하는 서비스 시스템 (웹,모바일등) : 가용성, 응답시간, 처리량
스토리지 시스템(백업,저장 시스템): 가용성, 응답시간, 내구성
빅데이터 분석 시스템 : 처리량, 전체 End-to-End 처리 시간
머신러닝 시스템 : 서빙 응답시간, 학습 시간, 처리량, 가용성, 서빙 정확도
이 서비스는 "사용자에게 서비스를 제공하는 서비스 시스템 패턴" 이기 때문에, 이 중에서 가용성과 응답시간을 SLI로 사용하기로 한다.
가용성 SLI
가용성은 프로파일 페이지가 성공적으로 로드된 것으로 측정한다.
그러면 성공적으로 로드 되었다는 것은 어떻게 측정할 것인가? 그리고, 성공호출 횟수와 실패 횟수는 어떻게 측정할것인가? 에 대한 질문이 생긴다.
이 서비스는 웹기반 서비스이기 때문에, HTTP GET /profile/{users}와 /profile/{users}/avatar 가 성공적으로 호출된 비율을 측정하면 된다. 성공 호출은 어떻게 정의할것인가? HTTP response code 200번만 성공으로 생각할 수 있지만 5xx는 시스템 에러이지만 3xx, 4xx는 애플리케이션에서 처리하는 에러 처리 루틴이라고 봤을때, 3xx,4xx도 성공 응답에 포함시켜야 한다. 그래서 2xx,3xx,4xx의 횟수를 성공 호출로 카운트 한다.
그러면 이 응답을 어디서 수집해야 할것인가? 앞의 아키텍쳐 다이어그램을 보면 API/웹서비스 앞에 로드밸런서가 있는 것을 볼 수 있는데, 개별 서버 (VM)에서 측정하는 것이 아니라 앞단의 로드밸런서에서 측정해도 HTTP 응답 코드를 받을 수 있기 때문에, 로드밸런서의 HTTP 응답 코드를 카운트 하기로 한다.
응답시간 SLI
그러면 같은 방식으로 응답시간에 대한 SLI를 정의해보자
응답 시간은 프로파일 페이지가 얼마나 빨리 로드 되었는지를 측정한다. 그런데 빠르다는 기준은 무엇이고, 언제부터 언제까지를 로딩 시간으로 측정해야 할것인가?
이 서비스는 HTTP GET /profile/{users} 를 호출하기 때문에, 이 서비스가 100ms 를 임의의 기준값으로 하여, 이 값 대비의 응답시간으로 정의한다.
응답 시간 역시 가용성과 마찬가지로 로드밸런서에서 측정하도록 한다.
이렇게 SLI를 정의하였으면, 여기에 측정 기간과 목표값을 정해서 SLO를 정한다.
가용성 SLO는 28일 동안 99.95%의 응답이 성공한것으로 정의한다.
응답시간 SLO는 28일 동안 90%의 응답이 500ms 안에 도착하는 것으로 정의한다. 또는 좀더 발전된 방법으로 99% 퍼센타일의 응답의 90%가 500ms 안에 도착하는 것으로 높게 잡을 수 있지만, 처음 정한 SLO이기때문에, 이정도 수준으로 시작하고 점차 높여가는 모델을 사용한다.
복잡한 서비스의 SLI 의 정의
앞의 예제를 통해서 SLI와 SLO를 정의하는 방법에 대해서 알아보았다. 사용자 스토리 단위로 SLI를 정한다하더라도, 현실에서의 서비스는 훨씬 복잡하고 많은 개수를 갖는다.
SLI가 많아지면, 관련된 사람들이 전체 SLI를 보기 어렵기 때문에 조금 더 단순화되고 직관적인 지표가 필요하다.
예를 들어보다. 구글 플레이 스토어를 예를 들어봤을때, 구글 플레이스토어는 홈 화면, 검색, 카테고리별 앱 리스트 그리고 앱 상세 정보와 같이 크게 4가지 사용자 스토리로 정의할 수 있다.
이 4가지 사용자 스토리를 aggregation (합이나 평균)으로 합쳐서 하나의 지표인 탐색(Browse)라는 지표로 재 정의할 수 있다. 아래는 4개의 SLI를 각각 측정한 값이다.
이 개별 SLI들을 합쳐서 표현하면 다음과 같이 표현할 수 있다. 전체 SLI의 값을 합친 후에, 백분률로 표현하였다.
이 하나의 지표를 사용하면 4개의 기능에 대한 SLI를 대표할 수 있다. 이렇게 개별 SLI의 합이나 평균을 사용하는 경우는 대부분의 경우에는 충분하지만 특정 서비스가 비지니스 임팩트가 더 클 경우 이를 동일하게 취급해서 합해버리면 중요한 서비스가 나도 이 대표값에는 제대로 반영이 안될 수 있기 때문에, 필요한 경우 개별 SLI에 적절한 가중치를 곱해서 값을 계산하는 것도 방법이 된다.
본 예제는 텐서플로우 1.1과 파이썬 2.7 그리고 Jupyter 노트북 환경 및 구글 클라우드를 사용하여 개발되었다.
준비된 데이타
학습에 사용한 데이타는 96x96 사이즈의 얼굴 이미지로, 총 5명의 사진(안젤리나 졸리, 니콜키드만, 제시카 알바, 빅토리아 베컴,설현)을 이용하였으며, 인당 학습 데이타 40장 테스트 데이타 10장으로 총 250장의 얼굴 이미지를 사용하였다.
사전 데이타를 준비할때, 정면 얼굴을 사용하였으며, 얼굴 각도 변화 폭이 최대한 적은 이미지를 사용하였다. (참고 : https://www.slideshare.net/Byungwook/ss-76098082 ) 만약에 이 모델로 학습이 제대로 되지 않는다면 학습에 사용된 데이타가 적절하지 않은것이기 때문에 데이타를 정재해서 학습하기를 권장한다.
CSV 파일을 순차적으로 읽은 후에, train_images와 train_labels라는 배열에 넣은 다음 tf.train.slice_input_producer를 이용하여 큐를 만들어냈다. 이때 중요한 점은 shuffle=True라는 옵션을 준것인데, 만약에 이 옵션을 주지 않으면, 학습 데이타를 큐에서 읽을때 CSV에서 읽은 순차적으로 데이타를 리턴한다. 즉 현재 데이타 포맷은 Jessica Alba가 40개, Jolie 가 40개, Nicole Kidman이 40개 .. 식으로 순서대로 들어가 있기 때문에, Jessica Alba를 40개 리턴한 후 Jolie를 40개 리턴하는 식이 된다. 이럴 경우 Convolutional 네트워크가 Jessica Alba에 치우쳐지기 때문에 제대로 학습이 되지 않는다. Shuffle은 필수이다.
read_data()
input_queue에서 데이타를 읽는 부분인데 특이한 점은 input_queue에서 읽어드린 이미지 파일명의 파일을 읽어서 데이타 객체로 저장해야 한다. 텐서플로우에서는 tf.image.decode_jpeg, tf.image.decode_png 등을 이용하여 이러한 기능을 제공한다.
마지막으로 read_data_batch() 함수 부분이다.get_input_queue에서 읽은 큐를 가지고 read_data함수에 넣어서 이미지 데이타와 라벨을 읽어서 리턴하는 값을 받아서 일정 단위로 (배치) 묶어서 리턴하는 함수이다. 중요한 부분이 데이타를 뻥튀기 하는 부분이 있다.
이 모델에서 학습 데이타가 클래스당 40개 밖에 되지 않기 때문에 학습데이타가 부족하다. 그래서 여기서 사용한 방법은 read_data에서 리턴된 이미지 데이타에 대해서 tf.image.random_xx 함수를 이용하여 좌우를 바꾸거나, brightness,contrast,hue,saturation 함수를 이용하여 매번 색을 바꿔서 리턴하도록 하였다.
그리고 마지막 부분에 label을 tf.one_hot을 이용해서 변환한것을 볼 수 있는데, 입력된 label은 0,1,2,3,4 과 같은 단일 정수이다. 그런데, CNN에서 나오는 결과는 정수가 아니라 클래스가 5개인 (분류하는 사람이 5명이기 때문에) 행렬이다. 즉 Jessica Alba일 가능성이 90%이고, Jolie일 가능성이 10%이면 결과는 [0.9,0.1,0,0,0] 식으로 리턴이 되기 때문에, 입력된 라벨 0은 [1,0,0,0,0], 라벨 1은 [0,1,0,0,0] 라벨 2는 [0,0,1,0,0] 식으로 변환되어야 한다. tf.one_hot 이라는 함수가 이 기능을 수행해준다.
모델 코드
모델은 앞서 설명했듯이 4개의 Convolutional 계층과, 2개의 Fully connected 계층 그리고 Dropout 계층을 사용한다. 각각의 계층별로는 코드가 다르지 않고 인지만 다르니 하나씩 만 설명하도록 한다.
Convolutional 계층
아래 코드는 두번째 Convolutional 계층의 코드이다.
FLAGS.conv2_layer_size 는 이 Convolutional 계층의 뉴런의 수로 32개를 사용한다.
FLAGS.conv2_filter_size 는 필터 사이즈를 지정하는데, 3x3 을 사용한다.
다음 Weight 값 W_conv2 와 Bias 값 b2를 지정한후에, 간단하게 tf.nn.conv2d 함수를 이용하면 2차원의 Convolutional 네트워크를 정의해준다. 다음 결과가 나오면 이 결과를 액티베이션 함수인 relu 함수에 넣은 후에, 마지막으로 max pooling 을 이용하여 결과를 뽑아낸다.
Fully connected 계층은 단순하게 relu(W*x + b) 함수이기 때문에 이 함수를 위와 같이 그대로 적용하였다.
마지막 계층
Fully connected 계층을 거쳐 나온 데이타는 Dropout 계층을 거친후에, 5개의 카테고리에 대한 확률로 결과를 내기 위해서 final_out 계층을 거치게 되는데, 이 과정에서 softmax 함수를 사용해야 하나, 학습 과정에서는 별도로 softmax 함수를 사용하지 않는다. softmax는 나온 결과의 합이 1.0이 되도록 값을 변환해주는 것인데, 학습 과정에서는 5개의 결과 값이 어떤 값이 나오던 가장 큰 값에 해당하는 것이 예측된 값이기 때문에, 그 값과 입력된 라벨을 비교하면 되기 때문이다.
즉 예를 들어 Jessica Alba일 확률이 100%면 실제 예측에서는 [1,0,0,0,0] 식으로 결과가 나와야 되지만, 학습 중는 Jessica Alaba 로 예측이 되었다고만 알면 되기 때문에 결과가 [1292,-0.221,-0.221,-0.221] 식으로 나오더라도 최대값만 찾으면 되기 때문에 별도로 softmax 함수를 적용할 필요가 없다. Softmax 함수는 연산 비용이 큰 함수이기 때문에 일반적으로 학습 단계에서는 적용하지 않는다.
이제 각 CNN의 각 계층을 함수로 정의 하였으면 각 계층을 묶어 보도록 하자. 묶는 법은 간단하다 앞 계층에서 나온 계층을 순서대로 배열하고 앞에서 나온 결과를 뒤의 계층에 넣는 식으로 묶으면 된다.
# build cnn_graph
def build_model(images,keep_prob):
# define CNN network graph
# output shape will be (*,48,48,16)
r_cnn1 = conv1(images) # convolutional layer 1
print ("shape after cnn1 ",r_cnn1.get_shape())
# output shape will be (*,24,24,32)
r_cnn2 = conv2(r_cnn1) # convolutional layer 2
print ("shape after cnn2 :",r_cnn2.get_shape() )
# output shape will be (*,12,12,64)
r_cnn3 = conv3(r_cnn2) # convolutional layer 3
print ("shape after cnn3 :",r_cnn3.get_shape() )
# output shape will be (*,6,6,128)
r_cnn4 = conv4(r_cnn3) # convolutional layer 4
print ("shape after cnn4 :",r_cnn4.get_shape() )
# fully connected layer 1
r_fc1 = fc1(r_cnn4)
print ("shape after fc1 :",r_fc1.get_shape() )
# fully connected layer2
r_fc2 = fc2(r_fc1)
print ("shape after fc2 :",r_fc2.get_shape() )
## drop out
# 참고 http://stackoverflow.com/questions/34597316/why-input-is-scaled-in-tf-nn-dropout-in-tensorflow
# 트레이닝시에는 keep_prob < 1.0 , Test 시에는 1.0으로 한다.
r_dropout = tf.nn.dropout(r_fc2,keep_prob)
print ("shape after dropout :",r_dropout.get_shape() )
# final layer
r_out = final_out(r_dropout)
print ("shape after final layer :",r_out.get_shape() )
return r_out
이 build_model 함수는 image 를 입력 값으로 받아서 어떤 카테고리에 속할지를 리턴하는 컨볼루셔널 네트워크이다. 중간에 Dropout 계층이 추가되어 있는데, tf.nn.dropout함수를 이용하면 간단하게 dropout 계층을 구현할 수 있다. r_fc2는 Dropout 계층 앞의 Fully Connected 계층에서 나온 값이고, 두번째 인자로 남긴 keep_prob는 Dropout 비율이다.
r_dropout = tf.nn.dropout(r_fc2,keep_prob)
print ("shape after dropout :",r_dropout.get_shape() )
모델 학습
데이타를 읽는 부분과 학습용 모델 정의가 끝났으면 실제로 학습을 시켜보자
def main(argv=None):
# define placeholders for image data & label for traning dataset
먼저 학습용 모델에 넣기 위한 image 데이타를 읽어드릴 placeholder를 images로 정의하고, 다음으로 모델에 의해 계산된 결과와 비교하기 위해서 학습데이타에서 읽어드린 label 데이타를 저장하기 위한 placeholder를 labels로 정의한다. 다음 image_batch,label_batch,fle_batch 변수에 배치로 학습용 데이타를 읽어드린다. 그리고 dropout 계층에서 dropout 비율을 지정할 keep_prob를 place holder로 정의한다.
각 변수가 지정되었으면, build_model 함수를 호출하여, images 값과 keep_prob 값을 넘겨서 Convolutional 네트워크에 값을 넣도록 그래프를 정의하고 그 결과 값을 prediction으로 정의한다.
keep_prob = tf.placeholder(tf.float32) # dropout ratio
prediction = build_model(images,keep_prob)
# define loss function
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction,labels=labels))
학습용 데이타가 아니라 검증용 데이타를 VALIDATION_FILE에서 읽어서 데이타를 validate_image_batch,validate_label_batch,validate_file_batch에 저장한다. 다음, 정확도 체크를 위해서 학습에서 예측된 라벨값과, 학습 데이타용 라벨값을 비교하여 같은지 틀린지를 비교하고, 이를 가지고 평균을 내서 정확도 (accuracy)로 사용한다.
학습용 모델과, 테스트용 데이타 등이 준비되었으면 이제 학습을 시작한다.
학습을 시직하기 전에, 학습된 모델을 저장하기 위해서 tf.train.Saver()를 지정한다. 그리고, 그래프로 loss와 accuracy등을 저장하기 위해서 Summary write를 저장한다.
다음 tf.global_variable_initializer()를 수행하여 변수를 초기화 하고, queue에서 데이타를 읽기 위해서 tf.train.Corrdinator를 선언하고 tf.start_queue_runners를 지정하여, queue 러너를 실행한다.
#build the summary tensor based on the tF collection of Summaries
summary = tf.summary.merge_all()
with tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)) as sess:
saver = tf.train.Saver() # create saver to store training model into file
안녕하세요~ 올려주신 글 잘 참고하고 있습니다! 제가 이번에 모바일로 얼굴인식을 해보고 싶어서 올려주신 글 참고해서 만들어 보고 있는데요! 모바일에 최적화된 모델을 제작하기위해 optimize_for_inference 과정을 거쳐야 하는 걸로 알고 있습니다! 그 과정에서 입력이름과 아웃풋레이어이름을 인자로 실행해야 한다고 알고 있는데요~
tensorflow/bazel-bin/tensorflow/python/tools/optimize_for_inference \
--input=input_graph.pb \
--output=optimized_graph.pb \
--input_names=? \
--output_names=?
여기서 input_names와 output_names이 각각 image와 final_out이 맞을까요? ㅠㅠ
그리고 해당 코드를 실행했을때ㅠㅠ
tf.train.batch 부분에서
OutOfRangeError (see above for traceback): FIFOQueue '_3_batch/fifo_queue' is closed and has insufficient elements (requested 100, current size 0)
와 같이 에러가 발생하는데요 ㅠㅠ 어떻게 해야할까요? 제 데이터 셋은 classes=5 한 클래스당 사진은 50장입니다 ㅠㅠ
대협님 좋은글 잘보았습니다.
다만,
여쭙고 싶은 중요한 부분이 있는데요.
필터를 초기화 하는부분에서
2x2x3차원(rgb)을 갖는 필터를 30개를 생성해야한다고하면,
대협님 글대로 쓰면[2,2,3,30] 이렇게 생겻는데,
[2,2,3,30] 이면 3x30짜리 2개를쓰는 필터를 2개를 생성하라 라는 의미로 생각이됩니다.
실제로 돌려봐도 텐서가 그렇게나옵니다.
이부분은 중요한 문제인것같은데,, 어떻게 된것인지 알려주세요 ㅠ
감사합니다.
마이크로소프트 azure의 jupyter notebok으로 돌리며 train:validate = 7:3으로 조정, 18명 연예인에 한 연예인당 50장의 사진을 이용했습니다.
그런데 다음과 같이 나옵니다. 어떻게 해야 할까요?
OutOfRangeError: FIFOQueue '_23_batch_4/fifo_queue' is closed and has insufficient elements (requested 70, current size 0)
[[Node: batch_4 = QueueDequeueManyV2[component_types=[DT_UINT8, DT_INT32, DT_STRING], timeout_ms=-1, _device="/job:localhost/replica:0/task:0/cpu:0"](batch_4/fifo_queue, batch_4/n)]]
텐서플로우를 학습하면서 실제 모델을 만들어보려고 하니 생각보다 데이타 처리에 대한 부분에서 많은 노하우가 필요하다는 것을 알게되었다. MNIST와 같은 예제는 데이타가 다 이쁘게 정리되어서 학습 하기 좋은 형태로 되어 있지만, 실제로 내 모델을 만들고 학습을 하기 위해서는 데이타에 대한 정재와 분류 작업등이 많이 필요하다.
이번글에서는 학습에 필요한 데이타를 파일에서 읽을때 필요한 큐에 대한 개념에 대해서 알아보도록 한다.
피딩 (Feeding) 개념 복습
텐서플로우에서 모델을 학습 시킬때, 학습 데이타를 모델에 적용하는 방법은 일반적으로 피딩 (feeding)이라는 방법을 사용한다. 메모리상의 어떤 변수 리스트 형태로 값을 저장한 후에, 모델을 세션에서 실행할 때, 리스트에서 값을 하나씩 읽어서 모델에 집어 넣는 방식이다.
위의 그림을 보면, y=W*x라는 모델에서 학습 데이타 x는 [1,2,3,4,5]로, 첫번째 학습에는 1, 두번째 학습에는 2를 적용하는 식으로 피딩이 된다.
그런데, 이렇게 피딩을 하려면, 학습 데이타 [1,2,3,4,5]가 메모리에 모두 적재되어야 하는데, 실제로 모델을 만들어서 학습을할때는 데이타의 양이 많기 때문에 메모리에 모두 적재하고 학습을 할 수 가 없고, 파일에서 읽어드리면서 학습을 해야 한다.
텐서플로우 큐에 대해서
이러한 문제를 해결하기 위해서는 파일에서 데이타를 읽어가면서, 읽은 데이타를 순차적으로 모델에 피딩하면 되는데, 이때 큐를 사용한다.
파일에서 데이타를 읽는 방법에 앞서서 큐를 설명하면, 큐에 데이타를 넣는 것(Enqueue) 은 Queue Runner 라는 것이 한다.
이 Queue Runner가 큐에 어떤 데이타를 어떻게 넣을지를 정의 하는 것이 Enqueue_operation인데, 데이타를 읽어서 실제로 어떻게 Queue에 Enqueue 하는지를 정의한다.
이 Queue Runner는 멀티 쓰레드로 작동하는데, Queue Runner 안의 쓰레드들을 관리해주기 위해서 별도로 Coordinator라는 것을 사용한다.
이 개념을 정리해서 도식화 해주면 다음과 같다.
=
Queue Runner 는 여러개의 쓰레드 (T)를 가지고 있고, 이 쓰레드들은 Coordinator들에 의해서 관리된다. Queue Runner 가 Queue에 데이타를 넣을때는 Enqueue_op이라는 operation에 의해 정의된 데로 데이타를 Queue에 집어 넣는다.
위의 개념을 코드로 구현해보자
import tensorflow as tf
QUEUE_LENGTH = 20
q = tf.FIFOQueue(QUEUE_LENGTH,"float")
enq_ops = q.enqueue_many(([1.0,2.0,3.0,4.0],) )
qr = tf.train.QueueRunner(q,[enq_ops,enq_ops,enq_ops])
sess = tf.Session()
# Create a coordinator, launch the queue runner threads.
첫번째 인자는 큐의 길이를 정하고, 두번째는 dtype으로 큐에 들어갈 데이타형을 지정한다.
Queue Runner 생성
다음은 Queue Runner를 만들기 위해서 enqueue_operation 과, QueueRunner를 생성한다.
enq_ops = q.enqueue_many(([1.0,2.0,3.0,4.0],) )
qr = tf.train.QueueRunner(q,[enq_ops,enq_ops,enq_ops])
enqueue operation인 enq_ops는 위와 같이 한번에 [1.0,2.0,3.0,4.0] 을 큐에 넣는 operation으로 지정한다.
그리고 Queue Runner를 정의하는데, 앞에 만든 큐에 데이타를 넣을것이기 때문에 인자로 큐 ‘q’를 넘기고 list 형태로 enq_ops를 3개를 넘긴다. 3개를 넘기는 이유는 Queue Runner가 멀티쓰레드 기반이기 때문에 각 쓰레드에서 Enqueue시 사용할 Operation을 넘기는 것으로, 3개를 넘긴것은 3개의 쓰레드에 Enqueue 함수를 각각 지정한 것이다.
만약 동일한 enqueue operation을 여러개의 쓰레드로 넘길 경우 위 코드처럼 일일이 enqueue operation을 쓸 필요 없이
qr = tf.train.QueueRunner(q,[enq_ops]*NUM_OF_THREAD)
[enq_ops] 에 쓰레드 수 (NUM_OF_THREAD)를 곱해주면 된다.
Coordinator 생성
이제 Queue Runner에서 사용할 쓰레드들을 관리할 Coordinator를 생성하자
coord = tf.train.Coordinator()
Queue Runner용 쓰레드 생성
Queue Runner와 쓰레드를 관리할 Coordinator 가 생성되었으면, Queue Runner에서 사용할 쓰레드들을 생성하자
좋은 글 감사합니다. 정독하면서 공부중입니다!
아직 잘 모르는 부분이 많아서요.. 한가지 질문을 드리고 싶은데
enq_ops = q.enqueue_many(([1.0,2.0,3.0,4.0],) )
enqueue_many 안에 데이터를 튜플로 넣는지,
[1.0, 2.0, 3.0, 4.0] 뒤에 컴마를 적지 않고 하나의 리스트만 넣으면 왜 작동이 안되는지 궁금합니다.
coordinator와 thread 개념 찾다가 이 블로그에 들어왔는데, 덕분에 queue와 thread, coordinator의 개념을 쉼게 이해할 수 있었습니다. 감사드려요. 저번에도 tensorflow정보 찾다가 이 블로그 들어와서 좋은 정보를 얻은 적이 있었어요ㅋㅋ 이렇게 명쾌하고 친절하게 설명하기 힘드셨을텐데 진짜 대단하세요. 항상 공부하시고 지식을 널리 알려주시는 점 존경스럽습니다!
를 이용하여 x에 image를 넣고, 그리고 dropout 비율을 0%로 하기 위해서 keep_prob를 1.0 (100%)로 한다. (예측이기 때문에 당연히 dropout은 필요하지 않다.)
이렇게 하면 이 이미지가 어떤 숫자인지에 대한 확률이 p에 저장된다.
그래프로 표현
그러면 이 p의 값을 찍어 보자
fig = plt.figure(figsize=(4,2))
pred = p_val[0]
subplot = fig.add_subplot(1,1,1)
subplot.set_xticks(range(10))
subplot.set_xlim(-0.5,9.5)
subplot.set_ylim(0,1)
subplot.bar(range(10), pred, align='center')
plt.show()
그래프를 이용하여 0~9 까지의 숫자 (가로축)일 확률을 0.0~1.0 까지 (세로축)으로 출력하게 된다.
다음은 위에서 입력한 숫자 “4”를 인식한 결과이다.
(보너스) 첫번째 컨볼루셔널 계층 결과 출력
컨볼루셔널 네트워크를 학습시키다 보면 종종 컨볼루셔널 계층을 통과하여 추출된 특징 이미지들이 어떤 모양을 가지고 있는지를 확인하고 싶을때가 있다. 그래서 각 필터를 통과한 값을 이미지로 출력하여 확인하고는 하는데, 여기서는 이렇게 각 필터를 통과하여 인식된 특징이 어떤 모양인지를 출력하는 방법을 소개한다.
아래는 우리가 만든 네트워크 중에서 첫번째 컨볼루셔널 필터를 통과한 결과 h_conv1과, 그리고 이 결과에 bias 값을 더하고 활성화 함수인 Relu를 적용한 결과를 출력하는 예제이다.
첫번째 필터는 총 32개로 구성되어 있기 때문에, 32개의 결과값을 imshow 함수를 이용하여 흑백으로 출력하였다.
다음은 bias와 Relu를 통과한 값인 h_conv_cutoff를 출력하는 예제이다. 위의 코드와 동일하며 subplot.imgshow에서 전달해주는 인자만 conv1_vals → cutoff1_vals로 변경되었다.
코드
fig = plt.figure(figsize=(16,4))
for f in range(num_filters1):
subplot = fig.add_subplot(4, 16, f+1)
subplot.set_xticks([])
subplot.set_yticks([])
subplot.imshow(cutoff1_vals[0,:,:,f],
cmap=plt.cm.gray_r, interpolation='nearest')
plt.show()
출력 결과는 다음과 같다
이제까지 컨볼루셔널 네트워크를 이용한 이미지 인식을 텐서플로우로 구현하는 방법을 MNIST(필기체 숫자 데이타)를 이용하여 구현하였다.
실제로 이미지를 인식하려면 전체적인 흐름은 같지만, 이미지를 전/후처리 해내야 하고 또한 한대의 머신이 아닌 여러대의 머신과 GPU와 같은 하드웨어 장비를 사용한다. 다음 글에서는 MNIST가 아니라 실제 칼라 이미지를 인식하는 방법에 대해서 데이타 전처리에서 부터 서비스까지 전체 과정에 대해서 설명하도록 하겠다.
파이어 베이스는 모바일 백앤드의 종합 솔루션으로, 크래쉬 리포팅, 리모트 컨피그를 이용한 A/B 테스팅 플랫폼, 클라우드와 자동 동기화가 가능한 리얼타임 데이타 베이스, 사용자 인증 기능, 강력한 푸쉬 플랫폼 다양한 모바일 기기에 대해서 테스트를 해볼 수 있는 테스트랩 등, 모바일 앱 개발에 필요한 모든 서비스를 제공해주는 종합 패키지와 같은 플랫폼이라고 보면 된다. 안드로이드 뿐만 아니라 iOS까지 지원하여 모든 모바일 앱 개발에 공통적으로 사용할 수 있다.
그중에서 파이어베이스 애널러틱스 (Firebase analytics)는 모바일 부분은 모바일 앱에 대한 모든 이벤트를 수집 및 분석하여 자동으로 대쉬 보드를 통하여 분석을 가능하게 해준다.
이 글에서는 파이어베이스 전체 제품군중에서 파이어베이스 애널러틱스에 대해서 수회에 걸쳐서 설명을 하고자 한다.
파이어베이스 애널러틱스
이미 시장에는 모바일 앱에 대한 데이타 분석이 가능한 유료 또는 무료 제품이 많다.
대표적으로 야후의 flurry, 트위터 fabric, 구글 애널러틱스등이 대표적인 제품군인데, 그렇다면 파이어베이스가 애널러틱스가 가지고 있는 장단점은 무엇인가?
퍼널 분석 및 코호트 분석 지원
파이어베이스 애널러틱스는 데이타 분석 방법중에 퍼넬 분석과 코호트 분석을 지원한다.
퍼널 분석은 한글로 깔데기 분석이라고 하는데, 예를 들어 사용자가 가입한 후에, 쇼핑몰의 상품 정보를 보고 주문 및 결재를 하는 단계 까지 각 단계별로 사용자가 이탈하게 된다. 이 구조를 그려보면 깔데기 모양이 되는데,사용자 가입에서 부터 최종 목표인 주문 결재까지 이루도록 단계별로 이탈율을 분석하여 서비스를 개선하고, 이탈율을 줄이는데 사용할 수 있다.
코호트 분석은 데이타를 집단으로 나누어서 분석하는 방법으로 일일 사용자 데이타 (DAU:Daily Active User)그래프가 있을때, 일일 사용자가 연령별로 어떻게 분포가 되는지등을 나눠서 분석하여 데이타를 조금 더 세밀하게 분석할 수 있는 방법이다.
이러한 코호트 분석과 퍼넬 분석은 모바일 데이타 분석 플랫폼 중에서 일부만 지원하는데, 파이어베이스 애널러틱스는 퍼넬과 코호트 분석을 기본적으로 제공하고 있으며, 특히 코호트 분석으로 많이 사용되는 사용자 잔존율 (Retention 분석)의 경우 별다른 설정 없이도 기본으로 제공하고 있다.
구글의 빅쿼리는 아마존 S3나, 구글의 스토리지 서비스인 GCS 보다 저렴한 비용으로 데이타를 저장하면서도, 수천억 레코드에 대한 연산을 수십초만에 8~9000개의 CPU와 3~4000개의 디스크를 사용해서 끝낼만큼 어마어마한 성능을 제공하면서도, 사용료 매우 저렴하며 기존 SQL 문법을 사용하기 때문에, 매우 쉽게 접근이 가능하다.
모바일 데이타 분석을 쉽게 구현이 가능
보통 모바일 서비스에 대한 데이타 분석을 할때는 무료 서비스를 통해서 DAU나 세션과 같은 기본적인 정보 수집은 가능하지만, 추가적인 이벤트를 수집하여 저장 및 분석을 하거나 서버나 다른 시스템의 지표를 통합 분석 하는 것은 별도의 로그 수집 시스템을 모바일 앱과 서버에 만들어야 하였고, 이를 분석 및 저장하고 리포팅 하기 위해서 하둡이나 스파크와 같은 복잡한 빅데이타 기술을 사용하고 리포팅에도 많은 시간이 소요 되었다.
파이어베이스 애널러틱스를 이용하면, 손 쉽게, 추가 이벤트나 로그 정보를 기존의 로깅 프레임웍을 통하여 빅쿼리에 저장할 수 있고, 복잡한 하둡이나 스파크의 설치나 프로그래밍 없이 빅쿼리에서 간략하게 SQL만을 사용하여 분석을 하고 오픈소스 시각화 도구인 Jupyter 노트북이나 구글의 데이타스튜디오 (http://datastudio.google.com)을 통하여 시작화가 간단하기 때문에, 이제는 누구나 쉽게 빅데이타 로그를 수집하고 분석할 수 있게 된다.
실시간 데이타 분석은 지원하지 않음
파이어베이스 애널러틱스가 그러면 만능 도구이고 좋은 기능만 있는가? 그건 아니다. 파이어베이스 애널러틱스는 아직까지는 실시간 데이타 분석을 지원하고 있지 않다. 수집된 데이타는 보통 수시간이 지나야 대쉬 보드에 반영이 되기 때문에 현재 접속자나, 실시간 모니터링에는 적절하지 않다.
그래서 보완을 위해서 다른 모니터링 도구와 혼용해서 사용하는 게 좋다. 실시간 분석이 강한 서비스로는 트위터 fabric이나 Google analytics 등이 있다.
이러한 도구를 이용하여 데이타에 대한 실시간 분석을 하고, 정밀 지표에 대한 분석을 파이어베이스 애널러틱스를 사용 하는 것이 좋다.
다음으로 파이어베이스 프로젝트를 생성한다. 상단 메뉴에서 “CREATE NEW PROJECT”를 선택하면 새로운 파이어 베이스 프로젝트를 생성할 수 있다. 만약에 기존에 사용하던 구글 클라우드 프로젝트등이 있으면 별도의 프로젝트를 생성하지 않고 “IMPORT GOOGLE PROJECT”를 이용하여 기존의 프로젝트를 불러와서 연결할 수 있다.
프로젝트가 생성되었으면 파이어베이스를 사용하고자 하는 앱을 등록해야 한다.
파이어베이스 화면에서 “ADD APP” 이라는 버튼을 누르면 앱을 추가할 수 있다.
아래는 앱을 추가하는 화면중 첫번째 화면으로 앱에 대한 기본 정보를 넣는 화면이다.
“Package name” 에, 파이어베이스와 연동하고자 하는 안드로이드 앱의 패키지 명을 넣는다.
ADD APP 버튼을 누르고 다음 단계로 넘어가면 google-services.json 이라는 파일이 자동으로 다운된다. 이 파일은 나중에 안드로이드 앱의 소스에 추가해야 하기 때문에 잘 보관한다.
Continue 버튼을 누르면 아래와 같이 다음 단계로 넘어간다. 다음 단계에서는 안드로이드 앱을 개발할때 파이어베이스를 연동하려면 어떻게 해야 하는지에 대한 가이드가 나오는데, 이 부분은 나중에 코딩 부분에서 설명할 예정이니 넘어가도록 하자.
자 이제 파이어베이스 콘솔에서, 프로젝트를 생성하고 앱을 추가하였다.
이제 연동을 할 안드로이드 애플리케이션을 만들어보자.
안드로이드 빌드 환경 설정
콘솔에서 앱이 추가되었으니, 이제 코드를 작성해보자, 아래 예제는 안드로이드 스튜디오 2.1.2 버전 (맥 OS 기준) 으로 작성되었다.
먼저 안드로이드 프로젝트를 생성하였다. 이때 반드시 안드로이드 프로젝트에서 앱 패키지 명은 앞에 파이어베이스 콘솔에서 지정한 com.terry.hellofirebase가 되어야 한다.
안드로이드 프로젝트에는 프로젝트 레벨의 build.gradle 파일과, 앱 레벨의 build.gradle 파일이 있는데
프로젝트 레벨의 build.gradle 파일에 classpath 'com.google.gms:google-services:3.0.0' 를 추가하여 다음과 같이 수정한다.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.google.gms:google-services:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
다음으로, 앱레벨의 build.gradle 파일도 dependencies 부분에 compile 'com.google.firebase:firebase-core:9.4.0' 를 추가하고, 파일 맨 아래 apply plugin: 'com.google.gms.google-services' 를 추가 하여 아래와 같이 수정한다.
MainActivity 클래스에 FirebaseAnalytics 객체를 mFirebaseAnalytics라는 이름으로 정의하고 onCreate메서드에서 FirebaseAnalytics.getInstance(this) 메서드를 이용하여 파이어베이스 애널러틱스 객체를 생성한다.
다음 onSendEvent라는 메서드를 구현한다. 이 메서드는 화면에서 “SEND EVENT”라는 버튼을 누르면 EditText 박스에서 입력된 값으로 SELECT_CONTENT라는 이벤트를 만들어서 파이어베이스 애널러틱스 서버로 보내는 기능을 한다.
컨텐츠 ID,NAME,CATEGORY를 EditText 박스에서 읽어온 후에, Bundle 이라는 객체를 만들어서 넣는다.
파이어베이스 애널러틱스 로그는 이벤트와 번들이라는 개념으로 구성이 된다.
이벤트는 로그인, 컨텐츠 보기, 물품 구매와 같은 이벤트이고, Bundle은 이벤트에 구체적인 인자를 묶어서 저장하는 객체이다. 위의 예제인 경우 SELECT_CONTENTS 라는 이벤트가 발생할때 컨텐츠 ID, 이름(Name), 종류(Category)를 인자로 하여, Bundle에 묶어서 전달하도록 하였다.
Bundle 클래스를 생성한후, bundle.putString(“인자명",”인자값") 형태로 Bundle 객체를 설정한 후에, mFirebaseAnalytics.logEvent(“이벤트명",”Bundle 객체") 메서드를 이용하여 SELECT_CONTENTS 이벤트에 앞서 작성한 Bundle을 통하여 인자를 전달하였다.
앱 개발이 모두 완료되었다. 이제 테스트를 해보자
실행하기
앱을 실행하고 아래와 같이 데이타를 넣어보자
컨텐츠 ID는 200, 컨텐츠 이름은 W, 그리고 컨텐츠 종류는 webtoon으로 입력하였다.
SEND EVENT 눌러서 이벤트를 보내서 파이어베이스 웹콘솔에 들어가서 Analytics 메뉴에 상단 메뉴인 “Events”를 선택하면 처음에는 아무런 값이 나오지 않는다.
앞에서 설명했듯이 파이어베이스 애널러틱스는 아직까지 실시간 분석을 지원하지 않기 때문에 수시간이 지난 후에야 그 값이 반영 된다.
본인의 경우 밤 12시에 테스트를 진행하고 아침 9시경에 확인을 하였더니 아래와 같은 결과를 얻을 수 있었다.
실제로 테스트 시에 select contents 이벤트를 3번을 보냈더니, Count가 3개로 나온다.
그러나 이벤트에 보낸 컨텐츠 ID, 이름 , 분류등은 나타나지 않는다. 기본 설정에서는 이벤트에 대한 디테일 정보를 얻기가 어렵다. 그래서 빅쿼리 연동이 필요한데 이는 후에 다시 다루도록 하겠다.
Dashboard 메뉴를 들어가면 다음과 같이 지역 분포나 단말명등 기본적인 정보를 얻을 수 있다.
이벤트와 이벤트 인자
앞서처럼 이벤트와 인자등을 정해줬음에도 불구하고 대쉬보드나 기타 화면에 수치들이 상세하지 않은 것을 인지할 수 있다. 정확한 데이타를 분석하려면 마찬가지로 정확한 데이타를 보내줘야 하는데, 화면 로그인이나 구매등과 같은 앱에서의 이벤트를 앱 코드내에 삽입해줘야 상세한 분석이 가능하다.
지금까지 간략하게 나마 파이어베이스 애널러틱스의 소개와 예제 코드를 통한 사용 방법을 알아보았다.
모바일 데이타 분석이나 빅데이타 분석에서 가장 중요한 것은 데이타를 모으는 것도 중요하지만, 모아진 데이타에 대한 지표 정의와 그 의미를 파악하는 것이 중요하다. 그래서 다음 글에서는 파이어베이스 애널러틱스에 정의된 이벤트의 종류와 그 의미 그리고, 대쉬 보드를 해석하는 방법에 대해서 설명하고, 그 후에 빅쿼리 연동을 통해서 상세 지표 분석을 하는 방법에 대해서 소개하고자 한다.
설치 후에, Create Connection에서, 로컬호스트에 설치된 mongoDB를 연결하기 위해서 연결 정보를
기술하고, 연결을 만든다
Figure 2 robomongo에서 localhost에 있는 mongodb 연결 추가
주소는 localhost, 포트는 디폴트 포트로 27017를 넣으면 된다.
환경이 준비가 되었으면 간단한 테스트를 해보자. 테스트 전에 기본적인
개념을 숙지할 필요가 있는데, mongoDB는 NoSQL 계열중에서도
도큐먼트DB (Document DB)에 속한다. 기존 RDBMS에서 하나의 행이 데이타를 표현했다면, mogoDB는 하나의
JSON 파일이 하나의 데이타를 표현한다. 이 JSON을 도큐먼트라고 하기 때문에, 도큐먼트 DB라고 한다.
제일 상위 개념은 DB의 개념에 대해서 알아보자, DB는 여러개의 테이블(컬렉션)을
저장하는 단위이다.
Robomongo에서 mydb라는
이름으로 DB 를 생성해보자
Figure 3 robomongo에서 새로운 DB를 추가 하는 화면
다음으로 생성된 DB안에, 컬렉션을
생성한다. 컬렉션은 RDBMS의 단일 테이블과 같은 개념이다.
Robomongo에서 다음과 같이 ‘users’라는
이름의 컬렉션을 생성한다
Figure 4 robomongo에서 컬렉션(Collection) 생성
users 컬렉션에는 userid를
키로 해서, sex(성별), city(도시) 명을 입력할 예정인데, userid가 키이기 때문에, userid를 통한 검색이나 소팅등이 발생한다. 그래서 userid를 인덱스로 지정한다.
인덱스 지정 방법은 createIndex 명령을 이용한다. 다음과 같이 robomongo에서 createIndex 명령을 이용하여 인덱스를 생성한다.
Figure 5 users 컬렉션에서 userid를 인덱스로 지정
mongoDB는 디폴트로, 각
컬렉션마다 “_id”라는 필드를 가지고 있다. 이 필드는
컬렉션 안의 데이타에 대한 키 값인데, 12 바이트의 문자열로 이루어져 있고 ObjectId라는 포맷으로 “시간-머신이름,프로세스ID,증가값” 형태로
이루어지는 것이 일반적이다.
이 _id 필드에 userid를
저장하지 않고 별도로 인덱스를 만들어가면서 까지 userid 필드를 별도로 사용하는 것은 mongoDB는 NoSQL의 특성상 여러개의 머신에 데이타를 나눠서
저장한다. 그래서 데이타가 여러 머신에 골고루 분산되는 것이 중요한데,
애플리케이션상의 특정 의미를 가지고 있는 필드를 사용하게 되면 데이타가 특정 머신에 쏠리는 현상이 발생할 수 있다.
예를 들어서, 주민번호를 _id로
사용했다면, 데이타가 골고루 분산될것 같지만, 해당 서비스가 10~20대에만 인기있는 서비스라면, 10~20대 데이타를 저장하는
머신에만 데이타가 몰리게 되고, 10세이하나, 20세 이상의
데이타를 저장하는 노드에는 데이타가 적게 저장된다.
이런 이유등으로 mongoDB를 지원하는 node.js 드라이버에서는 _id 값을 사용할때, 앞에서 언급한 ObjectId 포맷을 따르지 않으면 에러를 내도록
설계되어 있다. 우리가 앞으로 살펴볼 mongoose나 monk의 경우에도 마찬가지이다.
이제 데이타를 집어넣기 위한 테이블(컬렉션) 생성이 완료되었다.
다음 컬렉션 에 대한 CRUD (Create, Read, Update,
Delete) 를 알아보자
SQL 문장과 비교하여, mongoDB에서
CRUD 에 대해서 알아보면 다음과 같다.
CRUD
SQL
MongoDB
Create
insert into users
("name","city")
values("terry","seoul")
mongoDB를 node.js에서
호출하는 방법은 여러가지가 있으나 대표적인 두가지를 소개한다.
첫번째 방식은 mongoDB 드라이버를 이용하여 직접 mongoDB 쿼리를 사용하는 방식이고, 두번째 방식은 ODM (Object Document Mapper)를 이용하는 방식이다. ODM
방식은 자바나 다른 프로그래밍 언어의 ORM (Object Relational Mapping)과
유사하게 직접 쿼리를 사용하는 것이 아니라 맵퍼를 이용하여 프로그램상의 객체를 데이타와 맵핑 시키는 방식이다. 뒷부분에서
직접 코드를 보면 이해가 빠를 것이다.
Monk를 이용한 연결
첫번째로 mongoDB 네이티브 쿼리를 수행하는 방법에 대해서 소개한다. monk라는 node.js용 mongoDB
클라이언트를 이용할 것이다.
monk 모듈을 이용하기 위해서 아래와 같이 package.json에 monk에 대한 의존성을 추가한다.
{
"name": "mongoDBexpress",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"express": "~4.13.1",
"jade": "~1.11.0",
"morgan": "~1.6.1",
"serve-favicon": "~2.3.0",
"monk":"~1.0.1"
}
}
Figure 9 monk 모듈에 대한 의존성이 추가된 package.json
app.js에서 express가
기동할때, monk를 이용해서 mongoDB에 연결하도록
한다.
var monk = require('monk');
var db = monk('mongodb://localhost:27017/mydb');
var mongo = require('./routes/mongo.js');
app.use(function(req,res,next){
req.db = db;
next();
});
app.use('/', mongo);
Figure 10 monk를 이용하여 app.js에서 mongoDB 연결하기
mongoDB에 연결하기 위한 연결 문자열은 'mongodb://localhost:27017/mydb' 로
mongo://{mongoDB 주소}:{mongoDB 포트}/{연결하고자 하는 DB명} 으로
이 예제에서는 mongoDB 연결을 간단하게 IP,포트,DB명만 사용했지만, 여러개의 인스턴스가 클러스터링 되어 있을 경우, 여러 mongoDB로 연결을 할 수 있는 설정이나, Connection Pool과 같은 설정, SSL과 같은 보안 설정등
부가적인 설정이 많으니, 반드시 운영환경에 맞는 설정으로 변경하기를 바란다. 설정 방법은 http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/문서를 참고하자.
이때 주의깊게 살펴봐야 하는 부분이 app.use를 이용해서 미들웨어를
추가하였는데, req.db에 mongodb 연결을 넘기는
것을 볼 수 있다. 미들웨어로 추가가 되었기 때문에 매번 HTTP 요청이
올때 마다 req 객체에는 db라는 변수로 mongodb 연결을 저장해서 넘기게 되는데, 이는 HTTP 요청을 처리하는 것이 router에서 처리하는 것이 일반적이기
때문에, 이 router로 db 연결을 넘기기 위함이다. 아래 데이타를 삽입하는 라우터 코드를
보자
req 객체에서 폼 필드를 읽어서 userid,sex,city등을
읽어내고, 앞의 app.js 에서 추가한 미들웨어에서 넘겨준
db 객체를 받아서 db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc) 를수행하여데이타를insert 하였다.
Figure 16 http://localhost:3000/monksample.html 실행 결과
아래 insert 버튼을 누르면, 채워진
필드로 새로운 레코드를 생성하고, update 버튼은 user 필드에
있는 사용자 이름으로된 데이타를 업데이트 한다. list 버튼은 컬렉션에서 전체 데이타를 조회해서 출력하고, delete 버튼은 user 필드에 있는 사용자 이름으로된 레코드를
삭제한다. get 버튼은 user 필드에 있는 사용자 이름으로
데이타를 조회하여 리턴한다.
다음은 list로 전체 데이타를 조회하는 화면이다.
Figure 17 /list를 수행하여 mongoDB에 저장된 전체 데이타를 조회하는
화면
댓글을 달아 주세요