多维

多维数组 属于 Julia

2 个练习

关于 多维数组

早在向量这一概念里,我们就提到过:“数组可以有任意大小(只受硬件内存的限制),也可以有任意多的维度。”

从那以后,为了保持简单,我们基本忽略了多维数组。如果你试着通读 Julia 参考文档、体会一下它完整的复杂度,就会更理解这个决定。

不过,高维数组在科学计算中_非常非常_重要,所以我们需要理解它们。

命名约定: 沿袭数百年的数学惯例,我们把一维数组叫作 Vectors,把二维数组叫作 Matrices。

本文档中的示例大多是矩阵。处理 3 维或更高维时,语法几乎完全相同,但输出(在二维屏幕上)难以阅读,也让人费解。

一个 eltype 为 T 的 N 维数组,其 type 是 Array{T, N}。(译注:原文如此。)

为了方便,并与数学命名保持一致,Julia 定义了一些类型别名:Vector{T} 是 Array{T, 1} 的别名,Matrix{T} 是 Array{T, 2} 的别名。

# 3-D array
julia> m3 = ones(2, 3, 4);

julia> typeof(m3)
Array{Float64, 3}

# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
 1
 2

julia> typeof(v) == Array{Int64, 1}
true

# Matrix
julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})

julia> typeof(m) == Array{Int64, 2}
true

构造数组

我们已经在方括号里放一个用逗号分隔的列表,创建过很多向量。分号也可以用作分隔符。

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> w = [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v == w
true

如果改用空格(或制表符)作分隔符,结果就不一样了。

julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

Julia 现在把它定义成一个 1×3 的矩阵(在其他场景里,我们会把它叫作 row vector)。

一般来说,空格把东西水平地连接起来,分号(或换行符)则把它们垂直地连接起来。

这里说“东西”是故意含糊的,因为 Julia 会尽量处理你给它的任何内容。

julia> [v 2v]
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> [v; 2v]
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

函数 hcat() 和 vcat() 也能做同样的事,更明确地表示这是水平拼接和垂直拼接。更高维的推广版本是 cat() 函数。

jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> vcat(v, 2v)
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

显式地输入矩阵时,按行优先顺序书写很方便,因为这符合人的直觉(对使用横向文字的文化来说,这样更容易看):

julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

不过要注意,Julia(和 Fortran、R 及 Matlab 一样,但 和 C/C++ 或 NumPy 不同)以列优先顺序存储 N 维数组,如果你遍历其中的元素,这会造成巨大的性能差异。 帮你的 CPU 缓存一把,它就会帮你!

# put these integers in 2 rows and 3 columns

julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
 1  3  5
 2  4  6

上面的例子取 1 到 6 这些整数,按列依次填入一个 2×3 的矩阵。

有各种实用函数,可以用来构造常见的数组(取值均匀或随机)。

julia> zeros(2, 3)  # see also ones()
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> falses(2, 2)  # booleans, see also trues()
2×2 BitMatrix:
 0  0
 0  0

julia> rand(Float32, 2, 3)  # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
 0.768823  0.169633  0.632565
 0.388451  0.109176  0.850381

生成等距的值

熟悉 NumPy 的人会知道,np.linspace 是一个广泛使用的函数,用来生成一个指定长度、指定端点、取值等距的数组。它通常用作图表的 x 轴,或线性模型中的自变量。

Julia 没有完全对应的函数,但非常灵活的 range() 函数可以模仿它:指定上下界,再加上 length 关键字参数。

任何在本地工作、能访问 Plots 包的人都可以运行这段代码:

julia> using Plots

julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586

julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}

julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
 0.0         0.0           1.0
 0.0634665   0.0634239     0.997987
 0.126933    0.126592      0.991955
 0.1904      0.189251      0.981929
(...truncated)

julia> plot(vals[:, 1], vals[:, 2:3])

StepRangeLen 可以像向量一样使用,包括转换成矩阵的一列。

另请参见对应的 logrange() 函数,它可以在对数轴上生成等距的取值。

索引

对于二维数组,我们一般按 [row, col] 的顺序使用两个下标。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[1, 2] # row 1, col 2
2

# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]

julia> m[3]
2

最后一个例子也许让人意外:用_单个_下标并不会报错,而是返回一个元素。

原因还是前面提到的列优先顺序:Julia 会先顺着第 1 列往下,再顺着第 2 列往下,直到在_内存中_找到第 3 个元素。

注意:在编写通用库时这有一些用处,但更可能让人困惑!

查询数组大小时也有类似的问题。length() 给出元素的总数,size() 给出一个元组,其中有 ndims() 个元素,分别是每个维度的长度。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> length(m)
6

julia> size(m)  # 2 rows, 3 cols
(2, 3)

julia> ndims(m)  # how many dimensions? Like `m |> size |> length`
2

用区间和数组做索引

我们可以轻松地复制一个子矩阵。

