Scikit-learnとChainerの勉強がてら、SIGNATEの「お弁当の需要予測」をやってみた。
・python3,pandas,numpy,scikit-learn,chainer
・JupyterNotebook
・Mac
(1)環境構築
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
# Scikit-learnの回帰モデルを行うライブラリをインストール
from sklearn.linear_model import LinearRegression as LR
# Chainerのインストール
import chainer
# 非線型変換を行うライブラリ
import chainer.functions as F
# 線形変換を行うライブラリ
import chainer.links as L
(2)現状分析
①データ取得
SIGNATEからtrainデータとtestデータをダウンロードして取得する。
train = pd.read_csv("train.csv")
test = pd.read_csv("test.csv")
sample = pd.read_csv("sample.csv",header=None)
# 行や列を省略しない処理(以下は500文字をmaxとした)
# 解除する場合は、数値の箇所をNoneに変更
pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 500)
train.shape
# (207,12)
test.shape
# (40,11)
# データを確認する①
train.head()
# データを確認する②
train.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 207 entries, 0 to 206
# Data columns (total 12 columns):
# Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 datetime 207 non-null object
# 1 y 207 non-null int64
# 2 week 207 non-null object
# 3 soldout 207 non-null int64
# 4 name 207 non-null object
# 5 kcal 166 non-null float64
# 6 remarks 21 non-null object
# 7 event 14 non-null object
# 8 payday 10 non-null float64
# 9 weather 207 non-null object
# 10 precipitation 207 non-null object
# 11 temperature 207 non-null float64
# dtypes: float64(3), int64(2), object(7)
# memory usage: 19.5+ KB
②欠損値を埋める
train.isnull().sum()
test.isnull().sum()
train = train.fillna(0)
test = test.fillna(0)
③入力変数を加工(datetime)
datetimeを分解して、int型に変更する。
train["year"] = train["datetime"].apply(lambda x :x.split("-")[0])
train["month"] = train["datetime"].apply(lambda x :x.split("-")[1])
test["year"] = test["datetime"].apply(lambda x :x.split("-")[0])
test["month"] = test["datetime"].apply(lambda x :x.split("-")[1])
train["year"] = train["year"].astype(np.int)
train["month"] = train["month"].astype(np.int)
test["year"] = test["year"].astype(np.int)
train["year"] = train["year"].astype(np.int)
④入力変数を加工(remarks)
remarksの”お楽しみメニュー”を数値に変換する。
def henkan(x):
if x == "お楽しみメニュー":
return 1
else:
return 0
train["remarks_henkan"] = train["remarks"].apply(lambda x:henkan(x))
test["remarks_henkan"] = train["remarks"].apply(lambda x:henkan(x))
⑤入力変数を加工(event)
eventの中身を数値に変換する。
def henkan2(x):
if x == 0:
return 0
else:
return 1
train["event_henkan"] = train["event"].apply(lambda x:henkan2(x))
test["event_henkan"] = test["event"].apply(lambda x:henkan2(x))
⑥入力変数を加工(week)
# weekカラムのデータを抽出
train_week = train.iloc[:,2]
type(train_week)
# pandas.core.series.Series
# pandasのSeries型を、pandasのDataframe型に変更
train_week = pd.DataFrame(train_week)
# ダミー変数化する
train_week = pd.get_dummies(train_week["week"])
# testも同様に対応する
test_week = test.iloc[:,1]
test_week = pd.DataFrame(test_week)
test_week = pd.get_dummies(test_week["week"])
⑦入力変数を加工(temperature)
temperatureをビニングする。
# 最小値と最大値を確認する
train["temperature"].describe()
# count 207.000000
# mean 19.252174
# std 8.611365
# min 1.200000
# 25% 11.550000
# 50% 19.800000
# 75% 26.100000
# max 34.600000
# Name: temperature, dtype: float64
temperature_bining_trainX = pd.cut(train["temperature"],[0,10,20,30,40])
type(temperature_bining_trainX)
# pandas.core.series.Series
# DataFrame型に変換
temperature_bining_trainX = pd.DataFrame(temperature_bining_trainX)
testXも同様の対応する。
test["temperature"].describe()
temperature_bining_testX = pd.cut(test["temperature"],[0,10,20,30,40])
temperature_bining_testX = pd.DataFrame(temperature_bining_testX)
testX["temperature_bining"] = temperature_bining_testX["temperature"]
⑧trainX、testX、yを設定
trainX = train[["event_henkan","remarks_henkan","year","month","payday"]]
testX = test[["event_henkan","remarks_henkan","year","month","payday"]]
y = train["y"]
⑨trainX,testXに加工したweekとtemperatureをドッキングする
temperature_bining_trainXや、temperature_bining_testXは、category型であるが、数値でないと、scikit-learnやchainerで回帰分析できないためint型にする。以下のようにstr型を経てint型にする。
trainX[["月","火","水","木","金"]] = train_week[["月","火","水","木","金"]]
testX[["月","火","水","木","金"]] = test_week[["月","火","水","木","金"]]
trainX["temperature_bining"] = temperature_bining_trainX["temperature"]
testX["temperature_bining"] = temperature_bining_testX["temperature"]
trainX["temperature_bining"] = pd.DataFrame(trainX["temperature_bining"],dtype=np.str)
testX["temperature_bining"] = pd.DataFrame(testX["temperature_bining"],dtype=np.str)
# str型をint型にする
def henkan_u(x):
if "(0, 10]" in x:
return 0
elif "(10, 20]" in x:
return 1
elif "(20, 30]" in x:
return 2
else:
return 3
trainX["temperature_bining"] = trainX["temperature_bining"].apply(lambda x:henkan_u(x))
testX["temperature_bining"] = testX["temperature_bining"].apply(lambda x:henkan_u(x))
(2)入力変数を整理
trainX、testXの入力変数の確認、index数、columns数を確認する。
trainX.columns
# Index(['event_henkan', 'remarks_henkan', 'year', 'month', 'payday', '月', '火','水', '木', '金', 'temperature_bining'],dtype='object')
trainX.shape
# (207, 11)
testX.shape
# (40, 11)
y.shape
# (207,)
(3)Scikit-learnによる回帰分析を行う
model = LR()
model.fit(trainX,y)
result = model.predict(testX)
sample[1] = result
sample.to_csv("submit.csv",header = None,index = None)
ファイルをSIGNATEにアップデートしたところ、結果は、13.8053143となった。
(4)Chainerによる回帰分析を行う
①データ準備
pandasのDataFrame型からnumpyのndarray型に直す必要がある。
ndarray型にはvalues関数を使う。また、Chainerにおいて64bitではダメなので32ビットにする必要がある。float型64bitを32bitに変えるには、astype('f')とし、int型64bitを32bitに変えるにはastype('i')とする。(メモ:ただし、chainer回帰分析では入力変数、出力変数共に合わせないとtrainer.runでエラーになるので、x,t共にfloat型のastype('f')とする。)
x = trainX.values
x.dtype
# dtype('float32')
x = trainX.values.astype('f')
type(x)
# numpy.ndarray
x.shape
# (207, 11)
yも同様にarray, float型にする。
t = y.values
y.dtype
# dtype('int64')
t = y.values.astype('f')
t.dtyep
# dtype('float32')
t.shape
# (207,)
ここで、(207,)という結果値は「分類」の場合は問題ないが、「回帰」の場合は(207,1)
となっていないと、trainer.runのタイミングでエラーとなる(207と1列という明確な形が必要)ため、以下のようにする。
t = t.reshape(len(t),1)
t.shape
# (207, 1)
testXも対応
tx = testX.values.astype('f')
②データセットの準備
Chainerで使用するデータセットの形式に変換。
x(入力変数)と、t(出力変数(教師データ))をChainerで使えるようにするには、まず、タプルで囲い(zip関数を使う)リスト化する必要がある。タプルで囲う際は(入力変数、出力変数(教師データ))の順で囲う。
dataset = list(zip(x,t))
③訓練データと検証データに分類
入力データを分類する。訓練データを7割、検証データを3割りとする。また、分割後はint型にして整数型にしておく。以下のように分割するがデータに偏りが出るためrandom関数でランダムとする。なおシードも設定する。
len(dataset)
# 207
n_train = int(len(dataset)*0.7)
n_train
# 144
train,test = chainer.datasets.split_dataset_random(dataset,n_train,seed=0)
len(train)
# 144
len(test)
# 63
④モデルを定義する
以下のようにクラスを作成。
class NN(chainer.Chain):
def __init__(self,n_mid_units1=5,n_mid_units2=3,n_out=1):
super().__init__()
with self.init_scope():
self.fc1 = L.Linear(None,n_mid_units1)
self.fc2 = L.Linear(None,n_mid_units2)
self.fc3 = L.Linear(None,n_out)
self.bn = L.BatchNormalization(11)
def __call__(self,x):
h = self.bn(x)
h = self.fc1(h)
h = F.relu(h)
h = self.fc2(h)
h = F.relu(h)
h = self.fc3(h)
return h
上記でクラスの定義(モデルの定義)が完了したのでインスタンス化する。
加えて、モデルを計算していく(学習)際に、評価関数など進捗のレポートの機能を標準装備するL.Classifierを使う。モデルを設定した際にランダムに初期化されるので、モデル定義の前にシードを設定する(数値はなんでもいい、この場合は1とした)。
また、分類では交差エントロピー誤差を使うのに対して、回帰では平均二乗誤差を使う。
L.Classifier(nn)のnnにマウスを当てて、shift + tabを押すと、
L.Classifier(
predictor,
lossfun=,
accfun=,
label_key=-1,
)
と記載あり、lossfunとは、損失関数を表す引数。softmax_cross_entropy(交差エントロピー誤差)がデフォルトになっているので、lossfunを、mean_squeared_error(平均二乗誤差)に書き換える必要がある。
加えて、modelの中に、compute_accuracyがあるが、分類は精度を表す指標があるがこれはどれくらい100個中10個あってるといったものが精度(どれくらい正解だったか)、ただ、回帰は誤差(どれくらい予測した値と実際の値が違うのか観点)のため、accuracyという概念はないことから、accuracyをFalseに変更する。
np.random.seed(0)
nn = NN()
model = L.Classifier(nn,lossfun = F.mean_squared_error)
model.compute_accuracy = False
⑤その他設定
# optimiezeの設定
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)
# iteratorの設定
# 大体10から100で設定、サンプルが207の設定なので10バッジで20回パラメータ更新して1epoch、どのくらいのepochを設定するかは後述で設定。
batchsize = 10
train_iter = chainer.iterators.SerialIterator(train,batchsize)
test_iter = chainer.iterators.SerialIterator(train,batchsize,repeat=False,shuffle=False)
# updaterの設定
from chainer import training
updater = training.StandardUpdater(train_iter,optimizer,device=-1)
# trainerとextensionsの設定
from chainer.training import extensions
epoch = 1500
trainer = training.Trainer(updater,(epoch,'epoch'),out='result/obentou')
trainer.extend(extensions.Evaluator(test_iter,model,device=-1))
trainer.extend(extensions.LogReport(trigger=(1,'epoch')))
trainer.extend(extensions.PrintReport(['epoch','main/loss','validation/main/loss','elapsed_time']),trigger=(1,'epoch'))#trainデータに対するloss(損失関数の値))、testデータに対するloss(損失関数の値)、経過時間を出力するという意味
⑥学習の実行
trainer.run()
⑦学習結果の可視化
import json
with open("result/obentou/log")as f:
logs = json.load(f)
results = pd.DataFrame(logs)
results[["main/loss","validation/main/loss"]].plot()
二乗誤差なので、スケールを戻す。
import math
math.sqrt(loss)
⑧testデータを予測する
データの予測にあたっては、txをそのまま使うことはできない。chainerでは、(バッジサイズ、入力変数の数)の形にして、model.predictorに代入する必要がある。そのため以下を実施していmる
tx[0].shape
# (11,)
tx[0][np.newaxis].shape
# (1, 11) ←この形が必要
上記から以下のように記述する。
result=[]
for i in range (40):
x0 = tx[i].reshape(1,len(tx[i]))
with chainer.using_config('train' , False),chainer.using_config('enable_backprop',False):
#推論実施
y0_predict = model.predictor(x0)
#chainerで使う形式から、numpyに変換
y1_predict = y0_predict.array
#array([[●●●]], dtype=float32)となるため、[0][0]で取り出す
result.append(y1_predict[0][0])
ファイルをSIGNATEにアップデートしたところ、結果は、13.2649969となった。
(5)まとめ
Scikit-learnで回帰分析を行うと13.805、一方でChainerで回帰分析を行うと、13.264とわずかにChainerの方が良い結果となった。
結果として、成績は両方ともイマイチであった。恐らくもっと有効な手法があると思うが、これから掘り下げて学習していきたい。
(6)Chainerで「回帰」分析と「分類」との違いをメモ
1)pandasの形から、numpyの形に変換する際に、x(trainX)とyの型を合わせる必要がある。
(本文抜粋)pandasのDataFrame型からnumpyのndarray型に直す際にはvalues関数を使う。また、Chainerにおいて64bitではダメなので32ビットにする必要がある。float型64bitを32bitに変えるには、astype('f')とし、int型64bitを32bitに変えるにはastype('i')とする。(メモ:ただし、「回帰」は入力変数、出力変数共に合わせないとtrainer.runでエラーになるので、x,t共にfloat型のastype('f')とする。「分類」では異なっていてもエラーにならない。)
2)pandasからnumpyの型に変換した際に、明示的に2次元の型にしなければならない
(本文抜粋)ここで、(207,)という結果値は「分類」の場合は問題ないが、「回帰」の場合は(207,1)となっていないと、trainer.runのタイミングでエラーとなる(207と1列という明確な形が必要)ため、以下のようにする。
t = t.reshape(len(t),1)
t.shape
# (207, 1)
3)「分類」では交差エントロピー誤差を使うのに対して、「回帰」では平均二乗誤差を使う。また、compute_accuracyをFalseにする必要がある。
(本文抜粋)
np.random.seed(0)
nn = NN()
model = L.Classifier(nn,lossfun = F.mean_squared_error)
model.compute_accuracy = False