乱数を固定する

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\)ですでに総和が求められていることがわかります。

meshgrid

格子状の2次元配列を作るときに使います。前回の等差数列を作る関数と合わせて使うことも多いと思います。

【書式】X, Y = np.meshgrid(x1, x2)
 引数には2つの1次元配列を指定します。

文章だけだと説明しづらいため、実際に動かしてみます。
まず、元となる1次元配列を作ります。

x = np.arange(0,5)
y = np.arange(0,10)
print(x)
print(y)

# x
array([0, 1, 2, 3, 4])

# y
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

ここで作った配列を引数にして2次元配列を作ります。

xx, yy = np.meshgrid(x,y)
print(xx)
print(yy)

# xx
array([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4],
[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]])

# yy
array([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4],
[5, 5, 5, 5, 5], [6, 6, 6, 6, 6], [7, 7, 7, 7, 7], [8, 8, 8, 8, 8], [9, 9, 9, 9, 9]])

2次元配列のイメージにすると、下図のようになります。
meshgridは2次元の配列の全組み合わせを作ってくれるということですね。

また、\(X=xx(i,j)\)、\(Y=yy(i,j)\)として、\((X, Y)\)をみると、\((i, j)\)と値が一致します。つまりどういうことかというと、座標情報を計算可能な形にできるということです。

PyTorch

pytorchでも同じようなことができます。

x = torch.arange(0,5)
y = torch.arange(0,10)
print(x)
print(y)     

# x
tensor([0, 1, 2, 3, 4])

# y
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

同じように、torchのmeshgridで配列を作ります。

xx, yy = torch.meshgrid(x, y)
print(xx)
print(yy)

# xx
tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 4, 4, 4, 4, 4, 4, 4, 4, 4]])

# yy
tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])

numpyの時と形状が違いますね。
図にすると、下のような感じです。

メモリの使い方が関係あるようでよくわからないのですが、PyTorchの場合、y方向(画像だと高さ方向)が順序として先に扱うみたいです。

numpyと同じ形状にしたい場合は、xとyの並びを逆にします。この差は意外にハマりどころですので要注意です。

yy, xx = torch.meshgrid(y, x)
print(yy)
print(xx)    

# yy
tensor([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5], [6, 6, 6, 6, 6], [7, 7, 7, 7, 7], [8, 8, 8, 8, 8], [9, 9, 9, 9, 9]])

# xx
tensor([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]])

利用例

meshgridの使い道がいまいちわからないでいたのですが、画像の変形を行うときに使えるのだと気づきました。

PyTorchで画像変形を行うときのやり方のひとつに、grid_sampleという関数を使う方法があります。この関数は、変形元画像と変形パラメータとして、x, yの座標が求められます。

下図のように、変形パラメータの各gridには、変換後の画像の値が、元画像のどこの座標の値を使うのかを指定します。

なので、例えばaffine変換をかけたいよ、という場合、まずは等差数列+meshgridで座標位置が値として格納された配列を作ります。この配列に対してaffine変換をかけるのです。

その結果をもって、
warp_img = F.grid_sample(img, warp)
で変形できます。

等差数列を作る

同じstep数でインクリメントしていくような配列って、けっこう使用頻度が高いと思います。for文を回しても作れなくはないのですが、関数を使った2通りの作り方を見つけましたので、メモとして残しておきます。

linspace関数

配列の値の区間(start/stop)と何等分するか(N)を指定します。

numpy

【書式】 numpy.linspace(start, stop, N)

例えば、-1~+1までの区間を20等分したい場合は、

import numpy as np

# start:-1, stop:1, N:20の行列
a_np = np.linspace(-1,1,20)
print(a_np)

array([-1. , -0.89473684, -0.78947368, -0.68421053, -0.57894737, -0.47368421, -0.36842105, -0.26315789, -0.15789474, -0.05263158, 0.05263158, 0.15789474, 0.26315789, 0.36842105, 0.47368421, 0.57894737, 0.68421053, 0.78947368, 0.89473684, 1. ])

デフォルトでは”stop”の「+1」は区間に含まれるため、含みたくない場合は、「endpoint=False」を引数に入れます。

b_np = np.linspace(-1,1,20, endpoint=False)
print(b_np)             

