RLTrader의 가시화 모듈 개발

Posted on Thu 02 April 2020 in book • 9 min read

가시화 모듈에 포함된 가시화기 클래스의 속성과 함수를 살펴보고 파이썬으로 구현한 소스 코드를 상세히 확인합니다.

가시화 모듈의 주요 속성과 함수

가시화 모듈(visualizer.py)은 신경망을 학습하는 과정에서 에이전트의 보유 주식 수, 가치 신경망 출력, 정책 신경망 출력, 투자 행동, 포트폴리오 가치 등을 시간에 따라 연속으로 보여주기 위한 시각화 기능을 담당하는 가시화기 클래스(Visualizer)를 가집니다. 가시화기 클래스의 주요 속성 및 함수는 다음과 같습니다.

속성

  • fig: 캔버스 같은 역할을 하는 Matplotlib의 Figure 클래스 객체
  • axes: 차트를 그리기 위한 Matplotlib의 Axes 클래스 객체
  • title: 가시화될 그림의 제목

함수

  • prepare(): Figure를 초기화하고 일봉 차트를 출력
  • plot(): 일봉 차트를 제외한 나머지 차트를 출력
  • save(): Figure를 그림 파일로 저장
  • clear(): 일봉 차트를 제외한 나머지 차트를 초기화

가시화 모듈이 만들어 내는 정보

가시화 모듈이 만들어내는 결과는 다음 정보를 포함합니다.

  • Figure 제목: 파라미터, 에포크 및 탐험률
  • Axes 1: 종목의 일봉 차트
  • Axes 2: 보유 주식 수 및 에이전트 행동 차트
  • Axes 3: 가치 신경망 출력
  • Axes 4: 정책 신경망 출력 및 탐험 차트
  • Axes 5: 포트폴리오 가치 및 학습 지점 차트

학습 설정 정보, 에포크, 탐험률은 Figure의 제목으로 표시합니다. 그 외 차트는 각각 Axes로 구성돼 있습니다. Axes 하나가 하나의 차트라고 보면 되며 최종 Figure의 구조는 다음과 같습니다.

Visualizer가 그리는 최종 Figure 구조

Visualizer가 그리는 최종 Figure 구조

Figure는 Axes 다섯 개를 포함합니다. 5행 1열 구조로 Axes들이 세로로 나열돼 있습니다. 위에서부터 종목의 일봉 차트, 보유 주식 수 및 에이전트 행동 차트, 가치 신경망 출력, 정책 신경망 출력 및 탐험 차트, 포트폴리오 가치 및 학습 지점 차트 순으로 가시화합니다.

코드 조각 1: 가시화기 클래스의 생성자

일봉 차트를 그리기 위해서 mpl_finance 모듈을 사용합니다. 이 모듈은 원래 Matplotlib 라이브러리에 포함돼 있었지만, Matplotlib 2.0 버전부터 향후 삭제 예정(deprecated) 상태가 됐습니다. 향후 삭제 예정 상태는 해당 기능의 중요도가 떨어져 언젠가는 해당 기능을 없앨 것이므로 더 이상 사용하지 말기를 권장한다는 의미입니다. 개인적으로는 그 이유를 도메인에 특화된 코드를 별도로 관리해 Matplotlib의 코드를 깔끔하게 유지하기 위해서라고 생각합니다.

파이썬 팁: mpl_finance 모듈은 다음 Github 리파지토리에서 내려받을 수 있습니다.

https://github.com/matplotlib/mpl_finance

내려받은 후 'Anaconda Prompt'에서 mpl_finance 폴더로 이동하고 다음과 같이 설치할 수 있습니다.

python setup.py install

Visualizer 클래스는 NumPy, Matplotlib, mpl_finance 모듈을 사용합니다. 예제 5.25는 이 모듈들을 임포트하는 부분과 Visualizer 클래스의 선언 부분을 보여줍니다.

Visualizer 클래스: 생성자

import threading
import numpy as np
import matplotlib.pyplot as plt
plt.switch_backend('agg')

from mplfinance.original_flavor import candlestick_ohlc
from agent import Agent

lock = threading.Lock()

