Introduction to tensor


Definition

In TensorFlow, a tensor is a fundamental data structure representing multi-dimensional arrays or vectors. It's similar to NumPy arrays but designed specifically for deep learning computations. Tensors are the basic building blocks of TensorFlow computations.

Key concept related to tensors in TensorFlow:

Rank: The rank of a tensor refers to the number of dimensions it has. A rank-0 tensor is a scalar (a single value), a rank-1 tensor is a vector (one-dimensional array), a rank-2 tensor is a matrix (two-dimensional array), and so on.

Shape: The shape of a tensor defines the number of elements in each dimension. For example, a tensor with shape [3, 4] represents a matrix with 3 rows and 4 columns.

Data Types: TensorFlow tensors can have different data types such as float32, int32, or string, among others. The data type of a tensor determines the type of data it can hold.

Operations: TensorFlow provides a wide range of operations (ops) for manipulating tensors, such as addition, multiplication, reshaping, and more. These operations can be performed on tensors to perform various computations in neural networks.

import numpy as np
import tensorflow as tf

Basic Tensors

rank-0 or scalar tensor. The scalar tensor have no axes, it represent single value.

tensor_0 = tf.constant(4) # default dtype is int32

print(tensor_0) 

A one dimensional list of values is called as rank-1 tesnor or simply called as vector.

tensor_1 = tf.constant([2.0,3.0,4.0,5.0])
print(tensor_1)

A tensor with 2-axes is called as matrix or rank-2 tensors.

tensor_2 = tf.constant([
    [1,2],
    [3,4],
    [5,6]
])
print(tensor_2)

shape of all these tensors:

print(tensor_0.shape)
print(tensor_1.shape)
print(tensor_2.shape)

A tensor can have more than 2-axes

# A tensor can have arbitary number of axes also called as dimension of tensor.
tensor_3 = tf.constant([
    [[1,2],[3,4]],
    [[5,6],[7,8]],
    [[9,10],[11,12]]
])

print(tensor_3)

print(tensor_3.shape)

we can convert tensor into numpy array using following method:

print(np.array(tensor_2)) # using np.array()

# or we can use tensor.numpy()
x = tensor_2.numpy()
print(x)
print(type(x))

Basic Math operations on tensor

Addition

x = tf.constant([
    [1,2],
    [3,4]
    ])
y = tf.constant([
    [10,11],
    [12,13]
])

a = tf.add(x,y)
print(a)

element-wise multiplication

b = tf.multiply(x,y)
print(b)

Matrix Multiplication

c = tf.matmul(x,y)
print(c)

The above operations can be done in following ways too:

print('Addition')
print(x+y)
print('Multiplication')
print(x*y)
print('Matrix Multiplication')
print(x@y)

We can perform various types of other operations i.e ops on tensors

a = tf.constant([[5.0,6.0],[8.0,10.5]])

find largest value in given matrix

print(tf.reduce_max(a))

find the index of largest value

print(tf.math.argmax(a))

compute the softmax of given matrix

sft = tf.nn.softmax(a)
print(sft)

About tensor shapes:

Tensors have shapes. Some vocabulary:

Shape: The length (number of elements) of each of the axes of a tensor.

Rank: Number of tensor axes. A scalar has rank 0, a vector has rank 1, a matrix is rank 2.

Axis or Dimension: A particular dimension of a tensor.

Size: The total number of items in the tensor, the product of the shape vector's elements.

Understanding higher dimensional tensor

tensor_4 = tf.zeros([3,2,4,5])

print(tensor_4)

print("Type of every element:", tensor_4.dtype)
print("Number of axes:", tensor_4.ndim)
print("Shape of tensor:", tensor_4.shape)
print("Elements along axis 0 of tensor:", tensor_4.shape[0])
print("Elements along the last axis of tensor:", tensor_4.shape[-1])
print("Total number of elements (3*2*4*5): ", tf.size(tensor_4).numpy())
Generally, in deep learning models a higher dimensional tensor as below, can be understood as:

x = tf.zeros([3 ,2, 4, :5])

here,

3 => Batch

2 => Height

4 => width

5 => number of features 

Indexing

Single-axis indexing

TensorFlow follows standard Python indexing rules, similar to indexing a list or a string in Python, and the basic rules for NumPy indexing.

indexes start at 0

negative indices count backwards from the end

colons, :, are used for slices: start:stop:step

