Numpy exercise 100とは
GithubにあげられているNumpyの練習問題的なものです。おそらく非公式?リンク⬇️
https://github.com/rougier/numpy-100/
解答付きのNotebookと解答なしのNotebook、ヒント付きのNotebookの三種類があり、解く際は解答なしの方のNotebookをローカルに落としてJupyter Notebookとかでいじるのが良さそう。一応問題ごとに三段階でレベル付けがされてたりします。
結構有名な問題集らしく、⬇︎のリンクのように、他のブログでも解説が上がってたりします。
https://gr8developer.blogspot.com/2017/12/100-numpy-exercise-310.html
#レベル感
とりあえず50問くらいやってからこの記事書いてるので後の方はわかりませんが、普通に難しい気がします。僕のNumpyに触れてる経験が少ないのもあり、レベル1のところから半分くらい知らなかったり忘れてたりするところが多かったです。Numpyそもそも触れたことない方は下のリンクのNumpyのところとかやるのがオススメです。
http://www.tsjshg.info/udemy/notebooks.html
#この記事について
半分備忘録的なところがあるため、答えだけでなく周辺の関数や実践応用例(主に機械学習周り)なども調べてみました。もしかしたら間違ってる可能性もあるので、その場合は指摘していただけると嬉しいです
また、一応問題を解いてる最中の方向けの参考記事なので、解答の実行結果や問題箇所は省略してます。
3回くらいに分けて記事を書こうと思っており、この記事は第一弾的なやつです。問題30まで解いていきます。
#早速やってみる
問題1~10
###1
NumpyのImport。まずはここから。
import numpy as np
###2
NumpyのVersionとconfig fileの確認。地味に知らなかった…
print(np.__version__)
np.show_config()
###3
zeroベクトルの作成。この問題だとsizeが10と指定されてる。
np.zeros(10)
###4
メモリサイズを調べる。itemsizeで一要素につき何バイトかを計算し、それを配列のsizeと掛けている。ちなみにZ.nbytes
でもいける。
巨大配列を使うときに使いそう?調べてもイマイチわからなかった…
Z = np.zeros((10,10))
print("%d bytes" % (Z.size * Z.itemsize))
###5
コマンドライン上でnumpyの情報を表示する方法。
%run `python -c "import numpy;numpy.info(numpy.add)"`
###6
5つ目の要素だけ1のzeroベクトルを作る。要はインデックスの指定をすれば良い。
Z = np.zeros(10)
Z[4] = 1
print(Z)
###7
10から49が要素にあるベクトルを作る。普通にarangeを使う。stop引数のところは指定より1つ分大きい必要があることに注意。
Z = np.arange(10,50)
print(Z)
###8
ベクトルを逆にする。[::-1]の意味は、スライス表記の引数で start:step:stop
があるとき、step
だけを::step
という形で表す省略表記らしい。例えば[::2]だと2の倍数ごとの要素を指定する。
Z = np.arange(10)
Z = Z[::-1]
print(Z)
###9
配列のshapeを変える時はreshape
を実行する。shapeを確認したいときは.shape
を使う。
ちなみに一次元の配列だと特に忘れがちだが、shapeは軸のあるなしで違うので注意。例えば、np.array([1,2,3])
とnp.array([[1,2,3]])
だと、前者のshapeは(3,)
、後者は1,3
である。
Z = np.arange(9).reshape(3,3)
print(Z)
###10
行列の中で0でない要素のインデックス番号を出す際にはnp.nonzero
を使う。
nz = np.nonzero([1,2,0,4,0,0])
print(nz)
問題11~20
###11
いわゆる単位行列を作る。np.eye
以外にもnp.identity
があるが、違いとしてはnp.eye
は正方行列(行の個数と列の個数が同じ行列)以外でも作れるのに対して、np.identity
は正方行列でしか使えないという点。
Z = np.eye(3) # Z = np.identity(3)でもOK
print(Z)
###12
numpy.random
には多くの乱数生成関数がある。
この問題で使ってるnumpy.random.rand()
は0.0から1.0での範囲の一様分布の乱数、numpy.random.randn()
は一様分布ではなく正規分布で乱数を生成する。
範囲内を指定して一様な乱数を作る際は、numpy.random.randint()
などもよく使う。一方numpy.random.randint()
では整数しか代入/出力できないため、小数がある場合はnumpy.random.uniform()
を使う。
pythonのrandom
モジュールとnumpy.random
モジュールの違いは配列での処理速度らしい。
Z = np.random.rand(3,3,3)
print(Z)
###13
配列の中で最大の数を指定する際は max()
、最小の数を指定する際は min()
を使う。
Z = np.random.rand(10,10)
Z_min, Z_max = Z.min(),Z.max()
print(Z_min,Z_max)
###14
平均を出すにはmean()
を使う。中央値はmedian()
、標準偏差はstd()
。
Z = np.random.random(30)
m = Z.mean()
print(m)
###15
Numpyのborder関連の問題。単にインデックスをスライス表記で指定してあげればOK。border、何に使うんだ…?画像処理とか?
Z = np.ones((10,10))
Z[1:-1,1:-1] = 0
print(Z)
###16
同じくNumpyのborder関連の問題。pad関数のほうが手動で作るより便利。
Z = np.ones((5,5))
Z = np.pad(Z, pad_width=1, mode='constant', constant_values=0)
print(Z)
17
nanの取り扱いについて。numpy.nan
は概念としてはnullとほぼ同じ。ちなみにNone
は要素がそもそも存在しないことを示しているので、nanやnullとは違う。詳しくは⬇︎の記事を参照。
https://qiita.com/karintou/items/1c2434697e1658d1d4e0
0 * np.nan
は、当然だが0だろうと1だろうとnanと掛け算をすると結果はnanになる。
np.nan == np.nan
は成立しないので注意。条件式などを使う際に間違えると詰まりそうなので注意。nanはそもそも非数(数ではない)なので比較ができない。
np.inf
のinfはinfinityの略で、無限大というのを指す。例えば0より大きい数を0で割ると当然infが出力される。一見np.infの方がnp.nanより大きいと感じるかもしれないが、こちらもnanが非数故に比較はできない。(ちなみにinfは非数ではない)
np.nan
からnp.nan
を引いても0にはならず、nanになる。これもnanが非数だからで、演算は使用できない。
set()
は集合型に変換する際に使用する。nanは数ではないが、要素としては存在するのでこれはTrueになる。実際、None in set([np.nan])
はFalseになる。
一番下はnanやnumpyの性質というよりもコンピューターの性質に関係がある気がするが…?ちなみに0.4 == 4*0.1
はTrueとして成立する。詳しい方いたら是非理由を教えてほしいです。
print(0 * np.nan)
print(np.nan == np.nan)
print(np.inf > np.nan)
print(np.nan - np.nan)
print(np.nan in set([np.nan]))
print(0.3 == 3 * 0.1)
18
対角成分を出力する関数はdiag()
。対角成分とは、行列のうち行番号と列番号が一致してる要素の集まりのこと。diag()
の面白いところは、引数に2次元の行列を代入すると普通に対角成分を出力してくれるのだが、引数に1次元の行列を代入するとそれを対角成分とする対角行列を作ってくれる。
※対角行列とは、正方行列のうちh行番号と列番号が一致してる部分以外(非対角成分という)が0である行列のこと。例えば、単位行列も対角行列のうちの一つ。
また、引数でkを指定することで、対角行列をずらした(説明が難しいため、実行してみてください)行列の作成にも使える。
Z = np.diag(1+np.arange(4),k=-1)
print(Z)
19
チェッカーボード状の行列を作る。こちらも画像処理とかで使うことが多い?問題8と同じく、[1::2]は1起点で1個間隔の要素を指定、[::2]は0起点で1個間隔の要素を指定する。
Z = np.zeros((8,8),dtype=int)
Z[1::2,::2] = 1
Z[::2,1::2] = 1
print(Z)
20
行列の中で「100番目」の要素が、shapeが(6,7,8)の行列の中で何個目の要素かを指定する。ここではunravel_index()
関数を使用。行列を作らなくとも使える。
print(np.unravel_index(100,(6,7,8)))
問題21~30
21
19と同じくチェッカーボード状の行列を作るが、ここではtile関数を使用。模様を作るのに便利。
Z = np.tile(np.array([[0,1],[1,0]]),(4,4))
print(Z)
22
行列の正規化をする。ちなみに正規化(normalization)と標準化(standardization)は正確には違う。正規化も標準化も数値のスケールを合わせるのが目的という点では同じだが、正規化は数値を最大値と最小値を基準に等倍で[0,1]の範囲内に収めるのに対して、標準化は平均を0、標準偏差を1にするという違いがある。(ちなみにこの問題の原文はnormalizeしろという指示だが、回答は正規化ではなく標準化の方である。ミスか、同じ意味で使っているのか?)
# 正規化の場合
Z = np.random.random((5,5))
Z = (Z - np.min(Z))/(np.max(Z) - np.min(Z))
print(Z)
# 標準化(standardization)の場合
Z = np.random.random((5,5))
Z = (Z - np.mean(Z))/(np.std(Z))
print(Z)
23
color
という新しいデータ型を定義する。ここではnp.dtypeを使えば良い。np.ubyte
でカスタムデータ型を表すことができる。3つ目の引数はshapeを表すよう。
color = np.dtype([('r', np.ubyte, 1), ('g', np.ubyte, 1),
('b', np.ubyte, 1), ('a', np.ubyte, 1)])
24
これは普通にベクトルの内積を計算すれば良い。matmul、もしくはdotを使う。
ちなみにdot
とmatmul
は少し違っており、dotは単に片方のベクトルの最後の軸ともう片方のベクトルの最後から2番目の軸を掛け合わせする一方で、matmul
は行列積を計算する。要はmatmul
のほうが3次元以上の際でも行列積を出せて汎用性が高い。
X = np.ones((5,3))
Y = np.ones((3,2))
print(X.dot(Y))
25
要は行列Zの中でのスカラー値の指定。ここでは、3より大きく8以下の要素にのみ−1を掛けている。
Z = np.arange(11)
Z[(3 < Z) & (Z <= 8)] *= -1
print(z)
26
Numpyライブラリと標準ライブラリでのsumの挙動の違い。標準ライブラリでは第二引数でstartの数を取る一方で、Numpyライブラリでは第二引数でaxisの値をとるというだけ。start=-1
だと、-1,0,1,2,3,4
の合計になる。なお当たり前だがaxis=-1
はaxis=0
と同じ。
print(sum(range(5),-1))
from numpy import *
print(sum(range(5),-1))
27
-
Z**Z
は行列の各要素値ごとで累乗の計算ができる。ここだと3**3
(=3)とか。 -
2<<Z>>2
は、ビット演算を行ってる。例えば、2<<Z
は2をZの分だけビット、つまり2の2進数表記である0b10
の桁を左にずらすということである。(なお0b
とは、その後に続く数字が2進数であることを表している。ちなみに0d
はその後に続く数字が10進数であることを表している)。2<<3
の場合、0d2
->0b10
->0b010000
(3桁分左にずらしている)->0d16
ということになる。逆に、Z>>2
の場合、Zを2桁分右にずらすということである。3>>2
の場合、0d3
->0b11
->消滅(全て最下位の先に押し出たため) となり、8>>2
の場合、0d7
->0b111
->0b001
->0d1
となる。結果、2<<Z>>2
の出力結果は2<<Z
とZ>>2
をマージさせたものになる。実用例としては、8や16など2の累乗の数字で割る際、>>3
や>>4
とした方が速いとか。 -
Z <- Z
は、Z < -Z
の真偽値を出力する。当たり前だが、Zはここでは全て負ではないので全てFalse。 -
1j*Z
の1j
は複素数。高校数学の基礎だが、i
単体は虚数、a+bi
(a,bは実数)が複素数である。なので、1j*2
などすると、出力で0.+2.j
と一応実数と虚数が分かれた出力がされる。どう使うのか興味があるので高校理系数学をやりたくなった。。。 -
Z/1/1
は単に2回除算を行ってるだけ。地味に便利。 -
Z<Z>Z
はエラーが出る。スカラー値だとOKっぽい。Z>Z
はOKなのに何故…?
Z = np.arange(6)
Z**Z
2<<Z>>2
Z<-Z
1j*Z
Z/1/1
Z<Z>Z
28
- 1個目は一応エラーは出ないが、0では割れないよ!という警告が出る。
- 2個目の
//
は整数の商を出す演算。例えば3//2
の結果は1
になる。1個目と同様一応エラーは出ないが、警告は出る。 - 3個目は非数でないnanを無理やり整数型のintに変換しようとするとintの最小数に変換されるらしい。float型は、桁数が一定以上になると
e+(整数)
という表記を用いる。これは10の(整数)乗という意味である。
数値周り、奥が深い…
print(np.array(0) / np.array(0))
print(np.array(0) // np.array(0))
print(np.array([np.nan]).astype(int).astype(float))
29
ここではいわゆる「最近接丸め」をする。四捨五入と違う点は、端数が0.5のものを切り捨てるところ。ここでは、np.ceil()
という切り上げの関数で行ってる。ちなみにnp.floor()
は切り捨ての関数でnp.round()
は四捨五入の関数。
Z = np.random.uniform(-10,+10,10)
print(np.copysign((np.ceil(np.abs(Z)),Z))
30
行列間の共通要素の抜き出し。intersect1d()
関数で2つの行列間で共通してる要素を抜き出せる。intersect1d()
は集合関数の一つで、いわゆる積集合を取り出してくれる。2次元配列以上の集合関数は自分で定義する必要があるらしい。
Z1 = np.random.randint(0,10,10)
Z2 = np.random.randint(0,10,10)
print(np.intersect1d(Z1,Z2))
とりあえず所感
やはり線形代数方面での知識が要求されるところが多く、キャッチアップする必要性を改めて感じました。また、nanやfloatのあたりはまだまだ知ることが多いですね。もし間違ったところや説明で気になったところがあれば指摘お願いします!