Programming for Artificial Intelligence
AI-130
NumPy for Numerical Computing
Spring 2026
Mr. Mustajab Hussain
mustajab@[Link]
1
Today’s Outline
• NumPy Arrays and Array Creation
• Array Indexing, Slicing, and Reshaping
• Broadcasting and Vectorization
• Mathematical Operations on Arrays
• Linear Algebra Operations
• Random Number Generation for AI
• Performance Optimization Techniques
• NumPy vs Python Lists
• Activity
2
NumPy Arrays and Array Creation
• NumPy (Numerical Python) is the core library for numerical computing in Python
• It provides the ndarray object , a fast, multi-dimensional array far more efficient than a Python list
• Arrays store elements of the same data type, which makes operations very fast
• You can create arrays from lists, using built-in functions, or from ranges
import numpy as np # Zeros, ones, and identity
z = [Link]((3, 3)) # all 0s
# From a Python list o = [Link]((2, 4)) # all 1s
a = [Link]([1, 2, 3, 4, 5]) e = [Link](3) # identity
print(a) # [1 2 3 4 5]
print([Link]) # int64 # Range-based arrays
print([Link]) # (5,) r = [Link](0, 10, 2) # [0,2,4,6,8]
l = [Link](0,1,5) # 5 evenly spaced
# 2D array (matrix)
m = [Link]([[1, 2, 3], # Full array
[4, 5, 6]]) f = [Link]((2,2), 7) # [[7,7],[7,7]]
print([Link]) # (2, 3) print(f)
3
NumPy Array Attributes
• Every NumPy array has useful attributes that tell you about its structure
• ndim tells you how many dimensions the array has (1D, 2D, 3D, etc.)
• shape returns a tuple like (rows, cols) so you know the size of each dimension
• dtype tells you the data type , changing it can save memory or speed up code
import numpy as np # Commonly used dtypes:
# np.int32 / np.int64
a = [Link]([[1,2,3],[4,5,6]], dtype=np.float32) # np.float32 / np.float64
# np.bool_
print([Link]) # 2 (two dimensions) # np.complex128
print([Link]) # (2, 3)
print([Link]) # 6 (total elements) # Tip: use float32 in AI/ML to
print([Link]) # float32 # save memory on large datasets
print([Link]) # 24 (bytes used)
x = [Link]((1000,1000), dtype=np.float32)
# Change dtype print([Link]) # 4,000,000 bytes
b = [Link](np.int32)
print([Link]) # int32 y = [Link]((1000,1000), dtype=np.float64)
print([Link]) # 8,000,000 bytes
4
Array Indexing and Slicing
• NumPy arrays use zero-based indexing just like Python lists
• Use [row, col] to access elements in a 2D array , no nested brackets needed
• Slicing uses the same start:stop:step syntax , and it works on each dimension
• Negative indices count from the end , e.g., -1 means the last element
import numpy as np # Slicing a sub-matrix
sub = m[0:2, 1:3]
a = [Link]([10, 20, 30, 40, 50]) print(sub)
# [[2 3]
print(a[0]) # 10 (first element) # [5 6]]
print(a[-1]) # 50 (last element)
print(a[1:4]) # [20 30 40] # Boolean indexing
print(a[::2]) # [10 30 50] (every 2nd) scores = [Link]([55, 80, 45, 90, 70])
passed = scores[scores >= 60]
# 2D indexing print(passed) # [80 90 70]
m = [Link]([[1,2,3],[4,5,6],[7,8,9]])
print(m[0, 1]) # 2 (row 0, col 1) # Fancy indexing
print(m[1, :]) # [4 5 6] (entire row 1) idx = [0, 2, 4]
print(m[:, 2]) # [3 6 9] (entire col 2) print(scores[idx]) # [55 45 70]
5
Array Reshaping
• reshape() lets you change the shape of an array without changing its data
• The total number of elements must stay the same (e.g., 6 elements: 2x3 or 3x2)
• Use -1 in reshape() to let NumPy figure out one dimension automatically
• flatten() and ravel() both convert multi-dimensional arrays into 1D
import numpy as np # flatten (returns a copy)
d = [Link]()
a = [Link](1, 13) # [1..12] print(d) # [ 1 2 3 ... 12]
print([Link]) # (12,)
# ravel (returns a view if possible)
# Reshape to 3 rows x 4 cols e = [Link]()
b = [Link](3, 4)
print(b) # Transpose: swap rows and cols
# [[ 1 2 3 4] f = b.T
# [ 5 6 7 8] print([Link]) # (4, 3)
# [ 9 10 11 12]]
# Add a new axis
# Use -1: NumPy infers the dimension g = [Link](1, -1) # (1, 12) - row vector
c = [Link](4, -1) # (4, 3) h = [Link](-1, 1) # (12, 1) - col vector
print([Link]) # (4, 3) print([Link]) # (12, 1)
6
Broadcasting
• Broadcasting lets NumPy perform operations on arrays of different shapes
• Instead of writing a loop, NumPy automatically stretches the smaller array
• Rule: dimensions are compared from the right; they must be equal or one of them must be 1
• Broadcasting makes code shorter and much faster than using Python for-loops
import numpy as np # Column vector broadcast
col = [Link]([[100],
# Scalar broadcast to entire array [200]]) # shape (2,1)
a = [Link]([1, 2, 3, 4]) print(m + col)
print(a + 10) # [11 12 13 14] # [[101 102 103]
print(a * 2) # [ 2 4 6 8] # [204 205 206]]
# 1D array + 2D array # Without broadcasting (slow loop)
m = [Link]([[1,2,3], result = []
[4,5,6]]) for row in m:
v = [Link]([10, 20, 30]) # shape (3,) [Link](row + v)
# v is broadcast across each row of m # With broadcasting (fast, one line)
print(m + v) result = m + v
# [[11 22 33] # Much faster for large arrays!
# [14 25 36]]
7
Vectorization
• Vectorization means applying an operation to an entire array at once , no Python loop needed
• NumPy's internal operations are written in C, making them much faster than Python loops
• Universal functions (ufuncs) like [Link], [Link], [Link] work element-wise on arrays
• Always prefer vectorized operations over loops when working with large data
import numpy as np # Common ufuncs
import time a = [Link]([1.0, 4.0, 9.0, 16.0])
# Slow Python loop print([Link](a)) # [1. 2. 3. 4.]
data = list(range(1_000_000)) print([Link](a)) # natural log
start = [Link]() print([Link](a)) # e^x
result = [x**2 for x in data] print([Link](a)) # absolute value
print(f'Loop: {[Link]()-start:.3f}s')
# Comparison returns boolean array
# Fast NumPy vectorization b = [Link]([3, 1, 4, 1, 5, 9])
arr = [Link](1_000_000) print(b > 3) # [F F T F T T]
start = [Link]() print([Link](b > 3)) # 3 (count True)
result = arr ** 2
print(f'NumPy: {[Link]()-start:.3f}s') # Apply custom function
# NumPy is 50-200x faster! f = [Link](lambda x: x*2 + 1)
print(f(b))
8
Mathematical Operations on Arrays
• NumPy supports all basic arithmetic (+, -, *, /) element-wise on arrays
• Aggregate functions like sum, mean, std, min, max work on the whole array or along an axis
• Use the axis parameter to compute along rows (axis=1) or columns (axis=0)
• [Link]() is a vectorized if-else that works element-wise across an array
import numpy as np # Aggregate along axes
print([Link](a, axis=0)) # [5 7 9] col sums
a = [Link]([[1, 2, 3], print([Link](a, axis=1)) # [6 15] row sums
[4, 5, 6]]) print([Link](a, axis=1)) # [3 6] row maxes
# Element-wise operations # [Link] (vectorized if-else)
print(a + a) # [[2,4,6],[8,10,12]] scores = [Link]([45, 80, 60, 30, 95])
print(a * 3) # [[3,6,9],[12,15,18]] grades = [Link](scores >= 60,
print(a ** 2) # [[1,4,9],[16,25,36]] 'Pass', 'Fail')
print(grades)
# Aggregate over entire array # ['Fail' 'Pass' 'Pass' 'Fail' 'Pass']
print([Link](a)) # 21
print([Link](a)) # 3.5 # Clip values between min and max
print([Link](a)) # standard deviation print([Link](scores, 50, 90))
# [50 80 60 50 90]
9
Linear Algebra Operations
• NumPy has a dedicated linear algebra module [Link] for matrix operations
• Matrix multiplication uses @ operator or [Link]() , not * (which is element-wise)
• [Link]() finds the inverse of a matrix , [Link]() finds the determinant
• These operations are essential in AI: neural networks use matrix multiply every forward pass
import numpy as np # Determinant
print([Link](A)) # -2.0
A = [Link]([[1, 2],
[3, 4]]) # Inverse
B = [Link]([[5, 6], inv_A = [Link](A)
[7, 8]]) print(inv_A)
# [[-2. 1. ]
# Matrix multiplication # [ 1.5 -0.5]]
C = A @ B
print(C) # Eigenvalues and eigenvectors
# [[19 22] vals, vecs = [Link](A)
# [43 50]] print(vals) # eigenvalues
# Dot product of vectors # Solve linear system Ax = b
u = [Link]([1, 2, 3]) b = [Link]([1, 2])
v = [Link]([4, 5, 6]) x = [Link](A, b)
10
Random Number Generation for AI
• [Link] module provides tools to generate random numbers for simulations and AI experiments
• Always set a seed with [Link]() to make experiments reproducible
• randn() generates numbers from a normal (Gaussian) distribution , common for weight initialization
• randint() and choice() are useful for sampling data and shuffling datasets
import numpy as np # Normal dist with custom mean/std
weights = [Link](0, 0.01, (4,2))
# Set seed for reproducibility print([Link]) # (4, 2)
[Link](42)
# Random choice from an array
# Uniform random floats [0, 1) data = [Link]([10, 20, 30, 40, 50])
a = [Link](3, 3) sample = [Link](data, size=3)
print(a) print(sample) # random 3 elements
# Normal distribution (mean=0, std=1) # Shuffle an array in-place
b = [Link](4) arr = [Link](10)
print(b) # e.g. [-0.47, 1.23, ...] [Link](arr)
print(arr) # shuffled order
# Random integers
c = [Link](0, 10, size=5) # Split into train/test (80/20)
print(c) # e.g. [3 7 1 8 4] train = arr[:8]
11
Distributions Used in AI
• Different distributions are used at different stages of building AI models
• Xavier / He initialization uses normal or uniform distributions to prevent vanishing gradients
• Bernoulli distribution models binary outcomes , useful for dropout in neural networks
• [Link] (new API) is preferred in modern NumPy for better randomness control
import numpy as np # Binomial: dropout mask (keep 80%)
keep_prob = 0.8
rng = [Link].default_rng(seed=0) mask = [Link](1, keep_prob, (3,4))
print(mask)
# Xavier initialization (uniform) # [[1 1 0 1]
fan_in, fan_out = 4, 8 # [1 1 1 0]
limit = [Link](6 / (fan_in + fan_out)) # [1 0 1 1]]
W = [Link](-limit, limit, (fan_in, fan_out))
print([Link]) # (4, 8) # Apply dropout
activations = [Link](3, 4)
# He initialization (normal) dropped = activations * mask / keep_prob
std = [Link](2 / fan_in)
W_he = [Link](0, std, (fan_in, fan_out)) # Permutation (new shuffled copy)
print(W_he.mean()) # close to 0 data = [Link](10)
shuffled = [Link](data)
print(shuffled) # original unchanged
12
Performance Optimization Techniques
• Avoid Python loops , always use vectorized NumPy operations instead
• Use in-place operations (+=, *=) to avoid creating new arrays and save memory
• Choose the right dtype , float32 uses half the memory of float64 and is faster on GPUs
• [Link]() is a powerful tool for expressing complex tensor operations in one line
import numpy as np # [Link] for matrix ops
import time A = [Link](100, 200)
B = [Link](200, 150)
n = 1_000_000
a = [Link](n) # Matrix multiply using einsum
b = [Link](n) C = [Link]('ij,jk->ik', A, B)
print([Link]) # (100, 150)
# In-place is faster (no new array)
a += b # faster than a = a + b # Batch dot product
u = [Link](50, 3)
# Memory-efficient dtype v = [Link](50, 3)
big = [Link]((1000, 1000), dtype=np.float64) dots = [Link]('ij,ij->i', u, v)
small = [Link](np.float32) print([Link]) # (50,)
print([Link]) # 8,000,000
print([Link]) # 4,000,000 # Contiguous memory check
print([Link]['C_CONTIGUOUS']) # True
13
Views, Copies, and Memory Tips
• NumPy slice operations return a view , not a copy , changes to the view affect the original
• Use .copy() when you need an independent copy of an array
• [Link]() ensures memory is laid out row-by-row, improving cache performance
• Pre-allocate output arrays with [Link]() when running many repeated operations in a loop
import numpy as np # Pre-allocate output array
out = [Link](1_000_000)
a = [Link](10) for i in range(10):
data = [Link](1_000_000)
# Slice is a VIEW (shares memory) [Link](data, 2, out=out)
b = a[2:5]
b[0] = 99 # Check memory layout
print(a) # [0 1 99 3 4 5 6 7 8 9] x = [Link](3, 4)
print([Link]['C_CONTIGUOUS']) # row-major
# Use .copy() for independence
c = a[2:5].copy() # Force contiguous (good for C libs)
c[0] = 0 y = [Link](x.T)
print(a) # original unchanged
# Profile with timeit
# Check if two arrays share memory # python -m timeit -s 'import numpy as np;
print(np.shares_memory(a, b)) # True # a=[Link](1000000)'
14
NumPy vs Python Lists
• Python lists are flexible but slow , they store pointers to objects scattered in memory
• NumPy arrays store data in a contiguous block of memory , making access much faster
• Python lists can hold mixed types , NumPy arrays must have all elements of the same type
• For math and data processing, NumPy is almost always faster (10x to 200x) than plain Python
import numpy as np, time # Memory comparison
import sys
N = 1_000_000
py_list = list(range(1000))
# Python list: multiply each element by 2 np_arr = [Link](1000, dtype=np.int64)
py_list = list(range(N))
t1 = [Link]() # Python list (each element is an object)
result = [x * 2 for x in py_list] py_mem = [Link](py_list)
print(f'List: {[Link]()-t1:.4f}s') print(f'List: {py_mem} bytes')
# NumPy: same operation # NumPy array (raw data only)
np_arr = [Link](N) np_mem = np_arr.nbytes
t2 = [Link]() print(f'NumPy: {np_mem} bytes')
result = np_arr * 2
print(f'NumPy: {[Link]()-t2:.4f}s') # Summary:
# NumPy is ~100x faster! # Lists: flexible but slow and big
15
Assessment
• Q1. Create a 4x4 NumPy array of random integers (1-100). Find the mean of
each row.
• Q2. Given array a = [1,2,3,4,5,6,7,8,9,10], reshape it to (2,5), then select all
values greater than 5.
• Q3. Using broadcasting, add the vector [10, 20, 30] to each row of a 4x3 matrix.
• Q4. Write a function that initializes weights using He initialization (normal,
mean=0, std=sqrt(2/fan_in)).
• Q5. Compare the speed of [Link](A, B) vs a nested Python loop for multiplying
two 500x500 matrices.
• Q6. Explain with code why [Link] slices are views, and how to avoid accidental
modification.
16
Complete Example , NumPy in Action
• Let’s put everything together in one practical workflow
• We generate fake student data, normalize it, apply a linear model, and
evaluate
• This mirrors the core steps in any machine learning pipeline
import numpy as np # 5. Gradient descent (10 steps)
lr = 0.1
[Link](42) for _ in range(10):
y_pred = X @ W
# 1. Generate data: 100 students, 3 features error = y_pred - y
X = [Link](100, 3) grad = X.T @ error / 100
W -= lr * grad
# 2. Normalize each feature (mean=0, std=1)
X = (X - [Link](axis=0)) / [Link](axis=0) # 6. Evaluate
y_pred = X @ W
# 3. True weights + noise mse = [Link]((y_pred - y)**2)
W_true = [Link]([2.0, -1.0, 0.5]) print(f'MSE: {mse:.4f}')
y = X @ W_true + [Link](100)*0.1 print(f'Learned W: {W}')
print(f'True W: {W_true}')
# 4. Initialize random weights # MSE should be close to 0.01
W = [Link](3) * 0.01
17
The End
18