CRFモデルで時系列データからある時点の状態を推定する

### これを読んで得られること
- CRF（条件付き確率場）を用いた時系列データの分類
    - 自分のためにまとめておこうというモチベーション
    - まとめているうちに自分の中で整理されてきて、記事にするほどじゃないよねという気になっている

### これを読んでも書いていないこと
- 自然言語のCRFによる分類の方法
    - 各ライブラリにあるチュートリアルが詳しいのでそちらへ
- CRFの詳細説明
    - これも大学の講義資料とか書籍とかたくさん詳しい方がいらっしゃるのでそちらへ...


より正確にはCRFに入れる前に分布を仮定して離散化プロセスが必要です。


# CRFとは
深層学習がはやる前に流行っていた教師あり学習のひとつ。例えば、文章中の固有表現の抽出といった系列に対するラベルの付与(系列ラベリング)問題を解く際に適用される。

### 系列ラベリング
- 系列ラベリング
    - 系列に対してラベルを付与するもの
        - 隠れマルコフモデル（HMM）やCRFや深層学習のLSTMがある
- 系列データ
    - 自然言語処理や時系列データ（前後関係がある）
    - 画像（周辺情報も大事）
- ラベリング
    - 自然言語処理は分類で何かを分類すること（MeCabで形態素解析するとか）
    - 時系列データの場合は回帰で今の時点から次の時点を予測すること
    - 画像のピクセル毎の分類（セマンティックセグメンテーション）

今回やりたいことは、時系列データを用いてある時点での挙動を分類すること。
例えば、人の速度と歩数から歩いているか、走っているか、自転車に乗っているかといった推定をしたり、心臓の周波数データから緊張しているとかリラックスしているとかを推定すること。

HMMやCRFの関係性をまとめると次のような図となる。（日本語を付した。一部意訳。）
<img width="800" alt="relationship_diagram_HMM_CRF.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121131/d3540887-8aaa-1854-83ed-9247d7c5c055.png">
（引用: An Introduction to Conditional Random Fields https://arxiv.org/abs/1011.4088 ）

### CRFとは
- マルコフ確率場の一種
    - マルコフ確率場とは依存関係を無向グラフで表すもの（上図の下段）
- 学習で最大化する対象
    - 設計する事後確率
- 教師あり学習


# 実装手順
### ライブラリ候補
- PyStruct
- CRFSuite
- sklearn_crfsuite

sklearn_crfsuiteはCRFSuiteのラッパーでsklearnのモデルセレクションのパッケージが利用できるようになっている。つまりはハイパーパラメーターチューニングが楽。なので今回はsklearn-crfsuiteを用いる。ちなみに線形連鎖条件付き確率場を実現しているライブラリである。


### 実装ステップ
1. データを集める
2. データを学習データとテストデータに分割する train_test_split
3. 学習データとテストデータを整形する 
4. 系列データに分割する for文を用いて切ったりくっつけたり
5. CRFのモデルを決めるsklearn_crfsuite.CRF()で最適化アルゴリズムを設定したり
6. 学習データを学習させる crf.fit(X_train, y_train)
7. テストデータで推論する crf.predict(X_test)
8. テストデータの正解率を確認する metrics.flat_classification_report() recallとかもこれ
9. ハイパーパラメータのチューニング RandomizedSearchCV
10. ハイパーパラメーターと正解のスコアのばらつきを可視化

手順は公式チュートリアルを見ながら時系列データに応用
https://sklearn-crfsuite.readthedocs.io/en/latest/tutorial.html




### 1. データを集める
今回は気象庁の気温と気圧から天気を予測する。
2020年1月1日から2021年1月22日までの3時間毎の気温と気圧と天気をお借りした。
https://www.data.jma.go.jp/gmd/risk/obsdl/index.php

```python
df = pd.read_csv('202001data.csv', encoding='shift-jis', skiprows=3)
df = df.append(pd.read_csv('202004data.csv', encoding='shift-jis', skiprows=3))
df = df.append(pd.read_csv('202007data.csv', encoding='shift-jis', skiprows=3))
df = df.append(pd.read_csv('202010data.csv', encoding='shift-jis', skiprows=3))
df.columns = ['datetime', 'temparature', 'weather', 'pressure']
df = df[['datetime', 'temparature', 'pressure', 'weather']]
df = df.dropna(how='any')
df = df.reset_index(drop = True)
```

<img width="300" alt="データの外観" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121131/aae504d6-501f-07de-e5ba-39b40e854f4b.png">




