機械学習エンジニアの備忘録

主に自分が勉強したことのメモ

アンサンブル学習 アルゴリズム入門 〜〜決定木その1〜〜

アンサンブル学習とは

アンサンブル学習とは、複数の機械学習モデルを組み合わせて使用するタイプの機械学習アルゴリズムのことです。
Kaggleのようなデータ分析コンペティションでもよく使われているLightGBMなどもアンサンブル学習アルゴリズムの一つです。

この記事のシリーズは2019年6月に出版された「作ってわかる!アンサンブル学習 アルゴリズム入門」の内容に沿っています。

作ってわかる! アンサンブル学習アルゴリズム入門

作ってわかる! アンサンブル学習アルゴリズム入門

この本では、Pythonを使用して、複数のアンサンブル学習アルゴリズムを1からスクラッチで作成します。

アンサンブル学習の系譜

アンサンブル学習の中にもいくつかの種類があり、基本的な手法として、バギング、ブースティング、スタッキングなどの手法が知られています。
さらに、ブースティングとして分類されるアルゴリズムの中にも、AdaBoost系のある、勾配ブースティング系のアルゴリズムなどと複数の種類があります。

アンサンブル学習のアルゴリズムでは、一般的に決定木アルゴリズムをベースのアルゴリズムとして使用するので、本シリーズでも基本的に決定木アルゴリズムをベースとして採用します。

決定木

決定木アルゴリズムは、条件による分岐を根からたどることで、最も条件に合致する葉を検索するというアルゴリズムで、IF-THENルーチンと同じです。
機械学習における決定木は、学習データをもとにして、説明変数からなる条件式をノードとすることで説明変数に対するモデルを含む葉を検索することになります。

枝と葉

決定木アルゴリズムで作成されるモデルは、ある一つのノードが根となり、そのノードから伸びる複数の枝が、次のノードまたは葉を示す構造となっています。

f:id:rikeiin:20190923112813p:plain
https://datachemeng.com/decisiontree/

決定木を使用した機械学習では、各ノードには、からなる条件式が入ることになります。そうすると、ある入力に対して決定木をたどっていけば、いずれかの葉にたどり着くことになります。

決定木によるデータの分割

言い換えるとデータセットに含まれるデータは、全ていずれかの葉に属することになり、このことは決定木はデータを葉の数に分割するということを表しています。そしてデータを葉の数に分割し、それぞれの葉でクラス分類や回帰のモデルを適用することで全体としてより望ましいモデルを構築することが決定木アルゴリズムの目的です。

決定木にはそれが単体が機械学習モデルというわけではなく、基本的にはデータの分割に関するアルゴリズムであり、葉となる機械学習モデルと組み合わせて利用されます。

葉の部分で利用できるモデルはデータの部分集合に対して学習と実行行えるモデルであれば何でも良いのですが決定木の葉の部分にあまり複雑なモデルを使用することは一般的には行われません。本記事ではZeroRule(クラス分類であれば「学習時に最も多かったクラス」、回帰であれば「学習した正解データの平均値」)と線形回帰モデルという二つのアルゴリズムをモデルとして使用します 。

木の分割

決定木による分割それ自体も機械学習アルゴリズムの一種であり学習データからモデルを作成します。決定木の学習ではまず深さの浅い決定木を作成しその葉の部分に新しいノードを追加していくことで順次より深さの深い決定木を作成しますこのような葉を新しいノードとしてデータを分割していくことを木の分割と呼びます 。

f:id:rikeiin:20190923114951p:plain
http://codecrafthouse.jp/p/2014/09/decision-tree/

上記の決定木ではそれぞれのノードでは一度に説明変数内の一つの値しか見ていません。つまりノード内にある条件式は横軸または縦軸のどちらかの値によって処理を振り分けるので図中の全ての分割線は水平または垂直になっています。そのように単純な条件式のみからなるのノードでも、気の深さを深くしてやることで複雑なデータでも分割を可能とするのが決定木アルゴリズムの特徴です。

Metics関数

決定木アルゴリズムでは説明変数からなる条件式で最もよく目的変数を分割できる条件をもとにノードを作成します。ここで「最もよく」目的変数を分割するために目的変数の分割の良さを数値で比較できるスコアが必要になりますが、そのために使用するのが損失関数またはMetrics関数と呼ばれる関数になります(本記事では以後Metics関数と呼びます)。この関数でノード内の条件式を評価し、最もよく目的変数を分割できるように学習を行います。

