7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

miniROCKET in tsai 備忘録

Last updated at Posted at 2022-07-03

概要

時系列用SOTAなモデルたちを一同に実装したimeseriesai、tsaiを評価した。fastaiの時系列特化版パケージである。
LSTMのようなNNモデルはハイパラ調整で苦労することが多い。そんな中、この非NNなminiROCKETは軽くて調整に苦労することもなさそうだったので少し突っ込んでみた。
そのときの備忘録

  • 実施期間: 2022年7月
  • 環境:Ubuntu20.04 LTS
  • Anaconda: Python3.8.13
  • Package: tsai, sktime, sklearnなど

miniROCKET

時系列データのclassificationやregressionを行うモデルはたくさんあり、最新のモデルを簡単に評価できるパケージがtsaiである。(実装済みモデル一覧参照)
最近の週末はこれをちょこちょこ評価していて、感じたことをあげると

良い点

  • SOTAなモデルを最小のコード変更で比較可能
  • scikit learn互換な書き方もサポート(つまりpipelineやoptunaに組み込める)
  • PyTorchで高速GPU演算

イマイチな点

  • ドキュメントが完成していない(これが一番痛い)
  • multi labelのbugに未対応
  • early stoppingは自分で実装(callback()はあり)
  • PyTorchが入るため、condaへのinstallにひと癖あり

実装されているモデル中、試していて気になったモデルがinception timeとminiROCKET。
前者はCNNを組み合わせたもので、LSTMよりも圧倒的に少ないepoch数でlossが低下してくれ(LSTMが遅いだけという話もあるが)、accも良さげ。tsaiでもっと評価して、使えそうならKerasで実装する方法も次回以降投稿してみよう。
後者のminiROCKETは、convolution kernelを複数回(10000回とか)当てて、time step次元のデータを圧縮し、featureの次元方向にdatasetを広げているように見えた。この前処理がROCKETの特徴であり、その後は普通のML同様に線形モデルでclassificationする。この線形モデルはpaperではRidgeClassifierCV()が試されている。

miniROCKETのpaperのabstractより(by DeepL)
「Rocketは、ランダムな畳み込みカーネルを用いて入力時系列を変換し、変換された特徴量を用いて線形分類器を学習することにより、既存の多くの手法と比較してわずかな計算量で最先端の時系列分類精度を達成することができます。我々は、Rocketを新しい手法であるMiniRocketに再定式化した。
MiniRocketは、大規模なデータセットにおいてRocketの最大75倍高速で、ほぼ決定論的(オプションで完全決定論的)であり、基本的に同じ精度を維持することができます。この方法を用いると、UCRアーカイブの109個のデータセットのすべてについて、分類器の学習とテストを10分以内に最先端の精度で行うことが可能である。
MiniRocketは、同等の精度を持つ他の手法(Rocketを含む)よりも大幅に高速であり、遠隔的に同様の計算費用を要する他の手法よりも大幅に高精度です。」

0. データ

今回はtsaiのget_UCR_data()関数で取得できるデータを使用する。X.shapeでわかるとおりXは下記の形になっている。
(samples, feature(s), time series)
この形のnumpy.array型にreshapeすると、自分のseries datasetも簡単に使用できた。

1. GPU非対応版(scikit learn friendly)

1-1. Packages

from tsai.basics import *
import sktime
import sklearn
my_setup(sktime, sklearn)
from sklearn.metrics import accuracy_score, mean_squared_error
os             : Linux-5.13.0-52-generic-x86_64-with-glibc2.10
python         : 3.8.13
tsai           : 0.3.1
fastai         : 2.6.3
fastcore       : 1.4.4
sktime         : 0.12.1
sklearn        : 1.1.1
torch          : 1.10.2
device         : 1 gpu (['NVIDIA GeForce RTX 3070'])
cpu cores      : 12
RAM            : 31.23 GB
GPU memory     : [7.79] GB

"import *"はなるべく避けたいところだが、何分ドキュメントが貧相なためチュートリアルのままとした。

1.2. Training

