딥러닝이 발달함에 따라 비전과 음성인식 분야에서 좋은 결과를 보이고 있으나, 시계열 분야에서는 RNN과 1D-CNN으로는 만족스러운 결과를 보이지 못하고 있다. 오늘은 시계열 자료를 이미지로 인코딩하는 방법 중 하나인 Gramian Angular Field(GAF)에 대해 알아보고자 한다. GAF는 시계열 데이터를 극좌표계 변환을 통해 이미지로 표현한 것을 의미한다. 또한 가시화된 결과가 우상단에서 좌하단으로 시계열자료를 가시화하므로 시간적 종속성유지한다는 특징이 있다.

GAF 분류

GAF는 크게 GASF와 GADF로 구분된다고 한다. pyts패키지의 GADF 함수의 경우 $sin(-\phi_i+\phi_j)$로 계산되므로 역산의 편의성을 위해 아래와 같이 기술하였다.

$$GASF=\begin{pmatrix} cos(\phi_1+\phi_1) & cos(\phi_1+\phi_2) & \cdots & cos(\phi_1+\phi_n) \\ cos(\phi_2+\phi_1) & cos(\phi_2+\phi_2)& \cdots & cos(\phi_2+\phi_n) \\ \vdots & \vdots & \ddots &\vdots \\ cos(\phi_m + \phi_1) & cos(\phi_m+\phi_2) & \cdots & cos(\phi_m+\phi_n) \end{pmatrix} $$

$$GADF=\begin{pmatrix} sin(\phi_1+\phi_1) & sin(\phi_1+\phi_2) & \cdots & sin(\phi_1+\phi_n) \\ cos(\phi_2+\phi_1) & sin(\phi_2+\phi_2)& \cdots & sin(\phi_2+\phi_n) \\ \vdots & \vdots & \ddots &\vdots \\ sin(\phi_m + \phi_1) & sin(\phi_m+\phi_2) & \cdots & sin(\phi_m+\phi_n) \end{pmatrix} $$

GAF를 파이썬으로 구현하면 아래와 같다.

import numpy as np

def cos_sum(a, b):
    """To work with tabulate."""
    return(np.cos(a+b))

def sin_sum(a, b):
    """To work with tabulate."""
    return(np.sin(a+b))
    
x_min=np.min(x)
x_max=np.max(x)
scale=(2*x - x_max- x_min)/(x_max-x_min)
x = np.linspace(0, 1, len(scale))

phi=np.arccos(scale)

gasf=np.vectorize(cos_sum)(
  *np.meshgrid(phi, phi, sparse=True))
gadf=np.vectorize(sin_sum)(
  *np.meshgrid(phi, phi, sparse=True))

python의 구현

python에는 pyts패키지라는 유용한 패키지가 있다. pyts패키지 구현은 아래와 같다.

from pyts.image import GramianAngularField
gasf = GramianAngularField(method='summation')
gadf = GramianAngularField(method='difference')

plt.imshow(gasf.fit_transform(X)[0,:,:])
plt.imshow(gadf.fit_transform(X)[0,:,:])
import numpy as np
import math
import matplotlib.pyplot as plt
time_points = np.linspace(0, 4 * np.pi, 1000)
x = np.sin(time_points)
X = np.array([x])

def cos_sum(a, b):
    """To work with tabulate."""
    return(math.cos(a+b))

def sin_sum(a, b):
    """To work with tabulate."""
    return(math.sin(b-a))
x_min=np.min(x)
x_max=np.max(x)
scale=(2*x - x_max- x_min)/(x_max-x_min)

phi=np.arccos(scale)
r = np.linspace(0, 1, len(scale))

#gasf
plt.imshow(np.vectorize(cos_sum)(
  *np.meshgrid(phi, phi, sparse=True)))

#gadf
plt.imshow(np.vectorize(sin_sum)(
  *np.meshgrid(phi, phi, sparse=True)))

추가 내용은 아래 링크를 참고하기 바란다.

pyts.image.GramianAngularField — pyts 0.12.0 documentation

GAF의 역변환

수식을 통한 역변환

GAF는 아쉽게도 역변환이 안된다고 한다. GADF의 주 대각선은 $cos(2\Phi_i)$, GASF의 주 대각선은 $sin(2 \Phi_i)$이 되므로 arctan를 통해 각도를 계산할 수 있다. 다만 pyts패키지를 통해서는 주 대각선이 $sin(0)$으로 0이 되므로, 계산이 불가하였다.

import numpy as np
import math
import matplotlib.pyplot as plt
time_points = np.linspace(0, 4 * np.pi, 1000)
x = np.sin(time_points)
X = np.array([x])

