Pythonで数値の平方根を計算する

数の平方根は、物理学、数学、コンピュータサイエンスなど、科学のあらゆる場面で用いられる非常に一般的な数学関数である。

数や式の平方根は、科学のあらゆる事柄における公式で、特に、微積分で観察できるものをモデル化して現実を表現する方法において、非常に頻繁に使用されます。

この記事では、Pythonで数の平方根を計算する様々な方法を見ていきます。

最後に、すべてのアプローチをテストするために、定数と乱数、および乱数のリストを使ったパフォーマンスベンチマークを行います。

PythonでNumPyを使って平方根を計算する。

NumPy は科学計算ライブラリで、多くのアプリケーションやユースケースで利用されています。

当然ながら、ヘルパーメソッドとして数学関数のラッパーをたくさん持っています。

まだインストールされていない場合は、pip経由でインストールすることができます。

$ pip install numpy


NumPyで言えば、 sqrt() 関数は数の平方根を計算し、その結果を返します。

import numpy as np
x = np.sqrt(2)
print(x)


という結果になります。

1.4142135623730951


引数として1つの変数を取る他に、sqrt() はリストをパースして平方根のリストを返すことができます。

arr = [2, 3, 5, 7]
roots = np.sqrt(arr)
print(roots)


この結果は次のようになります。

[1.41421356 1.73205081 2.23606798 2.64575131]


しかし、sqrt()関数には制限があります。

実数に対する平方根の演算は正の数に対してのみ定義されているので、負の数の平方根を計算することができないのです。

なぜなら、実数に対する平方根の演算は正の数に対してのみ定義されているからです。

print(np.sqrt(-4))


負の数の平方根を計算しようとすると、警告と nan 値が表示されます。

RuntimeWarning: invalid value encountered in sqrt
nan


Numpyを使った複素数の平方根の計算

幸いなことに、NumPyは実数だけでなく、複素数も扱うことができます。

import numpy as np


complex_number = -1 + 1j
complex_array = [-2, 3, complex_number]


complex_root = np.sqrt(complex_number)
complex_array_roots = np.sqrt(complex_array)


