Home 캡스톤일지 | ~12.13 뒤늦게 쓰는 모델학습 tfjs 마무리
Post
Cancel

캡스톤일지 | ~12.13 뒤늦게 쓰는 모델학습 tfjs 마무리

캡스톤이 드디어 끝났다!

저번 주 금요일 최종발표.. 인줄 알았지만 다른팀 발표는 커녕 소회의실에서 5분간의 질의응답이 끝나고.. 유튜브에는 우리(현지의) 발표 영상이 올라갔다. 🎥 https://youtu.be/zWzvFuL0sKU (영상은 유튜브 가서 봐주세욤)

누군가 이 글을 본다면 꼭 좋아요를 눌러주세욤 🙇🏻‍♀️🙇🏻‍♀️🙇🏻‍♀️ 나 인기상 받구싶단 말이야

아직 프로그램 등록과 뭐 이것저것 최종보고서가 많이 남아서 끝난거 같진 않지만 어쨌든 개발은 완료! 저번주에 회의 끝나고 tfjs 포스팅 쓰면서 마무리 포스팅 할려했는데 시험기간이랑 겹쳐서 호다닥 지나가버렸다 아니 근데.. 좋아요 수 다른 팀 왜케 많은건데.. 나 상받고 싶었단말야 😥🥺


이전 포스팅에서 직접 모델 학습을 위한 데이터셋 코드를 생성하고 RNN 모델을 이용하여 모델 학습을 해보았다.

혼자 테스트하고 코드를 보며 수정 & 보완한 점과 드디어 노드 서비스와의 모델 연결!을 포스팅할 예정

블로그 코드에서의 데이터셋 생성

참고한 블로그에서의 각도 계산 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for res in result.multi_hand_landmarks:
    joint = np.zeros((21, 4))
    for j, lm in enumerate(res.landmark):
        joint[j] = [lm.x, lm.y, lm.z, lm.visibility]

    # Compute angles between joints
    v1 = joint[[0,1,2,3,0,5,6,7,0,9,10,11,0,13,14,15,0,17,18,19], :3] # Parent joint
    v2 = joint[[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], :3] # Child joint
    v = v2 - v1 # [20, 3]
    # Normalize v
    v = v / np.linalg.norm(v, axis=1)[:, np.newaxis] 

    # Get angle using arcos of dot product
    angle = np.arccos(np.einsum('nt, nt->n',
    v[[0,1,2,4,5,6,8,9,10,12,13,14,16,17,18], :], 
    v[[1,2,3,5,6,7,9,10,11,13,14,15,17,18,19], :])) # [15,]

    angle = np.degrees(angle) # Convert radian to degree

    angle_label = np.array([angle], dtype=np.float32)
    angle_label = np.append(angle_label, idx) 
                    
    d = np.concatenate([joint.flatten(), angle_label])

    data.append(d)

이전까지는 이해가 잘 안갔었는데 이제는 조금 이해가 갈 것 같다.
아마도 두 벡터 사이의 각도를 계산하는 것 같다..! 그 근거로는.. (코드 이해를 근거를 찾아 한다는게 좀 웃기긴함)

  • 첫 번째로 각도를 계산하는데 두 개의 랜드마크 정보를 이용한다는 점과
  • 두 번째는 x, y, z 정보 모두 이용한다는 점을 미뤄보아 그렇게 결론지었다.

민박사님께 모델 성능과 관련하여 면담하러 갔었을때 박사님도 잘 이해가 안가신다고..ㅎ 그리고 제시되어있는 제스처 그대로 했을 때도 유튭영상처럼 성능이 좋지 않았음 흥

이전 포스팅에서의 데이터셋 생성

검지손가락에 있는 미디어파이프 랜드마크 중 3쌍의 x, y좌표를 이용하여 중앙에 있는 랜드마크의 각도 계산

