python-numpy/

Я много слышал про библиотеку NumPy , что дескать в ней есть много полезных математических функций, или что-то в этом роде. Однако какой-то практической задачи, где был бы нужен NumPy, мне как-то не подворачивалось, и потому большого интереса изучать данную библиотеку у меня не было. Потом я заинтересовался Software Defined Radio , и, как следствие, цифровой обработкой сигналов. И вот тут иногда возникает желание по-быстрому поиграться в REPL, скажем, с быстрым преобразованием Фурье (Fast Fourier Transform, FFT) или чем-то таким. Оказалось, что это задача как раз для NumPy. Поэтому было решено разобраться в возможностях этой библиотеки как следует, выяснив заодно, что в ней еще есть помимо FFT.

Простой пример: как выглядит функция?

Для начала попробуем понять общую идею, стоящую за NumPy. Главным объектом, которым оперирует NumPy, является массив. Например, следующий код объявляет массив целых чисел от -5 до 5:

>>> import numpy as np
>>> x = np.linspace(-5, 5, 11)
>>> x
array([-5., -4., -3., -2., -1.,  0.,  1.,  2.,  3.,  4.,  5.])

Массивы не обязательно должны быть именно одномерными, но пока что остановимся на одномерных. Массивы можно складывать, вычитать и умножать:

>>> x — x
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
>>> x * x
array([25., 16.,  9.,  4.,  1.,  0.,  1.,  4.,  9., 16., 25.])

Также определены арифметические операции между массивами и числами:

>>> 10 + 2 * x
array([ 0.,  2.,  4.,  6.,  8., 10., 12., 14., 16., 18., 20.])

А еще массивы имеют разные полезные методы, например:

>>> (x / 2).round()
array([-2., -2., -2., -1., -0.,  0.,  0.,  1.,  2.,  2.,  2.])

Само собой разумеется, массивы NumPy можно преобразовывать в обычные списки языка Python, и обратно:

>>> y = np.array([1,2,3])
>>> y
array([1, 2, 3])
>>> y.tolist()
[1, 2, 3]

Вы спросите, а в чем практическая польза таких массивов? Допустим, мы хотим по-быстрому посмотреть, как выглядит график какой-то функции. Для определенности возьмем сигмоиду , часто используемую в качестве функции активации в нейронных сетях . Рисовать будем при помощи уже знакомой нам библиотеки Matplotlib .

С NumPy код получается крайне простым:

# vim: set ai et ts=4 sw=4:

import matplotlib . pyplot as plt
import numpy as np

x = np. linspace ( 5 , 5 , 100 )

def sigmoid ( alpha ) :
return 1 / ( 1 + np. exp ( — alpha * x ) )

dpi = 80
fig = plt. figure ( dpi = dpi , figsize = ( 512 / dpi , 384 / dpi ) )

plt. plot ( x , sigmoid ( 0.5 ) , ‘ro-‘ )
plt. plot ( x , sigmoid ( 1.0 ) , ‘go-‘ )
plt. plot ( x , sigmoid ( 2.0 ) , ‘bo-‘ )

plt. legend ( [ ‘A = 0.5’ , ‘A = 1.0’ , ‘A = 2.0’ ] , loc = ‘upper left’ )

fig. savefig ( ‘sigmoid.png’ )

Результат:

Сигмоид, нарисованный при помощи NumPy и Matplotlib

Заметьте, что больше не нужно писать циклы, вычисляющие значение функции в различных точках, поскольку NumPy делает все это за нас. Фактически, мы просто применяем функции к функциям и выводим получившиеся функции. Декларативное и чисто функциональное программирование в самом натуральном его виде!

Всякая статистика

Иногда бывает нужно по-быстрому посчитать медиану или, скажем, 99-ый процентиль по каким-то значениям. NumPy приходит на помощь и здесь.

Сгенерим немного случайных данных:

>>> import numpy as np
>>> ones = np.ones(50)
>>> rnd = np.random.random(50) * 0.1
>>> samples = ones + rnd

Посчитаем среднее:

>>> np.average(samples)
1.0456167098847502
>>> np.mean(samples)
1.0456167098847502

Медиану:

>>> np.median(samples)
1.0446054794173638

Процентили:

>>> np.percentile(samples, 50)
1.0446054794173638
>>> np.percentile(samples, 95)
1.0957859973475361
>>> np.percentile(samples, 99)
1.0973855545421

Максимум, минимум, peak-to-peak:

>>> samples.max()
1.0967179044332522
>>> samples.min()
1.0060578855891802
>>> samples.ptp()
0.09066001884407204

А заодно уж и стандартное отклонение с дисперсией :

>>> np.std(samples)
0.027949661469244255
>>> np.var(samples)
0.000781183576245357

Использованная выше функция np.random.random генерирует случайные числа с равномерным распределением . А если мы хотели бы использовать нормальное распределение ? Нет проблем:

>>> import matplotlib.pyplot as plt
>>> samples = np.random.normal(loc=0, scale=5, size=100000)
>>> plt.hist(samples, 200)
>>> plt.show()

Помимо нормального и равномерного распределения NumPy также предлагает beta , dirichlet , gamma , binomial , exponential , и другие.

Линейная алгебра

До сих пор мы рассматривали одномерные массивы. Когда массив двумерный, он фактически становится матрицей. А там где матрицы, там и линейная алгебра.

Объявим несколько матриц:

>>> import numpy.matlib
>>> import numpy.linalg
>>> m1 = np.arange(1, 10).reshape(3,3)
>>> m1
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
>>> m2 = np.identity(3)
>>> m2
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])

Транспонируем первую матрицу, а также посчитаем след и детерминант второй:

>>> m1.transpose()
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
>>> m2.trace()
3.0
>>> np.linalg.det(m2)
1.0

