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

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

アンサンブル学習 アルゴリズム入門 〜〜プルーニング〜〜

前回の記事では、決定木による機械学習プログラムを作成しました。

rikeiin.hatenablog.com

この決定木では、作成する木の深さに上限がなくパラメータの設定を深くすればするほど複雑なモデルが作成できます。なので、単に学習データをよく再現できるモデルを作成するという観点からは、決定木アルゴリズムはメモリと計算時間が許す限り、学習データを完全に再現できるモデルを作成できます。しかし、これだと学習データに過学習してしまい、学習していないデータに対する性能が低下してしまいます。そのため、いかに過学習を防止し、汎化誤差を少なくするかが問題となりますが、本記事では決定木アルゴリズムにおける過学習を防止するための手法であるプルーニングを実装してみます。

決定木のプルーニング

プルーニングとは

プルーニング(枝刈り)は木構造をしたデータに対して適用されるアルゴリズムです。
前記事で作成した決定木アルゴリズムでは、作成される木は常に葉まで同じ深さを持つ完全二分木と呼ばれるデータ構造をしていました。
しかし、このようなモデルで深い木を作成すると無駄な判断を行うノードが増えてしまうので過学習が起こりやすくなります。そこで一度深い深さで決定木を作成した後で不要な枝を削除することで、よりシンプルな決定木を作成する手法がとられます。この木構造のデータから不要な枝を削除することをプルーニングと呼びます。

f:id:rikeiin:20191022235901p:plain
深さが2で葉の数が4つある決定木からプルーニングによって葉Dが取り除かれる例

プルーニングのルール

プルーニングのアルゴリズムでは決定木を走査して削除すべき枝と残すべき枝を選択します。どの枝を削除するべきかの判断がプルーニングアルゴリズムのキモとなるのですが、最も単純なルールは「枝を削除しても結果が変わらないならばその枝を削除する」というものになります。すなわちその枝を削除する前後でモデルを実行した結果のスコアが悪化しないならばその枝を削除するということになります。プルーニングの際に使用するスコアの計算には決定木に対する学習データと同じデータを使用する方法と学習データを決定木の学習用とプルーニング用のテストデータに二分する方法があります 。

再帰関数によるプルーニング

上記のルールをプログラムで実装すると、決定木のノードをたどりながら、次の2つのケースでノード内の枝を削除すればプルーニングのアルゴリズムを実装することができあす。

  1. 学習データのすべてが左右のノードどちらか一方に振り分ける場合
  2. ノード内どちらかの枝を削除してもノード全体のスコアが悪化しない場合

図に表すと以下のようになります。

f:id:rikeiin:20191104152704p:plain

決定木のノードをたどるには再帰関数を仕様し、与えられたノードの枝を関数内で都度更新することでプルーニングを行います。
本記事で作成する再帰関数の基本的な構造は次のようになります。

def プルーニング関数(決定木内のノード, 学習データ):
    if 左右にデータが振り分けられるか:
        # 1つの枝のみの場合、その枝を置き換える
        return プルーニング関数(枝, 学習データ)
    # 再帰呼び出しで枝を辿る
    左の枝 = プルーニング関数(左の枝, 学習データ)
    右の枝 = プルーニング関数(右の枝, 学習データ)
    # 枝刈りを行う
    if 枝刈りを行うべき:
        return ノード内の残す方の枝
    return 決定木内のノード

関数の中では一つのノードを見て再帰的にそのノードの左右の枝を更新していきます。再帰関数は枝刈り行う場合は残された枝を、そうでない場合は現在のノードを返します。そして再帰的に呼び出される関数の戻り値でノートの枝を更新することで、不要な枝を削除して行きます。

プルーニングの実装

実際にプルーニングを行うアルゴリズムを実装していきます。
ここでは、前記事で作成した決定木と同じ構造を持つPrunedTreeというクラスがあるものとして再帰関数を実装していきます。

スコアによる判定

再帰関数の名前はreducederrorとして、引数として決定木内の現在のノードを表すnode, 学習データと正解データを表すxとyを引数にとります。
まずはreducederror関数のひな形として、ノードが葉でないことを確認して、現在のノードを返す関数を実装します。

