読者です 読者をやめる 読者になる 読者になる

人間だったら考えて

考えて考えて人間だったら考えて

Chainerでマルチタスクニューラルネットワークを実装した

注:この記事の実装は非効率的かもしれません.

この記事は何?

マルチタスク学習をニューラルネットワークに適用した研究がいくつか報告されています.
適用例として,Georgeら(2014)の研究では,タンパク質に対する化合物の活性予測にニューラルネットワークを用いたマルチタスク学習を適用しています.
また,Xiaodongら(2015)の研究では,自然言語処理のタスクとしてクエリ分類と情報検索の2つのタスクを同時に解くマルチタスク学習を提案しています.

私はDNNを実装する時はChainerを使っているのですが,Chainerでマルチタスク学習を実装した例が見当たらなかったため自分で実装してみました.

ネットワーク構造

解くタスクは2つとします.2つのタスクに対応するネットワークが存在し,それぞれのネットワーク間で一部の層を共有するようなネットワーク構造を考えます(下図参照).

f:id:sz_dr:20170306225240p:plain

学習の進め方と実装

学習は,「task: 0のデータで学習」→「task: 1のデータで学習」→「task: 0のデータで学習」→…というように交互に行っていきます.

記事冒頭でも示したように上手い実装が思いつかなかった,分からなかったので,次図のようにナイーブな実装を行いました.

f:id:sz_dr:20170306230707p:plain

この記事で紹介する実装では,各タスク毎にネットワークを定義します.
「task: 0のデータで学習」が終わると, W^{(1)}および W^{(2, t_0)}の重みがそれぞれ更新されます.
その後,共有したい重み W^{(1)}の値をもう片方のタスクにおけるネットワークにコピーします.

この操作を繰り返すことで,タスク毎に一部の重みは共有し,一部の重みは独立に学習を進めるということができます.

Chainerによる実装

まずはネットワーク構造を実装します,共有したい層をSharedNet,タスク毎に独立させたい層をSeparetedNet,それぞれの層を結合したものをCombinedNetとします.今回は簡単のためMLPで実装しましたが,他のネットワーク構造も簡単に扱えます.

import chainer
import chainer.links as L
import chainer.functions as F


class SharedNet(chainer.Chain):
    def __init__(self, n_out):
        super(SharedNet, self).__init__(
            l1=L.Linear(None, n_out)
        )

    def __call__(self, x):
        a = self.l1(x)
        z = F.sigmoid(a)
        return z


class SeparatedNet(chainer.Chain):
    def __init__(self, n_out):
        super(SeparatedNet, self).__init__(
            l1=L.Linear(None, n_out)
        )

    def __call__(self, x):
        a = self.l1(x)
        z = a
        return z


class CombinedNet(chainer.Chain):
    def __init__(self, shared_net, separated_net):
        super(CombinedNet, self).__init__(
            shared_net=shared_net,
            separated_net=separated_net
        )

    def __call__(self, x):
        a1 = self.shared_net(x)
        a2 = self.separated_net(a1)
        return a2

次に,学習用のコードを書きます.データセットには有名なirisデータセットを用いました.
irisデータセットのサンプルには3つのラベル(0, 1, 2)のどれかが振られていますが,task: 0を「ラベル0とラベル1の分類問題」,task: 1を「ラベル1とラベル2の分類問題」としました.3クラス分類問題を無理やりマルチタスクの問題にしてみました.

import numpy as np
import chainer
import chainer.functions as F
from sklearn import datasets
import mymodel

N_TASK = 2


