数组和矩阵是 Julia 数值计算的根本,进而也是大多数机器学习任务的根本。它们提供了一种高效方式来存储和处理数据集合,例如特征集、模型参数或图像像素。Julia 对数组的实现尤为强大,兼具高性能和灵活、富有表现力的语法。假定您对编程有一定了解,Julia 对这些数据结构的处理方法成为关注的焦点。您会发现 Julia 的数组是基于1的索引,这意味着第一个元素位于索引1,而非0,这在 Python 或 C++ 等语言中很常见。Julia 中的数组也是可变的,它们的元素可以是任意类型,但指定具体的元素类型(例如 Float64)通常会因为 Julia 的类型系统和方法专门化而带来更好的性能。创建数组和矩阵Julia 提供多种方式来创建数组,满足不同需求。向量(一维数组)向量是一维数组。它们本质上是元素的列表。# 整数向量 vec1 = [10, 20, 30] println(vec1) # 输出: [10, 20, 30] println(typeof(vec1)) # 输出: Vector{Int64} (or Array{Int64, 1}) # 浮点数向量 vec2 = [1.5, 2.5, 3.5] println(vec2) # 输出: [1.5, 2.5, 3.5] println(typeof(vec2)) # 输出: Vector{Float64} # 显式类型向量 vec3 = Float32[1, 2, 3] # 元素将为 Float32 类型 println(vec3) # 输出: Float32[1.0, 2.0, 3.0] println(eltype(vec3)) # 输出: Float32 # 在 Julia 中,一维数组通常被视为列向量。 # 要创建 1xN 行向量(这是一个二维数组),可以使用: row_vector = [1 2 3] # 注意是空格分隔,没有逗号 println(row_vector) # 输出: 1×3 Matrix{Int64}: # 1 2 3 println(typeof(row_vector)) # 输出: Matrix{Int64} (or Array{Int64, 2})矩阵(二维数组)矩阵是二维数组,是表示数据集的核心,其中行表示样本、列表示特征,或用于存储神经网络中的权重矩阵。# 创建一个 2x3 矩阵(2行,3列) matrix1 = [1 2 3; 4 5 6] println(matrix1) # 输出: # 2×3 Matrix{Int64}: # 1 2 3 # 4 5 6 # 行中的元素用空格分隔,行与行之间用分号分隔。 # 如果需要,也可以用逗号分隔行中的元素, # 但对于矩阵字面量,空格更常见。 matrix2 = Float64[1.0 2.0; 3.0 4.0] println(matrix2) # 输出: # 2×2 Matrix{Float64}: # 1.0 2.0 # 3.0 4.0高维数组Julia 支持任意维度的数组。# 一个三维数组 (2x2x2) array3d = Array{Int, 3}(undef, 2, 2, 2) # 创建一个未初始化的三维数组 # 接下来您可以填充数值。 # 示例: array3d[1,1,1] = 10 println(array3d[1,1,1]) # 输出: 10undef 关键字表示创建数组时不会将其元素初始化为任何特定值;它们将包含该内存位置中的任意数据。初始化函数Julia 提供便捷函数来创建具有特定初始值的数组:# 一个未初始化的 2x3 Float64 矩阵 uninit_matrix = Matrix{Float64}(undef, 2, 3) # 一个 3x2 的零矩阵 zeros_matrix = zeros(3, 2) println(zeros_matrix) # 输出: # 3×2 Matrix{Float64}: # 0.0 0.0 # 0.0 0.0 # 0.0 0.0 # 一个指定类型且全为1的向量 ones_vector = ones(Int8, 4) println(ones_vector) # 输出: Int8[1, 1, 1, 1] # 一个填充了值7的 2x2 矩阵 fill_matrix = fill(7, (2, 2)) println(fill_matrix) # 输出: # 2×2 Matrix{Int64}: # 7 7 # 7 7 # 具有随机值的数组 rand_vector = rand(3) # 包含3个0到1之间 Float64 值的向量 rand_matrix = rand(2, 2) # 包含0到1之间 Float64 值的 2x2 矩阵 randn_matrix = randn(2,3) # 包含来自标准正态分布的 Float64 值的 2x3 矩阵范围和推导式数组也可以从范围构建,或通过使用推导式创建,这是一种基于迭代集合来创建数组的简洁方式。# 从范围创建 range_array = collect(1:5) # 创建一个向量: [1, 2, 3, 4, 5] println(range_array) # 数组推导式 squares = [i^2 for i in 1:4] # 向量: [1, 4, 9, 16] println(squares) # 矩阵推导式 matrix_comp = [i * j for i in 1:2, j in 1:3] println(matrix_comp) # 输出: # 2×3 Matrix{Int64}: # 1 2 3 # 2 4 6检查数组属性一旦有了数组,您常常需要查询其属性:my_matrix = [10 20; 30 40; 50 60] # 3×2 Matrix{Int64}: # 10 20 # 30 40 # 50 60 println(eltype(my_matrix)) # 输出: Int64 (元素类型) println(size(my_matrix)) # 输出: (3, 2) (以元组表示的维度) println(size(my_matrix, 1)) # 输出: 3 (第一维的大小 - 行) println(size(my_matrix, 2)) # 输出: 2 (第二维的大小 - 列) println(length(my_matrix)) # 输出: 6 (元素总数) println(ndims(my_matrix)) # 输出: 2 (维度数量) # axes 提供每个维度的有效索引范围 println(axes(my_matrix)) # 输出: (Base.OneTo(3), Base.OneTo(2)) println(axes(my_matrix, 1)) # 输出: Base.OneTo(3) (行的索引: 1到3)索引和切片访问和修改数组的元素或子部分是常用操作。请记住,Julia 是基于1的索引。A = [1, 2, 3, 4, 5] M = [10 20 30; 40 50 60] # 访问单个元素 println(A[1]) # 输出: 1 println(M[2, 3]) # 输出: 60 (第2行,第3列) # 使用 `end` 指代最后一个索引 println(A[end]) # 输出: 5 println(M[1, end]) # 输出: 30 # 切片获取子数组(切片默认创建副本) sub_A = A[2:4] # 索引 2, 3, 4 处的元素。输出: [2, 3, 4] println(sub_A) col_2 = M[:, 2] # 所有行,第2列。输出: [20, 50] (一个向量) println(col_2) row_1 = M[1, :] # 第1行,所有列。输出: [10, 20, 30] (一个向量) println(row_1) sub_M = M[1:2, [1, 3]] # 第1-2行,第1和第3列 println(sub_M) # 输出: # 2×2 Matrix{Int64}: # 10 30 # 40 60 # 修改元素 A[1] = 100 M[2, 3] = 600 println(A) # 输出: [100, 2, 3, 4, 5] println(M) # 输出: # 2×3 Matrix{Int64}: # 10 20 30 # 40 50 600逻辑索引您可以使用布尔数组来选择元素:data = [1.2, -0.5, 2.3, 0.0, -1.8] positive_data = data[data .> 0] # 注意元素级比较 '.>' println(positive_data) # 输出: [1.2, 2.3]数组操作元素级操作(广播)对于数组的逐元素操作,Julia 使用简洁的点(.)语法。这称为广播。如果两个数组大小相同,A .+ B 会将它们逐元素相加。如果有一个数组 A 和一个标量 s,A .+ s 会将 s 加到 A 的每个元素。广播非常高效,是数值代码中的常见模式。X = [1 2; 3 4] Y = [5 6; 7 8] scalar = 10 # 逐元素相加 Z_add = X .+ Y println(Z_add) # 输出: # 2×2 Matrix{Int64}: # 6 8 # 10 12 # 广播标量 Z_scalar_add = X .+ scalar println(Z_scalar_add) # 输出: # 2×2 Matrix{Int64}: # 11 12 # 13 14 # 逐元素相乘 Z_mult = X .* Y println(Z_mult) # 输出: # 2×2 Matrix{Int64}: # 5 12 # 21 32 # 逐元素函数应用 Z_sin = sin.(X) # 将 sin 应用于 X 的每个元素 println(Z_sin) # 输出(近似值): # 2×2 Matrix{Float64}: # 0.841471 0.909297 # 0.14112 -0.756802如果数组的维度兼容(例如,矩阵和向量,其中向量可能被视为行或列),广播会智能处理不同形状数组之间的操作。digraph G { rankdir=TB; node [shape=box, style="filled", fillcolor="#e9ecef", fontname="Arial"]; edge [fontname="Arial", fontsize=10]; subgraph cluster_M { label = "矩阵 M (2x2)"; bgcolor="#f8f9fa"; M_node [label="[1 2;\n 3 4]", shape=plaintext, fontsize=12]; } subgraph cluster_V { label = "向量 V (隐式 1x2 行向量)"; bgcolor="#f8f9fa"; V_node [label="[10 20]", shape=plaintext, fontsize=12]; } subgraph cluster_S { label = "标量 S"; bgcolor="#f8f9fa"; S_node [label="5", shape=plaintext, fontsize=12]; } subgraph cluster_Result1 { label = "M .+ V (V 广播到 M 的每一行)"; bgcolor="#f8f9fa"; R1_node [label="[1+10 2+20;\n 3+10 4+20]", shape=plaintext, fontsize=12]; R1_final [label="[11 22;\n 13 24]", shape=plaintext, fontsize=12, fillcolor="#b2f2bb"]; R1_node -> R1_final [style=invis]; } subgraph cluster_Result2 { label = "M .* S (S 广播到 M 的每个元素)"; bgcolor="#f8f9fa"; R2_node [label="[1*5 2*5;\n 3*5 4*5]", shape=plaintext, fontsize=12]; R2_final [label="[5 10;\n 15 20]", shape=plaintext, fontsize=12, fillcolor="#b2f2bb"]; R2_node -> R2_final [style=invis]; } M_node -> R1_node; V_node -> R1_node [label=" V 实际变为 [10 20;\n 10 20] "]; M_node -> R2_node; S_node -> R2_node [label=" S 应用于所有元素 "]; }将向量 V 广播到矩阵 M,并将标量 S 广播到矩阵 M。维度会扩展或值会重复以匹配。您也可以使用 .= 执行原地广播,它直接修改左侧的数组,可能节省内存分配:A = [1.0, 2.0, 3.0] B = [0.5, 0.5, 0.5] A .+= B # A 现在是 [1.5, 2.5, 3.5] println(A)线性代数Julia 自带一个全面的 LinearAlgebra 标准库。您常常需要 using LinearAlgebra 来使用其函数。using LinearAlgebra mat_A = [1 2; 3 4] mat_B = [5 0; 0 5] # 一个缩放矩阵 vec_v = [10, 20] # 矩阵乘法 mat_C = mat_A * mat_B println(mat_C) # 输出: # 2×2 Matrix{Int64}: # 5 10 # 15 20 # 矩阵-向量乘法 result_vec = mat_A * vec_v println(result_vec) # 输出: [50, 110] # 转置 # A' 执行递归转置(对复数而言是共轭转置) # transpose(A) 执行非递归转置 mat_A_T = mat_A' println(mat_A_T) # 输出: # 2×2 adjoint(::Matrix{Int64}) with eltype Int64: # 1 3 # 2 4 mat_A_transpose = transpose(mat_A) println(mat_A_transpose) # 输出: # 2×2 transpose(::Matrix{Int64}) with eltype Int64: # 1 3 # 2 4 # 点积(内积) u = [1, 2, 3] v = [4, 5, 6] dot_prod = dot(u, v) # 或者对于实向量使用 u' * v println(dot_prod) # 输出: 32 (1*4 + 2*5 + 3*6) # 矩阵的逆 square_mat = [3.0 1.0; 1.0 2.0] inv_mat = inv(square_mat) println(inv_mat) # 输出(近似值): # 2×2 Matrix{Float64}: # 0.4 -0.2 # -0.2 0.6 # 行列式 det_val = det(square_mat) println(det_val) # 输出: 5.0 # 特征值和特征向量 eigen_decomp = eigen(square_mat) eigenvalues = eigen_decomp.values eigenvectors = eigen_decomp.vectors println("特征值: ", eigenvalues) # 输出: Eigenvalues: [1.58579, 3.41421] println("特征向量:\n", eigenvectors) # Output: # Eigenvectors: # -0.850651 -0.525731 # 0.525731 -0.850651 # 奇异值分解 (SVD) svd_decomp = svd(mat_A) U, S_vals, V_mat = svd_decomp.U, svd_decomp.S, svd_decomp.V # U 和 V 是正交矩阵,S_vals 是奇异值向量。 println("奇异值: ", S_vals) # 输出: Singular values: [5.46499, 0.365966]这些线性代数操作是许多机器学习算法的核心工具,从线性回归到主成分分析 (PCA) 以及深度学习模型的内部机制。修改数组重塑您可以在不改变数组内容的情况下改变其维度,只要元素总数不变。reshape 通常返回原始数组内存的视图。original_vec = collect(1:6) # [1, 2, 3, 4, 5, 6] reshaped_mat = reshape(original_vec, (2, 3)) # 2行,3列 println(reshaped_mat) # Output: # 2×3 reshape(::Vector{Int64}, 2, 3) with eltype Int64: # 1 3 5 # 2 4 6 # 注意列主序:元素先按列填充。 reshaped_mat[1,1] = 100 println(original_vec[1]) # 输出: 100,因为 reshape 返回一个视图扁平化要将多维数组转换为一维向量(列向量),请使用 vec():M = [1 2; 3 4] V = vec(M) println(V) # 输出: [1, 3, 2, 4] (列主序扁平化) V[2] = 300 println(M[2,1]) # 输出: 300,vec 也返回一个视图连接Julia 提供函数和语法来组合数组:A = [1, 2] B = [3, 4] M1 = [1 2; 3 4] M2 = [5 6; 7 8] # 垂直连接 v_concat = vcat(A, B) # 对于向量: [1, 2, 3, 4] v_concat_mat = vcat(M1, M2) println(v_concat_mat) # Output: # 4×2 Matrix{Int64}: # 1 2 # 3 4 # 5 6 # 7 8 # 垂直连接的字面量语法 v_concat_literal = [A; B] v_concat_mat_literal = [M1; M2] println(v_concat_mat_literal == v_concat_mat) # 输出: true # 水平连接 h_concat_mat = hcat(M1, M2) println(h_concat_mat) # Output: # 2×4 Matrix{Int64}: # 1 2 5 6 # 3 4 7 8 # 水平连接的字面量语法 # 确保维度兼容;A 和 B 是列向量。 A_col = [1; 2] B_col = [3; 4] h_concat_literal_vec = [A_col B_col] # 结果为 2x2 矩阵: [1 3; 2 4] h_concat_mat_literal = [M1 M2] println(h_concat_mat_literal == h_concat_mat) # 输出: true # 使用 cat() 进行通用连接 C1 = ones(2,2) C2 = zeros(2,2) cat_dim3 = cat(C1, C2; dims=3) # 沿着新的第三维度连接 println(size(cat_dim3)) # 输出: (2, 2, 2)对于一维 Vector,您还可以使用 push!、pop!、append!、prepend! 进行修改操作。视图:高效处理子数组当您切片数组时,例如 sub_array = M[1:2, :],Julia 默认情况下会创建该数组部分的副本。如果您处理大型数据集或执行许多切片操作,这种复制在内存和时间方面效率不高。另一种方法是创建视图。视图是一个 SubArray 对象,它引用原始数组的数据而不进行复制。修改视图会修改原始数组。data_matrix = rand(5, 5) # 切片创建副本 slice_copy = data_matrix[1:2, 1:2] slice_copy[1,1] = 999.0 println(data_matrix[1,1]) # 原始矩阵未改变 # 创建视图 view_of_data = @view data_matrix[1:2, 1:2] # or: view_of_data = view(data_matrix, 1:2, 1:2) view_of_data[1,1] = -100.0 println(data_matrix[1,1]) # 输出: -100.0 (原始矩阵*已*改变) # 视图在将数组部分传递给函数时很有用,可以避免复制开销 function process_row!(row_view) row_view .*= 2 # 原地修改行 end first_row_view = @view data_matrix[1, :] process_row!(first_row_view) println(data_matrix[1,:]) # data_matrix 的第一行现在翻倍了。当您需要操作数组的一部分时,尤其是在打算修改它或数组很大时,请使用视图以避免不必要的内存分配。如果您需要一个独立的副本,那么标准切片是合适的。数组类型和性能说明Julia 的优势之一是其类型系统。当您创建一个像 Float64[1.0, 2.0, 3.0] 这样的数组时,Julia 知道每个元素都是 Float64 类型。这使得编译器可以为该数组的操作生成高度专门化和优化的机器码。如果您创建一个像 Any[1, "hello", 3.0] 这样的数组,Julia 在运行时需要做更多工作来确定类型,这可能较慢。对于机器学习,您几乎总是使用具体类型的数组,通常是像 Float64、Float32 或 Int 这样的数字。这是 Julia 在数值任务中表现出高效率的一个重要因素。Julia 还为数组定义了一个抽象层次结构:AbstractArray{T,N}: 所有 N 维数组的超类型,元素类型为 T。AbstractVector{T}: AbstractArray{T,1} 的别名。AbstractMatrix{T}: AbstractArray{T,2} 的别名。编写接受 AbstractArray(或 AbstractVector、AbstractMatrix)的函数可以使您的代码通用,并与各种类似数组的结构一起工作,包括像稀疏数组或静态大小数组这样的专门化结构,我们在此处不会详细介绍它们,但它们值得了解。数组和矩阵与机器学习的关系数组和矩阵是机器学习实现的通用语言:特征矩阵 (X): 通常,数据集表示为矩阵,其中行是单个样本(或实例),列是特征(或属性)。如果您有 $m$ 个样本和 $n$ 个特征,您的特征矩阵 $X$ 将是 $m \times n$。目标向量 (y): 在监督学习中,对应每个样本的标签或目标值通常存储在向量中。如果您有 $m$ 个样本,您的目标向量 $y$ 将有 $m$ 个元素。模型参数(权重/系数): 许多模型,如线性回归或神经网络,都具有以向量或矩阵形式存储和处理的参数(权重、偏置)。例如,在线性回归 $y = Xw + b$ 中,$w$ 是一个权重向量。中间计算: 神经网络中的激活、优化过程中的梯度以及许多其他量都表示为数组。让我们考虑一个简单示例:计算特征矩阵中每个特征的平均值。# 示例特征矩阵(3个样本,2个特征) X_features = [1.0 10.0; 2.0 12.0; 3.0 14.0] # 计算每列(特征)的平均值 # Julia 中的 mean 函数可以沿着指定维度操作 using Statistics # 用于 mean 函数 mean_feature1 = mean(X_features[:, 1]) # 第一列的平均值 mean_feature2 = mean(X_features[:, 2]) # 第二列的平均值 println("特征 1 的平均值: ", mean_feature1) # 输出: 2.0 println("特征 2 的平均值: ", mean_feature2) # 输出: 12.0 # 更一般地,获取所有列的平均值: feature_means = mean(X_features, dims=1) # dims=1 表示对每列沿着行进行操作 println("特征平均值(1x2 行矩阵):\n", feature_means) # Output: # 特征平均值(1x2 行矩阵): # 2.0 12.0熟练掌握 Julia 中的数组和矩阵操作是高效实现和理解机器学习算法的根本。富有表现力的语法、广播功能以及强大的线性代数支持相结合,使 Julia 成为处理这些任务的引人入胜的环境。随着学习深入,您将看到这些结构被广泛使用。