BETA

fastai動かしてみたLesson6①

投稿日:2019-06-07
最終更新:2019-06-10

今回はLesson6 https://course.fast.ai/videos/?lesson=6

notebookはこちら https://github.com/fastai/course-v3/tree/master/nbs/dl1

#Tabular learner
以下はTabular learnerのModelである

class TabularModel(nn.Module):  

    "Basic model for tabular data."  
    def __init__(self, emb_szs:ListSizes, n_cont:int, out_sz:int, layers:Collection[int], ps:Collection[float]=None,  
                 emb_drop:float=0., y_range:OptRange=None, use_bn:bool=True, bn_final:bool=False):  
        super().__init__()  
        ps = ifnone(ps, [0]*len(layers))  
        ps = listify(ps, layers)  
        self.embeds = nn.ModuleList([embedding(ni, nf) for ni,nf in emb_szs])  
        self.emb_drop = nn.Dropout(emb_drop)  
        self.bn_cont = nn.BatchNorm1d(n_cont)  
        n_emb = sum(e.embedding_dim for e in self.embeds)  
        self.n_emb,self.n_cont,self.y_range = n_emb,n_cont,y_range  
        sizes = self.get_sizes(layers, out_sz)  
        actns = [nn.ReLU(inplace=True) for _ in range(len(sizes)-2)] + [None]  
        layers = []  
        for i,(n_in,n_out,dp,act) in enumerate(zip(sizes[:-1],sizes[1:],[0.]+ps,actns)):  
            layers += bn_drop_lin(n_in, n_out, bn=use_bn and i!=0, p=dp, actn=act)  
        if bn_final: layers.append(nn.BatchNorm1d(sizes[-1]))  
        self.layers = nn.Sequential(*layers)

ここではKaggleのRossmann datasetを使う
https://www.kaggle.com/c/rossmann-store-sales/data
このデータセットは3000店のドラッグストアのデータからなり、どれだけ多くの製品が次の数週で売れるかを予測する事が目的である
このコンペの評価方法はRoot mearn squared percent errorである

$$RMSPE = \sqrt{\frac{1}{n}\sum_{i=1}^n{(\frac{y_i - \hat{y_i}}{y_i})^2}}$$

この講義では、時間の都合上あらかじめデータセットを使いやすい形にしたpickleが配布されている
しかしどこにあるかわからないので見つけてください

Colaboratoryで動かしてみる

%reload_ext autoreload  
%autoreload 2  
from fastai.tabular import *  

今回はpickleがどこにあるかわかんなかったので、以下のコードは適当

path = Path('path/to/pickle')  
train_df = pd.read_pickle(path/'train_clean')  

データの頭の部分を転置して読み込む
中身はこんな感じ

train_df.head().T  

Preprocesses 前処理

TransformsもPreprocessesもデータを変形する事だが、PreprocessesはTrainingを行う前に行う処理という点が違っている

idx = np.random.permutation(range(n))[:2000]  
idx.sort()  
small_train_df = train_df.iloc[idx[:1000]]  
small_test_df = train_df.iloc[idx[1000:]]  
small_cont_vars = ['CompetitionDistance', 'Mean_Humidity']  
small_cat_vars =  ['Store', 'DayOfWeek', 'PromoInterval']  
small_train_df = small_train_df[small_cat_vars + small_cont_vars + ['Sales']]  
small_test_df = small_test_df[small_cat_vars + small_cont_vars + ['Sales']]   

idxはランダムに2000個のIDを取り出している
そしてsmall_cont_varsとsmall_cat_varsで変数を5個選択している
small_train_dfとsmall_test_dfで5個の変数と目的であるSalesを加えたdfを作る
出来たのがこれ

Preprocessor:Categorify

categorify = Categorify(small_cat_vars, small_cont_vars)  
categorify(small_train_df)  
categorify(small_test_df, test=True)  

Categorifyは.classと同じことを行う
つまり、それぞれの独立変数に対してなんであるかの認識を行う事である
例えば上の図にあるPromoIntervalのFeb,May,Aug,Novといった物に行ってみると、全ての固有の値を探して、リストを作り、文字列でなく数値として扱えるようにする(0,1,2,3とか)
以下だと普通にカテゴリを返す