def main():
    np.random.seed(0)

    # Model definition
    shared_nets = [mymodel.SharedNet(3) for i_task in range(N_TASK)]
    separated_nets = [mymodel.SeparatedNet(2) for i_task in range(N_TASK)]
    combined_nets = [mymodel.CombinedNet(shared_net, separated_net)
                     for shared_net, separated_net in zip(
                        shared_nets, separated_nets)]

    # Setup an optimizer
    optimizers = [chainer.optimizers.Adam() for i_task in range(N_TASK)]
    for i_task, optimizer in enumerate(optimizers):
        optimizer.use_cleargrads()
        optimizer.setup(combined_nets[i_task])

    # Load dataset
    X, ys = datasets.load_iris(return_X_y=True)
    X = X.astype(np.float32)
    ys = ys.astype(np.int32)
    task_index_lst = [(ys == 0) | (ys == 1),
                      (ys == 1) | (ys == 2)]
    X_by_task = [X[task_index] for task_index in task_index_lst]
    ys_by_task = [ys[task_index] for task_index in task_index_lst]
    """
    labels should be 0 or 1
    change labels of task1 1 or 2 -> 0 or 1
    """
    ys_by_task[1] -= 1

    # First Training
    for i_task in range(N_TASK):
        combined_nets[i_task].cleargrads()
        optimizers[i_task].update(F.softmax_cross_entropy,
                                  combined_nets[i_task](X_by_task[i_task]),
                                  ys_by_task[i_task])
        # Multi-task DNN (parameter sharing)
        if i_task == 0:
            combined_nets[1].shared_net.copyparams(combined_nets[0].shared_net)
        elif i_task == 1:
            combined_nets[0].shared_net.copyparams(combined_nets[1].shared_net)

    # Before training on task0
    print("** Before training on task0 **")
    for i_task in range(N_TASK):
        print("task{}".format(i_task))
        print("shared\n", combined_nets[i_task].shared_net.l1.W.data)
        print("separated\n", combined_nets[i_task].separated_net.l1.W.data)

    # Training on task0
    optimizers[0].update(F.softmax_cross_entropy,
                         combined_nets[i_task](X_by_task[0]),
                         ys_by_task[0])

    # After training on task0
    print("\n** After training on task0 **")
    for i_task in range(N_TASK):
        print("task{}".format(i_task))
        print("shared\n", combined_nets[i_task].shared_net.l1.W.data)
        print("separated\n", combined_nets[i_task].separated_net.l1.W.data)

    # Multi-task DNN (parameter sharing)
    combined_nets[1].shared_net.copyparams(combined_nets[0].shared_net)
    print("\n** After sharing parameter **")
    for i_task in range(N_TASK):
        print("task{}".format(i_task))
        print("shared\n", combined_nets[i_task].shared_net.l1.W.data)
        print("separated\n", combined_nets[i_task].separated_net.l1.W.data)


if __name__ == '__main__':
    main()

TrainerやUpdaterを使わず,泥臭く実装してみました.
First trainingではtask: 0およびtask: 1についてそれぞれ1回学習を行います.
その後,重みが正しく更新されているかどうかを見ていきます.

実行結果

上のコードを実行すると,以下のような結果が得られました.

** Before training on task0 **
task0
shared
 [[ 0.88203126  0.20008963  0.48937538  1.12046123]
 [ 0.93177909 -0.49063882  0.47304434 -0.07767794]
 [-0.05160943  0.20529924  0.0700218   0.72513676]]
separated
 [[ 0.44038531  0.0712491   0.25726452]
 [ 0.19164696  0.86160696 -0.11944815]]
task1
shared
 [[ 0.88203126  0.20008963  0.48937538  1.12046123]
 [ 0.93177909 -0.49063882  0.47304434 -0.07767794]
 [-0.05160943  0.20529924  0.0700218   0.72513676]]
separated
 [[ 0.18174972 -0.49211243 -1.47296929]
 [ 0.37636688  0.49808249 -0.42948917]]

** After training on task0 **
task0
shared
 [[ 0.88270104  0.20075931  0.4900445   1.12112439]
 [ 0.93110901 -0.49130887  0.47237432 -0.07834772]
 [-0.05093938  0.2059693   0.06935176  0.72446674]]
separated
 [[ 0.44105536  0.07191915  0.25793457]
 [ 0.1909769   0.86093688 -0.12011819]]
task1
shared
 [[ 0.88203126  0.20008963  0.48937538  1.12046123]
 [ 0.93177909 -0.49063882  0.47304434 -0.07767794]
 [-0.05160943  0.20529924  0.0700218   0.72513676]]
separated
 [[ 0.18174972 -0.49211243 -1.47296929]
 [ 0.37636688  0.49808249 -0.42948917]]

** After sharing parameter **
task0
shared
 [[ 0.88270104  0.20075931  0.4900445   1.12112439]
 [ 0.93110901 -0.49130887  0.47237432 -0.07834772]
 [-0.05093938  0.2059693   0.06935176  0.72446674]]
separated
 [[ 0.44105536  0.07191915  0.25793457]
 [ 0.1909769   0.86093688 -0.12011819]]
task1
shared
 [[ 0.88270104  0.20075931  0.4900445   1.12112439]
 [ 0.93110901 -0.49130887  0.47237432 -0.07834772]
 [-0.05093938  0.2059693   0.06935176  0.72446674]]
separated
 [[ 0.18174972 -0.49211243 -1.47296929]
 [ 0.37636688  0.49808249 -0.42948917]]

"** Before training on task0 **"の段階では,task: 0およびtask: 1それぞれの共有部分の重みが等しくなっていることが分かります.

"** After training on task0 **"の段階では,task: 0のネットワークの重みが更新されたことが分かります.