def reducederror(node, x, y):
    # ノードが葉でなかったら
    if isinstance(node, PrunedTree):
        # ここにプルーニングの処理が入る
        
    # 現在のノードを返す
    return node

次に上記の「# ここにプルーニングの処理が入る」という部分に学習データに対するノードのスコアと左右の端のスコアを計算する部分を作成します。
この関数では決定木の種類がクラス分類であるかは回帰であるかによって処理を分けますが、それは目的変数の次元数で判断できます。
クラス分類の場合、ノードそれ自体と左右の葉に対してpredict関数を呼び出した結果から、間違いの個数をカウントします。
回帰の場合は、同様に正解データとの二乗誤差を作成して、その値をスコアとします。
その後、枝を削除してもスコアが悪化しないようならスコアが良い方の枝を返すようにします。

# ここに枝刈りのコードが入る
# calculate score with train data
p1 = node.predict(x)
p2 = node.left.predict(x)
p3 = node.right.predict(x)
# if classification
if y.shape[1] > 1:
    # socre as a number of misclassifications
    ya = y.argmax(axis=1)
    d1 = np.sum(p1.argmax(axis=1) != ya)
    d2 = np.sum(p2.argmax(axis=1) != ya)
    d3 = np.sum(p3.argmax(axis=1) != ya)
else:
    # score as mean squared error
    d1 = np.mean((p1 - y) ** 2)
    d2 = np.mean((p2 - y) ** 2)
    d3 = np.mean((p3 - y) ** 2)

if d2 <= d1 or d3 <= d1:  # score is not worse with which left or right
    # return node with better score
    if d2 < d3:
        return node.left
    else:
        return node.right

決定木のプルーニング

上記の「# ここに枝刈りのコードが入る」という箇所ではノードに左右の枝が両方あるかどうかをチェックし、一つの枝のみにデータが振り分けられるのであれば、現在のノードをその枝で置き換えます。
そしてその後、関数を再帰的に呼び出して左右の枝を更新します。

feat = x[:, node.feat_index]
val = node.feat_val
l, r = node.make_split(feat, val)
if val is np.inf or len(r) == 0:
    return reducederror(node.left, x, y)
elif len(l) == 0:
    return reducederror(node.right, x, y)

# update the branch of right and left
node.left = reducederror(node.left, x[l], y[l])
node.right = reducederror(node.right, x[r], y[r])

以上で「Reduce Error」プルーニングのための再帰関数が完成します。

Critical Valueによるプルーニング

先ほど作成したReduce Errorプルーニングは、実際の実行結果をもとに処理を行うので、シンプルのアルゴリズムながら性能の良い結果を得ることができるという特徴があります。
一方でReduce Errorプルーニングは、プルーニングの際に木とその各枝に対して毎回決定木の実行を行われるため、処理時間の面で不利になるという欠点があります。決定木に対するプルーニングの処理については他にもさまざまなものがありますが、ここではプルーニングのアルゴリズムとして、もう一つ Critical Value プルーニングというアルゴリズムを実装してみます。

Critical Valueの概要

Critical Value プルーニングは決定木の学習時に使用した分割スコアを元にプルーニングの処理を行います。
決定木の学習ではすべてのノードにおいてMetrics関数の値から分割のスコアが一度求められていました。そこでプルーニングの処理を行う際には決定木全体の中での最も良い分割スコアを求め、その値をもとにある程度以下の値で分割されたノードを枝刈りすることでプルーニングの処理を行います。また削除するノードのしきい値は全ての枝のスコアから削除する枝の割合を指定することで求めます。
Critical Value プルーニングは決定木の分割のみを完成した時点で行うことができるので、必ずしも全ての葉を学習する必要はありません。葉の学習をプルーニングの後に行うことで深い階層の決定木を作成しても学習の時間が指数関数的に増加することを防ぐことができます。また決定木の分割が完成した時点でCritical Valueプルーニングを行い、さらに葉の学習が終了したらReduce Errorプルーニングを行うといったこともできます 。