よく用いられるMetics関数にはいくつか種類があります。

標準偏差

回帰に使用されるMetics関数として標準偏差の合計が挙げられます。これは目的変数を分割する際に、分割後のグループごとの標準偏差を取り、その合計が少ないほどよくデータ分割できているとするものです。
ここで作成するMetics関数は「entropy.py」という名前のファイルに作成して、後で紹介する決定木アルゴリズムでインポートして使用します。

import numpy as np
def deviation(y):
    return y.std()
Gini impurity (ジニ不純物)

クラス分類では目的変数はクラスを表す番号であり、値そのものの比較には意味がないので与えられた目的変数のリスト内がどのくらい単一のクラスからなっているかを評価することになります。
そのために使用されるのがGini impurityです。経済学で使われるジニ係数 (Gini coefficient)とは異なるので注意してください。

nをデータに含まれるクラスの個数、p_iをデータがクラスiである確率とするとGini impurityは以下の式で表されます。

\displaystyle{
Gini(p) = \sum_{i=1}^n p_i (1-p_i)=\sum_{i=1}^n p_i - \sum_{i=1}^n p_i^2 = 1 - \sum_{i=1}^n p_i^2
}

[決定木]ジニ不純度と戯れる - Qiita

上記の式をpythonで実装すると下記のようになります。

def gini(y):
    # compute gini impurity
    m = y.sum(axis=0)
    size = y.shape[0]
    e = (m / size) ** 2
    return 1.0 - np.sum(e)
Information gain

クラス分類で使用される metrics 関数としてInformation Gainもあります。
これは情報理論で使用するエントロピーに基づく関数で、分割後の目的変数に含まれる情報量が少なくなるように分割点を求めます。

Information Gainの正しい定義は、元の集合のエントロピーから分割後の集合のエントロピーの重み付き合計を引いた値でその値が大きくなるような分割を目指します。
決定木の場合親ノードから与えられたデータのエントロピーから子ノードに渡すデータのエントロピーの合計を引いたものがノード内の条件式が持っている情報量ということになるので、Information gainはノード内の条件式が持つ情報量を最大化するようにデータを分割するMetics関数とも言えます。
ここでは決定木のノードにノートの一つの分割を考えるので単純に分割後のエントロピーの合計が小さくなるように目的変数を分割します 。

エントロピーは乱雑度とも考えられるので、乱雑度を小さくするように分割する、と考えてもいいかもしれません。

情報量 - Wikipedia
情報理論の基礎~情報量の定義から相対エントロピー、相互情報量まで~ | Logics of Blue

nをデータに含まれるクラスの個数、p_iをデータがクラスiである確率とするとエントロピーは以下の式で表されます。

\displaystyle{
Entropy(p) = - \sum_{i=1}^n p_i \log_2 (p_i)
}

上記の式をpythonで実装すると下記のようになります。

def infgain(y):
    m - y.sum(axis=0)
    size = y.shape[0]
    e = [p * np.log2(p / size) / size for p in m if p != 0.0]
    return -np.sum(e)

scipyでも計算できます。
【python】pythonで情報エントロピーの計算 - 静かなる名辞

DecisionStump

DecisionStumpとはアルゴリズムの評価に使用される最もシンプルな構造をした決定木で、深さが1の決定木のことです。
DecisionStumpにはノードが一つと葉が二つの要素しかありませんが、データの分割と葉によるモデルの結合という決定木アルゴリズムの基礎が含まれており、データの分割アルゴリズムなども評価に用いられます。
またDecisionStumpはアンサンブル学習のアルゴリズムを評価する際のベースにも使用されます。
アルゴリズムを評価する際のベースとしては複雑なモデルを使用してもベースのモデルの性能として良い結果が出ているのか、アンサンブル学習アルゴリズムの性能としていい結果が出ているのかわからないため、最もシンプルな構造をした決定木であるDecisionStumpが使用されます。

木分割の実装

実際にDecisionStumpの実装を行っていきます。
dstump.pyというファイルに下記クラスを実装します。

class DecisionStump:
    def __init__(self, metric=entropy.gini, leaf=ZeroRule):
        self.metric = metric # 使用するMetics関数
        self.leaf = leaf # 葉のモデル
        self.left = None # 左の葉のモデルインスタンス
        self.right = None # 右の葉のモデルインスタンス
        self.feat_index = 0 # 分割に使用する目的変数の位置を表す
        self.feat_val = np.nan # 分割に使用する目的変数の値を表す
        self.score = np.nan # 分割の際のMetics関数の値を表す(今後使用)