class Visualizer:
    COLORS = ['r', 'b', 'g']

    def __init__(self, vnet=False):
        self.canvas = None
        # 캔버스 같은 역할을 하는 Matplotlib의 Figure 클래스 객체
        self.fig = None
        # 차트를 그리기 위한 Matplotlib의 Axes 클래스 객체
        self.axes = None
        self.title = ''  # 그림 제목

Visualizer 클래스는 fig, axes, title 등을 속성으로 가집니다. fig는 Figure 클래스의 객체로 전체 가시화 결과를 관리하고 axes는 Axes 클래스의 객체로 fig에 포함되는 차트의 배열입니다.

코드 조각 2: 가시화 준비 함수

다음 코드조각은 전체 차트를 그릴 준비를 하고 에포크에 상관없이 공통된 첫 번째 차트인 종목 일봉 차트를 그립니다.

Visualizer 클래스: 가시화 준비 함수

    def prepare(self, chart_data, title):
        self.title = title
        with lock:
            # 캔버스를 초기화하고 5개의 차트를 그릴 준비
            self.fig, self.axes = plt.subplots(
                nrows=5, ncols=1, facecolor='w', sharex=True)
            for ax in self.axes:
                # 보기 어려운 과학적 표기 비활성화
                ax.get_xaxis().get_major_formatter() \
                    .set_scientific(False)
                ax.get_yaxis().get_major_formatter() \
                    .set_scientific(False)
                # y axis 위치 오른쪽으로 변경
                ax.yaxis.tick_right()
            # 차트 1. 일봉 차트
            self.axes[0].set_ylabel('Env.')  # y 축 레이블 표시
            x = np.arange(len(chart_data))
            # open, high, low, close 순서로 된 2차원 배열
            ohlc = np.hstack((
                x.reshape(-1, 1), np.array(chart_data)[:, 1:-1]))
            # 양봉은 빨간색으로 음봉은 파란색으로 표시
            candlestick_ohlc(
                self.axes[0], ohlc, colorup='r', colordown='b')
            # 거래량 가시화
            ax = self.axes[0].twinx()
            volume = np.array(chart_data)[:, -1].tolist()
            ax.bar(x, volume, color='b', alpha=0.3)

plt.subplots() 함수를 호출해 5행 1열의 구조를 가지는 Figure를 생성합니다. 각 행에 해당하는 차트는 Axes 객체의 배열로 반환됩니다.

파이썬 팁: Matplotlib의 subplots() 함수는 여러 Axes로 구성된 Figure를 생성할 때 효과적입니다. 인자로 들어가는 nrows는 행 개수, ncols는 열 개수를 의미합니다. nrows가 2이고 ncols가 3이라면 2x3 Axes, 즉 6개의 Axes로 구성된 Figure가 생성됩니다.

subplots() 함수는 2개의 변수를 Tuple로 반환합니다. 첫 번째는 Figure 객체이고 두 번째는 이 Figure 클래스 객체에 포함된 Axes 객체의 배열입니다.

파이썬 팁: Matplotlib에서는 주요 색깔 이름을 다음과 같이 줄여서 사용할 수 있습니다.

검정색: k, 흰색: w, 빨간색: r, 파란색: b, 초록색: g, 노란색: y

모든 Axes가 숫자 표기 단위로 과학적 표기를 쓰지 않도록 합니다. 이 차트들이 주로 원, 확률 등을 표기하기 때문에 과학적 표기로 변환되면 보기가 어렵기 때문에 과학적 표기법을 비활성화합니다.

axes[0]은 첫 번째 차트인 일봉 차트를 가시화하기 위한 Axes입니다. y축 레이블을 "Environment"의 약자인 "Env."로 표기합니다. 거래량을 표시하기 위해 막대 차트를 그립니다. 그리고 일봉 차트를 구성하기 위해 ohlc 데이터를 구성합니다. ohlc란 open, high, low, close의 약자로 Nx5차원의 배열입니다. 열의 수가 5인 이유는 첫 번째 열은 인덱스이기 때문입니다. N은 일봉의 수입니다.

다음과 같이 ohlc 배열을 만들기 위해서 1차원 인덱스 배열을 만들고 chart_data의 4번째 열까지 수평으로 붙입니다.

