Scheduler

学習を行うときの学習率の設定はなかなか悩ましいものがあります。
収束は早くしてほしいけど、きちんとロスの底にたどり着いてほしい。

そういう場合に、Schedulerの機能を使います。Schedulerは学習率を習の途中で変化させることができます。

下の図は、学習率が学習に与える影響を表しています。グラフの底になるように学習をしたいのですが、学習率が高すぎる(左)と底を飛び越えてしまう可能性があり、低すぎる(中央)と収束に時間がかかったり局所解に陥る可能性もあります。
なので、右のように学習の進行に従って学習率を下げていくのが効率的な学習と考えることができます。

PyTorchにもそんなschedulerがいくつか用意されています。
全部見ようと思ったのですが、理解するのが大変そうなので、考え方が分かりやすかったものを2つだけピックアップすることにします。

torch.optim.lr_scheduler.StepLR

一番シンプルなアップデート方法でしょうか。

【書式】StepLR(optimizer, step_size, gamma)

optimizer:ラップ対象のオプティマイザ。SGDとかAdamやらを定義したものが個々に入ります。
step_size:更新タイミングのエポック数
例えば、step_size=30を設定した場合、30, 60, 90, …のタイミングで学習率にgammaが乗算されます。
gamma:更新率。デフォルトは0.1でこの場合、更新タイミングで学習率が1/10になっていきます。

更新タイミングを2エポックごとに設定して、学習率の推移を見ていきます。
学習率の更新はscheduler.step()で、学習率の取得はscheduler.get_lr()で行えます。
※コードは一部を抜き出しています。

num_epoch = 10
scheduler = optim.lr_scheduler.StepLR(opt, step_size=2, gamma=0.1)

for m in range(num_epoch):
    for i_batch, sample_batched in enumerate(train_data):
        x = model(sample_batched[0])
        y = sample_batched[1]

        loss = loss_func(x, y)

        loss.backward()
        opt.step()
        opt.zero_grad()

        print('epoch:{}, lr:{}'.format(epoch, scheduler.get_lr()[0]))       
        scheduler.step()

結果は以下となり、2エポックごとに学習率が1/10になっていることがわかります。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.0001
epoch:3, lr:0.0001
epoch:4, lr:1.0000000000000003e-05
epoch:5, lr:1.0000000000000003e-05
epoch:6, lr:1.0000000000000002e-06
epoch:7, lr:1.0000000000000002e-06
epoch:8, lr:1.0000000000000002e-07
epoch:9, lr:1.0000000000000002e-07

torch.optim.lr_scheduler.MultiStepLR

Multiという単語がついているように、 こちらの関数はより柔軟な設定ができます。

【書式】MultiStepLR(optimizer, milestones, gamma)

違いは、milestonesです。
milestones:学習率の更新エポックのリスト
milestonesのエポックごとに、学習率はgammaを乗算した値になります。

schedulerの設定を、2エポック、6エポックで学習率の更新をするように変更して結果を見てみます。変えるのはこの行だけです。

optim.lr_scheduler.MultiStepLR(opt, milestones=[2, 6], gamma=0.1)

動かした結果です。設定通り、2エポックと6エポックで学習率が1/10になっています。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.0001
epoch:3, lr:0.0001
epoch:4, lr:0.0001
epoch:5, lr:0.0001
epoch:6, lr:1.0000000000000003e-05
epoch:7, lr:1.0000000000000003e-05
epoch:8, lr:1.0000000000000003e-05
epoch:9, lr:1.0000000000000003e-05

他にもいろいろな更新方法があるようなので、かなりフレキシブルな学習が行えるのですね。

追記

スケジューラの動作について、PyTorch1.1にはどうもバグがあるみたいです。
参照:https://github.com/pytorch/pytorch/issues/22107

import torch 
print(torch.__version__) 

1.1.0

PyTorch1.1のバージョンで、StepLR()を動かしてみます。
2エポックごとだと動きが分かりづらいため、step_sizeを4にします。

scheduler = optim.lr_scheduler.StepLR(opt, step_size=4, gamma=0.1)

下に示すように、更新エポックのときだけ学習率がおかしくなっています。gammaが2回かけられているみたいですね。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.001
epoch:3, lr:0.001
epoch:4, lr:1e-05
epoch:5, lr:0.0001
epoch:6, lr:0.0001
epoch:7, lr:0.0001
epoch:8, lr:1.0000000000000002e-06
epoch:9, lr:1e-05

最初に動かした環境がPyTorch1.1だったので、結構はまってしまいました。こういうこともあるんですね。

乱数を固定する

CNNの既存コードを見ていると、torch.manual_seed()なんていう一文があります。
おまじないみたいなものだろうと全然気にしないでいたのですが、調べてみたら深い意味をもつものでしたので、備忘のために書いておきます。

