Транспонирование, swapaxes и смысл осей в N измерениях

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

Транспонирование — перестановка осей массива; в 2D это привычный «поворот» матрицы (строки становятся столбцами), в N измерениях — любая перестановка осей.

Что вообще значит «ось» в N-мерных данных

Прежде чем переставлять оси, стоит твёрдо понять, что они означают. Ось — это направление, вдоль которого меняется один из индексов. У вектора одна ось (одно направление). У матрицы две: ось 0 («вниз» по строкам) и ось 1 («вправо» по столбцам). У трёхмерного массива три, и тут наглядность теряется — представить «куб» из чисел сложнее. Поэтому полезно думать об осях не геометрически, а семантически: каждая ось отвечает за одну «категорию», по которой данные различаются. Для набора фотографий это может быть «какая фотография», «какая строка пикселей», «какой столбец», «какой цветовой канал» — четыре оси, четыре независимых способа адресовать конкретное число (яркость). Перестановка осей не меняет данные, она меняет порядок этих категорий в описании массива. Когда вы держите в голове смысл каждой оси, операции transpose, swapaxes и выбор axis в агрегациях перестают быть угадыванием и становятся осмысленными. Это, пожалуй, главный навык работы с многомерными данными — и он важнее, чем знание конкретных функций.

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

Для двумерного массива атрибут .T меняет строки и столбцы местами: элемент (i, j) становится элементом (j, i). Форма (m, n) превращается в (n, m). Как мы выяснили в уроке про strides, это бесплатная операция: NumPy просто переставляет элементы кортежа strides, не копируя данные — результат это view.

import numpy as np
a = np.array([[1, 2, 3],
              [4, 5, 6]])     # (2, 3)

print(a.T)            # (3, 2)
print(a.T.shape)
print(a.T.strides)    # strides переставлены, данные те же
print(np.shares_memory(a, a.T))  # True — это view

Вывод:

[[1 4]
 [2 5]
 [3 6]]
(3, 2)
(8, 24)
True

Логику транспонирования легко увидеть на чистом Python — это просто обмен ролей индексов:

a = [[1, 2, 3],
     [4, 5, 6]]
rows, cols = 2, 3

# Транспонированная: t[j][i] = a[i][j]
t = [[a[i][j] for i in range(rows)] for j in range(cols)]
for row in t:
    print(row)

Вывод:

[1, 4]
[2, 5]
[3, 6]

Зачем нужно транспонирование

Транспонирование постоянно встречается в линейной алгебре: для умножения матриц нужно согласовать размеры, и часто один из операндов транспонируют. Например, чтобы получить матрицу попарных скалярных произведений строк X, считают X @ X.T. Также транспонирование меняет «ориентацию» данных: превратить таблицу «объекты × признаки» в «признаки × объекты».

Ещё транспонирование незаменимо для согласования форм при broadcasting. Иногда два массива «почти совместимы», но их оси расположены в зеркальном порядке; транспонировав один из них, вы выравниваете оси так, чтобы операция стала возможной и осмысленной. А в статистике и анализе данных транспонирование — это переключение между «взглядом по наблюдениям» и «взглядом по переменным» на одну и ту же таблицу. Поскольку оно бесплатно, его не жалко применять для удобства: если вашему вычислению удобнее, чтобы признаки шли по строкам, транспонируйте — это не потратит ни памяти, ни времени.

Транспонирование меняет логику, но не данные

Стоит подчеркнуть концептуальную сторону. После a.T вы видите «перевёрнутую» матрицу, но в памяти ничего не перевернулось — те же байты в том же порядке. Изменилась лишь инструкция чтения: NumPy теперь идёт по буферу с другими шагами. Это значит, что транспонирование — операция мысли, а не вычисления: вы переобъявляете, какая ось считается строками, а какая столбцами. Отсюда два следствия. Первое — оно мгновенно для массивов любого размера. Второе — результат разделяет память с оригиналом (это view), поэтому запись в транспонированный массив меняет исходный. Если нужна независимая транспонированная копия (например, чтобы передать данные во внешнюю библиотеку, требующую непрерывности), берите a.T.copy() — вот тогда данные действительно переразложатся в новом порядке.