左右のインデックスを取得する

上記のDecisionStumpクラス内に決定木アルゴリズムに必要となる機能を実装します。
まず作成するのは、目的変数から取得した値の配列を特定の値より小さなものとそれ以外に分ける関数で「make_split」という名前の関数を作成します。
この関数は1次元の数値からなる配列を与えられた値で分割した際のインデックスを返します 。

def make_split(self, feat, val):
        # featをval以下と以上で分割するインデックスを返す
        left, right = [], []
        for i, v in enumerate(feat):
            if v < val:
                left.append(i)
            else:
                right.append(i)
        return left, right

分割した後のスコアを計算する

次に作成するのは記事上部で作成したMetics関数を使用して、分割した後のスコアを計算する「make_loss」という名前の関数です。
この関数では「self.metric」に代入されているMetics関数でそのスコアの重み付き合計を求めます。
関数の引数は左右に分割した目的変数となります。

def make_loss(self, y1, y2):
        y1_size = y1.shape[0]
        y2_size = y2.shape[0]
        if y1_size == 0 or y2_size == 0:
            return np.inf
        totol = y1_size + y2_size
        m1 = self.metric(y1) * (y1_size / totol)
        m2 = self.metric(y2) * (y2_size / totol)
        return m1 + m2

データを左右に分割する

次に作成するのは説明変数と目的変数からデータを左右の枝に振り分ける「split_tree」関数です。
この関数では説明変数内の全ての次元に対して、その中の値でデータを分割した際のスコアを計算します。
そして、そのスコアが最も小さくなる説明変数内の次元の位置と分割値を「self.feat_index」と「self.feat_val」変数に保存しておきます 。
最後に、分割した後の左右の葉に振り分けられるデータのインデックスを返します。

def split_tree(self,x, y):
        # データを分割して左右の枝に属するインデックスを返す。
        self.feat_index = 0
        self.feat_val = np.inf
        score = np.inf
        left, right = list(range(x.shape[0])), []
        # 説明変数内のすべての次元に対して
        for i in range(x.shape[1]):
            feat = x[:, i]
            for val in feat:
                # 最もよく分割する値を探す
                l, r = self.make_split(feat, val)
                loss = self.make_loss(y[l], y[r])
                if score > loss:
                    score = loss
                    left = l
                    right = r
                    self.feat_index = i
                    self.feat_val = val
        self.score = score
        return left, right

学習部分の実装

DecisionStumpは深さが1の決定木なので、学習のためのコードはデータを左右の端に振り分けてそれぞれの葉の学習を行うだけです。
self.leaf変数に葉となるモデルが入っているので、それを呼び出してインスタンス化しておき、 split_tree()で左右の端に振り分けたデータを学習させます。
また、必ずしも左右に値が振り分けられるとは限らず、どちらか一方の端にのみデータが集中する可能性もあるので if 文でデータの長さをチェックしてから学習を行います。

def fit(self, x, y):
        # 左右の葉のモデルをインスタンス化
        self.left = self.leaf()
        self.right = self.leaf()

        # データを左右の葉に振り分ける
        left, right = self.split_tree(x, y)
        # 左右の葉を学習
        if len(left) > 0:
            self.left.fit(x[left], y[left])
        if len(right) > 0:
            self.right.fit(x[right], y[right])
        return self

推論部分の実装

DecisionStumpの推論は、左右の端へデータを振り分けて、それぞれの葉のモデルの実行結果から最終的な出力を作成します。
データを左右の端に振り分けるのはmake_split()を使用し、左右ともにデータがあれば左右の葉の実行結果をそのインデックスに代入して最終的な結果とします。
また左右のどちらかのみにデータがある場合は左右の葉のいずれかの実行結果が最終的な結果となります 。

def predict(self, x):
        feat = x[:, self.feat_index]
        val = self.feat_val
        l, r = self.make_split(feat, val)
        z = None
        if len(l) > 0 and len(r) > 0:
            left = self.left.predict(x[l])
            right = self.right.predict(x[r])
            z = np.zeros((x.shape[0], left.shape[1]))
            z[l] = left
            z[r] = right
        elif len(l) > 0:
            z = self.left.predict(x)
        elif len(r) > 0:
            z = self.right.predict(x)
        return z

DecisionStumpの評価