RNG (Random Number Generator:乱数ジェネレータ)

PyTorchに限らず、ランダムな処理を行う場合には、RNGと呼ばれるジェネレータが乱数を生成し、その数字に基づきランダム処理を行っています。

例えば、データセットからバッチを生成するときに、データを取ってくるためのランダム性だったり、重みの初期化に使う乱数だったり。

この乱数は、seedと呼ばれる数字から生成しています。
(ちゃんと理解できていませんが、)seedが同じであれば、同じ乱数が生成されるそうなのです。

同じ乱数が生成されて何が嬉しいかというと、結果に再現性をもたせることができるのです。同じバッチを使って、同じ初期化重みを使えるので、学習は同じ経過をたどりますね。

最初、seedの意味が分からなかったので、何度学習しても同じ画像を同じタイミングで読み込んでいるのがずっと不思議でした。shuffleが機能していないのだと勘違いしたり……
ランダムに取り出すデータが固定されていたからなのですね。

RNGの動作

numpyの関数で、RNGの動作を確認してみます。

import numpy as np

for n in range(5):
    np.random.seed(0) # seedの設定
    x = np.random.randn(1) # 乱数の生成
    print(x)

[1.76405235]
[1.76405235]
[1.76405235]
[1.76405235]
[1.76405235]

出力結果を見ると、seedに同じ値が入っているため、生成される乱数が同一であることが分かります。

ちなみに、以下のようにseedの設定ループを前に出すと、繰り返すたびに乱数は変わります。毎回、同じseedの値を使うわけではなく、1度使ったseedの続きの値を使っているからだそうです。

import numpy as np

np.random.seed(0) # seedの設定
for n in range(5):
    x = np.random.randn(1) # 乱数の生成
    print(x)

[1.76405235]
[0.40015721]
[0.97873798]
[2.2408932]
[1.86755799]

RNGの固定

再現性のある学習を行うために、以下をコードのはじめに設定します。引数としてのseedは任意の数字を入れます。
様々な段階で乱数を使っていますので、完全再現のためには4種類、忘れずに設定しましょう。

np.random.seed(seed)
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)

クロスエントロピーとKLダイバージェンス

機械学習を行うときのロス関数の定義については、いろいろと考えることが多いだろうと思います。
分類などを行うとき、クロスエントロピー(交差エントロピー)KLダイバージェンスというロス関数がしばしば登場しますが、理解しないまま使っていましたので、その意味について調べてみました。

エントロピー

まずは、エントロピーとは何か、ということからです
wikipediaでは、以下のように記述されています。

情報理論においてエントロピー確率変数が持つ情報の量を表す尺度で、それゆえ情報量とも呼ばれる。 確率変数Xに対し、XのエントロピーH(X)は
\(H(X)=-\sum _{i}P_{i}\log P_{i}\) (ここでPiX = iとなる確率)

wikipedia

情報量とはざっくり言うと、ある確率に基づいて発生する事象に対して、発生した事象をどの程度絞り込みができるかを表したもので、つまり、情報の有益度合いを示すものです。

例えば、サイコロを振って出た目を推測する場合、以下の2つの情報を比較すると、(2)のほうが目を当てやすくなっています。
 (1) 偶数が出た:1/6 -> 1/3
 (2) 2以下の目が出た: 1/6 -> 1/2
なので、(2)のほうが情報量が多いということができます。

また、エントロピーとは「予測できなさ」とも言うことができます。
これは、情報量と逆を表していることはすぐにわかるのではないでしょうか。情報量が多くて、推定のための材料が多ければ、予測も容易になりますが、情報量が少なければ、予測は当然、困難になります。

では、サイコロの目を予測する場合、それぞれの目が出る(\(X=X_1, X_2, …, X_6\))確率を、\(P=P_1, P_2, …, P_6\)とすると、
事象Xのエントロピーは、
$$H(x)=-\sum_{n=1}^6 P_n log P_n$$
と定義されます。

どれだけランダム性があるかを表す指標で、サイコロの例の場合、6面がそれぞれ1/6の確率で出る場合が最もエントロピーが大きくなります。

例えば、
 (1) すべてが1/6の確率で出るサイコロ
 (2) 1の目が1/2、他が1/10の確率で出るサイコロ
のエントロピーを計算してみると、(1) \(H(x) = 0.778\)、(2) \(H(x) = 0.651\)となります。

つまり、(2)の方がエントロピーが小さい=予測しやすいということになります。とりあえず、1を予測しておけば、50%の確率で当たるわけですから、感覚的にも納得です。

交差エントロピー

続いて、交差エントロピーについてみていきます。
交差エントロピーは、正解値と推定値の比較を行うときによく使用されます。

正解の確率分布を\(p(x)\)、推定した確率分布を \(q(x)\)としたとき、交差エントロピーは、
$$H(p, q) = -p(x) log q(x) $$
で表されます。
元のエントロピーの式のlogの中を、推定した分布に変えたものになります。