수평으로 인덱스 배열과 차트 데이터 일부를 붙여서 ohlc 데이터 구성

수평으로 인덱스 배열과 차트 데이터 일부를 붙여서 ohlc 데이터 구성

파이썬 팁: Numpy의 arrange() 함수는 0부터 입력으로 들어온 값만큼 순차적으로 값을 생성해서 배열로 반환합니다. np.arrange(3)이면 Numpy 배열 [0, 1, 2]를 반환합니다. 시작 값(start), 종료 값(stop), 값 사이 간격(step)을 옵션으로 넣을 수 있습니다.

mpl_finance 모듈의 candlestick_ohlc() 함수의 입력은 Axes 객체와 ohlc 데이터입니다. 여기서 옵션으로 양봉(colorup) 및 음봉(colordown)의 색을 지정할 수 있습니다.

코드 조각 3: 가시화 함수

다음은 에포크 결과를 가시화하는 plot() 함수의 초반 부분을 보여줍니다.

Visualizer 클래스: 가시화 함수 (1)

    def plot(self, epoch_str=None, num_epoches=None, epsilon=None,
            action_list=None, actions=None, num_stocks=None,
            outvals_value=[], outvals_policy=[], exps=None, 
            learning_idxes=None, initial_balance=None, pvs=None):
        with lock:
            x = np.arange(len(actions))  # 모든 차트가 공유할 x축 데이터
            actions = np.array(actions)  # 에이전트의 행동 배열
            # 가치 신경망의 출력 배열
            outvals_value = np.array(outvals_value)
            # 정책 신경망의 출력 배열
            outvals_policy = np.array(outvals_policy)
            # 초기 자본금 배열
            pvs_base = np.zeros(len(actions)) + initial_balance

plot() 함수는 Visualizer의 핵심 함수이므로 이 함수의 인자들에 대해 다음과 같이 먼저 짚고 넘어가겠습니다.

  • epoch_str: Figure 제목으로 표시할 에포크
  • num_epoches: 총 수행할 에포크 수
  • epsilon: 탐험률
  • action_list: 에이전트가 수행할 수 있는 전체 행동 리스트
  • actions: 에이전트가 수행한 행동 배열
  • num_stocks: 주식 보유 수 배열
  • outvals_value: 가치 신경망의 출력 배열
  • outvals_policy: 정책 신경망의 출력 배열
  • exps: 탐험 여부 배열
  • learning_idxes: 학습 위치 배열
  • initial_balance: 초기 자본금
  • pvs: 포트폴리오 가치 배열

먼저 모든 차트가 공유할 x축 데이터를 생성합니다. actions, num_stocks, outvals_value, outvals_policy, pvs의 크기가 모두 같기 때문에 그중 하나인 actions의 크기만큼 배열을 생성해 x축으로 사용합니다.

그리고 Matplotlib이 NumPy 배열을 입력으로 받기 때문에 리스트를 모두 NumPy 배열로 감싸줍니다.

포트폴리오 가치 차트에서 초기 자본금에 직선을 그어서 포트폴리오 가치와 초기 자본금을 쉽게 비교할 수 있게 배열 pvs_base를 준비합니다.

파이썬 팁: Numpy의 zeros() 함수는 인자로 배열의 형태인 shape를 받아서 0으로 구성된 NumPy 배열을 반환합니다. 예를 들어, zeros(3)[0, 0, 0]을, zeros((2, 2))[ [0, 0], [0, 0] ]을 반환합니다. 주의할 점은 다차원 배열의 경우 그 형태를 튜플로 넘겨줘야 한다는 점입니다.

다음은 에이전트 상태 차트를 그리는 코드를 보여줍니다.

Visualizer 클래스: 가시화 함수 (2)

            # 차트 2. 에이전트 상태 (행동, 보유 주식 수)
            for action, color in zip(action_list, self.COLORS):
                for i in x[actions == action]:
                    # 배경색으로 행동 표시
                    self.axes[1].axvline(i, color=color, alpha=0.1)
            self.axes[1].plot(x, num_stocks, '-k')  # 보유 주식 수 그리기