以上でDecisionStumpの実装ができました。
検証用データに対して学習して評価するコードは下記にあるので参考にしてみてください。

DecisionStump全体のコードはこちら

次の記事では今作ったDecisionStumpクラスを利用して深さが1より大きい決定木アルゴリズムを実装していきたいと思います。

rikeiin.hatenablog.com

Kaggleの網膜コンペで銅メダルをとったので振り返る

Kaggle で開催されたAPTOS 2019 Blindness Detection(網膜コンペ)にソロで参加したのでその振り返りです。

www.kaggle.com

結果は174th/2943 (TOP6%)で銅メダルでした。このコンペで2枚目の銅メダルを獲得し、Kaggle Expertになることができました。

f:id:rikeiin:20190917104024p:plain

コンペ概要

網膜の画像から糖尿病網膜症の重症度を予測します。
ラベルは0〜4の5段階で数字が大きいほど重症を表しています。

糖尿病網膜症については以下のスライドが分かりやすいです。

評価指標はquadratic weighted kappaです。
またkernel only コンペだったので学習は手元でOKですが推論はkernel上で完結させる必要があり、以下にkernelの制限時間内に推論できるかもこのコンペのポイントでした。

自分の手法

Validation Strategy

2015年にも同様のコンペ (APTOS2015)が行われており、そのデータと現コンペ (APTOS2019)のデータを単純に結合して学習データとしました。discussionでは2015のデータでpretrainして2019のデータでfine-tuningする手法が出ていましたが、めんどくさかったので単純に結合して使いました。
検証データは訓練データと同じラベルの分布になるように分割。最初は5-foldでCVしていましたが、時間がかかるので中盤からはsigle fold で学習させました。

あとの祭りですが、external dataとしてAPTOS2015だけでなくIDRIDやMessidorのような他のデータもあったようでそちらも使うべきだったなあと反省…。

preprosessing

前回コンペの優勝者が使用していた前処理がkernelで公開されていたのでそちらのコードを丸コピして使わせてもらいました。
処理内容は黒の背景部分を自動でcropし、ガウシアンフィルタをかけるというものです。

ガウシアンフィルタの強さを3段階くらい試してその中で最もlocal CVのスコアが高いものを使用しました。

画像サイズは256, 340, 512でいろいろ試したが画像サイズを増やすとoverfitする傾向があったので256 or 340の小さめのサイズでやっていました。

Augmentation

以下を使用しました。

  • Flip
  • Rotate
  • ShiftScaleRotate
  • RandomBrightness
  • RandomContrast
  • RandomGamma

後で上位の解法を見ると相当heavyな拡張をしていたのでもっといろいろ試しても良かったかも。

Models

CNNのモデルはResnet34, Resnet50, SE-ResNext50, Efficient-Netなどいろいろ試しました。
Efficient-Netは最近GoogleがNeural Architecture Searchで発見したモデルの構造で既存モデルより少ないパラメータで精度が良いようです。
実際、上位の解法の多くがEfficient-Netを使用していました。

ai.googleblog.com

Efficient-Netはパラメータの数によってB0~B7の7段階あり、自分は学習済みモデルが公開されていたB0~B5まで試してローカルのスコアが最も高かったB3を使用しました。

Training

discussionではロスをMSEとして回帰問題として解いているものが多かったですが、自分は最初あえてCrossEntropyLossを使った分類問題として解いていました。最終的には回帰として解く方法も試しました。
optimizerはAdam+CosineAnnealingを使用。
また、discussionで同じ画像でも異なるラベルがついていることがわかったりとラベル付けが適当な感じあったのでミスラベリング&汎化性能向上のためにLabelSmoothingを試してみました。
Label Smoothing: An ingredient of higher model accuracy

実際LabelSmoothingによってスコアが0.05ほど上がりました。

inference

最終的には以下の4つのモデルのアンサンブルした結果で提出しました。

  • Classificationとして解いたEfficient-NetB3で画像サイズを3種類で学習したモデル(size256, 340, 512)
  • Regressionとして解いたEfficient-NetB3で画像サイズ320(しきい値はローカルスコアで最適化)

自分のkernelの単純なコードだと制限時間で4回推論するのが限界だったんですが他の解法を見るとさらに多くのアンサンブルをしているものもあったのでここは工夫してもっと短時間で大量に推論してアンサンブルできるようにするべきだったかもしれません。

結果

Public LB: 0.808829, Private LB 0.918672で無事銅メダル圏内に残ることができました。
QWKで0.9超えるってどういう問題なんだ…