チュートリアル通りにTrainingする。
sktimeのminiROCKETをwrapしているとどこかに書かれていた(気がする)。従いsktimeで実装していないGPUには非対応なのだと。
ただ、miniROCKET自体非常に高速なのでデータ数が少なければCPUでも問題ない。

## sklearn-type API (<10k samples)
from tsai.models.MINIROCKET import *

# Univariate classification with sklearn-type API
dsid = 'OliveOil'
X_train, y_train, X_valid, y_valid = get_UCR_data(dsid)   # Download the UCR dataset

# Computes MiniRocket features using the original (non-PyTorch) MiniRocket code.
# It then sends them to a sklearn's RidgeClassifier (linear classifier).
model = MiniRocketClassifier()
timer.start(False)
model.fit(X_train, y_train)
t = timer.stop()
print(f'valid accuracy    : {model.score(X_valid, y_valid):.3%} time: {t}')

1.3 Prediction

predictionも可能 下記ではもちろん前述のmodel.scoreと同じスコアとなる。

preds = model.predict(X_valid)
accuracy_score(y_valid, preds)

2. GPU対応版 Classification

チュートリアルではOffline feature calculationとOnline feature calculationの2パターンが掲載されている。
違いは、前者がtraining前にdatasetへ明示的にconvolutionを当ててdatasetをfixさせ、後者がmodel内部でtraining中にconvolutionを当てる。後者のほうがやや遅い(多分fitが)らしいが、将来scaleしやすくコードもやや読みやすいので後者を残す。

2.1 Packages

## Pytorch implementation (any # samples)
from tsai.models.MINIROCKET_Pytorch import *
from tsai.models.utils import *

2.2 Training

まずデータの読み込み。datasetのshapeは下記のようになっている。validation用のX,yは無いが内部ではRidgeClassifierCV()でcloss validationするため指定が無いのだと思う。
であればfold数など指定できてしかるるべきだが何分ドキュメントが…

# Create the MiniRocket features and store them in memory.
dsid = 'LSST'
X, y, splits = get_UCR_data(dsid, split_data=False)
print(X.shape, y.shape)
print(y[:10])
(4925, 6, 36) (4925,)
['6' '6' '6' '6' '6' '6' '6' '6' '6' '6']

tfのようにbatchに分けてモデルに入力するdata loader、get_ts_dls()を使用している。
最適なLearning rateがSuggestedLRsとして表示されている。この値が自動的にfit時に適用されるわけではない。あくまで確認だけ。

## Online feature calculation

# (したければ)lrの確認
tfms = [None, TSClassification()]
batch_tfms = TSStandardize(by_sample=True)

# onlineではmodel内部でconvolutionしてくれるので、そのままXをdata loaderへ入れる
dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms)
model = build_ts_model(MiniRocket, dls=dls)
learn = Learner(dls, model, metrics=accuracy, cbs=ShowGraph())
print(learn.lr_find())
model

Screenshot from 2022-07-03 15-42-33.png

SuggestedLRs(valley=4.365158383734524e-05)
MiniRocket(
  (backbone): MiniRocketFeatures()
  (head): Sequential(
    (0): Flatten(start_dim=1, end_dim=-1)
    (1): BatchNorm1d(9996, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): Linear(in_features=9996, out_features=14, bias=True)
  )
)

上記はlrを確認するだけなので、実行しなくてもOK。
本番のTrainingは下記となる。
コメントアウトしているがdatasetはもはやtime seriesではないためか、なんとData Augmentationが可能!!
確かにAugmentationさせると精度の向上が確認できた。
time series dataではaugmentなど絶対にできないのでちょっと感動した。

# 本番
tfms = [None, TSClassification()]

batch_tfms = TSStandardize(by_sample=True)
# MiniRocket with data augmentation
# batch_tfms = [TSStandardize(by_sample=True), TSMagScale(), TSWindowWarp()]

dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms)
model = build_ts_model(MiniRocket, dls=dls)
learn = Learner(dls, model, metrics=accuracy, cbs=[ShowGraph()])
timer.start()
learn.fit_one_cycle(30, 3e-4)
timer.stop()

