KivyMd를 이용한 Toy project DeepMom -3-

치삼이·2021년 10월 9일
0
post-thumbnail

👨‍💻 Toy Project DeepMom -3-

토이 프로젝트 DeepMom 소개 및 후기 세 번째이자 마지막 포스팅이다. 지난번 🔗 KivyMd를 이용한 Toy project DeepMom -2- 에서 말했듯, DeepMom은 크게 ConnectScreen, DashboardScreen 두 가지 Screen으로 나뉜다.

마지막 포스팅은 DashboardScreen과 DashboardScreen의 구성요소에 마지막으로 토이 프토젝트를 진행하며 느낀점을 서술하려고 한다.

1. Kivy.Garden

자고로 DashBorad 및 Monitoring tool 이라 하면 그래프 및 여러 로그 및 수치를 시각적으로 보여 주며 화려한 것들이 머릿속으로 떠오른다. 마치 아래의 🔗 Nuxt Black Dashboard 같이 말이다.

Kivy에서는 직접적으로 Graph나 혹은 Plot을 그리는 모듈은 존재하지 않는다. 대신 Garden 이라는 모듈을 이용해 다른 사용자들이 만들어놓은 모듈을 이용해 Grpah를 그릴 수 있다.

Garden is a project to centralize addons for Kivy maintained by users. You can find more information at Kivy Garden. All the garden packages are centralized on the kivy-garden Github repository.

쉽게 말해 Garden 은 Kivy 사용자들이 만들어놓은 여러 Kivy의 써드파티 모듈 혹은 컴포넌트의 집합소이다. Garden 에서 다운로드 받을 수 있는 모듈은 Garden 공식 🔗 Github 에서 확인 할 수 있다. 직접 접속해 확인해 보면 알겠지만... 활발하게 업로드가 이루어 지는 곳은 아니다.😭

1.1 Garden.Grpah

Kivy엔 Grpah나 Plot이라는 모듈이 없다. 고로 만들어야 한다. 하지만 만드는건 내가아니다. 그렇다 이미 Kivy.Garden에 🔗Graph 라는 모듈이 있다. 이 Graph는 Kivy 공식 문서에 Garden예제를 위해 설명할 정도로 대표적인 kivy의 써드파티 모듈이다.

이 포스팅에서는 Graph의 모듈의 사용법을 다루지는 않는다. 정말 간단한 라인 그래프가 필요하다면 추천 하겠지만 여러 다양한 셋팅 파이 차트, 바 차트, 히스토그램나 한 그래프에 Y축을 두 개 이용한다던가 하는 여러 기능은 없다(개발자분이 파이차트, 스캐터 플롯, 바 차트 같은 여러 기능을 만들려고한 흔적은 있더라..).

이 Graph모듈 말고 Kivy내부에 직접 Python의 대표 그래프 라이브러리인 Matplotlib을 이용하기 위한 모듈이 있으니 참고해서 사용하길 바란다.

2. Kivy.Garden.Grpah in DeepMom

위에서 말한 여러 약점에도 불구하고 내가 Graph 모듈을 사용하는 이유는 가볍고 Matplotlib 보다 단순한 구성으로 이루어져 부가 기능을 추가하기 수월해서 이다.

2.1 DashboardScreen

DashboardScreen에는 여러 정보들이 표시된다. 사실 가장 중요한 부분은 그래프다. 딥러닝 네트워크를 학습시킬때 매 epoch마다 정확도와 손실값을 그래프로 보여주면서 학습진행을 시각화하기 때문이다. 나머지는 최대나 최소값 특정 epoch일때의 값 혹은 직전에 비해 올라갔는지 내려갔는지 등을 보여주고 부가적으로 남은 예상시간 학습이 어느정도 진행 됬는지를 보여준다.

2.2 Dashboard Hovering

DashboardScreen을 만들때 스스로 구현해야하는 기능이 있는데 바로 Hovering 기능이다. Hovering 기능의 정확한 정의는 아니지만, 여기서 내가 말하는 Hovering기능은 마우스 커서를 그래프 위에 올렸을때 현재 커서 값을 기준으로 가장 가까운 그래프 값을 표시하는 것을 말한다.

Garden.Graph 처럼 라인 차트 외에 거의 기능이 없는 모듈에 이런 옵션이 탑재 되어 있을리 없어서 직접 만들어야 한다. 내가 만든 Hovering 기능은 제약 사항이 있는데 일단 X축이 0과 자연수여야 한다. 실제 epoch값은 0 이상의 자연수 이기 때문에 이런 제약사항을 만족했다(실제 만족한것은 아니고 0이상 자연수라는 특징을 이용한 것이지만..🤣🤣).