上位ソリューションのまとめ

1st place solution

1st place solution summary | Kaggle

  • 2015+2019のデータを学習データとして使用。Public LBをvalidationとして信頼した。
  • 特別な前処理なく単純なresizeのみ ← !!
  • 以下8つのアンサンブル
    • 2 x inception_resnet_v2, input size 512
    • 2 x inception_v4, input size 512
    • 2 x seresnext50, input size 512
    • 2 x seresnext101, input size 384
  • nn.SmoothL1Loss()
  • 最後のプーリング層をGeneralized mean poolingに変更
  • pseudo labeing

2nd place solution

4th place solution (2nd after LB cleaning) | Kaggle

  • APTOS2015でpretrained
  • 以下3つのモデル
    • Efficient-Net B3 image size 300
    • Efficient-Net B4 image size 460
    • Efficient-Net B5 image size 456
  • 黒背景をクロップしてサイズを変えるだけの単純な前処理
  • a lot of augmentation
  • pseudo labeling
  • Test Time Augmentation (TTA)

4th place solution

4th place solution | Kaggle

  • 黒背景除去&学習データの画像サイズごと3種類に分け、それぞれに対応した前処理
  • Effcient-Net B2~B7
  • heavy augmentation
  • APTOS2015で事前学習、2019で5-fold
  • 5 model x 8 TTA
    • 前処理を1回しかしないことで推論時間を短縮

5th place solution

5th place solution | Kaggle

  • APTOS2015でpretrained、2019でfine-tuning
  • 分類と回帰のマルチタスク学習 ← !!
  • Efficient-Net B0~B3
  • 2 TTA
  • pseudo labeling

7th place solution

10th place solution w/ code [Catalyst, Albumentations] | Kaggle

コードも公開されていました。
GitHub - BloodAxe/Kaggle-2019-Blindness-Detection

  • APTOS2015でpretrained、2019でfine-tuning. 外部データとしてidrid&Messidor使用
  • SeResNext50, SeResNext101, InceptionV4
  • 特殊な回帰として解く
    • 最終層でsigmoidを通した4要素のベクトルを予想し合計をとる
    • MSE
  • Loss function
    • focal kappa > soft CE(label smoothing) > plain CE
    • Regression: WingLoss
    • Ordinal Regression: Huber loss (aka smooth L1 loss) -> Cauchy Loss (?)
  • mixtured precision FP16 (Apex)
  • pseudo labeling
  • RAdam
  • 4 model ensumble

9th place solution

12th place solution | Kaggle
9th place solution | Kaggle
APTOS 2019 Blindness Detection - ALOHA

  • APTOS2015で事前学習、2019で4-fold
  • 前処理はBen's croppingをベースにアスペクト比を維持するようなresize
  • Efficientnet-B7 and SE-Resnext 50
  • 4 TTA
  • duplicate imageのラベルを平均するような処理?
  • blending
    • 12個の異なるモデルの4-hold x 4 TTA(計192)
    • どうやったらカーネルの制限時間で192回も推論できるんだろう…

10th place solution

13th place solution | Kaggle

日本人の方がブログで手法を公開してくれています。
nmaviv.hatenablog.com

反省

上位のソリューションを見るとほとんどがpseudo labeling を使用していました。このコンペはtrainとtestの分布がかなり違ったのでpseudo labelingでtestの情報を与えることが重要だったのだと思います。
まだ自分はコンペでpseudo labelingをしたことがないので次回はしっかり習得したい使ってみたい。

また、Grad-CAMのような手法をつかってデータセットや前処理の手法を分析をしているカーネルがあったりしてとても勉強になりました。今後は自分もそういう分析ができるようになりたいですね。

何はともあれKaggle Expertに上がれてよかったです。
ymicky | Kaggle

次はNIPSコンペ、鉄鋼コンペでやろうと思います。

参考

28thの方も手法を公開してくれています。
speakerdeck.com

docker imageの保存先を変更する

最近自分はdeep learningやデータサイエンスの環境構築も基本的にdockerで行うようにしています。
dockerfileさえあれば他のVMでも簡単に同じような環境が作れるので重宝しています。

しかし、docker imageって何気にサイズがでかいのでストレージの容量がすくない端末やVMだとimageをたくさん作ったときにすぐ容量がなくって辛いです…

そこで今回はdocker imageの保存先を変えてみたいと思います。

