はじめに
DROBE ではユーザーとアイテムのマッチ度を算出するモデルを利用していますが、ユーザー側の特徴量として商品の提案日というものを使っています。
これは、例えば夏に商品を提案する際にコートを提案するのはあまり意味が無いという事から、ファッションにおける推薦には季節性を考慮する必要があるという所をモチベーションとしています。
この記事では日付を特徴量として使った際に線形モデルで季節を予測してアプリケーションで使う際の注意点について解説します。
特徴量としての配送日
配送日は日付なので、循環するものであると言えます。例えば12月31日と1月1日はとなり合わせの日付なので特徴としては非常に近いはずです。
ところが日付をそのまま入力とすると、機械学習のモデルは12月31日と1月1日の間に数字的な繋がりを見出す事が出来ません。
そこでモデルが日付を循環したものとして捉えられるように三角関数を使って日付を変換します。
(このコードは こちらの記事 を参考にさせていただきました)
import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt
import seaborn as sns
def encode(df, col):
""""
この方法だと場合によって最大値が変化するデータでは正確な値は出ない
例:月の日数が30日や31日の場合がある
""""
df[col + '_cos'] = np.cos(2 * np.pi * df[col] / df[col].max())
df[col + '_sin'] = np.sin(2 * np.pi * df[col] / df[col].max())
return df
SEASON = {
1: "WINTER",
2: "WINTER",
3: "SPRING",
4: "SPRING",
5: "SPRING",
6: "SUMMER",
7: "SUMMER",
8: "SUMMER",
9: "FALL",
10: "FALL",
11: "FALL",
12: "WINTER",
}
def make_dataset():
values = []
for i in range(0, 365):
now = datetime.datetime(2021, 1, 1) + datetime.timedelta(days=i)
season = SEASON[now.month]
values.append((now.date(), i, season))
df = pd.DataFrame(values, columns=["date", "yday", "season"])
df = encode(df, "yday")
return df
上記関数を使ってエンコードすると、以下のような dataframe を得る事が出来ます
df = make_dataset()
print(df)
date yday season yday_cos yday_sin
0 2021-01-01 0 WINTER 1.000000 0.000000e+00
1 2021-01-02 1 WINTER 0.999851 1.726064e-02
2 2021-01-03 2 WINTER 0.999404 3.451614e-02
3 2021-01-04 3 WINTER 0.998659 5.176135e-02
4 2021-01-05 4 WINTER 0.997617 6.899114e-02
... ... ... ... ... ...
360 2021-12-27 360 WINTER 0.997617 -6.899114e-02
361 2021-12-28 361 WINTER 0.998659 -5.176135e-02
362 2021-12-29 362 WINTER 0.999404 -3.451614e-02
363 2021-12-30 363 WINTER 0.999851 -1.726064e-02
364 2021-12-31 364 WINTER 1.000000 -2.449294e-16
Plot してみると、循環的な性質を持っている事が良くわかります
sns.scatterplot(data=df, x='yday_cos', y='yday_sin', hue='season')
エンコードされた日付を使って季節を推定してみる
このように sin cos にエンコードされた日付を使えば数字的な繫がりを加味した入力と出来そうですが、実はこのデータを線形モデルの入力としてそのまま使うと思わぬ落とし穴にハマる可能性があるので解説します。
作られたデータを使って以下のようなコードで季節を推定してみます。
from sklearn.metrics import roc_curve, auc
def solve(clf, target_seasons):
df = make_dataset()
X = df[["yday_sin", "yday_cos"]]
y = np.where(df['season'].isin(target_seasons), 1, 0)
clf.fit(X, y)
preds = clf.predict_proba(X)[:, 1] # 検証データがクラス1に属する確率
fpr, tpr, thresholds = roc_curve(y, preds)
plt.plot(fpr, tpr, label='roc curve (area = %0.3f)' % auc(fpr, tpr))
plt.plot([0, 1], [0, 1], linestyle='--', label='random')
plt.legend()
plt.xlabel('false positive rate')
plt.ylabel('true positive rate')
plt.show()
print("{} accuracy={} target_seasons={}".format(clf.__class__.__name__, clf.score(X, y), target_seasons))
まずは季節が春である事を判断出来るかどうかを試してみます。推定には sklearn の LogisticRegression を使います。
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
solve(lr, target_seasons=["SPRING"])
sin cos にエンコードされた日付データから、上手く春を推定できている事がわかります。
LogisticRegression accuracy=1.0 target_seasons=['SPRING']
次に季節が 春 or 秋 である事を判断できるかを試してみます。
lr = LogisticRegression()
solve(lr, target_seasons=["SPRING", "FALL"])
LogisticRegression accuracy=0.5506849315068493 target_seasons=['SPRING', 'FALL']
このように、全く判断出来ていない事がわかります。
ちなみに、冬 or 春 といった隣り合った季節の推定は問題なく出来ます。
lr = LogisticRegression()
solve(lr, target_seasons=["SPRING", "WINTER"])
LogisticRegression accuracy=1.0 target_seasons=['SPRING', 'WINTER']
これは直感的には以下のように考える事ができると思います。
単一の季節や、春と冬、といった隣り合わせの季節は直線を 1 本引けば分離可能なので線形モデルで判断が可能です。一方で、春と秋、といった隣り合っていない季節は分離するためには直線を 2 本引く必要があり、線形モデルでは判断が不可能であると考えられます。
このようにエンコードされた日付を入力として使った場合に、線形モデルを使ってしまうと上手く特徴が使えない事があり得るという事がわかります。
非線形モデルでの挙動
それでは非線形なモデルではどのような挙動になるかを試してみます。
from sklearn.ensemble import RandomForestClassifier
rfc = RandomForestClassifier()
solve(rfc, target_seasons=["SPRING", "FALL"])
このように、隣り合ってない季節でも正確に予測する事が出来ます。
RandomForestClassifier accuracy=1.0 target_seasons=['SPRING', 'FALL']
まとめ
ファッションにおける推薦に欠かせない季節性を考慮する際に重要となる日付データの取り扱いと、線形 / 非線形モデルによる挙動の違いについて解説しました。
データを上手く作っても、モデルの特性によっては望んだ性能が得られないといったケースがあり得てしまう事を検証しました。
アプリケーションやデータによって、どういったモデルを使うのか、また選択したモデルでデータを上手く扱う事が出来るのか、といった事を考慮に入れながら開発をしていくのが大事だと考えいます。