2.1. Manipulação de Dados¶ Open the notebook in SageMaker Studio Lab
Para fazer qualquer coisa, precisamos de alguma forma de armazenar e manipular dados. Geralmente, há duas coisas importantes que precisamos fazer com os dados: (i) adquirir eles; e (ii) processá-los assim que estiverem dentro do computador. Não há sentido em adquirir dados sem alguma forma de armazená-los, então vamos brincar com dados sintéticos. Para começar, apresentamos o array \(n\)-dimensional, também chamado de tensor.
Se você trabalhou com NumPy, o mais amplamente utilizado pacote de
computação científica em Python, então você achará esta seção familiar.
Não importa qual estrutura você usa, sua classe de tensor (ndarray
em MXNet, Tensor
em PyTorch e TensorFlow) é semelhante
aondarray
do NumPy com alguns recursos interessantes. Primeiro, a
GPU é bem suportada para acelerar a computação enquanto o NumPy suporta
apenas computação de CPU. Em segundo lugar, a classe tensor suporta
diferenciação automática. Essas propriedades tornam a classe tensor
adequada para aprendizado profundo. Ao longo do livro, quando dizemos
tensores, estamos nos referindo a instâncias da classe tensorial, a
menos que seja declarado de outra forma.
2.1.1. Iniciando¶
Nesta seção, nosso objetivo é colocá-lo em funcionamento, equipando você com as ferramentas básicas de matemática e computação numérica que você desenvolverá conforme progride no livro. Não se preocupe se você lutar para grocar alguns dos os conceitos matemáticos ou funções de biblioteca. As seções a seguir revisitarão este material no contexto de exemplos práticos e irá afundar. Por outro lado, se você já tem alguma experiência e quiser se aprofundar no conteúdo matemático, basta pular esta seção.
Para começar, importamos o np
(numpy
) e Módulos npx
(numpy_extension
) da MXNet. Aqui, o módulo np
inclui funções
suportadas por NumPy, enquanto o módulo npx
contém um conjunto de
extensões desenvolvido para capacitar o Deep Learning em um ambiente
semelhante ao NumPy. Ao usar tensores, quase sempre invocamos a função
set_np
: isso é para compatibilidade de processamento de tensor por
outros componentes do MXNet.
from mxnet import np, npx
npx.set_np()
Para começar, importamos torch
. Note que apesar de ser chamado
PyTorch, devemos importar torch
ao invés de pytorch
.
import torch
Importamos tensorflow
. Como o nome é longo, importamos abreviando
tf
.
import tensorflow as tf
Um tensor representa uma matriz (possivelmente multidimensional) de valores numéricos. Com uma dimensão, um tensor corresponde (em matemática) a um vetor. Com duas dimensões, um tensor corresponde a uma * matriz *. Tensores com mais de dois eixos não possuem nomes matemáticos.
Para começar, podemos usar arange
para criar um vetor linha x
contendo os primeiros 12 inteiros começando com 0, embora eles sejam
criados como float por padrão. Cada um dos valores em um tensor é
chamado de elemento do tensor. Por exemplo, existem 12 elementos no
tensor x
. A menos que especificado de outra forma, um novo tensor
será armazenado na memória principal e designado para computação baseada
em CPU.
x = np.arange(12)
x
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.])
x = torch.arange(12)
x
tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
x = tf.range(12)
x
<tf.Tensor: shape=(12,), dtype=int32, numpy=array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], dtype=int32)>
Podemos acessar o formato do tensor (o comprimento em cada coordenada)
inspecionando sua propriedade shape
.
x.shape
(12,)
x.shape
torch.Size([12])
x.shape
TensorShape([12])
Se quisermos apenas saber o número total de elementos em um tensor, ou
seja, o produto de todos os shapes, podemos inspecionar seu tamanho.
Porque estamos lidando com um vetor aqui, o único elemento de seu
shape
é idêntico ao seu tamanho.
x.size
12
x.numel()
12
tf.size(x)
<tf.Tensor: shape=(), dtype=int32, numpy=12>
Para mudar o shape de um tensor sem alterar o número de elementos ou
seus valores, podemos invocar a função reshape
. Por exemplo, podemos
transformar nosso tensor, x
, de um vetor linha com forma (12,) para
uma matriz com forma (3, 4). Este novo tensor contém exatamente os
mesmos valores, mas os vê como uma matriz organizada em 3 linhas e 4
colunas. Para reiterar, embora a forma tenha mudado, os elementos não.
Observe que o tamanho não é alterado pela remodelagem.
X = x.reshape(3, 4)
X
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
X = x.reshape(3, 4)
X
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
X = tf.reshape(x, (3, 4))
X
<tf.Tensor: shape=(3, 4), dtype=int32, numpy=
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]], dtype=int32)>
A remodelação especificando manualmente todas as dimensões é
desnecessária. Se nossa forma de destino for uma matriz com forma
(altura, largura), então, depois de sabermos a largura, a altura é dada
implicitamente. Por que devemos realizar a divisão nós mesmos? No
exemplo acima, para obter uma matriz com 3 linhas, especificamos que
deve ter 3 linhas e 4 colunas. Felizmente, os tensores podem calcular
automaticamente uma dimensão considerando o resto. Invocamos esse
recurso colocando -1
para a dimensão que gostaríamos que os tensores
inferissem automaticamente. No nosso caso, em vez de chamar
x.reshape (3, 4)
, poderíamos ter chamado equivalentemente
x.reshape (-1, 4)
ou x.reshape (3, -1)
.
Normalmente, queremos que nossas matrizes sejam inicializadas seja com zeros, uns, algumas outras constantes, ou números amostrados aleatoriamente de uma distribuição específica. Podemos criar um tensor representando um tensor com todos os elementos definido como 0 e uma forma de (2, 3, 4) como a seguir:
np.zeros((2, 3, 4))
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.]]])
torch.zeros((2, 3, 4))
tensor([[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]],
[[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]]])
tf.zeros((2, 3, 4))
<tf.Tensor: shape=(2, 3, 4), 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.]]], dtype=float32)>
Da mesma forma, podemos criar tensores com cada elemento definido como 1 da seguinte maneira:
np.ones((2, 3, 4))
array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
torch.ones((2, 3, 4))
tensor([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]])
tf.ones((2, 3, 4))
<tf.Tensor: shape=(2, 3, 4), dtype=float32, numpy=
array([[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]],
[[1., 1., 1., 1.],
[1., 1., 1., 1.],
[1., 1., 1., 1.]]], dtype=float32)>
Frequentemente, queremos amostrar aleatoriamente os valores para cada elemento em um tensor de alguma distribuição de probabilidade. Por exemplo, quando construímos matrizes para servir como parâmetros em uma rede neural, vamos normalmente inicializar seus valores aleatoriamente. O fragmento a seguir cria um tensor com forma (3, 4). Cada um de seus elementos é amostrado aleatoriamente de uma distribuição gaussiana (normal) padrão com uma média de 0 e um desvio padrão de 1.
np.random.normal(0, 1, size=(3, 4))
array([[ 2.2122064 , 1.1630787 , 0.7740038 , 0.4838046 ],
[ 1.0434405 , 0.29956347, 1.1839255 , 0.15302546],
[ 1.8917114 , -1.1688148 , -1.2347414 , 1.5580711 ]])
torch.randn(3, 4)
tensor([[-1.0383, 2.7221, 1.6101, 0.3270],
[ 1.2290, 0.3447, -0.8467, -1.8943],
[ 0.7013, -1.5338, -0.2593, -0.6438]])
tf.random.normal(shape=[3, 4])
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 1.6746727 , 0.14110997, 2.2796707 , -0.46627936],
[ 0.5949626 , 0.47052133, -0.511894 , -0.81917727],
[-0.21937501, -0.5358674 , -0.8519913 , -0.6297386 ]],
dtype=float32)>
Podemos também especificar os valores exatos para cada elemento no tensor desejado fornecendo uma lista Python (ou lista de listas) contendo os valores numéricos. Aqui, a lista externa corresponde ao eixo 0 e a lista interna ao eixo 1.
np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
array([[2., 1., 4., 3.],
[1., 2., 3., 4.],
[4., 3., 2., 1.]])
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
tensor([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]])
tf.constant([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
<tf.Tensor: shape=(3, 4), dtype=int32, numpy=
array([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]], dtype=int32)>
2.1.2. Operações¶
Este livro não é sobre engenharia de software. Nossos interesses não se limitam a simplesmente leitura e gravação de dados de/para matrizes. Queremos realizar operações matemáticas nessas matrizes. Algumas das operações mais simples e úteis são as operações elemento a elemento. Estes aplicam uma operação escalar padrão para cada elemento de uma matriz. Para funções que usam dois arrays como entradas, as operações elemento a elemento aplicam algum operador binário padrão em cada par de elementos correspondentes das duas matrizes. Podemos criar uma função elemento a elemento a partir de qualquer função que mapeia de um escalar para um escalar.
Em notação matemática, denotaríamos tal um operador escalar unário (tomando uma entrada) pela assinatura \(f: \mathbb{R} \rightarrow \mathbb{R}\). Isso significa apenas que a função está mapeando de qualquer número real (\(\mathbb{R}\)) para outro. Da mesma forma, denotamos um operador escalar binário (pegando duas entradas reais e produzindo uma saída) pela assinatura \(f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}\). Dados quaisquer dois vetores \(\mathbf{u}\) e \(\mathbf{v}\) de mesmo shape, e um operador binário \(f\), podemos produzir um vetor \(\mathbf{c} = F(\mathbf{u},\mathbf{v})\) definindo \(c_i \gets f(u_i, v_i)\) para todos \(i\), onde \(c_i, u_i\) e \(v_i\) são os elementos \(i^\mathrm{th}\) dos vetores \(\mathbf{c}, \mathbf{u}\), e \(\mathbf{v}\). Aqui, nós produzimos o valor vetorial \(F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d\) transformando a função escalar para uma operação de vetor elemento a elemento.
Os operadores aritméticos padrão comuns (+
,
-
,*
,/
e**
) foram todos transformados em operações
elemento a elemento para quaisquer tensores de formato idêntico de forma
arbitrária. Podemos chamar operações elemento a elemento em quaisquer
dois tensores da mesma forma. No exemplo a seguir, usamos vírgulas para
formular uma tupla de 5 elementos, onde cada elemento é o resultado de
uma operação elemento a elemento.
2.1.2.1. Operações¶
Os operadores aritméticos padrão comuns (+
,
-
,*
,/
e**
) foram todos transformados em operações
elemento a elemento.
x = np.array([1, 2, 4, 8])
y = np.array([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # O ** é o operador exponenciação
(array([ 3., 4., 6., 10.]),
array([-1., 0., 2., 6.]),
array([ 2., 4., 8., 16.]),
array([0.5, 1. , 2. , 4. ]),
array([ 1., 4., 16., 64.]))
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # O ** é o operador exponenciação
(tensor([ 3., 4., 6., 10.]),
tensor([-1., 0., 2., 6.]),
tensor([ 2., 4., 8., 16.]),
tensor([0.5000, 1.0000, 2.0000, 4.0000]),
tensor([ 1., 4., 16., 64.]))
x = tf.constant([1.0, 2, 4, 8])
y = tf.constant([2.0, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y # O ** é o operador exponenciação
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 3., 4., 6., 10.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([-1., 0., 2., 6.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 2., 4., 8., 16.], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([0.5, 1. , 2. , 4. ], dtype=float32)>,
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 1., 4., 16., 64.], dtype=float32)>)
Muitos mais operações podem ser aplicadas elemento a elemento, incluindo operadores unários como exponenciação.
np.exp(x)
array([2.7182817e+00, 7.3890562e+00, 5.4598148e+01, 2.9809580e+03])
torch.exp(x)
tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])
tf.exp(x)
<tf.Tensor: shape=(4,), dtype=float32, numpy=
array([2.7182817e+00, 7.3890562e+00, 5.4598148e+01, 2.9809580e+03],
dtype=float32)>
Além de cálculos elemento a elemento, também podemos realizar operações de álgebra linear, incluindo produtos escalar de vetor e multiplicação de matrizes. Explicaremos as partes cruciais da álgebra linear (sem nenhum conhecimento prévio assumido) em Section 2.3.
Também podemos concatenar vários tensores juntos, empilhando-os ponta a ponta para formar um tensor maior. Só precisamos fornecer uma lista de tensores e informar ao sistema ao longo de qual eixo concatenar. O exemplo abaixo mostra o que acontece quando concatenamos duas matrizes ao longo das linhas (eixo 0, o primeiro elemento da forma) vs. colunas (eixo 1, o segundo elemento da forma). Podemos ver que o comprimento do eixo 0 do primeiro tensor de saída (\(6\)) é a soma dos comprimentos do eixo 0 dos dois tensores de entrada (\(3 + 3\)); enquanto o comprimento do eixo 1 do segundo tensor de saída (\(8\)) é a soma dos comprimentos do eixo 1 dos dois tensores de entrada (\(4 + 4\)).
X = np.arange(12).reshape(3, 4)
Y = np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
np.concatenate([X, Y], axis=0), np.concatenate([X, Y], axis=1)
(array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
array([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]),
tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]]))
X = tf.reshape(tf.range(12, dtype=tf.float32), (3, 4))
Y = tf.constant([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
tf.concat([X, Y], axis=0), tf.concat([X, Y], axis=1)
(<tf.Tensor: shape=(6, 4), dtype=float32, numpy=
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]], dtype=float32)>,
<tf.Tensor: shape=(3, 8), dtype=float32, numpy=
array([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]], dtype=float32)>)
Às vezes, queremos construir um tensor binário por meio de declarações
lógicas. Tome X == Y
como exemplo. Para cada posição, se X
eY
forem iguais nessa posição, a entrada correspondente no novo
tensor assume o valor 1, o que significa que a declaração lógica
X == Y
é verdadeira nessa posição; caso contrário, essa posição
assume 0.
X == Y
array([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
X == Y
tensor([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])
X == Y
<tf.Tensor: shape=(3, 4), dtype=bool, numpy=
array([[False, True, False, True],
[False, False, False, False],
[False, False, False, False]])>
Somando todos os elementos no tensor resulta em um tensor com apenas um elemento.
X.sum()
array(66.)
X.sum()
tensor(66.)
tf.reduce_sum(X)
<tf.Tensor: shape=(), dtype=float32, numpy=66.0>
2.1.3. Mecanismo de Broadcasting¶
Na seção acima, vimos como realizar operações elemento a elemento em dois tensores da mesma forma. Sob certas condições, mesmo quando as formas são diferentes, ainda podemos realizar operações elementar invocando o mecanismo de Broadcasting. Esse mecanismo funciona da seguinte maneira: Primeiro, expanda um ou ambos os arrays copiando elementos de forma adequada de modo que após esta transformação, os dois tensores têm a mesma forma. Em segundo lugar, execute as operações elemento a elemento nas matrizes resultantes.
Na maioria dos casos, nós transmitimos ao longo de um eixo onde uma matriz inicialmente tem apenas o comprimento 1, como no exemplo a seguir:
a = np.arange(3).reshape(3, 1)
b = np.arange(2).reshape(1, 2)
a, b
(array([[0.],
[1.],
[2.]]),
array([[0., 1.]]))
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
(tensor([[0],
[1],
[2]]),
tensor([[0, 1]]))
a = tf.reshape(tf.range(3), (3, 1))
b = tf.reshape(tf.range(2), (1, 2))
a, b
(<tf.Tensor: shape=(3, 1), dtype=int32, numpy=
array([[0],
[1],
[2]], dtype=int32)>,
<tf.Tensor: shape=(1, 2), dtype=int32, numpy=array([[0, 1]], dtype=int32)>)
Uma vez que a
eb
são matrizes \(3\times1\) e
\(1\times2\) respectivamente, suas formas não correspondem se
quisermos adicioná-los. Nós transmitimos as entradas de ambas as
matrizes em uma matriz \(3\times2\) maior da seguinte maneira: para
a matriz a
ele replica as colunas e para a matriz b
ele replica
as linhas antes de adicionar ambos os elementos.
a + b
array([[0., 1.],
[1., 2.],
[2., 3.]])
a + b
tensor([[0, 1],
[1, 2],
[2, 3]])
a + b
<tf.Tensor: shape=(3, 2), dtype=int32, numpy=
array([[0, 1],
[1, 2],
[2, 3]], dtype=int32)>
2.1.4. Indexação e Fatiamento¶
Assim como em qualquer outro array Python, os elementos em um tensor podem ser acessados por índice. Como em qualquer matriz Python, o primeiro elemento tem índice 0 e os intervalos são especificados para incluir o primeiro, mas antes do último elemento. Como nas listas padrão do Python, podemos acessar os elementos de acordo com sua posição relativa ao final da lista usando índices negativos.
Assim, [-1]
seleciona o último elemento e [1: 3]
seleciona o
segundo e o terceiro elementos da seguinte forma:
X[-1], X[1:3]
(array([ 8., 9., 10., 11.]),
array([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
Além da leitura, também podemos escrever elementos de uma matriz especificando índices.
X[1, 2] = 9
X
array([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]])
X[-1], X[1:3]
(tensor([ 8., 9., 10., 11.]),
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
Além da leitura, também podemos escrever elementos de uma matriz especificando índices.
X[1, 2] = 9
X
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]])
X[-1], X[1:3]
(<tf.Tensor: shape=(4,), dtype=float32, numpy=array([ 8., 9., 10., 11.], dtype=float32)>,
<tf.Tensor: shape=(2, 4), dtype=float32, numpy=
array([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]], dtype=float32)>)
Tensors
in TensorFlow are immutable, and cannot be assigned to.
Variables
in TensorFlow are mutable containers of state that support
assignments. Keep in mind that gradients in TensorFlow do not flow
backwards through Variable
assignments. Os Tensors
no TensorFlow
são imutáveis e não podem ser atribuídos a eles. Variables
no
TensorFlow são contêineres mutáveis de estado que suportam atribuições.
Lembre-se de que gradientes no TensorFlow não fluem para trás por meio
de atribuições Variable
.
Beyond assigning a value to the entire Variable
, we can write
elements of a Variable
by specifying indices. Além de atribuir um
valor a toda a Variable
, podemos escrever elementos de um
Variable
especificando índices.
X_var = tf.Variable(X)
X_var[1, 2].assign(9)
X_var
<tf.Variable 'Variable:0' shape=(3, 4) dtype=float32, numpy=
array([[ 0., 1., 2., 3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]], dtype=float32)>
Se quisermos para atribuir a vários elementos o mesmo valor,
simplesmente indexamos todos eles e, em seguida, atribuímos o valor a
eles. Por exemplo, [0: 2,:]
acessa a primeira e a segunda linhas,
onde :
leva todos os elementos ao longo do eixo 1 (coluna). Enquanto
discutimos a indexação de matrizes, isso obviamente também funciona para
vetores e para tensores de mais de 2 dimensões.
X[0:2, :] = 12
X
array([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
X[0:2, :] = 12
X
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])
X_var = tf.Variable(X)
X_var[0:2, :].assign(tf.ones(X_var[0:2,:].shape, dtype = tf.float32) * 12)
X_var
<tf.Variable 'Variable:0' shape=(3, 4) dtype=float32, numpy=
array([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]], dtype=float32)>
2.1.5. Economizando memória¶
As operações em execução podem fazer com que uma nova memória seja
alocado aos resultados do host. Por exemplo, se escrevermos
Y = X + Y
, vamos desreferenciar o tensor que Y
costumava apontar
para e, em vez disso, aponte Y
para a memória recém-alocada. No
exemplo a seguir, demonstramos isso com a função id ()
do Python,
que nos dá o endereço exato do objeto referenciado na memória. Depois de
executar Y = Y + X
, descobriremos queid (Y)
aponta para um
local diferente. Isso ocorre porque o Python primeiro avalia Y + X
,
alocar nova memória para o resultado e, em seguida, torna Y
aponte
para este novo local na memória.
before = id(Y)
Y = Y + X
id(Y) == before
False
before = id(Y)
Y = Y + X
id(Y) == before
False
before = id(Y)
Y = Y + X
id(Y) == before
False
Isso pode ser indesejável por dois motivos. Em primeiro lugar, não queremos alocar memória desnecessariamente o tempo todo. No aprendizado de máquina, podemos ter centenas de megabytes de parâmetros e atualizar todos eles várias vezes por segundo. Normalmente, queremos realizar essas atualizações no local. Em segundo lugar, podemos apontar os mesmos parâmetros de várias variáveis. Se não atualizarmos no local, outras referências ainda apontarão para a localização da memória antiga, tornando possível para partes do nosso código para referenciar inadvertidamente parâmetros obsoletos.
Felizmente, executar operações no local é fácil. Podemos atribuir o
resultado de uma operação para uma matriz previamente alocada com
notação de fatia, por exemplo, Y [:] = <expressão>
. Para ilustrar
este conceito, primeiro criamos uma nova matriz Z
com a mesma forma
de outro Y
, usando zeros_like
para alocar um bloco de \(0\)
entradas. : end_tab:
Z = np.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 140128414209472
id(Z): 140128414209472
Se o valor de X
não for reutilizado em cálculos subsequentes, também
podemos usar X [:] = X + Y
ouX + = Y
para reduzir a sobrecarga
de memória da operação.
before = id(X)
X += Y
id(X) == before
True
Felizmente, executar operações no local é fácil. Podemos atribuir o
resultado de uma operação para uma matriz previamente alocada com
notação de fatia, por exemplo, Y [:] = <expressão>
. Para ilustrar
este conceito, primeiro criamos uma nova matriz Z
com a mesma forma
de outro Y
, usando zeros_like
para alocar um bloco de \(0\)
entradas. : end_tab:
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 140194801216832
id(Z): 140194801216832
Se o valor de X
não for reutilizado em cálculos subsequentes, também
podemos usar X [:] = X + Y
ouX + = Y
para reduzir a sobrecarga
de memória da operação.
before = id(X)
X += Y
id(X) == before
True
Variables
são contêineres mutáveis de estado no TensorFlow. Eles
providenciam uma maneira de armazenar os parâmetros do seu modelo.
Podemos atribuir o resultado de uma operação para uma Variable
comassign
. Para ilustrar este conceito, criamos uma
Variable`
Z` com a mesma forma de outro tensor Y
, usando
zeros_like
para alocar um bloco de \(0\) entradas.
Z = tf.Variable(tf.zeros_like(Y))
print('id(Z):', id(Z))
Z.assign(X + Y)
print('id(Z):', id(Z))
id(Z): 140017345889040
id(Z): 140017345889040
Mesmo depois de armazenar o estado persistentemente em uma Variável
,
você pode querer reduzir ainda mais o uso de memória, evitando o excesso
de alocações para tensores que não são os parâmetros do seu modelo.
Porque os Tensors
do TensorFlow são imutáveis e gradientes não
fluem através de atribuições de Variable
, o TensorFlow não fornece
uma maneira explícita de executar uma operação individual no local.
No entanto, o TensorFlow fornece o decorador tf.function
para
encerrar a computação dentro de um gráfico do TensorFlow que é compilado
e otimizado antes da execução. Isso permite que o TensorFlow remova
valores não utilizados e reutilize alocações anteriores que não são mais
necessárias. Isso minimiza a sobrecarga de memória de cálculos do
TensorFlow.
@tf.function
def computation(X, Y):
Z = tf.zeros_like(Y) # Este valor não utilizado será esvaziado
A = X + Y # Alocações serão reutilizadas quando não mais necessárias
B = A + Y
C = B + Y
return C + Y
computation(X, Y)
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 8., 9., 26., 27.],
[24., 33., 42., 51.],
[56., 57., 58., 59.]], dtype=float32)>
2.1.6. Conversão para outros objetos Python¶
Converter para um tensor NumPy, ou vice-versa, é fácil. O resultado convertido não compartilha memória. Este pequeno inconveniente é muito importante: quando você executa operações na CPU ou GPUs, você não quer interromper a computação, esperando para ver se o pacote NumPy do Python deseja fazer outra coisa com o mesmo pedaço de memória.
A = X.asnumpy()
B = np.array(A)
type(A), type(B)
(numpy.ndarray, mxnet.numpy.ndarray)
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
(numpy.ndarray, torch.Tensor)
A = X.numpy()
B = tf.constant(A)
type(A), type(B)
(numpy.ndarray, tensorflow.python.framework.ops.EagerTensor)
Para converter um tensor de tamanho 1 em um escalar Python, podemos
invocar a função item
ou as funções integradas do Python.
a = np.array([3.5])
a, a.item(), float(a), int(a)
(array([3.5]), 3.5, 3.5, 3)
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
(tensor([3.5000]), 3.5, 3.5, 3)
a = tf.constant([3.5]).numpy()
a, a.item(), float(a), int(a)
(array([3.5], dtype=float32), 3.5, 3.5, 3)
2.1.7. Sumário¶
A principal interface para armazenar e manipular dados para Deep Learning é o tensor (array \(n\) -dimensional). Ele fornece uma variedade de funcionalidades, incluindo operações matemáticas básicas, transmissão, indexação, divisão, economia de memória e conversão para outros objetos Python.
2.1.8. Exercícios¶
Execute o código nesta seção. Altere a declaração condicional
X == Y
nesta seção paraX < Y
ouX > Y
, e então veja que tipo de tensor você pode obter.Substitua os dois tensores que operam por elemento no mecanismo de transmissão por outras formas, por exemplo, tensores tridimensionais. O resultado é o mesmo que o esperado?