small_train_df.PromoInterval.cat.categories  

Index(['Feb,May,Aug,Nov', 'Jan,Apr,Jul,Oct', 'Mar,Jun,Sept,Dec'], dtype='object')

以下だとカテゴリを数値をして扱ったものを返す

small_train_df['PromoInterval'].cat.codes[:5]  

267 -1
604 -1
983 0
1636 -1
2348 -1
dtype: int8

-1はNaNであり、0などの数はカテゴリを表している

Preprocessor:Fill Missing

FillMissingをつかってみる
データフレームを呼び出して、test=trueを渡す

fill_missing = FillMissing(small_cat_vars, small_cont_vars)  
fill_missing(small_train_df)  
fill_missing(small_test_df, test=True)  

small_train_df[small_train_df['CompetitionDistance_na'] == True]  


これは何らかの欠損値を持っているデータがあれば、新たな列にTrueと表示する
CompetitionDistanceには中央値を入れている

これらのPreprocessesは上記のようにやらなくても、以下のようにすれば簡単に行える

procs=[FillMissing, Categorify, Normalize]  

data = (TabularList.from_df(df, path=path, cat_names=cat_vars, cont_names=cont_vars, procs=procs,)  
                .split_by_idx(valid_idx)  
                .label_from_df(cols=dep_var, label_cls=FloatList, log=True)  
                .add_test(TabularList.from_df(test_df, path=path, cat_names=cat_vars, cont_names=cont_vars))  
                .databunch())  

Categorical and Continuous Variables(カテゴリ変数と連続変数)

カテゴリ変数は文字列であるとは限らない、例えば何曜日だとか月の何日目だとかであり、それが数値で表されていてもカテゴリ変数として扱える
以下のようにそれぞれを指定

cat_vars = ['Store', 'DayOfWeek', 'Year', 'Month', 'Day', 'StateHoliday', 'CompetitionMonthsOpen',  
    'Promo2Weeks', 'StoreType', 'Assortment', 'PromoInterval', 'CompetitionOpenSinceYear', 'Promo2SinceYear',  
    'State', 'Week', 'Events', 'Promo_fw', 'Promo_bw', 'StateHoliday_fw', 'StateHoliday_bw',  
    'SchoolHoliday_fw', 'SchoolHoliday_bw']  

cont_vars = ['CompetitionDistance', 'Max_TemperatureC', 'Mean_TemperatureC', 'Min_TemperatureC',  
   'Max_Humidity', 'Mean_Humidity', 'Min_Humidity', 'Max_Wind_SpeedKm_h',   
   'Mean_Wind_SpeedKm_h', 'CloudCover', 'trend', 'trend_DE',  
   'AfterStateHoliday', 'BeforeStateHoliday', 'Promo', 'SchoolHoliday']  

今回の最終的なデータフレームはカテゴリ変数、連続変数、独立変数と時間を持つTraining datasetである
以下で指定

dep_var = 'Sales'  
df = train_df[cat_vars + cont_vars + [dep_var,'Date']].copy()  

データセットの一番古いデータと一番新しいデータの時間を表示

test_df['Date'].min(), test_df['Date'].max()  

('2015-08-01', '2015-09-17')

train_dfのindexの最大値を取得

cut = train_df['Date'][(train_df['Date'] == train_df['Date'][len(test_df)])].index.max()  
cut  

41395

valid_idx = range(cut)  
df[dep_var].head()  

0 5263
1 6064
2 8314
3 13995
4 4822
Name: Sales, dtype: int64

databunchを作成

data = (TabularList.from_df(df, path=path, cat_names=cat_vars, cont_names=cont_vars, procs=procs,)  
                .split_by_idx(valid_idx)  
                .label_from_df(cols=dep_var, label_cls=FloatList, log=True)  
                .add_test(TabularList.from_df(test_df, path=path, cat_names=cat_vars, cont_names=cont_vars))  
                .databunch())  

y_range

max_log_y = np.log(np.max(train_df['Sales'])*1.2)  
y_range = torch.tensor([0, max_log_y], device=defaults.device)  

今回の目的の値はSalesである
ここで、前回の映画の評価と同様に少し取りうる値を広くとるようにする