에이전트가 수행한 행동을 배경색으로 표시하고 보유 주식 수를 라인 차트로 그립니다. 매수 행동의 배경색을 빨간색으로, 매도 행동의 배경색을 파란색으로 그립니다. 그리고 보유 주식 수를 실선을 그립니다.

파이썬 팁: zip()는 파이썬 내장 함수로 두 개의 배열에서 같은 인덱스의 요소를 순서대로 묶어 줍니다. 예를 들어, zip([1,2,3], [4,5,6])[(1, 4), (2, 5), (3, 6)]이 됩니다.

파이썬 팁: Matplotlib의 axvline()은 x축 위치에서 세로로 선을 긋는 함수입니다. 이 선의 색깔은 color 인자로, 선의 투명도는 alpha로 정할 수 있습니다.

파이썬 팁: Matplotlib의 plot() 함수는 x축의 데이터, y축의 데이터, 차트의 스타일을 인자로 받습니다. x축 데이터와 y축 데이터의 크기가 같아야 합니다. 스타일은 실선, 점선 등의 선 형태와 선 색깔로 정의합니다. 예를 들어서 '-k'는 검정색 실선을 의미합니다.

다음은 가치 신경망의 출력을 가시화하는 소스 코드를 보여줍니다.

Visualizer 클래스: 가시화 함수 (3)

            # 차트 3. 가치 신경망
            if len(outvals_value) > 0:
                max_actions = np.argmax(outvals_value, axis=1)
                for action, color in zip(action_list, self.COLORS):
                    # 배경 그리기
                    for idx in x:
                        if max_actions[idx] == action:
                            self.axes[2].axvline(idx, 
                                color=color, alpha=0.1)
                    # 가치 신경망 출력의 tanh 그리기
                    self.axes[2].plot(x, outvals_value[:, action], 
                        color=color, linestyle='-')

가치 신경망 출력은 세 번째 행에 그립니다. 행동에 대한 예측 가치를 라인 차트로 그립니다. 매수는 빨간색, 매도는 파란색, 관망은 초록색으로 그립니다. 가치를 예측할 행동에 관망이 없으면 초록색 라인 차트는 그려지지 않습니다. 배경은 가장 예측 가치가 높은 행동에 대한 색으로 칠합니다. 이는 가장 높은 예측 가치를 쉽게 파악하기 위해서입니다.

파이썬 팁: zip(list1, list2)에서 list1의 크기가 2개, list2의 크기가 3개라면 zip 결과 크기는 2개가 됩니다. 즉, zip가 가능한 크기까지 두 리스트가 묶이고 나머지는 버려집니다. 예를 들어, zip([1, 2], ['a', 'b', 'c'])의 결과는 [(1, 'a'), (2, 'b')]가 됩니다.

다음 코드조각에서는 정책 신경망의 출력과 탐험한 부분을 가시화하는 소스 코드를 살펴보겠습니다.

Visualizer 클래스: 가시화 함수 (4)

            # 차트 4. 정책 신경망
            # 탐험을 노란색 배경으로 그리기
            for exp_idx in exps:
                self.axes[3].axvline(exp_idx, color='y')
            # 행동을 배경으로 그리기
            _outvals = outvals_policy if len(outvals_policy) > 0 \
                else outvals_value
            for idx, outval in zip(x, _outvals):
                color = 'white'
                if np.isnan(outval.max()):
                    continue
                if outval.argmax() == Agent.ACTION_BUY:
                    color = 'r'  # 매수 빨간색
                elif outval.argmax() == Agent.ACTION_SELL:
                    color = 'b'  # 매도 파란색
                self.axes[3].axvline(idx, color=color, alpha=0.1)
            # 정책 신경망의 출력 그리기
            if len(outvals_policy) > 0:
                for action, color in zip(action_list, self.COLORS):
                    self.axes[3].plot(
                        x, outvals_policy[:, action], 
                        color=color, linestyle='-')

먼저 탐험을 수행한 지점을 네 번째 차트에 그립니다. exps 배열이 탐험을 수행한 x축 인덱스를 가지고 있습니다. 탐험한 x축 인덱스에 대해 배경을 노란색으로 표시합니다. 탐험하지 않은 지점에서는 에이전트의 행동을 매수는 빨간색, 매도는 파란색으로 표시합니다.