2.3 Hovering 구현하기

이 Hovering 기능을 어떻게 구현했는지 설명해보려고 한다. DashboardScreen을 가장 Dashborad 답게 보여주며 이것 외에는 TK나 QT 혹은 애플리케이션을 만들어본 개발자라면 나머지는 어렵게 구현할 수 있을 것이다.

일단 Graph를 그리는 Graph layout과 Hovering 선, 현재 값을 표시하는 박스와 원을 표시하는 Hovering layout을 똑같은 크기와 똑같은 좌표로 넣어 줌으로써 겹치게 배치한다. 이때 Hovering layout은 Grpah layout 보다 앞에 배치한다.

#:import utils kivy.utils
#:import os os
#:import window kivy.core.window.Window

<HoverGraph>
    FloatLayout:
        pos: root.pos
        size_hint: None, None
        size: root.width, root.height
        canvas.before:
            Color:
                rgba: utils.get_color_from_hex('#26283A')
            RoundedRectangle:
                pos: self.pos
                size: self.size
                radius: [5, 5, 5, 5]
        BoxLayout:
            id: graph_plot
            text: 'graph_plot'
            pos_hint: {'center_x':.5, 'center_y':.5}
            size_hint: 1, 1
...

        BoxLayout:
            id: hover_plot
            pos_hint: {'center_x':.5, 'center_y':.5}
            size_hint: 1, 1

DashboardScreen.kv 에서 확인하면 grpah_plot 과 hover_plot이라는 같은 계층, 같은 사이즈, 같은 좌표의 두 BoxLayout이 있다. 이렇게 셋팅을하고 몇 개의 계산을 통해 선과 원을 그릴 수 있다.

위 그림에서 보이듯 먼저 필요한 좌표는 DeepMom에서 실제 저 그래프 모듈이 위치한 레이아웃의 위치(노란색 원)의 시작 좌표, 크기, 또 실제의 그래프가 그려지는 눈금의 시작 위치 좌표(빨간색 원) 및 길이(혹은 끝의 좌표)가 필요하다.

좌표와 현재 마우스 포인터의 위치를 기반으로 선과 원 그리고 값을 표시하는 박스를 표기하는 draw_line 메소드다. 차근차근 뜯어보자.


    def draw_line(self, mouse_pos):
        graph_x_start = self.x + self.ids.graph.view_pos[0]
        graph_x_end = graph_x_start + self.ids.graph.view_size[0]
        graph_y_start = self.y + self.ids.graph.view_pos[1]
        graph_y_end = graph_y_start + self.ids.graph.view_size[1]
        if (graph_x_start <= mouse_pos[0] <= graph_x_end) and (graph_y_start <= mouse_pos[1] <= graph_y_end):
            self.ids.hover_plot.canvas.clear()
            Window.set_system_cursor('crosshair')
            step = self.ids.graph.view_size[0] / self.epoch
            with self.ids.hover_plot.canvas:
                idx = round((mouse_pos[0] - graph_x_start) / step)
                if idx == 0:
                    idx = 1
                if idx <= self.current_epoch:
                    if self.touch_flag:
                        for value in self.plot_dict.values():
                            y_pos = ((value['plot'].points[idx - 1][1] * self.ids.graph.view_size[1]) / self.ids.graph.ymax) + graph_y_start
                            x_pos = (step * idx) + graph_x_start
                            if self.ids.graph.ymin <= value['plot'].points[idx - 1][1] <= self.ids.graph.ymax:
                                Color(.85, .85, .85, .8)
                                Line(circle=(x_pos, y_pos, 2.5), width=1.25)
                            value['tooltip'].text = value['index_prefix'] + str(round(value['plot'].points[idx - 1][1], 6))
                        if idx < self.epoch * .85:
                            self.ids.tooltips.pos = (mouse_pos[0] + 15, mouse_pos[1] - 55)
                        else:
                            self.ids.tooltips.pos = (mouse_pos[0] - self.ids.tooltips.width - 15, mouse_pos[1] - 55)
                        self.ids.idx_legend_label.text = str(idx)
                        self.ids.tooltips.opacity = 1
                        self.ids.idx_legend.opacity = 1
                        self.ids.legend.opacity = 0
                else:
                    self.ids.tooltips.opacity = 0
                    self.ids.idx_legend.opacity = 0
                    self.ids.legend.opacity = 1
                Color(0, 0, 0, .5)
                Line(points=[mouse_pos[0], graph_y_start, mouse_pos[0], graph_y_end], width=1)
        else:
            self.ids.hover_plot.canvas.clear()
            self.ids.tooltips.opacity = 0
            Window.set_system_cursor('arrow')
            self.enter_flag = True

