Состояние устаревания класса матрицы NumPy - программирование
Подтвердить что ты не робот

Состояние устаревания класса матрицы NumPy

Каково состояние класса matrix в NumPy?

Мне постоянно говорят, что вместо этого я должен использовать класс ndarray. Стоит ли/безопасно использовать matrix класс в новом коде, который я пишу? Я не понимаю, почему я должен использовать ndarray вместо этого.

4b9b3361

Ответ 1

ТЛ; dr: класс numpy.matrix устарел. Существуют некоторые высококлассные библиотеки, которые зависят от класса как зависимости (самой большой из которых является scipy.sparse), которая препятствует надлежащему краткосрочному устареванию класса, но пользователям настоятельно рекомендуется использовать класс ndarray (обычно создаваемый с использованием удобная функция numpy.array). С введением оператора @ для умножения матриц многие относительные преимущества матриц были удалены.

Почему (не) класс матрицы?

numpy.matrix является подклассом numpy.ndarray. Первоначально он был предназначен для удобного использования в вычислениях с использованием линейной алгебры, но есть и ограничения, и удивительные различия в их поведении по сравнению с экземплярами более общего класса массивов. Примеры фундаментальных различий в поведении:

  • Формы: массивы могут иметь произвольное количество измерений в диапазоне от 0 до бесконечности (или 32). Матрицы всегда двумерные. Как ни странно, хотя матрица не может быть создана с большим количеством измерений, можно ввести одноэлементные измерения в матрицу, чтобы получить технически многомерную матрицу: np.matrix(np.random.rand(2,3))[None,...,None].shape == (1,2,3,1) (не то, чтобы это имело какое-либо практическое значение).
  • Индексирование: индексирование массивов может дать вам массивы любого размера, в зависимости от того, как вы их индексируете. Индексирование выражений на матрицах всегда даст вам матрицу. Это означает, что как arr[:,0] и arr[0,:] для 2d-массива дают вам 1d- ndarray, в то время как mat[:,0] имеет форму (N,1) а mat[0,:] имеет форму (1,M) в случае matrix.
  • Арифметические операции: основная причина использования матриц в старые времена заключалась в том, что арифметические операции (в частности, умножение и вычисление степени) над матрицами выполняют операции над матрицами (умножение матрицы и мощность матрицы). То же самое для массивов приводит к поэлементному умножению и мощности. Следовательно, mat1 * mat2 действителен, если mat1.shape[1] == mat2.shape[0], но arr1 * arr2 действителен, если arr1.shape == arr2.shape (и, конечно, результат означает что-то совершенно другое). Также, что удивительно, mat1/mat2 выполняет поэлементное деление двух матриц. Это поведение, вероятно, унаследовано от ndarray но не имеет смысла для матриц, особенно в свете значения *.
  • Специальные атрибуты: матрицы имеют несколько удобных атрибутов в дополнение к тем, что имеют массивы: mat.A и mat.A1 являются представлениями массивов с тем же значением, что и np.array(mat) и np.array(mat).ravel() соответственно., mat.T и mat.H - транспонированная и сопряженная транспонированная (сопряженная) матрицы; arr.T - единственный такой атрибут, который существует для класса ndarray. Наконец, mat.I является обратной матрицей mat.

Достаточно просто написать код, который работает как для ndarrays, так и для матриц. Но когда есть шанс, что два класса должны взаимодействовать в коде, вещи начинают становиться трудными. В частности, большая часть кода могла бы работать естественным образом для подклассов ndarray, но matrix - это плохо себя ndarray подкласс, который может легко сломать код, который пытается полагаться на типизацию с использованием утилит. Рассмотрим следующий пример с использованием массивов и матриц формы (3,4):

import numpy as np

shape = (3, 4)
arr = np.arange(np.prod(shape)).reshape(shape) # ndarray
mat = np.matrix(arr) # same data in a matrix
print((arr + mat).shape)           # (3, 4), makes sense
print((arr[0,:] + mat[0,:]).shape) # (1, 4), makes sense
print((arr[:,0] + mat[:,0]).shape) # (3, 3), surprising

Добавление срезов двух объектов катастрофически отличается в зависимости от размера, вдоль которого мы нарезаем. Добавление как в матрицы, так и в массивы происходит поэлементно, когда фигуры одинаковы. Первые два случая в приведенном выше являются интуитивно понятными: мы добавляем два массива (матрицы), затем мы добавляем две строки из каждого. Последний случай действительно удивителен: мы, вероятно, намеревались добавить два столбца и получили матрицу. Причина, конечно, в том, что arr[:,0] имеет форму (3,) которая совместима с формой (1,3), но mat[:.0] имеет форму (3,1). Два транслируются вместе, чтобы сформировать (3,3).