Screenshot from 2022-07-03 15-54-23.png
Screenshot from 2022-07-03 15-54-49.png

2.3. model保存と読込

# model保存
PATH = Path('./models/MiniRocket_aug.pkl')
PATH.parent.mkdir(parents=True, exist_ok=True)
learn.export(PATH)
del learn

# model読込
PATH = Path('./models/MiniRocket_aug.pkl')
learn = load_learner(PATH, cpu=False)

2.4. Prediction

前図のaccuracyチャートとほぼ同じaccを確認できる。

probas, _, preds = learn.get_X_preds(X[splits[1]])
accuracy_score(y[splits[1]], preds)

0.6828872668288727

2.5 その他

viz用のutilityもあるので表示させてみるが、そもそもacc=6.8って相当悪いので覚悟が必要

各labelについての出力確率をプロットしている。

learn.show_probas()

Screenshot from 2022-07-03 16-14-58.png

confusion matrixは対角に数字が並ぶものだが、16と65の誤判定が目立つ。
EDAをしていないので、そもそもかなりinbarrancedなdatasetだったことがここでわかった。

interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

Screenshot from 2022-07-03 16-06-44.png

3. GPU対応版 Regression

チュートリアルはClassificationだけだったので自作。だが大した変更はない。

3.1 Training

Regression用に違うdatdasetの読込と、lrの確認

del learn    # 前のモデルが残っていると追加でfitするのであってもなくても消す
dsid = 'AppliancesEnergy' 
X, y, splits = get_regression_data(dsid, split_data=False)
print(X.shape, y.shape, y[:10])
> (137, 24, 144) (137,) [19.38 12.68  5.34 12.72 13.25 26.28 13.1  14.06 10.92 10.46]

tfms  = [None, [TSRegression()]]
batch_tfms = TSStandardize(by_sample=True, by_var=True)
dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=128)
model = build_ts_model(MiniRocket, dls=dls)
learn = Learner(dls, model, metrics=[mae, rmse], cbs=ShowGraph())    # InceptionTime
learn.lr_find()
model

Screenshot from 2022-07-03 16-24-21.png

で、Training

learn = ts_learner(dls, model, metrics=[mae, rmse], cbs=ShowGraph())
learn.fit_one_cycle(50, 1e-2)

Screenshot from 2022-07-03 16-32-07.png

early stoppingの実装が望まれる。
lilacsさんにfastaiのearly stoppingが使用できることを教えていただいた。
ありがとうございます!
上記最後のセルについて下記を実行すると、early stoppingする。

learn = ts_learner(dls, model, metrics=[mae, rmse], cbs=ShowGraph())
learn.fit_one_cycle(100, 1e-2, 
                    cbs=[EarlyStoppingCallback(monitor='valid_loss', min_delta=0.001, patience=20)])

image.png
No improvement since epoch 20: early stopping
image.png

3.2 Prediction

probas, _, preds = learn.get_X_preds(X[splits[1]])
mean_squared_error(y[splits[1]], preds, squared=False)

4.265168326494313

4. Convolutionを当てると…

前出の通りOff-lineでは明示的にConvolutionを当てる。shapeがどのように変わるか確認できる。

dsid = 'LSST'
X, y, splits = get_UCR_data(dsid, split_data=False)
print(X.shape)

# convolution kernelを当てると、
mrf = MiniRocketFeatures(X.shape[1], X.shape[2]).to(default_device())
X_train = X[splits[0]]
mrf.fit(X_train)
X_feat = get_minirocket_features(X, mrf, chunksize=1024, to_np=True)
print(X_feat.shape)

(4925, 6, 36)
(4925, 9996, 1)

36 time stepの元データが1個になり、6 featuresが10000個近いfeaturesに広がっている。
CNNやってる人にとっては当たり前のことだけど、正直この部分だけでもかなりワクワクしてしまう。

5. 手元のデータでお試し

下記のデータを使用する。

5.1 Data Preparation

