서론
CNN 모델을 이용하여 얼굴 표정을 인식하는 모델입니다.
angry, disgusted, fearful, happy, sad, suprised, neutral 총 7개의 표정을 인식합니다.
Facial Expression Recognition(FER) 2013에서 65%의 accuracy를 기록했습니다.
https://github.com/xionghc/Facial-Expression-Recognition
GitHub에서 찾아 표정 인식 프로젝트를 진행했습니다. 이 코드를 선택한 이유는, 회사에서 사용하는 임베디드가 rknn 라이브러리를 이용하기 때문에, 원할한 모델 convert를 위해 선택했습니다. ^^
pytorch, keras 등 최근에 더 좋은 모델도 많으니, github에서 좋은 코드를 검색해서 사용하시길 추천드립니다!
본론
원래 코드는 webcam을 띄워 스크린샷하여 그 때의 표정을 인식하는 코드입니다.
외부 서버에는 webcam을 연결할 수 없기 때문에 웹캠을 이미지로 바꾸어주었습니다.
동영상으로 계속 표정을 인식하도록 구현하려 했으나, rknn 변환 후 init 시간에 의해 Segmentation Fault가 발생하여 (원인을 파악하지 못함) 일단 이미지 처리로 프로젝트를 마무리했습니다!
https://github.com/Seojiyoung/Emotion-Recognition
코드를 수정하고 rknn 모델로 바꾸어 임베디드에서 사용할 수 있는 예제를 마무리했습니다 :)
코드 분석 및 개념 정리
텐서(Tensor): 텐서플로우에서 다양한 수학식을 계산하기 위한 가장 기본적이고 중요한 자료형
- Rank, Shape 개념을 가짐
텐서플로우 프로그램은 그래프 생성 ⇒ 그래프 실행 으로 짜여짐
- 그래프: 텐서들의 연산 모음
- 그래프 실행은 sess.run()
def demo(modelPath, showBox=False):
#데이터타입은 float32, shape=[None,2304]
face_x = tf.placeholder(tf.float32, [None, 2304])
플레이스홀더는 그래프에 사용할 입력값을 나중에 받기 위해 사용하는 매개변수
>> (?, 2304) 을 가져올 것임을 알 수 있다.
#deepnn 모델 적용
y_conv = deepnn(face_x)
deepnn은 직접 만든 함수로, model.py에서 deepnn 함수를 import하여 사용
- 모델 output을 y_conv 변수에 저장한다.
#softmax
probs = tf.nn.softmax(y_conv)
y_conv를 softmax 활성화하여 probs에 저장한다.
* softmax 함수 식: tf.exp(logits) / tf.reduce_sum(tf.exp(logits), axis)
https://medium.com/@a.ydobon/ml-softmax-소프트맥스-함수-2f4740141bfe
자세한 설명은 위의 블로그를 참고했습니다. :) 이미지로는 이해가 되지 않으니 들어가서 설명을 읽어보세요!
#변수 선언
saver = tf.train.Saver()
ckpt = tf.train.get_checkpoint_state(modelPath)
sess = tf.Session()
# ckpt 모델 불러와 모델구동
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess, ckpt.model_checkpoint_path)
print('Restore model sucsses!!')
ckpt와 ckpt모델 path가 있으면, ckpt로부터 파라미터, 모델 구조를 그래프에 저장한다.
[ckpt와 pbtxt의 차이점]
1) .ckpt는 모델의 변수(가중치) 저장 되어 있고, .pbtxt는 모델 구조가 텍스트로 저장되어 있다.
⇒ 일반적으로 .ckpt=.ckpt.data이기 때문에 이렇게 설명할 수 있다.
2) .pb = .ckpt + .pbtxt
[체크포인트의 종류]
1) .ckpt.data : 모델을 제외한 학습한 가중치 (weight)만 있는 파일
2) .ckpt.meta : 모델(graph)만 있는 파일
3) .ckpt.index : 체크포인트에 대한 정보(index)
result = None
#이미지 읽기
input_image = cv2.imread('input1111.jpg', cv2.IMREAD_COLOR)
#resize한 이미지, 가장 큰 얼굴 object
detected_face, face_coor = format_image(input_image)
#탐지된 이미지가 있다면,
if detected_face is not None:
#image를tensor로 변환: tensor size는 (1,2304) // detected_face는 48X48
tensor = image_to_tensor(detected_face)
입력 이미지를 전처리 하는 부분이다.
- result는 결과 값을 저장하기 위해 선언한 변수
- 이미지를 읽고 ⇒ resize & 얼굴 object 탐지
- 탐지된 이미지가 있다면, 이미지를 배열로 변환
#probs는softmax
#feed dictionary는 텐서를 매핑
result = sess.run(probs, feed_dict={face_x: tensor})
모델 구동 부분입니다.
- tensor를 입력으로 deepnn 모델 ⇒ softmax 함수를 통해 얻은 결과 배열을 result에 저장합니다.
if result is not None:
max_result=result.max()
for i in range(7):
if result[0,i] == max_result:
print(EMOTIONS[i])
모델 출력 후처리 부분입니다.
- 출력의 max값 (1)을 max_result에 저장
- result 배열 중 가장 큰 값을 가지는 index를 통해, EMOTION 배열의 결과 값을 출력합니다.
모델 네트워크 구조
deepnn모델 구조 (CNN 모델)을 분석한 내용입니다.
def deepnn(x):
x_image = tf.reshape(x, [-1, 48, 48, 1])
[1, 2304] 배열을 4차원 [-1, 48, 48, 1] 배열로 변환
: -1은 갯수가 정해지지 않아서 모르는 경우에 사용한다.
x_image : (?, 48, 48, 1)
# conv1
W_conv1 = weight_variables([5, 5, 1, 64])
b_conv1 = bias_variable([64])
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
- random한 값으로 채워진 [5, 5, 1, 64] 모양의 배열을 w_conv1 에 저장
5X5X1 필터를 64개 만드는 것임
(자르기 전 정규 분포의 표준편차 = 0.1)
- bias는 0.1로 텐서의 shape와 동일한 배열을 b_conv1 에 저장
h_conv1: (?, 48, 48, 64)
- strides = [1, stride, stride, 1]
- 출력 채널(activation maps)은 padding=same이므로 원본 사이즈인 48X48과 동일한 출력 값을 얻는다.
- ReLU 활성화 함수 이용
# pool1
h_pool1 = maxpool(h_conv1)
- ksize가 [1, 3, 3, 1]이라는 뜻은 3칸씩 이동하면서 출력 결과를 1개 만들어 낸다는 것이다.
- stride가 [1, 2, 2, 1]이므로 1/4로 resize됨
h_pool1: (?, 24, 24, 64)
# conv2
W_conv2 = weight_variables([3, 3, 64, 64])
b_conv2 = bias_variable([64])
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
norm2 = tf.nn.lrn(h_conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75)
h_pool2 = maxpool(norm2)
conv1은 conv → maxpool 순이었다면,
conv2는 conv → lrn → maxpool 순이다.
- random한 값으로 채워진 [3, 3, 64, 64] 모양의 배열을 w_conv2 에 저장
3X3X64 필터를 64개 만드는 것임
- bias는 0.1가 64개 존재
- convolution 연산 후 relu 함수 이용
- lrn으로 정규화
- maxpooling 진행 (9개 중 1개의 max값 뽑아냄) & 1/4크기로 resize
* tf.nn.local_response_normalize 함수
: 인접한 커널의 결과들끼리 연산을 함으로써 이웃에 대해 균일하게 큰 응답을 감소시키고 이웃 내에서 큰 활성화를 더욱 두드러지게 함
⇒ 활성화 할 때 더 높은 대비가 생성되게 함
⇒ 요즘에는 dropout 또는 batch normalization을 사용
h_pool2: (?, 12, 12, 64)
# Fully connected layer
W_fc1 = weight_variables([12 * 12 * 64, 384])
b_fc1 = bias_variable([384])
h_conv3_flat = tf.reshape(h_pool2, [-1, 12 * 12 * 64])
h_fc1 = tf.nn.relu(tf.matmul(h_conv3_flat, W_fc1) + b_fc1)
- fully connected layer으로 출력을 7로 맞추어 주어야 한다! 여기 코드부터 끝까지 fully connected layer라고 보면 된다.
- matmul과 add를 이용한다.
h_conv3_flat: (?, 9216) #12X12X64
h_fc1: (?, 384) #[?, 9216]X[9216, 384] = [?, 384]
# Fully connected layer
W_fc2 = weight_variables([384, 192])
b_fc2 = bias_variable([192])
h_fc2 = tf.matmul(h_fc1, W_fc2) + b_fc2
h_fc2: (?, 192) #[?, 384]X[384, 192] = [?, 192]
# linear
W_fc3 = weight_variables([192, 7])
b_fc3 = bias_variable([7])
y_conv = tf.add(tf.matmul(h_fc2, W_fc3), b_fc3)
return y_conv
y_conv: (?, 7) #[?, 192]X[192, 7] = [?, 7]
결론
인터넷에서 이미지를 다운 받아 실행한 결과를 print해줍니다. 얼굴 위치를 알고 있으니, 각 감정에 맞는 이모지를 얼굴 위에 올리는 프로젝트를 진행할 수도 있겠네요! (두근두근)
딥러닝에 대해 전혀 모르던 상태로 시작한 인턴인데, 그래도 이제는 어느정도 대화할 수준(?)은 되는 것 같아 뿌듯합니다. 저는 이제 CNN에서 벗어나 최근 논문 모델을 구현해보려고 합니다. ^-^
부족하지만 봐주신 분들 감사합니다 :)
'인턴일지' 카테고리의 다른 글
Semantic Segmentation 강의 정리 (0) | 2020.05.08 |
---|---|
Revisiting Single Image Depth Estimation: 실시간 Depth map (0) | 2020.04.27 |
[7,8주차] 주간프레젠테이션 (0) | 2020.04.24 |
Git 문법 정리 (0) | 2020.04.24 |
인턴 일지 작성 (2) | 2020.04.21 |