在机器学习中大量的使用NumPy
作为其基础的数据结构,ndarray
是NumPy
的核心数据对象。对于ndarray
高维数组的一个非常容易产生的误解是,使用数学中的矩阵(或者叫“行列式”)概念去尝试理解更高维的场景,或者使用更高维空间去理解,这样都会导致难以较好的理解更高维(5或6维)的数组。本文使用较为直观的示例和可视化的展示,更为“标准”(文档推荐的)的方式去理解ndarray
的更高维数组。更多详细内容,可以参考阅读:
问题
在机器学习中,经常要对多维的数组做各种操作。对高维数组建立更好的直觉理解,则有利于去理解这些操作。例如,我们考虑右侧的代码,想一想该代码的输出是什么?
>>> import numpy as np
>>> np.array([[[1],[2]],[[3],[4]]]).shape
(考虑输出是什么)
要回答这个问题,则需要建立对于多维数组结构的理解。
文档中对于高维数组理解的建议
在NumPy: the absolute basics for beginners
中有如下一段话:
It is familiar practice in mathematics to refer to elements of a matrix by the row index first and the column index second. This happens to be true for two-dimensional arrays, but a better mental model is to think of the column index as coming last and the row index as second to last. This generalizes to arrays with any number of dimensions.
“矩阵”是“线性代数”的主要研究对象,一个\( m \times n \)的矩阵即是一个平面上的\( m \)行\( m \)列的行列式。一种常见的向高维扩展的思考方式是,会将三维数组扩展为三维空间中的数组。但,这样的扩展,非常不利于去理解更高维的数组。这里提到的方案是这样:“a better mental model is to think of the column index as coming last and the row index as second to last.”。
这种理解,也是本文的核心,概况如下:
- 总是将最后一个维度理解为列维度
- 总是将倒数第二个维度理解为行维度
- 剩余的维度,则是通过层的方式去构建
从 4×5 的数组开始
先通过直观的书写表达,看看这样的数组应该是怎样的。
一般的矩阵(行列式表示):
\begin{bmatrix}
0 & 1 & 2 & 3 & 4 \\
5 & 6 & 7 & 8 & 9 \\
10 & 11 & 12 & 13 & 14 \\
15 & 16 & 17 & 18 & 19 \\
\end{bmatrix}
本文推荐的理解方式:
\[
\begin{bmatrix}
[0 & 1 & 2 & 3 & 4] \\
[5 & 6 & 7 & 8 & 9] \\
[10 & 11 & 12 & 13 & 14] \\
[15 & 16 & 17 & 18 & 19]
\end{bmatrix}
\]
numpy的输出:
>>> np.arange(20).reshape(4,5)
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
如果按照“矩阵”思想去理解这个矩阵很简单。但这里,我们重新按照上述的原则去理解这个数组。即:
- 最后一个维度(即第二个维度),该维度的长度是5,将其理解为列维度
- 倒数第二个维度,即第一个维度,该维度的长度是4,将其理解为行维度
形式上,这与一般的矩阵,是完全一致的。只是,思维方式,反过来了。
再考虑 3x4x5 的数组
这个数组已经不能用简单的平面表示了,这里使用了符合上述描述的形式描述,“剩余的维度,则是通过层的方式去构建”,则有:
本文推荐的理解方式:
\[
\begin{array}{r c}
\text{Layer 1:} &
\left[
\begin{array}{c}
[0 & 1 & 2 & 3 & 4] \\
[5 & 6 & 7 & 8 & 9] \\
[10 & 11 & 12 & 13 & 14] \\
[15 & 16 & 17 & 18 & 19] \\
\end{array}
\right]
\\
\text{Layer 2:} &
\left[
\begin{array}{c}
[20 & 21 & 22 & 23 & 24] \\
[25 & 26 & 27 & 28 & 29] \\
[30 & 31 & 32 & 33 & 34] \\
[35 & 36 & 37 & 38 & 39] \\
\end{array}
\right]
\\
\text{Layer 3:} &
\left[
\begin{array}{c}
[40 & 41 & 42 & 43 & 44] \\
[45 & 46 & 47 & 48 & 49] \\
[50 & 51 & 52 & 53 & 54] \\
[55 & 56 & 57 & 58 & 59] \\
\end{array}
\right]
\end{array}
\]
>>> np.arange(60).reshape(3,4,5)
array([[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]],
[[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29],
[30, 31, 32, 33, 34],
[35, 36, 37, 38, 39]],
[[40, 41, 42, 43, 44],
[45, 46, 47, 48, 49],
[50, 51, 52, 53, 54],
[55, 56, 57, 58, 59]]])
这时,矩阵的想法就不太好用了。这里继续按照上面的原则,考虑:
- 最后一个维度,即这里的第三个维度,该维度的长度是5,将其理解为列维度
- 倒数第二个维度,即这里第二个维度,该维度的长度是4,将其理解为行维度
- 倒数第三个维度(第一个维度)该维度长度是3,将其理解为行列式前面的层
循着这样的思考模式,不断地叠加更多的“层”,就可以理解更高维度的数组了。
考虑 2x3x4x5 的数组
这里先试用“层”的思维,可视化的表示该数组如下:
\[
\left[
\begin{array}{c}
\text{Layer}^{(0)}_1
\left[
\begin{array}{c}
\text{Layer}^{(1)}_1
\left[
\begin{array}{c}
[000 & 001 & 002 & 003 & 004] \\
[005 & 006 & 007 & 008 & 009] \\
[010 & 011 & 012 & 013 & 014] \\
[015 & 016 & 017 & 018 & 019] \\
\end{array}
\right] \\
\text{Layer}^{(1)}_2
\left[
\begin{array}{c}
[020 & 021 & 022 & 023 & 024] \\
[025 & 026 & 027 & 028 & 029] \\
[030 & 031 & 032 & 033 & 034] \\
[035 & 036 & 037 & 038 & 039] \\
\end{array}
\right] \\
\text{Layer}^{(1)}_3
\left[
\begin{array}{c}
[040 & 041 & 042 & 043 & 044] \\
[045 & 046 & 047 & 048 & 049] \\
[050 & 051 & 052 & 053 & 054] \\
[055 & 056 & 057 & 058 & 059] \\
\end{array}
\right]
\end{array}
\right] \\
\text{Layer}^{(0)}_2
\left[
\begin{array}{c}
\text{Layer}^{(1)}_1
\left[
\begin{array}{c}
[060 & 061 & 062 & 063 & 064] \\
[065 & 066 & 067 & 068 & 069] \\
[070 & 071 & 072 & 073 & 074] \\
[075 & 076 & 077 & 078 & 079] \\
\end{array}
\right] \\
\text{Layer}^{(1)}_2
\left[
\begin{array}{c}
[080 & 081 & 082 & 083 & 084] \\
[085 & 086 & 087 & 088 & 089] \\
[090 & 091 & 092 & 093 & 094] \\
[095 & 096 & 097 & 098 & 099] \\
\end{array}
\right] \\
\text{Layer}^{(1)}_3
\left[
\begin{array}{c}
[100 & 101 & 102 & 103 & 104] \\
[105 & 106 & 107 & 108 & 109] \\
[110 & 111 & 112 & 113 & 114] \\
[115 & 116 & 117 & 118 & 119] \\
\end{array}
\right]
\end{array}
\right]
\end{array}
\right]
\]
>>> np.arange(120).reshape(2,3,4,5)
array([[[[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[ 10, 11, 12, 13, 14],
[ 15, 16, 17, 18, 19]],
[[ 20, 21, 22, 23, 24],
[ 25, 26, 27, 28, 29],
[ 30, 31, 32, 33, 34],
[ 35, 36, 37, 38, 39]],
[[ 40, 41, 42, 43, 44],
[ 45, 46, 47, 48, 49],
[ 50, 51, 52, 53, 54],
[ 55, 56, 57, 58, 59]]],
[[[ 60, 61, 62, 63, 64],
[ 65, 66, 67, 68, 69],
[ 70, 71, 72, 73, 74],
[ 75, 76, 77, 78, 79]],
[[ 80, 81, 82, 83, 84],
[ 85, 86, 87, 88, 89],
[ 90, 91, 92, 93, 94],
[ 95, 96, 97, 98, 99]],
[[100, 101, 102, 103, 104],
[105, 106, 107, 108, 109],
[110, 111, 112, 113, 114],
[115, 116, 117, 118, 119]]]])
继续按照上面的原则,考虑:
- 最后一个维度,即这里的第四个维度,该维度的长度是5,将其理解为列维度
- 倒数第二个维度,即这里第三个维度,该维度的长度是4,将其理解为行维度
- 倒数第三个维度,即第二个维度,该维度的长度是3,将其理解为行列式前面的层
- 倒数第四个维度,即第一个维度,该维度的长度是2,将其理解为行列式前面的层的层,也就是上述的“Layer”
使用这样的模式,就可以将一个多维数组的表示平面化。并且注意到,这与ndarray
输出的形式是几乎完全一致的。
回到前面的问题
有了上面的可视化展示以及上面逐步的介绍,应该可以更容易理解前面NumPy: the absolute basics for beginners
所提到的直觉“a better mental model is to think of the column index as coming last and the row index as second to last”。
有了这个直觉,我们再来考虑最前面提到的问题:
>>> import numpy as np
>>> np.array([[[1],[2]],[[3],[4]]]).shape
最内层的列,就是最后的维度长度,这里是 1,所以就是 ? x 1
;该列所对应的行数,就是倒数第二个维度的长度,这里做如下的格式化,可以看到有两行,所以这是一个? x 2 x 1
的数组;再向上看一层,共有两个该2x1
的数组,故,该数组的shape
时:2x2x1
。
[
[
[1],
[2]
],
[
[3],
[4]
]
]
再确认最后的输出:
>>> import numpy as np
>>> np.array([[[1],[2]],[[3],[4]]]).shape
(2, 2, 1)
最后
这种理解的核心即是“a better mental model is to think of the column index as coming last and the row index as second to last”,简单概括如下:
- 总是将最后一个维度理解为列维度
- 总是将倒数第二个维度理解为行维度
- 剩余的维度,则是通过层的方式去构建
Leave a Reply