An Introduction to Tensorflow and Tensors

Rather than building machine learning and deep learning models from "scratch", tensorflow contains many of the most common machine learning functions you'll want to use.

Goals

- creating tensors

  • Getting information from tensors
  • Manipulating tensors
  • Comparing & Combining Tensorflow, Tensors, and NumPy
  • Using @tf.function as a way to speed up Python functions
  • Using GPUs with TensorFlow

Imports

In [1]:
import datetime
import numpy as np
import tensorflow as tf
print(f'tensorlfow version: {tf.__version__}') # find the version number (should be 2.x+)
2025-02-05 12:41:16.756317: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: SSE4.1 SSE4.2, in other operations, rebuild TensorFlow with the appropriate compiler flags.
tensorlfow version: 2.12.0

Tensors

Like Numpy Arrays

NumPy arrays are similar to tensors. One major difference between tensors and NumPy arrays (also an n-dimensional array of numbers) is that tensorflow (and tensors) can be used & processed on GPUs (graphical processing units) and TPUs (tensor processing units). One benefit of being able to run on GPUs and TPUs is faster computation. GPUs and TPUs will process data faster than CPUs.

Tensors are like multi-dimensional numerical representations (also referred to as n-dimensional, where n can be any number) of things:

  • numbers themselves, using tensors to represent the price of houses)
  • images, using tensors to represent the pixels of an image)
  • text, using tensors to represent words

Working With Tensors

Creating Tensors with tf.constant()

Creating tensors, "from scratch", may not be common. TensorFlow has modules built-in (such as tf.io and tf.data) which are able to read input data sources and automatically convert them to tensors. Here, though, a look at creating tensors with tf.constant().

A scalar is known as a "rank 0" tensor. Scalars have no dimensions - just a single number.

By default, TensorFlow creates tensors with either an int32 or float32 datatype.

This is known as [32-bit precision](https://en.wikipedia.org/wiki/Precision_(computer_science) (the higher the number, the more precise the number, the more space it takes up on your computer).

There are different types of numbers and number datatypes:

  • scalar: a single number.
  • vector: a number with direction (e.g. wind speed with direction).
  • matrix: a 2-dimensional array of numbers.
  • tensor: an n-dimensional arrary of numbers (where n can be any number, a 0-dimension tensor is a scalar, a 1-dimension tensor is a vector).

"Matrix" and "Tensor" may be used interchangably.

For more on the mathematical difference between scalars, vectors and matrices see the visual algebra post by Math is Fun.

Scalar

In [2]:
# Create a scalar (rank 0 tensor)
scalar = tf.constant(7)
print(f'scalar tensor: {scalar}')
scalar
scalar tensor: 7
2025-02-05 12:41:32.290387: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:306] Could not identify NUMA node of platform GPU ID 0, defaulting to 0. Your kernel may not have been built with NUMA support.
2025-02-05 12:41:32.290439: I tensorflow/core/common_runtime/pluggable_device/pluggable_device_factory.cc:272] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 0 MB memory) -> physical PluggableDevice (device: 0, name: METAL, pci bus id: <undefined>)
Out [2]:
<tf.Tensor: shape=(), dtype=int32, numpy=7>
In [3]:
# Check the number of dimensions of a tensor (ndim stands for number of dimensions)
scalar.ndim
Out [3]:
0
In [4]:
# Create a vector (more than 0 dimensions)
vector = tf.constant([10, 10])
vector
Out [4]:
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([10, 10], dtype=int32)>
In [5]:
# Check the number of dimensions of our vector tensor
vector.ndim
Out [5]:
1
In [6]:
# Create a matrix (more than 1 dimension)
matrix = tf.constant([[10, 7],
                      [7, 10]])
matrix
Out [6]:
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[10,  7],
       [ 7, 10]], dtype=int32)>
In [7]:
matrix.ndim
Out [7]:
2
In [8]:
# Create another matrix and define the datatype
another_matrix = tf.constant([[10., 7.],
                              [3., 2.],
                              [8., 9.]], dtype=tf.float16) # specify the datatype with 'dtype'
another_matrix
Out [8]:
<tf.Tensor: shape=(3, 2), dtype=float16, numpy=
array([[10.,  7.],
       [ 3.,  2.],
       [ 8.,  9.]], dtype=float16)>
In [9]:
# Even though another_matrix contains more numbers than matrix, its 'ndim' (dimension count) is the same:
another_matrix.ndim
Out [9]:
2
In [10]:
# How about a tensor with more than 2 dimensions
tensor = tf.constant([[[1, 2, 3],
                       [4, 5, 6]],
                      [[7, 8, 9],
                       [10, 11, 12]],
                      [[13, 14, 15],
                       [16, 17, 18]]])
tensor
Out [10]:
<tf.Tensor: shape=(3, 2, 3), dtype=int32, numpy=
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]],

       [[13, 14, 15],
        [16, 17, 18]]], dtype=int32)>
In [11]:
tensor.ndim
Out [11]:
3

On Tensor Dimensions

This tensor is known as a rank 3 tensor (3-dimensions):

tensor = tf.constant([[[1, 2, 3],
                       [4, 5, 6]],
                      [[7, 8, 9],
                       [10, 11, 12]],
                      [[13, 14, 15],
                       [16, 17, 18]]])

Dimension Count from Source Data

Tensors may be created based on a series of images:

  • image-width of 224
  • image-height of 224
  • 3 color channels: (red, green blue)
  • instruct tensorflow to process 32 images at-a-time, in a "batch" of 32 That tensor might have a shape (224, 224, 3, 32)

Squeezing A Tensor

* tf.squeeze() - remove all dimensions of 1 from a tensor.

In [12]:
# Create a rank 5 (5 dimensions) tensor of 50 numbers between 0 and 100
G = tf.constant(np.random.randint(0, 100, 50), shape=(1, 1, 1, 1, 50))
print(f'G Shape: {G.shape}')
print(f'G Dimension count: {G.ndim}')

# Squeeze tensor G (remove all 1 dimensions)
G_squeezed = tf.squeeze(G)
print(f'SQUEEZED G Shape: {G_squeezed.shape}')
print(f'SQUEEZED G Dimension count: {G_squeezed.ndim}')
G Shape: (1, 1, 1, 1, 50)
G Dimension count: 5
SQUEEZED G Shape: (50,)
SQUEEZED G Dimension count: 1

Creating Tensors with tf.Variable()

Often, when working with data, tensors are created automatically. Here, creating tensors using tf.Variable().

Constants are Immutable, Variables are mutable

The difference between tf.Variable() and tf.constant() is tensors created with tf.constant() are immutable (can't be changed, can only be used to create a new tensor), where as, tensors created with tf.Variable() are mutable (can be changed).

To change an element of a tf.Variable() tensor requires the assign() method.

In [13]:
# Create the same tensor with tf.Variable() and tf.constant()
changeable_tensor = tf.Variable([10, 7])
unchangeable_tensor = tf.constant([10, 7])
print(changeable_tensor)
print(unchangeable_tensor)
<tf.Variable 'Variable:0' shape=(2,) dtype=int32, numpy=array([10,  7], dtype=int32)>
tf.Tensor([10  7], shape=(2,), dtype=int32)
In [14]:
# 
# FAILING at changing a Variable  without ".assign()"
# 
# Will error (requires the .assign() method)
# changeable_tensor[0] = 7
# changeable_tensor

# will return
# TypeError: 'ResourceVariable' object does not support item assignment
In [15]:
# 
# Success changing a tensor
# 
changeable_tensor[0].assign(7)
changeable_tensor
Out [15]:
<tf.Variable 'Variable:0' shape=(2,) dtype=int32, numpy=array([7, 7], dtype=int32)>
In [16]:
# 
# FAILURE attemtpting to change a constant tensor
# 
# unchangeable_tensor[0].assign(7)
# unchangleable_tensor

# will return
# AttributeError: 'tensorflow.python.framework.ops.EagerTensor' object has no attribute 'assign'

Creating random tensors

Random tensors are tensors of some abitrary size which contain random numbers.

Why would you want to create random tensors?

Random tensors are what neural networks use to intialize their weights when recognizing (patterns) in data. The process of a neural network learning might often involve taking a random n-dimensional array of numbers and refining them until they represent some kind of pattern (a compressed way to represent the original data).

Here, creating random tensors by using the tf.random.Generator class.

In [17]:
# Create two random (but the same) tensors
random_1 = tf.random.Generator.from_seed(42) # set the seed for reproducibility
random_1 = random_1.normal(shape=(3, 2)) # create tensor from a normal distribution 
random_2 = tf.random.Generator.from_seed(42)
random_2 = random_2.normal(shape=(3, 2))

# Are they equal?
print(random_1)
print(random_2)
print(f'random_1 == random_2: {random_1 == random_2}')
tf.Tensor(
[[-0.75658023 -0.06854693]
 [ 0.07595028 -1.2573844 ]
 [-0.23193759 -1.8107857 ]], shape=(3, 2), dtype=float32)
tf.Tensor(
[[-0.75658023 -0.06854693]
 [ 0.07595028 -1.2573844 ]
 [-0.23193759 -1.8107857 ]], shape=(3, 2), dtype=float32)
random_1 == random_2: [[ True  True]
 [ True  True]
 [ True  True]]

The random tensors here are pseudorandom numbers (they appear as random, but really aren't). Once a seed is set,tensorflow will generate the same random numbers.

In [18]:
# Create two random (and different) tensors
random_3 = tf.random.Generator.from_seed(42)
random_3 = random_3.normal(shape=(3, 2))
random_4 = tf.random.Generator.from_seed(11)
random_4 = random_4.normal(shape=(3, 2))

# Check the tensors and see if they are equal
# Are they equal?
print(random_3)
print(random_4)
print(f'random_3 == random_4: {random_3 == random_4}')
tf.Tensor(
[[-0.75658023 -0.06854693]
 [ 0.07595028 -1.2573844 ]
 [-0.23193759 -1.8107857 ]], shape=(3, 2), dtype=float32)
tf.Tensor(
[[ 0.27305746 -0.29925638]
 [-0.36523244  0.61883324]
 [-1.0130817   0.28291693]], shape=(3, 2), dtype=float32)
random_3 == random_4: [[False False]
 [False False]
 [False False]]
In [19]:
# Shuffle a tensor (valuable for when you want to shuffle your data)
not_shuffled = tf.constant([[10, 7],
                            [3, 4],
                            [2, 5]])
# Gets different results each time
tf.random.shuffle(not_shuffled)
Out [19]:
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[ 3,  4],
       [10,  7],
       [ 2,  5]], dtype=int32)>
In [20]:
# Shuffle in the same order every time using the seed parameter (won't acutally be the same)
tf.random.shuffle(not_shuffled, seed=42)
Out [20]:
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[ 2,  5],
       [ 3,  4],
       [10,  7]], dtype=int32)>

Rule #4 of the tf.random.set_seed() documentation says that

"4. If both the global and the operation seed are set: Both seeds are used in conjunction to determine the random sequence."

tf.random.set_seed(42) sets the global seed, and the seed parameter in tf.random.shuffle(seed=42) sets the operation seed.

Because, "Operations that rely on a random seed actually derive it from two seeds: the global and operation-level seeds. This sets the global seed."

In [21]:
# Shuffle in the same order every time

# Set the global random seed
tf.random.set_seed(42)

# Set the operation random seed
tf.random.shuffle(not_shuffled, seed=42)
Out [21]:
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[10,  7],
       [ 3,  4],
       [ 2,  5]], dtype=int32)>
In [22]:
# Set the global random seed
tf.random.set_seed(42) # if you comment this out you'll get different results

# Set the operation random seed
tf.random.shuffle(not_shuffled)
Out [22]:
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[ 3,  4],
       [ 2,  5],
       [10,  7]], dtype=int32)>

Creating Tensors: Ones and Zeros

In [23]:
# Make a tensor of all ones
print(tf.ones(shape=(3, 2)))

# Make a tensor of all zeros
print(tf.zeros(shape=(3, 2)))
tf.Tensor(
[[1. 1.]
 [1. 1.]
 [1. 1.]], shape=(3, 2), dtype=float32)
tf.Tensor(
[[0. 0.]
 [0. 0.]
 [0. 0.]], shape=(3, 2), dtype=float32)
In [24]:
numpy_A = np.arange(1, 25, dtype=np.int32) # create a NumPy array between 1 and 25
A = tf.constant(numpy_A,  
                shape=[2, 4, 3]) # note: the shape total (2*4*3) has to match the number of elements in the array
numpy_A, A
Out [24]:
(array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17,
        18, 19, 20, 21, 22, 23, 24], dtype=int32),
 <tf.Tensor: shape=(2, 4, 3), dtype=int32, numpy=
 array([[[ 1,  2,  3],
         [ 4,  5,  6],
         [ 7,  8,  9],
         [10, 11, 12]],
 
        [[13, 14, 15],
         [16, 17, 18],
         [19, 20, 21],
         [22, 23, 24]]], dtype=int32)>)
In [25]:
# Create a new tensor with default datatype (float32)
B = tf.constant([1.7, 7.4])

# Create a new tensor with default datatype (int32)
C = tf.constant([1, 7])
print(f'B.dtype: {B.dtype}')
print(f'C.dtype: {C.dtype}')
B.dtype: <dtype: 'float32'>
C.dtype: <dtype: 'int32'>
In [26]:
# Change from float32 to float16 (reduced precision)
B = tf.cast(B, dtype=tf.float16)
print(f'B.dtype: {B.dtype}')
B.dtype: <dtype: 'float16'>
In [27]:
# Change from int32 to float32
C = tf.cast(C, dtype=tf.float32)
print(f'C.dtype: {C.dtype}')
C.dtype: <dtype: 'float32'>

Getting information from tensors

Attributes

Common bits to get about a tensor:

  • Shape: The length (number of elements) of each of the dimensions of a tensor
  • Rank: The number of tensor dimensions:
    • A scalar has rank 0
    • a vector has rank 1
    • a matrix is rank 2
    • a tensor has rank n
  • Axis or Dimension: A particular dimension of a tensor
  • Size: The total number of items in the tensor

These might be especially useful when trying to organize the shapes of input data to inform/match the shapes of a model. I.E, , making sure the shape of image tensors are the same shape as a models input layer.

In [28]:
# Create a rank 4 tensor (4 dimensions)
rootDim = 2
nestedOneDim = 3
nestedTwoDim = 4
nestedThreeDim = 5
rank_4_tensor = tf.zeros([rootDim, nestedOneDim, nestedTwoDim, nestedThreeDim])
rank_4_tensor
Out [28]:
<tf.Tensor: shape=(2, 3, 4, 5), dtype=float32, numpy=
array([[[[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]]],


       [[[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.],
         [0., 0., 0., 0., 0.]]]], dtype=float32)>
