人間だったら考えて

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

/etc/shadowの暗号化されたパスワードを確認

CentOS7では/etc/shadowで各ユーザーのパスワードを管理しているとのことなので、どのように管理しているのかを確認してみます。

まず、CentOS7で新しくユーザー(test)を追加します。

useradd test

次に、追加したユーザーにパスワードを付与します、ここでは"himitu"としておきます。

passwd test

この状態で、/etc/shadowを確認してみます。

cat /etc/shadow | grep test
test:$6$U.yvs5yq$R4m.GKg/C/uY/BK2/Ymo2DUjNJzYLbwFPiRhMtCtXdsqLoe9mpV37W4czwbO6Npq7doe2iTsr579s3JnpJUh6/:17304:0:99999:7:::

1番目のフィールドにユーザー名、2番目のフィールドに暗号化されたパスワードが入っています。
暗号化されたパスワードの先頭に"$6$"という文字列がありますが、これはパスワードの暗号化の際に用いられたアルゴリズムを表しており、"6"はSHA-512を表しています。(cryptのマニュアルを参照)

pythonのcryptモジュールで同様にパスワードを暗号化してみます。SHA-512の暗号化の際にはsalt文字列を渡します。

python -c 'import crypt; print(crypt.crypt("himitu", salt="$6$U.yvs5yq"))'
$6$U.yvs5yq$R4m.GKg/C/uY/BK2/Ymo2DUjNJzYLbwFPiRhMtCtXdsqLoe9mpV37W4czwbO6Npq7doe2iTsr579s3JnpJUh6/

当たり前ですが、上の結果と一致していることがわかります。

これで、オレオレパスワード管理ができますね(・∀・)

複数のインターフェースを実装したクラスに対するオーバーロード問題

2つのインターフェース(interfaceA, interfaceB)を実装したクラス TestAandB を考えます。

interface interfaceA {}

interface interfaceB {}

class TestAandB implements interfaceA, interfaceB {}

interfaceA型のオブジェクトを引数に取る関数 test(interfaceA iA) を定義します。

public static void test(interfaceA iA) {
	System.out.println("interfaceA");
}

以下のようにmain関数を実行すると、"interfaceA"と出力されます。

public class Main {

	public static void main(String[] args) {
		TestAandB obj = new TestAandB();
		test(obj);
	}
	
	public static void test(interfaceA iA) {
		System.out.println("interfaceA");
	}
	
}

次に、interfaceB型のオブジェクトを引数に取る関数 test(interfaceB iB) を追加で定義します。(オーバーロード

public class Main {

	public static void main(String[] args) {
		TestAandB obj = new TestAandB();
		test(obj);
	}
	
	public static void test(interfaceA iA) {
		System.out.println("interfaceA");
	}

	public static void test(interfaceB iB) {
		System.out.println("interfaceB");
	}
	
}

このとき、test関数はどちらが実行されるのでしょうか?


実は、このコードはコンパイルできません。

Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
	The method test(interfaceA) is ambiguous for the type Main

どちらのtest関数を呼べばいいのか分からないので、"ambiguous"だと言ってくれます。

今回は単純な例でしたが、親クラスの方でインターフェースを実装していることに意識を向けていなかったりすると、このようなコードを書いてしまう(?)かもしれないですね。。。

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をあんまり触れたことが無かったので…