これが何を意味するのか、式の導出は遡っていかないと分からないため、今回は書き(書け)ませんが、 \(p(x)\)の分布を想定したときに、 \(q(x)\)がどのくらい予測しにくいかを表す指標だと理解しています。
\(p(x)\)の分布それ自体にも、エントロピーはあるのですが、\(q(x)\)は \(p(x)\)と一致しません。そのため、一致しない分だけ \(q(x)\)のほうがより予測しにくくなっていると考えられます。

なお、 \(p(x)=q(x)\)の時、交差エントロピーとエントロピーは一致します。

バイナリクロスエントロピー

ちなみに、2値分類に限定したものとして、バイナリクロスエントロピーという指標もあります。
下の式で表され、これを見れば、交差エントロピーの\(N=2\)の場合を取りだしたものであることが分かります。
$$D_{BC}=−P(x=0)log Q(x=0)−(1−P(x=0))log(1−Q(x=0))$$
なお、\(P(x=0)\)、\(Q(x=0)\)は共に、\(x=0\)となる確率を指します。

KLダイバージェンス

交差エントロピーは \(p(x)\)を前提としたときに、 \(q(x)\)の予測しにくさを表す指標でした。
しかし、正解分布である \(p(x)\) と推定分布 \(q(x)\)がどのくらい似ているかについては、交差エントロピーではよく分かりません。そこで、KLダイバージェンスの指標が登場します。

KLダイバージェンスは、
$$D_{KL}(p||q)=\int_{-\infty}^{\infty} p(x)log{\frac{p(x)}{q(x)}} dx $$
$$D_{KL}(P||Q)=\sum_x P(x) log{\frac{P(x)}{Q(i)}}$$
で表されます。
上が連続関数、下が離散関数の場合です。(意味合いとしては同じですので、以降では離散関数の式だけ使います)

ぱっと見、何を意味しているのか分かりにくいですが、logの中身を展開してみると理解しやすいと思います。
$$ D_{KL}(P||Q)=\sum_x {P(x) log P(x) – P(x) log Q(i)}$$
右辺の第1項は、\(P(x)\)のエントロピー、第2項は\(P(x)\)と\(Q(x)\)の交差エントロピーであることが分かります。

つまり、KLダイバージェンスとは、 確率分布\(P(x)\)をベースとしたとき、 \(Q(x)\)はどれだけエントロピーがあるかを示すものになります。当然、 \(P(x)=Q(X)\)の場合は、 \(D_{KL}(P||Q) =0\)です。
また、\(D_{KL}(P||Q) \neq D_{KL}(Q||P)\)のため、注意が必要です。

交差エントロピーは、予測のしにくさそのものを表していた一方、KLダイバージェンスは、 \(P(x)\)から\(Q(x)\)の“相対的”な予測のしにくさを表します。
言い換えれば、 \(P(x)\)から\(Q(x)\)がどれだけ似ているかを意味します。

ロス関数として扱うときは、交差エントロピーもKLダイバージェンスも、数値を最小とするという点では共通していますが、KLダイバージェンスのほうが感覚的に分かりやすい気がします。

データの白色化 (2/2)

前回はデータの共分散行列の表しかたについてまとめました。共分散行列は、要素ごとの分散と、要素間の相関関係を表す行列です。

では、白色化の目的である【要素間の相関をなくす】とはどういうことでしょうか。 下は\(n \times n\)の共分散行列です。

行列内の非対角成分(水色部分)は要素間の相関関係を表し、ここがゼロでない=各成分間で相関があるということです。したがって、この共分散行列を対角成分以外がゼロ、つまり共分散成分がゼロになる対角行列にしてしまえば、各成分が独立となります。
そのために、共分散行列が対称行列になるように元となる特徴空間\( {\it{X}}=(x_1, …, x_n)\)を変形させなければなりません。

言い換えると、特徴空間\(x_n\)に対して、\(D\times D\)行列\( \it {P}\)による線形変換を行い、\( {\it{u}_n}= {\it{Px_n}}\)を作ります。このとき、\({u_n}\)の共分散行列、
$$\Phi_u \equiv \frac{1}{N}\sum^N_{n=1}u_n u_n^T=\frac{1}{N} {\it{UU}^T}$$
が対角行列になるような\(\it{P}\)を定めるということです。

目標とする対角行列を単位行列\(\it{I}\)( \(\Phi_u= \it{I}\) )として、\(\it {P}\)の満たす式べきを求めます。この式は、 \( \it{U}= \it{PX}\)を代入することで、\(\it{P^T P}=\Phi^{-1}_x\)となります。


(多分)導出

この式の過程がよくわからなかったので、自分なりに道筋を考えてみました。

