Примеры использования Python-библиотеки NumPy
30 июля 2018
Я много слышал про библиотеку NumPy, что дескать в ней есть много полезных математических функций, или что-то в этом роде. Однако какой-то практической задачи, где был бы нужен NumPy, мне как-то не подворачивалось, и потому большого интереса изучать данную библиотеку у меня не было. Потом я заинтересовался Software Defined Radio, и, как следствие, цифровой обработкой сигналов. И вот тут иногда возникает желание по-быстрому поиграться в REPL, скажем, с быстрым преобразованием Фурье (Fast Fourier Transform, FFT) или чем-то таким. Оказалось, что это задача как раз для NumPy. Поэтому было решено разобраться в возможностях этой библиотеки как следует, выяснив заодно, что в ней еще есть помимо FFT.
Простой пример: как выглядит функция?
Для начала попробуем понять общую идею, стоящую за NumPy. Главным объектом, которым оперирует NumPy, является массив. Например, следующий код объявляет массив целых чисел от -5 до 5:
>>> x = np.linspace(-5, 5, 11)
>>> x
array([-5., -4., -3., -2., -1., 0., 1., 2., 3., 4., 5.])
Массивы не обязательно должны быть именно одномерными, но пока что остановимся на одномерных. Массивы можно складывать, вычитать и умножать:
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.])
Также определены арифметические операции между массивами и числами:
array([ 0., 2., 4., 6., 8., 10., 12., 14., 16., 18., 20.])
А еще массивы имеют разные полезные методы, например:
array([-2., -2., -2., -1., -0., 0., 0., 1., 2., 2., 2.])
Само собой разумеется, массивы NumPy можно преобразовывать в обычные списки языка Python, и обратно:
>>> y
array([1, 2, 3])
>>> y.tolist()
[1, 2, 3]
Вы спросите, а в чем практическая польза таких массивов? Допустим, мы хотим по-быстрому посмотреть, как выглядит график какой-то функции. Для определенности возьмем сигмоиду, часто используемую в качестве функции активации в нейронных сетях. Рисовать будем при помощи уже знакомой нам библиотеки Matplotlib.
С NumPy код получается крайне простым:
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 делает все это за нас. Фактически, мы просто применяем функции к функциям и выводим получившиеся функции. Декларативное и чисто функциональное программирование в самом натуральном его виде!
Всякая статистика
Иногда бывает нужно по-быстрому посчитать медиану или, скажем, 99-ый процентиль по каким-то значениям. NumPy приходит на помощь и здесь.
Сгенерим немного случайных данных:
>>> ones = np.ones(50)
>>> rnd = np.random.random(50) * 0.1
>>> samples = ones + rnd
Посчитаем среднее:
1.0456167098847502
>>> np.mean(samples)
1.0456167098847502
Медиану:
1.0446054794173638
Процентили:
1.0446054794173638
>>> np.percentile(samples, 95)
1.0957859973475361
>>> np.percentile(samples, 99)
1.0973855545421
Максимум, минимум, peak-to-peak:
1.0967179044332522
>>> samples.min()
1.0060578855891802
>>> samples.ptp()
0.09066001884407204
А заодно уж и стандартное отклонение с дисперсией:
0.027949661469244255
>>> np.var(samples)
0.000781183576245357
Использованная выше функция np.random.random
генерирует случайные числа с равномерным распределением. А если мы хотели бы использовать нормальное распределение? Нет проблем:
>>> samples = np.random.normal(loc=0, scale=5, size=100000)
>>> plt.hist(samples, 200)
>>> plt.show()
Помимо нормального и равномерного распределения NumPy также предлагает beta
, dirichlet
, gamma
, binomial
, exponential
, и другие.
Линейная алгебра
До сих пор мы рассматривали одномерные массивы. Когда массив двумерный, он фактически становится матрицей. А там где матрицы, там и линейная алгебра.
Объявим несколько матриц:
>>> 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.]])
Транспонируем первую матрицу, а также посчитаем след и детерминант второй:
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
>>> m2.trace()
3.0
>>> np.linalg.det(m2)
1.0
Матрицы можно складывать, умножать на число, умножать на вектор, а также умножать на другую матрицу:
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.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:
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()
Исходный сигнал:
Его разложение на частоты:
К сожалению, объяснение преобразования Фурье выходит за рамки данного поста. Однако по этой теме есть масса источников в интернете. В частности, можно порекомендовать статью 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 фильтров.
Метки: Python.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.