array([-1. , -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

pytorch

pytorchの場合も使い方はnumpyとほとんど同じです。

a_torch = torch.linspace(-1,1,20)
print(a_torch)

tensor([-1.0000, -0.8947, -0.7895, -0.6842, -0.5789, -0.4737, -0.3684, -0.2632, -0.1579, -0.0526, 0.0526, 0.1579, 0.2632, 0.3684, 0.4737, 0.5789, 0.6842, 0.7895, 0.8947, 1.0000])

stopも分割に含まれるところも同じですが、numpyと違い、endpoint=Falseは使えません。

b_torch = torch.linspace(-1,1,20,endpoint=False)                    

Traceback (most recent call last): File “”, line 1, in
TypeError: linspace() received an invalid combination of arguments – got (int, int, int, endpoint=bool), but expected one of:
(Number start, Number end, Tensor out, torch.dtype dtype, torch.layout layout, torch.device device, bool requires_grad)
(Number start, Number end, int steps, Tensor out, torch.dtype dtype, torch.layout layout, torch.device device, bool requires_grad)

同じことをやるならどうすればいいかを考えてみました。N+1の配列を作って、Nまでを取り出すのはひとつの手でしょうか。
もしかしたら、普通に関数があるのかもしれませんが……

n_21 = torch.linspace(-1, 1, 21)
n_20 = n_21[:-1]           

# N=21
tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000, 1.0000])

# N=20まで取り出し
tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000])

arange関数

配列の値の区間とstepの指定により数列を生成します。
(分割数(N)は指定しません)

numpy

【書式】numpy.linspace(start, stop, step=1)

stepはデフォルトで1ですので、下のように何も指定しない場合は整数の配列が生成されます。arangeの場合は、stopの値は配列に含まれないのですね。

x_np = np.arange(0,5)
print(x_np)

array([0, 1, 2, 3, 4])

pytorch

PyTorchの場合もnumpyと同じです。

x_torch = torch.arange(0,5)
print(x_torch)

tensor([0, 1, 2, 3, 4])

また、stepの数字を0.1にしてみると、0.1ごとにインクリメントする数列が生成されました。

x_torch = torch.arange(-1,1,0.1)
print(x_torch)

tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000])

まとめ

linspaceとarangeは両方とも等差数列を作る関数ですが、分割数が決まっている場合はlinspace、step数が決まっている場合はarangeを使うという棲み分けになっているようです。

作りたい数式の条件で使い分けが必要ですね。

OpenCVとPILの基本操作

Pythonでの画像操作に使うライブラリは、OpenCVとPILではないかと思います。
私はいつもOpenCVで読み込むことが多いですが、単純な読み書きの場合にPILも使うことがあります。

ファイル操作の方法について、OpenCVとPILで比較を行ってみました。
ちなみに、PILはPython Imaging Libraryの略だそうです。

インポート

OpenCVとPILのインポートは以下のコマンドでできます。
画像表示用にpyplotもインポートしておきます。

import cv2 # OpenCV
from PIL import Image # PIL
import matplotlib.pyplot as plt

ファイルの読み込み

OpenCV

・cv2.imread()
 画像を読み込みます。
 読み込んだ画像をmatplotで表示してみます。

img_cv = cv2.imread('img00001.jpg')
plt.imshow(img_cv)
plt.show()

なんだか色合いが変ですね。
それもそのはず、OpenCVはカラー画像をRGBではなくBRGの順に読み込んでいるのです。
OpenCVの機能だけで読み込み、書き込みをするのであればBGRのままで問題ないのですが、今回のようにOpenCV以外のライブラリを使う場合には、RGBの順に直してあげる必要があります。

・cv2.cvtColor()
 画像タイプを変換します。第2引数にcv2.COLOR_BGR2RGBを指定すればRGBの順序に変換されます。逆方向はcv2.COLOR_RGB2BGRです。

img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)

RGB変換処理を追加して、画像を表示してみます。

img_cv = cv2.imread('img00001.jpg')
img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)
plt.imshow(img_cv)
plt.show()

きれいな海が表示されました。


PIL

・Image.open()
 こっちはRGBの順番で読んでくれるので、そのまま表示できます。

img_pil = Image.open('img00001.jpg')
plt.imshow(img_pil)
plt.show()

おんなじ画像ですが……

データフォーマット


OpenCVとPILで読み込んだ画像に対して、何かしらの演算処理をしたいという場合、読み込んだときのフォーマットが異なりますので、注意が必要です。

【OpenCVでの読み込み】

img_cv = cv2.imread('img00001.jpg')
print(type(img_cv))

<class ‘numpy.ndarray’>

【PILでの読み込み】

img_pil = Image.open('img00001.jpg')
print(type(img_pil))

<class ‘PIL.JpegImagePlugin.JpegImageFile’>

OpenCVはnumpyのndarray形式ですが、PILのほうはPILの独自フォーマットになっていることがわかります。

どういうことかというと、PILで読み込んだままの状態だと、演算子による計算ができないとうことです。