まず、\((\Phi_x=)\frac{1}{N} \it{U U^T}= \it{I}\)に\(\it{U}= \it{PX}\)を代入します。
$$\frac{1}{N} \it{U U^T}= \it{I}\\
\frac{1}{N} \it{PX}( \it{(PX)^T}= \it{I}\\
\frac{1}{N} \it{PX X^T} \it{P^T}= \it{I}$$

右から\( \it{P^{-1}}\)、左から\(\it{({P^T})^{-1}}\)を掛けます。

$$\frac{1}{N} \it{X} \it{X^T} = \it{P^{-1}(P^T)^{-1}}\\
\frac{1}{N} \it{X} \it {X^T}=(\it{P^T} \it{P})^{-1}\\
\frac{1}{N}( \it {XX^T})^{-1}= \it {P^T P}\\
\Phi_x^{-1}= \it {P^T P}$$

導出できました。
途中の式変形に、\(\it{(AB)^T}= \it{B^T A^T}\)と\(\it {(AB)^{-1}}= \it{B^{-1}} \it {A^{-1}}\)を使用。

これで\(x\)の共分散行列から\( \it {P}\)が求められることがわかりました。

固有値問題への落とし込み

白色化は主成分分析と似た考え方からできています。
主成分分析は、情報を圧縮するために分散が最大となる(つまりデータ間で差がでやすい)軸を調べます。

白色化も求めるところは、分散がばらけるようにベクトルを投影しなおすということで共通しています。

主成分分析の軸を求めるのには、固有値問題を解く必要があり、 白色化も同様に、固有値の問題に落とし込むことができるそうです。(主成分分析については、いずれ調べてまとめます)

\(\Phi_x\)を固有値\(D\)と固有ベクトル\(E\)に分解します。
$$\Phi_x= \it{EDE^T}$$
また、\(\it{P^T P}=\Phi_x^{-1}\)より、\( \it{P}= \it{QD^{-\frac{1}{2}}E}\)に変形することができます。なお、\( \it{QQ^T}= \it{I}\)です。

この変形もよくわからなかったので、細かく見ていきます。

まず、\( \it{P^T P}= \it{E} \it{D E^T}\)を以下のように分解します。
$$ \it {P^T P}= \it{ED^{-\frac{1}{2}}} \it{Q^T Q} \it {D^{-\frac{1}{2}}} \it{E^T}$$
\( \it{QQ^T}= \it{Q^T Q} = \it{I}\)なので、このように分解できます。
$$ \it{P^T P}=(\it{Q} \it{D^{-\frac{1}{2}}} \it{E})^T \it{Q} \it{D^{-\frac{1}{2}}} \it{E^T}\\
\it{P}= \it{Q} \it{D^{-\frac{1}{2}}} \it{E^T}$$
ということだと思います。

ただし、ここでは\(\it{QQ^T}= \it{Q^TQ} = \it{I}\)以外に\( \it{Q}\)に対する制約はなく、\( \it{P}\)は\( \it{Q}\)のとりうる値の範囲で複数存在することになります。

PCA白色化

このように、分散が最大になるように各ベクトルを定める(変形する)方法は、前述のように主成分分析(principal component analysis, PCA)に似た考えです。そのため、これをPCA白色化※と呼ぶそうです。
※もしかしたら、本書だけの呼称かもしれません。

ゼロ位相白色化

PCA白色化では自由度を持っていた\(\it{Q}\)に対し、\( \it{P}\)を対称行列(\( \it{P}= \it{P^T}\))に制限することで、一意に定める考えがあります。
式は以下のように、固有値と固有ベクトルだけで表されます。

$$ \it{P_{ZCA}}= \it{ED^{-\frac{1}{2}} E^T}$$

この式変形の過程は、以下のようになります。
$$ \it{PP^T}= \it{ED^{-1} E^T}\\
\it{P^2}= \it{ED^{-\frac{1}{2}} E^T ED^{-\frac{1}{2}} E^T} = ( \it{ED^{-\frac{1}{2}}E^T)^2}$$
ここで、\( \it{EE^T}= \it{E^T E}= \it{I}\)を使っています。

このような白色化は、ゼロ位相白色化(zero-phase whitening)もしくは、ゼロ位相成分分析(zero-phase component analysis, ZCA)などと呼びます。

名前の由来は、\(\it {P}\)の各行ベクトルを\(x\)に適用するフィルタと見たとき、それが対称であること(ゼロ位相であること)に由来するそうですが、よく理解できませんでした。

前処理に関してはかなり面倒そうですが、関数で提供されていたりするのでしょうか?

データの白色化 (1/2)

ディープラーニングの教科書として、岡谷貴之「深層学習」を読んでいます。
一巡目は、タイトルの項目が数式の嵐で、飛ばしてしまっていましたので、ひとつひとつ理解してみることにしました。なお、ここで使う記号は、上記の本に準拠します。

白色化とは

【訓練サンプルの成分間の相関をなくす目的】で行う処理です。オートエンコーダの説明の文脈で登場する内容で、よりよい特徴を獲得するための処理として扱われています。

サンプルの特徴量がD次元で表されるとした場合、\(x=[x_1, …., x_d]\)の任意の2成分\(x_p, x_q\)の間の相関をなくすことを意味します。

例えば、\(x_p\)が増えると\(x_q\)も増える、という関係がある場合、\(x_p\)と\(x_q\)のどちらかで十分ではないか、ということでしょう。
特徴量はデータを端的に表現するためのものですが、相関があるということは、うまく特徴を取れていないことを意味します。

後々出てきますが、これは主成分分析と似た考えを持っています。情報をもっとも効率よく圧縮するにはどうすればよいか、という観点です。

共分散行列

白色化を行うには共分散行列を使います。
共分散行列とは、ベクトル間の相関関係を表す行列で、以下のようなものです。

$$\Sigma = \begin{pmatrix}
\sigma^2_1 & \sigma_{12} \\
\sigma_{12} & \sigma^2_2 \\
\end{pmatrix}$$

ここで、
\(\sigma^2_1\):\(x_1\)の分散
\(\sigma^2_2\):\(x_2\)の分散
\(\sigma_{21}\):\(x_1\)と\(x_2\)の共分散 を表します。

分散・共分散

ここで出てきた、分散・共分散について確認します。

分散

\(\sigma^2_1 = (x_1-\overline{x_1})^2\)で表され、平均値に比べてどのくらい散らばりがあるかを表します。

共分散

\(\sigma_{12} = (x_1-\overline{x_1}) \times (x_2-\overline{x_2})\)で表され、\(x_1\)と\(x_2\)の散らばり具合の相関を表します。

つまり、分散で成分ごとの散らばり具合を、共分散で成分間の相関を見ることができます。

なお、共分散行列はn*nの大きさでも同じ構造で表すことができます。

$$\Sigma =
\begin{pmatrix}
\sigma^2_1 & \sigma_{12} & \cdots & \sigma_{1n}\\
\sigma_{21} & \sigma^2_2 & \cdots & \sigma_{2n}\\
\vdots & \vdots & \ddots & \vdots\\
\sigma_{n1} & \sigma_{n2} & \cdots & \sigma^2_n\\
\end{pmatrix}$$

対角成分が分散、それ以外の成分が共分散で構成される対称行列(*)であることがわかります。
* \((x_1-\overline{x_1}) \times (x_2-\overline{x_2}) = (x_2-\overline{x_2}) \times (x_1-\overline{x_1})\) ですので、対角成分以外はひっくり返しても同じになりますね。

全サンプルの共分散行列

では、D次元の特徴空間からなるN個のデータがあるとします。

\({\bf\it{X}}_1, {\bf\it{X}}_2, …, {\bf\it{X}}_N\)
ここで、
\({\bf\it{x}} = [x_1, x_2, …, x_D]\) です。

このN個のサンプルの分散、共分散の平均(\(\Phi_x\))を式で表すと、以下のようになります。
$$\Phi_x \equiv \frac{1}{N} \sum_{n=1}^N {\bf\it{x}}_n{\bf\it{x}}_n^T = \frac{1}{N} {\bf\it{X}}{\bf\it{X}}^T$$

ちょっとわかりにくいので、少し内容を見てみましょう。
まずは、\(\frac{1}{N} \sum^N_{n=1} \bf\it{x}_n \bf\it{x}_n^T\)の項について。これは、データ単位での表現です。
\(\bf\it{x}=(x_1, …, x_D)\)なので、(説明のため、\(n\)番目のデータの場合、\({\bf\it{x}_n} = (x^n_1, x^n_2, …, x^n_D)\)と書きます)

$$ \frac{1}{N} \sum^N_{n=1}{\bf\it{x}}_n{\bf\it{x}}_n^T = \frac{1}{N} \sum^N_{n=1}\begin{pmatrix}
x^n_1\\
\vdots \\
x^n_D \\
\end{pmatrix}\begin{pmatrix}
x^n_1 & \cdots & x^n_D \\
\end{pmatrix} = \begin{pmatrix}
x^n_1 x^n_1 & \cdots & x^n_D x^n_D \\
\vdots & \ddots & \vdots \\
x^n_1 x^n_D & \cdots & x^n_D x^n_D\\
\end{pmatrix}$$

なので、以下のように書けます。
$$ \frac{1}{N} \sum^N_{n=1}{\bf\it{x}}_n{\bf\it{x}}_n^T = \frac{1}{N} \sum^N_{n=1}\begin{pmatrix}
(\sigma^n_1)^2 & \cdots & \sigma^n_{D1} \\
\vdots & \ddots & \vdots \\
\sigma^n_{D1} & \cdots & (\sigma^n_D)^2\\
\end{pmatrix} $$

1つのデータに対する共分散行列の総和をとって、\(N\)で割っていますので、共分散行列の平均値になることはすぐにわかります。

続いて、\(\frac{1}{N} {\bf\it{X}}{\bf\it{X}}^T\)について考えてみます。 こちらは、\(\bf\it{X}=(\bf\it{x}_1,…,\bf\it{x}_n)\)なので、全データをまとめている表現しているということになります。

$$\frac{1}{N} {\bf\it{X}}{\bf\it{X}}^T =
\begin{pmatrix}
x_1^1 & \cdots & x_1^N \\
\vdots & \ddots & \vdots \\
x_1^D & \cdots & x_D^N \\
\end{pmatrix}
\begin{pmatrix}
x_1^1 & \cdots & x_1^D \\
\vdots & \ddots & \vdots \\
x_1^N & \cdots & x_D^N\\
\end{pmatrix}
= \begin{pmatrix}
x_1^1 \times x_1^1+\cdots + x_1^N \times x_1^N & \cdots & x_1^1 \times x_D^1+\cdots + x_1^N \times x_D^N \\
\vdots & \ddots & \vdots \\
x_D^1 \times x_1^1 + \cdots +x_D^N \times x_1^N& \cdots & x_D^1 \times x_D^1 + \cdots x_D^N \times x_D^N\\
\end{pmatrix}$$

となり、\(\bf\it{XX}^T\)ですでに総和が求められていることがわかります。

ヤコビ行列

ヤコビ行列とは何ぞやということで、調べてみました。
なお、スーパー初心者が自分でどうにかかみ砕いた結果を書いていますので、数学的な正確性についての保証はありません。

まずはwikiの記述から。

数学、特に多変数微分積分学およびベクトル解析におけるヤコビ行列(やこびぎょうれつ、Jacobian matrix)あるいは単にヤコビアン[1]または関数行列(かんすうぎょうれつ、Funktionalmatrix)は、一変数スカラー値関数における接線の傾きおよび一変数ベクトル値函数の勾配の、多変数ベクトル値関数に対する拡張、高次元化である。

wikipedia

「接線の傾き」とか「勾配」という単語からも予想できるように、微分積分に関連しています。ヤコビ行列は特に、多変数の関数についてのものです。

置換積分

ヤコビ行列を理解するのに、まず、カギとなるのは「置換積分」です。
「置換積分」のおさらいをします。

そのままだと積分しにくい複雑な式について、式の一部をより単純な形の式で置き換えることによって積分しやすい形に変換するという手法です。
例えば、下のような定積分を考えます。
$$F(x) = \int_a^b f(x) dx$$
ここで、\(f(g(t))\) となる関数\(g(t)\)を定義します。変数を\(x\)から\(t\)に変えるわけです。
さらに、\(a \le x \le b \Rightarrow \alpha \le t \le \beta \) と積分範囲も式に合わせて変換します。

そうすると、最終的に以下の式で結果を求められるようになります。
これが「置換積分」です。
$$\int_a^b f(x) dx = \int_\alpha^\beta f(g(t))g'(t) dt$$
簡単な例として、以下のような式をイメージすることができます。
$$\int_a^b (2x+1)^2 dx$$
このとき、\(t = 2x+1\) とすると、\(f(x) = f(g(t))\)、\(g(t) = t\)と書き換えることができます。
なお、\(a \le x \le b \Rightarrow \frac{a}{2} \le t \le \frac{b}{2} \) です。

そのため、最終的に求めるべきは、
$$\int_\frac{a}{2}^\frac{b}{2} f(g(t))g'(t) dt
=\int_\frac{a}{2}^\frac{b}{2} t^2 \frac{dt}{2}$$
と、非常にシンプルな形に変わります。
また、 \(\frac{dt}{dx} = 2\) なので\(dx = \frac{dt}{2}\) です。

さて、上で求めたものは変数がひとつ(\(=x\))に対する積分でした。
では変数が複数、つまり多変数の場合どうすればよいか? といった時に登場するのが「ヤコビ行列」です。
wikiで「多変数ベクトル値関数に対する拡張、高次元化である。」と書かれているのはそのためですね。

多変数への拡張

簡単に2つの変数を持つ関数 \(f(x, y)\) を考えます。
この関数は、\(x = \phi (u, v), y = \psi (u, v)\) で表されるとします。

イメージしにくい場合は、\(xy\)平面から\(\theta\), \(r\)への極座標への変換
\(x =r cos\theta, y = rsin\theta\)
を考えてみるといいかもしれません。 ある座標から別の座標への変換ということですね。
微分しづらい座標系を、もっと簡単な座標系に投影して計算をするという意味合いでしょうか。

この場合、\(u\)と\(v\)が微笑値変わった時の\(x\)と\(y\)の値、つまり\(x\)と\(y\)の全微分はどのようになるでしょうか。

全微分

また新たな単語が出てきましたので、整理します。
「全微分」とは、多変数の関数において、各変数の\(偏微分\times 無限小\)をすべての変数について足し合わせたものを指します。
\(x\)と\(y\)からなる2変数\(f(x, y)\)の全微分は下の式で表せます。
$$f'(x, y) = \frac{\partial f}{\partial x}*dx + \frac{\partial f}{\partial y}*dy$$
\(\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y} \) :\(x, y\)それぞれの傾き
\(dx, dy\) :\(x, y\)の微小値