def cos_sum(a, b):
    """To work with tabulate."""
    return(np.cos(a+b))

def sin_sum(a, b):
    """To work with tabulate."""
    return(np.sin(+a+b))
x_min=np.min(x)
x_max=np.max(x)
scale=(2*x - x_max- x_min)/(x_max-x_min)

phi=np.arccos(scale)
r = np.linspace(0, 1, len(scale))

diag_c=np.diag(np.vectorize(cos_sum)(
  *np.meshgrid(phi, phi, sparse=True)))
diag_s=np.diag(np.vectorize(sin_sum)(
  *np.meshgrid(phi, phi, sparse=True)))


angs=np.arctan2(diag_s,diag_c)/2

ang_=list()
for ang in angs:
    if ang>=0:
        ang_.append(ang)
    else:
        ang_.append(ang+np.pi)
plt.plot(phi)
plt.plot(np.arctan2(diag_s,diag_c)/2)
plt.plot(ang_)

패키지를 통한 역변환

역변환 연산이 복잡하여 딥러닝 모델을 활용하여 해결해 보았다. 적어도 기온 자료에 대해서는 적용이 가능해 보였다.

library(tensorflow)
library(reticulate)
library(keras)
library(data.table)
reticulate::py_config()
pyts=import('pyts')
tf$test$is_gpu_available()
# 디비 연결
con <- RMariaDB::dbConnect(RMariaDB::MariaDB(),
    port = 3307,
    host = "ducj3.iptime.org",
    user = "ducj", password = Sys.getenv('db_key'),
	dbname='mysql'
)
# DB 데이터 로드
RMariaDB::dbGetQuery(con,'select * from asos_h limit 1')
load(file='/home/ducj/shiny/shinyapps/asos_flexdashboard/station.rda')
df=RMariaDB::dbGetQuery(con,
  'select tm,ta from asos_h where stnId=100')
df[,'tm']=as.POSIXct(df[,'tm'])
RMariaDB::dbDisconnect(con)

times=df$tm
time=times[1]

# -1~1 사이로 자료 변환
df$scale_ta=(
  2*df$ta-max(df$ta)-min(df$ta))/
    (max(df$ta)-min(df$ta))
gasf=pyts$image$GramianAngularField(
  method='summation',sample_range =NULL)
gadf=pyts$image$GramianAngularField(
  method='difference',sample_range =NULL)

# 학습자료 생성 
## train_x
## (71시간 전부터 현재까지 자료를 GASF, GADF로 변환)
## train_y
## (71시간 전부터 현재까지 -1~1 사이로 스케일 변환한 기온 자료)
gasf_tas=list(); gadf_tas=list();series=list()
for(time in as.character(times[71:length(times)])){
  message(as.character(time))
  time=as.POSIXct(time)
  scale_ta=df[
    which(df$tm%in%(time-(71:0)*3600)),'scale_ta']
  gasf_ta=gasf$fit_transform(list(scale_ta))
  gadf_ta=gadf$fit_transform(list(scale_ta))
  gasf_ta=array(gasf_ta,c(1,72,72,1))
  gadf_ta=array(gadf_ta,c(1,72,72,1))
  scale_ta=array(scale_ta,c(1,72))
  gasf_tas[[as.character(time)]]=gasf_ta
  gadf_tas[[as.character(time)]]=gadf_ta
  series[[as.character(time)]]=scale_ta
  
  rm(list=c('gasf_ta','gadf_ta','scale_ta'))
}
gc()

series=abind::abind(series,along=1)
gasf_ta_df=abind::abind(gasf_tas,along=1)
gadf_ta_df=abind::abind(gadf_tas,along=1)
dfs=abind::abind(gasf_ta_df,gadf_ta_df,along=4)
rm(list=c('gasf','gadf','gasf_ta_df',
  'gadf_ta_df','gadf_tas','gasf_tas'))
gc()

time_idx=times[71:length(times)]

# 학습 모델 생성
height=72
width=72
channels=2

# 3일(72시간) 자료
set_random_seed(42)
model_input=layer_input(shape=c(height,width,channels))
model_output=model_input%>%layer_conv_2d(
  filters = 32, kernel_size = c(3,3),
  activation='relu')%>%
  layer_max_pooling_2d(pool_size=c(2,2))%>%
  layer_conv_2d(filters=64, kernel_size=c(3,3),
  activation='relu')%>%
  layer_max_pooling_2d(pool_size=c(2,2))%>%
  layer_conv_2d(filters=64,kernel_size = c(3,3),
  activation='relu')%>%
  layer_flatten()%>%
  layer_dense(units=512)%>%
  layer_dense(units=72)