Матрицы можно складывать, умножать на число, умножать на вектор, а также умножать на другую матрицу:

>>> m1 + m2
array([[ 2.,  2.,  3.],
[ 4.,  6.,  6.],
[ 7.,  8., 10.]])
>>> m1 * 3
array([[ 3,  6,  9],
[12, 15, 18],
[21, 24, 27]])
>>> m1 + np.array([1,2,3])
array([[ 2,  4,  6],
[ 5,  7,  9],
[ 8, 10, 12]])
>>> m1 * m2
array([[1., 0., 0.],
[0., 5., 0.],
[0., 0., 9.]])

Посчитать матрицу, обратную к данной, можно функцией np.linalg.inv :

>>> m3 = np.matlib.rand(3, 3)
>>> (m3 * np.linalg.inv(m3))
matrix([[1.0000000e+00, 0.0000000e+00, 0.0000000e+00],
[0.0000000e+00, 1.0000000e+00, 0.0000000e+00],
[4.4408921e-16, 0.0000000e+00, 1.0000000e+00]])
>>> (m3 * np.linalg.inv(m3)).round()
matrix([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])

На первый взгляд, линейная алгебра может показаться не самым интересным предметом. Но это только до тех пор, пока вы не попытаетесь писать свои игровой движок на OpenGL. Кое-какие подробности по этой теме можно найти в посте Продолжаем изучение OpenGL: управление камерой при помощи мыши и клавиатуры .

FFT и обратный FFT

Казалось бы, в начале поста была сказано про FFT, а в итоге вместо FFT мы говорим про какие-то там массивы и матрицы. Ладно, пожалуйста, вот вам FFT:

#!/usr/bin/env python3

import numpy as np
import matplotlib . pyplot as plt

dpi = 80

tau = 2 * np. pi
t = np. linspace ( 0 , 0.5 , 500 )
x_1 = 0.9 * np. sin ( 100 * tau * t )
x_2 = 0.4 * np. sin ( 150 * tau * t )
x = x_1 + x_2
N = x. size

# x = x * np.hamming(N)

fig = plt. figure ( dpi = dpi , figsize = ( 512 / dpi , 384 / dpi ) )
plt. plot ( t , x )
plt. xlabel ( «Time (s)» )
plt. ylabel ( «Amplitude» )
fig. savefig ( «x.png» )
plt. close ( )

X = np. fft . fft ( x )

# t[1] — t[0] = sample rate
# 1/(t[1] — t[0]) = frequency
freq = np. linspace ( 0 , 1 / ( t [ 1 ] — t [ 0 ] ) , N ) [ : ( N // 2 ) ]

# 1 / N is a normalization factor
X_amp = ( 1 /N ) * np. abs ( X ) [ : ( N // 2 ) ]
X_phs = ( 1 /N ) * np. angle ( X ) [ : ( N // 2 ) ]

fig = plt. figure ( dpi = dpi , figsize = ( 512 / dpi , 384 / dpi ) )
plt. bar ( freq , X_amp )
plt. xlabel ( «Frequency (Hz)» )
plt. ylabel ( «Amplitude» )
fig. savefig ( «X_amp.png» )
plt. close ( )

fig = plt. figure ( dpi = dpi , figsize = ( 512 / dpi , 384 / dpi ) )
plt. bar ( freq , X_phs )
plt. xlabel ( «Frequency (Hz)» )
plt. ylabel ( «Amplitude» )
fig. savefig ( «X_phs.png» )
plt. close ( )

fig = plt. figure ( dpi = dpi , figsize = ( 512 / dpi , 384 / dpi ) )
x2 = np. fft . ifft ( X )
plt. plot ( t , x2. real )
plt. ylabel ( «Amplitude» )
plt. xlabel ( «Time (s)» )
fig. savefig ( «x2.png» )
plt. close ( )

Исходный сигнал:

FFT в NumPy: исходный сигнал

Его разложение на частоты:

FFT в NumPy: разложение сигнала на частоты

К сожалению, объяснение преобразования Фурье выходит за рамки данного поста. Однако по этой теме есть масса источников в интернете. В частности, можно порекомендовать статью Understanding the Fourier Transform by example в блоге ritchievink.com. Приведенный выше отрывок кода основан на коде из этой статьи.

Заключение

Как видите, библиотека NumPy оказалась крайне полезной в целом ряде задач. И это мы рассмотрели далеко не все ее возможности. В качестве источников дополнительной информации можно порекомендовать книги, NumPy их посвящено больше одной. Я лично читал «NumPy Beginners Guide, Third Edition» за авторством Ivan Idris, она оказалась вполне приличной.

Считается, что NumPy потребляет меньше памяти и работает до 10 раз быстрее аналогичного кода, написанного на Python. В этом посте я решил не приводить никаких бенчмарков, потому что тут есть масса нюансов , и все сильно зависит от специфики конкретного приложения (объемы данных, размеры мыссивов, и т.д.). Заинтересованные читатели могут изучить вопрос о производительности NumPy в качестве домашнего задания.

Еще в качестве домашнего задания рекомендую взять два сигнала, один с частотой 250 Гц, и второй с частотой 50 Гц. Перемножьте эти сигналы. Как выглядит FFT результата? Как он изменяется при изменении частоты перемножаемых сигналов? За основу возьмите последний из приведенных скриптов.

Дополнение: Еще примеры использования NumPy вы найдете в статьях Передача изображений в SSB-сигнале с помощью Python и Рисуем диаграммы Вольперта-Смита на Python . Кое-какие пояснения по поводу работы преобразования Фурье вы найдете в Шпаргалке по математике, связанной с I/Q-сигналами и Шпаргалке по работе DSP фильтров .

EnglishRussianUkrainian