全部の変数が少しだけ変わった時の傾き=関数全体の傾きを求めていることになります。

ヤコビ行列の導入

というわけで、あらためて \(x = \phi (u, v)\), \(y = \psi (u, v)\)に対する全微分を考えます。全微分の式に従って、\(dx, dy\)は、以下のように書き表せます。

$$dx = \frac {\partial \phi}{\partial u}du + \frac {\partial \phi}{\partial v}dv$$
$$dy = \frac {\partial \psi}{\partial u}du + \frac {\partial \psi}{\partial v}dv$$

これを行列の形に整理して、
$$\begin{pmatrix}
dx\\
dy
\end{pmatrix}
= \begin{pmatrix}
\frac {\partial \phi}{\partial u} & \frac {\partial \phi}{\partial v}\\
\frac {\partial \psi}{\partial u} & \frac {\partial \psi}{\partial v }
\end{pmatrix}\begin{pmatrix}
du\\
dv
\end{pmatrix} $$

右辺の1つ目の行列を見ると、異なる座標系\(x, y\)と\(u, v\)の橋渡しをしてくれていることがわかります。これが「ヤコビ行列」です。(ようやくたどり着きました)

【ヤコビ行列】
$$ \begin{pmatrix}
\frac {\partial \phi}{\partial u} & \frac {\partial \phi}{\partial v}\\
\frac {\partial \psi}{\partial u} & \frac {\partial \psi}{\partial v }
\end{pmatrix} $$