model=keras_model(model_input,model_output)

model%>%compile(
  optimizer=optimizer_rmsprop(learning_rate =.001),
  loss=loss_mean_absolute_error()
)

library(lubridate)
min(which(year(times[71:length(times)])==2022)) # 8691
dim(dfs);dim(series)
history=model%>%fit(
  dfs[1:8691,,,1:2,drop=F],
  series[1:8691,],
  epochs=500,
  batch_size=256,
  validation_split=.2
)
pred=model%>%predict(dfs[8692:10682,,,1:2,drop=F])

R에서의 구현

R을 통해 GASF와 GADF를 구현하는 것은 아래와 같다.

library(forecast)
minmax=function(x){
  (2*x-max(x,na.rm=T)-min(x,na.rm=T))/
    (max(x,na.rm=T)-min(x,na.rm=T))
}
phi=acos(
  as.vector(minmax(AirPassengers)))

cos_sum=function(a,b){
  cos(a+b)
}
sin_sum=function(a,b){
  sin(b-a)
}

raster::plot(
  raster::raster(
    outer(phi,phi,FUN=Vectorize(cos_sum))))
raster::plot(
  raster::raster(
    outer(phi,phi,FUN=Vectorize(sin_sum))))

library(reticulate)
pyts=import('pyts')
gasf=pyts$image$GramianAngularField(
  method='summation',sample_range =NULL)
gadf=pyts$image$GramianAngularField(
  method='difference',sample_range =NULL)

gasf_ta=gasf$fit_transform(
  list(as.vector(minmax(AirPassengers))))
gadf_ta=gadf$fit_transform(
  list(as.vector(minmax(AirPassengers))))

raster::plot(
  raster::raster(gasf_ta[1,,]))
raster::plot(
  raster::raster(gadf_ta[1,,]))

# 역변환
library(raster)
gasf=outer(phi,phi,FUN=Vectorize(cos_sum))
plot(raster(gasf))
gadf=outer(phi,phi,FUN=Vectorize(sin_sum))
plot(phi,type='l')
plot(atan2(diag(gadf),diag(gasf)),type='l')
ang=atan2(diag(gadf),diag(gasf))/2
plot(ifelse(ang>=0,ang,ang+pi),type='l')
plot(cos(ifelse(ang>=0,ang,ang+pi)),type='l')

향후 계획

ConvLSTM의 경우 다음 이미지를 예측할 때 활용 가능한 딥러닝 기술이다. 해당 기술을 통해 다음 시점의 GAF를 예측할 수 있다면, 좀 더 정밀한 시계열 예측을 수행할 수 있지 않을까 한다. 해당부분이 잘 수행되면 논문화와 포스팅을 진행해보고자 한다.

또한 정형데이터의 분석에서는 딥러닝보다 머신러닝 기술이 우수한 경우가 다반사였는데, tabnet은 부스팅계열의 성능과 유사하게 나타난다고 한다. 따라서 향후에 이를 자세히 알아보고 앞서 말한 GAF를 통한 예측과 비교해볼까 한다.

GitHub - lucidrains/tab-transformer-pytorch: Implementation of TabTransformer, attention network for tabular data, in Pytorch
Implementation of TabTransformer, attention network for tabular data, in Pytorch - GitHub - lucidrains/tab-transformer-pytorch: Implementation of TabTransformer, attention network for tabular data,...
GitHub - dreamquark-ai/tabnet: PyTorch implementation of TabNet paper : https://arxiv.org/pdf/1908.07442.pdf
PyTorch implementation of TabNet paper : https://arxiv.org/pdf/1908.07442.pdf - GitHub - dreamquark-ai/tabnet: PyTorch implementation of TabNet paper : https://arxiv.org/pdf/1908.07442.pdf

Markov Transition Fields도 향후 확인 예정

참고

[시계열 데이터를 이미지화하기] Gramian Angular Field(GAF) Imaging 개념이해
Gramian Angular Field (GAF) 개요 시계열 데이터를 비-데카르트 좌표계(non-Cartesian coordinates system)의 이미지로 표현한 것 데카르트 좌표계를 사용하지 않는 대신에, 각각의 좌표 값들은 극좌표계(Polar..
Classification of Time-Series Images Using Deep Convolutional Neural Networks
Convolutional Neural Networks (CNN) has achieved a great success in image recognition task by automatically learning a hierarchical feature representation from raw data. While the majority of Time-Series Classification (TSC) literature is focused on 1D signals, this paper uses Recurrence Plots (RP)…
IEEE Xplore Full-Text PDF: