Introdução ao TensorFlow
TensorFlow é uma biblioteca de Machine Learning open-source para pré-processamento de dados, modelagem de dados e criação de modelos. Em vez de criar modelos de ML
do zero, podemos utilizar TensorFlow que contém muitas funções de ML
(principalmente as mais utilizadas). Ok, TensorFlow é vasto, mas o foco principal é simples: transformar dados em números que são chamados de (tensors
) e construir algoritmos de ML
para encontrar padrões neles.
Tensors
Os tensors
são como matrizes NumPy. Daqui pra frente pense em um tensor como uma representação numérica multidimensional (n-dimensional, onde n
pode ser qualquer número) de algo. Esse algo pode ser quase qualquer coisa que possamos imaginar:
- números (tensors representando o preço de carros).
- imagens (tensors representando os pixels de uma foto).
- texto (tensors representando palavras).
Ou alguma outra forma de informação (dados) que você deseja representar como números.
A principal diferença entre tensors e matrizes NumPy é que os tensors podem ser utilizados em GPUs
(unidades de processamento gráfico, placas de vídeo) e TPUs
(unidades de processamento de tensor). O benefício disso é poder executar tarefas computacionais mais rápidas, ou seja, para encontrar padrões em representações numéricas nos dados de forma mais rápida.
Vamos começar nossa jornada, a primeira coisa que faremos é importar o TensorFlow, o alias mais comum utilizado é o tf
:
import tensorflow as tf
print(tf.__version__)
2.8.0
Criando Tensors com tf.constant()
No geral, normalmente não criaremos tensors
por conta própria, pois o TensorFlow possui módulos integrados capazes de ler nossas fontes de dados e convertê-las automaticamente em tensors. Apenas para exemplificar nesse momento em que estamos nos familiarizando, criaremos tensors e veremos como manipulá-los. Começando por tf.constant()
:
scalar = tf.constant(5)
scalar
<tf.Tensor: shape=(), dtype=int32, numpy=5>
Um
scalar
é conhecido como um tensor derank
0 por não ter dimensões (é apenas um número). No momento não precisamos saber muito sobre os diferentesranks
de tensors, veremos mais detalhes sobre isso mais na frente. O importante agora é saber que os tensors podem ter um intervalo ilimitado de dimensões (a quantidade exata, vai depender dos dados que vamos representar).
scalar.ndim
0
vetor = tf.constant([10, 10])
vetor
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([10, 10], dtype=int32)>
vetor.ndim
1
matriz = tf.constant([[10, 5],
[5, 10]])
matriz
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[10, 5],
[ 5, 10]], dtype=int32)>
matriz.ndim
2
Por padrão o TensorFlow cria tensors utilizando int32
como tipo de dados ou float32
. Isso também é conhecido como precisão de 32 bits
(quanto maior o número, mais preciso ele é).
tensor = tf.constant([[[1, 2, 3],
[4, 5, 6]],
[[7, 8, 9],
[10, 11, 12]],
[[13, 14, 15],
[16, 17, 18]]])
tensor
<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)>
tensor.ndim
3
Esse é um exemplo de um tensor rank
3 (possui 3 dimensões), como dito antes, um tensor pode ter uma quantidade ilimitada de dimensões. Imagine que você quer transformar uma série de imagens em tensors, no formato (223,223, 3, 32), onde:
223, 223 são as primeiras 2 dimensões, altura e largura das imagens em pixels. 3 é o número de canais de cores da imagem (vermelho, verde e azul) e 32 é o tamanho do lote (número de imagens que uma rede neural vê). Todas as variáveis criadas acima, são na verdade tensors, na literatura podemos encontrar referências com nomes diferentes:
- scalar: um único número
- vetor: um número com direção (ex: velocidade de um carro)
- matriz: uma matriz bidimensional numérica
- tensor: uma matriz n-dimensional numérica (onde
n
pode ser qualquer número, logo um tensor com dimensão0
é um scalar, um tensor com 1 dimensão é um vetor).
MEMO: inserir uma imagem aqui com ex. de álgebra visual comparando as referências para um tensor, algo parecido com: https://www.mathsisfun.com/algebra/scalar-vector-matrix.html
Criando Tensors com tf.Variable()
Outra opção para criar tensors é utilizando tf.Variable()
. A diferença é que tf.constant()
cria tensors imutáveis (não podem ser alterados, só podem ser utilizados para criar um novo tensor) enquanto tf.Variable()
cria tensors mutáveis (que podem ser alterados).
tensor_mutavel = tf.Variable([10, 5])
tensor_imutavel = tf.constant([10, 5])
tensor_mutavel, tensor_imutavel
(<tf.Variable 'Variable:0' shape=(2,) dtype=int32, numpy=array([10, 5], dtype=int32)>,
<tf.Tensor: shape=(2,), dtype=int32, numpy=array([10, 5], dtype=int32)>)
Por exemplo, podemos alterar os valores do tensor mutável por meio da função assign()
:
tensor_mutavel[0].assign(3)
tensor_mutavel
Os valores forma alterados de [10, 5]
para [3, 5]
. Se tentarmos a mesma alteração no tensor imutável, teremos uma mensagem de erro:
tensor_imutavel[0].assign(3)
tensor_imutavel
A escolha de qual utilizar, tf.Variable()
ou tf.constant()
vai depender do tipo de solução que o seu problema exige. Na maioria das vezes, o TensorFlow escolherá automaticamente para você (ao carregar dados).
Criando Tensors aleatórios
Tensors aleatórios possuem algum tamanho arbitrário com números aleatórios. Isso é bastante utilizado em redes neurais para inicializar as configurações (pesos padrões) que estão tentando aprender nos dados.
O processo de aprendizado de uma rede neural geralmente envolve o uso de uma matriz aleatória n-dimensional
e refiná-los até que representem algum tipo de padrão (uma forma compacta de representar os dados originais).
Como uma rede neural aprende ?
No exemplo acima, de forma simplificada a primeira etapa é converter as imagens em números, os números agora são tensors (no TensorFlow), então a rede neural busca por padrões que identifiquem as imagens, para no final classificar se é uma coisa ou outra.Em detalhes o aprendizado começa com padrões aleatórios e depois passa para exemplos demonstrativos de dados, ao mesmo tempo em que tenta atualizar seus padrões aleatórios para representar os exemplos (pizza ou carne).
Podemos criar tensors, aleatórios utilizando a classe tf.random.Generator
:
random_1 = tf.random.Generator.from_seed(42)
random_1 = random_1.normal(shape=(3, 2))
random_1
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[-0.7565803 , -0.06854702],
[ 0.07595026, -1.2573844 ],
[-0.23193763, -1.8107855 ]], dtype=float32)>
O tensor aleatório criado, na verdade é pseudoaleatório. Se criarmos outro tensor aleatório e definirmos o mesmo valor de seed
teremos os mesmos números aleatórios (lembre do NumPy, é bem semelhante np.random.seed(42)
):
random_2 = tf.random.Generator.from_seed(42)
random_2 = random_2.normal(shape=(3, 2))
random_2
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[-0.7565803 , -0.06854702],
[ 0.07595026, -1.2573844 ],
[-0.23193763, -1.8107855 ]], dtype=float32)>
Comparando:
random_1 == random_2
<tf.Tensor: shape=(3, 2), dtype=bool, numpy=
array([[ True, True],
[ True, True],
[ True, True]])>
E se trocarmos o valor do seed
?
random_3 = tf.random.Generator.from_seed(11)
random_3 = random_3.normal(shape=(3, 2))
random_3
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[ 0.27305737, -0.29925638],
[-0.3652325 , 0.61883307],
[-1.0130816 , 0.28291714]], dtype=float32)>
random_1 == random_3
<tf.Tensor: shape=(3, 2), dtype=bool, numpy=
array([[False, False],
[False, False],
[False, False]])>
Ok, e se você quiser embaralhar a ordem de um tensor ? Digamos que você esteja em um projeto com 20.000 imagens de pizzas e carnes e as primeiras 15.000 imagens são de pizzas e as próximas 5.000 são de carnes. Essa distribuição pode afetar a forma como uma rede neural aprende, ou seja, pode ser induzida a aprender pela ordem dos dados, em vez disso, pode ser uma boa estratégia embaralhar os dados.
nao_embaralhado = tf.constant([[10, 7],
[3, 4],
[2, 5]])
tf.random.shuffle(nao_embaralhado)
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[10, 7],
[ 2, 5],
[ 3, 4]], dtype=int32)>
Agora vamos embaralhar:
tf.random.shuffle(nao_embaralhado, seed=42)
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[ 2, 5],
[ 3, 4],
[10, 7]], dtype=int32)>
Outras formas de criar Tensors
Assim como no NumPy, TensorFlow disponibiliza funções para gerar arrays de 1 e 0 (nesse caso tensors). Embora raramente você utilize isso:
tf.ones(shape=(3, 2))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[1., 1.],
[1., 1.],
[1., 1.]], dtype=float32)>
tf.zeros(shape=(3, 2))
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[0., 0.],
[0., 0.],
[0., 0.]], dtype=float32)>
Também podemos transformar matrizes NumPy em tensors, lembre-se que a principal diferença entre matrizes NumPy e tensors é que o segundo pode ser ser executado em GPUs.
import numpy as np
numpy_A = np.arange(1, 25, dtype=np.int32)
A = tf.constant(numpy_A,
shape=[2, 4, 3])
A
<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)>
Listando informações de Tensors
Em algum momento vamos precisar obter informações diferentes sobre os tensors, as principais formas:
- Shape número de elementos de cada dimensão de um tensor.
- Rank número de dimensões de um tensor.
- Axis uma dimensão particular de um tensor.
- Size número total de elementos no tensor.
Vamos criar um novo tensor com zeros e obter todas essas informações:
rank_tensor_4 = tf.zeros([2, 3, 4, 5])
rank_tensor_4
<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)>
Exemplificando as principais funções para coletar informações sobre o tensor que criamos:
print("Tipo de dados de cada elemento:", rank_tensor_4.dtype)
print("Número de dimensões:", rank_tensor_4.ndim)
print("Shape (forma) do tensor:", rank_tensor_4.shape)
print("Elementos no eixo 0 do tensor:", rank_tensor_4.shape[0])
print("Elementos ao longo do último eixo do tensor:", rank_tensor_4.shape[-1])
print("Número total de elementos:", tf.size(rank_tensor_4).numpy())
Tipo de dados de cada elemento: <dtype: 'float32'>
Número de dimensões: 4
Shape (forma) do tensor: (2, 3, 4, 5)
Elementos no eixo 0 do tensor: 2
Elementos ao longo do último eixo do tensor: 5
Número total de elementos: 120
Manipulando Tensors
Para encontrar padrões nos dados (tensors) é necessário manipulá-los. Ao criar modelos no TensorFlow, grande parte da descoberta de padrões é feita automaticamente, veremos apenas algumas das instruções que ocorrem por baixo dos panos.
Operações básicas
Podemos executar várias das operações matemáticas básicas diretamente em um tensor utilizando operadores Python +
, -
, *
:
tensor = tf.constant([[7, 3], [4, 5]])
tensor
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[7, 3],
[4, 5]], dtype=int32)>
tensor + 10
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[17, 13],
[14, 15]], dtype=int32)>
Agora com os outros operadores:
tensor * 10
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[70, 30],
[40, 50]], dtype=int32)>
tensor - 10
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[-3, -7],
[-6, -5]], dtype=int32)>
Outra forma seria utilizando uma função do TensorFlow:
# equivalente ao operador "*"
tf.multiply(tensor, 10)
<tf.Tensor: shape=(2, 2), dtype=int32, numpy=
array([[70, 30],
[40, 50]], dtype=int32)>
WIP
Multiplicação de Matriz
Alterando o tipo de dados de um Tensor
Tensors e NumPy
Acessando GPUs