ちなみに、3次元以上になっても同じ考えで行列式を作ることができます。

では、学習したヤコビ行列を胸に、次回は再びPyTorch Tutorialに戻ります。
ヤコビアンとかその他については、また必要に迫られた時に調べます。

テンソルを作る

では、さっそくPyTorchを動かしてみようと思います。

ライブラリのインポート

from __future__ import print_function
import torch

さっそく「__future__」とはなんぞや? という疑問が生じました。
(スーパー初心者ですので)

”__future__”というのは、python2系を動かす際、互換性のないpython3系も動かせるようにするためのものらしいです(参考)。

ということは、python3を使っている私の環境だと1行目は不要ということかもしれませんね。入れたらダメということはないと思いますので、チュートリアル通りに動かしてみることにします。

テンソルの作成

■ 初期化なし

初期化されていない\(5*3\)のマトリックスを作成します。

x = torch.empty(5, 3)
print(x)
# -----出力-----
tensor([[-3.1582e+35,  4.5570e-41, -3.1582e+35], 
        [ 4.5570e-41,  3.8235e-14,  7.2296e+31], 
        [ 5.6015e-02,  4.4721e+21,  1.8042e+28], 
        [ 8.1477e-33,  1.3563e-19,  1.6114e-19], 
        [ 2.8175e+20,  4.5103e+16,  1.4585e-19]])