GPUを使用しない方のモデルを使用する。
GPU版で試したが下図のようにLossの挙動が変だったので、途中からこのCPU版に切り替えた。
splitsタプルでTrainとvalのそれぞれに使用するsampleのindexを指定したりもしなければならず面倒だったし…
データ量が大きくなればGPU版が有効なのかもしれない。

Screenshot from 2022-07-16 15-14-53.png

import datetime
from datetime import datetime as dt
import statistics
import matplotlib.pyplot as plt
from tsai.models.MINIROCKET import *

intOpen = 0
intHigh = 1
intLow = 2
intClose = 3
df_raw = pd.read_csv('data/all_stocks_2006-01-01_to_2018-01-01.csv')
clm_lst = list(df_raw.columns)
print(clm_lst)

comp_name_lst = list(df_raw['Name'].unique())
print(comp_name_lst)
['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Name']
['MMM', 'AXP', 'AAPL', 'BA', 'CAT', 'CVX', 'CSCO', 'KO', 'DIS', 'XOM', 'GE', 'GS', 'HD', 'IBM', 'INTC', 'JNJ', 'JPM', 'MCD', 'MRK', 'MSFT', 'NKE', 'PFE', 'PG', 'TRV', 'UTX', 'UNH', 'VZ', 'WMT', 'GOOGL', 'AMZN', 'AABA']

月曜から金曜の行にする。元データになければ-1.0とする(あとで内挿する)。

start_str = '2006-01-03'
start_date = dt.strptime(start_str, '%Y-%m-%d')
end_str = '2017-12-31'
end_date = dt.strptime(end_str, '%Y-%m-%d')
td = datetime.timedelta(days=1)

lst = [start_date.date()]
wk_date = start_date
for d in range((end_date - start_date).days):
    wk_date = wk_date + td
    if (wk_date.isoweekday() >= 1) & (wk_date.isoweekday() <= 5):    # 月曜から金曜 pythonのisoweekdayとPandasのdt.weekdayは違う
        lst.append(wk_date)

df_0 = pd.DataFrame(lst, columns=['Date'])
df_0['Weekday'] = df_0['Date'].dt.weekday
df_0.set_index('Date', inplace=True)
df_wk = pd.DataFrame(data = df_raw[df_raw['Name'] == comp_name_lst[0]][clm_lst[:-1]])   # 'MMM'
df_wk['Date'] = pd.to_datetime(df_wk['Date'])
df_wk.set_index('Date', inplace=True)
df_wk = pd.merge(df_0, df_wk, how='outer', left_index=True, right_index=True)
df_wk = df_wk.fillna(-1.0)    # merge()でnanになる要素を-1.0に置き換え(.loc[]でnanの指定方法がわからなかったから)

内挿(前後の平均)処理 連休中は同じ値とする(「途中」で対応)。

date_idx = list(df_wk.index)
nan_flag = False

date_lst1 = []
date_lst = []
date_all_lst = []
open_lst = []
high_lst = []
low_lst = []
close_lst = []
volume_lst = []

for i in range(len(date_idx)):
    if (df_wk.loc[date_idx[i], 'Open']==-1.0) & (nan_flag==False):    # 始まり
        date_lst1.append(date_idx[i])
        open_lst.append(df_wk.loc[date_idx[i-1], 'Open'])
        high_lst.append(df_wk.loc[date_idx[i-1], 'High'])
        low_lst.append(df_wk.loc[date_idx[i-1], 'Low'])
        close_lst.append(df_wk.loc[date_idx[i-1], 'Close'])
        volume_lst.append(df_wk.loc[date_idx[i-1], 'Volume'])
        nan_flag = True
    elif (df_wk.loc[date_idx[i], 'Open']==-1.0) & (nan_flag==True):    # 途中
        date_lst1.append(date_idx[i])
        print(date_lst1)
    elif nan_flag==True:    # 終わり
        open_lst.append(df_wk.loc[date_idx[i], 'Open'])
        high_lst.append(df_wk.loc[date_idx[i], 'High'])
        low_lst.append(df_wk.loc[date_idx[i], 'Low'])
        close_lst.append(df_wk.loc[date_idx[i], 'Close'])
        volume_lst.append(df_wk.loc[date_idx[i], 'Volume'])
        date_lst.append(date_lst1)
        date_lst.append((round(statistics.mean(open_lst),3), 
                         round(statistics.mean(high_lst),3),
                         round(statistics.mean(low_lst),3),
                         round(statistics.mean(close_lst),3),
                         round(statistics.mean(volume_lst),3)))
        date_all_lst.append(date_lst)
        nan_flag = False
        date_lst1 = []
        date_lst = []
        open_lst = []
        high_lst = []
        low_lst = []
        close_lst = []
        volume_lst = []

