4
7

More than 1 year has passed since last update.

僕の逆引きnumpy・scipyメモ

Last updated at Posted at 2020-02-15

逆引きnumpyメモ

numpy? ムズいよね。使い慣れるまで好きになれないと思うよ。だけど、俺は負けないよ。
え〜、numだっ、numpyが躍動する俺のpythonを、皆さんに見せたいね。
※ この記事は少しずつ加筆修正すると思います。

俺的マイナーイディオム

下記は解説記事とかであまり見ない書き方だけど、
僕の中でほとんどイディオムなやつなのでこの記事では使うね。

あと、xとyは全部np.ndarrayの行列とします。

# グラム行列ベースに何かする
# この書き方なら、例えばカーネル関数を明確に分けて定義できる。
# *は関数にぶち込む時に展開して貰うための修飾
fuga = hoge(*np.meshgrid(x, y))

# Aという行列のx軸に対してhogeを行ったものを取り出す
# map的な何か。何故か皆for文を使うけど、numpyは関数型っぽく書いたほうがわかり易くない?
fuga = np.apply_along_axis(hoge, x, A)

# イディオムというか、普通にdot積なんだけど、
# 何故皆これ使わずにnp.dotばかりなんですか?サクッと書く時は楽でいいんだけど…
hoge @ fuga

初期化系

Iを作る

この辺は流石に要らないかと思ったけど、意外とググってる自分が居るんだな。

np.eye(5)

ただの等差数列をつくる

0から100までの大きさの、互いの差が1の等差数列。

np.arange(0, 100, 1)

といっても、最後の1とかは黙ってれば1にしてくれるので

np.arange(100)

とか。ま、基本やね。

等比数列を作る

これは、1から10の3乗までを1000分割するよという意味。
適当に読み替えてくださいな。

np.logspace(1, 3, 1000, base=10.0)

基本的なところ

一番大きい数を取ってくる

np.nanmaxが良い。
この関数はNanを無視してくれる。
まぁ、Nan引っ掛けて欲しい場合もあるやろうけどね。

逆順にする

np.flipでおk

連結

並列というか、縦につなげるやつはnp.vstack
直列というか、横につなげるやつnp.hstack
なんだけど、この関数はnp.vstack(hoge, fuga)ではなくて
np.vstack((hoge, fuga))みたいに使うのです。
あとは、np.concatenateとかもあります。

グラム行列を作る

普通にグラム行列を作る。こんな時はmeshgridがいい。
…なんだけど、meshgrid自体はnumpy芸とも言いたくなるような
技巧たっぷりな書き方になってしまうのは僕だけ?

x = np.arange(10)
y = np.arange(5)
xg, yg = np.meshgrid(x, y)
gram = xg * yg  # ここがやりたい演算

だけど、こう書くことも出来る。この場合、下記のカーネル法によく似ているという意味で
わかりやすいので、この方が僕は好き。(カーネル法は一般化内積ですしね)

from operator import mul
x = np.arange(10)
y = np.arange(5)
gram = mul(*np.meshgrid(x, y))  # ここではまだ大差ない

回帰とか

カーネル法

取り敢えずガウシアンカーネルで書いてみた。
上記グラム行列のmulを適当にカーネルで置き換えてあげればいいだけ。
ちなみに、この書き方なら色々なカーネル関数…
例えば、ガウシアンや線形等を簡単に入れ替えられるので便利。

x = np.arange(10)
y = np.arange(5)

def kernel(a: np.ndarray, b: np.ndarray, v: float) -> np.ndarray:
    return np.exp(- v * np.square(a - b))

gram = kernel(*np.meshgrid(x, y), 3)

二次正則化付きの場合は

lmd = 0.1  # 適当
gram = kernel(*np.meshgrid(x, x)) + lmd * np.eye(x.shape[0])

というのを応用して、以前、ふざけて書いたカーネル回帰ゴルフ。

def solve_kernel(x: np.ndarray, y: np.ndarray,
                 kernel: Callable, lmd: float) -> np.ndarray:
    K = kernel(*np.meshgrid(x, x)) + lmd * np.eye(x.shape[0])
    alpha = np.linalg.solve(K, y)
    return np.vectorize(lambda xs: np.sum(alpha * kernel(x, xs)))

カーネル回帰はPEP8守っても実質たった4行で書ける。numpyはえらいなぁ…。

三次元で山っぽいのを作ってみる

100行100列で、ガウシアンな山を作ってみる。
これにもmeshgridが使える。若干黒魔術っぽいのが悲しいが、大したことはない。

def mount(x: int, y: int) -> np.ndarray:
    return np.exp(-(np.square(x) + np.square(y)) / 1000)

x, y = 100, 100
g = mount(*np.meshgrid(*(np.arange(-n/2, n/2, 1) for n in [x, y])))

何故こんなの作ったかって?クラスタリングの実験用に書いたやつです。
meshgridと*の組み合わせは黄金かも知れないし、コーディング規約違反と言われて殴られるのかも知れない。