추가로 네 번째 차트에 정책 신경망의 출력을 표시합니다. 매수에 대한 정책 신경망의 출력값을 빨간색 선으로, 매도에 대한 정책 신경망의 출력값을 파란색 선으로 그립니다. 빨간색 선이 파란색 선보다 위에 위치하면 에이전트가 매수를 했을 것이고 그 반대의 경우 에이전트는 매도를 수행했을 것입니다.

파이썬 팁: Matplotlib plot() 함수에서는 스타일을 다양하게 구성할 수 있습니다. 다양한 모양과 색깔을 조합해 표시할 수 있습니다. 예를 들어, '-'은 선, '.'은 점, 'r'은 빨간색, 'b'는 파란색을 의미하는데, 이들의 조합인 '-r', '-b', '.r', '.b'는 각각 빨간 선, 파란 선, 빨간 점, 파란 점을 의미합니다. 색깔과 스타일을 각각 colorlinestyle 키워드 인자로 지정할 수도 있습니다.

예제 5.31은 포트폴리오 가치 차트를 그리고 제목을 포함해 Figure를 최종적으로 구성합니다.

Visualizer 클래스: 가시화 함수 (5)

            # 차트 5. 포트폴리오 가치
            self.axes[4].axhline(
                initial_balance, linestyle='-', color='gray')
            self.axes[4].fill_between(x, pvs, pvs_base,
                where=pvs > pvs_base, facecolor='r', alpha=0.1)
            self.axes[4].fill_between(x, pvs, pvs_base,
                where=pvs < pvs_base, facecolor='b', alpha=0.1)
            self.axes[4].plot(x, pvs, '-k')
            # 학습 위치 표시
            for learning_idx in learning_idxes:
                self.axes[4].axvline(learning_idx, color='y')

            # 에포크 및 탐험 비율
            self.fig.suptitle('{} \nEpoch:{}/{} e={:.2f}'.format(
                self.title, epoch_str, num_epoches, epsilon))
            # 캔버스 레이아웃 조정
            self.fig.tight_layout()
            self.fig.subplots_adjust(top=0.85)

초기 자본금을 가로로 일직선을 그어서 손익을 쉽게 파악할 수 있게 합니다. 포트폴리오 가치가 초기 자본금보다 높은 부분은 빨간색으로, 포트폴리오 가치가 초기 자본금보다 낮은 부분을 파란색으로 배경을 칠합니다. 그리고 그 위에 포트폴리오 가치를 검정 실선으로 그립니다. 추가로 학습을 수행한 위치를 노란색 수직선으로 표시합니다.

제목에는 prepare() 함수의 인자로 들어오는 title 문자열을 출력하고 그다음 줄에 에포크와 탐험 비율을 적어 줍니다. 인자로 들어오는 문자열 title은 종목 코드, 강화학습 기법, 신경망 종류, 학습 속도, 할인율, 투자 단위, 지연 보상 임곗값 등의 강화학습 설정에 대한 정보를 담고 있습니다.

파이썬 팁: Matplotlib의 axhline()은 y축 위치에서 가로로 선을 긋는 함수입니다. 이 선의 색깔은 color 인자로, 선의 투명도는 alpha로 정할 수 있습니다.

파이썬 팁: Matplotlib의 fill_between() 함수는 x축 배열과 두 개의 y축 배열을 입력으로 받습니다. 두 개의 y축 배열의 같은 인덱스 위치의 값 사이에 색을 칠합니다. where 옵션으로 색을 칠할 조건을 추가할 수 있고 facecolor 옵션으로 칠할 색을 지정하고 alpha 옵션으로 투명도를 조정할 수 있습니다.

파이썬 팁: 파이썬에서 문자열에 값을 넣어주는 방법으로 %를 사용할 수 있습니다. 문자열 내에서 %s는 문자열을, %d는 정수를, %f는 실수를 입력하는 자리입니다. 예를 들어, 'Hello %s' % 'World!''Hello World!'가 됩니다. 정수와 실수의 경우 '%d / %d = %.2f' % (10, 3, 10/3)'10 / 3 = 3.33'으로 표현됩니다.