内挿済み全データを一旦df_wkとする。

for l1 in date_all_lst:
    for l2 in l1[0]:
        df_wk.loc[l2, ['Open', 'High', 'Low', 'Close', 'Volume']] = 
        [l1[-1][0], l1[-1][1], l1[-1][2], l1[-1][3], l1[-1][4]]

モデルに使用するdatasetの作成

df_cp = df_wk[['Weekday', 'Open', 'High', 'Low', 'Close']].copy()    # 説明変数の指定('Weekday'は必須)

append_lst = []
wday_prv = 0    # 生データが1始まりだから
for row in df_cp.itertuples():
    wday_now = row.Weekday
    now = (wday_prv, wday_now)
    append_lst.append(tuple(row)[1:])    # Weekdayは入れない
    wday_prv = wday_now
    row_prv = row
    
sample_num = len(append_lst)

モデルの入力データ型にreshapeし、train&val(CV)とtestにデータをスプリット

lst = []
ts_len = 12    # 3次元目のtime series長 唯一のハイパラ
fst_flag = True
for i in range(sample_num-ts_len):
    lst = []
    for j in range(ts_len):
        lst.append(list(append_lst[i: i+ts_len][j][1:]))    # overlapあり
#         lst.append(list(append_lst[i*ts_len: (i+1)*ts_len][j][1:]))    # overlapなし
    wk_arr = np.array(lst).T
    wk_arr = wk_arr[np.newaxis, :, :]
    if fst_flag == True:
        data_arr = wk_arr
        fst_flag = False
    else:
        data_arr = np.concatenate([data_arr, wk_arr])
    
X = data_arr[:2800]
print(X.shape)
X_test = data_arr[2800+ts_len:-1]    # +ts_len:leakage対策, -1:tgtが1time step先だから最後は使えない
print(X_test.shape)

tgt_lst = [data_arr[i+1][intOpen][-1] for i in range(len(append_lst) - ts_len - 1)]
tgt_arr = np.array(tgt_lst)
y = tgt_arr[:2800]
print(y.shape)
y_test = tgt_arr[2800+ts_len:]
print(y_test.shape)

# GPU版API用
train_idx = [i for i in range(0, 2000)]
val_idx = [i for i in range(2000, 2800)]
splits = (train_idx, val_idx)
## CPU版
model = MiniRocketRegressor(random_state=47)
model.fit(X, y)

preds = model.predict(X_test)
print('MSE: {0:.3f} ,  R^2: {1:.3f}'.
      format(mean_squared_error(y_test, preds, squared=False), r2_score(y_test, preds)))
fig, ax = plt.subplots(figsize=(16,5), tight_layout=True)
plt.suptitle('Close @' + comp_name_lst[0])
ax.plot(y_test[:], label='y_true')
ax.plot(preds[:], label='y_pred')
ax.legend()
plt.show()

全test datasetについてはこんな感じ
※図のタイトルはCloseではなく、Openの間違い
Screenshot from 2022-07-17 09-33-31.png
直近30日分はこう 傾向はつかめているがラグがあり致命的
Screenshot from 2022-07-17 09-32-46.png
参考まで続く2銘柄 これらも上図と同じ傾向
Screenshot from 2022-07-17 09-49-48.png
Screenshot from 2022-07-17 09-56-52.png

説明変数にはお試しなので安直にOpen,High,Low,Closeを入れただけだが、工夫をすればレスポンスは良くなる可能性あり。

以上

7
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?