関数型的な何か

for文が嫌な時

複数やり方がある。まぁ、mapとかは今更なのでいいでしょう。
ちなみに、mapに限らず、内包表記もpythonなので糞遅いことを自覚すること。
そんなものに頼っているようではnumpy使いとして二流である。
(ごく稀に、そのほうが速いこともありはする)

ユニバーサル関数を使う

ユニバーサル関数という、numpyっぽい挙動の関数を作れるので、それを使う方法が一つ。
ただし、速度が速くなるわけではない。

A = [[2, 4, 8], [1, 1, 1]]
def test(x):
    return x * 2

np.vectorize(test)(A)
> array([[4, 8, 16], [2, 2, 2]])

ちなみに、デコレータとしても使えるけど、コーディング規約的な意味で許されるかは分からん。
ちゃんと関数になるためには引数がただの数じゃないと駄目とか、色々あるっぽい。

np.apply_along_axisを使う

この方法は軸を指定できるのでかなり応用が効く。
しかも、公式曰く速いらしい。vectorizeとどっちのほうが速いかは知らない。

# 上記と同様にして
np.apply_along_axis(test, 0, A)

ちゃんと動くためには次元の制約があったりもするらしい。
他にも関数はいくつかある。https://numpy.org/devdocs/reference/routines.functional.html

np.tileの応用

例えば、あるarrayの列一つ一つに、違う事をしていきたいときがあるとする。
こんな感じで。

matrix = np.reshape(np.arange(70), (7, 10))
nums = np.arange(10)

result = []
for r, num in zip(matrix, nums):
    result.append(r * num)

このコードは愚の骨頂である。非常に遅いからだ。だけど、apply_along hogehogeもどうも思い浮かばなかった。
そこで、こうする。

matrix = np.reshape(np.arange(70), (7, 10))
nums = np.arange(10)

num_arr = tile(i, (matrix.shape[0], 1))
result = matrix * num_arr

勿論、圧倒的に速い。しかし、どうもnumpy芸である…
そこで、関数にしてしまうのはどうか。
ここでは、先に書いたnp.meshgridグラム行列風のインターフェイスを目指してみる。

def for_each_row(x, y):
    return np.tile(x, (y.shape[0], 1)), y

matrix = np.reshape(np.arange(70), (7, 10))
nums = np.arange(10)
def func(x, y): return x * y

result = func(*for_each_row(nums, matrix))

ちなみに、実はこのケースではこれで良い。

matrix = np.reshape(np.arange(70), (7, 10))
nums = np.arange(10)
matrix * nums

上記のやり方は、より複雑なものを実装しないといけなくなった時に役立つ…かもしれない。

逆問題

連立方程式を解く(解ける場合)

普通の連立方程式。例えばこう。
$5x + 3 = y$
$x + 1 = y$
行列表現ではこう。
$$AX=Y$$
これを解く場合は下記のようにする。

X = np.linalg.solve(A, Y)

この場合、未知の数は変数Xだった。
しかし、実用的には測定値Xから定数Aを知りたい事がほとんどと思う。
だから、最小二乗法なんかではここのAとXが逆の立場になる。
逆行列は明示的に出さずにこの関数を使うほうが良いらしい。

最小二乗法を解く(解がない場合)

解が存在しないパターンの場合は最小二乗法を使うことになる。
「解が存在しない」とは例えば下記の様なやつ。

$8x + 4 = y$
$5x + 3 = y$
$x + 1 = y$

この連立方程式は互いに矛盾する解となるため、解がない。
最小二乗法の解は以下の式なので

$$X^TXA = X^TY$$

A = np.linalg.solve(X.T @ X, X.T @ Y)

疑似逆行列を使うことも出来る。

A = pinv(x) @ y

なんだけど、既にちゃんと関数があるから上記ではなく下記が正解。

A = np.linalg.lstsq(X, Y)[0]

正則化付き最小二乗法

正則化付きの場合はどうするのかよく分からない。
ググったけど、上記のような正解を見つけられなかったから、適当に自分で書くことになるのかな?
$$(X^TX + \lambda I)A = X^TY$$

lmd = 1
seisoku = lmd * np.eye(min(X.shape))
A = np.linalg.solve(X.T @ X + seisoku , X.T @ Y)

最小ノルム解を解く(解が無限にある場合)

このような時は解が無限にある。
$8x_1 + x_2 + 6x_3 = 4$
$5x_1 + x_2 + 2X_3 = 3$
この方程式の最適解を求めてみる。
$$A = X^T{(XX^T)}^{-1}Y$$

A = X.T @ np.linalg.inv(X @ X.T) @ Y

ってなりそうじゃん?
だけど、ムーアペンローズの逆行列は凄いので、普通にいける。

A = pinv(X) @ Y

でも、これも正解は下記。

A = np.linalg.lstsq(X, Y)[0]