img_pil += 1
img_cv += 1

この計算をすると、OpenCVで読み込んだほうはエラーなく処理が行われますが、PILのほうは下のようなエラーが返ってきます。

Traceback (most recent call last):
File “”, line 1, in
TypeError: unsupported operand type(s) for +=: ‘JpegImageFile’ and ‘int’

ですので、読み込んだデータをnumpy arrayに変換する処理が必要になります。

img_pil = np.array(img_pil)

ちなみに、numpy arrayをPIL形式に変換するには、Image.fromarray()を使えばOKです。

img_np = np.array(img)
print(type(img_np))
 >> <class 'numpy.ndarray'>
img_pil = Image.fromarray(img_np)
print(type(img_np))
 >> <class 'numpy.ndarray'>

書き込み

OpenCV

・cv2.imwrite()

out_cv_fname = 'output_cv.jpg'
cv2.imwrite(out_cv_fname, img_cv)

ちなみに、OpenCVの場合は出力もBGR並びが前提になっていますので、もしもRGBに並び替えたときは、cv2.cvtColor(img, cv2.COLOR_RGB2BGR)でBGR並びに変更しておく必要があります。

PIL

img.save()

out_fname = 'output_pil.jpg'
img_pil.save(out_fname)

とりあえず、基本の基本はここまで。

OpenCVで画像マッチングをする

OpenCVというのは、コンピュータビジョン用のライブラリです。
カメラキャリブレーションもできたり、(多分)すごい機能がたくさん入っているんだと思います。

その道の人には怒られるかもしれないですが、普通に画像処理をするにもなかなか便利なライブラリがたくさん入っていたり、画像の入出力も分かりやすかったりで、Pythonで画像を扱うとき、私はだいたいOpenCV経由でやっています。

さて、OpenCVを使って画像マッチングというものを行うことができます。
どんなものかというと、同じ対象が写っている2枚の画像から、対応点を見つけ出すという処理です。

何のために行うのかというと、例えば以下のような用途でしょうか。
・パノラマ画像の作成
 重複するエリアの同じ地点を見つけ出せるので、ずれることなく重ねられます。
・3次元形状の復元
 視点の異なる2点間の対応点が取れれば、カメラの相対姿勢が分かり、
 対象の形状を推定できます

実際にやってみた例が下の画像です。

これはORBによるもので、若干、マッチングポイントに偏りがあるような気がしますが、写真の視点と範囲が異なっていても、対応が取れていることがわかります。
ロープウェーに乗っているときに撮った写真ですので、撮影したときの高さに差異があって、写真の範囲も重なる部分はありますが、異なっています。

OpenCVのインストール

OpenCVは完全フリーなものと権利的な制約のあるものとでパッケージが明示的に分かれています。制約付きのものはcontribというパッケージに入っています。
contribに含まれているライブラリには、例えば、SIFTやSURFなどの特徴抽出・記述アルゴリズムがあります。

インストールは 以下のコマンドで 簡単にできます。1行目のopencv-pythonだけでも動くのですが、私はSIFTも使いたいのでcontribも入れました。
なお、contribも入れる場合、バージョン指定は必須です。

★2019/9/8時点でバージョン3.4.2.16がなくなっていたようでしたので、指定バージョンを修正しました。

pip install opencv-python==3.4.2.17
pip install opencv-contrib-python==3.4.2.17

そうでないと、エラーが出ることがあります(下はSIFTを動かしたときに発生)。多分、デフォルトだとバージョンの不一致が生じるのでしょう。

# -----出力-----
cv2.error: OpenCV(4.1.0) /io/opencv_contrib/modules/xfeatures2d/src/sift.cpp:1207: error: (-213:The function/feature is not implemented) This algorithm is patented and is excluded in this configuration; Set OPENCV_ENABLE_NONFREE CMake option and rebuild the library in function 'create'

特徴点抽出

早速動かしてみます。

マッチングは二段階の処理で実施しています。
①画像中から特徴点を見つけて、その特徴量を記述する
②2画像間の特徴量を比較して、似ている特徴点同士をマッチングする。

この特徴量抽出/記述に各アルゴリズムの工夫があります。
英語論文なので、そのうち読んでまとめたいなと思います。

基本的なコードは、ここを参考にしています。ただし、仕様の変更(?)により、そのままでは動かない箇所がありますので、手を加えてはいます。

img1 = cv2.imread('IMG001.JPG')
img2 = cv2.imread('IMG002.JPG')

h,w,_ = img1.shape
rate = int(max(h,w)/512)

