Python:リストとNumpy配列の違い

f:id:monozukuri-bu:20191007172946j:plain

ふっちーです。
今回は戻ってPythonです。

研修で触ったNumpyについてまとめたいと思います。

目次

・Numpyの用途
・Numpyのリスト
・リストと配列の変換
・スライシング
・データ型は統一せよ
・四則演算

環境

今回はGoogle Colabolatory(Colab)を使用しました。
colab.research.google.com 本来Numpyはインストールが必要なモジュールです。
しかし、colabは機械学習を目的に作られているので、インポートするだけで使用できます。

Numpyとは

そもそもNumpyとは何でしょうか。
Wikipedia先生に頼ってみます。

Pythonは動的型付け言語であるため、プログラムを柔軟に記述できる一方で、純粋にPythonのみを使って数値計算を行うと、ほとんどの場合C言語Javaなどの静的型付き言語で書いたコードに比べて大幅に計算時間がかかる。そこでNumPyは、Pythonに対して型付きの多次元配列オブジェクト (numpy.ndarray) と、その配列に対する多数の演算関数や操作関数を提供することにより、この問題を解決しようとしている。NumPyの内部はC言語 (およびFortran)によって実装されているため非常に高速に動作する。したがって、目的の処理を、大きな多次元配列(ベクトル・行列など)に対する演算として記述できれば(ベクトル化できれば)、計算時間の大半はPythonではなくC言語によるネイティブコードで実行されるようになり大幅に高速化する。さらに、NumPyは BLAS APIを実装した行列演算ライブラリ (OpenBLAS, ATLAS, Intel Math Kernel Library など)を使用して線形代数演算を行うため、C言語で単純に書いた線形代数演算よりも高速に動作しうる

NumPy - Wikipedia

な、なるほど!
つまり、普通にPythonで書くと遅いけど、Numpyを使えば内部でC言語Fortranが動作して高速に演算ができる、ということですね。

NumpyはPythonのモジュールとだけあってPythonと似たように動かすことができます。
しかし、異なる部分も多くありPythonに似てる分ややこしいこともあります。
今回はこのNumpyをPythonとどう違うのかを見ていきましょう。

Numpyのリスト

早速Numpyに触ってみましょう。
NumpyはPythonで言うところのリストによく似ています。
それもそのはずでNumpyはリストと同じ構造の配列を基本としています。

しかし、リストとは異なり、Numpyには数学の行列の概念が存在します。
Numpyでは行を「次元数」、列を「要素数」など表記する場合があります。
また、Pythonの組み込み関数で使えるものを「リスト」、Numpyで使われるものを「配列」または「多次元配列」と呼びます。
今回は上記の書き方で統一します。

では、早速Numpyに触れていきましょう。

import Numpy as np

x = np.array([[1, 2, 3], [4, 5, 6]])#arrayで具体的に配列を作る
print(x)
# [[1 2 3]
#  [4 5 6]]

これでNumpyの配列が作成できました。
Numpyではこれをndarrayというデータ型として扱います。
list型とは異なるので、間違えないようにしましょう。

type(x)
# Numpy.ndarray

配列の作り方は他にもあります。

arange関数

y = np.arange(0, 12).reshape(2,6)#reshape(n,m)でn×mの行列に変換
print(y)
# [[ 0  1  2  3  4  5]
#  [ 6  7  8  9 10 11]]

0から11の配列ができました。
arange関数はrange関数のように配列を作ることができます。

linspace関数

z = np.linspace(1, 25, 12).reshape(3,4)
print(z)
# [[ 1.          3.18181818  5.36363636  7.54545455]
#  [ 9.72727273 11.90909091 14.09090909 16.27272727]
#  [18.45454545 20.63636364 22.81818182 25.        ]]

1から25の間を12等分した値の配列ができました。
linspace関数は始点と終点を決めて、その間を好きな数で等分した作ることができます。
また、linspace関数はarange関数とは違って、終点を含める点に注意しましょう。

リストと配列の変換

PythonのリストとNumpyの配列は互いに変換することができます。
最初に作った多次元配列のxをリストにしてみましょう。

多次元配列からリストへ

x_list=x.tolist()
print(x_list)
print(type(x_list))
# [[1, 2, 3], [4, 5, 6]]
# <class 'list'>

to_list()を使うことでこのようにリストに変換できます。

今度はx_listを多次元配列に変換して、元に戻るか試してみましょう。

リストから多次元配列へ

x_ndarray = np.array(x_list)
print(x_ndarray)
print(type(x_ndarray))
# [[1 2 3]
#  [4 5 6]]
# <class 'Numpy.ndarray'>

多次元配列を作ったarray関数で変換することができました。
最初に作ったxと同じ結果になっているのが分かります。

スライシング

Numpyでもスライシングは可能です。