この関数はすごいねー。

正則化付き最小二乗法(解が無限にある場合)

これも正攻法が分からん。だけど、式は簡単に作れる。
$$A = X^T{(XX^T + \lambda I)}^{-1}Y$$

lmd = 1
seisoku = lmd * np.eye(min(X.shape))
A = X.T @ np.linalg.inv(X @ X.T + seisoku) @ Y

うーん…(・ัω・ั)
正則化付き探せなかったのは僕の勉強不足か?
大して難しくないから良いんだけどさ…

その他、いろんな最適化問題

速くする

numpyでは**とかの演算子は比較的遅い。e**hogeとかやるべきではなくて、np.expとかを使うほうが速い。
僕の書いたWavelet変換のパッケージではそうだった。(なので可読性が悪い…)

hoge = np.arange(1000)
e ** hoge  # 遅い
np.exp(hoge)  # 速い

hoge ** 2  # 遅い
np.square(hoge)  # 速い

hoge ** (0.5)  # 遅い
np.sqart(hoge)  # 速い

hoge ** (3)  # 遅い
np.power(hoge, 3)  # 速い

hoge ** (3.4)  # 遅い
np.float_power(hoge, 3.4)  # 速い

この他、np.powerとか色々あるけど、基本、最適化された関数を使うほうが速い。

楽をするために

行列の積

何故皆@演算子使わないのかよくわからない。np.dotとかより読みやすいと思うんだよな…。
遅かったりするんですか?

np.arange(10).T@np.arange(10)

複素数関連

複素共役

np.conjでおk

実軸虚軸関連

hoge.real, hoge.imag
という感じで書けますから、こいつを代入する時は

fuga.real, fuga.imag = hoge.real, hoge.imag

何故これが欲しいかと言うと、虚軸だけ、実軸だけに何らかの処理をすることが意外とあるから。

フーリエ変換

フーリエ変換はどっちかと言うとscipyを使うことが多いと思う。numpyにもあるんだけどね。

scipy.fftpack.fft(wave)

逆変換

comp  # フーリエ変換後の複素数の行列とする
scipy.fftpack.ifft(comp)

cupy関連

cupyへ変換

cp.asarray(numpy_array)

numpyへ変換

cp.asnumpy(cupy_array)

cupy自体、numpyと違う所もそれなりにあるので、色々気をつけようね。

場合分け

場合分け

np.whereを使う。
もしもxが条件式に合致すれば第二引数を、合致しなければ第三引数を出す。

x = np.arange(10)
np.where(x > 5, x * 10, x)
> array([0, 1, 2, 3, 4, 5, 60, 70, 80, 90])

条件文

条件式をぶっこむとRっぽい挙動になる。

x = np.arange(10)
x > 5
> array([False, False, False, False, False, False, True, True, True, True])

数学の基礎っぽいやつ

行列に関して

np.det(hoge)  # 行列式
np.norm(hoge)  # ノルム

積分

これもscipy
これ、マジカヨすげーってなった。

from scipy.integrate import quad
def test(x):
    return x ** 2 - x
print(quad, test, -5, 5)

他に、2回積分するのとかもある。それはdblquad

特殊関数

さては君は数学好きだね?
このリンクを見ると良い。
https://docs.scipy.org/doc/scipy/reference/special.html#module-scipy.special

ゼロ埋め

hogeは配列
次のは3番目以前と5番目移行を埋めるということ。
constantは定数で埋めるという意味。
最後のは左右で埋める定数。
他にも色々モードがある。

np.pad(hoge, [3, 5], 'constant' ,[0, 0])

応用

画像のくり抜き

値の大きな所だけくり抜いてみる。
さっきのようにTrue, Falseの配列を作って、条件式でくり抜く。
例ではnumpyで山みたいな物を作った上で、山のてっぺんを切り取っている。
この時に、np.nanが必要になったりする。matplotlibのimshowではnanが無視されるからだ。

def mount(x: int, y: int) -> np.ndarray:
    return np.exp(-(np.square(x) + np.square(y)) / 1000)


# くり抜く関数。
# valuesが普通の行列で、clusterがboolの行列
def make_single_cluster(values, cluster):
    img = np.nan * np.zeros_like(values)
    img[cluster] = tvalues[cluster]
    return img


x_y = 100, 100
# 今回は一寸ノイズを入れてみた
X = np.random.rand(*x_y) / 100
# 山を作る
X += mount(*np.meshgrid(*(np.arange(-n/2, n/2, 1) for n in x_y)))

plt.imshow(make_single_cluster(X, X > 0.5))
plt.show()

Figure_1.png

互換性

matlab読み書き

scipyにmatlab読み書き機能がある。

import scipy.io as sio
sio.loadmat('hoge.mat')

data = {'fuga', np.arange(100)}
sio.savemat(fuga.mat, data)

残念ながら僕はmatlab使ったことないからよく分からん。

4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7