print(f"Square root of '{complex_number}':
 {complex_root}")
print(f"Square roots of '{complex_array}':
 {complex_array_roots}")


もしリストの中に少なくとも1つの複素数があれば、すべての数値は複素数にキャストされて扱われるので、負の整数であっても足し算が可能です。

Square root of '(-1+1j)':
 (0.45508986056222733+1.09868411346781j)
Square roots of '[-2, 3, (-1+1j)]':
 [0.        +1.41421356j 1.73205081+0.j         0.45508986+1.09868411j]


Pythonのmathモジュール

Python の math モジュールは Python に同梱されている標準のモジュールです。

常に利用可能ですが、インポートする必要があり、平方根や累乗など、いくつかの一般的な関数のラッパーを提供します。

import math


math.sqrt()

mathモジュールのsqrt()` 関数は、任意の正の数の平方根を返す素直な関数です。

print(math.sqrt(2))


これは次のような結果になります。

1.4142135623730951


NumPy の sqrt() 関数とは異なり、1つの要素に対してのみ作用します。

したがって、リスト内のすべての要素の平方根を計算したい場合は、 for ループかリスト内包を使用しなければなりません。

import math


arr = [2, 3, 5, 7]
roots = []


for x in arr:
    roots.append(math.sqrt(x))


# OR
roots = [math.sqrt(x) for x in arr]


どちらの場合も、rootsリストが含まれます。

[1.4142135623730951, 1.7320508075688772, 2.23606797749979, 2.6457513110645907]


math.pow()

数の平方根は、数を1/2の累乗にすることによっても計算することができます。

√x=x12x=x12
Γ x = x^{frac 1 2} Γ x= x12

つまり、ある数の平方根を求めるには、その数を1/2乗することで表現することができます。

math.pow()` は底辺と指数を引数にとり、底辺を指数のべき乗にします。

print(math.pow(2, 0.5))


当然、この結果は

1.4142135623730951


♪The * Operator

演算子 *** は二項演算子で、通常の * との乗算と同じように2つの値で演算します。

ただし、指数演算に使われる演算子なので、左引数を右引数のべき乗にします。

この方法は、前のものと同じ形で使うことができます。

print(2 ** 0.5)


という結果にもなる。

1.4142135623730951


pow()関数

Python にはもう1つ、組み込みの pow() メソッドがあり、これは math モジュールのインポートを必要としません。

このメソッドは内部的には math.pow() メソッドとは異なっています。

math.pow()は暗黙のうちに要素をdoubleにキャストしますが、pow()**` 演算子を中心としたオブジェクトの内部実装を使用します。

この実装の違いは、ある文脈ではどちらかを使うことを正当化するかもしれませんが、単に数の平方根を計算するのであれば、その違いを実感することはないでしょう。

print(pow(2, 0.5))


この結果、次のようになります。

1.4142135623730951


パフォーマンスベンチマーク

では、どれが最も優れたパフォーマンスをもたらすのか、そしてどれを選ぶべきなのでしょうか。

いつものように、明確な勝者は存在せず、それはメソッドの使用方法に依存します。

つまり、定数や乱数、乱数の配列をより大規模に扱う場合、これらのメソッドは異なるパフォーマンスを発揮するのです。

それでは、定数、乱数、乱数の配列で試してみましょう。

import timeit


print("Time to execute 100k operations on constant number: 
")
print("math.sqrt(): %ss" % timeit.timeit("math.sqrt(100)", setup="import math", number=100000))
print("math.pow(): %ss" % timeit.timeit("math.pow(100, 0.5)", setup="import math", number=100000))
print("pow(): %ss" % timeit.timeit("pow(100, 0.5)", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(100)", setup="import numpy as np", number=100000))
print("** operator: %ss" % timeit.timeit("100 ** 0.5", number=100000))


print("
Time to execute 100k operations on random number: 
")
print("math.sqrt() %ss" % timeit.timeit("math.sqrt(random.random())", setup="import math; import random;", number=100000))
print("math.pow(): %ss" % timeit.timeit("math.pow(random.random(), 0.5)", setup="import math; import random", number=100000))
print("pow(): %ss" % timeit.timeit("pow(random.random(), 0.5)", setup="import random", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(random.random())", setup="import numpy as np; import random", number=100000))
print("** operator: %ss" % timeit.timeit("random.random() ** 0.5", setup="import random", number=100000))


print("
Time to execute 100k operations on list of random numbers: 
")
print("math.sqrt() %ss" % timeit.timeit("[math.sqrt(x) for x in np.random.rand(100)]", setup="import math; import numpy as np;", number=100000))
print("math.pow(): %ss" % timeit.timeit("[math.pow(x, 0.5) for x in np.random.rand(100)]", setup="import math; import numpy as np;", number=100000))
print("pow(): %ss" % timeit.timeit("[pow(x, 0.5) for x in np.random.rand(100)]", setup="import numpy as np;", number=100000))
print("np.sqrt(): %ss" % timeit.timeit("np.sqrt(np.random.rand(100))", setup="import numpy as np; import numpy as np;", number=100000))
print("** operator: %ss" % timeit.timeit("np.random.rand(100) ** 0.5", setup="import numpy as np", number=100000))


定数(最適化のためにキャッシュされる可能性があります)、100k回の反復処理ごとの乱数、100個の乱数リストです。

注:100個の乱数を生成するには、(キャッシュされた)定数値を使用するよりも時間がかかるため、そのテストにおける他のメソッドと比較した各テストの相対値のみが関連します。

このコード片を実行した結果は

Time to execute 100k operations on constant number:


math.sqrt(): 0.014326499999999999s
math.pow(): 0.0165132s
pow(): 0.018766599999999994s
np.sqrt(): 0.10575379999999998s
** operator: 0.0006493000000000193s


Time to execute 100k operations on random number:


math.sqrt() 0.019939999999999958s
math.pow(): 0.022284300000000035s
pow(): 0.0231711s
np.sqrt(): 0.09066460000000004s
** operator: 0.018928s


Time to execute 100k operations on list of random numbers:


math.sqrt() 2.7786073s
math.pow(): 2.9986906s
pow(): 3.5157339999999992s 
np.sqrt(): 0.2291957s
** operator: 0.2376024000000001s


定数の場合 – math.pow(), math.sqrt(), pow() 関数は、言語レベルでCPUのキャッシュをうまく利用できるため、NumPyの sqrt() 関数を大きく上回ります。

乱数では、キャッシングがうまく働かず、より小さな差異が見られます。

乱数のリストでは、 np.sqrt() は3つの組み込みメソッドよりも大きく性能が向上し、 ** 演算子も同じような性能になります。

まとめると

  • 定数に対しては、** 演算子がテストマシンで最も良い性能を示し、組み込みメソッドよりも 16 倍も速く実行されました。
  • 乱数に対しては, np.sqrt() 演算子が組み込み関数や ** 演算子を上回りますが,結果に大きな差はありません.
  • ランダムな配列に対しては、 np.sqrt() 関数は組み込みのメソッドよりも優れていますが、 ** 演算子は非常に近い結果になっています。

扱う具体的な入力に応じて – これらの関数のどちらかを選択することになります。

一見、どれも性能が良さそうで、ほとんどの場合、大きな違いはありませんが、巨大なデータセットを扱う場合、処理時間が10%でも短くなれば、長期的には助かるでしょう。

処理するデータに応じて – ローカルマシンで様々なアプローチを試してみてください。

結論

この短い記事では、Python で数値の平方根を計算する方法をいくつか見てきました。

ここでは、 math モジュールの pow()sqrt() 関数、そして組み込みの pow() 関数、NumPy の sqrt() 関数と ** 演算子について見てきました。

最後に、定数、乱数、乱数リストという異なるタイプの入力に対する性能を比較するために、メソッドのベンチマークを行いました。

タイトルとURLをコピーしました