今回はPythonの数値計算モジュールであるNumPyを用います。
NumPyはこの講座を通して用いるので基本的な使い方を学んでいきましょう。
ただしこの章で扱うものはNumPyの氷山の一角であり、機械学習でよく使うものを扱います。
この節からNumPyに入門していきます。
機械学習ではデータをベクトルとして捉え、数学的な計算(行列演算、最適化など)を通して学習をします。
このベクトルや行列といったものに対する演算に対して基本的な関数が組み込まれているのがNumPyモジュールとなります。
まず、なぜNumPyを使うのかを知るために、Python標準の機能とNumPyを比べてみましょう。まず、以下の2つのPythonリストを考えます。
a = [2, 9, 9, 7, 9, 2]
b = [3, 1, 4, 1, 5, 9]
この2つのリストを要素数が6のベクトルとして操作することを考えます。
例えばこのベクトルに対して、内積やaとbの各成分の和を取ったベクトルを計算する場合、Pythonでは次のように書く必要があります。
# 内積
def dot_product(a, b):
assert len(a)==len(b) # aとbの長さが同じでないときエラーを返すようにする
ab = 0
for i in range(len(a)):
ab += a[i]*b[i]
return ab
def add_vector(a, b):
assert len(a)==len(b) # aとbの長さが同じでないときエラーを返すようにする
add_ab = [a[i]+b[i] for i in range(len(a))]
return add_ab
print("内積:", dot_product(a, b))
print("和:", add_vector(a, b))
しかし、NumPyにはすでにこれらの関数や演算が実装されています。実際に使ってみましょう。
import numpy as np # numpyをimportする
慣習として、numpyはnpと省略して使います。本講座でもそれに則ります。
NumPyではnp.ndarrayという配列が用意されています。これはN次元配列といい、ベクトルや行列・それ以上の次元の配列(テンソル)に対して統一して用意された型です。
Pythonのリストはnp.array()という関数を用いて簡単にnp.ndarrayに変換できます。
a = np.array(a)
b = np.array(b)
print("a: ", a)
print("b: ", b)
print("type of a: ", type(a))
print("type of b: ", type(b)) # aとbの型を確認する
aとbの内積と要素ごとの和は次のように計算されます。
print("内積:", np.dot(a, b))
print("和:", a+b)
このように、NumPyではすでに基本的な操作や関数が充実しており、Pythonでわざわざ実装しなくてよいのです。
実は、実行速度の観点からもNumPyを使うメリットがあります。NumPyの内部ではC言語やFortranで記述されており、効率的なメモリ管理や、BLAS/LAPACKといった高性能な線形代数ライブラリを利用しているため、Pythonのリスト操作と比較して非常に実行速度が高速です。
ここでは上で用いたa, bとは別に新しいものを使用します。np.ndarrayに変換するのを忘れないように!
a = np.array([1, 2, 3, 4, 5])
b = np.array([2, 2, 3, 6, 0])
このセクションではnp.ndarrayの基本的な演算を紹介していきます。
ここでNumPyの演算の多くは**ユニバーサル関数(ufunc)**として実装されています。ここでのユニバーサルな演算とは、配列の各要素に対して個別に演算を適用することを指します。実際に例を見て実感していきましょう。
四則演算は、通常のPythonの数値型と同じ演算子(+, -, *, /)を用いて計算できます。これは各要素ごとの四則演算の結果を返します(ユニバーサル関数)。
print("和: ", a + b) #これは通常のベクトルの和と一致
print("差: ", a - b) #これは通常のベクトルの差と一致
print("積: ", a * b) #これはベクトルの各要素の積(=アダマール積)
print("商: ", a / b) #これはベクトルの各要素の商
商の計算では、0による除算が発生した要素で
inf(無限大)という値が現れています。
np.infはNumPyで定義された特殊な浮動小数点数で、0 で割る演算や、計算結果がオーバーフローしたときに生成されます。
また、ゼロ除算が発生したことを示す警告(RuntimeWarning)が表示されています。\
今後この講座で扱うscikit-learnなどのライブラリの一部の関数は
np.infやnp.nan(非数)が含まれているとエラーを返す関数もあります。
Pythonの通常の比較演算子(==や>など)はブール値(TrueまたはFalse)が返ってきていました。
np.ndarrayではどのようになるでしょうか?試してみましょう(Jupyter Notebookのよいところは気になったことをすぐに試せるところです)。
3>5
print("a==a: ", a==a)
print("a==b: ", a==b)
print("a>b: ", a>b)
print("a==bの型: ", type(a==b))
なんと、np.ndarrayに対する比較演算では、結果は単一のブール値ではなく、**各要素の比較結果を要素とするブール値のnp.ndarray**となるのです。
そして、各要素を見てみると、対応する要素間で比較演算が行われた結果が格納されています(これもユニバーサル関数)。これもPythonのリストとは異なる動作です。
他にも数学的な関数をユニバーサル関数として計算してくれるものが実装されています。以下ではその例を紹介します。ユニバーサルな関数一覧はこちらを参考にしてください。
print("sin: ", np.sin(a))
print("log: ", np.log(a))
print("exp: ", np.exp(a))
np.ndarrayのユニバーサル関数では、形状(shape)が異なる配列同士でも、ブロードキャストという仕組みによって計算できる場合があります。ここでは、1次元配列(ベクトル)に対するブロードキャストを学びます。
ベクトルにはスカラー倍(実数倍)という演算が定義されています。NumPyでも同様にスカラー倍(実数倍)を行うことができます。
print("3a = ", 3*a)
これは、次の計算をしていることと同義です。
threes = np.array([3, 3, 3, 3, 3])
print("3a = ", threes*a)
実はNumPyでは形状の異なる2つの配列に対してユニバーサル関数による二項演算を行う場合、特定のルールに従って一方または両方の配列の形状を仮想的に拡張し、形状を揃えてから計算を行います。これをブロードキャストと呼びます。
上の例でどのようなブロードキャストが起きているか考察してみましょう。3はint型ですが、NumPyはこれを0次元配列(スカラー)np.array(3)として扱います。a(形状(5,))とスカラーnp.array(3)(形状())で要素ごとの積*を計算するためには、形状を揃える必要があります。
この場合、まずスカラー3は0次元配列np.array(3)(形状())として扱われます。次に、aの形状(5,)に合わせて、np.array(3)が仮想的にnp.array([3, 3, 3, 3, 3])(形状(5,))に拡張(ブロードキャスト)され、要素ごとの積が計算されます。
では以下の2つの1次元配列の2項演算を行った場合、どうなるでしょうか?
c = np.array([1, 2, 3, 4, 5])
d = np.array([1, 2])
print(c * d)
ここではエラーが出てしまいます。
ブロードキャストのルールでは、次元ごとのサイズを比較し、サイズが一致するか、どちらかが1である場合にのみ計算可能です。サイズが1の次元は、もう一方の配列の対応する次元のサイズに合わせて拡張されます。この例では、最後の次元のサイズが5と2で一致せず、どちらも1ではないため、ブロードキャストできません。
●ブロードキャストの仕様
・各次元が同じであるか、または一方の次元が 1 でなければならない。
・1 次元は、その長さが異なる次元に合わせて繰り返し拡張される。
Pythonのリストにはmax()やsum()といった組み込み関数が利用できました。np.ndarrayに対しても同様の計算を行う関数がNumPyに用意されています。
print("max: ", np.max(a))
print("sum: ", np.sum(a))
print("mean: ", np.mean(a)) # meanは平均値
print("std: ", np.std(a)) # stdは標準偏差
また、Pythonのリストでは+演算子でリストの結合を行いますが、np.ndarrayでは+は要素ごとの和を計算するため、配列の結合にはnp.concatenate()やnp.r_、np.c_などを使用し、次のように計算します。
print("aとbの連結 (concatenate): ", np.concatenate((a, b)))
print("aとbの連結(r_): ", np.r_[a, b])
concat = np.r_[a, b]
concat
また、1次元配列の要素数はPythonと同様にlen()関数で取得できます。
print("aの長さ: ", len(a))
print(a.shape[0])
最後に、np.ndarrayをPythonのリストに変換するにはtolist()メソッドまたはlist()関数を用います。
a_list = list(a)
print(a_list)
print(type(a_list))
a_tolist = a.tolist()
print(a_tolist)
print(type(a_tolist))
問1
(1) 1次元であなたの好きな値を要素にもつ同じ長さのnp.ndarrayを2つ作成し、type()関数を用いてnp.ndarrayになっていることを確認してください。
(2) (1)で作成した2つの1次元配列に対して要素ごとの四則演算を行ってください。
# (1)
# WRITE ME
# (2)
# WRITE ME
問2
1次元配列を入力とし、その配列をL2ノルムで規格化した配列を出力する関数を作成してください。
ここで、ベクトルのL2ノルムによる規格化とは、元のベクトルと同じ向きで、長さ(L2ノルム、つまり全要素の2乗和の平方根)が1になるようにスカラー倍することです。
このとき、NumPyのユニバーサル関数や基本的な演算のみを用いて実装してください。
def normalize(a):
# WRITE ME
return a
このセクションではnp.ndarray、特に1次元配列(ベクトル)におけるインデックスを用いた要素や部分配列の取得方法を学びます。
Pythonのリストでは[]を使うことによって、任意の位置の値を取得できました。
これはNumPyの1次元配列でも同様です。
このとき、インデックスは0からスタートするので注意しましょう。
a = np.array([2, 9, 9, 7, 9, 2, 4, 5, 8])
print("aの前から4番目の値はa[3]=", a[3])
負のインデックスはリストと同様に、末尾から数えることを意味します。先頭からのインデックスは0から始まりますが、末尾からのインデックスは-1から始まることに注意しましょう。例えば,aの前から4番目はa[3]で取得しましたが,後ろから4番目は以下のように取得します。
print("aの後ろから4番目の値はa[-4]=", a[-4])
また、PythonのリストとNumPyの1次元配列のインデクシングには違いがあります。Pythonのリストのインデックスに使用できるのは整数値と後程説明するスライスのみですが、NumPy配列では、インデックスとして整数のリストや配列を指定することで、複数の要素を同時に取得できます(ファンシーインデックス)。
print(a[[1, 3, 0, 6]])
# 一旦別の変数に保存しておくことも可能です
ids = [1, 3, 0, 6]
print(a[ids])
ここではNumPy配列のスライシングを学習します。その前に、Pythonのリストにおけるスライシングを復習しましょう。
Pythonのリストではスライシングはstart: end: stepという形式をしていました。これは,リストから取り出すインデックスを初項start,公差stepの等差数列で終項がend未満で最大の数になるように取り出します。
a = list(range(30)) # 0~29を要素にもつList
print(a[2: 15: 3])
ここで,start, end, stepはそれぞれ省略することが可能で,省略した場合はstart=0, endは要素の最後まで取得する, step=1となります(stepがマイナスになると,逆向きに値を取得します)。
print("startを省略した場合:", end='\t')
print(a[: 15: 3])
print("endを省略した場合:", end='\t')
print(a[2: : 3])
print("stepを省略した場合1:", end='\t')
print(a[2: 15:])
# 最後の:は省略できる
print("stepを省略した場合2:", end='\t')
print(a[2: 15])
print("stepとendを省略した場合:", end='\t')
print(a[2:])
print("stepがマイナスの場合:", end='\t')
print(a[15: 2: -3])
それではNumPyにおけるスライシングを見ていきましょう。実は、NumPyの1次元配列もリストとほぼ同じようにstart:stop:stepでスライシングできます。
a = np.arange(30) # np.arange(30)はnp.array(range(30))と等価
print(a[2: 15: 3])
print(type(a[2: 15: 3]))
print("startを省略した場合:\t", a[: 15: 3])
print("endを省略した場合:", end='\t')
print(a[2: : 3])
print("stepを省略した場合1:", end='\t')
print(a[2: 15:])
# 最後の:は省略できる
print("stepを省略した場合2:", end='\t')
print(a[2: 15])
print("stepとendを省略した場合:", end='\t')
print(a[2:])
print("stepがマイナスの場合:", end='\t')
print(a[15: 2: -3])
ここでは、NumPyのインデックス操作の中でも非常に便利なブールインデックス参照(boolean indexing)を学習します。
1次元配列のブールインデックス参照とは次のような動作をします:
Trueに対応する位置の要素だけを取り出した新しい1次元配列を返します。実際に例を見てみましょう。
a = np.array([1, 1, 2, 3, 5, 8, 13, 21])
# bool値をもつListを作成
idx = [True, False, False, True, False, True, True, False]
# ブールインデックス参照
print(a[idx])
また、1.2で学んだ比較演算子と組み合わせると、次のように特定の条件を満たす要素だけを抽出できます。
a % 3
# aの要素のうち、3の倍数である要素に対応する位置をTrue、それ以外をFalseとするブール配列
idx = (a % 3 == 0) # %は余りを計算する演算子で,ユニバーサル
print("ブールインデックス:", idx)
# aのうち3の倍数となるものを取得
print("aの要素で3の倍数であるもの:", a[idx])
問1 インデキシングの基礎
あなたが作成した任意の配列に対して,前から一番目と後ろから一番目の値を取得して表示してください。
a = np.array([])
print("前から一番目の値:")
print("後ろから一番目の値:")
問2 ブールインデックス参照
(1) 整数値を要素にもつNumPyの1次元配列を引数として、その配列の中から、要素が奇数であるか、または4で割って2余る数である要素のみを抽出した新しい配列を出力する関数を実装してください。
(2) 正の整数nを引数として、nより小さい正の平方数の個数を、ブールインデックス参照を用いて求める関数を実装してください(ヒント:十分な数の正の平方数を小さい順に列挙し,その中で引数より小さいものを数える)。
# (1)
def make_bool_ids(a):
# WRITE ME
return b # b is `np.ndarray`
# 入力例
a = np.array([1, 1, 2, 2, 3, 3, 4, 4, 5, 5])
# 出力例
# np.array([1, 1, 2, 2, 3, 3, 5, 5])
# (2)
def count_square(n):
# WRITE ME
return answer
# 入力例
n = 20
# 出力例
# 20未満の正の平方数は1, 4, 9, 16
# 4
この章では1次元配列からさらに発展して2次元配列を扱います。これは数学における行列と対応するものです。
この講座の機械学習部分では主に2次元配列までしか扱わないため、3次元以上についてはここでは詳しく触れませんが、1次元配列から2次元配列への拡張を理解すれば、3次元以上にも応用できます。
機械学習では、行(縦軸、axis 0)にサンプル(データ点、レコード)、列(横軸、axis 1)に特徴量(説明変数)を格納した2次元配列をデータとして扱うことがよくあります。この章で扱う内容は特に基本なのでマスターしましょう。
実際に2次元配列を作成してみましょう。これも1次元配列と同じく、np.array()関数を用います。
(注) 以下では、
# 3*2行列を作ってみる
# 3*2のListを変換する
a = np.array(
[[1, 2],
[3, 4],
[5, 6]])
print(a)
print("shape: ", a.shape) # 配列aの形(shape)を表すタプルを返す
二次元のリストを作ってnp.array()をするのではかなり面倒です。そこで、予め1次元配列を作った上でnp.ndarray.reshape()という関数を使って2次元配列に整形することができます。
a = np.array([1, 2, 3, 4, 5, 6])
a = a.reshape(3, 2)
print(a)
print("shape: ", a.shape)
このように、reshapeメソッドに引数として変形後の形状(タプル)を渡すと、要素数が同じであればその形状に変形できます。ただし、reshapeは元の配列と要素数が異なる形状には変換できません。例えば、以下のような場合はエラーを返します。
a = np.array([1, 2, 3, 4, 5, 6])
a = a.reshape(3, 1) # 6個の要素を持つ配列は3*1個の要素を持つ配列に整形できない
また,指定するshapeのうち一つの要素を-1にすることで,自動で整形してくれます。
a = np.array([1, 2, 3, 4, 5, 6])
a = a.reshape(3, -1)
print(a)
print("shape: ", a.shape)
a = np.array([1, 2, 3, 4, 5, 6])
a = a.reshape(4, -1) # 要素数6は4で割り切れないためエラー
これも1次元配列の場合と同様の方法で、要素ごとの四則演算を行うことができます。ただし、*演算子は要素ごとの積(アダマール積)であり、数学的な行列の積とは異なることに注意してください(行列積は後述)。商も同様に要素ごとです。また、1次元配列で説明したブロードキャストも同じルールで適用されます。
a = np.array([1, 2, 3, 4, 5, 6]).reshape(3, -1)
b = np.array([1, 1, 2, 3, 5, 8]).reshape(3, -1)
print(a)
print(b)
print("和: ", a + b) #これは通常のベクトルの和と一致
print("差: ", a - b) #これは通常のベクトルの差と一致
print("積: ", a * b) #これはベクトルの各要素の積(=アダマール積)
print("商: ", a / b) #これはベクトルの各要素の商
print("累乗:", a**2)
1.2節ではユニバーサル関数を紹介しました。ユニバーサル関数は要素ごとに関数を適用するものでした。これは2次元配列に対しても同様に適用できます。
a = np.array([1, 1, 2, 3, 5, 8])
a = a.reshape(3, 2)
print(a)
print("各要素の指数関数を計算: ", np.exp(a))
NumPyの2次元配列では、これまで行と列、あるいは縦軸と横軸と呼んできました。しかし、さらに高次元の配列になると、このような呼び方では区別が難しくなります。そこで、NumPyでは軸(次元)をaxisと呼び、0から始まる番号で区別します。例えば2次元配列では、行方向(縦)の軸がaxis 0、列方向(横)の軸がaxis 1となります。
1.2節で扱った平均値np.mean()や最大値np.max()のように、配列の複数の要素から一つの値を計算する関数を集約関数と呼びます。これらをそのまま2次元配列に適用すると、デフォルトでは配列全体の要素に対して計算が行われます。
a = np.array([1, 1, 2, 3, 5, 8])
a = a.reshape(3, 2)
print(a)
print("最大値: ", np.max(a))
すなわち、集約関数をaxis引数を指定せずに適用すると、配列全体の要素に対して集約が行われ、一つの値(スカラー)を返します。この例では、reshapeを行う前の1次元配列に対して集約関数を適用するのと同じ結果になり、配列の形状情報が失われていると見なせます。
しかし、実際のデータ分析では、特定の軸に沿って集約を行いたい場合が多くあります。 次の例を考えてみましょう。
今から考える2次元配列には4人の生徒の3回分の数学のテストの点数を格納することにします。このとき、axis 0が生徒(4人)、axis 1がテストの回(3回)に対応するshape=(4, 3)の2次元配列を考えます(点数は適当です):
a = np.array([88, 78, 76, 98, 88, 100, 64, 78, 77, 89, 67, 78]).reshape(4, 3)
print(a)
np.max(a)
このデータに対して、全体の最高点を求めるには先ほどと同じ方法で良いですが、そのほかにも「①各テストにおける最高点」「②各生徒の最高点」を求めたくなるはずです。そこで、多くのNumPy集約関数にはaxisという引数があり、集約を行いたい軸を指定することができます。
print("①: ", np.max(a, axis=0))
print("②: ", np.max(a, axis=1))
axisを指定して集約を行うと、指定した軸が縮約されるため、返り値の配列の次元数が1つ減ります(この例では1次元配列になります)。場合によっては、元の配列と同じ次元数を保ったまま結果を得たいことがあります。その場合は、引数keepdimsにTrueを指定します。この結果を見ると、集約された軸のサイズが1になり、元の配列と同じ次元数が保たれているため、各axisの意味(生徒、テスト回)が変わらないことがわかります。
print("①: ", np.max(a, axis=0, keepdims=True))
print("②: ", np.max(a, axis=1, keepdims=True))
問1
(1) 5人の生徒の4回分のテスト結果(100点満点)を要素とするNumPy 2次元配列を作成してください。
このとき、axis 0を生徒でaxis 1をテストの各回だとします。また、値は任意であるとします。
(2) それぞれの生徒の4回分のテスト結果の最高点・最低点・平均・分散をそれぞれ計算してください。
その結果を、axis 0が生徒、axis 1が集約結果(最高点、最低点、平均、分散の順)となるshape=(5, 4)の2次元配列にしてください。
(3) 各テストの点数について、その点数とその生徒の平均点との差の絶対値を計算し、shape=(5, 4)の2次元配列としてください。
# (1)
# (2)
# (3)
この章では2次元配列のインデックス操作を学びます。
a_list = [[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 1, 2, 3]]
a = np.array(a_list)
# 0行目を取得
print("a_list[0]=", a_list[0])
print("a[0]=", a[0])
# (0, 1)成分を取得
print("a_list[0][1]=", a_list[0][1])
print("a[0][1]=", a[0][1])
このように、基本的な行の取得や要素へのアクセスは、リストのリストとNumPyの2次元配列で同様に行えます。また、Numpyの配列ではIndexをa[axis 0, axis 1]のように1つの[]で書くことができます。
# a[0][1]とa[0, 1]は同じ
print("a[0][1]=", a[0][1])
print("a[0, 1]=", a[0, 1])
また、1次元配列と同様に、各軸のインデックスに対してリストや配列を指定するファンシーインデックスも利用できます。
# axis 0の0番目と2番目を取得する
print(a[[0, 2]]) # a[0, 2]とは異なる
# axis 1の0番目と2番目を取得
print(a[:, [0, 2]]) # : の意味はこの節の後で説明します
# 上図
# axis 1のインデックスが1に固定されているとき,a[0, 1]とa[2, 1]は次のように取れる
print(a[[[0],
[2]], 1]) # このとき,axis 0に指定するListや配列はaxis 0と同じ向き(縦向き)にする
print(a[[0, 2], 1])
#下図
# axis 0のインデックスが2に固定されているとき,a[2, 1]とa[2, 3]は次のように取れる
print(a[2, [[1, 3]]]) # このとき,axis 1に指定するListや配列はaxis 1と同じ向き(横向き)にする
では、次のように、各軸から複数ずつ取ってくる(=小行列を取ってくる)場合はどのように書くでしょうか?
さっきと同じように各axisにリストや1次元配列を指定すれば良いです。
print(a[[[0], [2]], [[1, 3]]]) # このとき,axis 0に指定するListや配列はaxis 0と同じ向き(縦向き)にする
print(a[[0, 2], [1, 3]]) # こうしてしまうと,a[0, 1], a[2, 3]を取得してしまう
ここで、a[[[0], [2]], [[1, 3]]] のようなブロードキャストを利用したインデックス指定は、読みにくく、入力の手間もかかります。
そこで、行インデックスのリスト [0, 2] と列インデックスのリスト [1, 3] を引数に与えると、ブロードキャスト可能なインデックスのタプル (array([[0], [2]]), array([[1, 3]])) を生成するヘルパー関数 np.ix_() があります。
# 以下の2つは同じ
idx = np.ix_([0, 2], [1, 3])
print(a[idx])
print(a[[[0], [2]], [1, 3]])
2次元配列でも、各軸に対してスライスを使うことができます。スライスはstart:stop:stepと表しますが、その軸のすべての要素を選択する場合は : と記述します。
# aの0行目から1行目までを取り出す
# すなわち, axis 0の選択はスライスで書くと「 0:2 」で,axis 1での選択はスライスで書くと「 : 」となる
print(a[0:2])
# aの0列目から2列目までを取り出す
# すなわち, axis 0の選択はスライスで書くと「 : 」で,axis 1での選択はスライスで書くと「 0: 3 」となる
print(a[:, 0: 3])
# aの0行目から1行目かつ0列目から2列目を取り出す
# すなわち, axis 0の選択はスライスで書くと「 0: 2 」で,axis 1での選択はスライスで書くと「 0: 3 」となる
print(a[0: 2, 0: 3])
2次元配列でも、インデックスに同じ形状を持つブール値の配列を指定することで、Trueに対応する要素を1次元配列として抜き出すことができます。このとき、インデックスとして使うブール配列は、元の配列と**同じ形状(shape)**でなければなりません。
a = np.array([2, 2, 3, 6, 0, 6, 7, 9]).reshape(2, 4)
print("a: ", a)
# aのうち3の倍数である値を抜き出す
idx = a%3 == 0
print("ブールインデックス: \n", idx)
print(type(idx))
print(a[idx])# 結果は1次元配列
また、特定の軸(axis)に対してのみブールインデックス(またはスライスや整数配列と組み合わせる)を指定することもできます。
print(a[:, [False, True, False, True]]) # axis 1にブールインデックスを指定
4.1でやったように、2つのaxisにブールインデックスを指定して交差した部分を得るにはnp.ix_()関数が使えます。
print(a[np.ix_([True, True], [True, True, False, True])])
問1
(1) 0~99までの100個の連続整数を値にもつ1次元のnp.ndarrayを作成し、shapeが(10, 10)の2次元配列となるように整形してください。
(2) (1)の配列から偶数行目を抜き出したshapeが(5, 10)の2次元配列を作成してください。ただし行は0から数えます。
(3) (1)の配列から奇数列目を抜き出したshapeが(10, 5)の2次元配列を作成してください。ただし列は0から数えます。
(4) (1)の配列から、偶数行目かつ奇数列目にある値を要素にもつshapeが(5, 5)の2次元配列を作成してください。ただし行と列は0から数えます。
NumPyでは、np.pi) や np.e) といった数学定数や、無限大を表す np.inf、非数(Not a Number)を表す np.nan などが定義されています。
print("円周率: ", np.pi)
print("ネイピア数: ", np.e)
print("正の無限大: ", np.inf)
print("負の無限大: ", - np.inf)
np.infは四則演算で次のような性質を持ちます。
print(1 + np.inf)
print(1 - np.inf)
print(2 * np.inf)
print(2 / np.inf)
また,条件演算ではaの値によらず次の性質を持ちます。
a = 10000000
print(a < np.inf)
print(- np.inf < a)
NumPyには乱数を生成するためのサブモジュール numpy.random があり、乱数を要素とする配列を簡単に作成できます。
# [0, 1)上の一様乱数を一つ生成.0は含むが1は含まない.
print(np.random.rand())
# 引数にshapeをかくとそのshapeの一様乱数が生成される
print(np.random.rand(5)) # 長さ5(=shapeが(5, ) )の1次元配列
print(np.random.rand(5, 5)) # shapeが(5, 5)の2次元配列
また,rand()ではなくrandn()を使うことで正規分布からサンプリングすることもできます。
# 標準正規分布(平均0, 分散1)に従う乱数を一つ生成
print(np.random.randn())
# 引数にshapeをかくとそのshapeの乱数が生成される
print(np.random.randn(5)) # 長さ5(=shapeが(5, ) )の1次元配列
print(np.random.randn(5, 5)) # shapeが(5, 5)の2次元配列
また,平均と標準偏差を指定したいときはnormal(mean, std, shape)を使います。
print(np.random.normal(100, 10)) # 平均100, 標準偏差10の正規分布に従う乱数
print(np.random.normal(100, 10, 5)) # 平均100, 標準偏差10の正規分布に従う乱数を5つ持つ1次元配列として作成
print(np.random.normal(100, 10, (5, 5)))
# 平均100, 標準偏差10の正規分布に従う乱数を値にもつshape (5, 5)の2次元配列として作成
また,整数を乱数として生成するrandintというものもあります。
print(np.random.randint(4)) # 0~3の整数を生成
print(np.random.randint(2, 10, (3, 3))) # 2~9の整数をランダムに値にもつshape (3, 3)の2次元配列を生成
上の乱数の生成でしたが,毎回実行すると結果が変わっていました。しかし,再現性の観点から発生する乱数を固定したいことがあります。そこでseed(乱数の種)というものを設定するとseedが同じならば同じ乱数が生成されることがわかります。
np.random.seed(seed=32) # seedは好きなintで良い
# 何回実行しても値は同じ
print(np.random.rand(5))
print(np.random.rand(5))
print(np.random.randint(2, 10, (3, 3)))
線形代数関連の計算はNumPyの重要な機能の一つです。今回は次の配列を使います。
x = np.arange(1, 4)
y = np.arange(6, 9)
A = np.arange(1, 10).reshape(3, 3)
B = np.random.randint(1, 10, (3, 3))
print("x: ", x)
print("y: ", y)
print("A: ", A)
print("B: ", B)
転置は主に2次元配列(行列)に対して定義される操作で、np.transpose()関数またはndarrayオブジェクトの.T属性で計算できます。
# 以下の二つは同じ
print(np.transpose(A))
print(A.T)
ベクトル間の内積や行列間の行列積は、np.dot()関数、np.matmul()関数、または @演算子(Python 3.5以降)を用いて計算します。これらの関数/演算子は、高次元配列に対する挙動が異なりますが、ベクトルや行列に対してはほぼ同じように使えます。詳細はここでは省略します。この講座のレベルでは同じものだと思ってもらって構わないでしょう。ただし、@演算子はPython 3.5で導入され、NumPyなどのライブラリで行列積を表すために広く使われるようになっています。np.matmul と同じ動作をします。
print("xとyの内積")
print(np.dot(x, y))
print(np.matmul(x, y))
print(x@y)
print("AとBの行列積")
print(np.dot(A, B))
print(np.matmul(A, B))
print(A@B)
以下ではnp.matmul()を用いていくとします。
# 行列なので積の順番を変えて答えが一致するとは限らない。
print(np.matmul(A, B))
print(np.matmul(B, A))
ブロードキャスト
行列とベクトルの積などでも、ブロードキャストが適用される場合があります。以下の例を見てみましょう。
# ブロードキャストの簡単な例
print(np.matmul(A, x)) # xは縦ベクトルとして解釈される
print(np.matmul(x, A)) # xは横ベクトルとして解釈される
ベクトル(1次元配列)において縦ベクトルと横ベクトルを区別したい時が多いので,実際には1次元配列ではなく2次元配列として扱うと便利なことが多いです。例えば,5次元ベクトルを扱うのにshape (5, )の1次元配列ではなくshape (5, 1)の2次元配列とすることによって転置などの操作が可能になります。
# Tips
a = np.array([1, 0, 1, 0, 1])# 5次元ベクトルを表す1次元配列
# 転置をする前後で変わらない(axisが一つしかないため)
print("転置前: ", a)
print("転置後: ", a.T)
# 2次元配列にすると縦ベクトルと横ベクトルが区別できて,わかりやすいことがある
# aを横ベクトルの2次元配列にする
a = a.reshape(1, -1) # axisを-1にすると要素数を自動で計算してくれる
print("転置前: ", a)
print("転置後:\n ", a.T)
NumPyは線形代数に関する様々な計算を行うためのサブモジュール numpy.linalg を提供しています。
linalg モジュールの基本的な関数を紹介します。
# 行列式を求める
print(np.linalg.det(A))
print(np.linalg.det(B))
# 逆行列を求める
print(np.linalg.inv(B)) # Aはdet(A)=0のため非正則行列であり、逆行列を求めようとするとLinAlgErrorが出る
# ノルムを求める
# デフォルトではL2ノルム
print(np.linalg.norm(x))
print(np.linalg.norm(y))
# ordという引数を指定するとLpノルムに対応
print(np.linalg.norm(x, ord=1))
print(np.linalg.norm(y, ord=np.inf)) # ordにnp.infを指定するときちんとL∞ノルムになっている
また,このnp.lianalg.norm()関数は集約関数であり,2次元以上の配列を指定するとaxisを指定することができます。keepdimsをTrueにすると形状を保ちます。
print("A:")
print(A)
print()
print("keepdims=False(デフォルト)")
print("axis=0", np.linalg.norm(A, axis=0))
print("axis=1", np.linalg.norm(A, axis=1))
print()
print("keepdims=True")
print("axis=0")
print(np.linalg.norm(A, axis=0, keepdims=True))
print("axis=1")
print(np.linalg.norm(A, axis=1, keepdims=True))
他にもlinalgには固有値や特異値を求めたり,特異値分解やLU分解をはじめとした行列の各種標準形を求めることもできます。
今回,たくさんのNumpyの機能を紹介してきましたが,これはあくまで氷山の一角です。今日扱った内容を含めて,どの程度まで覚える必要があるのでしょうか?
この節では,Numpyとの上手な付き合い方について解説していきます。ここに書いてあることは今後この講座で扱う全てのライブラリに共通することです。特に,Pythonプログラミング初心者に学習の道標を掲げていきたいと思います。
まず、NumPyの機能についてどこまで覚える必要があるのか?ということですが、これに関しては自分から覚えるべきことは少ないと思っています。特に,今回の講義だと、1次元、2次元配列の基本的な扱い方を覚えるだけで十分で、他の機能については、必要になったときにその都度調べればよいのです。そうすることで、自然とよく使うものは覚え、滅多に使わない機能まで無理に暗記する必要がなくなります。実際、経験豊富なエンジニアでも、普段使わない機能についてはその都度ドキュメントなどを調べて確認します。
では、実装したい機能があったときにどのように調べれば良いのでしょうか?今回は、NumPyの1次元配列を降順(大きい順)にソートするという操作が必要になった状況を仮定して、調べる手順を見てみましょう。
手順① やりたいことの言語化
例: Numpyの配列に対してソートをしたい
手順② 検索エンジンで検索。このとき、「言語(or ライブラリ名) キーワード1、キーワード2, ...」
日本語で検索すると日本語の解説記事が出てきます。
例: 「numpy 配列 ソート」で検索をする
また、今後エンジニアになりたい人は英語で入力して公式のリファレンスで検索するということを癖づけましょう。これは、いずれあるレベルに達したときに日本語の記事での解説がなかったり誤っていたりすることが少なくないためです。
例: numpyの公式リファレンス内で「ndarray sort」で検索
手順③ 公式リファレンスをみた場合,引数と戻り値とその定義を確認する
ここで、自分の実装したい機能が作れそうかわかります。また、多くの場合exampleを載せてくれているのでそこも参考にできます。
例: 公式リファレンスを見ると,いくつか検索結果が出てきます。一番上のnumpy.ndarray.sortを見ると、ソートするaxisを指定したり、ソートアルゴリズムを選択したりできるようだ。また、
Sort an array in-place.と書いてあるので、ソート後の配列を出力するわけではなく実行するだけで元の配列がソートされることもわかる。
以上の手順で、目的の操作を行う関数や方法を見つけることができます。しかし、公式のリファレンスを見るとソートの際は小さい順に並べられて,大きい順にならべるように指定するような引数がないことがすぐにわかるため、小さい順に並べた後、自分で逆順にすればいいということがわかりました。
a = np.random.randint(1, 100, (5,))
print(a)
# 小さい順に並べ替え
a.sort() # 実行するだけでaがsortされる
# a_sorted = a.sort() これは間違い。a.sort()は何も返さないのでNoneになる。
print(a)
# スライスを使って逆順にする
print(a[: : -1])
ある程度慣れてくると、どういうキーワードで計算すれば目的のものに辿り着けるのかわかってくるようになります。また,公式のリファレンスを読んで理解して使うことで,よく使うものは自然に覚えるようになります(日本語の解説記事を毎回コピペしていてはそうはなれません)。
問1
(1) 1次元配列に対して、最大値のインデックスを取得する関数を自分で調べて、実行してください。
(2) 2次元配列のaxis 1の方向で各最大値のインデックスを取得してください。その際次元を保持するようにして下さい。
(3) 2次元配列の最大値のインデックスの場所が何行何列目かを取得する操作を自分で調べて実装してください。
# (1)
# (2)
# (3)
#(1)
#入力例
a = np.array([3, 8, 2, 6, 4, 4])
# 出力例
# 1
#(2)
# 入力例
a = np.array([3, 8, 2, 6, 4, 4]).reshape(2, 3)
# 出力例
# [[1],
#[0]]
#(3)
# 入力例
a = np.array([3, 8, 2, 6, 4, 4]).reshape(2, 3)
# 最大値8は(0, 1)成分にある
# 出力例
# (0, 1)