tensor_1 = tf.constant([1,2,3,4,5,6,7,8,9,10])

print(tensor_1.numpy())

print(tensor_1[1].numpy())
print(tensor_1[-1].numpy())
print(tensor_1[5].numpy())

# slicing operations
print(tensor_1[2:7].numpy())
print(tensor_1[5:].numpy())

Multi-axis indexing

indexing multi-axis is sames as single axis, whereas in multi-axis indexing rules applies independently to each of its axis

a = tf.constant([[1.0,2.0,3.0],[4.0,5.0,6.0]])
print(a.numpy())

print(a[1,1].numpy())
print(a[1:,1:].numpy())
print(a[0,:].numpy())
print(a[:,0].numpy())

Manipulating shapes of tensor

x = tf.constant([[10],[20],[30]])

print(x)
print(x.shape)
y = tf.reshape(x,(1,3))
print(y.numpy())
print(y.shape)

reshaping rank-3 tensor

x = tf.constant([[[1,2,3,4],[5,6,7,8]],[[10,20,30,40],[50,60,70,80]]])
print(x)

print(x.shape)

let us flatten the tensor:

y = tf.reshape(x,[-1])
print(y.numpy())

z = tf.reshape(x,[2,-1])
print(z)

print(tf.reshape(x,[2*2,-1]).numpy())
Note: Reshaping will "work" for any new shape with the same total number of elements, but it will not do anything useful if you do not respect the order of the axes.
try:
  tf.reshape(x,[7,-1])
except Exception as e:
  print(e)

Tensor DType

when creating tensor from python object, optionally specify the data type

a = tf.constant([10,20,30]) # default int32
print(a.dtype)

b = tf.constant([1.0,2.0,3.0])
print(b.dtype)

# specify dtype as argument
x = tf.constant([10,20,30],dtype=tf.int64)
print(x)
print(x.dtype)

y = tf.constant([5.56,7.89,3.20,4.0],dtype=tf.float16)
print(y)

typecasting tensor

a = tf.cast(y,dtype=tf.int16) # results in data loss
print(a)

Broadcasting

Broadcasting is a concept borrowed from the equivalent feature in NumPy. In short, under certain conditions, smaller tensors are "stretched" automatically to fit larger tensors when running combined operations on them.

a = tf.constant([4,5,6])
b = tf.constant([3])
c = tf.constant([8,9,10])

print(tf.multiply(a,b))
print(tf.add(a,b))

The simplest and most common case is when you attempt to multiply or add a tensor to a scalar. In that case, the scalar is broadcast to be the same shape as the other argument.

Ragged Tensors

A tensor with variable numbers of elements along some axis is called "ragged". Use tf.ragged.RaggedTensor for ragged data.

ragged_l = [
    [10,20,30],
    [40],
    [50,60]
]

try:
  r = tf.constant(ragged_l)
except Exception as e:
  print(e)

Can't convert non-rectangular Python sequence to Tensor.

#instead we can use ragged tensor as follows:
try:
  r = tf.ragged.constant(ragged_l)
  print(r)
except Exception as e:
  print(e)

# The shape of a tf.RaggedTensor will contain some axes with unknown lengths:

print(r.shape)

String tensors

tf.string is a dtype, which is to say you can represent data as strings (variable-length byte arrays) in tensors.The strings are atomic and cannot be indexed the way Python strings are. The length of the string is not one of the axes of the tensor.

string_tensor_0 = tf.constant('hello')
print(string_tensor_0)

# here strings are byte strings
string_tensor_1 = tf.constant(['hello','world','Thank You','welcome'])
print(string_tensor_1) 

Basic string operations

split strings

s_t = tf.constant('hello how are you?')
splitted = tf.strings.split(s_t,sep=" ")
print(splitted)
print(splitted.numpy())

x = tf.constant([['ab cd'],["efg hijkl hikl"]]) # results in ragged tensor
splitted_x = tf.strings.split(x,sep=" ")
print(splitted_x)

string to number

a = tf.constant("1 100 120 567")
b = tf.strings.to_number(tf.strings.split(a,sep=" "))
print(b)

Although you can't use tf.cast to turn a string tensor into numbers, you can convert it into bytes, and then into numbers.

byte_strings = tf.strings.bytes_split(tf.constant("apple"))
print(byte_strings)

byte_ints = tf.io.decode_raw(tf.constant("apple"), tf.uint8)
print("Bytes:", byte_ints)