f:id:rikeiin:20191109123744p:plain
Critical Valueプルーニングの例

Critical Valueの実装

それでは実際にCritical Valueを行うコードの実装をしていきます。先程と同じくpruning.pyの中に書いていきます。

全ノードのスコア

まずは決定木全体から分割時に使用した最も良いメトリクス関数の値を求めるために、すべてのノードのスコアを取得する関数を作成します。
前々回の記事でDecisionStumpクラスを作成した際に、分割で求めた良いメトリクス関数の値はそのノードの分割のスコアとしてスコア変数に保存しておきました。本記事で作成するPrunedTreeクラスもDecisionStumpクラスの派生クラスなので、同じくスコア変数から分割のスコアを取得することができます。ここで気をつけておくことは、このスコア変数が表すスコアは値が小さいほど良い値だという点です。全てのノードをたどるには先ほどと同じく再帰関数を使用し、引数に与えられたリストにノードのスコア変数の値を追加していきます。ここではgetscore()という名前ですべてのノードのスコアを取得する関数を作成します。

def getscore(node, score):
    if isinstance(node, PrunedTree):
        if node.score >= 0 and node.score is not np.inf:
            score.append(node.score)
        getscore(node.left, score)
        getscore(node.right, score)
Critical Valueを行う関数

次に、実際にプルーニングを行う関数criticalscore()を実装します。この関数も再帰関数として実装し、前節と同じ構造をしています。
今回は引数としてしきい値となるscore_maxが追加されています。

def criticalscore(node, score_max):
    if type(node) is PrunedTree:
        # pruning process
        # update the branch of right and left
        node.left = criticalscore(node.left, score_max)
        node.right = criticalscore(node.right, score_max)
        # delete node
        if node.score > score_max:
            leftisleaf = not isinstance(node.left, PrunedTree)
            rightisleaf = not isinstance(node.right, PrunedTree)
            # leave one leaf if both are leaf
            if leftisleaf and rightisleaf:
                return node.left
            # leave branch if which one is leaf
            elif leftisleaf and not rightisleaf:
                return node.right
            elif not leftisleaf and rightisleaf:
                return node.left
            # leave node with better score if both are branch
            elif node.left.score < node.right.score:
                return node.left
            else:
                return node.right

プルーニング用決定木クラスの実装

先程までは前記事で作成した決定木と同じ構造を持つPrunedTreeというクラスがあるものとして、再帰関数を実装していました。
ここではそのPrunedTreeクラスを実装していきます

プルーニング用の決定木の作成

PrunedTreeクラスは前記事で作成したDecisionTreeクラスの派生クラスとして作成します。本記事で使用するPrunedTreeクラスでは、クラス内の変数でプルーニング用の関数名を表すprunfncと、学習データとプルーニング用のテストデータを別にするかどうかを表すpruntestを作成します。ここでprunfnc変数は、文字列型で"reduce"または"critical"いずれかの値が入るものとします。またプルーニング用のテストデータを別にするかどうかを表すpruntest、 テストデータの割合を表すsplitratio、Critical Value プルーニングで使用するパーセンテージの変数であるcriticalも作成します。

class PrunedTree(DecisionTree):
    def __init__(self, prunfnc='critical', pruntest=False, splitratio=0.5, critical=0.8,
                 max_depth=5, metric=entropy.gini, leaf=ZeroRule, depth=1):
        super().__init__(max_depth=max_depth, metric=metric, leaf=leaf, depth=depth)
        self.prunfnc = prunfnc # プルーニング用関数
        self.pruntest = pruntest # プルーニング用にテストデータを取り分けるか
        self.splitratio = splitratio # プルーニング用テストデータの割合
        self.critical = critical # criticalプルーニング用のしきい値

新しいノードを作成するためのget_node()も次のようにオーバーライドしておきます。

def get_node(self):
    return PrunedTree(prunfnc=self.prunfnc, pruntest=self.pruntest, splitratio=self.splitratio, critical=self.critical,
                        max_depth=self.max_depth, metric=self.metric, leaf=self.leaf, depth=self.depth + 1)

