Документация Engee

Векторизация и логическое индексирование

Страница в процессе разработки.

Векторизация

Векторизация — это адаптация функции для работы со всеми элементами вектора или матрицы одновременно, вместо использования циклов и скалярных операций. Язык программирования Engee — Julia, поддерживает векторизацию операций. Векторизация предоставляет следующие преимущества:

  • Сокращает количество строк кода, уменьшая вероятность ошибок;

  • Векторизованный код чаще всего выглядит как математические выражения, что упрощает его понимание и проверку;

  • Векторизованный код выполняется быстрее, так как Julia обрабатывает векторные и матричные операции с высокой эффективностью.

Добавление точки перед оператором (., .^, ./) превращает его в операцию над массивами, что позволяет применять их конкретному элементу массива. Базовые операторы, поддерживающие векторизацию, включают +, -, , /, ^ и сравнения, такие как <, >, <=, >=, ==, !=.

В Julia практически все базовые операции поддерживают векторизацию через добавление точки перед оператором. Ниже представлен список базовых операций, которые можно векторизовать:

Список операций с поддержкой векторизации в Julia

Ниже перечислены основные операции, которые поддерживают векторизацию в Julia. Однако точный список включает еще больше функций, так как векторизация в Julia распространяется на многие встроенные функции. Важно понимать, что векторизацию можно применять не только к базовым арифметическим и логическим операторам, но и к математическим функциям, некоторым функциям из стандартных библиотек (если они поддерживают работу с массивами, например .min, .max) и пользовательским функциям, если они созданы с учетом поддержки работы с массивами.

Арифметические операции:

  1. .+ — поэлементное сложение;

  2. .- — поэлементное вычитание;

  3. .* — поэлементное умножение;

  4. ./ — поэлементное деление;

  5. .^ — поэлементное возведение в степень;

  6. .% — поэлементный остаток от деления.

Операции сравнения:

  1. .== — поэлементное равенство;

  2. .!= — поэлементное неравенство;

  3. .> — поэлементное больше;

  4. .< — поэлементное меньше;

  5. .>= — поэлементное больше или равно;

  6. .< — поэлементное меньше или равно.

Логические операции:

  1. .& — поэлементное логическое "и";

  2. .| — поэлементное логическое "или";

  3. .⊻ — поэлементное логическое "исключающее или";

  4. .! — поэлементное логическое "не".

Побитовые операции:

  1. .>> — поэлементный сдвиг вправо;

  2. .<< — поэлементный сдвиг влево;

  3. .& — поэлементное побитовое "и";

  4. .| — поэлементное побитовое "или";

  5. .⊻ — поэлементное побитовое "исключающее или";

  6. .~ — поэлементное побитовое "не".

Унарные операции:

  1. .√ — поэлементный квадратный корень;

  2. .log — поэлементный логарифм;

  3. .exp — поэлементное экспоненциальное преобразование;

  4. .sin, .cos, .tan и другие тригонометрические функции также поддерживают поэлементную работу.

Не все операции требуют векторизации. Например, оператор присваивания = и некоторые специфические функции (например, работающие с одним массивом целиком, такие как sort) не поддерживают векторизацию.

Поддерживаемые операции

Векторизация позволяет применять операции ко всем элементам массива одновременно. Это снижает необходимость в явных циклах, которые могут замедлять выполнение программы, и позволяет компилятору оптимизировать выполнение.

В Julia векторизация часто достигается за счет встроенных функций и средств работы с массивами, что облегчает создание кода, близкого к математическим формулам. Например, нужно вычислить синус для 1001 значений от 0 до 10. Обычный код на Julia выглядел бы так:

y = []
for t in 0:0.01:10
    push!(y, sin(t))
end

Тогда векторизованный код может выглядеть следующим образом:

t = 0:0.01:10
y = sin.(t)

Здесь sin.(t) означает, что функция sin применяется к каждому элементу массива t. Этот вариант кода выполняется быстрее первого и является более эффективным использованием возможностей Julia как языка для математических вычислений.

В Julia можно выполнять операции с массивами, применяя их ко всем элементам одновременно. Например, если есть данные о диаметре D и высоте H для 10000 геометрических конусов, то можно рассчитать их объемы без использования циклов:

D = rand(10_000)
H = rand(10_000)
V = (1 / 12) * π * D.^2 .* H

Логическое индексирование