다른 방법으로 format() 함수를 사용할 수 있습니다. 이 함수에서는 값을 넣을 자리를 {}로 표현합니다. 예를 들어 'Hello {}'.format('World!')'Hello World!'가 됩니다. 이 자리에 이름을 정할 수도 있습니다. 예를 들어, 'Hello {tail}'.format(tail='World!')'Hello World!'가 됩니다.

파이썬 팁: Matplotlib의 tight_layout() 함수는 Figure의 크기에 알맞게 내부 차트의 크기를 조정해줍니다.

코드 조각 4: 가시화 정보 초기화 및 결과 저장 함수

다음은 Figure를 초기화하고 저장하는 함수를 보여줍니다.

Visualizer 클래스: 클리어 함수 및 저장 함수

    def clear(self, xlim):
        with lock:
            _axes = self.axes.tolist()
            for ax in _axes[1:]:
                ax.cla()  # 그린 차트 지우기
                ax.relim()  # limit를 초기화
                ax.autoscale()  # 스케일 재설정
            # y축 레이블 재설정
            self.axes[1].set_ylabel('Agent')
            self.axes[2].set_ylabel('V')
            self.axes[3].set_ylabel('P')
            self.axes[4].set_ylabel('PV')
            for ax in _axes:
                ax.set_xlim(xlim)  # x축 limit 재설정
                ax.get_xaxis().get_major_formatter() \
                    .set_scientific(False)  # 과학적 표기 비활성화
                ax.get_yaxis().get_major_formatter() \
                    .set_scientific(False)  # 과학적 표기 비활성화
                # x축 간격을 일정하게 설정
                ax.ticklabel_format(useOffset=False)

    def save(self, path):
        with lock:
            self.fig.savefig(path)

clear() 함수에서는 학습 과정에서 변하지 않는 환경에 관한 차트를 제외하고 그 외 차트들을 초기화합니다. 입력으로 받는 xlim은 모든 차트의 x축 값 범위를 설정해줄 튜플입니다.

Axes의 cla() 함수로 이전에 그린 차트를 지우고 차트의 x축과 y축의 값 범위를 초기화합니다. 그리고 자동 크기 조정 기능을 활성화합니다.

이어서 차트들의 y축 레이블과 x축 값의 범위를 설정해 줍니다. x축과 y축의 값을 있는 그대로 보여주기 위해 과학적 표기 기능을 비활성화합니다. 그리고 x축 간격을 일정하게 설정합니다. 그림 5.8은 x축 간격을 값에 맞게 조정한 결과와 x축 간격을 일정하게 설정한 결과의 차이를 보여줍니다.

x축 간격을 값에 맞게 조정한 결과와 일정하게 설정한 결과의 차이

x축 간격을 값에 맞게 조정한 결과와 일정하게 설정한 결과의 차이

이렇게 x축 간격을 일정하게 설정하는 이유는 일봉 차트의 경우 x축을 날짜로 지정하면 토요일이나 일요일과 같이 휴장하는 날에는 그 부분의 차트가 비어서 보기에 좋지 않기 때문입니다. 대부분 서비스에서 일봉 차트는 일반적인 휴장일 부분은 표시하지 않습니다. save() 함수에서는 Figure를 그림 파일로 저장합니다. 다음은 이렇게 생성된 가시화된 결과를 보여줍니다.

가시화 모듈에서 생성한 가시화 결과 그림 파일

가시화 모듈에서 생성한 가시화 결과 그림 파일

첫 번째 차트는 prepare() 함수가 그린 종목의 일봉 차트입니다. 이 차트는 전체 학습 과정에서 동일하기 때문에 한 번만 호출되는 prepare() 함수가 그려줍니다. 그 외의 차트들은 에포크마다 다르기 때문에 plot() 함수가 그립니다. 여기서 threading 모듈의 Lock 클래스를 활용하는 이유는 A3C 강화학습의 경우 여러 스레드가 병렬로 강화학습을 수행하기 때문에 안정적인 가시화를 위해 가시화 작업 도중에 다른 스레드의 간섭을 받지 않게 한 것입니다.