Наконец, наибольшее преимущество класса матриц (то есть возможность кратко формулировать сложные выражения матриц, включающие много матричных произведений) было удалено, когда оператор @ matmul был введен в python 3.5, впервые реализованном в numpy 1.10. Сравните вычисление простой квадратичной формы:

v = np.random.rand(3); v_row = np.matrix(v)
arr = np.random.rand(3,3); mat = np.matrix(arr)

print(v.dot(arr.dot(v))) # pre-matmul style
# 0.713447037658556, yours will vary
print(v_row * mat * v_row.T) # pre-matmul matrix style
# [[0.71344704]]
print(v @ arr @ v) # matmul style
# 0.713447037658556

Глядя на вышесказанное, становится ясно, почему матричный класс был широко предпочтительным для работы с линейной алгеброй: оператор infix * сделал выражения гораздо менее многословными и намного проще для чтения. Однако мы получаем одинаковую читаемость с оператором @ использующим современный python и numpy. Кроме того, обратите внимание, что матричный случай дает нам матрицу формы (1,1) которая технически должна быть скалярной. Это также подразумевает, что мы не можем умножить вектор столбца на этот "скаляр": (v_row * mat * v_row.T) * v_row.T в приведенном выше примере вызывает ошибку, потому что матрицы с формами (1,1) и (3,1) не может быть умножено в этом порядке.

Для полноты картины следует отметить, что хотя оператор matmul исправляет наиболее распространенный сценарий, в котором ndarrays неоптимальны по сравнению с матрицами, все же есть несколько недостатков в элегантной обработке линейной алгебры с использованием ndarrays (хотя люди все еще склонны полагать, что в целом это предпочтительнее придерживаться последнего). Одним из таких примеров является матричная мощность: mat ** 3 - собственная третья матричная мощность матрицы (тогда как это поэлементный куб ndarray). К сожалению, numpy.linalg.matrix_power довольно многословен. Кроме того, умножение матрицы на месте прекрасно работает только для класса матрицы. Напротив, хотя и PEP 465, и грамматика python допускают @= как расширенное назначение с помощью matmul, это не реализовано для ndarrays с numpy 1.15.

История обесценивания

Учитывая вышеупомянутые сложности, связанные с классом matrix в течение длительного времени велись постоянные дискуссии о его возможном устаревании. Введение оператора @ infix, которое было огромной предпосылкой для этого процесса, произошло в сентябре 2015 года. К сожалению, преимущества класса матрицы в прежние времена означали, что его использование широко распространилось. Существуют библиотеки, которые зависят от класса матрицы (одним из наиболее важных зависимостей является scipy.sparse который использует семантику numpy.matrix и часто возвращает матрицы при уплотнении), поэтому их полное устаревание всегда было проблематичным.

Уже в нудистой ветке списка рассылки с 2009 года я нашел такие замечания, как

Numpy был разработан для вычислительных нужд общего назначения, а не для какой-либо отдельной области математики. nd-массивы очень полезны для многих вещей. Напротив, Matlab, например, изначально был разработан, чтобы быть простым интерфейсом к линейной алгебре. Лично, когда я использовал Matlab, я обнаружил, что это очень неловко - я обычно писал сотни строк кода, которые не имели никакого отношения к линейной алгебре, для каждых нескольких строк, которые фактически выполняли математическую математику. Поэтому я предпочитаю простой способ - строки кода в линейной алгебре длиннее и неудобнее, а в остальном гораздо лучше.

Класс Matrix является исключением из этого: он был написан для обеспечения естественного способа выражения линейной алгебры. Тем не менее, все становится немного сложнее, когда вы смешиваете матрицы и массивы, и даже если придерживаться матриц, есть путаница и ограничения - как вы выражаете строку по сравнению с вектором столбца? что вы получаете, когда перебираете матрицу? и т.п.

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

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

Следующее упоминание, которое я смог найти, - это 2014 год, который оказался очень плодотворным. Последующее обсуждение поднимает вопрос о том, как обращаться с обалденными подклассами в целом, и эта общая тема все еще находится на рассмотрении. Есть также сильная критика:

Что спровоцировало это обсуждение (на Github), так это то, что невозможно написать код типа утки, который работает правильно для:

  • ndarrays
  • матрицы
  • scipy.sparse разреженные матрицы

Семантика всех трех различна; scipy.sparse находится где-то между матрицами и ndarrays, некоторые вещи работают случайным образом, как матрицы, а другие нет.

С некоторой добавленной гиперболой можно сказать, что с точки зрения разработчика, np.matrix делает и уже совершил зло, просто существовав, путая неустановленные правила семантики ndarray в Python.