この結果を見る限り、使用するデータ型で取りうる値全体の範囲でランダムな値を格納するみたいです。
同じ値がちょいちょい出てきているのがなぜか気になりますが、とりあえず、箱だけ作るとイメージしておくことにします。

■ ランダムに初期化

ランダムに初期化された \(5*3\) のマトリクスを作成します。

x = torch.rand(5, 3)
print(x)
# -----出力-----
tensor([[0.5287, 0.4393, 0.9229],
        [0.3146, 0.8343, 0.1759],
        [0.0451, 0.3627, 0.3233],
        [0.2978, 0.5777, 0.9817],
        [0.3242, 0.8804, 0.8938]])

ほほう、確かにランダムな値が入っていますね。ランダムで初期化をする際には、マイナスの値は入らないんですね。

■ ALLゼロ

次に、全部ゼロが入ったlong型の5×3のマトリクスです。
ここでは”dtype”でデータ型も一緒に指定しています。
numpyの場合も、np.zeros()で配列を作れるので、感覚的にわかりやすいですね。

x = torch.zeros(5, 3, dtype=torch.long)
print(x)

5×3の配列が作成されました。

# -----出力-----
tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])

dtypeをfloatに指定すると「0」->「0.」に変わります。
小数点以下がありますよ、ということですね。これはPythonでも同じなので、特に気にすることはありませんが。

