NumPy Einsum для научных вычислений

NumPyNumPyBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В научных вычислениях выполнение эффективных операций линейной алгебры является важным. NumPy, фундаментальная библиотека Python для численных вычислений, предоставляет множество инструментов для таких операций. Среди этих инструментов функция einsum (суммирование по Эйнштейну) выделяется как особенно мощный способ кратко выражать сложные операции с массивами.

В этом руководстве вы узнаете, что такое einsum и как эффективно использовать его в своем Python - коде. По завершении этого практического занятия (LabEx) вы сможете выполнять различные операции с массивами с помощью функции einsum и понять ее преимущества по сравнению с традиционными функциями NumPy.

Основы работы с функцией einsum в NumPy

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

Запуск Python - оболочки

Начнем с запуска Python - оболочки. Откройте терминал на рабочем столе и введите:

python3

Вы должны увидеть приглашение Python (>>>), что означает, что вы теперь в интерактивной оболочке Python.

Импорт библиотеки NumPy

Сначала нам нужно импортировать библиотеку NumPy:

import numpy as np
Open Python Shell

Что такое einsum?

Функция einsum в NumPy позволяет задавать операции с массивами с помощью строковой нотации, которая описывает, какие индексы (размерности) массивов должны быть использованы в операции.

Базовый формат операции einsum выглядит так:

np.einsum('notation', array1, array2, ...)

Где строка notation описывает операцию, которую нужно выполнить.

Простой пример: Скалярное произведение векторов

Начнем с простого примера: вычисление скалярного произведения двух векторов. В математической нотации скалярное произведение двух векторов u и v записывается как:

\sum_i u_i \times v_i

Вот как можно вычислить его с помощью einsum:

## Create two random vectors
u = np.random.rand(5)
v = np.random.rand(5)

## Print the vectors to see their values
print("Vector u:", u)
print("Vector v:", v)

## Calculate dot product using einsum
dot_product = np.einsum('i,i->', u, v)
print("Dot product using einsum:", dot_product)

## Verify with NumPy's dot function
numpy_dot = np.dot(u, v)
print("Dot product using np.dot:", numpy_dot)
Dot Product

Нотация 'i,i->' означает:

  • i представляет индекс первого массива (u)
  • Второй i представляет индекс второго массива (v)
  • Стрелка -> без последующих символов означает, что мы хотим получить скалярный результат (сумму по всем индексам)

Понимание нотации einsum

Нотация einsum следует общему шаблону:

'index1,index2,...->output_indices'
  • index1, index2: Метки для размерностей каждого входного массива
  • output_indices: Метки для размерностей выходного массива
  • Повторяющиеся индексы в входных массивах суммируются
  • Индексы, которые появляются в выходе, сохраняются в результате

Например, в нотации 'ij,jk->ik':

  • i, j — размерности первого массива
  • j, k — размерности второго массива
  • j появляется в обоих входных массивах, поэтому мы суммируем по этой размерности
  • i, k появляются в выходе, поэтому эти размерности сохраняются

Это именно формула для умножения матриц!

Часто используемые операции с einsum

Теперь, когда мы понимаем основы работы с функцией einsum, давайте рассмотрим некоторые распространенные операции, которые можно выполнять с помощью этой мощной функции.

Транспонирование матрицы

Транспонирование матрицы означает обмен ее строк и столбцов. В математической нотации, если A - это матрица, то ее транспонированная матрица A^T определяется как:

A^T_{ij} = A_{ji}

Давайте посмотрим, как выполнить транспонирование матрицы с помощью einsum:

## Create a random matrix
A = np.random.rand(3, 4)
print("Original matrix A:")
print(A)
print("Shape of A:", A.shape)  ## Should be (3, 4)

## Transpose using einsum
A_transpose = np.einsum('ij->ji', A)
print("\nTransposed matrix using einsum:")
print(A_transpose)
print("Shape of transposed A:", A_transpose.shape)  ## Should be (4, 3)

## Verify with NumPy's transpose function
numpy_transpose = A.T
print("\nTransposed matrix using A.T:")
print(numpy_transpose)

Нотация 'ij->ji' означает:

  • ij представляет индексы входной матрицы (i - для строк, j - для столбцов)
  • ji представляет индексы выходной матрицы (j - для строк, i - для столбцов)
  • По сути, мы меняем местами позиции индексов

Умножение матриц

Умножение матриц является фундаментальной операцией в линейной алгебре. Для двух матриц A и B их произведение C определяется как:

C_{ik} = \sum_j A_{ij} \times B_{jk}

Вот как выполнить умножение матриц с помощью einsum:

## Create two random matrices
A = np.random.rand(3, 4)  ## 3x4 matrix
B = np.random.rand(4, 2)  ## 4x2 matrix

print("Matrix A shape:", A.shape)
print("Matrix B shape:", B.shape)

## Matrix multiplication using einsum
C = np.einsum('ij,jk->ik', A, B)
print("\nResult matrix C using einsum:")
print(C)
print("Shape of C:", C.shape)  ## Should be (3, 2)

## Verify with NumPy's matmul function
numpy_matmul = np.matmul(A, B)
print("\nResult matrix using np.matmul:")
print(numpy_matmul)

Нотация 'ij,jk->ik' означает:

  • ij представляет индексы матрицы A (i - для строк, j - для столбцов)
  • jk представляет индексы матрицы B (j - для строк, k - для столбцов)
  • ik представляет индексы выходной матрицы C (i - для строк, k - для столбцов)
  • Повторяющийся индекс j суммируется (умножение матриц)

Поэлементное умножение