"** After sharing parameter **"の段階では,task: 0およびtask: 1それぞれの共有部分の重みが等しくなっていることが分かります.

その他

重みを更新する度に,他方のタスクへの重みコピーが走るので効率的とは言えませんが,一応実装はできました.
ネットワーク定義自体で何とか出来るのでしょうか…ご存知の方いらっしゃったらご教授お願いします…(ヽ´ω`)

2017/3/8 追記

marugari2さんに,CombinedNetおよびoptimizerをそれぞれ単一で実装する方法を紹介していただきました.ありがとうございます!コメント欄をご参照ください.

CNNモデル比較論文 "An Analysis of Deep Neural Network Models for Practical Applications"を読んだ

深層学習を用いた画像認識分野では様々なCNNのネットワーク構造が提案されており,ImageNetデータセット等を用いた予測精度比較が広く行われています.
じゃあどのCNNモデルを使うべきなんだろう…と考えていましたが,最近こんなtweetが目に入りました.

予測精度による比較は広く行われていますが,計算速度やメモリ使用量による比較は見たことが無かったので,tweetで紹介されている論文"An Analysis of Deep Neural Network Models for Practical Applications"を読んでみました.

比較対象のCNNモデル

この論文では以下のCNNモデルを比較しています.

  • AlexNet
  • BN-AlexNet (batch normalized AlexNet)
  • BN-NIN (batch normalized Network In Network)
  • ENet
  • GoogLeNet
  • ResNet-18, 34, 50, 101, 152
  • VGG-16, 19
  • Inception-v3, v4

比較方法

この論文では以下の比較を行っています.

  • 予測精度 (Top-1 Accuracy, single central-crop)
  • 命令数
  • 予測時間
  • 消費電力
  • メモリ使用量

予測精度による比較

まずは色々なところで目にする予測精度比較を見てみます.
f:id:sz_dr:20170228232931p:plain
(論文中Figure 1から引用)

縦軸はTop-1 Accuracyなので縦棒が長いほど良いCNNモデルです.ResNetやInceptionは後発なだけあって予測精度が高いことが分かります.

予測精度と予測時間による比較

予測精度が高いのはもちろん良いことなのですが,実応用では予測時間が気になるケースも多々あります.
コンペティションのように予測にいくらでも時間をかけられるなら良いのですが,リアルタイム性が要求される応用先ではなるべく高速に予測を行いたい気持ちがあります.

論文中では予測精度 vs 予測時間による比較を行っていました.

f:id:sz_dr:20170228233651p:plain
(論文中Figure 9から引用)

縦軸はAccuracy,横軸は1秒あたりに予測された平均画像数を表しています.図中の右上に円があれば,そのモデルは高速かつ高精度な予測を行えることを意味します.
各円の色はFigure 1の手法と対応しており,円の大きさは命令数と比例しています.
左のグラフはバッチサイズを1,右のグラフはバッチサイズを16としています.

予測精度と予測時間はトレードオフの関係にあることがわかります.当たり前と言えば当たり前かもしれませんが…
ResNetとInceptionはVGGと同等の予測時間にも関わらず高精度な予測を行っていることから,VGGを選ぶ理由はあまり無いのかなという印象です.
AlexNetおよびNINの予測時間は短いため,精度をそこまで気にしないケースならば選択肢に上がりそうです.

その他

予測精度と予測時間に特に興味があったため,この記事では上の比較を紹介しましたが,他にも様々な比較を行っているので興味のある方は論文を読んでみてください.
(論文で触れられているENetは著者らの提案手法のようです,論文中でもちょいちょいENetを推している言い回しが出てきましたね…)

参考文献

予測ランキング評価指標:NDCGの2つの定義と特徴の比較

この記事は何?

機械学習の応用例としてランキング予測があります.
ランキング予測の例としてウェブページランキングがあります.GoogleYahoo!のような検索エンジンでは,ユーザーが入力したクエリに対して適合度の高い順にウェブページをランキングし,ランキング結果を提示します.

ランキング予測結果の評価指標として,Normalized Discounted Cumulative Gain (NDCG) が広く使われています.NDCGは0から1の値を取り,1に近いほど正しいランキング予測結果であることを表します.

実はNDCGには2つの定義があります.この記事ではNDCGの2つの定義を紹介し,それぞれの特徴を比較します.

NDCGの定義

NDCGはDiscounted Cumulative Gain (DCG) を正規化した値です.
具体的には,予測ランキングを用いて得られたDCGを,真の正しいランキングを用いて得られるDCGで割ることで正規化します.
NDCGには2つの定義がありますが,どちらも正規化する方法は同じです.異なる点はDCGの計算方法であるため,2つのDCGの定義を紹介します.

1つ目のDCGはK. Jarvelinら(2002)によって提案されました.彼らのDCG(この記事ではDCG1と呼びます)は次式で定義されます.

 \textrm{DCG1}=\textrm{rel}_1 + \sum_{i=2}^{k}\frac{\textrm{rel}_i}{\log_2 i}

ここで, \textrm{rel}_iはランキング中の i番目の要素の適合度(レイティング)を表します. kは評価に用いる要素数を表します.検索エンジン等では k=10とすることが多いようです,ユーザーは上から10件までウェブページをポチポチするということでしょうか.

2つ目のDCGはC. Burgesら(2005)によって提案されました.彼らのDCG(この記事ではDCG2と呼びます)は次式で定義されます.

 \textrm{DCG2}=\sum_{i=1}^{k}\frac{2^{\textrm{rel}_i} - 1}{\log_2 (i+1)}

DCG2では適合度を2のべき乗で扱う点がDCG1と異なります.

計算例

いくつかのランキング例を使って,2つのNDCGの結果を比べていきます.

予測ランキングが正しいランキングのとき

予測順位 1 2 3 4 5 6 7 8 9 10
適合度 5 3 3 3 3 3 0 0 0 0

このとき,2つのNDCGは

NDCG1 1.00
NDCG2 1.00

となります.正しいランキング予測結果のときはNDCGはどちらも1になります.

そこそこの適合度の要素に対する予測は成功したが,高い適合度の要素に対する予測は失敗したとき

予測順位 1 2 3 4 5 6 7 8 9 10
適合度 3 3 3 3 3 0 0 0 0 5

このとき,2つのNDCGは

NDCG1 0.88
NDCG2 0.63

となります.NDCG1の方が高い値となります.

高い適合度の要素に対する予測は成功したが,そこそこの適合度の要素に対する予測は失敗したとき

予測順位 1 2 3 4 5 6 7 8 9 10
適合度 5 0 0 0 0 3 3 3 3 3

このとき,2つのNDCGは

NDCG1 0.73
NDCG2 0.89

となります.NDCG2の方が高い値となります.

2つのNDCGの特徴の違い

上の計算例で見たように,同じ予測ランキングでもNDCG1とNDCG2とで結果が変わることが分かりました.
NDCG2は適合度を2のべき乗で扱っているため,適合度の高い要素を強く重み付けして評価することになります.そのため,適合度5の要素が上位に来る予測ランキングではNDCG2の方が高い値となった一方で,適合度5の要素の予測を失敗してしまった予測ランキングではNDCG2の方が低い値となったと考えられます.

そこそこの適合度でも良いからたくさんのヒットが欲しいケースではNDCG1を使って,どれか1つでも良いから高い適合度の要素を当てたいケースではNDCG2を使うのが良いと言えるでしょう.

ソースコード

実験に使ったソースコードを貼っておきます(jupyter notebookベタ貼り).

# coding: utf-8

# In[1]:

import numpy as np


# In[2]:

def ndcg(y_true, y_pred, k=None, powered=False):
    def dcg(scores, k=None, powered=False):
        if k is None:
            k = scores.shape[0]
        if not powered:
            ret = scores[0]
            for i in range(1, k):
                ret += scores[i] / np.log2(i + 1)
            return ret
        else:
            ret = 0
            for i in range(k):
                ret += (2 ** scores[i] - 1) / np.log2(i + 2)
            return ret
    
    ideal_sorted_scores = np.sort(y_true)[::-1]
    ideal_dcg_score = dcg(ideal_sorted_scores, k=k, powered=powered)
    
    pred_sorted_ind = np.argsort(y_pred)[::-1]
    pred_sorted_scores = y_true[pred_sorted_ind]
    dcg_score = dcg(pred_sorted_scores, k=k, powered=powered)
    
    return dcg_score / ideal_dcg_score

def ndcg1(y_true, y_pred, k=None):
    return ndcg(y_true, y_pred, k=k, powered=False)

def ndcg2(y_true, y_pred, k=None):
    return ndcg(y_true, y_pred, k=k, powered=True)


# In[3]:

y_true = np.array([5, 3, 3, 3, 3, 3, 0, 0, 0, 0])


# In[4]:

y_pred = np.array([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
print("ndcg1", ndcg1(y_true, y_pred))
print("ndcg2", ndcg2(y_true, y_pred))


# In[5]:

# 5, 0, 0, 0, 0, 3, 3, 3, 3, 3
y_pred = np.array([10, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print("ndcg1", ndcg1(y_true, y_pred))
print("ndcg2", ndcg2(y_true, y_pred))


# In[6]:

# 3, 3, 3, 3, 3, 0, 0, 0, 0, 5
y_pred = np.array([1, 10, 9, 8, 7, 6, 5, 4, 3, 2])
print("ndcg1", ndcg1(y_true, y_pred))
print("ndcg2", ndcg2(y_true, y_pred))

その他

適合度が10とか100みたいに大きな値のケースではNDCG2を使いづらいですよね…このようなケースではどのように扱うのでしょうか…

参考文献

  • Järvelin, Kalervo, et al. "Cumulated gain-based evaluation of IR techniques." ACM Transactions on Information Systems (TOIS) 20.4 (2002): 422-446.
  • Burges, Chris, et al. "Learning to rank using gradient descent." Proceedings of the 22nd International Conference on Machine Learning. ACM, 2005.

正定値でないグラム行列を正定値に変換して学習すると精度は変化する?

この記事は何?

カーネル法を用いた機械学習では,データセットに対するグラム行列を生成して学習を行いますが,非正定値カーネルを用いた場合やサンプル間の類似度をカーネルとして用いた場合は,得られるグラム行列が正定値にならない場合があります.
得られたグラム行列が正定値でないとき,何も考えずにそのまま扱ってしまう方法と,正定値に変換してから扱う方法の2つの方法が考えられます.
この記事では,正定値でないグラム行列を正定値となるように変換することで予測精度が変化するかどうかを検証します.

正定値性を満たすように変換する方法

赤穂さんによるカーネル多変量解析 - 岩波書店では,第5.2節で正定値でない類似度・距離からの設計として,次の3つの正定値性を満たすように変換する方法を紹介しています.

  • グラム行列Kに単位行列のλ倍を加える
  • グラム行列を固有値展開し,固有値の並んだ対角行列の成分の中で0以下のものを正の小さい値で埋める
  • 指数カーネルを用いる

この記事では,上の2つを実装してみます.

グラム行列Kに単位行列のλ倍を加える方法

 K'=K+\lambda Iとします.また, K固有値 \lambda_i固有ベクトル \textbf{u}_iとします.

ここで, K'=K+\lambda Iに右から \textbf{u}_iをかけると,
 \begin{eqnarray}
K' \textbf{u}_i & = & K \textbf{u}_i + \lambda \textbf{u}_i \\
& = & \lambda_i \textbf{u}_i + \lambda \textbf{u}_i \\
& = & (\lambda_i + \lambda)\textbf{u}_i
\end{eqnarray}
となり, K'固有値 K固有値 \lambdaが加わったものとなります.

行列の全ての固有値が正であることと,行列が正定値であることは同値であるため,行列の全ての固有値が正となるように \lambdaを調整して加えることで正定値性を満たすように変換することができます.

これを実装すると,次のようになります.

def convert_add_lmd(mat, eps=0.0001):
    """
    K' = K + \lmd I
    \lmd is decided as all eigen values are larger than 0
    """
    mat_positive_definite = np.copy(mat)
    eigen_values = np.linalg.eigvals(mat)
    min_eigen_values = np.min(eigen_values)
    if min_eigen_values < 0:
        lmd = -min_eigen_values + eps  # new eigen values are larger than 0
        mat_positive_definite += lmd * np.eye(mat.shape[0])
    return mat_positive_definite

元の行列の最小固有値が正のときは何もしません.最小固有値が負のときは正になるように \lambdaを調整します.

固有値分解を用いる方法

 K=PDP^\textrm{T}として固有値分解されるとします. P固有ベクトルが並んだ行列であり, Dは対角成分に固有値が並んだ行列です.
ここで, Dの対角成分のうち0以下のものを正の小さな値 \epsilonで埋めた行列を D'とします.
得られた D'を用いて, K'=PD'P^\textrm{T}とすると, K'は正定値となります.

これを実装すると,次のようになります.

def convert_with_decomposition(mat, eps=0.0001):
    """
    K' = P D' P^T
    P: eigen vectors matrix
    D: diag eigen values matrix
    D': negative eigen values of D are \epsilon
    """
    eigen_values, eigen_vectors_mat = np.linalg.eig(mat)
    eigen_values[eigen_values <= 0] = eps
    # K' = P D' P^T
    mat_positive_definite = eigen_vectors_mat.dot(np.diag(eigen_values)).dot(eigen_vectors_mat.T)
    return mat_positive_definite

評価

正定値に変換する前と後とで精度が変化するかを検証します.
カーネル関数としてシグモイドカーネルを使います,シグモイドカーネルは正定値カーネルではありません.

データセットとしはscikit-learnのdatasetsモジュールにあるmake_classification関数によって生成します.データサイズを1000としました.

評価は5-fold Cross Validationによる正解率の平均値で行います.

学習器としてSVMを使いました.ハイパーパラメータについて簡単のため,SVMのコストパラメータは1.0,シグモイドカーネル \gammaパラメータは0.1に固定しています.

結果

正解率の平均値は以下のようになりました.

手法 正解率の平均値
正定値への変換無し 0.830
単位行列のλ倍を加える方法 0.931
固有値分解を用いる方法 0.952

グラム行列を正定値に変換してから学習する方が,予測精度が向上することがわかりました.
ハイパーパラメータチューニングは行っていないのですが,精度が大きく変化することから正定値への変換を考慮する必要があると言えます.

ソースコード

評価に用いたソースコードを載せておきます(jupyter notebookベタ貼り)

# coding: utf-8

# In[1]:

import numpy as np
from sklearn import datasets, metrics, model_selection, svm


# In[2]:

np.random.seed(0)


# In[3]:

def convert_add_lmd(mat, eps=0.0001):
    """
    K' = K + \lmd I
    \lmd is decided as all eigen values are larger than 0
    """
    mat_positive_definite = np.copy(mat)
    eigen_values = np.linalg.eigvals(mat)
    min_eigen_values = np.min(eigen_values)
    if min_eigen_values < 0:
        lmd = -min_eigen_values + eps  # new eigen values are larger than 0
        mat_positive_definite += lmd * np.eye(mat.shape[0])
    return mat_positive_definite


# In[4]:

def convert_with_decomposition(mat, eps=0.0001):
    """
    K' = P D' P^T
    P: eigen vectors matrix
    D: diag eigen values matrix
    D': negative eigen values of D are \epsilon
    """
    eigen_values, eigen_vectors_mat = np.linalg.eig(mat)
    eigen_values[eigen_values <= 0] = eps
    # K' = P D' P^T
    mat_positive_definite = eigen_vectors_mat.dot(np.diag(eigen_values)).dot(eigen_vectors_mat.T)
    return mat_positive_definite


# In[5]:

X, ys = datasets.make_classification(n_samples=1000)


# In[6]:

kf = model_selection.StratifiedKFold(n_splits=5, shuffle=True)


# In[7]:

# sigmoid kernel is not positive definite
K = metrics.pairwise.sigmoid_kernel(X, gamma=0.1)


# In[8]:

def acc_evaluation(K):
    acc_scores = []
    for train_index, test_index in kf.split(K, ys):
        K_train, K_test = K[train_index][:, train_index], K[test_index][:, train_index]
        ys_train, ys_test = ys[train_index], ys[test_index]
        clf = svm.SVC(kernel="precomputed")
        clf.fit(K_train, ys_train)
        acc_score = metrics.accuracy_score(ys_test, clf.predict(K_test))
        acc_scores.append(acc_score)
    return np.mean(acc_scores)


# In[9]:

# without convert to positive definite
print(acc_evaluation(K))


# In[10]:

# with adding lmd * I
K_added_lmd = convert_add_lmd(K)
print(acc_evaluation(K_added_lmd))


# In[11]:

# with decomposition
K_with_decomposition = convert_with_decomposition(K)
print(acc_evaluation(K_with_decomposition))

その他

アイテム間類似度をグラム行列として使うとき,正定値かどうか気にせずに学習してる例が多いように見えます.
精度があまり出なかったりする場合は,正定値性を満たすように変換してから学習する方が良いかもしれません(必ず精度が上がる保証は無いのですががが).

Slackerを使ったslack bot開発 & AWS EC2上でcronを使って定期post

この記事は何?

SlackerというSlack APIPythonラッパーを使ってslack botを作ったときのメモです.

Slackerを使った簡単なbot

まず,Slackerをインストールします.

pip install slacker

Slackerについては,公式サイトを参照してください.
github.com

次に,slack api tokenを発行します.
Slack Web API | Slackにアクセスし,Generate test tokensをクリックすると発行できます.
得られたslack api tokenを環境変数にSLACK_API_TOKENとして設定しておきます.

f:id:sz_dr:20170219233535p:plain

Slackerの公式サイトを参考に,"hello from slacker"とpostするだけのbotを作ります.
先程環境変数に設定したslack api tokenをスクリプト内で読み込んでいます.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import os
from slacker import Slacker

def main():
    api_token = os.environ["SLACK_API_TOKEN"]
    slack = Slacker(api_token)
    slack.chat.post_message("#random", "hello from slacker")


if __name__ == '__main__':
    main()

このスクリプトを実行すると,randomチャンネルで"hello from slacker"とpostされるはずです.

AWS EC2上でcronを使って定期post

EC2のcronに上のスクリプトを設定して,定期postするようにします.

まず,EC2上に先程のスクリプトをscpなり何なりでアップロードしておきます.

次に,EC2上でcrontabを設定します.

crontab -e

crontabの編集画面が出て来るので,先程のスクリプトを定期実行するように設定します.
ここで,slack api tokenもcrontabの設定ファイルに書いておきます.
cronの環境変数の設定方法は色々あると思いますが,とりあえず簡単な書き方ということで…

SLACK_API_TOKEN='<your-slack-api-token-goes-here>'

0-59 * * * * '<your-script-path>'

保存して終了すると,1分ごとに定期postされます.

その他

hubotでも良いんですけど,coffee scriptをあんまり触れたことが無かったので…

卒論・修論tips

この記事は何?

卒論・修論を書き始めるときに知っておくと良い知識を研究室内でまとめていたのですが,恐らく研究室外の方々にも役に立つのではないかと思い公開してみます.
基本的にTeXを使った理系の卒論・修論を想定しています,分野毎に作法が異なる点もあるとは思いますが適宜対応してください.

TeXのお作法

このページが分かりやすくまとまっているので,こちらを参照してください.
ichiro-maruta.blogspot.jp

半角と全角

句読点

分野によります.
「,」「.」を使う場合はIMEの設定で変更しておくと簡単です.

括弧

日本語文のときは全角括弧を使います.

✕:新しい物質(成分)を発見する.
◯:新しい物質(成分)を発見する.

英文のときは半角括弧を使いますが,括弧の外側に半角スペースを置きます.

✕:Support Vector Machine(SVM)is a supervised machine learning algorithm.
◯:Support Vector Machine (SVM) is a supervised machine learning algorithm.

日本語文の中に英単語および括弧を用いる場合には宗派があると思いますが,半角括弧+半角スペースで良いと思います.

Support Vector Machine (SVM) を用いた.

引用

引用スタイル

引用スタイルを整えましょう.弊研究室では,引用スタイルが統一されているかどうかをかなり厳しくチェックされました.

また,松尾さんによると

論文の質は、参考文献を見るだけで、ほぼ言い当てることができます

とのことです.

bibitemに参考文献を書いていくときは,

  • 氏名表記(Myoji N.やN. Myojiといった書き方や,何人以上をet al.にするかなど)
  • 雑誌名(省略形にするかフルで書くか,イタリックにするか)
  • vol.issue
  • pages
  • 年号

を統一します.

BibTeXを使うときは,bstファイルを編集することで引用スタイルを統一させることができます.
しかし,bstファイルの編集は面倒なので既存のものを使う方が良いと思います.
こちらのページで紹介されているjecon.bstを編集すると簡単かもしれません.
jecon.bst: 経済学用BibTeXスタイルファイル

参考文献の順番(名前順?引用順?)は分野によって異なるため確認してください.
bibitemで参考文献を書いている場合は,文献を手動で並べる必要があるため注意が必要です.
BibTeXを使っている場合はbstファイルを編集することで名前順や引用順の指定が可能です.
(jplain.bstは名前順に,junsrt.bstは引用順に並びます.)

文献管理ソフト

私は文献管理にはMendeleyを使っています.
Mendeleyにはbibファイルを出力する機能があり,引用の際は非常に楽です.
一方で,Mendeleyが取得する文献情報は間違っていることが多いため注意が必要です(巻・号・ページや雑誌名が省略されていたりなど).
そのため,Mendeleyが出力したbibファイルを用いて引用する際には,必ず文献情報が正しいかチェックする必要があります.
間違っていた場合には,Mendeleyから手作業で文献情報を修正する作業が必要となります.

日本語

接続詞

ひらがなを使ってください.

✕:「~である.従って~」
◯:「~である.したがって~」

係り受け

係り受けの分かりにくい文は避けましょう.
下の引用は係り受けの厳しい文の一例です.


表記ゆれ

一週間くらい卒論・修論を書いていると,だいたい最初の方と最後の方で表記がゆれています.
「ユーザー」or「ユーザ」など注意してください.

参考になりそうな図書

古典としては理科系の作文技術でしょうか.私は結城さんの数学文章作法を参考にしていました.

数字・数式

桁数の大きい数字

「,」を入れてください.「10000000」とか書かれると混乱します.

単位

単位の前には半角スペースを置きます.
ただし,「℃」や「%」のときは半角スペースを置きません.

✕:長さが100mの~
◯:長さが100 mの~
✕:50 %の濃度の~
◯:50%の濃度の~

数式

sin関数やexp関数はTeXのコマンドを使います.

✕:sin(x)
◯:\sin(x)

sin(x)と書いてしまうとsinの部分が斜体となり,sとiとnの積のように見えてしまいます.

図・表

キャプション

図のキャプションは下,表のキャプションは上に書きます.
図表目次を入れているとき,キャプションが長くなってしまうと目次が大変なことになってしまいます.
その場合は,\caption[目次]{内容}を使うと回避できます.

なるべくベクター画像

図はなるべくベクター画像で作りましょう.
ラスター画像は印刷したときには気にならなくても,ディスプレイ上で拡大すると汚くなってしまいます.

PowerPointで図を作っているケースが多いと思いますが,MetafileToEPSConverterを使うとPowerPointの図をEPSに変換してくれます.

その他

なんだかとりとめのない記事になってしまいました.
卒論・修論を先生にチェックしてもらう前に,なるべく自分でチェックできるところはチェックしておくと良いと思います.

スケーリングは訓練データだけでやる?テストデータも混ぜてスケーリングする?

この記事は何?

機械学習における前処理として,特徴量のスケーリングがあります.
スケーリングの有無によって,予測器の性能が変化することがあります.
スケーリングにも様々な手法があります.代表的なものとして,「最小値を0,最大値を1とする0-1スケーリング」や,「平均を0,分散を1とするZスコア」があります.

訓練データとテストデータに対してZスコアによるスケーリングを行うとします.Zスコアによるスケーリングでは各特徴量の平均と分散を求める必要がありますが,次の2つの求め方が考えられます.

  1. 訓練データから各特徴量の平均と分散を求める.
  2. 訓練データとテストデータを合わせて,各特徴量の平均と分散を求める.

意識している人は意識しているでしょうが,どちらが正しい求め方なのでしょうか?この記事では2つの求め方の違いについて考えてみます.

予測精度の違いを確かめてみる

簡単な回帰問題で,2つの平均・分散の求め方による予測精度の違いを確かめてみます.
データセットにはsklearnが用意しているdiabetesデータセットを使いました.
予測器にはLassoを使い,評価にはR^2スコアを用いました.
評価は3-fold Cross Validationによって行いました.

ソースコード

import numpy as np
from sklearn import datasets, preprocessing, cross_validation, linear_model, metrics

np.random.seed(0)
dataset = datasets.load_diabetes()
X = dataset.data
ys = dataset.target
n_folds=3
N = X.shape[0]
kf = cross_validation.KFold(N, n_folds=n_folds, shuffle=True)
def scaling_test(X, ys, kf, scaling_all=False):
    scores = []
    for train_index, test_index in kf:
        X_train, ys_train = X[train_index], ys[train_index]
        X_test, ys_test = X[test_index], ys[test_index]
        
        scaler = preprocessing.StandardScaler()
        if scaling_all:
            scaler.fit(X)
        else:
            scaler.fit(X_train)
        X_train_scaled = scaler.transform(X_train)
        X_test_scaled = scaler.transform(X_test)
        
        model = linear_model.Lasso()
        model.fit(X_train_scaled, ys_train)
        ys_pred = model.predict(X_test_scaled)
        
        score = metrics.r2_score(ys_test, ys_pred)
        scores.append(score)
    mean_cv_score = np.mean(scores)
    print(mean_cv_score, scores)
# scaling with train data
scaling_test(X, ys, kf, scaling_all=False)
    0.48948448111 [0.40182607340355103, 0.51899710839470226, 0.54763026153277639]
# scaling with all data
scaling_test(X, ys, kf, scaling_all=True)
    0.489511363845 [0.4017844911712618, 0.51900200106422789, 0.54774759929815264]

訓練データだけで各特徴量の平均と分散を求めるよりも,訓練データとテストデータを合わせて各特徴量の平均と分散を求めた方が,予測精度がわずかに高いようです.

じゃあどっちを使って評価するべきなの?

予測対象の特徴によって,どちらを使って評価するべきかが変わってくると思います.

  • 予測対象が手元に無い未知データのときを想定するならば,スケーリングは訓練データのみを用いて行うべきです.予測対象となるデータを使ってスケーリングはできません.
  • 予測対象が手元にあるデータ(ラベルは未知)だけのときを想定するならば,スケーリングはテストデータも含めて行って良いと思います.これは,トランスダクティブ学習の考え方を根拠としています.

トランスダクティブ学習では,未知データの予測は行わず,手元のデータの予測のみ行います.予測対象のデータはもう分かっているのだから,そのデータだけを正しく当てられるように学習すれば良い…という考え方です.

その他

スケーリングをしたからといって,予測精度は必ずしも上がらないものです…つらいですね……