우선은 저 노란색의 절대 좌표는 self.xself.y 를 통해 값을 얻어올 수 있다. 그 다음 Graph 모듈 내부에 있는 눈금의 시작의 상대 좌표인 view_pos[0]view_pos[1]
각각 self.xself.y 에 더하면 그래프의 눈금의 절대 좌표를 얻을 수 있다.

graph_x_start = self.x + self.ids.graph.view_pos[0]
graph_x_end = graph_x_start + self.ids.graph.view_size[0]
graph_y_start = self.y + self.ids.graph.view_pos[1]
graph_y_end = graph_y_start + self.ids.graph.view_size[1]

그림으로 표현하자면 아래와 같다. 네 좌표안에 마우스 포인터가 존재하면 포인터 모양을 바꾼다.

Line(points=[mouse_pos[0], graph_y_start, mouse_pos[0], graph_y_end], width=1)

Hovering 기능에서 마우스의 위치에 선을 그리는 기능은 간단하다 현재 마우스 좌표에서 x 좌표를 가져온뒤 그래프의 graph_y_start 에서 graph_y_end 까지 선을 그으면 일직선의 선을 얻을 수 있다.

다음은 Step이라는 변수를 보자. 이 Step은 그래프가 그려지는 길이 안에서 한 epoch의 길이다. 그래프의 길이가 100이고 epoch가 10번이면 한 epoch당 10이라는 길이를 가진다. 이 step을 이용해 현재 마우스 좌표에서 가장 가까운 epoch의 인덱스가 몇번인지를 계산할 수 있다.

step = self.ids.graph.view_size[0] / self.epoch
with self.ids.hover_plot.canvas:
    idx = round((mouse_pos[0] - graph_x_start) / step)

예시로 위 그림처럼 길이가 20이고 epoch가 10이라면 step은 2며 현재 가장 가까운 인덱스는 15.2 / 2 이므로 7이다.

아래 수식은 원을 그릴 좌표를 찾는 수식이다. y_pos 는 현재 정확도 혹은 손실값이 y좌표를 구하는 공식이다. 예를들면 y_start가 3, 그래프 세로 길이가 10, 정확도의 max값이 1, 현재 정확도가 0.95면 y의 절대 좌표는 12.5다.

비슷한 계산으로 현재 인덱스에 인덱스가 가지는 길이의 비율인 step을 곱하고 x그래프의 시작 지저을 더하면 x_pos를 구할 수 있다.

y_pos = ((value['plot'].points[idx - 1][1] * self.ids.graph.view_size[1]) / self.ids.graph.ymax) + graph_y_start
x_pos = (step * idx) + graph_x_start

자잘한 여러 공식이 많이 나왔지만 어렵지 않게 Hovering 기능을 완성할 수 있다. 그 외에는 선이 나 원이 계속 생기지 않게 주기적으로 지워주거나 혹은 마우스 클릭으로 호버링을 키고 끄는 것 등을 추가 했다.

3. Monologue

대학생때 이후로 오랜만에 토이프로젝트를 진행하였다. 그동안 F/W를 하면서 저런 GUI 애플리케이션은 오랜만에 만들면서 아키텍쳐의 구성이나 코드를 어떻게 짜는지 참... 과 답답하기도 하고 공부를 더 해야하겠구나... 라는 생각이 먼저 들었다. 반면에 간만에 재밌게 코딩을 한 것 같아 또 기분은 좋네 🤣 참고로 맥에서는 안돌아가더라...

시작은 정말 단순히 학습률을 볼려고 만든 토이프로젝트인데 이것저것 기능을 넣고싶어 욕심을 부려 생각했던것 보다 쫌 더 걸렸다. Kivy는 옛날에도 한번 썻는데 프레임워크 자체가 큰 인기가 없다보니... (다음에는 Flutter를 이용해서 데스크톱 애플리케이션을 짜봐야지..)

🔗 DeepMom Github

0개의 댓글