learn = tabular_learner(data, layers=[1000,500], ps=[0.001,0.01], emb_drop=0.04,   
                        y_range=y_range, metrics=exp_rmspe)  

全結合のネットワークを使う
ここで、中間の重み行列はlayersでは1000のactivationを500のactivationにする
これはつまり500000個の要素を含む重み行列であるという事である
データセットに2~300くらいの行しかない場合にはこのようにするとひどいことになる
そこで、正則化を行う事で変数の数を減らすことなく精度を上げる方法がある
fastaiではデフォルトでWeight Decayが入っている
今回は上記でpsで指定されているdropoutについて学ぶ

Dropout


(a)で示される左の図が普通のニューラルネットであり、全結合である
(b)で示される右の図は、dropoutを適用したニューラルネットであり、指定した割合のactivationを使わないでおくことである

殆どのfastaiのLearnerには、psという各レイヤーのdropoutのためのpという値を持っている
psに複数の値を設定すれば、層ごとに異なるdropoutを行う事ができる

Dropoutはtestを実行するときは使われない

learn = tabular_learner(data, layers=[1000,500], ps=[0.001,0.01], emb_drop=0.04,   
                        y_range=y_range, metrics=exp_rmspe)  

こうすると、初めの層は0.001でdropoutを行い、次の層では0.01でdropoutを行う
そしてここでembedding層のdropoutについて見てみる

class TabularModel(nn.Module):  

    "Basic model for tabular data."  
    def __init__(self, emb_szs:ListSizes, n_cont:int, out_sz:int, layers:Collection[int], ps:Collection[float]=None,  
                 emb_drop:float=0., y_range:OptRange=None, use_bn:bool=True, bn_final:bool=False):  
        super().__init__()  
        ps = ifnone(ps, [0]*len(layers))  
        ps = listify(ps, layers)  
        self.embeds = nn.ModuleList([embedding(ni, nf) for ni,nf in emb_szs])  
        self.emb_drop = nn.Dropout(emb_drop)  
        self.bn_cont = nn.BatchNorm1d(n_cont)  
        n_emb = sum(e.embedding_dim for e in self.embeds)  
        self.n_emb,self.n_cont,self.y_range = n_emb,n_cont,y_range  
        sizes = self.get_sizes(layers, out_sz)  
        actns = [nn.ReLU(inplace=True) for _ in range(len(sizes)-2)] + [None]  
        layers = []  
        for i,(n_in,n_out,dp,act) in enumerate(zip(sizes[:-1],sizes[1:],[0.]+ps,actns)):  
            layers += bn_drop_lin(n_in, n_out, bn=use_bn and i!=0, p=dp, actn=act)  
        if bn_final: layers.append(nn.BatchNorm1d(sizes[-1]))  
        self.layers = nn.Sequential(*layers)  

    def get_sizes(self, layers, out_sz):  
        return [self.n_emb + self.n_cont] + layers + [out_sz]  

    def forward(self, x_cat:Tensor, x_cont:Tensor) -> Tensor:  
        if self.n_emb != 0:  
            x = [e(x_cat[:,i]) for i,e in enumerate(self.embeds)]  
            x = torch.cat(x, 1)  
            x = self.emb_drop(x)  
        if self.n_cont != 0:  
            x_cont = self.bn_cont(x_cont)  
            x = torch.cat([x, x_cont], 1) if self.n_emb != 0 else x_cont  
        x = self.layers(x)  
        if self.y_range is not None:  
            x = (self.y_range[1]-self.y_range[0]) * torch.sigmoid(x) + self.y_range[0]  
        return x  

TabularModelを見てみる
forwardの中を見てみると、まずそれぞれのembeddingを呼び出し、embeddingを1列の行に変換する
そしてdropoutを行う
continuous variableにはdropoutは行わない
なぜならそのままdropoutを行うとinputをそのまま消すという事になるからである
embeddingのdropoutはembedding後のoutputに対して行う

モデルの中身を見てみる

learn.model  

