Arrays and matrices are the bedrock of numerical computation in Julia, and by extension, for most machine learning tasks. They provide an efficient way to store and manipulate collections of data, such as feature sets, model parameters, or image pixels. Julia's implementation of arrays is particularly powerful, offering both high performance and a flexible, expressive syntax. As this course assumes some familiarity with programming, we'll focus on Julia's specific approach to these data structures.
You'll find that Julia's arrays are 1-indexed, meaning the first element is at index 1, not 0, which is common in languages like Python or C++. Arrays in Julia are also mutable, and their elements can be of any type, though specifying a concrete element type (e.g., Float64
) typically leads to better performance due to Julia's type system and method specialization.
Julia offers several ways to create arrays, catering to different needs.
Vectors are one-dimensional arrays. They are essentially lists of elements.
# A Vector of integers
vec1 = [10, 20, 30]
println(vec1) # Output: [10, 20, 30]
println(typeof(vec1)) # Output: Vector{Int64} (or Array{Int64, 1})
# A Vector of floating-point numbers
vec2 = [1.5, 2.5, 3.5]
println(vec2) # Output: [1.5, 2.5, 3.5]
println(typeof(vec2)) # Output: Vector{Float64}
# Explicitly typed Vector
vec3 = Float32[1, 2, 3] # Elements will be Float32
println(vec3) # Output: Float32[1.0, 2.0, 3.0]
println(eltype(vec3)) # Output: Float32
# In Julia, 1D arrays are typically treated as column vectors.
# To create a 1xN row vector (which is a 2D array), you'd use:
row_vector = [1 2 3] # Note the spaces, no commas
println(row_vector) # Output: 1×3 Matrix{Int64}:
# 1 2 3
println(typeof(row_vector)) # Output: Matrix{Int64} (or Array{Int64, 2})
Matrices are two-dimensional arrays, fundamental for representing datasets where rows might be samples and columns features, or for storing weight matrices in neural networks.
# Creating a 2x3 matrix (2 rows, 3 columns)
matrix1 = [1 2 3; 4 5 6]
println(matrix1)
# Output:
# 2×3 Matrix{Int64}:
# 1 2 3
# 4 5 6
# Spaces separate elements in a row, semicolons separate rows.
# Commas can also be used to separate elements in a row if desired,
# but spaces are common for matrix literals.
matrix2 = Float64[1.0 2.0; 3.0 4.0]
println(matrix2)
# Output:
# 2×2 Matrix{Float64}:
# 1.0 2.0
# 3.0 4.0
Julia supports arrays with any number of dimensions.
# A 3D array (2x2x2)
array3d = Array{Int, 3}(undef, 2, 2, 2) # Creates an uninitialized 3D array
# You would then fill it with values.
# Example:
array3d[1,1,1] = 10
println(array3d[1,1,1]) # Output: 10
The undef
keyword indicates that the array is created without initializing its elements to any specific value; they will contain whatever arbitrary bits were in that memory location.
Julia provides convenient functions to create arrays with specific initial values:
# An uninitialized 2x3 Matrix of Float64
uninit_matrix = Matrix{Float64}(undef, 2, 3)
# A 3x2 matrix of zeros
zeros_matrix = zeros(3, 2)
println(zeros_matrix)
# Output:
# 3×2 Matrix{Float64}:
# 0.0 0.0
# 0.0 0.0
# 0.0 0.0
# A vector of ones with a specific type
ones_vector = ones(Int8, 4)
println(ones_vector) # Output: Int8[1, 1, 1, 1]
# A 2x2 matrix filled with the value 7
fill_matrix = fill(7, (2, 2))
println(fill_matrix)
# Output:
# 2×2 Matrix{Int64}:
# 7 7
# 7 7
# Arrays with random values
rand_vector = rand(3) # Vector of 3 Float64s between 0 and 1
rand_matrix = rand(2, 2) # 2x2 Matrix of Float64s between 0 and 1
randn_matrix = randn(2,3) # 2x3 Matrix of Float64s from standard normal distribution
Arrays can also be constructed from ranges or by using comprehensions, which are a concise way to create arrays based on iterating over collections.
# From a range
range_array = collect(1:5) # Creates a Vector: [1, 2, 3, 4, 5]
println(range_array)
# Array comprehensions
squares = [i^2 for i in 1:4] # Vector: [1, 4, 9, 16]
println(squares)
# Matrix comprehension
matrix_comp = [i * j for i in 1:2, j in 1:3]
println(matrix_comp)
# Output:
# 2×3 Matrix{Int64}:
# 1 2 3
# 2 4 6
Once you have an array, you'll often need to query its characteristics:
my_matrix = [10 20; 30 40; 50 60]
# 3×2 Matrix{Int64}:
# 10 20
# 30 40
# 50 60
println(eltype(my_matrix)) # Output: Int64 (element type)
println(size(my_matrix)) # Output: (3, 2) (dimensions as a tuple)
println(size(my_matrix, 1)) # Output: 3 (size of the 1st dimension - rows)
println(size(my_matrix, 2)) # Output: 2 (size of the 2nd dimension - columns)
println(length(my_matrix)) # Output: 6 (total number of elements)
println(ndims(my_matrix)) # Output: 2 (number of dimensions)
# axes provides the valid index ranges for each dimension
println(axes(my_matrix)) # Output: (Base.OneTo(3), Base.OneTo(2))
println(axes(my_matrix, 1)) # Output: Base.OneTo(3) (indices for rows: 1 to 3)
Accessing and modifying elements or subsections of arrays is a frequent operation. Remember, Julia is 1-indexed.
A = [1, 2, 3, 4, 5]
M = [10 20 30; 40 50 60]
# Accessing single elements
println(A[1]) # Output: 1
println(M[2, 3]) # Output: 60 (2nd row, 3rd column)
# Using `end` to refer to the last index
println(A[end]) # Output: 5
println(M[1, end]) # Output: 30
# Slicing to get sub-arrays (slices create copies by default)
sub_A = A[2:4] # Elements at index 2, 3, 4. Output: [2, 3, 4]
println(sub_A)
col_2 = M[:, 2] # All rows, 2nd column. Output: [20, 50] (a Vector)
println(col_2)
row_1 = M[1, :] # 1st row, all columns. Output: [10, 20, 30] (a Vector)
println(row_1)
sub_M = M[1:2, [1, 3]] # Rows 1-2, columns 1 and 3
println(sub_M)
# Output:
# 2×2 Matrix{Int64}:
# 10 30
# 40 60
# Modifying elements
A[1] = 100
M[2, 3] = 600
println(A) # Output: [100, 2, 3, 4, 5]
println(M)
# Output:
# 2×3 Matrix{Int64}:
# 10 20 30
# 40 50 600
You can use boolean arrays to select elements:
data = [1.2, -0.5, 2.3, 0.0, -1.8]
positive_data = data[data .> 0] # Note the element-wise comparison '.>'
println(positive_data) # Output: [1.2, 2.3]
For element-by-element operations on arrays, Julia uses a concise dot (.
) syntax. This is known as broadcasting. If you have two arrays of the same size, A .+ B
adds them element-wise. If you have an array A
and a scalar s
, A .+ s
adds s
to every element of A
. Broadcasting is highly efficient and is a common pattern in numerical code.
X = [1 2; 3 4]
Y = [5 6; 7 8]
scalar = 10
# Element-wise addition
Z_add = X .+ Y
println(Z_add)
# Output:
# 2×2 Matrix{Int64}:
# 6 8
# 10 12
# Broadcasting a scalar
Z_scalar_add = X .+ scalar
println(Z_scalar_add)
# Output:
# 2×2 Matrix{Int64}:
# 11 12
# 13 14
# Element-wise multiplication
Z_mult = X .* Y
println(Z_mult)
# Output:
# 2×2 Matrix{Int64}:
# 5 12
# 21 32
# Element-wise function application
Z_sin = sin.(X) # Applies sin to each element of X
println(Z_sin)
# Output (approximate):
# 2×2 Matrix{Float64}:
# 0.841471 0.909297
# 0.14112 -0.756802
Broadcasting intelligently handles operations between arrays of different shapes if their dimensions are compatible (e.g., a matrix and a vector, where the vector might be treated as a row or column).
Broadcasting a vector
V
to matrixM
and a scalarS
to matrixM
. The dimensions are expanded or values repeated to match.
You can also perform in-place broadcasting using .=
, which modifies the array on the left-hand side directly, potentially saving memory allocations:
A = [1.0, 2.0, 3.0]
B = [0.5, 0.5, 0.5]
A .+= B # A is now [1.5, 2.5, 3.5]
println(A)
Julia comes with a comprehensive LinearAlgebra
standard library. You'll often need to using LinearAlgebra
to access its functions.
using LinearAlgebra
mat_A = [1 2; 3 4]
mat_B = [5 0; 0 5] # A scaling matrix
vec_v = [10, 20]
# Matrix multiplication
mat_C = mat_A * mat_B
println(mat_C)
# Output:
# 2×2 Matrix{Int64}:
# 5 10
# 15 20
# Matrix-vector multiplication
result_vec = mat_A * vec_v
println(result_vec) # Output: [50, 110]
# Transpose
# A' performs a recursive transpose (conjugate transpose for complex numbers)
# transpose(A) performs a non-recursive transpose
mat_A_T = mat_A'
println(mat_A_T)
# Output:
# 2×2 adjoint(::Matrix{Int64}) with eltype Int64:
# 1 3
# 2 4
mat_A_transpose = transpose(mat_A)
println(mat_A_transpose)
# Output:
# 2×2 transpose(::Matrix{Int64}) with eltype Int64:
# 1 3
# 2 4
# Dot product (inner product)
u = [1, 2, 3]
v = [4, 5, 6]
dot_prod = dot(u, v) # or u' * v for real vectors
println(dot_prod) # Output: 32 (1*4 + 2*5 + 3*6)
# Inverse of a matrix
square_mat = [3.0 1.0; 1.0 2.0]
inv_mat = inv(square_mat)
println(inv_mat)
# Output (approximate):
# 2×2 Matrix{Float64}:
# 0.4 -0.2
# -0.2 0.6
# Determinant
det_val = det(square_mat)
println(det_val) # Output: 5.0
# Eigenvalues and eigenvectors
eigen_decomp = eigen(square_mat)
eigenvalues = eigen_decomp.values
eigenvectors = eigen_decomp.vectors
println("Eigenvalues: ", eigenvalues) # Output: Eigenvalues: [1.58579, 3.41421]
println("Eigenvectors:\n", eigenvectors)
# Output:
# Eigenvectors:
# -0.850651 -0.525731
# 0.525731 -0.850651
# Singular Value Decomposition (SVD)
svd_decomp = svd(mat_A)
U, S_vals, V_mat = svd_decomp.U, svd_decomp.S, svd_decomp.V
# U and V are orthogonal matrices, S_vals is a vector of singular values.
println("Singular values: ", S_vals) # Output: Singular values: [5.46499, 0.365966]
These linear algebra operations are the workhorses for many machine learning algorithms, from linear regression to Principal Component Analysis (PCA) and the internals of deep learning models.
You can change the dimensions of an array without changing its contents, as long as the total number of elements remains the same. reshape
typically returns a view into the original array's memory.
original_vec = collect(1:6) # [1, 2, 3, 4, 5, 6]
reshaped_mat = reshape(original_vec, (2, 3)) # 2 rows, 3 columns
println(reshaped_mat)
# Output:
# 2×3 reshape(::Vector{Int64}, 2, 3) with eltype Int64:
# 1 3 5
# 2 4 6
# Note the column-major order: elements are filled down columns first.
reshaped_mat[1,1] = 100
println(original_vec[1]) # Output: 100, because reshape gives a view
To convert a multi-dimensional array into a 1D vector (column vector), use vec()
:
M = [1 2; 3 4]
V = vec(M)
println(V) # Output: [1, 3, 2, 4] (column-major flattening)
V[2] = 300
println(M[2,1]) # Output: 300, vec also returns a view
Julia provides functions and syntax to combine arrays:
A = [1, 2]
B = [3, 4]
M1 = [1 2; 3 4]
M2 = [5 6; 7 8]
# Vertical concatenation
v_concat = vcat(A, B) # For vectors: [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
# Literal syntax for vertical concatenation
v_concat_literal = [A; B]
v_concat_mat_literal = [M1; M2]
println(v_concat_mat_literal == v_concat_mat) # Output: true
# Horizontal concatenation
h_concat_mat = hcat(M1, M2)
println(h_concat_mat)
# Output:
# 2×4 Matrix{Int64}:
# 1 2 5 6
# 3 4 7 8
# Literal syntax for horizontal concatenation
# Ensure dimensions are compatible; A and B are column vectors.
A_col = [1; 2]
B_col = [3; 4]
h_concat_literal_vec = [A_col B_col] # Results in a 2x2 matrix: [1 3; 2 4]
h_concat_mat_literal = [M1 M2]
println(h_concat_mat_literal == h_concat_mat) # Output: true
# General concatenation with cat()
C1 = ones(2,2)
C2 = zeros(2,2)
cat_dim3 = cat(C1, C2; dims=3) # Concatenates along a new 3rd dimension
println(size(cat_dim3)) # Output: (2, 2, 2)
For 1D Vector
s, you can also use push!
, pop!
, append!
, prepend!
for modifications.
When you slice an array, like sub_array = M[1:2, :]
, Julia, by default, creates a copy of that part of the array. If you're working with large datasets or performing many slicing operations, this copying can be inefficient in terms of both memory and time.
An alternative is to create a view. A view is an SubArray
object that references the original array's data without copying it. Modifying a view will modify the original array.
data_matrix = rand(5, 5)
# Slicing creates a copy
slice_copy = data_matrix[1:2, 1:2]
slice_copy[1,1] = 999.0
println(data_matrix[1,1]) # Original matrix is unchanged
# Creating a view
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]) # Output: -100.0 (original matrix IS changed)
# Views are useful for passing parts of arrays to functions without incurring copy overhead
function process_row!(row_view)
row_view .*= 2 # Modify the row in-place
end
first_row_view = @view data_matrix[1, :]
process_row!(first_row_view)
println(data_matrix[1,:]) # The first row of data_matrix is now doubled.
Use views when you need to operate on a part of an array, especially if you intend to modify it or if the array is large, to avoid unnecessary allocations. If you need an independent copy, then standard slicing is appropriate.
One of Julia's strengths is its type system. When you create an array like Float64[1.0, 2.0, 3.0]
, Julia knows every element is a Float64
. This allows the compiler to generate highly specialized and optimized machine code for operations on this array. If you create an array like Any[1, "hello", 3.0]
, Julia has to do more work at runtime to determine types, which can be slower.
For machine learning, you'll almost always work with arrays of concrete types, usually numbers like Float64
, Float32
, or Int
. This is a significant factor in Julia's high performance for numerical tasks.
Julia also defines an abstraction hierarchy for arrays:
AbstractArray{T,N}
: The supertype of all N-dimensional arrays with elements of type T
.AbstractVector{T}
: Alias for AbstractArray{T,1}
.AbstractMatrix{T}
: Alias for AbstractArray{T,2}
.Writing functions that accept AbstractArray
(or AbstractVector
, AbstractMatrix
) allows your code to be generic and work with various array-like structures, including specialized ones like sparse arrays or statically-sized arrays, which we won't cover in detail here but are good to be aware of.
Arrays and matrices are the lingua franca of machine learning implementations:
Let's consider a simple example: calculating the mean of each feature in a feature matrix.
# Sample feature matrix (3 samples, 2 features)
X_features = [1.0 10.0;
2.0 12.0;
3.0 14.0]
# Calculate mean of each column (feature)
# mean function in Julia can operate along specified dimensions
using Statistics # for mean
mean_feature1 = mean(X_features[:, 1]) # Mean of the first column
mean_feature2 = mean(X_features[:, 2]) # Mean of the second column
println("Mean of Feature 1: ", mean_feature1) # Output: 2.0
println("Mean of Feature 2: ", mean_feature2) # Output: 12.0
# More generally, to get means of all columns:
feature_means = mean(X_features, dims=1) # dims=1 operates along rows for each column
println("Feature means (as a 1x2 row matrix):\n", feature_means)
# Output:
# Feature means (as a 1x2 row matrix):
# 2.0 12.0
Proficiency with array and matrix manipulations in Julia is foundational for effectively implementing and understanding machine learning algorithms. The combination of expressive syntax, broadcasting, and strong linear algebra support makes Julia a compelling environment for these tasks. As we move forward, you'll see these structures used extensively.
Was this section helpful?
© 2025 ApX Machine Learning