次に学習を行うfit()関数をオーバーライドします。 fit()関数内ではまず根のノードとそうでない時で処理が異なり、根のノードの時のみプルーニング用の処理が行われます。これはPrunedTreeクラスは決定木内のノードの一つを表すのに対して、プルーニング用の関数が学習後の決定木に対して再帰的に呼び出されるためです。PrunedTreeクラスで根のノードの時には、まずプルーニングの際に枝の削除を行うかどうかを判断するためのテストデータを用意します。テストデータはself.pruntestがTrueであれば学習データからランダムにself.splitratioで指定された割合のデータをテストデータとして取り分けます。self.pruntestがFalseの場合は学習データと同じデータを使用してプルーニングの処理を行います。

def fit(self, x, y):
    # if depth=1, root node
    if self.depth == 1 and self.prunfnc is not None:
        # data for pruning
        x_t, y_t = x, y

        if self.pruntest:
            n_test = int(round(len(x) * self.splitratio))
            n_idx = np.random.permutation(len(x))
            tmpx = x[n_idx[n_test:]]
            tmpy = y[n_idx[n_test:]]
            x_t = x[n_idx[:n_test]]
            y_t = y[n_idx[:n_test]]
            x = tmpx
            y = tmpy

        # ここで決定木の学習を行う

        return self
            

上記の「 # ここで決定木の学習を行う」の部分には以下のコードが入ります。前記事でのDecisionTreeの学習アルゴリズムとほぼ同じですが、Critical Valueプルーニングの場合は葉の学習は行わず、木の分割のみ学習します。

# 決定木の学習
self.left = self.leaf()
self.right = self.leaf()
left, right = self.split_tree(x, y)
if self.depth < self.max_depth:
    self.left = self.get_node()
    self.right = self.get_node()
if self.depth < self.max_depth or self.prunfnc != 'critical':
    if len(left) > 0:
        self.left.fit(x[left], y[left])
    if len(right) > 0:
        self.right.fit(x[right], y[right])

# ここでプルーニングの処理を行う。
            

上記の「# ここでプルーニングの処理を行う」には根のノードの時にself.prunfncに入っている関数の名前から、再帰関数を呼び出してプルーニングの処理を行います。Reduce Errorプルーニングの場合、再帰関数の引数には自分自身のインスタンスとプルーニング用のテストデータを渡してreducederror()関数を呼び出します。また Critical Value プルーニングの場合、まずgetscore()関数ですべてのノードの分割の際のスコアを取得し、その数からパラメーターで指定された割合を求め、しきい値となる値を計算します。そしてそのしきい値を引数にcriticalscore()関数呼び出し、プルーニングの処理を行った後、学習させていなかった葉について改めて学習を行います。葉のみ学習を行う関数はfit_leaf()という名前で作成します。

# pruning process
# only whene depth = 1, root node
if self.depth == 1 and self.prunfnc is not None:
    if self.prunfnc == 'reduce':
        reducederror(self, x_t, y_t)
    elif self.prunfnc == 'critical':
        # get score of metrics function when training
        score = []
        getscore(self, score)
        if len(score) > 0:
            # calculate max score of branch left
            i = int(round(len(score) * self.critical))
            score_max = sorted(score)[min(i, len(score) - 1)]
            # pruning
            criticalscore(self, score_max)

        # learn leaf
        self.fit_leaf(x, y)
            

fit_leaf()関数は、すでに作成されている枝に従ってデータを分割し、枝の指しているノードが葉であればfit()関数を呼び出します。

def fit_leaf(self, x, y):
    feat = x[:, self.feat_index]
    val = self.feat_val
    l, r = self.make_split(feat, val)

    # learn only leaf
    if len(l) > 0:
        if isinstance(self.left, PrunedTree):
            self.left.fit_leaf(x[l], y[l])
        else:
            self.left.fit(x[l], y[l])
    if len(r) > 0:
        if isinstance(self.right, PrunedTree):
            self.right.fit_leaf(x[r], y[r])
        else:
            self.right.fit(x[r], y[r])
            

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