TabularModel(
(embeds): ModuleList(
(0): Embedding(1116, 81)
(1): Embedding(8, 5)
(2): Embedding(4, 3)
(3): Embedding(13, 7)
(4): Embedding(32, 11)
(5): Embedding(3, 3)
(6): Embedding(26, 10)
(7): Embedding(27, 10)
(8): Embedding(5, 4)
(9): Embedding(4, 3)
(10): Embedding(4, 3)
(11): Embedding(24, 9)
(12): Embedding(9, 5)
(13): Embedding(13, 7)
(14): Embedding(53, 15)
(15): Embedding(22, 9)
(16): Embedding(7, 5)
(17): Embedding(7, 5)
(18): Embedding(4, 3)
(19): Embedding(4, 3)
(20): Embedding(9, 5)
(21): Embedding(9, 5)
(22): Embedding(3, 3)
(23): Embedding(3, 3)
)
(emb_drop): Dropout(p=0.04)
(bn_cont): BatchNorm1d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(layers): Sequential(
(0): Linear(in_features=233, out_features=1000, bias=True)
(1): ReLU(inplace)
(2): BatchNorm1d(1000, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.001)
(4): Linear(in_features=1000, out_features=500, bias=True)
(5): ReLU(inplace)
(6): BatchNorm1d(500, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): Dropout(p=0.01)
(8): Linear(in_features=500, out_features=1, bias=True)
)
)~~~

それぞれのembedding行列はそれぞれの入力の次元を教えてくれる
これらはcat_varsで一致する事が出来る
1つ目の数(1116)はStoreを表しており、1116店舗あるという事がわかる
2つ目の数(50)はembeddingの大きさを表している
embedding droprout層を得て、加えて16のinputを持つbatch norm層を得た
16というのは16個の連続変数を表している

forwardの中身を見てみよう

def forward(self, x_cat:Tensor, x_cont:Tensor) -> Tensor:  
    if self.n_emb != 0:  
        x = [e(x_cat[:,i]) for i,e in enumerate(self.embeds)]  
        x = torch.cat(x, 1)  
        x = self.emb_drop(x)  
    if self.n_cont != 0:  
        x_cont = self.bn_cont(x_cont)  
        x = torch.cat([x, x_cont], 1) if self.n_emb != 0 else x_cont  
    x = self.layers(x)  
    if self.y_range is not None:  
        x = (self.y_range[1]-self.y_range[0]) * torch.sigmoid(x) + self.y_range[0]  
    return x  

cont_namesの長さは16であり、この16個の変数はforwardのbn_contとして使われる
bn_contはnn.Batchnormidである
これは正則化と訓練をやりやすくするものである

Batch Norm(バッチ正則化)

Batch Normなしの場合とありの場合でLossの比較を見てみよう

Batch normが無い場合(赤色)だと、Lossの変化が激しい
Batch normを行う事によってそれを減らすことが出来、これによって学習率を上げて学習する事が可能になる

これがBatch normのアルゴリズムである
このアルゴリズムはミニバッチを用いる
そしてこれは層の事であり、それに入ってくるのはactivationsである
そこで、ここでは、activationsをx1, x2, x3 ,... として扱う
まず初めに、ミニバッチの平均をとる(Outputの一つ目の奴:μ)
次に分散を求める(σ^2)
そして正規化を行う(xi_hat)
最後に上記の値を用いて、バイアスのベクトルを追加し、γを掛ける(yi)
ニューラルネットには2種類の数しかない
1つはparameterで、gradient decentによって学習される
βはnormal biasの層であり、γは乗算可能なbias層である
$$\hat{y} = f(w_1, w_2, ..., w_{100000}, \vec{x})$$
この関数fはニューラルネットで何が起こっていてもこの通りである
$$L = \sum{(y-\hat{y})^2}$$
LossをMSEで求めるとしたら上記の式になる
映画評価の結果を予測しようとしていて、1~5の間になるとする
現在-1~1の間にモデルとactivationがなるようにtrainしようとしている
ここで行える事として、スプレッドを増加させ平均を増加させる新しいWeightを試すという事がある
$$\hat{y} = f(w_1, w_2, ..., w_{100000}, \vec{x}) × g + b$$
gは勾配である
平均を変えるために、bは平均を変えるための直接的な勾配を持っている
Batch normは基本的にoutputを上下させたり入れたり出したりすること簡単にするために行っている