### 2. データを学習データとテストデータに分割する
```python
X_df = df[['temparature', 'pressure']]
y_s = df['weather']
X_train, X_test, y_train, y_test = train_test_split(X_df, y_s, random_state=0)
```

### 3. 学習データとテストデータを整形する

入力データはdictionary型にする。

```python
X_train = X_train.to_dict(orient='records')
X_test = X_test.to_dict(orient='records')
```

正解ラベルはリスト型

```python
y_train = y_train.to_list()
y_test = y_test.to_list()
```

### 4. 系列データに分割する

全データを等間隔のwindow幅を持つ系列データに整形する。
これは辞書型の集合の配列とする。
学習データとテストデータは[[{}{}{}], [{}{}{}], [{}{}{}]]こんなイメージ。
正解ラベルは[[], [], []]こんなイメージ。

```python
from_i = 0
step = 10 # window幅
to_i = len(X_train) - step

X_trains = []
for i in range(from_i, to_i, step):
    X_trains.append(X_train[i:i+step])

y_trains = []
for i in range(from_i, to_i, step):
    y_trains.append(y_train[i:i+step])

to_i = len(X_test) - step
X_tests = []
for i in range(from_i, to_i, step):
    X_tests.append(X_test[i:i+step])

y_tests = []
for i in range(from_i, to_i, step):
    y_tests.append(y_test[i:i+step])
```

### 5. CRFのモデルを決める
```python
crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs',
    c1=0.1,
    c2=0.1,
    max_iterations=100,
    all_possible_transitions=True
)
```
### 6. 学習データを学習させる
```python
crf.fit(X_trains, y_trains)
```
### 7. テストデータで推論する
```python
labels = list(crf.classes_)
y_pred = crf.predict(X_tests)
metrics.flat_f1_score(y_tests, y_pred, 
                      average='weighted', labels=labels)
```
### 8. テストデータの正解率を確認する
```python
sorted_labels = sorted(
    labels, 
    key=lambda x: (x[1:], x[0])
)
print(metrics.flat_classification_report(
    y_tests, y_pred, labels=sorted_labels, digits=3
))
```

<img width="450" alt="精度結果" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121131/5b0547e4-5df6-bc03-b75b-2e616a7ab457.png">



### 9. ハイパーパラメータのチューニング

```python
# define fixed parameters and parameters to search
crf = sklearn_crfsuite.CRF(
    algorithm='lbfgs', 
    max_iterations=100, 
    all_possible_transitions=True
)
params_space = {
    'c1': scipy.stats.expon(scale=0.5),
    'c2': scipy.stats.expon(scale=0.05),
}

# use the same metric for evaluation
f1_scorer = make_scorer(metrics.flat_f1_score, 
                        average='weighted', labels=labels)

# search
rs = RandomizedSearchCV(crf, params_space, 
                        cv=2, 
                        verbose=1, 
                        n_jobs=-1, 
                        n_iter=50, 
                        scoring=f1_scorer)
rs.fit(X_trains, y_trains)
```

### 10. ハイパーパラメーターと正解のスコアのばらつきを可視化

```python
_x = [s['c1'] for s in rs.cv_results_['params']]
_y = [s['c2'] for s in rs.cv_results_['params']]
_c = [s for s in rs.cv_results_['mean_test_score']]

fig = plt.figure()
fig.set_size_inches(6, 6)
ax = plt.gca()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_xlabel('C1')
ax.set_ylabel('C2')
ax.set_title("Randomized Hyperparameter Search CV Results (min={:0.3}, max={:0.3})".format(
    min(_c), max(_c)
))

ax.scatter(_x, _y, c=_c, s=60, alpha=0.9)

print("Dark blue => {:0.4}, dark red => {:0.4}".format(min(_c), max(_c)))
```

<img width="400" alt="結果" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121131/13e95240-219b-61af-c3e9-78b7e79e7b74.png">

結果より気温と気圧だけでは最大で28%の予測精度であった。
つまり気温と気圧からCRFで天候を予測することは難しい。
本記事の目的としては予測精度を上げることではなく、CRFで時系列データのある点の予測を行うことである。

何かありましたらコメントいただけると幸いです。

### 参考ページ
sklearn-crfsuite
https://sklearn-crfsuite.readthedocs.io/en/latest/index.html

Using CRF in Python
http://acepor.github.io/2017/03/06/CRF-Python/

An Introduction to Conditional Random Fields
https://arxiv.org/abs/1011.4088