1
2
3
4
5
6
7
8
9
10
11
12
 for j, lm in enumerate(res.landmark):
    point_info[j] = [lm.x, lm.y]
      
    angles = []
    for point in index_point:
        # 검지손가락 포인트 각도 게산
        ba = point_info[point[0]] - point_info[point[1]]
        bc = point_info[point[2]] - point_info[point[1]]
        cosine_angle = np.dot(ba,bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
        angle = np.arccos(cosine_angle) 
        pAngle = np.degrees(angle)
        angles.append(pAngle)

image

이렇게 가운데 랜드마크의 각도 계산을 위해 양쪽의 랜드마크 정보 모두 이용하여 세점 사이 각도 계산하는 공식으로 각도를 계산한다.

그림에서는 살짝 측면으로 틀어서 미디어파이프를 인식했기에 각도 계산이 잘 되지만 정면을 본 상태에서 x, y의 정보만 이용하면 안될것 같다는 생각이 들었다..! 그래서 z좌표까지 이용해 각도를 계산하는 버전으로 바꿔보려 했는데 시간이 없어서 패쓰.. 벡터 사이 각도 계산이 되는데 블로그 버전과 크게 차이가 없을 것 같았다. (변명)

++ 그냥 손을 측면으로 들어 클릭 제스처를 취하는 걸로 제약사항을 뒀다.

내가 작성한 코드 2

현지가 클릭 제스처를 두 랜드마크 사이 거리가 특정 임계치 이하로 가까워졌을 때로 정의한 것에 힌트를 얻어 나도 포인트 사이 거리를 계산해 모델학습을 시켜보기로 했다.

검지손가락 가장 끝점인 8번과 손바닥에 가장 가까운 점 5번 사이 거리, 8번부터 손목의 0번까지의 거리, 그리고 7번과 0번사이 거리를 계산해 데이터셋을 생성했다.

데이터셋 생성 부분 코드

1
2
3
4
5
6
7
8
9
10
for j, lm in enumerate(res.landmark):
    point_info[j] = [lm.x, lm.y, lm.z]
      
    lengths = []
    for point in index_point:
    # 두 점 사이 거리 계산
    a = (point_info[point[0]][0], point_info[point[0]][1], point_info[point[0]][2]) # point[0]
    b = (point_info[point[1]][0], point_info[point[1]][1], point_info[point[1]][2]) # point[1]
    length = distance.euclidean(a, b)
    lengths.append(length)

x, y, z 좌표를 모두 이용하여 두 랜드마크 사이 거리 계산

학습은 동일하게 RNN 모델을 이용하였고 image 정확도는 폭망함 ㅎㅎ

image 수치가 높긴 한데.. 그래프가 상태가 영 좋지는 않다…. 그리고 성능도 딱히.. 그래서 이 방법은 바로 빠이

새로 배운 내용 추가

왜 내 데이터셋에는 LSTM이 적용이 안되는지 궁금했는데 드디어 이유를 알았다!!!!!

그 이유는 바로바로 LSTM은 3차원 데이터셋을 입력해야한다..!! 내 데이터셋은 2차원이여서 자꾸 input_dim이 맞지 않다고 나온 것이였던 것이다. 이걸 아니 왜 블로그 코드에서 numpy array를 합쳤다 펼쳤다 했는지 조금은 이해가 간다.

또 하나 LSTM 이 뭔지 조금 감이 왔다.
RNN의 한 종류로 기존 RNN이 출력과 먼 위치에 있는 정보를 기억할 수 없다는 단점을 보완한 장/단기 기억 신경망 구조 라고 한다.

결국 레이어를 쌓아 신경망을 구성하는 그 개념은 RNN과 동일하다는 거

이렇게 점점 하나씩 배워간다

발표 질의응답 준비

Q1. 랜드마크 정보 전처리 기준과 데이터셋 어떻게 구성하였나요?
A1. 클릭과 클릭이 아닐때의 각도 분포를 비교해보았습니다. 그 결과 확실히 클릭 모션에 사용되는 검지손가락은 그 각도 차이가 분명했지만 그렇지 않은 손가락은 다소 분포에 큰 차이가 없었습니다. 하지만 약간의 각도 차이는 존재하고, 더 좋은 모델 성능을 위하여 손가락 위에 존재하는 15개의 랜드마크 모두의 각도 정보를 모델 학습을 위한 데이터셋으로 구성하였습니다.

  • 검지손가락 위 7번 랜드마크 image

  • 검지손가락 위 6번 랜드마크image

푸른색이 클릭했을 때 붉은 색이 클릭을 하지 않았을 때로 검지손가락의 경우 확실한 각도 차이를 보이는 것을 볼 수 있습니다.

Q2. 모델 성능을 높히기 위해 어떤 방법들을 도입했나요?
A2. 다양한 연령대의 남, 녀 데이터셋을 수집하여 대략 1만~2만 row의 데이터셋을 사용하였으며 정적 제스처에서 동적 제스처로 넘어가는 트리거를 고안하고, 검지손가락 구부리기와 손목 구부리기, 두 손가락 이용 등 다양한 클리 제스처로 테스트를 해보았습니다.

Q3. 시간 데이터는 어떻게 처리하였나요?
A3. 원래는 1초당 데이터셋을 추가하여 시간 정보를 담은 3차원의 데이터셋을 이용하여 LSTM 모델을 학습을 시켰습니다(블로그 코드). 하지만 체감 정확도가 좋지 않고 자바스크립트로의 변환이 어려워 그 방법을 변경하였습니다. 클릭 제스처를 취한 채 정보를 수집하고 시간 정보를 제외한 2차원 데이터셋을 생성하였습니다.

Q4. 그렇다면 이는 동적 제스처라고 할 수 없지 않나요?
A4. 맞습니다… (그래서 발표때는 동적/정적 제스처 구분을 없애고 그냥 ok 제스처와 click 제스처 라고만 이름을 지었다.)
우리의 목표는 비접촉 기술을 이용해 키오스크를 이용하는 것까지가 서비스이기 때문에 제스처 인식 모델 학습보다는 이를 이용한 서비스 구현에 조금 더 힘을 실었습니다. 결국 전체적인 서비스 완성도를 위하여 제스처 범위는 축소하고 제스처를 이용한 서비스 이용 개발에 더 초점을 맞췄습니다..!

Q5. 클릭 모션에만 모델 학습을 사용한 이유는 무엇인가요?
A5. 엄지를 올리는 ok 제스처의 경우 미디어파이프 랜드마크의 정보만으로 직관적으로 판단이 가능하여 그 기준을 정하기 쉽습니다.

4번 랜드마크와 0번 랜드마크의 y좌표 비교 image 하지만 손가락을 구부리는 클릭 모션의 경우 사람마다 구부리는 정도나 방법이 다양하고 다른 손가락의 모양이 다양하여 기준을 정하기 쉽지 않습니다. 따라서 따라서 다양한 연령대와 성별의 각도 정보를 수집하여 모델링을 하기로 결정하였습니ㄷㅏ..

이렇게 모델학습 내용을 정리하였다. (살짝 과장이 포함된 것은 비밀)

파이썬 코드 자바스크립트로 변환

모델을 통과시키려면 데이터셋 생성과 동일하게 랜드마크 정보를 전처리해야한다. 파이썬으로 작성한 전처리 과정을 자바스크립트로 변경해주었다.

📑 order.model.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (results.multiHandLandmarks) {
    for (const landmarks of results.multiHandLandmarks) {
    
        // 랜드마크 정보를 담을 numjs array
        let point_info = nj.zeros((21, 2));
        for(var i=0; i<21; i++) {
          point_info[i] = [landmarks[i]['x'], landmarks[i]['y']];
        }
        
        var angles = [];
        for(const point of index_point) {
          var ba = [point_info[point[0]][0] - point_info[point[1]][0], point_info[point[0]][1] - point_info[point[1]][1]];
          var bc = [point_info[point[2]][0] - point_info[point[1]][0], point_info[point[1]][1] - point_info[point[1]][1]];
          var cosine_angle = math.multiply(ba, bc) / (math.norm(ba) *  math.norm(bc));
          var angle = math.acos(cosine_angle);
          var pAngle = radians_to_degrees(angle);
          angles.push(pAngle);
        }
        
    }
}

numpy 메서드와 javascript용 numjs 메서드가 동일하지 않다는 문제가 있어 이전에 파이썬 코드에서 사용한 numpy 메서드를 그대로 구현할 수 없었다.

  • [문제1] numpy 행렬과 norm 계산 메서드
    [해결1] mathjs 모듈 이용

    1
    2
    3
    4
    
      <!--numjs-->
      <script src="https://cdn.jsdelivr.net/gh/nicolaspanel/numjs@0.15.1/dist/numjs.min.js"></script>
      <!--mathjs-->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/7.5.1/math.min.js"></script>
    

    모듈 import는 모두 cdn 태그 이용함

  • [문제2] radian to degree 메서드 (np.degree)
    [해결2] 직접 메서드 작성함

    1
    2
    3
    4
    
      function radians_to_degrees(radians) {
          var pi = Math.PI;
          return radians * (180/pi);
      }
    

    이렇게 성공적으로 변환을 마쳤다!! 힘들었다 엉엉

tfjs 모델 연결

대망의 모델 연결 부분, 하루 꼬박 새서 성공했다.

이전에 생활코딩에서 살짝 맛본 tfjs
👉🏻 [캡스톤일지] 10.04 생활코딩으로 간단히 살펴본 TensorflowJS https://ijo0r98.github.io/posts/capstone2/

아쉽게도 생활코딩 강의 들을때는 직접 스크립트로 학습시키고 모델생성해서 바로 로컬 스토리지에 저장했었기에 내가 필요한 방법과는 달랐다.

  • [문제1] group1-shard1of1.bin의 이름 변경
    [해결1] 어디 구글링하다 나온 내용 중에 group1-shard1of1.bin의 파일명을 models.weights.bin(?)으로 변경해주어야한다길래 변경했는데 이는 적용되지 않음

  • [문제2] model.json과 group1-shard1of1.bin의 위치
    [해결2] 처음에는 그냥 모델만 저장할 폴더를 하나 만들고 불러왔는데 불러는 와지지만 모델로써 불러와지는게 아니라 그냥 json 파일로 읽어졌다. 그래서 혹시나 하고 js파일을 모아두기 위해 static로 지정한 폴더 안에 두고 불러오니 되었다!!

    ++ 기존의 js파일들과 구별하기 위해 새로 static 경로를 지정해서 해봤는데 이상하게 새로 지정한 static 경로가 안먹혀서 빠르게 포기했다ㅠ

먼저 사용한 tfjs cdn

1
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script>

모델 load와 predict

1
2
3
4
5
6
7
8
9
10
11
tf.loadLayersModel('/js/models/ver2/model.json').then(function(model) {
    // model.predict(tf.tensor([angles])).print(); // 결과가 바로 콘솔창에 출력됨
    var pred = model.predict(tf.tensor([angles]));
    var result = pred.arraySync()[0][0];      

    if (result == 0) {
        console.log('click!');
        socket.emit('click', true);
        sleep(300);
    }
})

잠시 서비스를 멈추는 sleep

1
2
3
4
function sleep(ms) {
  const wakeUpTime = Date.now() + ms;
  while (Date.now() < wakeUpTime) {}
}

위에서 생성한 angles 정보를 모델에 입력하여 예측값을 받는데 여기서 tensor로 출력되는 예측값을 arraySync() 메서드를 이용하여 배열로 변환해준다.

https://js.tensorflow.org/api/latest/#tf.Tensor.arraySync

tfjs를 이용할 때 특히 메서드가 어떤게 있고 어떻게 사용하는지 헷갈릴 때 참고하기 좋은 사이트다.

암튼 모델을 통과하여 결과가 클릭 모션일 때 소켓 통신 > robotjs를 통해 클릭 이벤트가 발생한다.

해결해야하는 문제

1. 모델 로드 문제
아무래도 랜드마크가 인식될 때 마다 각도정보를 수집하고 바로 모델을 불러 통과시키기때문에 랜드마크가 인식될 때 마다 모델을 로드하고 있다. 그러다보니 서비스를 이용하는 내내 실시간으로 모델이 계속 로드되고 있는 모양… 로컬에서만 사용하고 사용자가 나뿐이다보니 딱히 서비스를 이용하는데 부화가 걸린다거나 문제는 딱히 없지만 좋은 설계는 아닌것 같다. 모델을 서비스 시작과 동시에 불러와서 필요할 때만 사용하고싶은데 텐서플로우 모델 로드가 비동기로 이뤄지다보니 모델을 변수로 저장하는데 문제가 자꾸 발생한다. 비동기 어느정도 이해했다고 생각했는데 난 아직 갈길이 먼듯..^^ 스크립트를 사용하며 비동기 문제는 늘 고려해야함으로 이는 나중에 꼭 고쳐보도록 해야겠다.

2. 모델 통과와 클릭 이벤트 사이 시간 격차
그 외에도 클릭을 안한상태에서 손가락을 구부렸다 펴는 동작까지가 클릭이 아닌 손가락을 구부린 정적 제스처 자체를 인식하는 모델이다 보니 조금이라도 손가락을 늦게 펼치면 계속 클릭이라고 인식하는 문제가 또 있다. 지금은 sleep을 두어 클릭이 인식될때 마다 시간 격차를 두고 있는데 클릭이 인식되고 잠시 멈췄다가 3초 이후부터 인식되는게 아니라 클릭 이벤트는 계속 연속적으로 발생하고 그 사이 3초씩 쉬고있어 서비스 이용에 살짝 불편함이 있다.., 모델을 통과해 클릭으로 인식하고 > 클릭 이벤트가 발생하면 3초 쉬었다가 그 다음 랜드마크부터 모델에 통과시키는 구조로 변경해야할 것 같다.



뒤늦게 밀려썼지만 이렇게 개발이 완료되었다..

아직 부족한 부분도 좀 있고 예외처리나 자잘한 부분들이 모두 완료된 것은 아니지만 시연을 위한 준비는 모두 다 되었다! 잘 되다가 갑자기 시연영상 촬영할 때 잘 안되서 꽤 고생을 했지만 그래도 나름 만족스러운 최종발표가 되었다. 최종발표 질의응답 시간에 긴장 옴총했는데 생각보다 기술적인 질문보다는 서비스적인 질문들이 많이 들어와서 매끄럽게 통과하였다.

아직 결과가 나오진 않았는데 수상은 어려울 거 같기도.. 힝 학점이라도 기대해야지

암튼 담에는 진짜 진짜 마지막 캡스톤 일기써야지 뿅

This post is licensed under CC BY 4.0 by the author.

캡스톤일지 | ~ 11.28(2) 내가 작성한 모델학습 코드

캡스톤일지 | 캡스톤 즐거웠다..는 무슨 학교 망해라