class TabularModel(nn.Module):  

    "Basic model for tabular data."  
    def __init__(self, emb_szs:ListSizes, n_cont:int, out_sz:int, layers:Collection[int], ps:Collection[float]=None,  
                 emb_drop:float=0., y_range:OptRange=None, use_bn:bool=True, bn_final:bool=False):  
        super().__init__()  
        ps = ifnone(ps, [0]*len(layers))  
        ps = listify(ps, layers)  
        self.embeds = nn.ModuleList([embedding(ni, nf) for ni,nf in emb_szs])  
        self.emb_drop = nn.Dropout(emb_drop)  
        self.bn_cont = nn.BatchNorm1d(n_cont)  
        n_emb = sum(e.embedding_dim for e in self.embeds)  
        self.n_emb,self.n_cont,self.y_range = n_emb,n_cont,y_range  
        sizes = self.get_sizes(layers, out_sz)  
        actns = [nn.ReLU(inplace=True) for _ in range(len(sizes)-2)] + [None]  
        layers = []  
        for i,(n_in,n_out,dp,act) in enumerate(zip(sizes[:-1],sizes[1:],[0.]+ps,actns)):  
            layers += bn_drop_lin(n_in, n_out, bn=use_bn and i!=0, p=dp, actn=act)  
        if bn_final: layers.append(nn.BatchNorm1d(sizes[-1]))  
        self.layers = nn.Sequential(*layers)  

    def get_sizes(self, layers, out_sz):  
        return [self.n_emb + self.n_cont] + layers + [out_sz]  

    def forward(self, x_cat:Tensor, x_cont:Tensor) -> Tensor:  
        if self.n_emb != 0:  
            x = [e(x_cat[:,i]) for i,e in enumerate(self.embeds)]  
            x = torch.cat(x, 1)  
            x = self.emb_drop(x)  
        if self.n_cont != 0:  
            x_cont = self.bn_cont(x_cont)  
            x = torch.cat([x, x_cont], 1) if self.n_emb != 0 else x_cont  
        x = self.layers(x)  
        if self.y_range is not None:  
            x = (self.y_range[1]-self.y_range[0]) * torch.sigmoid(x) + self.y_range[0]  
        return x  

Batch normについて学んだので、全ての連続変数に対してBatch norm層を作る
n_contは連続変数の数である
fastaiの中では、n_ほにゃらら となっている奴はほにゃららの数を表している
contは連続変数を表している
forwardの中のself.n_cont != 0のところで、連続変数をBatch norm層に渡している

モデルの中身をもう一度見てみる

learn.model  

TabularModel(
(embeds): ModuleList(
(0): Embedding(1116, 81)
(1): Embedding(8, 5)
(2): Embedding(4, 3)
(3): Embedding(13, 7)
(4): Embedding(32, 11)
(5): Embedding(3, 3)
(6): Embedding(26, 10)
(7): Embedding(27, 10)
(8): Embedding(5, 4)
(9): Embedding(4, 3)
(10): Embedding(4, 3)
(11): Embedding(24, 9)
(12): Embedding(9, 5)
(13): Embedding(13, 7)
(14): Embedding(53, 15)
(15): Embedding(22, 9)
(16): Embedding(7, 5)
(17): Embedding(7, 5)
(18): Embedding(4, 3)
(19): Embedding(4, 3)
(20): Embedding(9, 5)
(21): Embedding(9, 5)
(22): Embedding(3, 3)
(23): Embedding(3, 3)
)
(emb_drop): Dropout(p=0.04)
(bn_cont): BatchNorm1d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(layers): Sequential(
(0): Linear(in_features=233, out_features=1000, bias=True)
(1): ReLU(inplace)
(2): BatchNorm1d(1000, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(3): Dropout(p=0.001)
(4): Linear(in_features=1000, out_features=500, bias=True)
(5): ReLU(inplace)
(6): BatchNorm1d(500, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(7): Dropout(p=0.01)
(8): Linear(in_features=500, out_features=1, bias=True)
)
)

momentumが(bn_cont): BatchNorm1d(16, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)の中にある事がわかる
これはmomentumがoptimizationの様な物ではなく、指数加重移動平均の様な物であるという事である

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

大学生の書きなぐりブログ 間違ってる事も書いてるので自己責任で勉強しましょう

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!