ensumble-learning-introduction/pruning.py at master · wdy06/ensumble-learning-introduction · GitHub

参考文献

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

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

kubernetesでsshできるpodを作る

普段自分は機械学習の研究用途でkubernetes (k8s)上のクラスターでjupyterlabを立ててそこでコーディングをしています。
なんですが、jupyterlabだと補完がいまいちだったり、pyファイルの編集においては補完が全く効かなかったりで何かいい方法はないか模索していたんですが、最近VSCodeでremote serverに接続して開発できる機能が追加されました。

code.visualstudio.com

crieit.net

この機能を使うことで、リモートのサーバにsshしてファイルを編集できますし、pythonインタープリタもサーバの環境を使えます。
つまり、dockerで機械学習ディープラーニング用の環境を作ってGPUが乗ったサーバでコンテナを立ててしまえば、Pytorchやkerasの補完がモリモリ効いたままコーディングができて実行もすぐできて超絶便利になりそうです。

単なるDockerでsshできるコンテナを建てることは簡単なんですが、Kubernetes上でやろうとすると少しハマったのでその備忘録を残しておきます。

ちなみに、単なるDockerでsshできるコンテナを建てるには以下の方法で簡単にできます。

docs.docker.jp

上記と同様のパスワード認証の方法でk8sにpodを立ててsshしようとしてもPermission Deny(Publickey, password)でアクセスできませんでした。

いろいろ調査したところ、鍵認証の方法ならsshに成功しました。
基本的なやり方は下記と同じです。

GitHub - ruediste/docker-sshd: Dockerized SSH server prepared for Kubernetes

まず、下記のようなDockerfileでssh用imageを作って適当なrepositoryにpush しておきます。

FROM ubuntu:16.04

RUN apt-get update
RUN apt-get install -y openssh-server
RUN mkdir /var/run/sshd

RUN apt-get install -y less curl iputils-ping

RUN sed -ri 's/^PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config \
&& sed -ri 's/^StrictModes\s+.*/StrictModes no/' /etc/ssh/sshd_config \
&& sed -ri 's/HostKey \/etc\/ssh\//HostKey \/etc\/ssh\/hostKeys\//g' /etc/ssh/sshd_config \
&& echo "PasswordAuthentication no" >> /etc/ssh/sshd_config \
&& echo "GatewayPorts yes" >> /etc/ssh/sshd_config

RUN rm /etc/ssh/ssh_host*

EXPOSE 22

CMD ["/usr/sbin/sshd", "-De"]

次に、ホスト鍵関連のファイルを生成するgenerateHostKeys.shのようなシェルスクリプトを作って実行します。

#!/bin/bash
rm -rf hostKeys
mkdir hostKeys
ssh-keygen -q -t rsa  -f hostKeys/ssh_host_rsa_key -N "" -C ""
ssh-keygen -q -t dsa  -f hostKeys/ssh_host_dsa_key -N "" -C ""
ssh-keygen -q -t ecdsa  -f hostKeys/ssh_host_ecdsa_key -N "" -C ""
ssh-keygen -q -t ed25519  -f hostKeys/ssh_host_ed25519_key -N "" -C ""
kubectl --namespace default delete secret ssh-host-keys
kubectl --namespace default create secret generic ssh-host-keys \
--from-file=hostKeys/ssh_host_rsa_key \
--from-file=hostKeys/ssh_host_rsa_key.pub \
--from-file=hostKeys/ssh_host_dsa_key \
--from-file=hostKeys/ssh_host_dsa_key.pub \
--from-file=hostKeys/ssh_host_ecdsa_key \
--from-file=hostKeys/ssh_host_ecdsa_key.pub \
--from-file=hostKeys/ssh_host_ed25519_key \
--from-file=hostKeys/ssh_host_ed25519_key.pub

また、ユーザ認証用の公開鍵をauthorized_keysとして保存しておきます。