print(x[1][:2])
# [4 5]

また、このような書き方も認可されています。

print(x[1, :2])
# [4 5]

行と列の順番は最初の例と変わりありません。

また、Numpyでは配列という特性から行と列という概念を持っていますと説明しました。
そのため列でスライシングすることができます。

print(x[:, 2])
# [3 6]

それぞれの行のインデックス値が2の要素を抜き出しました。
このようにNumpyでは縦横ともにスライシングできます。

当然ですが、リストはできません。

print(x_list[:, 1])
# TypeError: list indices must be integers or slices, not tuple

リストではそもそも[n, m]でスライシングできないため、列の要素を抜き出すといったことはできません。
どうしてもというなら以下の方法があります。

a = [row[2] for row in x_list]
print(a)
# [3, 6]

データ型は統一せよ

Numpyの多次元配列ではPythonのリストと違って要素のデータ型をすべて統一する必要があります。
xに文字列を入れてみましょう。

x[0][1]='g'
# ValueError: invalid literal for int() with base 10: 'g'

intに変換できないとエラーが出てきました。
これは多次元配列xのデータ型がintで固定されているからです。
配列のデータ型はdtypeで調べることができます。

print(x.dtype)
# int64

zでもやってみましょう。

z[0]='g'
# ValueError: could not convert string to float: 'g'

こちらもstrはfloatにできませんとエラーが出てきました。
このように配列のデータ型は統一する必要があります。

一方リストにはそのような制約はありません。

x_list[0][1]='g'
print(x_list)
# [[1, 'g', 3], [4, 5, 6]]

四則演算

Numpy配列の四則演算は各要素ごとにその計算が行われます。
また、Numpyでは配列の形が異なっていても、一定の条件で四則演算することができます。

print(x*3)
# [[ 3  6  9]
#  [12 15 18]]

このように配列ではない相手でもすべての要素に対して計算を行います。
これはスカラー倍と呼ばれています。

次に配列同士で計算してみましょう。

b = np.array([1,2,3])
print(x+b)
# [[2 4 6]
#  [5 7 9]]

列数は両方とも3ですが、bは次元数が1で、xは次元数が2ですが計算できました。
このように次元数が異なる場合は、次元数が少ないほうの配列が多いほうに次元数を合わせます。
つまり、aは計算する瞬間だけ
[[1,2,3]
[1,2,3]]
となり、それぞれの要素の積を計算できたということです。

最後に列数が異なる場合の計算も見てみましょう。

c = np.array([[5], [7]])
print(x-c)
# [[-4 -3 -2] 
#  [-3 -2 -1]]

こちらもcが
[[5,5,5]
[7,7,7]]
のようになり、計算することができました。

このように計算するために配列の形を合わせるのをブロードキャスト機能といいます。

ちなみに、このような場合は計算できません。
+ (n,m)と(n,l)

print(x-y)#(2,3)と(2,6)
# ValueError: operands could not be broadcast together with shapes (2,3) (2,6) 

これはブロードキャスト機能が適用されるのは条件があるからです。
それは計算に使える配列は次元数か要素数が1か最大値と同じ場合のみと限られているからです。
要するに次元数が2において計算できる配列の形は
・(n, m)と(n, m)
・(n, m)と(n, 1)
・(n, m)と(1, m)
・(n, 1)と(1, m)
・(n, m)とk(実数)
だけになります。

また、これは行列との計算とは異なるということに注意しましょう。
例えば、(a, b) 行列と (c, d) 行列の乗算を行う場合、b=cでないと計算できません。
Numpy配列で行列の計算を行う場合は、dot関数を使いましょう。

d = np.array([[3,3],[3,3],[3,3]])
print(np.dot(x,d))
# [[18 18]
#  [45 45]]

リストに対して同じようにすると以下のようになります。

print(x_list*3)
print(x_list+3)
# [[1, 'g', 3], [4, 5, 6], [1, 'g', 3], [4, 5, 6], [1, 'g', 3], [4, 5, 6]]
# TypeError: can only concatenate list (not "int") to list

最後に、Numpyの計算速度が速いことを確かめてみましょう。

d = np.arange(1,1000) 

%timeit sum(d)
%timeit np.sum(d)
# 10000 loops, best of 3: 93.9 ?s per loop
# The slowest run took 21.44 times longer than the fastest. This could mean that an intermediate result is being cached.
# 100000 loops, best of 3: 4.3 ?s per loop

上がPythonのsumで計算した場合の時間で、下がNumpyのnp.sumで計算した場合の時間です。
このようにNumpyの方が速いとわかります。

まとめ

Numpyの配列とPythonのリストの違いをまとめました。
Pythonに似ているからこそ、覚えやすい面もあれば、混乱する面もあります。
しっかり細かな違いを把握していきましょう。