Поэлементное умножение заключается в умножении соответствующих элементов двух массивов. Для двух матриц A и B одинаковой формы их поэлементное произведение C равно:

C_{ij} = A_{ij} \times B_{ij}

Вот как выполнить поэлементное умножение с помощью einsum:

## Create two random matrices of the same shape
A = np.random.rand(3, 3)
B = np.random.rand(3, 3)

print("Matrix A:")
print(A)
print("\nMatrix B:")
print(B)

## Element-wise multiplication using einsum
C = np.einsum('ij,ij->ij', A, B)
print("\nElement-wise product using einsum:")
print(C)

## Verify with NumPy's multiply function
numpy_multiply = A * B
print("\nElement-wise product using A * B:")
print(numpy_multiply)

Нотация 'ij,ij->ij' означает:

  • ij представляет индексы матрицы A
  • ij представляет индексы матрицы B
  • ij представляет индексы выходной матрицы C
  • Ни один из индексов не суммируется, что означает, что мы просто умножаем соответствующие элементы

Продвинутые операции с einsum

Теперь, когда мы освоили базовые операции с функцией einsum, давайте рассмотрим более продвинутые применения. Эти операции демонстрируют настоящую мощь и гибкость функции einsum.

Извлечение диагонали

Извлечение диагональных элементов матрицы - это распространенная операция в линейной алгебре. Для матрицы A ее диагональные элементы образуют вектор d, где:

d_i = A_{ii}

Вот как извлечь диагональ с помощью einsum:

## Create a random square matrix
A = np.random.rand(4, 4)
print("Matrix A:")
print(A)

## Extract diagonal using einsum
diagonal = np.einsum('ii->i', A)
print("\nDiagonal elements using einsum:")
print(diagonal)

## Verify with NumPy's diagonal function
numpy_diagonal = np.diagonal(A)
print("\nDiagonal elements using np.diagonal():")
print(numpy_diagonal)

Нотация 'ii->i' означает:

  • ii представляет повторяющийся индекс для диагональных элементов A
  • i означает, что мы извлекаем эти элементы в одномерный массив

След матрицы

След матрицы - это сумма ее диагональных элементов. Для матрицы A ее след равен:

\text{trace}(A) = \sum_i A_{ii}

Вот как вычислить след с помощью einsum:

## Using the same matrix A from above
trace = np.einsum('ii->', A)
print("Trace of matrix A using einsum:", trace)

## Verify with NumPy's trace function
numpy_trace = np.trace(A)
print("Trace of matrix A using np.trace():", numpy_trace)

Нотация 'ii->' означает:

  • ii представляет повторяющийся индекс для диагональных элементов
  • Пустой выходной индекс означает, что мы суммируем все диагональные элементы, чтобы получить скаляр

Пакетное умножение матриц

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

Если у нас есть пакет матриц A с формой (n, m, p) и пакет матриц B с формой (n, p, q), то пакетное умножение матриц дает нам результат C с формой (n, m, q):

C_{ijk} = \sum_l A_{ijl} \times B_{ilk}

Вот как выполнить пакетное умножение матриц с помощью einsum:

## Create batches of matrices
n, m, p, q = 5, 3, 4, 2  ## Batch size and matrix dimensions
A = np.random.rand(n, m, p)  ## Batch of 5 matrices, each 3x4
B = np.random.rand(n, p, q)  ## Batch of 5 matrices, each 4x2

print("Shape of batch A:", A.shape)
print("Shape of batch B:", B.shape)

## Batch matrix multiplication using einsum
C = np.einsum('nmp,npq->nmq', A, B)
print("\nShape of result batch C:", C.shape)  ## Should be (5, 3, 2)

## Let's check the first matrix multiplication in the batch
print("\nFirst result matrix from batch using einsum:")
print(C[0])

## Verify with NumPy's matmul function
numpy_batch_matmul = np.matmul(A, B)
print("\nFirst result matrix from batch using np.matmul:")
print(numpy_batch_matmul[0])

Нотация 'nmp,npq->nmq' означает:

  • nmp представляет индексы пакета A (n - для пакета, m - для строк, p - для столбцов)
  • npq представляет индексы пакета B (n - для пакета, p - для строк, q - для столбцов)
  • nmq представляет индексы выходного пакета C (n - для пакета, m - для строк, q - для столбцов)
  • Повторяющийся индекс p суммируется (умножение матриц)

Почему использовать einsum?

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

  1. Единый интерфейс: einsum предоставляет одну функцию для многих операций с массивами.
  2. Гибкость: Она может выражать операции, которые в противном случае потребовали бы нескольких шагов.
  3. Читаемость: Как только вы освоите нотацию, код станет более компактным.
  4. Производительность: Во многих случаях операции с einsum оптимизированы и эффективны.

Для сложных операций с тензорами einsum часто предоставляет наиболее ясную и прямую реализацию.

Резюме

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

  1. Основные концепции einsum: Вы научились использовать нотацию einsum для описания операций с массивами, где индексы представляют размерности массива, а повторяющиеся индексы обозначают суммирование.

  2. Распространенные операции: Вы реализовали несколько фундаментальных операций с использованием einsum:

    • Скалярное произведение векторов
    • Транспонирование матрицы
    • Умножение матриц
    • Поэлементное умножение
  3. Продвинутые применения: Вы изучили более сложные операции:

    • Извлечение диагонали
    • След матрицы
    • Пакетное умножение матриц

Функция einsum предоставляет единый и гибкий подход к операциям с массивами в NumPy. Хотя для конкретных операций доступны специальные функции, такие как np.dot, np.matmul и np.transpose, einsum предлагает единый интерфейс для широкого спектра операций, что особенно ценно при работе с многомерными массивами.

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