Многомерное транспонирование: любая перестановка осей

Для массивов с числом осей больше двух .T разворачивает порядок осей целиком: (a, b, c) → (c, b, a). На практике полная инверсия в N измерениях нужна редко и часто даёт не то, что хотелось, поэтому для многомерных массивов опытные пользователи почти всегда задают перестановку явно. Но часто нужна не полная инверсия, а конкретная перестановка. Тогда применяют transpose с явным порядком осей или специализированные swapaxes и moveaxis.

import numpy as np
t = np.arange(24).reshape(2, 3, 4)    # оси: (0, 1, 2)

print(t.T.shape)                  # (4, 3, 2) — полная инверсия
print(t.transpose(1, 0, 2).shape) # (3, 2, 4) — поменять оси 0 и 1
print(t.swapaxes(0, 2).shape)     # (4, 3, 2) — обменять оси 0 и 2
print(np.moveaxis(t, 0, -1).shape) # (3, 4, 2) — ось 0 в конец

Вывод:

(4, 3, 2)
(3, 2, 4)
(4, 3, 2)
(3, 4, 2)

Разберём инструменты:

  • transpose(p0, p1, ...) задаёт новый порядок осей перечислением: «новая ось 0 — это старая p0» и т. д. Самый общий способ.
  • swapaxes(i, j) меняет местами ровно две оси — удобно и читаемо, когда нужна именно пара.
  • moveaxis(a, src, dst) перемещает ось из позиции src в dst, сдвигая остальные. Идеален для «переставь канал в конец».

Смысл осей: практический пример с изображениями

В реальных данных оси имеют смысл. Классика — изображения. Разные библиотеки хранят цветные картинки в разном порядке осей: «высота × ширина × каналы» (H, W, C) или «каналы × высота × ширина» (C, H, W). Чтобы передать массив из одной библиотеки в другую, оси переставляют. Это типичнейшее применение moveaxis/transpose:

import numpy as np
# Картинка 100x200 в формате (H, W, C): 3 канала
img_hwc = np.zeros((100, 200, 3))

# Перевести в (C, H, W) — нужно многим нейросетевым фреймворкам
img_chw = np.moveaxis(img_hwc, -1, 0)
print(img_hwc.shape, "->", img_chw.shape)

Вывод:

(100, 200, 3) -> (3, 100, 200)

Здесь важно понимать: данные те же пиксели, меняется лишь то, какая ось «главная». Понимание смысла осей — половина успеха в работе с многомерными данными.

transpose, swapaxes, moveaxis: когда что

Три инструмента перестановки осей решают похожие задачи, но удобны в разных ситуациях. transpose(p0, p1, ...) — самый общий: вы задаёте полный новый порядок всех осей. Он мощен, но требует перечислить все оси, и в коде с многими осями легко ошибиться в перечислении. swapaxes(i, j) — для самого частого случая, когда нужно поменять местами ровно две оси; читается яснее, чем полный transpose, и труднее ошибиться. moveaxis(a, src, dst) — когда нужно «вытащить» одну ось и поставить её в другое место, а остальные пусть сдвинутся как есть; это идеальный инструмент для операций вроде «перенести ось каналов в конец» или «поставить ось батча первой», где вы думаете в терминах одной перемещаемой оси, а не полной перестановки. Совет: берите самый специфичный инструмент под задачу — swapaxes для пары, moveaxis для переноса одной оси, и лишь когда нужна действительно произвольная перестановка — transpose с полным списком. Чем специфичнее инструмент, тем понятнее намерение и меньше шанс перепутать оси.

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

Поскольку транспонирование лишь переставляет strides, результат почти всегда не C-непрерывен. Для большинства операций это незаметно, но некоторые функции (особенно те, что отдают данные во внешние библиотеки) работают быстрее с непрерывными массивами и могут втихую сделать копию через np.ascontiguousarray. Если вы видите неожиданное копирование после транспонирования — вот причина.

