Векторизация на примере вычисления статистик
Contents
Векторизация на примере вычисления статистик#
В домашнем задании №2 есть задача, в которой необходимо реализовать вычисление среднего арифметического и среднеквадратичного отклонения для каждого столбца массива чисел.
Рассмотрим в качестве примере то же задание, но для вычисления этих же характеристик одного числового ряда. Т.е. будем считать, что есть числовой ряд \(x = \{x_1, ..., x_N\}\), данный нам в виде массива x
. Реализуем функцию, которая выдаст нам среднее и среднеквадратичное отклонения этого числового ряда.
Подход в лоб#
Для начала реализуем этот алгоритм так, как мы бы это делали в C/C++
, т.е. используя циклы и не пользуясь векторизацией. Напишем отдельную функцию для вычисления среднего значения.
def mean1D(x):
s = 0
for number in x:
s += number
return s / x.size
Приведенная выше функция заводит переменную S
, в которую на протяжении цикла накапливается сумма. Когда сумма всего массива накоплена, то возвращается результат деления этой суммы на количество элементов в массиве, которое можно узнать у массива x
по атрибуту size
(предполагаем, что массив одномерный).
Реализуем функцию вычисления среднеквадратичного отклонения. В выражении для среднеквадратичного отклонения участвует среднее арифметическое. Чтобы не дублировать вычисления, будем передавать среднее значение вторым аргументов в функцию.
import math
def std1D(x, mean):
s = 0
for number in x:
s += (number - mean) ** 2
return math.sqrt(s / x.size)
Приведенная функция накапливает сумму квадратов отклонений чисел массива от среднего значения и возвращает корень из этой суммы, деленной на количество элементов в массиве. В NumPy
есть методы mean
и std
. Проведем хотя бы один тест, чтобы убедиться, что приведенные выше функции работают.
import numpy as np
N = 100 # размер массива
x = np.random.random(N)
mean_1 = mean1D(x)
mean_2 = x.mean()
std_1 = std1D(x, mean_1)
std_2 = x.std()
print(mean_1, mean_2)
print(std_1, std_2)
0.5153244859252695 0.5153244859252698
0.28374330737876574 0.28374330737876574
Векторизованные версии#
Функции работают, теперь перепишем их, используя векторизацию.
def mean1D_vectorized(x):
return x.sum() / x.size
Векторизованная версия функции mean1D
примитивна. Цикл суммирования заменили на вызов метода ndarrray.sum
.
def std1D_vectorized(x, mean):
tmp_1 = x - mean
tmp_2 = tmp_1 ** 2
tmp_3 = tmp_2.sum()
return math.sqrt(tmp_3 / x.size)
Вычисление среднеквадратичного отклонения рассмотрим подробнее.
def std1D_vectorized(x, mean):
tmp_1 = x - mean
Как и прежде, чтобы не вычислять дважды среднее принимаем, mean
в качестве второго аргумента. На следующей стоке вычисляется разница между массивом x
и средним значением mean
(скаляром). Формы не совпадают, а значит производится broadcasting
и в результате tmp_1[i] = x[i] - mean
.
def std1D_vectorized(x, mean):
tmp_1 = x - mean
tmp_2 = tmp_1 ** 2
Далее встречается возведение одномерного массива tmp_1
в степень 2
. Т.к. 2
— скаляр, то производится broadcasting
и значение tmp_2[i] = tmp_1[i] ** 2 = (x[i] - mean) ** 2
.
def std1D_vectorized(x, mean):
tmp_1 = x - mean
tmp_2 = tmp_1 ** 2
tmp_3 = tmp_2.sum()
В следующей строке вычисляется сумма элементов массива tmp_2
, т.е. \(\sum_{i=0}^{N-1}\) tmp_2[i]
= \(\sum_{i=0}^{N-1}\)tmp_1[i]
\({}^2\)= \(\sum_{i=0}^{N-1} (\)x[i] - mean
\() ^ 2\).
def std1D_vectorized(x, mean):
tmp_1 = x - mean
tmp_2 = tmp_1 ** 2
tmp_3 = tmp_2.sum()
return math.sqrt(tmp_3 / x.size)
И наконец, вычислив такую сумму, функция возвращает квадратный корень из нее деленной на размер массива.
Конечно, вводить переменные tmp_1
, tmp_2
и tmp_3
вовсе не обязательно. Это сделано в этом примере, лишь для того, чтобы подробнее по этапам объяснить, что происходит под капотом функции ниже.
def std1D_vectorized(x, mean):
return np.sqrt(((x - mean) ** 2).sum() / x.size)
print(x.std(), std1D_vectorized(x, x.mean()))
0.2686383564940283 0.2686383564940283
Кроме того первая версия этой функции позволяет явно обратить ваше внимание на то, что в процессе работы векторизованной функции (и изначальной и сокращенной) создаются дополнительные массивы. Для этого выделяется дополнительная память и в ряде случаев это может быть критично. Эти дополнительные массивы собираются сборщиком мусора, как только они перестают быть нужными. В первом случае на выходе из функции (когда локальные имена tmp_1
, tmp_2
и tmp_3
уничтожаются) и во втором случае сразу, как только вычисления с ними закончены.