次に、先ほどつくったホスト鍵ファイルたちとauthorized_keysをk8s上でセキュアに扱うためにSecretsを使います。
k8sのSecretsに関しては下記のサイトがわかりやすかったです。

ubiteku.oinker.me

上で作ったファイルたちをSecretsとして登録するUpdateKeys.shを作って実行します。

#!/bin/sh
# Update the keys stored in ssh.yaml from the authorized-keys file
kubectl --namespace default delete secret ssh-keys
kubectl --namespace default create secret generic ssh-keys --from-file=./authorized_keys

あとはk8ssshサーバを立てるマニフェストを書いて下記の設定を追加した上でpodを立ててあげればOKです。

volumeMounts:
    - mountPath: /root/.ssh/
        name: ssh-dir
      - mountPath: /etc/ssh/hostKeys/
        name: ssh-host-keys

volumes:
  - name: ssh-dir
    secret:
      secretName: ssh-keys
      defaultMode: 0600
  - name: ssh-host-keys
    secret:
      secretName: ssh-host-keys
      defaultMode: 0600

あとは普通にsshできます。

$ ssh root@<PodのIPアドレス>

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

前回記事からの続きになります。
rikeiin.hatenablog.com

前回の記事で作成したDecisionStumpは、深さが1の決定木でしたが、同じ方法によるデータの分割を再帰的に行えば、より深い階層を持つ決定木が作成できます。

この記事では先ほど作成したDecisionStumpの応用として、木の深さを指定できる決定木アルゴリズムを作成します。
深さが可変の決定木アルゴリズムはDecisionTreeという名前のクラスとして作成します。このクラスは先ほど作成したDecisionStumpクラスの派生クラスとして作成することで、決定木アルゴリズムに必要となる木分割の関数をDecisionStumpクラスから継承して利用できるようにします。
まずはdtree.pyという名前のファイルを作成し次のクラスを作成します。このクラスには学習させる決定木の深さを表すmax_depth変数と再帰的に生み出される際に使用する現在のノードの深さを表すdepth変数を作成します。

import numpy as np
import support
import entropy
from zeror import ZeroRule
from linear import Linear
from dstump import DecisionStump

class DecisionTree(DecisionStump):
    def __init__(self, max_depth=5, metric=entropy.gini, leaf=ZeroRule, depth=1):
        super().__init__(metric=metric, leaf=leaf)
        self.max_depth = max_depth
        self.depth = depth

このDecisionStumpは決定木内の一つのノードを表しており、葉となるノード自分自身のクラスで置き換えることで、高さが可変の決定を作成します。
それには次のようにfit()をオーバーライドし、現在のノードの深さが最大深さに達していないならば左右の葉をget_node()から取得する新しいノードで置き換えます。
新しく子ノードとなるDecisionTreeでは引数のdepthに現在のノードの深さに1を加えた値を入れることで、現在のノードの深さを増やしていきます。
現在のノードの深さが最初に指定したmax_depthに達するとDecisionTreeクラスの動作はDecisionStumpと同じになり、左右の端に対して学習を行います。

def fit(self, x, y):
    # create leaf node of left and right
    self.left = self.leaf()
    self.right = self.leaf()
    # split data into left and right node
    left, right = self.split_tree(x, y)

    if self.depth < self.max_depth:
        if len(left) > 0:
            self.left = self.get_node()
        if len(right) > 0:
            self.right = self.get_node()

    # learn left and right node
    if len(left) > 0:
        self.left.fit(x[left], y[left])
    if len(right) > 0:
        self.right.fit(x[right], y[right])
    return self

新しいノードを生成して返すget_node()は以下の様になります。

def get_node(self):
    return DecisionTree(max_depth=self.max_depth, metric=self.metric,
                        leaf=self.leaf, depth=self.depth + 1)

DecisionTreeでは、推論を行うpredict()は親クラスのDecisionTreeがそのまま使えます。

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

dtree.py全体のコードはこちら

次の記事ではプルーニングのアルゴリズムを実装していきます。
rikeiin.hatenablog.com

参考文献

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

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

アンサンブル学習 アルゴリズム入門 〜〜決定木その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