# 画像サイズを最大512に変更
img1 = cv2.resize(img1, (int(w/rate), int(h/rate)))
img2 = cv2.resize(img2, (int(w/rate), int(h/rate)))

detector = cv2.ORB_create()

# 特徴点抽出
kp1, des1 = detector.detectAndCompute(img1, None)
kp2, des2 = detector.detectAndCompute(img2, None)
    
bf = cv2.BFMatcher()

# マッチング
matches = bf.match(des1, des2)
matches = sorted(matches, key = lambda x:x.distance)

# 画像書き出し、出力
out = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=2)
cv2.imwrite('outimg.png', out)

detector=cv2.ORB_create()の箇所で、ORBの検出器のインスタンスを作成しています。
ここでは、ORBを使用しています。

detector.detectAndCompute()で、入力画像に対するキーポイント(kp)と特徴量(des)を出力します。なお、detector.detect()だけにすると、キーポイントだけを出力します。
マッチングには特徴量を使用しますので、マッチングしたいときは、detectAndCompute()を選びます。

マッチングポイントを確認する

Query画像とTrain画像で抽出したキーポイントのマッチング結果を確認するとき、drawMatches()を使うと、キーポイント同士を結んだ画像を出力できます。

では、具体的に座標値はいくつになるのでしょうか。
意外と情報が見つからなかったりしますので、以下、メモです。

Matchした点を単純にprintで出力してみます。

print(matches[:10])

DMatch型オブジェクトとして返されて中が見えません。
マッチングした点の情報を知るには、属性にアクセスしなければいけないのです。

# -----出力-----
[<DMatch 0x7fe03e2ac7b0>, <DMatch 0x7fe03e2ad3b0>, <DMatch 0x7fe03e2ac790>, <DMatch 0x7fe03e2ad6f0>, <DMatch 0x7fe03e2ab8d0>, <DMatch 0x7fe03e2ad310>, <DMatch 0x7fe03e2ad530>, <DMatch 0x7fe03e2adf10>, <DMatch 0x7fe03e2ae0f0>, <DMatch 0x7fe03e2adad0>]

DMatch型オブジェクトの持っている属性は、以下の4つです。
・distance:特徴量記述子間の距離
・trainIdx:学習記述子(参照データ)の記述子のインデックス
・queryIdx:クエリ記述子(検索データ)の記述子のインデックス
・imgIdx:学習画像のインデックス

ちなみに、上のコード「bf.match(des1, des2)」で、des1がクエリ、des2が学習画像に該当します。des1と近い特徴をdes2に問い合わせる(クエリ)というイメージですね。

では、1番目のマッチングの情報を見てみます。

print(matches[0].distance)
print(matches[0].trainIdx)
print(matches[0].queryIdx)
print(matches[0].imgIdx)
# -----出力-----
121.76616668701172
214
207
0

ここで、でてきたtrainIdxとqueryIdxはdetectAndComputeで出力されたキーポイントのインデックスです。
したがって、次に 画像の特徴抽出の際に計算したキーポイント情報にアクセス して、具体的に画像座標のどこの点が該当するかを調べなくてはなりません。

このキーポイント情報もKeypoint型のオブジェクトですので、同じように属性にアクセスします。

keypointオブジェクトは以下の属性を持っています。
・pt:キーポイントの座標
・size:キーポイント周辺の重要領域の直径
・angle:計算されたキーポイントの方向(計算できない場合は -1 )
・response:最も強いキーポイントが選択されたときの応答
・octave:キーポイントが抽出されるオクターブ(ピラミッドの階層)
・class_id:オブジェクトクラス

print(kp1[0].pt)
print(kp1[0].size)
print(kp1[0].angle)
print(kp1[0].response)
print(kp1[0].octave)
print(kp1[0].class_id)

クエリ側の最初(0番目)のキーポイントの情報はこんな感じです。

# -----出力-----
(60.0, 129.0)
31.0
276.7517395019531
0.001297724898904562
0
-1

とりあえず、キーポイントの情報で最も距離の近い(似ている)キーポイントを見てみます。
matchesに並べ替えを行っていましたので、一番先頭に来ているインデックスの座標を見てみます。

print('query: ', kp1[matches[0].queryIdx].pt)
print('train: ', kp2[matches[0].trainIdx].pt)

以下の数値が出力されました。
整数にすると、query: (108, 171) , train: (292, 266)です。

# -----出力------
query:  (108.00000762939453, 171.36000061035156)
train:  (292.32000732421875, 266.4000244140625)

画像に重ねるとこのようになりました。
建物の角の部分を最も似ていると判断したようです。

ヤコビ行列

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

まずは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()

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

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