docker imageの情報はデフォルトの設定を変えていなければ基本的に /var/lib/docker/配下に保存されます。
この保存先のパスをマウントされたディスクなどに変更してあげればいいわけです。

docker の環境ファイルの場所を確認

docker info で環境ファイルの場所を確認できます。

$ docker info
Containers: 16
 Running: 4
 Paused: 0
 Stopped: 12
Images: 53
Server Version: 17.12.0-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: nvidia runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 89623f28b87a6004d4b785663257362d1658a729
runc version: b2567b37d7b75eb4cf325b77297b140ea686ce8f
init version: 949e6fa
Security Options:
 apparmor
 seccomp
  Profile: default
Kernel Version: 4.13.0-1008-gcp
Operating System: Ubuntu 16.04.3 LTS
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 196.8GiB
Name: kirikei-gpu-2p100
ID: Y2TO:YVPN:Q5CC:CE2U:D36N:LAPL:ETKB:2CWC:6FHM:43TH:IWK2:UVTW
Docker Root Dir: /var/lib/docker   #環境ファイルの保存先
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false


それでは/var/lib/docker/を覗いてみましょう。

$sudo ls /var/lib/docker
builder  containerd  containers  image  network  overlay2  plugins  runtimes  swarm  tmp  trust  volumes

$sudo ls /var/lib/docker/containers
05bdbbd0ba717e5fa9c1278a36d4cadfd21c086e04cd2bf2f04cb1c80b7cb601  6a5be04d34dfe04c4d20c7085a2a7c79e6293917b1a34e746fb48beb16cab05a
29182f0a853741eae26c35a00acaff15e0ada8654b2c28b07ce5b799b6114a91  74d5762a9ab6a889f3a0742d226ee0dae4bc5bbc3b8a0b44ee89f3f9fd2e4d7c
3da8d78d42aa0be6331a462f3c1b19ad12e98ce84a35c90ef2363e91fa6f8753  84296104a53777287ac81126ea54353d216ac945e3767747fc0100f196cf3d6a
3f826eecd54f3f5c4f4669ca32da74cf6242d98f81b0290a11017db6bb5217e6  8a190f44c0c0b5e58cb842ea34a38bb1b0dae40e0d0c47e7b747fbc2b5414255
422966b08448176ae8b97530a95aaf6d38c0bf17ddfd82f4952c76f6ac1f73cb  b26f8795699f8341b37a3baaab099aba3fc0dfbfe4c4603d77a9097b78093cc4
42b231e2bb3069913c23c88cc0184593be3aed21292f4ba5462a69518fe65eb4  c6e9faebbcf89bbeabf0da0e7c2feaaed6beb212df4516abdde15cc1f3e80c4c
54f4ec7e7f2fbb744e47c1d39e89a82a873e0abdff64d6003895e319be02e9a9  f27eadcbbf130ee30dd1fb544a2644b06ca7d2608889543fe0fcaed59ad1587f
59f251808b4b6da9ee26f768cd0e5f1f22071a4e6514edbedf17e337f7829096  f834f94f5b6de0dbc2f1ad0d09ccc28b5d2ba9f387b4b89917e5d9caaadb8b05

このようにcontainerやimageが保存されていることがわかります。

環境ファイル保存先を変更する。

簡単に調べたところ、docker環境の保存先を変更する方法はdockerの設定ファイルをいじったり、docker daemonの起動オプションをつけたりと複数の方法があるようです。

forums.docker.com
stackoverflow.com

今回調べたなかでもっとも簡単にできそうなシンボリックリンクは貼る方法でやりたいと思います。
やりかたの基本は下記リンクの通りです。

www.crybit.com


1.docker daemonを停止させる

$ /etc/init.d/docker stop

2.docker関連のプロセスが動いていないことを確認

$ ps aux|grep docker

3./var/lib/docker/の中身を新しい場所に移動させる

$ mv /var/lib/docker /path/to/new/

4.デフォルトの場所にシンボリックリンクを貼る

$ ln -s /path/to/new/docker/ /var/lib/docker

5.docker daemonをスタートさせる

$ /etc/init.d/docker start

6.新しい場所でimageやcontainerが動いていることを確認する

以上です。
それではよいdockerライフを!

pytorchでレイヤーをフリーズさせる

画像分野で深層学習を用いる際はImageNetのような大規模データセットで学習した学習済みモデルを使用して転移学習、ファインチューニングを行うことが一般的です。
このファインチューニングを行う際に、最初の数エポックはモデルの最終層以外の重みを固定し最終層だけ学習、その後に全レイヤーの重みを学習するといったテクニックがあります。