import numpy as np
a = np.arange(6).reshape(2, 3)

print(a.flags['C_CONTIGUOUS'])     # True
print(a.T.flags['C_CONTIGUOUS'])   # False — транспонированный
print(np.ascontiguousarray(a.T).flags['C_CONTIGUOUS'])  # True (копия)

Вывод:

True
False
True

Подводные камни

  • .T для 1D ничего не делает. У вектора (n,) одна ось — транспонировать нечего. Чтобы сделать столбец, нужен np.newaxis, а не .T.
  • Путать transpose и swapaxes. transpose задаёт полный новый порядок, swapaxes меняет ровно две оси.
  • Забыть про непрерывность. Транспонированный массив не C-непрерывен; возможна скрытая копия в некоторых функциях.
  • Менять данные через .T. Это view — запись затронет оригинал.

Оси как смысл: почему перестановка — частая операция

В реальных задачах перестановка осей нужна постоянно, потому что разные инструменты и алгоритмы ожидают данные с разным порядком осей. Один фреймворк глубокого обучения хочет изображения в формате (батч, канал, высота, ширина), другой — (батч, высота, ширина, канал). Библиотека визуализации ждёт (высота, ширина, канал). Ваш собственный код может удобнее обрабатывать данные, поставив «ось признаков» первой. Каждый раз, когда данные переходят между такими контекстами, оси переставляют. И поскольку перестановка через transpose/moveaxis бесплатна (это view), её не страшно применять часто. Главное — точно понимать, какая ось что означает в каждом формате, иначе легко перепутать и получить мусор, который не вызовет ошибки (формы-то совместимы), но даст бессмысленный результат. Поэтому при перестановке осей всегда держите в голове их семантику и проверяйте форму до и после. Хорошая привычка — комментировать такие операции: «(H,W,C) -> (C,H,W) для подачи в модель».

Лучшие практики

  • Для 2D пользуйтесь коротким .T; для конкретной перестановки осей — transpose с явным порядком.
  • moveaxis — самый читаемый способ «перенести ось туда-то» (каналы, время, батч).
  • Чтобы сделать из вектора столбец, добавляйте ось через [:, None], а не транспонируйте.
  • Если внешняя функция требует непрерывности, явно вызывайте np.ascontiguousarray.

Перестановка осей — это, по сути, навык «смотреть на одни и те же данные под разным углом» без затрат. Освоив его вместе с пониманием смысла каждой оси, вы сможете уверенно работать с многомерными массивами: согласовывать форматы между библиотеками, готовить данные к broadcasting и агрегациям, и не теряться в кубах и тензорах. Это умение прямо переносится на работу с изображениями, тензорами нейросетей и многомерными таблицами.

Итог

  • Транспонирование переставляет оси; для 2D .T меняет строки и столбцы, для N измерений разворачивает порядок осей.
  • Это бесплатный view: переставляются strides, данные не копируются.
  • transpose, swapaxes, moveaxis дают точный контроль над порядком осей.
  • Транспонированный массив не C-непрерывен, что иногда вызывает скрытое копирование.
Проверьте себя
1. Почему транспонирование матрицы через .T в NumPy выполняется без копирования данных?
AПотому что NumPy заранее хранит обе версии матрицы
BПотому что транспонирование лишь переставляет элементы кортежа strides, возвращая view на те же данные
CПотому что данные копируются, но очень быстрым алгоритмом
DПотому что .T работает только с маленькими матрицами
2. Что нужно сделать, чтобы превратить вектор формы (n,) в столбец (n, 1)?
AИспользовать .T — транспонирование
BДобавить ось через v[:, np.newaxis] или reshape, потому что у 1D-вектора .T ничего не меняет
CПрименить v.flatten()
DПрименить np.swapaxes(v, 0, 1)
3. Что делает np.moveaxis(img, -1, 0) для массива img формы (100, 200, 3)?
AУдаляет последнюю ось
BПеремещает последнюю ось (каналы) в начало, давая форму (3, 100, 200)
CТранспонирует только первые две оси
DМеняет тип данных массива
Поддержать проект