Логическая индексация — это способ доступа к элементам массива на основе логических условий. Вместо указания индекса элемента, передается массив логических значений (true или false), где true указывает, какие элементы нужно выбрать. Это упрощает фильтрацию данных, особенно при работе с большими массивами. Логическая индексация позволяет:

  • Быстро отбирать данные, соответствующие определенному условию;

  • Устранять некорректные или нежелательные значения;

  • Создавать лаконичные алгоритмы с минимальным количеством строк кода.

Примеры

Замена цикла

Векторизация особенно полезна при выполнении специфических задач. Например, нахождение накопленной суммы на каждом пятом элементе массива. Без векторизации код будет выглядеть так:

x = 1:10000
y = []
for n in 5:5:length(x)
    push!(y, sum(x[1:n]))
end

Тогда векторизованный код будет иметь вид:

x = 1:10000
y = cumsum(x)[5:5:end]

Здесь функция cumsum вычисляет накопленную сумму для всех элементов массива, что позволяет избежать использования цикла.

Векторизация и логическое индексирование

Julia поддерживает векторизацию логических операций. Например, если у нас есть массивы с размерами конусов, и некоторые значения диаметра оказались отрицательными, с помощью векторизации можно легко определить корректные значения.

Для этого сначала создайте массивы D и H и рассчитайте объемы конусов:

# Определяем массивы D и H
D = [-0.2, 1.0, 1.5, 3.0, -1.0, 4.2, 3.14]
H = [1.0, 2.0, 2.5, 3.5, 1.5, 4.5, 3.0]

# Рассчитываем объемы
V = (1 / 12) * π * D.^2 .* H

Результатом будет массив V с рассчитанными объемами, в котором можно применить логическую индексацию (см. ниже), чтобы отфильтровать только корректные значения. Логическая операция создаст массив true и false, где true соответствует положительным значениям:

valid_D = D .>= 0
Vgood = V[valid_D]

Теперь переменная Vgood содержит объемы только для тех конусов, у которых диаметр положителен:

engee> 5-element Vector{Float64}:
  0.5235987755982988
  1.4726215563702154
  8.246680715673207
 20.78163540349648
  7.743711731833481

В Julia логическую индексацию можно комбинировать с векторизацией, например:

# Исходные данные (массив температур)
temps = [15.2, -3.0, 22.5, 0.0, 25.1, -10.5, 30.0]

# Условие - отбор только положительных значений
positive_temps = temps[temps .> 0]

println(positive_temps)

Логическая индексация позволяет применять не только простые, но и составные условия. Например:

# Исходные данные (массив лет)
ages = [25, 17, 34, 45, 15, 27, 18]

# Условие - отбор совершеннолетних (>= 18), но моложе 30
valid_ages = ages[(ages .>= 18) .& (ages .< 30)]

println(valid_ages)

Операции с матрицами

Часто векторизация помогает создавать матрицы нужного размера и структуры. Например, если нужно создать матрицу 5x5, в которой все элементы равны 10, то можно воспользоваться функцией fill:

A = fill(10, 5, 5)

Код выше выведет результат:

5×5 Matrix{Int64}:
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10
 10  10  10  10  10

С помощью векторизации можно складывать матрицы разного размера, если они совместимы для операции broadcast. Например, если матрица A — это матрица 3 × 3, а B — это вектор длиной 3:

A = [1 2 3; 4 5 6; 7 8 9]
B = [1, 2, 3]
C = A .+ B

Это создаст новую матрицу C, где каждый элемент вектора B добавляется к соответствующему столбцу матрицы A. Результатом будет: C = [2 3 4; 6 7 8; 10 11 12].


Рассмотрим умножение в Julia. Существует два типа операций умножения:

  • * — стандартное матричное умножение. * выполняет матричное умножение, соответствующее линейной алгебре. Каждый элемент в результирующей матрице получается через вычисление суммы произведений элементов строки первой матрицы на элементы столбца второй матрицы;

  • .* — поэлементное умножение. .* выполняет поэлементное умножение, где каждый элемент первой матрицы умножается на соответствующий элемент второй матрицы.

Например:

# Определение двух матриц
A = [1 2; 3 4]  # Матрица 2x2
B = [2 0; 1 3]  # Матрица 2x2

# Матричное умножение
C = A * B

# Поэлементное умножение
D = A .* B

Результаты:

  • Для C: C = [4 6; 10 12].

  • Для D: D = [2 0; 3 12]

Если же матрица умножается на скаляр, то оба оператора (* и .*) работают одинаково.