0% found this document useful (0 votes)
3 views18 pages

PAI - Lecture 10 - NumPy Library in Python

The document is a course outline for 'NumPy for Numerical Computing' focusing on various aspects of the NumPy library essential for AI programming. It covers topics such as array creation, indexing, mathematical operations, linear algebra, random number generation, and performance optimization techniques. The document emphasizes the advantages of using NumPy over Python lists and provides code examples for practical understanding.

Uploaded by

hamzahanif2874
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views18 pages

PAI - Lecture 10 - NumPy Library in Python

The document is a course outline for 'NumPy for Numerical Computing' focusing on various aspects of the NumPy library essential for AI programming. It covers topics such as array creation, indexing, mathematical operations, linear algebra, random number generation, and performance optimization techniques. The document emphasizes the advantages of using NumPy over Python lists and provides code examples for practical understanding.

Uploaded by

hamzahanif2874
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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

You might also like