# -----出力-----
tensor([[0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.],
        [0., 0., 0.]])

★ちなみにlong型は4バイトの符号型の整数を表現するためのもので、 \(-2^{31}~+2^{31}-1\) まで表せるものだそうです。データ型もいまいちよくわかっていないので、よく使うものだけでも勉強したいと思います。

■ 値の直接指定

値を直接指定してテンソルを作ることもできます。

x = torch.tensor([5.5, 3])
print(x)

小数と整数が混在していますが、精度の高い方に合わせてデータ型が決まります。

# -----出力-----
tensor([5.5000, 3.0000])

多分ですが、この配列はfloat型です。
試しに、double型を指定してテンソルを作ってみたところ、出力が以下のようになりました。

—–出力—–
tensor([5.5000, 3.0000], dtype=torch.float64)

小数値はfloat型が一般的(デフォルト?)なので、あえて出力に型を書いていないのでしょう。

■ 既存テンソルベース

あとは、既存のテンソルを使って新しいテンソルを作ることもできます。
データ型とかサイズとかを合わせないとダメなケースがたくさんあると思いますので、これは覚えておいた方が良さそうです。

既存のテンソルxのサイズ・型変換、1埋めを行ったテンソルを作ります。

x = x.new_ones(5, 3, dtype=torch.double)
print(x)

サイズ1×2のテンソルxが5×3に変わり、型もdoubleになっています。

# -----出力-----
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)

全部変わってるじゃん!!って思いました。
型以外にも、後々出てくるdeviceの設定とか、requires_gradの設定とかを改めてやらなくていい分、全く新しいのを作るよりも楽になるのでしょうかね。

上で作ったテンソルと同じサイズで、ランダム値が入ったテンソルを作ります。データタイプも合わせて変えています。

x = torch.randn_like(x, dtype=torch.float)
print(x)

こんな感じです。
dtype=torch.float64の文字が消えていますので、double->floatになったことがわかります。

# -----出力-----
tensor([[ 0.9637, -0.9373, -0.2481],
        [-0.6289, -0.1012, -0.3534],
        [ 0.5662,  0.7798,  1.5038],
        [ 1.0916, -0.3201, -1.7198],
        [ 1.3004,  1.4057, -0.6745]])
■ サイズ確認

テンソルのサイズを調べるためには、「.size()」を使います。

print(x.size())
# -----出力-----
torch.Size([5, 3])

これはPythonと同じですね。出力はタプルなので、行と列をそれぞれ、

row, col = x.size()

として別個で変数に入れることもできます。

というわけで、今日はここまで。

pytorchチュートリアルはじめました

Deep Learningを扱うにあたって私が使うフレームワークはpytorchです。ほかのフレームワークと比べてどんな特徴があるのか、なんかもそのうち調べていきたいと思いますが、今はこれ以外知りませんのでまずは動かせるところまでもっていきます。

pytorchの公式チュートリアルはここにあります。ここの項目を順次勉強していきます。とはいっても、pythonもほぼ素人ですのでコードの意味を読み解きつつ、将来的には自分でちゃんとネットワークを組めるようにしたいです。

PyTorchってなんだ?

ということで、チュートリアルの導入に当たる「WHAT IS PYTORCH?」からはじめていきます。

PyTorchとはPythonベースの科学計算用のパッケージで、以下の2つの特徴があるそうです。

  • GPUのパワーを使うためにnumpyの代わりに使用する
  • ディープラーニングの研究のためのプラットフォームで、最大のフレキシブルさと速度を持つ

CPUではなくGPUで処理を行うために開発されたものだということですね。
ちなみに、GPUとはいわゆるグラフィックボードのこと(私はこれまでゲーム用途でしか知りませんでした……)。行列演算にはCPUよりも適しているそうです。

ディープラーニングは基本、行列演算なので、画像の取り扱いを主眼に作られたGPUはぴったりなんだろうと。

PyTorchはテンソルというデータ形式で処理を行います。
テンソルはnumpyのndarrayに似た作りで、GPU上で処理できるようになっています。

ndarrayというのは、n-dimential arrayのことで、直訳すると「n次元配列」。任意の次元の行列の配列を作れて、ndarrayを使うと効率よく処理を行えるらしいです。
ただし、データ型(intとかfloatとか)は配列内で統一させなくてはいけません。

PyTorchインストール

次回、テンソルを作ってみるとして、とりあえずはPyTorchを使えるように、インストールだけしておきます。

pip install torch, torchvision

ちなみに、私のマシン環境はpython3.5で、anacondaが入っています。
その他のライブラリは動かして怒られたら入れていく方針です。

環境を作るのも大変だったので、環境設定周りもいずれ、まとめておこうと思います。