blog.floydhub.com

www.analyticsindiamag.com

一般的にニューラルネットワークの入力層に近いほど学習データの抽象的な特徴を学習していると言われていますが、
このテクニックを使うことにより、学習初期にいきなり入力層に近いレイヤーの重みが更新されることで抽象的な特徴抽出機能が崩壊することを防ぐ効果があります。


pytorchでは以下のようにパラメータをrequires_grad=Falseすることによってbackward()の際に重みが更新されないようにする(freeze)ことができます。

from torchvision.models import resnet34
model = resnet34(pretrained=True)

# freeze all layers
for param in model.parameters():
    param.requires_grad = False

上記の例だと最終層も含めてすべてのレイヤーがfreezeされてしまうので最終層はrequires_grad=Trueのままにしておきます。

from torchvision.models import resnet34
model = resnet34(pretrained=True)

# freeze layers except last layer
for param in model.parameters():
    param.requires_grad = False

last_layer = list(model.children())[-1]
print(f'except last layer: {last_layer}')
for param in last_layer.parameters():
    param.requires_grad = True

このようにしてやることで最終層だけ除いてレイヤーの重みを固定することができます。

この状態で数エポック学習を回して最終的に全レイヤーを学習する際はrequires_grad=Trueに戻してあげればOKです。

from torchvision.models import resnet34
model = resnet34(pretrained=True)

# unfreeze all layers
for param in model.parameters():
    param.requires_grad = True

jenkinsとgithubを連携して自動テストをする

web上にいろいろ情報が錯綜していていろいろハマったので成功したやりかたをメモしておく

やりたいこと

  • githubに新しいcommitがpushされるごとにそのソースコードをビルドしてテストする
  • テスト結果をgithub上から確認できるようにする

    github側の設定

  • ビルドしたいレポジトリにwebhookを設定する
    • settings -> webook
      • pyload url
      • content type
      • Which events would you like to trigger this webhook?
        • "Just the push event." or "Send me everything."
  • access tokenの設定
    • 個人アカウントのsettings -> developer settings -> personal access token
      • generate new token
      • repoとadmin:repo_hookをチェック

jenkins側の設定

  • jenkinsの管理 -> システムの設定 -> github
    • github server
      • name
      • API URL
      • credential
        • secret textで追加
          • 先程作成したpersonal access tokenを登録
      • manage hooks
        • よくわからん、とりあえずチェックなしにした
      • test connectionでちゃんとつながればおk
  • ジョブの設定
    • フリースタイルプロジェクトのビルド
      • github projcet
        • project url
          • ビルドしたレポジトリのURL
        • ビルドのパラメータ化
          • 名前
            • payload
          • デフォルト値
            • none
      • ソースコード管理
      • ビルド・トリガ
        • GitHub hook trigger for GITScm polling
      • ビルド
      • ビルド後の処理
      • うまく設定できていればレポジトリにpushされたときに自動でbuildが走ってcommit statusがgithub上から確認できる

fastaiのバージョン0.7をインストールする

fastaiという無料でディープラーニングについて学べる講座がありますが、その講義の中でfastaiというライブラリが使われています。 github.com

このライブラリはpytorchのラッパーのような形になっており、実際にディープラーニングをする上でのベストプラクティスが簡単に適用できることが特徴のようです。

実際に最適な学習率の初期値を見つけてくれる関数や[1506.01186] Cyclical Learning Rates for Training Neural Networksのような手法が簡単に使えるようになっています。

fastaiのライブラリは現在v1.0が公開されていますが、講義で使われてものやkaggleのkernelで使われているのはv0.7が多いようでv1を使っているとそのままでは動かないスクリプトが多数あります。 Githubにあるcondaやpipをつかってインストールすると最新版が入ってしますのでv0.7を使いたい場合は別の方法でインストールする必要があります。

fastaiのforumにはcondaの仮想環境を使ってv0.7のインストール方法がありますが、私の場合はどうもエラーが出てうまくいかなかったので別の方法をとりました。

v0.7のコードはfastaiレポジトリの old/fastai に移されているのでここにpythonのパスを通すことで使うことにします。

まず適当なディレクトリに移動してfastaiのレポジトリからクローンしてきます。

cd /tmp/
git clone https://github.com/fastai/fastai
cd fastai/old/