下面的例子里,reshape() 函数按列填充,把数组重塑成指定的维度,然后我们对它做切片。

julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12

julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
 2  6
 3  7
 4  8

# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
 2  6  10
 3  7  11
 4  8  12

单独一个 : 表示“复制这个维度里的全部内容”。

对于不连续的行或列,可以用向量指定:

julia> m12[[1, 3], :]  # rows 1 and 3
2×3 Matrix{Int64}:
 1  5   9
 3  7  11

单独一个 : 会把任意数组按列展平成一个向量。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[:]
6-element Vector{Int64}:
 1
 4
 2
 5
 3
 6

对数组应用函数

前面我们见过把 sum()、maximum() 这类聚合函数应用到一维集合上,它们会作用于所有元素,并返回一个标量结果。

这在更高维也同样适用。不过,我们有时只想把函数应用到某一个维度上,比如沿纵向或横向求和,返回一个在某个维度上长度为 1(即 singleton dimension)的数组。

为此,有一个可选的 dims 关键字参数。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> sum(m)  # sum everything
21

julia> sum(m; dims=1)  # sum down
1×3 Matrix{Int64}:
 5  7  9

julia> sum(m; dims=2)  # sum across
2×1 Matrix{Int64}:
  6
 15

# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
 3
 6

dims 的值是_最终长度为 1 的那个维度_,所以上面的例子里 dims=1 => 1×3,dims=2 => 2×1。

推而广之,如果把 dims 设成一个数组或区间,高维数组可以一次归约多个维度_(再说一次,理解输出结果可能需要多想一想!)_。

编写能感知维度的函数

dims 关键字参数在 sum() 这样的内置函数里很常见,但我们怎么在自己的代码里写出等价的功能呢?

一个好办法是使用 reduce() 这样的高阶函数,这会在后面的概念中详细介绍。

另一种做法是,Julia 提供了几个函数,让我们可以把 N 维数组当作嵌套的向量来处理。

对矩阵来说,eachrow() 和 eachcol() 很方便,但更通用的函数是 eachslice(),它可以处理任意维度。

# m is as in the previous examples

julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
 [1, 2, 3]
 [4, 5, 6]

julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
 [1, 4]
 [2, 5]
 [3, 6]

这个类型看着有点吓人,但那只是因为它是对原数组的一个_视图_,让我们不必复制就能对它操作。Julia 数组的大小可能达到 TB 级,所以复制可能会带来性能灾难!

这样的视图可以用于循环、广播,也可以用于我们目前在教学大纲里见过的其他各种操作。

此外,推导式在接收数组输入时既强大又灵活。循环这一概念里提过一些简单的情况,后面还会有更详细的讨论。

多维广播

我们已经在向量运算这一概念里讨论过一维的情况。

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v .- 0.5
3-element Vector{Float64}:
 0.7
 1.0
 1.2

点运算符 .- 通过 broadcasting 把单个值 0.5 扩展成与 [0.5, 0.5, 0.5] 等价,然后逐元素做减法。

把它扩展到更高维,其实也只是同样的道理。

例如,把一个 2x1 的行向量 [1.0 1.5 2.0] 与 2x3 的矩阵 [1 2 3; 4 5 6] 相乘并广播时,等价于先把 [1.0 1.5 2.0] 广播成 [1.0 1.5 2.0; 1.0 1.5 2.0],再逐元素相乘。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m .* [1.0 1.5 2.0]  # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0]  # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m ./ [1, 2]  # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

julia> m ./ [1 1 1; 2 2 2]  # 2x3 elementwise division
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

注意: 做广播时,非单例的维度大小必须匹配(例如 2x3 Matrix .* 2x1 Matrix)。

另外,函数也可以广播到 Matrix 的每个元素上,做法和用 Vector 时完全一样。

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> (x -> x^2).(m)  # broadcast a function to all elements
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

一开始你可能会觉得,维度越多,程序员被绕晕的可能就越是呈指数增长,但多练习会大有帮助。另外,尽管语法不同,对 NumPy 数组的熟悉程度能很好地迁移到 Julia。

延伸阅读

许多对向量和矩阵的常见操作,都属于线性代数这个大范畴。

Exercism 网站上(目前)还有两个额外的概念,在我们设计出能与之配套的练习之前,它们只有文档。

  1. 线性代数基础主要讨论各种类型的乘法。
  2. 线性方程求解讨论线性方程组,以及如何高效地求解它们。
Advanced

将来可能会有第三个关于矩阵分解的概念。

不过请留意,线性代数基础相当偏数学,未必适合所有学生。

后面的概念(方程求解和矩阵分解)更进阶,真正面向的是能轻松应对大学数学的人。

很抱歉,但这就是这门学科本身的样子,也是 Julia 的一个重要应用场景。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 多维数组

练习已锁定

再解锁 1 个练习即可练习 多维数组