последовало много ценного обсуждения возможных вариантов будущего матриц. Даже без оператора @ в то время много думают об устаревании класса матрицы и о том, как это может повлиять на пользователей в нисходящем направлении. Насколько я могу судить, эта дискуссия непосредственно привела к появлению PEP 465, представляющего matmul.

В начале 2015 года:

На мой взгляд, "фиксированная" версия np.matrix не должна (1) быть подклассом np.ndarray и (2) существовать в сторонней библиотеке, а не в виде numpy.

Я не думаю, что действительно возможно исправить np.matrix в его текущем состоянии как подкласс ndarray, но даже класс с фиксированной матрицей на самом деле не принадлежит самой numpy, которая имеет слишком длинные циклы выпуска и гарантии совместимости для экспериментов - не говоря уже о том, что само существование класса матриц в numpy вводит новых пользователей в заблуждение.

Как только оператор @ был доступен в течение некоторого времени, снова началось обсуждение вопроса об устаревании, вновь подняв тему о связи устаревания матрицы и scipy.sparse.

В конце концов, первое действие по устареванию numpy.matrix было предпринято в конце ноября 2017 года. Что касается иждивенцев класса:

Как сообщество будет обрабатывать подклассы матрицы scipy.sparse? Это все еще в общем пользовании.

Они никуда не денутся в течение долгого времени (пока, по крайней мере, скудные ndarrays не материализуются). Следовательно, np.matrix нужно перемещать, а не удалять.

(источник) и

в то время как я хочу избавиться от np.matrix так же, как и все остальные, в ближайшее время это будет очень разрушительным.

  • Есть множество маленьких сценариев, написанных людьми, которые не знали лучше; мы хотим, чтобы они научились не использовать np.matrix, но ломать все их сценарии - болезненный способ сделать это

  • Существуют крупные проекты, такие как scikit-learn, которые просто не имеют альтернативы использованию np.matrix из-за scipy.sparse.

Поэтому я думаю, что путь вперед - это что-то вроде:

  • Сейчас или всякий раз, когда кто-то собирает пиар: выведите PendingDeprecationWarning в np.matrix.__ init__ (если это не снижает производительность для scikit-learn и друзей) и поместите большое окно с предупреждением в верхней части документации. Идея здесь состоит в том, чтобы на самом деле никого не нарушать, а начать выводить сообщение, что мы определенно не думаем, что кто-то должен использовать это, если у них есть какая-либо альтернатива.

  • После альтернативы scipy.sparse: наращивайте предупреждения, возможно, вплоть до FutureWarning, чтобы существующие скрипты не ломались, но они получали шумные предупреждения

  • В конце концов, если мы думаем, что это сократит расходы на обслуживание: разделите его на подпакет

(источник).

Статус кво

По состоянию на май 2018 года (numpy 1.15, соответствующий запрос на включение и принятие) матричная строка класса содержит следующее примечание:

Больше не рекомендуется использовать этот класс, даже для линейной алгебры. Вместо этого используйте обычные массивы. Класс может быть удален в будущем.

И в то же время PendingDeprecationWarning был добавлен в matrix.__new__. К сожалению, предупреждения об устаревании (почти всегда) по умолчанию отключены, поэтому большинство конечных пользователей numpy не увидят этот сильный намек.

Наконец, в "дорожной карте" от ноября 2018 года упоминается множество связанных тем, поскольку одна из "задач и возможностей [сообщества пользователей] будет вкладывать ресурсы":

Некоторые вещи внутри NumPy на самом деле не соответствуют сфере действия NumPy.

  • Бэкэнд-система для numpy.fft (так что, например, fft-mkl не нужно monkeypatch numpy)
  • Переписать маскированные массивы, чтобы они не были подклассом ndarray - может быть, в отдельном проекте?
  • MaskedArray как тип массива утки и/или
  • dtypes, которые поддерживают пропущенные значения
  • Напишите стратегию о том, как бороться с перекрытием между numpy и scipy для linalg и fft (и реализуйте это).
  • Устаревать np.matrix

Вполне вероятно, что это состояние будет сохраняться до тех пор, пока более крупные библиотеки/многие пользователи (и в частности scipy.sparse) полагаются на класс матрицы. Тем не менее, продолжается обсуждение перемещения scipy.sparse в зависимости от чего-то другого, например, pydata/sparse. Независимо от развития процесса устаревания пользователям следует использовать класс ndarray в новом коде и, по возможности, портировать старый код. В конце концов, матричный класс, вероятно, окажется в отдельном пакете, чтобы снять некоторые нагрузки, вызванные его существованием в его текущей форме.