In [29]:
rank_4_tensor.shape, rank_4_tensor.ndim, tf.size(rank_4_tensor)
Out [29]:
(TensorShape([2, 3, 4, 5]), 4, <tf.Tensor: shape=(), dtype=int32, numpy=120>)
In [30]:
# Get various attributes of tensor
print("Datatype of every element:", rank_4_tensor.dtype)
print("Number of dimensions (rank):", rank_4_tensor.ndim)
print("Shape of tensor:", rank_4_tensor.shape)
print("Elements along axis 0 of tensor:", rank_4_tensor.shape[0])
print("Elements along last axis of tensor:", rank_4_tensor.shape[-1])
print("Total number of elements (2*3*4*5):", tf.size(rank_4_tensor).numpy()) # .numpy() converts to NumPy array
Datatype of every element: <dtype: 'float32'>
Number of dimensions (rank): 4
Shape of tensor: (2, 3, 4, 5)
Elements along axis 0 of tensor: 2
Elements along last axis of tensor: 5
Total number of elements (2*3*4*5): 120
In [31]:
# Get the first 2 items of each dimension
rank_4_tensor[:2, :2, :2, :2]
Out [31]:
<tf.Tensor: shape=(2, 2, 2, 2), dtype=float32, numpy=
array([[[[0., 0.],
         [0., 0.]],

        [[0., 0.],
         [0., 0.]]],


       [[[0., 0.],
         [0., 0.]],

        [[0., 0.],
         [0., 0.]]]], dtype=float32)>
In [32]:
# Get the dimension from each index except for the final one
rank_4_tensor[:1, :1, :1, :]
Out [32]:
<tf.Tensor: shape=(1, 1, 1, 5), dtype=float32, numpy=array([[[[0., 0., 0., 0., 0.]]]], dtype=float32)>
In [33]:
# Create a rank 2 tensor (2 dimensions)
rank_2_tensor = tf.constant([[10, 7],
                             [3, 4]])

# Get the last item of each row
rank_2_tensor[:, -1]
Out [33]:
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([7, 4], dtype=int32)>

Manipulating tensors (tensor operations)

Finding patterns in tensors (numberical representation of data) requires manipulating them.

Again, when building models in TensorFlow, much of this pattern discovery is done for you.

Add A Dimension with newaxis and expanddims

Add dimensions to a tensor whilst keeping the same information present using tf.newaxis

In [34]:
# Add an extra dimension (to the end)
rank_3_tensor = rank_2_tensor[..., tf.newaxis] # in Python "..." means "all dimensions prior to"
print('---- rank_2_tensor ----')
print(rank_2_tensor)
print('---- rank_3_tensor ----')
print(rank_3_tensor)
---- rank_2_tensor ----
tf.Tensor(
[[10  7]
 [ 3  4]], shape=(2, 2), dtype=int32)
---- rank_3_tensor ----
tf.Tensor(
[[[10]
  [ 7]]

 [[ 3]
  [ 4]]], shape=(2, 2, 1), dtype=int32)
In [35]:
# 
# expand_dims
# 
tf.expand_dims(rank_2_tensor, axis=-1) # "-1" means last axis
Out [35]:
<tf.Tensor: shape=(2, 2, 1), dtype=int32, numpy=
array([[[10],
        [ 7]],

       [[ 3],
        [ 4]]], dtype=int32)>

Add

Here, creating a tensor with tf.constant(), adding 10, and seeing that the original tensor is unchanged (the addition gets done on a copy).

In [36]:
# You can add values to a tensor using the addition operator
tensor = tf.constant([[10, 7], [3, 4]])
print(tensor + 10)
print(tensor)
tf.Tensor(
[[20 17]
 [13 14]], shape=(2, 2), dtype=int32)
tf.Tensor(
[[10  7]
 [ 3  4]], shape=(2, 2), dtype=int32)
In [37]:
print(tensor * 10)

# Use the tensorflow function equivalent of the '*' (multiply) operator
print(tf.multiply(tensor, 10))
tf.Tensor(
[[100  70]
 [ 30  40]], shape=(2, 2), dtype=int32)
tf.Tensor(
[[100  70]
 [ 30  40]], shape=(2, 2), dtype=int32)
In [38]:
tensor - 10
Out [38]:
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[ 0, -3],
       [-7, -6]], dtype=int32)>
Page Tags:
python
data-science
jupyter
learning
numpy