その後、pythonスクリプトでパスを通すことでimport できるようになります。

import sys
sys.path.append("/tmp/fastai/old") # on windows use \'s instead
import fastai

fastaiは裏でpytorchやtorchvisionを呼び出しているので別途インストールしておいてださい。

conda install pytorch torchvision cuda92 -c pytorch

Hash Tables (Cousera Data Structure Week 4)

CouseraのData Strucureコースのweek4の内容です。
www.coursera.org

前回のweek3の内容の記事はこちら
rikeiin.hatenablog.com

Week4の内容はHash Tablesです。

Hashingの応用例

IP Addresses

あるWebサーバにアクセスしてきた各IPアドレスが過去一定期間にアクセスがあったか判定するという問題を考える。

単純に配列を使った実装の場合、考えうるIPアドレス数の長さの配列を用意し、該当するIPアドレスに対応するインデックスの要素をインクリメントする方法がある。
このとき、

  • アクセスリストを更新する操作は(UpdateAccessList)は一つのログにつきO(1)
  • 一定期間にアクセスがあったか判定する操作(AccessedLastHour)はO(1)
  • IPv4でも2の32乗のメモリが必要
  • IPv6では2の128乗であり、もはやメモリに乗らない

配列を使った実装はメモリを多く消費するため、リストを使った実装を考える。

List-based Mapping

過去一定期間にアクセスしてきたIPアドレスのみをリストに保存する。
このとき、アクセス順にリストにつなげていく。

f:id:rikeiin:20180503183048p:plain

  • nは一定期間にアクセスがあったIPアドレスの数
  • メモリ消費量は\Theta (n)
  • L.Append, L.Top, L.Popは\Theta (1)
  • L.Find, L.Eraseは\Theta (n)
  • UpdateAccessListは\Theta (n)
  • AccessedLastHourは\Theta (n)

Hash Functions

ハッシュ関数の定義

あるオブジェクトの集合\mathcal Sm>0となる整数があるとき、関数h: \mathcal S \rightarrow \{0, 1,\dots, m-1\}ハッシュ関数と呼ぶ。

このとき、mハッシュ関数hの基数(cardinality)と呼ぶ。

ハッシュ関数の望ましい要件

  • hの計算が早い
  • 異なるオブジェクトには異なる値
  • メモリ使用量がO(m)でマップできる
  • 基数mは小さくしたい
  • もしオブジェクトの数|\mathcal S|mより大きいとき、すべてを異なる値にマップすることは不可能である

Collisions

h(o_1)=h(o_2)かつo_1\neq o_2のとき、これを衝突(collision)と呼ぶ。

Chaining

Map

マップはあるオブジェクトからあるオブジェクトへのマッピングを行う。
例えば、以下のような例がある。

  • ファイル名→ディスク上のファイルの場所
  • 学生ID→学生名
  • 契約名→契約電話番号

\mathcal Sから\mathcal VへのマップはHashKey(O), Get(O), Set(O, v)の操作を持つデータ構造である。このとき、O\in S, v \in V

Chaining

f:id:rikeiin:20180505224216p:plain

マップに関する各操作の疑似コードを以下に示す。

f:id:rikeiin:20180505225558p:plain
f:id:rikeiin:20180505225620p:plain
f:id:rikeiin:20180505225641p:plain

  • Aの最大のチェインの長さをcとすると、HashKey, Get, Setの時間計算量は\Theta(c+1)
    • c=0のときO(1)
  • nを現在のマップの異なるキーOの数、mハッシュ関数の基数とするとメモリの消費量は\Theta(n+m)

Hash Tables

Set

SetはAdd(O), Remove(O), Find(O)の操作を持つデータ構造である。
chainingを使ったSetの実装方法は二通りある。
f:id:rikeiin:20180505233025p:plain

  • SetはSからV=\{true, false\}へのマップと等しい
  • (O, v)のペアを保存する代わりにOのみを保存する

Setに対する各操作の疑似コードを以下に示す。

f:id:rikeiin:20180505233025p:plain
f:id:rikeiin:20180505233046p:plain
f:id:rikeiin:20180505233204p:plain

SetもしくはMapを用いたhashingの実装をHash tableと呼ぶ。

プログラミング言語では以下のようなクラスに実装されている。

まとめ

  • Chainingはhash tableを実装するためのテクニックのひとつである
  • メモリ消費量はO(n+m)
  • 各操作の時間計算量はO(c+1)
  • どのようにmとcの両方を小さくするか?